3be188a921
Añade el parámetro profile_level a render_automatic_eda como preset de consumo CPU/LLM que mapea a los flags existentes (run_models, run_series, run_llm, sample). Tres niveles: - lite (bajo consumo): run_llm=False, run_series=False, sample=2000 y modelos limitados a PCA + normalidad, SIN KMeans ni IsolationForest (lo caro en CPU). Para un vistazo rápido y barato. - standard (default): comportamiento histórico — modelos completos, serie, sin LLM. - full: standard + narrativa LLM por capítulo. Precedencia: un flag explícito del caller (run_llm=..., run_models=..., etc.) siempre prima sobre el default que fija el preset; el preset solo aplica al parámetro que se deja en None. Cableado del modo lite sin tocar profile_table (lo tocan otros agentes en paralelo): profile_table NO corre los modelos (evita pagar KMeans + IsolationForest); este pipeline los corre con run_eda_models(run_kmeans=False, run_isolation=False) reusando ctx['raw_numeric'], y quita raw_numeric del ctx para que el capítulo modelos no reproyecte clusters KMeans en vivo (project_clusters_2d). geo_points ya queda derivado, así que geospatial no se afecta. Cambio aditivo y retro-compatible: sin profile_level el comportamiento es idéntico al de v1.0.0 (standard). Tests nuevos cubren lite/standard, la precedencia flag-sobre-preset, y la equivalencia del default con el histórico. Bump 1.0.0 -> 1.1.0 + growth log en el .md. Skill /eda documenta --lite/--full. Verificación: golden lite/standard/full sobre titanic — lite 4.8s (PCA+norm, sin KMeans/iso/LLM/serie), standard 7.8s (modelos completos), full 38.3s (+LLM). Suite render_automatic_eda + automatic_eda: 96 passed. fn index sin error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
259 lines
12 KiB
Python
259 lines
12 KiB
Python
"""render_automatic_eda — EDA completo one-shot: perfil → ctx → PDF + PPTX.
|
|
|
|
Pipeline impuro del grupo de capacidad `eda`. Dada UNA tabla DuckDB (o
|
|
PostgreSQL), produce el informe AutomaticEDA COMPLETO en sus dos formatos a la
|
|
vez (PDF móvil A5 + PPTX 16:9) con los 11 capítulos POBLADOS, en una sola
|
|
llamada. Compone, sin reimplementar su lógica, cuatro funciones del registry:
|
|
|
|
- profile_table : perfila la tabla end-to-end (TableProfile agregado),
|
|
opcionalmente con modelos baratos y análisis de serie.
|
|
- build_eda_render_ctx : construye el `ctx` con los DATOS CRUDOS que el
|
|
TableProfile agregado no incluye (raw_numeric para
|
|
modelos/geo, timeseries_raw para series, geo_points
|
|
para el mapa, db_path/table para la agregación
|
|
push-down). Sin él, esos capítulos degradan.
|
|
- render_automatic_eda_pdf : renderiza el documento por capítulos a PDF.
|
|
- render_automatic_eda_pptx : renderiza el mismo documento a PPTX.
|
|
|
|
El TableProfile agregado basta para portada/overview/distribuciones/calidad/
|
|
correlación, pero los capítulos `modelos`, `timeseries`, `geospatial` y
|
|
`agregacion` necesitan datos crudos (los clusters proyectados sobre el PCA, la
|
|
serie valor-vs-tiempo, los puntos lat/lon, las filas para el groupby/pivot).
|
|
`build_eda_render_ctx` los muestrea (LIMIT + push-down, sin traer la tabla
|
|
entera a RAM) y los entrega en `ctx`; este pipeline los pasa como `meta['ctx']`
|
|
a ambos renderers para que el informe salga completo.
|
|
|
|
Estilo dict-no-throw del grupo `eda`: nunca lanza; captura cualquier error y
|
|
degrada a `{"status": "error", "error": str}`.
|
|
"""
|
|
|
|
import os
|
|
from datetime import datetime, timezone
|
|
|
|
from datascience import (
|
|
build_eda_render_ctx,
|
|
render_automatic_eda_pdf,
|
|
render_automatic_eda_pptx,
|
|
run_eda_models,
|
|
)
|
|
from pipelines.profile_table import profile_table
|
|
|
|
# Tokens de almacenamiento por backend (para la portada del informe).
|
|
_STORAGE = {"duckdb": "DuckDB", "postgres": "PostgreSQL"}
|
|
|
|
# Presets de consumo CPU/LLM: cada profile_level fija SOLO los DEFAULTS de los
|
|
# flags que controlan el coste (un flag explícito del caller siempre prima sobre
|
|
# el preset). model_opts != None marca el camino "modelos baratos" (lite): los
|
|
# modelos NO los corre profile_table (que ejecutaría KMeans + IsolationForest),
|
|
# sino run_eda_models con esa granularidad, de modo que el coste CPU de los
|
|
# multivariantes nunca se paga. model_opts None => modelos completos como hasta
|
|
# ahora (profile_table los corre con todos los algoritmos).
|
|
_PROFILE_PRESETS = {
|
|
# Bajo consumo: sin LLM, sin serie, sample reducido y modelos limitados a
|
|
# PCA + normalidad (sin KMeans ni IsolationForest, lo caro en CPU). Vistazo
|
|
# rápido y barato de una tabla.
|
|
"lite": {
|
|
"run_models": True,
|
|
"run_series": False,
|
|
"run_llm": False,
|
|
"sample": 2000,
|
|
"model_opts": {"run_kmeans": False, "run_isolation": False},
|
|
},
|
|
# Default: idéntico al comportamiento histórico del pipeline (modelos
|
|
# completos, serie temporal, sin LLM, sample 5000).
|
|
"standard": {
|
|
"run_models": True,
|
|
"run_series": True,
|
|
"run_llm": False,
|
|
"sample": 5000,
|
|
"model_opts": None,
|
|
},
|
|
# Máximo: standard + narrativa LLM (interpretación del perfil y de los
|
|
# capítulos modelos/geospatial/agregacion). Es la única parte que gasta
|
|
# tokens del modelo.
|
|
"full": {
|
|
"run_models": True,
|
|
"run_series": True,
|
|
"run_llm": True,
|
|
"sample": 5000,
|
|
"model_opts": None,
|
|
},
|
|
}
|
|
|
|
|
|
def render_automatic_eda(
|
|
db_path: str,
|
|
table: str,
|
|
backend: str = "duckdb",
|
|
sample: int = None,
|
|
run_models: bool = None,
|
|
run_series: bool = None,
|
|
run_llm: bool = None,
|
|
profile_level: str = "standard",
|
|
out_dir: str = "reports",
|
|
basename: str = None,
|
|
ctx_extra: dict = None,
|
|
) -> dict:
|
|
"""Perfila una tabla y emite el informe AutomaticEDA completo (PDF + PPTX).
|
|
|
|
Args:
|
|
db_path: ruta al archivo DuckDB, o DSN PostgreSQL si backend="postgres".
|
|
table: nombre de la tabla a perfilar.
|
|
backend: "duckdb" (default) o "postgres".
|
|
sample: máximo de filas/valores muestreados por columna para el perfil
|
|
y para los datos crudos del ctx (LIMIT). Default None => lo fija el
|
|
preset de profile_level (lite=2000, standard/full=5000).
|
|
run_models: corre los modelos baratos
|
|
(PCA/KMeans/IsolationForest/normalidad). Necesario para que el
|
|
capítulo `modelos` pinte los clusters sobre el plano PCA. Default
|
|
None => lo fija el preset (True en los tres niveles); en `lite` los
|
|
modelos se limitan a PCA + normalidad (ver profile_level).
|
|
run_series: calcula el análisis de serie temporal por
|
|
columna numérica. Necesario para el análisis del capítulo
|
|
`timeseries` (la gráfica de evolución sale de los datos crudos del
|
|
ctx aunque run_series sea False). Default None => lo fija el preset
|
|
(standard/full=True, lite=False).
|
|
run_llm: hace la interpretación LLM del perfil y
|
|
ACTIVA además la narrativa LLM de los capítulos modelos/geospatial/
|
|
agregacion (títulos de segmento, descripción de la zona, selección de
|
|
agregaciones). Con False esos capítulos usan su derivación
|
|
cuantitativa (siguen completos, sin llamadas de red). Default None =>
|
|
lo fija el preset (full=True, lite/standard=False).
|
|
profile_level: preset de consumo CPU/LLM. Mapea a defaults de los flags
|
|
anteriores; un flag explícito SIEMPRE prima sobre el preset (el
|
|
preset solo fija el default cuando el flag se deja en None):
|
|
|
|
- "lite" bajo consumo: run_llm=False, run_series=False,
|
|
sample=2000 y modelos limitados a **PCA + normalidad** (SIN KMeans
|
|
ni IsolationForest, que es lo caro en CPU). Pensado para un vistazo
|
|
rápido y barato. El capítulo `modelos` sale con PCA + normalidad,
|
|
sin el scatter de clusters.
|
|
- "standard" (default): comportamiento histórico — modelos completos
|
|
(PCA/KMeans/IsolationForest/normalidad), serie temporal, sin LLM.
|
|
- "full" standard + narrativa LLM (run_llm=True).
|
|
|
|
Ejemplo de precedencia: profile_level="lite" con run_llm=True
|
|
explícito => el LLM SÍ se ejecuta (el flag explícito gana al preset).
|
|
out_dir: directorio de salida (se crea si no existe). Default "reports".
|
|
basename: nombre base de los archivos sin extensión. Default
|
|
"aeda_<table>_<timestamp>".
|
|
ctx_extra: dict opcional con claves de presentación/contexto extra que se
|
|
mezclan en el ctx (p.ej. dataset_name, description, source_origin).
|
|
No pisan las claves de datos calculadas por build_eda_render_ctx.
|
|
|
|
Returns:
|
|
dict (nunca lanza). En éxito::
|
|
|
|
{"status": "ok", "pdf_path": str, "pptx_path": str,
|
|
"manifest_path": str|None, "n_pages": int, "n_slides": int,
|
|
"pdf_note": str, "pptx_note": str, "profile": <TableProfile>}
|
|
|
|
En error: {"status": "error", "error": str}.
|
|
"""
|
|
try:
|
|
# 0) Resolución del preset: el profile_level fija los DEFAULTS de los
|
|
# flags de coste; cualquier flag que el caller haya pasado explícito
|
|
# (!= None) prima sobre el preset. Un profile_level desconocido cae a
|
|
# "standard" (comportamiento histórico), sin lanzar.
|
|
preset = _PROFILE_PRESETS.get(profile_level, _PROFILE_PRESETS["standard"])
|
|
sample = preset["sample"] if sample is None else sample
|
|
run_models = preset["run_models"] if run_models is None else run_models
|
|
run_series = preset["run_series"] if run_series is None else run_series
|
|
run_llm = preset["run_llm"] if run_llm is None else run_llm
|
|
model_opts = preset["model_opts"]
|
|
|
|
# En el camino "modelos baratos" (lite) profile_table NO corre los
|
|
# modelos: los ejecuta este pipeline con run_eda_models y la granularidad
|
|
# del preset, evitando pagar el coste CPU de KMeans + IsolationForest.
|
|
# En standard/full profile_table los corre completos como siempre.
|
|
lite_models = bool(run_models) and model_opts is not None
|
|
pt_run_models = bool(run_models) and not lite_models
|
|
|
|
# 1) Perfil base + modelos/serie opcionales. No escribe report propio
|
|
# (write_report=False): este pipeline emite su propio par PDF/PPTX.
|
|
pres = profile_table(
|
|
db_path,
|
|
table,
|
|
backend=backend,
|
|
sample=sample,
|
|
run_models=pt_run_models,
|
|
run_llm=run_llm,
|
|
run_series=run_series,
|
|
emit_pdf=False,
|
|
write_report=False,
|
|
)
|
|
if pres.get("status") != "ok":
|
|
return {"status": "error",
|
|
"error": f"profile_table falló: {pres.get('error')}"}
|
|
prof = pres.get("profile") or {}
|
|
|
|
# 2) Contexto de presentación + datos crudos para los 4 capítulos que los
|
|
# necesitan. Las claves de presentación van en base_ctx; build_eda_render_ctx
|
|
# añade raw_numeric / timeseries_raw / geo_points / db_path / table.
|
|
base_ctx = {
|
|
"dataset_name": table,
|
|
"source_origin": db_path,
|
|
"storage": _STORAGE.get(backend, backend),
|
|
}
|
|
if run_llm:
|
|
# Activa la narrativa LLM de los capítulos que la soportan.
|
|
base_ctx.update({
|
|
"run_cluster_llm": True,
|
|
"run_geo_llm": True,
|
|
"run_agg_llm": True,
|
|
})
|
|
if ctx_extra:
|
|
base_ctx.update(ctx_extra)
|
|
|
|
ctx = build_eda_render_ctx(
|
|
db_path, table, prof, backend=backend, sample=sample,
|
|
base_ctx=base_ctx,
|
|
)
|
|
|
|
# 2.5) Camino lite — modelos baratos (PCA + normalidad, sin KMeans ni
|
|
# IsolationForest). profile_table no corrió los modelos; aquí se corren
|
|
# con run_eda_models reusando la muestra numérica alineada por fila que
|
|
# build_eda_render_ctx ya trajo en ctx['raw_numeric'] (no se reimplementa
|
|
# la lógica de los modelos: se delega en run_eda_models con la
|
|
# granularidad del preset).
|
|
if lite_models:
|
|
raw_numeric = ctx.get("raw_numeric") if isinstance(ctx, dict) else None
|
|
if isinstance(raw_numeric, dict) and raw_numeric:
|
|
model_input = {
|
|
col: {"values": vals, "type": "numeric"}
|
|
for col, vals in raw_numeric.items()
|
|
}
|
|
prof["models"] = run_eda_models(model_input, **model_opts)
|
|
# Quita raw_numeric del ctx para que el capítulo `modelos` NO
|
|
# reproyecte clusters KMeans en vivo (project_clusters_2d ejecuta
|
|
# KMeans): en lite ese coste se evita. geo_points ya quedó derivado
|
|
# en ctx por build_eda_render_ctx, así que el capítulo geospatial no
|
|
# se ve afectado.
|
|
if isinstance(ctx, dict):
|
|
ctx.pop("raw_numeric", None)
|
|
|
|
# 3) Render a ambos formatos desde el MISMO documento por capítulos.
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
base = basename or f"aeda_{table}_{ts}"
|
|
pdf_path = os.path.join(out_dir, base + ".pdf")
|
|
pptx_path = os.path.join(out_dir, base + ".pptx")
|
|
meta = {"title": f"EDA — {table}", "ctx": ctx}
|
|
|
|
rpdf = render_automatic_eda_pdf(prof, pdf_path, meta) or {}
|
|
rpptx = render_automatic_eda_pptx(prof, pptx_path, meta) or {}
|
|
|
|
return {
|
|
"status": "ok",
|
|
"pdf_path": rpdf.get("path"),
|
|
"pptx_path": rpptx.get("path"),
|
|
"manifest_path": rpdf.get("manifest_path"),
|
|
"n_pages": rpdf.get("n_pages"),
|
|
"n_slides": rpptx.get("n_slides"),
|
|
"pdf_note": rpdf.get("note"),
|
|
"pptx_note": rpptx.get("note"),
|
|
"profile": prof,
|
|
}
|
|
except Exception as e: # noqa: BLE001 — dict-no-throw: degradar, nunca lanzar.
|
|
return {"status": "error", "error": str(e)}
|