En este tutorial, implementamos Structured Data Models, la nueva biblioteca de NVIDIA que coloca varios modelos básicos de tablas previamente entrenados detrás de una interfaz de PyTorch y los usamos para hacer predicciones sin necesidad de entrenamiento. La biblioteca incluye TabicLv2 y KumoTabular para tablas individuales y KumoreLational para bases de datos relacionales completas, y cada una de ellas responde mediante el aprendizaje contextual: entregamos al modelo un conjunto de filas etiquetadas y las filas que queremos predecir, y devuelve probabilidades o una distribución predictiva completa en una sola pasada. Comenzamos con los tensores de tablas mecanografiados de la biblioteca, comparamos los dos modelos tabulares con líneas base entrenadas de scikit-learn, reutilizamos un contexto codificado en muchas consultas, leemos los intervalos de predicción calibrados de una regresión y escalamos a veinte clases. Luego analizamos los casos en los que los valores predeterminados actúan mal: una receta de preprocesamiento que elimina las marcas de tiempo y las columnas de texto y un explicativo con dos trampas, antes de crear una pequeña base de datos de tiendas de tres tablas y predecir directamente la actividad de los clientes a partir de ella, incluido el único argumento que separa un modelo relacional honesto de uno con fugas. Consulta los códigos completos aquí

.
sistema operativo de importación
importar sys
tiempo de importación
importar envoltorio de texto
importar el rastreo
subproceso de importación
 
RESULTADOS = {}
 
 
def banner (título):
 imprimir (»\n"+ «=» * 78)
 imprimir (título)
 imprimir («=» * 78)
 
 
sección def (nombre):
 def wrap (fn):
 def run (*a, **kw):
 banner (nombre)
 prueba:
 salida = fn (*a, **kw)
 RESULTADOS [nombre] = fuera si isinstance (out, str) sino «ok»
 volver a salir
 excepto la excepción e:
 RESULTADOS [nombre] = f"Omitido/FALLIDO -> {type (e). __nombre__}: {e}»
 print (f)»\n[!] {name} no se completó: {type (e). __name__}: {e}»)
 traceback.print_exc (límite = 3)
 devuelve Ninguno
 volver a correr
 envoltura de devolución
 
 
pancarta («1). Instala SDM y la URL de una rueda (su propio mensaje de error es incorrecto»)
subprocess.run ([sys.executable, «-m», «pip», «install», «-q», «git+ https://github.com/NVIDIA/structured-data-models.git@b982fbf4188ea8db17249fe81338723106100b63 «], check=true)
 
linterna de importación
 
# El muestreador relacional necesita pyg-lib en la CPU. Sus ruedas se publican según la versión de MINOR Torch
# (torch-2.14.0+cpu.html), pero el propio ImportError de SDM crea la URL a partir de la versión exacta del parche
# (torch-2.14.1+cpu.html), que es una página vacía. En su lugar, créela desde MAJOR.MINOR.0.
mayor, menor = antorcha. __version__.split («+») [0] .split («.») [:2]
flavor = «cpu» si torch.version.cuda es None else «cu» + torch.version.cuda.replace (» . «, «»)
PYG_INDEX = f» https://data.pyg.org/whl/torch-{major}. {minor} .0+ {sabor} .html»
subprocess.run ([sys.executable, «-m», «pip», «install», «-q», «pyg-lib», «-f», PYG_INDEX], check=true)
 
importar numpy como np
importar pandas como pdf
importar pyg_lib
importar sdm
importar sdm.processing como sp
desde sklearn.datasets importe load_breast_cancer, load_diabetes, make_classification
desde sklearn.ensemble importe HistGradientBoostingClassifier, HistGradientBoostingRegressor
desde sklearn.linear_model importe LogisticRegression, RidgeCV
desde sklearn.metrics importe roc_auc_score
desde sklearn.pipeline import make_pipeline
desde sklearn.preprocessing import StandardScaler
 
DISPOSITIVO = «cpu» # verificado de extremo a extremo en la CPU; en un entorno de ejecución T4, defina «cuda» para los modelos tabulares
print (f)» sdm {sdm. __version__} | antorcha {antorcha. __version__} | pyg-lib {pyg_lib. __versión__}»
 f» | dispositivo {DISPOSITIVO}»)
print (f» ruedas pyg-lib de {PYG_INDEX}»)
print (»\nUna interfaz sobre varios modelos básicos de datos estructurados previamente entrenados:»)
para nombre, qué, pesos en [
 («TabicLv2", «aprendizaje tabular en contexto (ICML '26)», «~110 MB por cabeza de tarea, BSD-3"),
 («KumoTabular», «tabular, pequeño/mediano/grande», «pequeño ~110 MB, grande ~855 MB, openMDW»),
 («KumoreLational», «mesa múltiple, KumorFM-2", «~120 MB, openMDW»),
 («TabFM, TimesFM 3", «Google tabular o series temporales», «6,5 GB/1,3 GB, no comercial: no se utiliza aquí»),
]:
 print (f» {name:18s} {what:42s} {pesos}»)
print (»\nNinguno de los modelos de los siguientes está entrenado o ajustado. Cada predicción es un pase hacia adelante que dice «)
print («filas de «contexto» etiquetadas como filas de «contexto» y respuestas como filas de «consulta», como un mensaje con ejemplos»
)

Instalamos los modelos de datos estructurados desde el repositorio de GitHub, anclados a la confirmación con la que se verificó este cuaderno, ya que el paquete de PyPI es solo un marcador de posición. El muestreador relacional también necesita pyg-lib, y ahí es donde reside la primera trampa. Las ruedas pyg-lib se publican según una versión menor de PyTorch, en páginas con nombres como torch-2.14.0+cpu, pero la propia biblioteca ImportError crea la URL a partir de la versión exacta del parche, torch-2.14.1+cpu, que es una página vacía, por lo que no se puede seguir sus consejos. En su lugar, creamos la URL del índice a partir de la versión principal y secundaria. Lo ejecutamos todo en la CPU, que es la ruta verificada de principio a fin, e imprimimos los modelos que la biblioteca expone junto con sus tamaños de punto de control y licencias; los dos modelos de Google que también incluye, TabFM y TimesFM 3, son grandes y no tienen fines comerciales y se omiten.

Consulta los CÓDIGOS COMPLETOS aquí.
@section («2. Tablas mecanografiadas: qué decide infer_stypes y qué cambia en un viaje de ida y vuelta»)
def typed_tables ():
 rng = np.random.default_rng (0)
 words = ["atrasado», «reembolso», «excelente», «roto», «rápido», «cobrado», «aplicación», «pedido», «soporte», «artículo"]
 df = pd.DataFrame ({
 «ticket_id»: np.arange (400),
 «amount»: rng.gamma (2, 30, 400) .round (2),
 «unidades»: pd.array (rng.integers (1, 5, 400), dtype="int64"),
 «channel»: rng.choice (["web», «app», «phone», Ninguno], 400),
 «opened_at»: pd.Timestamp («2026-01-01") + pd.to_timedelta (rng.integers (0, 86_400 * 90, 400), unit="s»),
 «comment»: [» «.join (rng.choice (words, rng.integers (3, 9))) + f» # {i}» para i en el rango (400)],
 })
 df.loc [rng.choice (400, 25, replace=false), «unidades"] = pd.na
 
 print (» infer_stypes (df) con los valores predeterminados y, a continuación, pidiéndole que busque el texto y los identificadores:»)
 para kw in ({}, {"text»: «infer», «id»: «infer"}):
 styles = sdm.infer_styles (f, **kw)
 print (f» {str (kw o 'defaults') :34s} {', '.join (f' {c} = {s}' para c, s en stypes.items ())}»)
 print (» Con los valores predeterminados, el comentario de texto libre se convierte en una categoría de 400 niveles y la columna de identificación en una»)
 print (» número). Ambas llegarían al modelo como características. Anule o pregunte: text='infer', id='infer'.»)
 
 table = sdm.TableTensor.from_pandas (df, sdm.infer_stypes (df, text="infer», id="infer»))
 print (»\n"+ textwrap.indent (repr (tabla)," «))
 
 back = table.to_pandas ()
 print (»\nto_pandas () ida y vuelta:»)
 para col en df.columns:
 a, b = df [col], atrás [col]
 si col in («cantidad», «unidades»):
 equal_values = bool (np.allclose (a.astype (float), b.astype (float), Equal_nan=true))
 otra cosa:
 na_a, na_b = a.isna () .to_numpy (), b.isna () .to_numpy ()
 valores iguales = bool (na_a = na_b) .all () y
 (a [~na_a] .astype (str) .to_numpy () == b [~na_b] .astype (str) .to_numpy ()) .all ())
 print (f» {col:10s} {str (a.dtype) :16s} -> {str (b.dtype) :16s} valores conservados: {equal_values}»)
 print (» Los valores sobreviven, incluso los que faltan; los dtypes no siempre lo hacen. Los números vuelven a flotar (32")
 print (» (el bloque numérico es un tensor flotante), Int64, que admite valores nulos, pasa a ser flotante con NaN y strings»)
 print («marcado como categórico vuelve como una categoría de pandas»)
 
 base = pd.to_datetime (["2026-01-01 08:00:00 «] * 3) .as_unit («ns»)
 entero = pd.DataFrame ({"t»: base})
 nanos = pd.DataFrame ({"t»: base + pd.to_timedelta ([0, 0, 1], unit="ns»)})
 print (f»\ndatetime64 [ns], segundos enteros: {sdm.tableTensor.from_pandas (entero, {'t': 'datetime'}) .size ()} OK»)
 prueba:
 sdm.tableTensor.from_pandas (nanos, {"t»: «fecha y hora"})
 excepto una excepción como e:
 print (f») en la misma columna, un valor 1 ns después: {type (e). __name__}: {str (e) .split (':') [0]}»)
 ok = sdm.tableTensor.from_pandas (nanos.assign (t=nanos.t.dt.floor («nosotros»)), {"t»: «fecha y hora"})
 print (f» floor ('us') first: {ok.size ()} OK»)
 print (» SDM almacena las fechas y horas como int64 microsegundos y Arrow rechaza cualquier lanzamiento que arroje un dígito. El»)
 print (» la unidad no es el problema; un solo valor con dígitos de nanosegundos no supera a toda la columna»).
 devuelve «6 columnas -> 5 bloques mecanografiados; es necesario seleccionar el texto y el identificador»
 
 
typed_tables
()

Todo en la biblioteca fluye a través de TableTensor, una subclase de tensores de PyTorch que mantiene un bloque escrito por tipo semántico: numérico, categórico, de fecha y hora, texto e id. infer_stypes decide los tipos y, con sus valores predeterminados, se equivoca en dos cosas en un marco realista de tickets de soporte: el comentario de texto libre se convierte en un categórico con cuatrocientos niveles y el identificador del ticket se convierte en un número, por lo que ambos llegarían a un modelo características. Pasar marcas de inferencia de texto e identificación soluciona este problema. El viaje de ida y vuelta a los pandas conserva todos los valores, incluidos los que faltan, pero no todos los tipos d: los números vuelven como float32 porque el bloque numérico es un solo tensor flotante, los enteros que admiten valores nulos se convierten en flotantes con NaN y las cadenas categóricas regresan como una categoría de pandas. En Pandas 2.x, las columnas de cadenas se imprimen como objeto en lugar de como str y la marca de tiempo aparece con una resolución de nanosegundos; los mismos valores se muestran de cualquier manera. Vale la pena conocer una trampa más antes de cargar un registro de eventos real: la biblioteca almacena las fechas y horas como microsegundos enteros, y Arrow rechaza cualquier lanzamiento que arroje un dígito. Una columna de nanosegundos con segundos enteros se convierte correctamente, pero un único valor con dígitos de nanosegundos no sirve para toda la columna, por lo que primero colocamos las marcas de tiempo en microsegundos. Consulta los CÓDIGOS completos aquí.

def as_table (df, objetivo):
 return sdm.tableTensor.from_pandas (df, sdm.infer_stypes (df, overrides= {target: «categórico"}), device=device)
 
 
@section («3. Clasificación sin entrenamiento: dos modelos básicos frente a dos líneas de base entrenadas»)
def zero_training ():
 df = load_breast_cancer (as_frame=true) .frame.sample (frac=1, random_state=0) .reset_index (drop=true)
 table = as_table (df, «objetivo»)
 n_ctx = 300
 x_ctx, y_ctx = tabla [:n_ctx] .drop_columns («objetivo»), tabla [:n_ctx, «objetivo"]
 x_q, y_q = tabla [n_ctx:] .drop_columns («objetivo»), df.target.to_numpy () [n_ctx:]
 filas = []
 para el nombre, haga en [(«tabicLV2", lambda: SDM.models.tabicLV2 (device=device)),
 («kumoTabular (pequeño)», lambda: SDM.models.kumotabular (size="small», device=device)):
 modelo = make ()
 torch.manual_seed (0)
 proba = modelo (x_context=x_ctx, y_context=y_ctx, x_query=x_q, num_estimators=4) .to_pandas ()
 rows.append (nombre, «ninguno», proba.idxmax (axis=1) .astype (int) .to_numpy (), proba ["1"] .to_numpy ()))
 X, y = df.drop (columns="objetivo»), df.target
 para el nombre, establézcalo en [(«LogisticRegression», make_pipeline (StandardScaler (), LogisticRegression (max_iter=2000))),
 («HistGradientBoosting», HistGradientBoostingClassifier (random_state=0))]:
 set.fit (X [:n_ctx], y [:n_ctx])
 rows.append ((name, «cabe en 300 filas», est.predict (X [n_ctx:]), est.predict_proba (X [n_ctx:]) [:, 1])
 print (f» cáncer de mama: {n_ctx} filas etiquetadas, {len (y_q)} para predecir, 30 características numéricas\n«)
 print (f» {'model':22s} {'entrenamiento':16s} {'precisión': >9s} {'AUC': >7s}»)
 para el nombre, la formación, la educación y la puntuación en filas:
 print (f» {name:22s} {training:16s} {np.mean (pred = y_q) :9.3f} {roc_auc_score (y_q, score) :7.3f}»)
 print (»\nLa salida es en sí misma un TableTensor: una columna de probabilidad por clase, denominada por etiqueta.»)
 best = max (rows [:2], key=lambda r: np.mean (r [2] == y_q))
 return f "{best [0]} {np.mean (best [2] == y_q) :.3f} precisión sin entrenamiento»
 
 
zero_training ()

Los modelos tabulares toman filas de contexto etiquetadas y filas de consulta sin etiquetar y devuelven un TableTensor con una columna de probabilidad por clase, nombrada por etiqueta, por lo que no hay ningún paso de ajuste ni hiperparámetros. En el conjunto de datos sobre el cáncer de mama, con trescientas filas etiquetadas y doscientas sesenta y nueve para predecir, TabicLv2 y KumoTabular alcanzan una precisión de 0,970 y un AUC de 0,996 y 0,995 sin entrenamiento, frente a 0,970 y 0,993 para una regresión logística y 0,967 y 0,992 para aumentar el gradiente, ambos ajustados en las mismas trescientas filas. num_estimators establece el número de vistas preprocesadas de manera diferente de los datos que promedia el modelo terminado, que es el conjunto integrado en la biblioteca.

@section («4. fit () una vez, predict () many: el contexto en caché»)
def kv_cache ():
 df = load_breast_cancer (as_frame=true) .frame.sample (frac=1, random_state=0) .reset_index (drop=true)
 table = as_table (df, «objetivo»)
 x_ctx, y_ctx = table [:400] .drop_columns («objetivo»), table [:400, «objetivo"]
 batches = [table [400 + 30 * i: 430 + 30 * i] .drop_columns («target») para i en el rango (5)]
 modelo = sdm.models.tabicLV2 (dispositivo=dispositivo)
 
 torch.manual_seed (0)
 t0 = time.time ()
 para b en lotes:
 modelo (x_context=x_ctx, y_context=y_ctx, x_query=b, num_estimators=4)
 t_forward = time.time () - t0
 
 torch.manual_seed (0)
 t0 = time.time ()
 model.fit (x=x_ctx, y=y_ctx, num_estimators=4)
 t_fit = time.time () - t0
 t0 = time.time ()
 para b en lotes:
 modelo.predict (b)
 t_predict = time.time () - t0
 modelo.clear ()
 print (» cinco lotes de 30 filas de consulta en las mismas 400 filas de contexto:»)
 print (f» forward () x5 {t_forward:6.2f} s (el contexto recodificó cada llamada)»)
 print (f» fit () once + predict () x5 {t_fit + t_predict:6.2f} s (fit {t_fit: .2f} s, predice {t_predict: .2f} s)»)
 print (f» por lote: {t_forward/5: .2f} s vs {t_predict/5: .2f} s»)
 print (»\nfit () codifica el contexto una vez en una caché de claves/valores; predict () solo ejecuta las consultas.»)
 print (» clear () lo libera. No se aprende nada: fit () aquí significa «recuerda estos ejemplos»).
 return f "{t_forward/(t_fit + t_predict) :.1f} x más rápido para cinco lotes»
 
 
kv_cache ()

Al llamar al modelo, se vuelve a codificar directamente el contexto en cada llamada. Cuando las mismas filas etiquetadas respondan a muchos lotes de consultas, fit codifica el contexto una vez en una caché de claves y valores y predict, luego ejecuta solo las filas de la consulta; clear libera la caché. No se aprende nada cuando se está en forma, lo que significa recordar estos ejemplos. Responder cinco lotes de treinta filas contra las mismas cuatrocientas filas de contexto terminaba varias veces más rápido de esta forma en nuestra CPU y, una vez que el contexto se almacenaba en caché, cada lote adicional costaba una pequeña fracción de lo que costaba una pasada completa. La proporción exacta depende de la máquina, por lo que la celda la mide en lugar de asumirla

.
@section (5). La regresión arroja una distribución: 999 cuantiles y qué tan honestos son»)
def quantile_regression ():
 df = load_diabetes (as_frame=true) .frame.sample (frac=1, random_state=0) .reset_index (drop=true)
 table = sdm.tableTensor.from_pandas (df, sdm.infer_stypes (df), device=dispositivo)
 modelo = sdm.models.tabicLv2 (dispositivo=dispositivo)
 torch.manual_seed (0)
 q = model (x_context=table [:300] .drop_columns («objetivo»), y_context=table [:300, «objetivo"],
 x_query=table [300:] .drop_columns («objetivo»), num_estimators=4) .to_pandas ()
 y = df.target.to_numpy () [300:]
 print (f» output: {q.shape [1]} columns {list (q.columns [:3])}... {list (q.columns [-2:])} por fila de consulta»)
 
 X, Y = df.drop (columns="objetivo»), df.target
 rmse = lambda p: float (np.sqrt (np.mean ((np.asarray (p) - y) ** 2)))
 print (f»\n{'predicción de puntos':34s} {'RMSE': >7s}»)
 print (f» {'mediana tabicLv2 (q500), sin entrenamiento': 34s} {rmse (q ['q500']) :7.2f}»)
 print (f» {'RidgeCV, cabe en 300 filas':34s} {rmse (RidgeCV () .fit (X [:300], Y [:300]) .predict (X [300:])) :7.2f}»)
 print (f» {'HistGradientBoosting, se ajusta a 300':34s}»
 f "{rmse (HistGradientBoostingRegressor (random_state=0) .fit (X [:300], Y [:300]) .predict (X [300:]) :7.2f}»)
 
 print (f»\n{'interval ':12s} {'nominal': >8s} {'cobertura empírica': >19s} {'anchura media': >11s}»)
 cobertura = {}
 para lo, hola en [(«q250", «q750"), («q100", «q900"), («q050", «q950")]:
 nominal = (int (hi [1:]) - int (lo [1:]))/1000
 cobertura [nominal] = float (np.mean ((y >= q [lo]) & (y <= q [hi])))
 print (f» {lo} - {hi:6s} {nominal: 8,0%} {cobertura [nominal] :19,3f} {np.mean (q [hi] - q [lo]) :11,1f}»)
 print (»\nLos cuantiles provienen de una pasada de más de 300 ejemplos, pero los intervalos del 80% y el 90%»)
 print (» cubre cerca del 80% y el 90% de los objetivos pendientes. La franja del 50% es un poco estrecha.»)
 return f"median RMSE {rmse (q ['q500']) :.1f}; el intervalo del 80% cubre {cobertura [0.8]: .1%}»
 
 
regresión
cuantitativa () En

el caso de la regresión, el modelo no devuelve un solo número. Devuelve 999 cuantiles por fila de consulta, una distribución predictiva completa que se lee en una pasada. En el conjunto de datos sobre diabetes con trescientas filas etiquetadas, la mediana de esa distribución tiene un RMSE de 53,6, ligeramente mejor que RidgeCV, con 54,3, y claramente mejor que el aumento de gradiente, con 64,7, ambos entrenados en las mismas filas. La propiedad más útil es la calibración: el intervalo entre los percentiles 10 y 90 contiene el 81,0 por ciento de los objetivos retenidos y el intervalo entre los percentiles 5 y 95 el 91,5 por ciento, cerca de sus valores nominales del 80 y el 90 por ciento, mientras que la banda del 50 por ciento es algo estrecha, con un 43,7 por ciento.

@section (6). Veinte clases con un pase adelante»).
def many_classes ():
 X, y = make_classification (n_samples=800, n_features=12, n_informative=8, n_redundant=2, n_classes=20,
 n_clusters_per_clase=1, class_sep=1.5, random_state=0)
 df = pd.DataFrame (X, columns= [f"f {i}» para i dentro del rango (12)])
 df ["label"] = [f"c {v:02d}» para v en y]
 table = as_table (df, «etiqueta»)
 modelo = SDM.models.tabicLv2 (dispositivo=dispositivo)
 torch.manual_seed (0)
 t0 = time.time ()
 proba = model (x_context=table [:600] .drop_columns («label»), y_context=table [:600, «label"],
 x_query=table [600:] .drop_columns («etiqueta»), num_estimators=1) .to_pandas ()
 segundos = time.time () - t0
 acc = float (np.mean (proba.idxmax (axis=1) .to_numpy () == df.label.to_numpy () [600:]))
 hgb = HistGradientBoostingClassifier (random_state=0) .fit (X [:600], y [:600])
 acc_gb = float (np.mean (hgb.predict (X [600:]) == y [600:]))
 print (f» 20 clases balanceadas, 600 filas etiquetadas (30 por clase), 200 consultas»)
 print (f» TabicLv2, sin entrenamiento {proba.shape [1]} columnas de probabilidad, precisión {acc: .3f} ({secs: .1f} s)»)
 print (f» HistGradientBoosting, precisión entrenada {acc_hgb: .3f}»)
 print (»\nEl cabezal previamente entrenado tiene un número fijo de espacios de clase. Más allá de eso, SDM divide el problema»).
 print (» en códigos de salida que corrijan errores (sdm.models.ecoc) y recombina las probabilidades logarítmicas,»)
 print (» para que la persona que llama siga recibiendo una columna por etiqueta sin hacer nada»).
 devuelve f"20 classes: {acc: .3f} frente a {acc_hgb: .3f} para aumentar el gradiente»
 
 
many_classes
()

El jefe de clasificación previamente entrenado tiene un número fijo de ranuras de clase, y la biblioteca gestiona los problemas con más clases que esa cantidad corrigiendo los códigos de salida, dividiendo el problema en subproblemas codificados más pequeños y recombinando las probabilidades logarítmicas. La persona que llama nunca ve esto: en un problema sintético equilibrado de veinte clases con treinta filas etiquetadas por clase, TabicLv2 devuelve veinte columnas de probabilidad y alcanza una precisión de 0,760 en una sola pasada hacia adelante, frente a 0,615 para aumentar el gradiente entrenado en las mismas filas.

def make_tickets (n=1600, seed=1):
 «" "Tickets de soporte cuya escalabilidad depende de CUÁNDO se abrieron y QUÉ digan. ""»
 rng = np.random.default_rng (velocidad)
 opened = pd.Timestamp («2026-03-02") + pd.to_timedelta (rng.integers (0, 86_400 * 56, n), unit="s»)
 neutral = ["pedido», «artículo», «entrega», «cuenta», «aplicación», «factura», «pregunta», «ayuda», «actualización», «estado"]
 hot = ["reembolso», «roto», «cobrado», «dos veces"]
 comentarios, has_hot = [], []
 para _ en el rango (n):
 words = list (rng.choice (neutral, rng.integers (4, 9)))
 bandera = rng.random () < 0.3
 si marca:
 words.insert (rng.integers (len (palabras)), rng.choice (hot))
 comments.append (» «.join (words))
 has_hot.append (bandera)
 hora, fin de semana = np.asarray (opened.hour), np.asarray (opened.dayyofweek >= 5)
 cantidad = rng.gamma (2, 40, n) .round (2)
 logit = (-1,6 + 2,2 * np.array (has_hot) + 1,4 * (hora >= 22) | (hora < 6)) + 1,0 * fin de semana
 + 0,004 * (cantidad - 80)
 escalado = (rng.random (n) < 1/(1 + np.exp (-logit))) .astype (int)
 return pd.DataFrame ({"amount»: amount, «channel»: rng.choice (["web», «app», «phone"], n),
 «opened_at»: abierto, «comentario»: comentarios, «escalado»: escalado})
 
 
@section («7. Recetas: la predeterminada elimina las marcas de tiempo y el texto, y lo dice una vez»)
recetas definidas ():
 df = make_tickets ()
 styles = sdm.infer_styles (df, overrides= {"escalated»: «categórico"}, text="infer»)
 table = sdm.tableTensor.from_pandas (df, styles, device=device)
 x_ctx, y_ctx = table [:1000] .drop_columns («escalado»), table [:1000, «escalado"]
 x_q, y_q = table [1000:] .drop_columns («escalado»), df.escalated.to_numpy () [1000:]
 print (f» columns: {', '.join (f' {c} = {s}' para c, s en stypes.items ())}»)
 print (» escalamiento se define por la apertura nocturna o el fin de semana y por palabras como «reembolso»\n«)
 
 default = sdm.models.tabicLV2.default_recipe ()
 with_calendar = sdm.models.tabiclv2.default_recipe ()
 with_calendar.prepend_features (sp.stypeDispatch (
 dateTime=sp.addCalendarFields (["hour», «weekday"], encoding="cíclico»))
 calendar_and_text = sp.StypeDispatch (
 dateTime=sp.addCalendarFields (["hora», «día de la semana"], encoding="cíclico»),
 text=sp.tfif (ngram_range =( 1, 1), max_features=64)
 with_both = sdm.models.tabiclv2.default_recipe ()
 con ambas características.prepend_( calendar_y_text)
 
 modelo = SDM.models.tabicLV2 (dispositivo=dispositivo)
 aucs = {}
 para etiqueta, receta en [(«receta predeterminada», predeterminada),
 («+ Agregar campos de calendario (hora, día de la semana)», with_calendar),
 («+ AddCalendarFields + TFIDF (64 términos)», con_both)]:
 torch.manual_seed (0)
 proba = modelo (x_context=x_ctx, y_context=y_ctx, x_query=x_q, num_estimators=4, recipe=recipe) .to_pandas ()
 aucs [etiqueta] = roc_auc_score (y_q, proba ["1"])
 print (f» {label:40s} AUC {aucs [label] :.3f}»)
 
 print (»\nLa receta predeterminada de TabicLv2 se envía por tipo y solo tiene ramas para «categórica» y «)
 print (») columnas numéricas, por lo que las columnas de fecha/hora y de texto nunca se convierten en funciones. SDM sí lo dice, en»)
 print (» la RuntimeWarning de arriba, pero es una advertencia: se activa la primera vez y se suprime durante»)
 print (» el resto del proceso. Todos los modelos posteriores de la misma libreta o servicio eliminan esas columnas»).
 print (» en silencio. La solución consiste en dos procesadores, precedidos de las funciones:»)
 print (textwrap.indent (repr (calendar_and_text), "«))
 return "-> «.join (f" {v: .3f}» para v en aucs.values ()) + "AUC»
 
 
recetas ()
El

preprocesamiento en la biblioteca es una receta de procesadores que admiten composición, y la receta tabicLV2 predeterminada se distribuye en tipo semántico con ramas solo para columnas categóricas y numéricas. Por lo tanto, las columnas de marca de tiempo y texto nunca se convierten en funciones. En los tickets de soporte cuyo escalamiento depende de cuándo se abrieron y en palabras como reembolso, la receta predeterminada obtiene un AUC de 0,525, casi al azar. La biblioteca emite un RuntimeWarning con los nombres de los tipos de columnas ignorados. Sin embargo, se trata de una advertencia: se activa la primera vez y se suprime durante el resto del proceso, por lo que todos los modelos posteriores de la misma libreta o servicio eliminan esas columnas en silencio. Lo solucionan dos procesadores: addCalendarFields convierte la marca de tiempo en funciones cíclicas de horas y días laborables, elevando el AUC a 0,634, y TFIDF añade los términos del comentario, elevándolo a 0,789

.
@section (8). Explicar una predicción: dos trampas dentro del explicador»)
def explain ():
 rng = np.random.default_rng (2)
 X = rng.normal (tamaño = (500, 8))
 true = {"x0": 2.0, «x3": -1.5, «x5": 1.0}
 logit = sum (w * X [:, int (c [1:])] para c, w en true.items ())
 df = pd.DataFrame (X, columns= [f"x {i}» para i en el rango (8)])
 df ["y"] = (rng.random (500) < 1/(1 + np.exp (-logit))) .astype (int)
 tabla = as_table (df, «y»)
 modelo = SDM.models.tabicLv2 (dispositivo=dispositivo)
 ctx = dict (x_context=table [:400] .drop_columns («y»), y_context=table [:400, «y"])
 x_q = tabla [400:] .drop_columns («y»)
 p1 = lambda pred: pred.numerical [..., pred.columns [SDM.stype.Numerical] .index («1")] # P (y=1), por NOMBRE
 
 visto = []
 def peek (pred):
 visto.append (pred.columns [sdm.stype.Numerical])
 devuelve pred.numerical.sum () * 0
 torch.manual_seed (0)
 sdm.explain.GradientExplainer (salida=peek) .explain (model, x_q, num_estimators=4, **ctx)
 print (f» num_estimators=4: la función de salida se llama {len (seen)} veces, orden de clases por llamada:»)
 print (f» {visto}»)
 
 def only_call (n):
 llamadas = {"i»: 0}
 def (pred):
 llama ["i"] += 1
 devuelve p1 (pred) si llama a ["i"] == n else p1 (pred) * 0
 devuelve f
 tamaños = {}
 para n in (1, len (seen)):
 torch.manual_seed (0)
 g = sdm.explain.GradientExplainer (salida=only_call (n)) .explain (model, x_q, num_estimators=4, **ctx)
 tamaños [n] = float (g.x.to_pandas () .abs () .mean () .mean ())
 print (f»\nTrap 1: solo se explica el ÚLTIMO estimador. La función de salida solo está activa en la llamada 1:»
 f» mean |grad| {sizes [1] :.4f};»)
 print (f» live only on call {len (seen)}: {sizes [len (seen)] :.4f}. Cada llamada sobrescribe el resultado anterior, por lo que»)
 print (» con varios estimadores se obtiene uno de ellos, no su promedio. Explica con num_estimators=1.»)
 
 print (»\nTrampa 2: el orden de las clases difiere según el estimador. num_estimators=1, seis semillas, P (y=1) seleccionado de dos maneras:»)
 print (f» {'seed': >6s} {'order': >12s} "+ «» .join (f "{c + f' ({true [c] :+.1f}) ': >20s}» para c en true))
 print (f» {» :6s} {» :12s} "+ «» .join (f "{'posición/nombre': >20s}» para _ en true))
 by_name = []
 invertido = name_ok = 0
 para semillas en el rango (6):
 pedido = []
 nota definida (pred):
 order.append (pred.columns [sdm.stype.Numerical])
 devuelve pred.numerical.sum () * 0
 torch.manual_seed (semilla)
 sdm.explain.GradientExplainer (salida=nota) .explain (model, x_q, num_estimators=1, **ctx)
 res = {}
 para etiqueta, fn in ((«position», lambda pred: pred.numerical [..., 1]), («name», p1)):
 torch.manual_seed (semilla)
 res [label] = sdm.explain.GradientExplainer (output=fn) .explain (
 model, x_q, num_estimators=1, **ctx) .x.to_pandas () .mean ()
 by_name.append (res ["nombre"])
 flipped += any (np.sign (res ["position"] [c])! = np.sign (w) para c, w en true.items ())
 name_ok += all (np.sign (res ["name"] [c]) == np.sign (w) para c, w en true.items ())
 print (f» {seed: >6d} {str (order [0]) :>12s} "+
 «» .join (f "{res ['position'] [c] :>10.2f}/{res ['name'] [c] :>7.2f}» para c en true)
 avg = pd.concat (by_name, axis=1) .mean (axis=1)
 ruido = float (avg.drop (list (true)) .abs () .max ())
 print (f»\nCuando la mezcla pone la clase '1' en primer lugar, la columna 1 es P (y=0) y todos los signos se invierten. Seleccionando por»)
 print (f» name indica el signo correcto en {name_ok} /6 semillas. Promediado sobre las semillas, por nombre: "+
 «, «.join (f" {c} {avg [c] :+.2f}» for c in true) + f»; característica de mayor ruido {noise: .2f}.»)
 print (» Los gradientes se refieren a las entradas PREPROCESADAS, es decir, estandarizadas.»)
 devuelve «posición invertida en {flipped} /6 semillas; nombre correcto en {name_ok} /6; solo en el último estimador»
 
 
explicar ()

El GradientExplainer de la biblioteca devuelve el gradiente de una salida elegida con respecto a las entradas de consulta preprocesadas, y lo probamos en datos sintéticos en los que solo importan tres de las ocho características, con signos conocidos. Tiene dos trampas, ambas visibles solo al observar lo que recibe la función de salida. En primer lugar, la función se llama una vez por estimador y cada llamada sobrescribe el resultado anterior, por lo que, con cuatro estimadores, una función de salida que solo esté activa en la primera llamada arroja una explicación totalmente cero y la explicación que se obtiene es solo la del último estimador. En segundo lugar, la salida sin procesar de cada estimador tiene su propio orden de clases, porque la receta predeterminada mezcla las etiquetas de clase por estimador. Al explicar con un solo estimador para seis semillas, al seleccionar la probabilidad de la clase uno por posición de columna, se invierte el signo de cada característica de las dos semillas cuyo orden está invertido, mientras que al seleccionarla por el nombre de la columna se obtiene el signo correcto en las seis, y al promediar las semillas se recuperan las tres características reales con las características de ruido cercanas a cero.

make_shop (seed=0, n_customers=800, n_products=60):
 «" "Clientes, productos y un año de pedidos. La actividad de algunos clientes se interrumpe en una fecha aleatoria. "»
 rng = np.random.default_rng (velocidad)
 planes = rng.choice (["basic», «pro"], n_customers, p= [0.7, 0.3])
 clientes = pd.DataFrame ({"customer_id»: np.arange (n_customers),
 «region»: rng.choice (["NA», «UE», «APAC», «LATAM"], n_customers), «plan»: plans})
 products = pd.DataFrame ({"product_id»: np.arange (n_products),
 «category»: rng.choice (["books», «tech», «home», «toys"], n_products),
 «precio»: rng.gamma (2.0, 20.0, n_products) .round (2)})
 weekly_rate = rng.gamma (1.5, 0.6, n_customers) * np.where (planes == «pro», 1.6, 1.0)
 fade_day = rng.uniform (60, 500, n_clientes)
 filas, start = [], pd.Timestamp («2025-01-01")
 para c en el rango (n_customers):
 t = 0.0
 si bien es cierto:
 rate = weekly_rate [c]/7 * (1.0 si t < fade_day [c] sino 0.08)
 t += rng.exponencial (1/max (velocidad, 1e-6))
 si t > 364:
 romper
 p = int (rng.integers (n_products))
 rows.append ((c, p, start + pd.timeDelta (float (t), unit="d»), float (products.price [p] * rng.integers (1, 4)))
 orders = pd.DataFrame (filas, columnas = ["customer_id», «product_id», «order_date», «amount"])
 orders.insert (0, «order_id», np.arange (len (orders)))
 orders ["order_date"] = orders ["order_date"] .dt.floor («s»)
 clientes, productos, pedidos que regresan
 
 
defina task_rows (clientes, pedidos, límite, horizon_days=30):
 «" "Etiqueta: ¿el cliente vuelve a realizar el pedido en un plazo de 30 días a partir de la fecha límite? ""»
 window = orders [(orders.order_date > límite) & (orders.order_date <= cutoff + PD.TimeDelta (horizon_days, unit="D»))]
 activo = establecido (window.customer_id)
 return pd.DataFrame ({"customer_id»: customers.customer_id, «timestamp»: límite,
 «label»: [int (c en activo) para c en customers.customer_id]})
 
 
@section («9). Una base de datos relacional como entrada: tablas, claves y un muestreador temporal»)
def relational_setup ():
 clientes, productos, pedidos = make_shop ()
    datos = SDM.RelationalData (
 tablas = {
 «customers»: sdm.tableTensor.from_pandas (customers, {"customer_id»: «id», «region»: «categórico»,
 «plan»: «categórico"}),
 «products»: sdm.tableTensor.from_pandas (products, {"product_id»: «id», «category»: «categórico»,
 «precio»: «numérico"}),
 «orders»: sdm.tableTensor.from_pandas (orders, {"order_id»: «id», «customer_id»: «id», «product_id»: «id»,
 «order_date»: «fecha/hora», «importe»: «numérico"}),
 },
 relaciones= [
 dict (left_table="pedidos», left_column="customer_id», right_table="clientes», right_column="customer_id»),
 dict (left_table="orders», left_column="product_id», right_table="products», right_column="product_id»),
 ],
 )
 cutoffs_ctx = [pd.Timestamp (d) para d en («2025-06-01", «2025-07-01", «2025-08-01")]
 cutoff_q = pd.Timestamp («2025-09-15")
 task_df = pd.concat ([task_rows (clientes, pedidos, c) para c en cutoffs_ctx + [cutoff_q]], ignore_index=true)
 task = sdm.TableTensor.from_pandas (task_df, {"customer_id»: «id», «timestamp»: «fecha y hora», «etiqueta»: «categórico"})
 n_ctx = len (cutoffs_ctx) * len (clientes)
 link = dict (task_link= {"task_column»: «customer_id», «table»: «clientes», «table_column»: «customer_id"},
 num_neighbors= [16, 16], task_time_column="timestamp»)
 
 print (f» {len (customers)} clientes, {len (products)} productos, {len (orders):,} pedidos superiores a 2025")
 print (» relaciones: orders.customer_id -> clientes, orders.product_id -> productos»)
 tarea print (f»): ¿el cliente realizará el pedido dentro de los 30 días posteriores a la fecha límite? tasa positiva {task_df.label.mean () :.2f}»)
 print (f» context: cutoffs {[str (c.date ()) para c en cutoffs_ctx]} ({n_ctx:,} filas); query: {cutoff_q.date ()}»)
 
 sampler = data.sampler (time_columns= {"orders»: «order_date"})
 sample, related = sampler (task [:3] .drop_columns («label»), **link)
 print (»\nlo que el muestreador entrega al modelo para las 3 primeras filas de tareas (16 adyacentes, luego 16 más):»)
 para el nombre, tbl en related.tables.items ():
 print (f» {name:10s} {tuple (tbl.size ())}»)
 latest = related.tables ["orders"] .to_pandas () ["order_date"] .max ()
 print (f» último pedido que devolvió el muestreador: {latest} fecha y hora de la tarea: {cutoffs_ctx [0]}»)
 print (» El muestreador recorre las claves foráneas hacia atrás en el tiempo: no hay ningún orden fechado después de la fila de tareas»)
 print («se admite una marca de tiempo propia, por lo que las filas que definen la etiqueta no pueden filtrarse»).
 globals () .update (shop=dict (customers=customers, orders=orders, data=data, task=task, task_df=task_df,
 n_ctx=n_ctx, enlace=enlace, cutoffs=cutoffs_ctx + [cutoff_q]))
 return f "{len (orders):,} pedidos en 3 tablas enlazadas»
 
 
relational_setup
()

KumoreLational utiliza una base de datos relacional en lugar de una sola tabla. Creamos una pequeña tienda con ochocientos clientes, sesenta productos y un año de pedidos, en la que la actividad de algunos clientes se interrumpe en una fecha aleatoria y nos preguntamos si cada cliente volverá a hacer su pedido dentro de los treinta días siguientes a la fecha límite. RelationalData coloca las tablas y las claves foráneas entre ellas, y su muestreador las separa de cada fila de tareas, recopilando hasta dieciséis teclas vecinas por salto. Declarar la fecha del pedido como una columna de tiempo hace que el recorrido sea temporal: comprobamos que el último pedido que devuelve el muestreador para una fila de tareas con fecha del 1 de junio se realizó el 31 de mayo, por lo que las filas que definen la etiqueta están fuera

de nuestro alcance.
def relational_auc (time_columns):
 s = TIENDA
 sampler = s ["data"] .sampler (time_columns=time_columns)
 modelo = sdm.models.KumoreLational (dispositivo=dispositivo)
 torch.manual_seed (0)
 contexto, relacionado = sampler (s ["tarea"] [:s ["n_ctx"]], **s ["enlace"])
 model.fit (x=context.drop_columns («etiqueta»), y=context ["etiqueta"], related_tables=related,
 generador=torch.generator () .manual_seed (0)
 query = s ["tarea"] [s ["n_ctx"]:] .drop_columns («etiqueta»)
 proba = model.predict (*sampler (query, **s ["enlace"])) .to_pandas ()
 return roc_auc_score (s ["task_df"] .label.to_numpy () [s ["n_ctx"]:], proba ["1"])
 
 
def history_features (clientes, pedidos, límite):
 «" "La característica de ingeniería que un científico de datos escribiría a mano, estrictamente antes del límite ""».»
 pasado = pedidos [orders.order_date <= límite]
 by_customer = past.groupby («customer_id»)
 f = pd.DataFrame ({"customer_id»: customers.customer_id})
 durante días en (30, 90):
 recent = past [past.order_date > cutoff - pd.timeDelta (days, unit="D»)] .groupby («customer_id»)
 [f"orders_ {days} "] = f.customer_id.map (recent.size ()) .fillan (0)
 [f"spend_ {days} "] = f.customer_id.map (recent.amount.sum ()) .fillna (0)
 f ["days_since_last"] = f.customer_id.map (punto de corte - by_customer.order_date.max ()) .dt.days) .fillna (999)
 f ["orders_total"] = f.customer_id.map (by_customer.size ()) .fillna (0)
 return f.merge (clientes, on="customer_id») .drop (columns="customer_id»)
 
 
def flat_auc (frame_fn):
 s = TIENDA
 X = pd.concat ([frame_fn (c) para c en s ["cutoffs"]], ignore_index=true)
 X ["label"] = s ["task_df"] .label.values
 table = as_table (X, «etiqueta»)
 modelo = SDM.models.tabicLv2 (dispositivo=dispositivo)
 torch.manual_seed (0)
 proba = model (x_context=table [:s ["n_ctx"]] .drop_columns («etiqueta»), y_context=table [:s ["n_ctx"], «etiqueta"],
 x_query=table [s ["n_ctx"]:] .drop_columns («etiqueta»), num_estimators=4) .to_pandas ()
 return roc_auc_score (s ["task_df"] .label.to_numpy () [s ["n_ctx"]:], proba ["1"]), lista (x.Columns [:-1])
 
 
@section («10. KumoreRelational y funciones creadas a mano: la misma pregunta, tres maneras»)
def relational_vs_features ():
 s = TIENDA
 resultados = []
 t0 = time.time ()
 auc, cols = flat_auc (lambda c: s ["clientes"] .drop (columns="customer_id»))
 results.append ((«TabicLv2 solo en la tabla de clientes», auc, f "{len (cols)} raw columns», time.time () - t0))
 t0 = time.time ()
 auc, cols = flat_auc (lambda c: history_features (s ["clientes"], s ["pedidos"], c))
 results.append ((«TabicLV2 sobre funciones diseñadas a mano», auc, "{len (cols) - 2} diseñado + 2 sin procesar», time.time () - t0))
 t0 = time.time ()
 results.append ((«KumoreLational en las tablas sin procesar», relational_auc ({"orders»: «order_date"}), «3 tablas sin procesar», time.time () - t0))
 print (f» {'approach':40s} {'AUC': >6s} {'entradas': 22s} {'segundos': >7s}»)
 para la etiqueta, el AUC, las entradas y los segundos en los resultados:
 print (f» {label:40s} {auc:6.3f} {inputs:22s} {secs:7.1f}»)
 print (»\nLa demografía por sí sola apenas supera a una moneda. El historial de compras es donde está la señal y el»)
 print (» el modelo relacional lo alcanza sin que nadie escriba orders_30d, spend_90d o days_since_last:»)
 print (» lee las propias filas del orden mediante las teclas, a partir de la marca de tiempo de cada fila de tareas»)
 globals () ["REL_AUC"] = resultados [2] [1]
 return (f"customers only {results [0] [1] :.3f}/handbuilt {results [1] [1] :.3f}»
 f»/KumoreLational {resultados [2] [1] :.3f} AUC»)
 
 
relacional_vs_caracteristicas ()

Luego respondemos a la misma pregunta de tres maneras. TabicLV2 solo en la lista de clientes, con solo la región y el plan para continuar, alcanza un AUC de 0,556. TabicLV2, con seis características del historial de ingeniería, los pedidos y los gastos de los últimos treinta y noventa días, los días transcurridos desde el último pedido y el recuento de pedidos de por vida, calculado estrictamente antes de cada límite, llega a 0,828. KumoreLational en las tablas sin procesar, sin ninguna característica escrita por nadie, llega a 0,823. El modelo relacional se encuentra dentro del error de redondeo del proceso creado a mano al leer las propias filas ordenadas a través de las claves, a partir de la

marca de tiempo de cada fila de tareas.
@section («11. El único argumento que hace que un modelo relacional sea honesto»)
def leaks ():
 leaky = relational_auc (Ninguno)
 print (f» KumoreLational with time_columns= {{'orders': 'order_date'}} AUC {REL_AUC: .3f}»)
 print (f» KumoreLational with TIME_COLUMNS=None AUC {leaky: .3f}»)
 print (»\nSin una columna de tiempo, el sampler es libre de devolver los pedidos realizados DESPUÉS de la fecha límite, que»)
 print (» son exactamente los pedidos que definen la etiqueta. El modelo ya no predice, es»)
 print (» leyendo la respuesta. No hay nada erróneo y el número parece un gran avance. En un «real»)
 print (» base de datos, un modelo relacional que es sospechosamente mejor que la base construida a mano»)
 print (» debería ser lo primero con lo que vuelvas a ejecutar cada columna de tiempo declarada»)
 return f "{REL_AUC: .3f} honest vs {leaky: .3f} con futuras filas visibles»
 
 
fuga ()

Finalmente, ejecutamos el mismo modelo relacional sin declarar la columna de tiempo. El AUC sube a 0,940. Sin una columna de tiempo, el muestreador es libre de devolver los pedidos realizados después del límite, que son exactamente los pedidos que definen la etiqueta, por lo que el modelo ya no predice sino que lee la respuesta, y nada se equivoca. En una base de datos real, lo primero que se vuelve a ejecutar con cada columna de tiempo declarada es un modelo relacional que sea sospechosamente mejor que el modelo de referencia creado a mano

.
banner («RESUMEN»)
para el nombre, res en Results.items ():
 print (f» {name: <76s} {res}»)
print («»
¿Qué transferir
 - Declare los estilos de forma deliberada: las columnas de texto e identificación no se detectan a menos que pregunte, y el
 La receta predeterminada de TabicLv2 elimina las columnas de fecha y hora y de texto con una advertencia que se activa una vez.
 - En una función de salida explicativa, seleccione las clases por nombre, nunca por posición.
 - Todas las tablas con una columna de tiempo van en time_columns. Un modelo relacional que no tiene en cuenta el tiempo
 es un detector de fugas, no un pronosticador.
¿A dónde ir ahora
 - Ajuste: examples/tabular/finetune.py desactiva la receta de salida y se basa en registros sin procesar.
 - Puntos de referencia relacionales reales: examples/relational/rel_bench.py ejecuta KumoreLational en RELBench.
 - En un entorno de ejecución T4, defina DEVICE = «cuda» para los modelos tabulares; cudf mantiene el trabajo del marco de datos en la GPU.
 - Documentos: nvidia.github.io/structured-data-models Código: github.com/nvidia/structured-data-models
«»)

El resumen muestra en una línea el resultado que arrojó cada sección y los tres hábitos que vale la pena conservar: declarar deliberadamente los tipos semánticos y las recetas, seleccionar las clases por su nombre dentro de un explicador y explicarlas con un estimador, y colocar todas las tablas con una columna de tiempo en time_columns. A continuación, apunta al ajuste fino, al ejemplo de RELBench y a la ejecución mediante GPU

.

En conclusión, los modelos de datos estructurados de NVIDIA cumplen su promesa central: entre la clasificación, la regresión, un problema de veinte clases y una base de datos de tres tablas, sus modelos preentrenados igualaron o superaron las líneas base de scikit-learn entrenadas en las mismas filas sin ser entrenados ellos mismos, devolvieron distribuciones predictivas calibradas de forma gratuita y, en el caso relacional, alcanzaron la precisión de una cartera de funciones creada a mano a partir de tablas sin procesar. Los valores predeterminados merecen un mayor escrutinio que los modelos. El texto libre y las identificaciones no se detectan a menos que se soliciten, la receta predeterminada elimina las marcas de tiempo y las columnas de texto y solo advierte una vez por proceso, la explicación solo informa sobre el último estimador y necesita que las clases se seleccionen por su nombre, la URL de pyg-lib del propio mensaje de error de la biblioteca apunta a una página vacía y un modelo relacional sin la columna de tiempo produce un número impresionante y sin sentido. Cada uno de ellos tiene una solución de una línea y, de lo contrario, todos pasarían desapercibidos, lo cual es la lección práctica que hay que incorporar a los datos reales: compruebe lo que llega al modelo antes de confiar en lo que sale de él.


Consulta los CÓDIGOS completos aquí. Todo el mérito es para el investigador de este proyecto. Además, siéntete libre de seguirnos en Twitter y no olvides unirte a nuestro subReddit de más de 150 000 ml y suscribirte a nuestro boletín. ¡Espera! ¿Estás en Telegram? ahora también puedes unirte a nosotros en Telegram.

The post Cómo hacer predicciones tabulares sin entrenamiento con modelos de datos estructurados de NVIDIA appeared first on MarkTechPost.

¿Tiene un proyecto de software o plataforma SaaS en mente?

Hable directamente con nuestros arquitectos de software en SoftAndino. Le brindamos asesoría técnica y cotización inmediata sin compromiso.