feat(eda): wiring AutomaticEDA — build_eda_render_ctx + pipeline render_automatic_eda + profile_table(emit_automatic)
Conecta el motor AutomaticEDA con los datos crudos para que los 4 capítulos
dependientes de ctx (modelos, timeseries, geospatial, agregacion) salgan
POBLADOS en vez de degradar a una nota.
- build_eda_render_ctx (datascience, impure, dict-no-throw): dado db_path+table
y el TableProfile agregado, construye el ctx con los datos crudos que el
perfil no incluye: raw_numeric {col:[float|None]} alineado por fila (modelos /
geospatial), timeseries_raw {time_col,t,series} vía extract_timeseries_raw,
geo_points {lats,lons} desde el par lat/lon detectado, y db_path/table para el
groupby/pivot push-down de agregacion. Muestrea con LIMIT (no trae la tabla
entera a RAM). Compone detect_time_column / extract_timeseries_raw /
detect_latlon_columns / duckdb_query_readonly (imports lazy para evitar ciclo).
- render_automatic_eda (pipeline): one-shot perfil -> ctx -> PDF + PPTX con los
11 capítulos poblados; devuelve rutas + manifest de versiones por capítulo.
- profile_table: flag aditivo emit_automatic=True emite el AutomaticEDA PDF+PPTX
además del flujo legacy (emit_pdf/render_eda_pdf intacto). Nuevas claves de
retorno aeda_pdf_path / aeda_pptx_path / aeda_manifest_path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ lang: py
|
||||
domain: pipelines
|
||||
purity: impure
|
||||
version: "1.0.0"
|
||||
signature: "def profile_table(db_path: str, table: str, backend: str = \"duckdb\", sample: int = 5000, run_models: bool = False, run_llm: bool = False, run_series: bool = False, emit_pdf: bool = False, report_dir: str = \"reports\", write_report: bool = True) -> dict"
|
||||
signature: "def profile_table(db_path: str, table: str, backend: str = \"duckdb\", sample: int = 5000, run_models: bool = False, run_llm: bool = False, run_series: bool = False, emit_pdf: bool = False, emit_automatic: bool = False, report_dir: str = \"reports\", write_report: bool = True) -> dict"
|
||||
description: "Orquestador one-shot del grupo de capacidad eda: perfila UNA tabla (DuckDB o PostgreSQL) end-to-end componiendo las funciones del grupo (perfil base SQL + muestreo read-only + inferencia semantica + promocion de tipo + estadistica numerica/categorica + score de calidad + correlaciones con correccion FDR + re-expresion de Tukey + avisos exploratorios) y, opcional, modelos baratos (run_models), interpretacion LLM (run_llm) y analisis de serie temporal por columna (run_series: estacionariedad ADF+KPSS, ACF/PACF, STL, retornos). Emite el TableProfile completo mas (opcional) report markdown + JSON sidecar + PDF movil (emit_pdf). Es la composicion canonica para hazme un EDA de esta tabla."
|
||||
tags: [eda, duckdb, postgres, profiling, data-quality, pipeline, dataops, timeseries]
|
||||
uses_functions:
|
||||
@@ -26,6 +26,9 @@ uses_functions:
|
||||
- exploratory_caveats_py_datascience
|
||||
- render_eda_markdown_py_datascience
|
||||
- render_eda_pdf_py_datascience
|
||||
- build_eda_render_ctx_py_datascience
|
||||
- render_automatic_eda_pdf_py_datascience
|
||||
- render_automatic_eda_pptx_py_datascience
|
||||
- duckdb_query_readonly_py_infra
|
||||
- pg_query_py_infra
|
||||
uses_types: []
|
||||
@@ -55,11 +58,13 @@ params:
|
||||
desc: "Si True (default False) calcula por columna numerica un bloque de serie temporal (estacionariedad ADF+KPSS, ACF/PACF, STL y, si parece de niveles, retornos). Ordena por la primera columna datetime si existe; si no, por el orden fisico. Guardado en col['series'] y agregado en prof['series']."
|
||||
- name: emit_pdf
|
||||
desc: "Si True (default False) renderiza un PDF multipagina vertical (legible en movil) del perfil junto al report markdown y devuelve su ruta en pdf_path."
|
||||
- name: emit_automatic
|
||||
desc: "Si True (default False) emite ADEMAS el informe AutomaticEDA completo en PDF (A5 movil) Y PPTX (16:9) con los 11 capitulos del motor; construye el ctx de datos crudos con build_eda_render_ctx para que modelos/timeseries/geospatial/agregacion salgan poblados. Aditivo: no sustituye a emit_pdf. Rutas en aeda_pdf_path / aeda_pptx_path / aeda_manifest_path."
|
||||
- name: report_dir
|
||||
desc: "Directorio donde escribir los reports si write_report (y el PDF si emit_pdf). Default 'reports'. Se crea si no existe."
|
||||
- name: write_report
|
||||
desc: "Si True (default) escribe report markdown + JSON sidecar timestamped en report_dir; si False no toca disco y los paths markdown/json del retorno son None (emit_pdf es independiente)."
|
||||
output: "dict {status:'ok', profile:<TableProfile enriquecido con quality_score, key_candidates, type_breakdown recalculado, correlaciones con FDR, reexpression por columna numerica, caveats, y (con run_series) series>, report_md_path:str|None, report_json_path:str|None, pdf_path:str|None} o {status:'error', error:str} (dict-no-throw)."
|
||||
output: "dict {status:'ok', profile:<TableProfile enriquecido con quality_score, key_candidates, type_breakdown recalculado, correlaciones con FDR, reexpression por columna numerica, caveats, y (con run_series) series>, report_md_path:str|None, report_json_path:str|None, pdf_path:str|None, aeda_pdf_path:str|None, aeda_pptx_path:str|None, aeda_manifest_path:str|None (estos tres solo con emit_automatic)} o {status:'error', error:str} (dict-no-throw)."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
@@ -32,11 +32,14 @@ from datascience import (
|
||||
acf_pacf,
|
||||
adf_kpss_stationarity,
|
||||
association_matrix,
|
||||
build_eda_render_ctx,
|
||||
column_quality_score,
|
||||
describe_numeric,
|
||||
eda_llm_insights,
|
||||
exploratory_caveats,
|
||||
infer_semantic_type,
|
||||
render_automatic_eda_pdf,
|
||||
render_automatic_eda_pptx,
|
||||
render_eda_markdown,
|
||||
render_eda_pdf,
|
||||
run_eda_models,
|
||||
@@ -385,6 +388,7 @@ def profile_table(
|
||||
run_llm: bool = False,
|
||||
run_series: bool = False,
|
||||
emit_pdf: bool = False,
|
||||
emit_automatic: bool = False,
|
||||
report_dir: str = "reports",
|
||||
write_report: bool = True,
|
||||
) -> dict:
|
||||
@@ -412,6 +416,15 @@ def profile_table(
|
||||
emit_pdf: si True (default False) renderiza un PDF multipagina vertical
|
||||
(legible en movil) del perfil junto al report markdown y devuelve su
|
||||
ruta en pdf_path.
|
||||
emit_automatic: si True (default False) emite ademas el informe
|
||||
AutomaticEDA COMPLETO en sus dos formatos (PDF A5 movil + PPTX 16:9)
|
||||
con los 11 capitulos del motor por capitulos. Construye el contexto
|
||||
de datos crudos con build_eda_render_ctx (raw_numeric para modelos/
|
||||
geo, timeseries_raw para series, geo_points para el mapa, db_path/
|
||||
table para la agregacion push-down) para que los capitulos modelos/
|
||||
timeseries/geospatial/agregacion salgan poblados, no degradados. Es
|
||||
ADITIVO: no sustituye a emit_pdf (render_eda_pdf). Sus rutas vuelven
|
||||
en aeda_pdf_path / aeda_pptx_path / aeda_manifest_path.
|
||||
report_dir: directorio donde escribir los reports si write_report.
|
||||
Default "reports". Se crea si no existe.
|
||||
write_report: si True (default), escribe un report markdown + un JSON
|
||||
@@ -727,12 +740,51 @@ def profile_table(
|
||||
except Exception: # noqa: BLE001
|
||||
pdf_path = None
|
||||
|
||||
# Informe AutomaticEDA completo (PDF + PPTX por capitulos). Aditivo:
|
||||
# convive con emit_pdf (render_eda_pdf) sin sustituirlo. Construye el ctx
|
||||
# con los datos crudos para que modelos/timeseries/geospatial/agregacion
|
||||
# salgan poblados; degrada por clave si build_eda_render_ctx falla.
|
||||
aeda_pdf_path = None
|
||||
aeda_pptx_path = None
|
||||
aeda_manifest_path = None
|
||||
if emit_automatic:
|
||||
try:
|
||||
os.makedirs(report_dir, exist_ok=True)
|
||||
base_ctx = {
|
||||
"dataset_name": table,
|
||||
"source_origin": db_path,
|
||||
"storage": "DuckDB" if backend == "duckdb" else (
|
||||
"PostgreSQL" if backend == "postgres" else backend),
|
||||
}
|
||||
if run_llm:
|
||||
base_ctx.update({"run_cluster_llm": True,
|
||||
"run_geo_llm": True, "run_agg_llm": True})
|
||||
ctx = build_eda_render_ctx(
|
||||
db_path, table, prof, backend=backend, sample=sample,
|
||||
base_ctx=base_ctx)
|
||||
meta = {"title": f"EDA — {table}", "ctx": ctx}
|
||||
aeda_pdf_target = os.path.join(report_dir,
|
||||
f"aeda_{table}_{ts}.pdf")
|
||||
aeda_pptx_target = os.path.join(report_dir,
|
||||
f"aeda_{table}_{ts}.pptx")
|
||||
rpdf = render_automatic_eda_pdf(prof, aeda_pdf_target, meta) or {}
|
||||
rpptx = render_automatic_eda_pptx(
|
||||
prof, aeda_pptx_target, meta) or {}
|
||||
aeda_pdf_path = rpdf.get("path")
|
||||
aeda_pptx_path = rpptx.get("path")
|
||||
aeda_manifest_path = rpdf.get("manifest_path")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"profile": prof,
|
||||
"report_md_path": report_md_path,
|
||||
"report_json_path": report_json_path,
|
||||
"pdf_path": pdf_path,
|
||||
"aeda_pdf_path": aeda_pdf_path,
|
||||
"aeda_pptx_path": aeda_pptx_path,
|
||||
"aeda_manifest_path": aeda_manifest_path,
|
||||
}
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: render_automatic_eda
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
purity: impure
|
||||
version: "1.0.0"
|
||||
signature: "def render_automatic_eda(db_path: str, table: str, backend: str = \"duckdb\", sample: int = 5000, run_models: bool = True, run_series: bool = True, run_llm: bool = False, out_dir: str = \"reports\", basename: str = None, ctx_extra: dict = None) -> dict"
|
||||
description: "Informe AutomaticEDA COMPLETO one-shot de una tabla DuckDB/PostgreSQL: perfila con profile_table, construye el ctx con los datos crudos (build_eda_render_ctx: raw_numeric para modelos/geo, timeseries_raw para series, geo_points para el mapa, db_path/table para la agregacion push-down) y emite PDF (A5 movil) Y PPTX (16:9) del mismo documento por capitulos, con los 11 capitulos POBLADOS de verdad (clusters pintados sobre el PCA, evolucion temporal, mapa geografico y tablas de agregacion), no degradados. Devuelve las rutas de PDF/PPTX y el manifiesto de versiones por capitulo."
|
||||
tags: [eda, duckdb, postgres, profiling, pipeline, dataops, report, pdf, pptx]
|
||||
uses_functions:
|
||||
- profile_table_py_pipelines
|
||||
- build_eda_render_ctx_py_datascience
|
||||
- render_automatic_eda_pdf_py_datascience
|
||||
- render_automatic_eda_pptx_py_datascience
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
tested: true
|
||||
tests:
|
||||
- "render end-to-end sobre DuckDB sintetico con categoricas + fecha + lat/lon emite PDF y PPTX con paginas/slides"
|
||||
test_file_path: "python/functions/pipelines/render_automatic_eda_test.py"
|
||||
file_path: "python/functions/pipelines/render_automatic_eda.py"
|
||||
params:
|
||||
- name: db_path
|
||||
desc: "Ruta al archivo DuckDB (read-only, debe existir) o DSN PostgreSQL si backend='postgres'."
|
||||
- name: table
|
||||
desc: "Nombre de la tabla a perfilar e informar."
|
||||
- name: backend
|
||||
desc: "'duckdb' (default) o 'postgres'. Selecciona el motor de perfilado y muestreo."
|
||||
- name: sample
|
||||
desc: "Maximo de filas/valores muestreados por columna para el perfil y para los datos crudos del ctx (LIMIT). Default 5000."
|
||||
- name: run_models
|
||||
desc: "Si True (default) corre los modelos baratos (PCA/KMeans/IsolationForest/normalidad); necesario para que el capitulo modelos pinte los clusters sobre el plano PCA."
|
||||
- name: run_series
|
||||
desc: "Si True (default) calcula el analisis de serie temporal por columna numerica; necesario para el analisis del capitulo timeseries (la grafica de evolucion sale de los datos crudos del ctx aunque sea False)."
|
||||
- name: run_llm
|
||||
desc: "Si True (default False) hace la interpretacion LLM del perfil y ACTIVA la narrativa LLM de los capitulos modelos/geospatial/agregacion (titulos de segmento, descripcion de zona, seleccion de agregaciones). Con False usan su derivacion cuantitativa sin red."
|
||||
- name: out_dir
|
||||
desc: "Directorio de salida (se crea si no existe). Default 'reports'."
|
||||
- name: basename
|
||||
desc: "Nombre base de los archivos sin extension. Default 'aeda_<table>_<timestamp>'."
|
||||
- name: ctx_extra
|
||||
desc: "Dict opcional con claves de presentacion/contexto extra que se mezclan en el ctx (dataset_name, description, source_origin, ...); no pisan las claves de datos calculadas por build_eda_render_ctx."
|
||||
output: "dict {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>} o {status:'error', error:str} (dict-no-throw)."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from pipelines.render_automatic_eda import render_automatic_eda
|
||||
|
||||
# Tabla DuckDB con categoricas + fecha + numericas: informe completo a reports/.
|
||||
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas",
|
||||
run_models=True, run_series=True, out_dir="reports")
|
||||
print(r["status"], r["pdf_path"], r["pptx_path"], r["n_pages"], r["n_slides"])
|
||||
# ok reports/aeda_ventas_20260630-120500.pdf reports/aeda_ventas_20260630-120500.pptx 14 16
|
||||
|
||||
# Con narrativa LLM (titulos de segmento, descripcion geografica, etc.):
|
||||
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas", run_llm=True)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras el informe AutomaticEDA COMPLETO (PDF + PPTX) de una tabla en una
|
||||
sola llamada, con los capitulos de modelos, series, geoespacial y agregacion ya
|
||||
poblados (no degradados). Es el reemplazo de "perfila + monta el ctx a mano +
|
||||
llama a los dos renderers": este pipeline orquesta `profile_table` ->
|
||||
`build_eda_render_ctx` -> `render_automatic_eda_pdf`/`_pptx`. Usalo como
|
||||
entregable para compartir un EDA, o como el motor detras de `profile_table(
|
||||
emit_automatic=True)` y del skill `/eda`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: ESCRIBE el PDF, el PPTX y `automatic_eda_manifest.json` en `out_dir`.
|
||||
- `db_path` debe existir: DuckDB read-only no crea la base.
|
||||
- `run_models=True` y `run_series=True` por defecto encarecen el perfil (PCA/
|
||||
KMeans/IsolationForest + ADF/KPSS/STL por columna). Para un informe mas barato
|
||||
ponlos a False: los capitulos modelos/timeseries se omiten o se reducen, pero
|
||||
el resto del informe sale igual.
|
||||
- `run_llm=True` hace llamadas de red (interpretacion del perfil + narrativa por
|
||||
capitulo). Sin red, dejalo en False: los capitulos siguen completos con su
|
||||
derivacion cuantitativa (titulos de segmento derivados, nota geografica
|
||||
derivada, seleccion de agregaciones cuantitativa).
|
||||
- El PPTX requiere `python-pptx`; si no esta instalado, `pptx_path` sera None y
|
||||
`pptx_note` lo explica (el PDF se emite igual).
|
||||
- Los datos crudos del ctx se muestrean con `sample` (LIMIT), no se trae la tabla
|
||||
entera a RAM; con tablas enormes sube `sample` si quieres mas representatividad
|
||||
(coste: mas memoria).
|
||||
@@ -0,0 +1,157 @@
|
||||
"""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,
|
||||
)
|
||||
from pipelines.profile_table import profile_table
|
||||
|
||||
# Tokens de almacenamiento por backend (para la portada del informe).
|
||||
_STORAGE = {"duckdb": "DuckDB", "postgres": "PostgreSQL"}
|
||||
|
||||
|
||||
def render_automatic_eda(
|
||||
db_path: str,
|
||||
table: str,
|
||||
backend: str = "duckdb",
|
||||
sample: int = 5000,
|
||||
run_models: bool = True,
|
||||
run_series: bool = True,
|
||||
run_llm: bool = False,
|
||||
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 5000.
|
||||
run_models: si True (default) corre los modelos baratos
|
||||
(PCA/KMeans/IsolationForest/normalidad). Necesario para que el
|
||||
capítulo `modelos` pinte los clusters sobre el plano PCA.
|
||||
run_series: si True (default) 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).
|
||||
run_llm: si True (default False) 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).
|
||||
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:
|
||||
# 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=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,
|
||||
)
|
||||
|
||||
# 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)}
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Test del pipeline render_automatic_eda — EDA completo a PDF + PPTX.
|
||||
|
||||
Self-contained: crea un DuckDB temporal pequeño con categóricas + fecha + lat/lon
|
||||
+ varias numéricas, corre el pipeline (sin LLM) y verifica que emite PDF y PPTX
|
||||
con páginas/slides, manifest, y que los capítulos dependientes de ctx quedan
|
||||
POBLADOS (sin la nota de degradación).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_FUNCTIONS = os.path.abspath(os.path.join(_HERE, "..", "..")) # python/functions
|
||||
if _FUNCTIONS not in sys.path:
|
||||
sys.path.insert(0, _FUNCTIONS)
|
||||
|
||||
import duckdb # noqa: E402
|
||||
|
||||
from pipelines.render_automatic_eda import render_automatic_eda # noqa: E402
|
||||
|
||||
|
||||
def _make_db(path):
|
||||
con = duckdb.connect(path)
|
||||
con.execute(
|
||||
"CREATE TABLE sales (d DATE, region VARCHAR, channel VARCHAR, "
|
||||
"amount DOUBLE, units INTEGER, lat DOUBLE, lon DOUBLE)"
|
||||
)
|
||||
from datetime import date, timedelta
|
||||
|
||||
regions = ["norte", "sur", "este"]
|
||||
channels = ["web", "tienda"]
|
||||
centers = {"norte": (43.0, -3.0), "sur": (37.0, -5.0), "este": (39.5, -0.4)}
|
||||
rows = []
|
||||
d0 = date(2024, 1, 1)
|
||||
for i in range(180):
|
||||
r = regions[i % 3]
|
||||
ch = channels[i % 2]
|
||||
clat, clon = centers[r]
|
||||
rows.append((
|
||||
d0 + timedelta(days=i), r, ch,
|
||||
round(100 + (i % 7) * 13.5 + (5 if ch == "web" else 0), 2),
|
||||
10 + (i % 5),
|
||||
round(clat + (i % 3) * 0.1, 4),
|
||||
round(clon + (i % 4) * 0.1, 4),
|
||||
))
|
||||
con.executemany("INSERT INTO sales VALUES (?,?,?,?,?,?,?)", rows)
|
||||
con.close()
|
||||
|
||||
|
||||
def test_pipeline_emits_pdf_and_pptx_with_chapters(tmp_path):
|
||||
db = str(tmp_path / "sales.duckdb")
|
||||
_make_db(db)
|
||||
out = str(tmp_path / "out")
|
||||
|
||||
r = render_automatic_eda(db, "sales", run_models=True, run_series=True,
|
||||
run_llm=False, out_dir=out, basename="test_sales")
|
||||
assert r["status"] == "ok", r.get("error")
|
||||
|
||||
# Both formats produced.
|
||||
assert r["pdf_path"] and os.path.exists(r["pdf_path"])
|
||||
assert r["pptx_path"] and os.path.exists(r["pptx_path"])
|
||||
assert (r["n_pages"] or 0) > 0
|
||||
assert (r["n_slides"] or 0) > 0
|
||||
# Per-chapter manifest written next to the output.
|
||||
assert r["manifest_path"] and os.path.exists(r["manifest_path"])
|
||||
|
||||
|
||||
def test_pipeline_chapters_populated_not_degraded(tmp_path):
|
||||
"""The 4 ctx-dependent chapters build with real data (no degradation note)."""
|
||||
import json
|
||||
|
||||
db = str(tmp_path / "sales.duckdb")
|
||||
_make_db(db)
|
||||
out = str(tmp_path / "out")
|
||||
r = render_automatic_eda(db, "sales", run_models=True, run_series=True,
|
||||
run_llm=False, out_dir=out, basename="t2")
|
||||
assert r["status"] == "ok"
|
||||
|
||||
# The manifest lists the ctx-dependent chapters as actually rendered.
|
||||
with open(r["manifest_path"], encoding="utf-8") as fh:
|
||||
man = json.load(fh)
|
||||
chapters = man.get("chapters") or {}
|
||||
for cid in ("modelos", "timeseries", "geospatial", "agregacion"):
|
||||
assert cid in chapters, f"capítulo {cid} ausente del manifest: {list(chapters)}"
|
||||
|
||||
|
||||
def test_pipeline_bad_db_degrades_without_raising(tmp_path):
|
||||
r = render_automatic_eda(str(tmp_path / "nope.duckdb"), "ghost",
|
||||
out_dir=str(tmp_path / "o"))
|
||||
assert r["status"] == "error"
|
||||
assert "error" in r
|
||||
Reference in New Issue
Block a user