32c7336bf6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
6.4 KiB
Python
164 lines
6.4 KiB
Python
"""Orquesta los modelos baratos del grupo `eda` en un solo bloque.
|
|
|
|
Compone las funciones puras de modelado del registry (PCA, KMeans, Isolation
|
|
Forest, tests de normalidad) sobre el subconjunto de columnas numericas de un
|
|
perfil de tabla y devuelve el bloque "models" canonico que consume el flag
|
|
``--models`` de ``profile_table``. No reescribe logica: delega en cada funcion
|
|
del registry. Es pura y determinista (todas las dependencias lo son).
|
|
"""
|
|
|
|
from datascience import (
|
|
isolation_forest_outliers,
|
|
kmeans_segments,
|
|
normality_tests,
|
|
pca_explained,
|
|
)
|
|
|
|
|
|
def _pf(v):
|
|
"""Parsea un valor a float; devuelve None si es None/bool/no parseable."""
|
|
if v is None or isinstance(v, bool):
|
|
return None
|
|
try:
|
|
return float(v)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _to_numeric_subset(columns: dict) -> dict:
|
|
"""Extrae las columnas numericas alineadas por fila (listwise deletion).
|
|
|
|
Solo se quedan las columnas con ``type == "numeric"``. CLAVE: la alineacion
|
|
por fila se preserva. Pasos:
|
|
1. Descarta columnas numericas con menos del 50% de valores parseables
|
|
(evita que una columna casi-toda-nula tire todas las filas en el paso 3).
|
|
2. Sobre las columnas buenas, conserva SOLO las filas en las que TODAS
|
|
tienen un valor numerico (listwise deletion).
|
|
El resultado es un mapa {nombre: [float, ...]} donde todas las listas tienen
|
|
la MISMA longitud (filas completas) — requisito de PCA/KMeans/IsolationForest
|
|
(matriz rectangular sin NaN). El bug previo descartaba None por columna,
|
|
dejando longitudes desiguales y reventando sklearn con ValueError.
|
|
|
|
Args:
|
|
columns: mapa {nombre_columna: {"values": list, "type": str}}; las listas
|
|
llegan alineadas por fila (misma longitud, None donde no hay dato).
|
|
|
|
Returns:
|
|
dict {nombre_columna: [float, ...]} con columnas numericas de igual
|
|
longitud. Vacio si no hay columnas numericas validas.
|
|
"""
|
|
if not isinstance(columns, dict):
|
|
return {}
|
|
raw: dict[str, list] = {}
|
|
for name, meta in columns.items():
|
|
if not isinstance(meta, dict):
|
|
continue
|
|
if meta.get("type") != "numeric":
|
|
continue
|
|
values = meta.get("values")
|
|
if isinstance(values, (list, tuple)):
|
|
raw[name] = list(values)
|
|
if not raw:
|
|
return {}
|
|
|
|
# Longitud comun (min, defensivo si llegaran desalineadas).
|
|
n = min(len(v) for v in raw.values())
|
|
if n == 0:
|
|
return {}
|
|
|
|
# 1) Parsea por celda y descarta columnas con <50% de valores parseables.
|
|
good: dict[str, list] = {}
|
|
for name, values in raw.items():
|
|
parsed = [_pf(values[i]) for i in range(n)]
|
|
if sum(1 for x in parsed if x is not None) >= 0.5 * n:
|
|
good[name] = parsed
|
|
if not good:
|
|
return {}
|
|
|
|
# 2) Listwise: conserva solo filas donde TODAS las columnas tienen valor.
|
|
names = list(good.keys())
|
|
numeric: dict[str, list] = {name: [] for name in names}
|
|
for i in range(n):
|
|
if all(good[name][i] is not None for name in names):
|
|
for name in names:
|
|
numeric[name].append(good[name][i])
|
|
return numeric
|
|
|
|
|
|
def run_eda_models(
|
|
columns: dict,
|
|
run_pca: bool = True,
|
|
run_kmeans: bool = True,
|
|
run_isolation: bool = True,
|
|
run_normality: bool = True,
|
|
) -> dict:
|
|
"""Ejecuta los modelos baratos del grupo `eda` sobre las columnas numericas.
|
|
|
|
Composicion canonica para el flag ``--models`` de ``profile_table``. Toma el
|
|
mapa de columnas con el mismo shape que recibe ``association_matrix`` (cada
|
|
columna con ``values`` y ``type``), extrae el subconjunto numerico, y corre
|
|
los modelos pedidos sobre el. No reescribe ninguno: compone las funciones
|
|
puras ``pca_explained``, ``kmeans_segments``, ``isolation_forest_outliers``
|
|
y ``normality_tests`` del registry.
|
|
|
|
Los tests de normalidad se corren por columna numerica individual (basta 1
|
|
columna). PCA, KMeans e Isolation Forest son multivariantes y necesitan al
|
|
menos 2 columnas numericas; con menos, sus claves quedan en None y se
|
|
devuelve una ``note`` explicativa. No lanza excepciones.
|
|
|
|
``trend_slope`` NO se ejecuta aqui: requiere un orden temporal explicito y
|
|
queda disponible suelto en el registry.
|
|
|
|
Args:
|
|
columns: mapa {nombre_columna: {"values": list, "type": str}}, mismo
|
|
shape que recibe ``association_matrix``; listas alineadas por fila.
|
|
run_pca: si True, ejecuta PCA sobre el subconjunto numerico.
|
|
run_kmeans: si True, ejecuta KMeans con seleccion automatica de k.
|
|
run_isolation: si True, ejecuta Isolation Forest multivariante.
|
|
run_normality: si True, ejecuta tests de normalidad por columna.
|
|
|
|
Returns:
|
|
dict con:
|
|
n_numeric_cols: numero de columnas numericas detectadas.
|
|
pca: salida de pca_explained o None (si run_pca False / <2 cols).
|
|
kmeans: salida de kmeans_segments o None (si run_kmeans False / <2).
|
|
outliers: salida de isolation_forest_outliers o None.
|
|
normality: {col: salida de normality_tests} o None (si run_normality
|
|
False o no hay columnas numericas).
|
|
note: descripcion de por que faltan los multivariantes, si aplica.
|
|
|
|
Con menos de 2 columnas numericas devuelve los multivariantes en None y
|
|
una ``note``; ``normality`` sigue poblandose si run_normality True y hay
|
|
al menos 1 columna numerica.
|
|
"""
|
|
numeric = _to_numeric_subset(columns)
|
|
n_numeric_cols = len(numeric)
|
|
|
|
# normality es univariante: basta una columna numerica.
|
|
normality = None
|
|
if run_normality and n_numeric_cols >= 1:
|
|
normality = {name: normality_tests(values) for name, values in numeric.items()}
|
|
|
|
if n_numeric_cols < 2:
|
|
return {
|
|
"n_numeric_cols": n_numeric_cols,
|
|
"pca": None,
|
|
"kmeans": None,
|
|
"outliers": None,
|
|
"normality": normality,
|
|
"note": "insuficientes columnas numericas para modelos multivariantes",
|
|
}
|
|
|
|
pca = pca_explained(numeric) if run_pca else None
|
|
kmeans = kmeans_segments(numeric) if run_kmeans else None
|
|
outliers = isolation_forest_outliers(numeric) if run_isolation else None
|
|
|
|
return {
|
|
"n_numeric_cols": n_numeric_cols,
|
|
"pca": pca,
|
|
"kmeans": kmeans,
|
|
"outliers": outliers,
|
|
"normality": normality,
|
|
"note": "",
|
|
}
|