54a9ab70c7
Permite renderizar un SUBCONJUNTO de capítulos del informe AutomaticEDA (only_chapters=[...]) para iterar/testear un capítulo concreto sin generar el documento entero, garantizando que el capítulo pedido SIEMPRE llegue poblado. - Nuevo módulo automatic_eda/chapter_deps.py: mapa central CHAPTER_DEPS (fuente de verdad) que declara, por capítulo de CHAPTER_ORDER, qué flags de cómputo (run_models/run_series/run_llm) y qué piezas de ctx (raw_numeric, timeseries_raw, geo_points, head_rows, db_path/table) necesita para no salir degradado. Helpers puros: resolve_requirements, resolve_profile_flags, needs_render_ctx, resolve_ctx_data_keys, validate_chapter_ids. - build_document(profile, ctx, only=None): parámetro only opcional que restringe el cuerpo a esos capítulos (portada primera + glosario última siempre). Lee la clave reservada ctx['_only_chapters'] cuando only es None, para propagar la selección a través de los renderers sin modificarlos. Retrocompatible. - render_automatic_eda(..., only_chapters=None): valida los ids (error claro dict-no-throw), resuelve las dependencias activando el cómputo necesario aunque el caller no lo pidiera (un flag explícito siempre prima) y construyendo solo las piezas de ctx que los capítulos pedidos leen (salta build_eda_render_ctx entero si ninguno necesita datos crudos). only_chapters=None produce el documento completo idéntico al de hoy. - Tests: chapter_deps_test.py (resolución pura), build_document_only_test.py (filtro), render_automatic_eda_only_test.py (golden con DuckDB: outliers suelto con IsolationForest poblado por resolución; timeseries activa run_series; eficiencia geospatial sin modelos; edge cases). - .md del pipeline: documenta only_chapters + emit_md; version 1.1.0 -> 1.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
206 lines
9.7 KiB
Python
206 lines
9.7 KiB
Python
"""chapter_deps — mapa central de dependencias de cómputo por capítulo del EDA.
|
|
|
|
Fuente de verdad ÚNICA de qué necesita cada capítulo de ``CHAPTER_ORDER`` para
|
|
computarse COMPLETO (sin caer en su rama degradada "datos insuficientes"). Lo
|
|
consume el pipeline ``render_automatic_eda`` cuando se le pide renderizar un
|
|
SUBCONJUNTO de capítulos (kwarg ``only_chapters``): antes de perfilar, resuelve
|
|
los requisitos de los capítulos pedidos y activa SOLO el cómputo que esos
|
|
capítulos necesitan, de modo que un capítulo suelto siempre llegue poblado y a la
|
|
vez no se malgaste CPU/LLM en piezas que ningún capítulo pedido usa.
|
|
|
|
Diseño: el mapa es CENTRAL (este módulo), NO una constante por capítulo. Así se
|
|
evita tocar los ``chapters/<id>.py`` (cada agente es dueño de su capítulo) y se
|
|
elimina el riesgo de colisión entre ramas. Si un capítulo cambia lo que lee del
|
|
``profile``/``ctx``, se actualiza ESTE mapa — es donde el motor mira.
|
|
|
|
Dos clases de dependencia, derivadas inspeccionando qué lee cada capítulo:
|
|
|
|
- ``profile_flags``: flags de coste de ``profile_table`` que hay que ACTIVAR
|
|
para que el ``profile`` traiga el bloque que el capítulo lee. Son los caros:
|
|
* ``run_models`` -> ``profile['models']`` (KMeans/IsolationForest/PCA).
|
|
Lo leen ``outliers`` (fallback del multivariante) y ``modelos``.
|
|
* ``run_series`` -> ``profile['series']`` (análisis de serie temporal).
|
|
Lo lee ``timeseries``.
|
|
* ``run_llm`` -> ``profile['llm']`` (interpretación del modelo).
|
|
Lo lee ``analisis_llm``.
|
|
|
|
- ``ctx``: etiquetas de las piezas de DATOS CRUDOS que construye
|
|
``build_eda_render_ctx`` y que el capítulo lee del ``ctx``. Si la lista está
|
|
vacía, el capítulo no necesita datos crudos y el pipeline puede saltarse
|
|
``build_eda_render_ctx`` por completo cuando ningún capítulo pedido los pide.
|
|
Etiquetas y claves reales que mapean (ver ``CTX_LABEL_TO_KEYS``):
|
|
* ``head_rows`` -> ``ctx['head_rows']`` (overview: df.head real).
|
|
* ``raw_numeric`` -> ``ctx['raw_numeric']`` (outliers/modelos/
|
|
correlacion/missingness/geospatial: muestra numérica alineada por fila).
|
|
* ``timeseries_raw`` -> ``ctx['timeseries_raw']`` (timeseries: serie cruda).
|
|
* ``geo_points`` -> ``ctx['geo_points']`` (+ ``raw_numeric``)
|
|
(geospatial: lat/lon).
|
|
* ``db_path_table`` -> ``ctx['db_path']`` + ``ctx['table']`` (agregacion/
|
|
text_distr/missingness/relaciones: push-down de queries propias).
|
|
|
|
``portada`` y ``glosario`` NO son opcionales: el pipeline los incluye SIEMPRE
|
|
(la portada resume el documento y el glosario es el destino de los términos
|
|
clicables), así que aquí se declaran sin requisitos de cómputo.
|
|
|
|
Todas las funciones de este módulo son PURAS (no I/O, deterministas): se prestan
|
|
a test unitario directo.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
# Mapa central. Una entrada por id de CHAPTER_ORDER. ``profile_flags`` lista los
|
|
# flags de coste a activar; ``ctx`` las etiquetas de datos crudos que lee. Las
|
|
# claves vacías significan "no necesita ese tipo de dependencia".
|
|
CHAPTER_DEPS = {
|
|
# Portada y glosario: SIEMPRE presentes, sin cómputo propio (la portada lee
|
|
# el document_summary que arma build_document; el glosario lee los términos
|
|
# que el resto registró). Se declaran para que el mapa cubra CHAPTER_ORDER
|
|
# entero y la validación los reconozca.
|
|
"portada": {"profile_flags": [], "ctx": []},
|
|
"overview": {"profile_flags": [], "ctx": ["head_rows"]},
|
|
"analisis_llm": {"profile_flags": ["run_llm"], "ctx": []},
|
|
"num_distr": {"profile_flags": [], "ctx": []},
|
|
"cat_distr": {"profile_flags": [], "ctx": []},
|
|
# text_distr empuja su propia query de texto (no usa raw_numeric); necesita
|
|
# db_path/table en el ctx para hacerlo.
|
|
"text_distr": {"profile_flags": [], "ctx": ["db_path_table"]},
|
|
"calidad": {"profile_flags": [], "ctx": []},
|
|
# missingness lee la muestra numérica cruda (co-ocurrencia de ausencias) y
|
|
# puede empujar una query de patrón de nulos con db_path/table.
|
|
"missingness": {"profile_flags": [], "ctx": ["raw_numeric", "db_path_table"]},
|
|
# outliers corre IsolationForest EN VIVO sobre ctx['raw_numeric']; run_models
|
|
# asegura además el fallback profile['models']['outliers'] si el ctx faltara.
|
|
"outliers": {"profile_flags": ["run_models"], "ctx": ["raw_numeric"]},
|
|
"correlacion": {"profile_flags": [], "ctx": ["raw_numeric"]},
|
|
"relaciones": {"profile_flags": [], "ctx": ["db_path_table"]},
|
|
"modelos": {"profile_flags": ["run_models"], "ctx": ["raw_numeric"]},
|
|
"timeseries": {"profile_flags": ["run_series"], "ctx": ["timeseries_raw"]},
|
|
"geospatial": {"profile_flags": [], "ctx": ["geo_points", "raw_numeric"]},
|
|
"agregacion": {"profile_flags": [], "ctx": ["db_path_table"]},
|
|
"glosario": {"profile_flags": [], "ctx": []},
|
|
}
|
|
|
|
# Capítulos que el documento incluye SIEMPRE, independientemente de only_chapters.
|
|
ALWAYS_PRESENT = ("portada", "glosario")
|
|
|
|
# Flags de coste reconocidos (el orden no importa; se devuelven como set).
|
|
KNOWN_PROFILE_FLAGS = ("run_models", "run_series", "run_llm")
|
|
|
|
# Mapeo de cada etiqueta de ctx a las claves REALES que produce
|
|
# build_eda_render_ctx. ``db_path_table`` es especial: db_path/table siempre se
|
|
# ponen para un backend válido y son inofensivos, por eso no se podan nunca (no
|
|
# aparecen en DATA_CTX_KEYS). El resto (head_rows/raw_numeric/timeseries_raw/
|
|
# geo_points) son las piezas de datos podables.
|
|
CTX_LABEL_TO_KEYS = {
|
|
"head_rows": {"head_rows"},
|
|
"raw_numeric": {"raw_numeric"},
|
|
"timeseries_raw": {"timeseries_raw"},
|
|
"geo_points": {"geo_points", "raw_numeric"},
|
|
"db_path_table": set(), # db_path/table siempre presentes; nunca se podan.
|
|
}
|
|
|
|
# Claves de datos crudos del ctx que se pueden podar cuando ningún capítulo
|
|
# pedido las necesita (las que cuestan muestreo). db_path/table NO entran aquí.
|
|
DATA_CTX_KEYS = ("head_rows", "raw_numeric", "timeseries_raw", "geo_points")
|
|
|
|
|
|
def _as_id_list(chapter_ids):
|
|
"""Normaliza la entrada a una lista de ids string, defensiva. None -> []."""
|
|
if chapter_ids is None:
|
|
return []
|
|
if isinstance(chapter_ids, str):
|
|
return [chapter_ids]
|
|
return [c for c in chapter_ids if isinstance(c, str)]
|
|
|
|
|
|
def validate_chapter_ids(chapter_ids, order):
|
|
"""Separa los ids pedidos en válidos y desconocidos respecto a ``order``.
|
|
|
|
Args:
|
|
chapter_ids: lista (o str) de ids de capítulo pedidos.
|
|
order: lista canónica de ids válidos (CHAPTER_ORDER).
|
|
|
|
Returns:
|
|
dict ``{"valid": [...], "unknown": [...]}`` preservando el orden de
|
|
aparición de la entrada. Función pura.
|
|
"""
|
|
valid_set = set(order or [])
|
|
valid, unknown = [], []
|
|
for cid in _as_id_list(chapter_ids):
|
|
(valid if cid in valid_set else unknown).append(cid)
|
|
return {"valid": valid, "unknown": unknown}
|
|
|
|
|
|
def resolve_requirements(chapter_ids):
|
|
"""Une los requisitos de cómputo de los capítulos pedidos.
|
|
|
|
Es el corazón de la resolución de dependencias: dado el subconjunto de
|
|
capítulos a renderizar, devuelve TODO lo que hay que activar/construir para
|
|
que esos capítulos lleguen COMPLETOS, y solo eso.
|
|
|
|
Los capítulos ``ALWAYS_PRESENT`` (portada/glosario) se añaden implícitamente
|
|
porque el pipeline siempre los incluye; como no tienen requisitos, no alteran
|
|
el resultado, pero se contemplan para que el conjunto sea coherente.
|
|
|
|
Args:
|
|
chapter_ids: lista (o str) de ids de capítulo. Ids desconocidos se
|
|
ignoran silenciosamente (la validación estricta es de quien llama).
|
|
None o lista vacía -> requisitos vacíos.
|
|
|
|
Returns:
|
|
dict ``{"profile_flags": set[str], "ctx_keys": set[str]}`` donde
|
|
``ctx_keys`` son las ETIQUETAS de ctx (no las claves reales). Función
|
|
pura.
|
|
"""
|
|
ids = set(_as_id_list(chapter_ids)) | set(ALWAYS_PRESENT)
|
|
profile_flags = set()
|
|
ctx_keys = set()
|
|
for cid in ids:
|
|
dep = CHAPTER_DEPS.get(cid)
|
|
if not isinstance(dep, dict):
|
|
continue
|
|
for f in dep.get("profile_flags", []) or []:
|
|
if f in KNOWN_PROFILE_FLAGS:
|
|
profile_flags.add(f)
|
|
for k in dep.get("ctx", []) or []:
|
|
ctx_keys.add(k)
|
|
return {"profile_flags": profile_flags, "ctx_keys": ctx_keys}
|
|
|
|
|
|
def resolve_profile_flags(chapter_ids):
|
|
"""Atajo: solo el set de profile_flags a activar para los capítulos pedidos.
|
|
|
|
Función pura. Devuelve un set ⊆ KNOWN_PROFILE_FLAGS.
|
|
"""
|
|
return resolve_requirements(chapter_ids)["profile_flags"]
|
|
|
|
|
|
def needs_render_ctx(chapter_ids):
|
|
"""True si algún capítulo pedido necesita datos crudos del ctx.
|
|
|
|
Cuando es False, el pipeline puede saltarse ``build_eda_render_ctx`` entero
|
|
(ahorro real de CPU/I/O): los capítulos pedidos no leen ninguna pieza de
|
|
datos crudos. Función pura.
|
|
"""
|
|
return bool(resolve_requirements(chapter_ids)["ctx_keys"])
|
|
|
|
|
|
def resolve_ctx_data_keys(chapter_ids):
|
|
"""Claves REALES de datos del ctx a CONSERVAR para los capítulos pedidos.
|
|
|
|
Traduce las etiquetas de ctx a las claves concretas que produce
|
|
``build_eda_render_ctx`` (head_rows/raw_numeric/timeseries_raw/geo_points).
|
|
El pipeline poda del ctx las claves de datos que NO estén en este set, para
|
|
que un capítulo suelto no arrastre piezas de datos que no usa. db_path/table
|
|
nunca se podan (no aparecen aquí). Función pura.
|
|
|
|
Returns:
|
|
set[str] subconjunto de DATA_CTX_KEYS.
|
|
"""
|
|
req = resolve_requirements(chapter_ids)
|
|
keep = set()
|
|
for label in req["ctx_keys"]:
|
|
keep |= CTX_LABEL_TO_KEYS.get(label, set())
|
|
# Solo claves de datos podables (db_path/table se gestionan aparte).
|
|
return {k for k in keep if k in DATA_CTX_KEYS}
|