merge: 4b flag profile_level lite/standard/full en render_automatic_eda (lite 4.5s vs full 39.3s, verificado met)

This commit is contained in:
2026-06-30 18:29:44 +02:00
4 changed files with 334 additions and 34 deletions
+3 -1
View File
@@ -27,6 +27,7 @@ Página madre del grupo: `docs/capabilities/eda.md` (léela primero para cargar
- `--series``run_series=True` (estacionariedad ADF+KPSS, ACF/PACF, STL, retornos por columna numérica).
- `--pdf``emit_pdf=True` (PDF A5 legacy de `render_eda_pdf`, legible en móvil).
- `--legacy-only` → emite SOLO el PDF legacy (sin AutomaticEDA), para casos en que solo se quiera el PDF rápido.
- `--lite` / `--bajo-consumo``render_automatic_eda(profile_level="lite")`: EDA barato y rápido (CI, vistazo previo, máquina sin GPU/red). Apaga LLM y serie temporal y limita los modelos a **PCA + normalidad** (sin KMeans ni IsolationForest, lo caro en CPU), con `sample` reducido. `--full``profile_level="full"` (standard + narrativa LLM). Por defecto `profile_level="standard"` (comportamiento histórico). Un flag explícito (`--llm`, `--models`, ...) prima sobre el preset.
Por defecto, **un EDA completo emite SIEMPRE el informe AutomaticEDA en sus dos formatos: PDF (A5 móvil) Y PPTX (16:9 para compartir)** con los 11 capítulos poblados (portada, overview, distribuciones, calidad, correlaciones, modelos, series, geoespacial, agregación, interpretación LLM). Usa el pipeline `render_automatic_eda` (o `profile_table(emit_automatic=True)`), que activa `run_models` y `run_series` para que los capítulos de modelos/series/geoespacial/agregación salgan poblados. Deja `run_llm` para cuando el usuario lo pida o interese la interpretación semántica + narrativa por capítulo (es la única parte que gasta tokens del modelo).
@@ -50,7 +51,8 @@ from pipelines.render_automatic_eda import render_automatic_eda
# tablas de agregación). run_llm=True añade la narrativa LLM por capítulo.
r = render_automatic_eda(
"/ruta/datos.duckdb", "ventas",
run_models=True, run_series=True, run_llm=False, out_dir="reports",
profile_level="standard", # "lite" = bajo consumo CPU/LLM; "full" = + narrativa LLM
out_dir="reports",
)
print("status:", r["status"])
print("pdf: ", r["pdf_path"], "(", r["n_pages"], "págs )")
@@ -4,9 +4,9 @@ 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."
version: "1.1.0"
signature: "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"
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. El parametro profile_level es un preset de consumo CPU/LLM (lite/standard/full) que mapea a los flags run_models/run_series/run_llm/sample; un flag explicito siempre prima sobre el preset. lite=bajo consumo (sin LLM, sin serie, modelos solo PCA+normalidad sin KMeans/IsolationForest, sample reducido); standard=comportamiento historico; full=standard+narrativa LLM. 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
@@ -31,13 +31,15 @@ params:
- 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."
desc: "Maximo 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). Un valor explicito prima sobre el preset."
- 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."
desc: "Corre los modelos baratos (PCA/KMeans/IsolationForest/normalidad); necesario para que el capitulo 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. Un valor explicito prima sobre el preset."
- 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)."
desc: "Calcula el analisis de serie temporal por columna numerica; necesario para el analisis del capitulo timeseries. Default None => lo fija el preset (standard/full=True, lite=False). Un valor explicito prima sobre el preset."
- 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."
desc: "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. Default None => lo fija el preset (full=True, lite/standard=False). Un valor explicito prima sobre el preset."
- name: profile_level
desc: "Preset de consumo CPU/LLM (default 'standard'). Mapea a defaults de run_models/run_series/run_llm/sample; un flag explicito SIEMPRE prima. 'lite'=bajo consumo (run_llm=False, run_series=False, sample=2000, modelos solo PCA+normalidad sin KMeans/IsolationForest); 'standard'=comportamiento historico (modelos completos, serie, sin LLM); 'full'=standard+narrativa LLM. Un nivel desconocido cae a 'standard'."
- name: out_dir
desc: "Directorio de salida (se crea si no existe). Default 'reports'."
- name: basename
@@ -52,14 +54,21 @@ output: "dict {status:'ok', pdf_path:str, pptx_path:str, manifest_path:str|None,
```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")
# Informe completo a reports/ (standard = comportamiento por defecto historico).
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas", 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
# ok reports/aeda_ventas_20260630-120500.pdf reports/aeda_ventas_20260630-120500.pptx 37 39
# Con narrativa LLM (titulos de segmento, descripcion geografica, etc.):
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas", run_llm=True)
# Bajo consumo (CPU/LLM): vistazo rapido y barato — sin LLM, sin serie, modelos
# solo PCA + normalidad (sin KMeans/IsolationForest), sample reducido.
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas", profile_level="lite")
# Maximo: standard + narrativa LLM por capitulo (titulos de segmento, etc.).
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas", profile_level="full")
# Precedencia: el flag explicito SIEMPRE prima sobre el preset. lite pero con LLM:
r = render_automatic_eda("/tmp/ventas.duckdb", "ventas",
profile_level="lite", run_llm=True) # el LLM SI se ejecuta
```
## Cuando usarla
@@ -72,20 +81,41 @@ llama a los dos renderers": este pipeline orquesta `profile_table` ->
entregable para compartir un EDA, o como el motor detras de `profile_table(
emit_automatic=True)` y del skill `/eda`.
Para un EDA **barato/rapido** (CI, vistazo previo, maquina sin GPU o sin red) usa
`profile_level="lite"`: evita KMeans + IsolationForest (lo caro en CPU), la serie
temporal y el LLM. Para el **maximo** con interpretacion narrativa por capitulo,
`profile_level="full"`. El default `"standard"` mantiene el comportamiento previo.
## 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).
- **Precedencia de flags vs preset**: `profile_level` solo fija los DEFAULTS de
`run_models`/`run_series`/`run_llm`/`sample` (los que quedan en None). Cualquiera
de esos flags pasado explicito gana al preset. Ej: `profile_level="lite",
run_llm=True` ejecuta el LLM pese a que lite lo apaga por defecto.
- **lite y la seleccion de features de modelo**: en lite los modelos (PCA +
normalidad) corren sobre la muestra numerica cruda (`ctx['raw_numeric']`), sin la
poda fina de features que aplica el modo standard (que excluye ids enteros y
columnas de baja cardinalidad antes de PCA/KMeans). Es el coste de mantener el
cableado minimo y barato; para el analisis fino de modelos usa standard/full.
- `profile_level="standard"`/`"full"` corren PCA/KMeans/IsolationForest +
ADF/KPSS/STL por columna (caro). Para un informe mas barato usa `"lite"` (o pon
los flags a False a mano): los capitulos modelos/timeseries se reducen pero el
resto del informe sale igual.
- `run_llm=True` (preset full o flag explicito) hace llamadas de red
(interpretacion del perfil + narrativa por capitulo). Sin red, usa lite/standard:
los capitulos siguen completos con su derivacion 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).
## Capability growth log
- v1.1.0 (2026-06-30) — anade el parametro `profile_level` (lite/standard/full),
preset de consumo CPU/LLM que mapea a los flags run_models/run_series/run_llm/
sample. lite limita los modelos a PCA+normalidad (cableado a run_eda_models con
run_kmeans=False/run_isolation=False) y apaga LLM/serie. Cambio aditivo y
retro-compatible: sin profile_level el comportamiento es identico al de v1.0.0.
@@ -34,21 +34,62 @@ 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 = 5000,
run_models: bool = True,
run_series: bool = True,
run_llm: bool = False,
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,
@@ -60,19 +101,39 @@ def render_automatic_eda(
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
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.
run_series: si True (default) calcula el análisis de serie temporal por
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).
run_llm: si True (default False) hace la interpretación LLM del perfil y
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).
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>".
@@ -90,6 +151,24 @@ def render_automatic_eda(
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(
@@ -97,7 +176,7 @@ def render_automatic_eda(
table,
backend=backend,
sample=sample,
run_models=run_models,
run_models=pt_run_models,
run_llm=run_llm,
run_series=run_series,
emit_pdf=False,
@@ -131,6 +210,28 @@ def render_automatic_eda(
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")
@@ -89,3 +89,170 @@ def test_pipeline_bad_db_degrades_without_raising(tmp_path):
out_dir=str(tmp_path / "o"))
assert r["status"] == "error"
assert "error" in r
# --------------------------------------------------------------------------- #
# profile_level: preset de bajo consumo CPU/LLM.
# --------------------------------------------------------------------------- #
def _make_db_models(path):
"""DB con >=2 numéricas continuas (alta cardinalidad, 3 clusters gaussianos).
El DB `sales` de _make_db solo deja UNA columna de modelo tras la selección de
features (units es baja cardinalidad, lat/lon discretizadas), insuficiente para
PCA/KMeans/IsolationForest (necesitan >=2). Este DB sí tiene 3 numéricas
continuas con estructura de clusters para que el modo completo ejecute los
multivariantes.
"""
import random
from datetime import date, timedelta
con = duckdb.connect(path)
con.execute(
"CREATE TABLE pts (d DATE, grp VARCHAR, x1 DOUBLE, x2 DOUBLE, x3 DOUBLE)"
)
random.seed(42)
centers = [(0.0, 0.0, 0.0), (10.0, 10.0, 10.0), (20.0, 5.0, 15.0)]
d0 = date(2024, 1, 1)
rows = []
for i in range(150):
cx, cy, cz = centers[i % 3]
rows.append((
d0 + timedelta(days=i), f"g{i % 3}",
round(cx + random.gauss(0, 1.0), 4),
round(cy + random.gauss(0, 1.0), 4),
round(cz + random.gauss(0, 1.0), 4),
))
con.executemany("INSERT INTO pts VALUES (?,?,?,?,?)", rows)
con.close()
def test_profile_level_lite_skips_expensive_models(tmp_path):
"""lite: el bloque models trae PCA + normalidad pero NO KMeans/IsolationForest.
Demuestra (DoD bajo consumo) que el camino lite no ejecuta los modelos caros
en CPU ni la capa LLM ni la serie temporal: prof['models'] queda con pca y
normality poblados y kmeans/outliers a None, prof['llm'] y prof['series'] a
None, y el capítulo `modelos` se renderiza igualmente (con PCA, sin clusters).
"""
import json
db = str(tmp_path / "pts.duckdb")
_make_db_models(db)
out = str(tmp_path / "out")
r = render_automatic_eda(db, "pts", profile_level="lite",
out_dir=out, basename="lite")
assert r["status"] == "ok", r.get("error")
models = (r["profile"] or {}).get("models") or {}
assert models.get("pca") is not None, "lite debe traer PCA"
assert models.get("normality") is not None, "lite debe traer normalidad"
assert models.get("kmeans") is None, "lite NO debe ejecutar KMeans"
assert models.get("outliers") is None, "lite NO debe ejecutar IsolationForest"
assert (r["profile"] or {}).get("llm") is None, "lite NO debe llamar al LLM"
assert (r["profile"] or {}).get("series") is None, "lite NO debe calcular serie"
# El capítulo modelos sigue presente (lo puebla el PCA), sin clusters KMeans.
with open(r["manifest_path"], encoding="utf-8") as fh:
man = json.load(fh)
assert "modelos" in (man.get("chapters") or {})
def test_profile_level_standard_runs_full_models(tmp_path):
"""standard (default): modelos completos (KMeans + IsolationForest) y serie."""
db = str(tmp_path / "pts.duckdb")
_make_db_models(db)
out = str(tmp_path / "out")
r = render_automatic_eda(db, "pts", profile_level="standard",
out_dir=out, basename="std")
assert r["status"] == "ok", r.get("error")
models = (r["profile"] or {}).get("models") or {}
assert models.get("pca") is not None
assert models.get("kmeans") is not None, "standard debe ejecutar KMeans"
assert models.get("outliers") is not None, "standard debe ejecutar IsolationForest"
assert (r["profile"] or {}).get("series") is not None, "standard calcula serie"
def _patch_pipeline_internals(monkeypatch, captured):
"""Stub de las dependencias del pipeline para tests de resolución de flags.
Sustituye profile_table / build_eda_render_ctx / renderers por stubs rápidos
sin red ni matplotlib, capturando los kwargs con los que se invocan. Permite
verificar la PRECEDENCIA flag-explícito-sobre-preset sin ejecutar el EDA real.
"""
import pipelines.render_automatic_eda as mod
def fake_profile_table(db_path, table, **kw):
captured["run_llm"] = kw.get("run_llm")
captured["run_models"] = kw.get("run_models")
captured["run_series"] = kw.get("run_series")
captured["sample"] = kw.get("sample")
return {"status": "ok", "profile": {"columns": []}}
def fake_ctx(db_path, table, prof, **kw):
captured["base_ctx"] = kw.get("base_ctx")
return {}
monkeypatch.setattr(mod, "profile_table", fake_profile_table)
monkeypatch.setattr(mod, "build_eda_render_ctx", fake_ctx)
monkeypatch.setattr(mod, "render_automatic_eda_pdf",
lambda *a, **k: {"path": "x.pdf", "n_pages": 1,
"manifest_path": "m.json"})
monkeypatch.setattr(mod, "render_automatic_eda_pptx",
lambda *a, **k: {"path": "x.pptx", "n_slides": 1})
def test_explicit_flag_overrides_preset(monkeypatch):
"""Precedencia: profile_level='lite' con run_llm=True explícito → LLM activo.
El flag explícito del caller gana al default del preset. Se verifica tanto en
el flag que llega a profile_table (run_llm=True ⇒ profile_table llamará al
LLM) como en el base_ctx (run_cluster_llm=True ⇒ narrativa LLM por capítulo).
"""
captured = {}
_patch_pipeline_internals(monkeypatch, captured)
captured.clear()
render_automatic_eda("db", "t", profile_level="lite", run_llm=True)
assert captured["run_llm"] is True, "flag explícito debe primar sobre preset lite"
assert (captured["base_ctx"] or {}).get("run_cluster_llm") is True
def test_full_preset_enables_llm(monkeypatch):
"""full: el preset resuelve run_llm=True y activa la narrativa LLM en el ctx."""
captured = {}
_patch_pipeline_internals(monkeypatch, captured)
captured.clear()
render_automatic_eda("db", "t", profile_level="full")
assert captured["run_llm"] is True
assert (captured["base_ctx"] or {}).get("run_cluster_llm") is True
def test_no_profile_level_defaults_to_standard(monkeypatch):
"""Retro-compat: sin profile_level ni flags, el comportamiento es el histórico.
standard = run_models True, run_series True, run_llm False, sample 5000. Es el
mismo default que tenía el pipeline antes de introducir profile_level (cambio
aditivo: las llamadas existentes no cambian de comportamiento).
"""
captured = {}
_patch_pipeline_internals(monkeypatch, captured)
captured.clear()
render_automatic_eda("db", "t") # sin profile_level ni flags de coste
assert captured["run_models"] is True
assert captured["run_series"] is True
assert captured["run_llm"] is False
assert captured["sample"] == 5000
def test_lite_preset_defaults(monkeypatch):
"""lite por defecto: run_llm/run_series False y sample reducido a 2000."""
captured = {}
_patch_pipeline_internals(monkeypatch, captured)
captured.clear()
render_automatic_eda("db", "t", profile_level="lite")
assert captured["run_llm"] is False
assert captured["run_series"] is False
assert captured["sample"] == 2000