"""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 _to_numeric_subset(columns: dict) -> dict: """Extrae las columnas numericas como {nombre: [float values]}. Solo se quedan las columnas con ``type == "numeric"``. Para cada una, los valores se convierten a float cuando es posible y los que son None o no parseables se descartan (la lista resultante puede ser mas corta que la original). Mantiene el orden de aparicion de las columnas. Args: columns: mapa {nombre_columna: {"values": list, "type": str}}. Returns: dict {nombre_columna: [float, ...]} solo con columnas numericas. """ numeric: dict[str, list] = {} if not isinstance(columns, dict): return numeric for name, meta in columns.items(): if not isinstance(meta, dict): continue if meta.get("type") != "numeric": continue values = meta.get("values") if not isinstance(values, (list, tuple)): continue parsed: list[float] = [] for v in values: if v is None or isinstance(v, bool): continue try: parsed.append(float(v)) except (TypeError, ValueError): continue numeric[name] = parsed 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": "", }