7ec2bb1b45
El .md del grupo `eda` es la salida pensada para pegar a un LLM, así que debe contener todo lo que el motor computó, aunque el PDF/PPTX (vista humana) resuman. La evaluación 2053 detectó 6 datos que el .md perdía respecto al profile. Se cierran de forma aditiva (el .md tiene MÁS que el PDF/PPTX, sin tocar esos renderers ni los capítulos). render_automatic_eda.py pasa el profile al serializador Markdown vía meta['profile'] (un meta propio del MD; el de PDF/PPTX queda intacto). render_md_impl.py añade un "Apéndice — Datos completos del perfil" al final del documento, emitido solo cuando hay profile y degradando limpio cuando falta una sección (lite sin modelos, profile sin correlaciones). El apéndice no se acopla a los ids de capítulo (que editan otros agentes en paralelo). Pérdidas cerradas: 1. Matriz de asociación COMPLETA: los N pares de correlations.pairs (no solo el top-17), incluidos correlation_ratio (num↔cat) y cramers_v (cat↔cat). 2. Numéricas: describe completo por columna — mean/median/mode/std/variance/cv, skew y kurtosis para TODAS (no solo las asimétricas), p1/p5/p25/p50/p75/p95/ p99, iqr, min/max, outliers, distribution_type. 3. Re-expresión: nombra la transformación concreta (log1p/sqrt/yeo-johnson) con potencia, razón y alternativas, no un vago "considerar re-expresión". 4. KMeans: tabla scores_by_k (silhouette + inercia por k) marcando el k elegido. 5. Normalidad: el estadístico (stat) de cada test junto al p-value. 6. Encabezados de figuras de barras/scree dejan de heredar "Desde/Hasta/Frecuencia" del histograma; usan "Inicio/Fin/Valor" cuando el caption no es un histograma. Test nuevo md_completeness_test.py: profile sintético, asserta los N pares de correlación, skew/kurtosis de cada numérica, percentiles extendidos, log1p, scores_by_k, stat de normalidad, headers de barras y los edges (sin modelos / sin correlaciones / sin profile, defensivo). Verificado con titanic (profile_level=full): 28 pares en la tabla (incl. Sex↔Embarked cramers_v), 7 numéricas con skew+kurtosis, p5/p95/p99, scores_by_k y JB/D'Agostino/Shapiro stat presentes. PDF/PPTX/manifest siguen saliendo. Suite automatic_eda + render_automatic_eda_test: 134 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
290 lines
14 KiB
Python
290 lines
14 KiB
Python
"""render_automatic_eda — EDA completo one-shot: perfil → ctx → PDF + PPTX + MD.
|
|
|
|
Pipeline impuro del grupo de capacidad `eda`. Dada UNA tabla DuckDB (o
|
|
PostgreSQL), produce el informe AutomaticEDA COMPLETO en sus tres formatos a la
|
|
vez (PDF móvil A5 + PPTX 16:9 + Markdown autocontenido para pegar a un LLM) con
|
|
los capítulos POBLADOS, en una sola llamada. Compone, sin reimplementar su
|
|
lógica, varias 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.
|
|
- render_automatic_eda_markdown : serializa el mismo documento a Markdown
|
|
autocontenido (texto + tablas markdown, sin
|
|
binarios) para incorporar a un LLM.
|
|
|
|
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_markdown,
|
|
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,
|
|
emit_md: bool = True,
|
|
) -> 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.
|
|
emit_md: además del PDF y el PPTX, emite un Markdown autocontenido del
|
|
MISMO documento por capítulos (texto plano + tablas markdown, sin
|
|
binarios), pensado para pegar a un LLM. Default True. La ruta sale en
|
|
la clave de retorno ``aeda_md_path``. No altera las demás salidas.
|
|
|
|
Returns:
|
|
dict (nunca lanza). En éxito::
|
|
|
|
{"status": "ok", "pdf_path": str, "pptx_path": str,
|
|
"aeda_md_path": str|None, "manifest_path": str|None,
|
|
"n_pages": int, "n_slides": int, "md_chars": int|None,
|
|
"pdf_note": str, "pptx_note": str, "md_note": str|None,
|
|
"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 {}
|
|
|
|
# Salida Markdown autocontenida (mismo documento por capítulos) para
|
|
# pegar a un LLM. Aditiva: no afecta a PDF/PPTX/manifest. dict-no-throw.
|
|
rmd = {}
|
|
md_path = None
|
|
if emit_md:
|
|
md_path = os.path.join(out_dir, base + ".md")
|
|
# El Markdown es la salida MÁS completa: además del documento por
|
|
# capítulos (compartido con PDF/PPTX) volca un apéndice con TODOS los
|
|
# datos numéricos del perfil (matriz de asociación completa, describe
|
|
# con skew/kurtosis/percentiles, re-expresiones, scores_by_k de
|
|
# KMeans, estadísticos de normalidad). Se le pasa el `prof` vía
|
|
# meta['profile']; un meta propio evita alterar el de PDF/PPTX.
|
|
md_meta = dict(meta)
|
|
md_meta["profile"] = prof
|
|
rmd = render_automatic_eda_markdown(prof, md_path, md_meta) or {}
|
|
|
|
return {
|
|
"status": "ok",
|
|
"pdf_path": rpdf.get("path"),
|
|
"pptx_path": rpptx.get("path"),
|
|
"aeda_md_path": rmd.get("path"),
|
|
"manifest_path": rpdf.get("manifest_path"),
|
|
"n_pages": rpdf.get("n_pages"),
|
|
"n_slides": rpptx.get("n_slides"),
|
|
"md_chars": rmd.get("n_chars"),
|
|
"pdf_note": rpdf.get("note"),
|
|
"pptx_note": rpptx.get("note"),
|
|
"md_note": rmd.get("note"),
|
|
"profile": prof,
|
|
}
|
|
except Exception as e: # noqa: BLE001 — dict-no-throw: degradar, nunca lanzar.
|
|
return {"status": "error", "error": str(e)}
|