Añade docs/automatic_eda_contract.md: documento autoritativo y autosuficiente para que otros agentes escriban capítulos en paralelo (NUM DISTR, CAT DISTR, CALIDAD, CORRELACIÓN, MODELOS, ANÁLISIS LLM, TIMESERIES, GEOSPATIAL, AGREGACIÓN). Cubre el modelo de bloques/capítulo exacto, la firma build_<chapter>(profile, ctx) -> Chapter|None, la declaración de CHAPTER_VERSION, dónde colocar el módulo, cómo se registra el orden del documento, qué claves del profile consume cada capítulo, las claves nuevas que la fase de cálculo debe añadir (head_rows, columns[].examples) y un ejemplo completo del capítulo de referencia OVERVIEW. Enlaza las dos funciones nuevas y el contrato desde docs/capabilities/eda.md y actualiza el recuento del grupo eda en el índice de capabilities. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
AutomaticEDA — contrato de capítulos
Documento autoritativo para escribir capítulos del informe AutomaticEDA. Léelo
entero antes de añadir un capítulo: define el modelo de bloques, la firma del builder,
el versionado, dónde colocar el módulo, cómo se registra en el orden del documento, qué
claves del profile consume cada capítulo y un ejemplo completo de capítulo de
referencia (OVERVIEW).
AutomaticEDA es la capa intermedia entre contenido (lo que un capítulo quiere decir) y formato de salida (PDF móvil + PPTX para compartir). Un mismo documento por capítulos se renderiza a los dos formatos con garantía de no-corte: el texto se envuelve a líneas completas, las tablas largas se parten por filas repitiendo la cabecera, y figuras/imágenes se escalan para caber enteras.
- Código del motor:
python/functions/datascience/automatic_eda/(paquete de soporte). - Funciones públicas del registry (grupo
eda):render_automatic_eda_pdf,render_automatic_eda_pptx. - Sustituye evolutivamente a
render_eda_pdfde forma aditiva (ese sigue activo enprofile_table(emit_pdf=True)).
1. Modelo de documento
Document = list[Chapter]
Chapter = { id: str, title: str, version: str, blocks: list[Block] }
Block = Heading | Markdown | KVTable | DataTable | Figure | Image | Caption | Note
Importa el modelo desde datascience.automatic_eda.model (o
from datascience.automatic_eda import ...). Todos los bloques son dataclasses; los
renderers también aceptan dicts con la clave kind (lectura defensiva: lo no
reconocido se degrada a Note, nunca lanza).
Bloques
| Bloque | Construcción | Qué hace en el render |
|---|---|---|
Heading(text, level=1) |
título de sección, level 1 (grande) … 3 (chico) |
una o varias líneas en negrita; nivel 1 lleva subrayado de acento |
Markdown(text) |
texto markdown ligero | ver subset abajo; nunca corta a media línea |
KVTable(rows, title=None) |
rows = [(clave, valor), ...] |
tabla de 2 columnas etiqueta/valor; el valor se envuelve |
DataTable(header, rows, title=None, note=None) |
header=[...], rows=[[...],...] |
tabla con cabecera; se parte por filas repitiendo cabecera; las celdas largas se envuelven dentro de su columna |
Figure(fig=None, make=None, caption=None, height_in=None) |
una matplotlib.figure.Figure ya construida (fig) o un callable make()->Figure (perezoso) |
se rasteriza y escala para caber entera (nunca recortada) |
Image(path, caption=None, height_in=None) |
ruta a PNG/JPG | se escala para caber entera |
Caption(text) / Note(text) |
texto auxiliar pequeño | pie/nota en gris; Note es además el fallback de lo desconocido |
Subset de markdown soportado (Markdown)
#/##/### → headings; -/* → viñetas; líneas | a | b | consecutivas → una
DataTable; línea en blanco → separación de párrafo; **bold**/__bold__/`code`
→ se quitan los marcadores y se conserva el texto. Todo lo demás se renderiza tal cual.
Garantía: ningún carácter se pierde; lo que no cabe se envuelve o pasa de página/slide.
2. Firma del builder de capítulo (OBLIGATORIA)
Cada capítulo es un módulo python/functions/datascience/automatic_eda/chapters/<id>.py
que expone dos símbolos:
CHAPTER_VERSION = "1.0.0" # semver de generación del capítulo (ver §4)
def build_<id>(profile: dict, ctx: dict) -> "Chapter | None":
"""Construye el capítulo desde el TableProfile y el contexto de presentación.
Devuelve None si el capítulo NO aplica a este dataset (p.ej. timeseries sin
columna fecha). Lee SIEMPRE defensivamente con .get y NUNCA lanza.
"""
- El nombre de la función es exactamente
build_<id>donde<id>es el del módulo y el deCHAPTER_ORDER(§3). Ej.:chapters/num_distr.py→build_num_distr. - Devuelve un
model.Chapter(id, title, version=CHAPTER_VERSION, blocks=[...])oNone. - Un capítulo que devuelve
Noneo cuyosblocksquedan vacíos se omite del documento.
3. Registro y orden del documento
El orden canónico está pre-declarado en
python/functions/datascience/automatic_eda/chapters_registry.py:
CHAPTER_ORDER = [
"portada", "overview", "num_distr", "cat_distr", "calidad", "correlacion",
"modelos", "analisis_llm", "timeseries", "geospatial", "agregacion",
]
build_document(profile, ctx) recorre este orden, importa perezosamente
chapters/<id>.py y llama build_<id>. Para añadir un capítulo NO se edita
chapters_registry.py: basta crear el módulo chapters/<id>.py (con su <id> ya en
CHAPTER_ORDER) y aparecerá automáticamente en su posición. Esto permite que muchos
agentes trabajen en paralelo sin contención: cada uno toca solo su archivo.
Si tu capítulo usa un <id> que aún no está en CHAPTER_ORDER, añádelo en la posición
correcta (única edición compartida; coordínala con el orquestador).
build_document nunca lanza: un capítulo cuyo módulo no existe se salta, y uno que falla
o devuelve None se omite.
4. Versionado por capítulo + manifiesto
CHAPTER_VERSION(semver) identifica la generación del capítulo. Bumpéalo cuando cambies qué/cómo emite el capítulo (no en cada corrida). Se estampa en el pie de cada página/slide:<Título> · v<version>.ENGINE_VERSION(enmodel.py) versiona el motor global.- Al renderizar se escribe
automatic_eda_manifest.jsonjunto a la salida:
{
"engine": "AutomaticEDA",
"engine_version": "1.0.0",
"generated_at": "2026-06-30 12:20:56 UTC",
"chapters": {
"portada": { "version": "1.0.0", "n_pages": 1, "n_slides": 1 },
"overview": { "version": "1.0.0", "n_pages": 2, "n_slides": 2 }
}
}
Llamar a uno o ambos renderers crea/actualiza el manifiesto (read-modify-write defensivo). Esto habilita el seguimiento y la mejora continua por capítulo.
5. ctx — contexto de presentación
ctx lleva metadatos que no están en el TableProfile (lo aporta el caller via
meta['ctx']). Claves convencionales (todas opcionales):
| Clave | Uso |
|---|---|
dataset_name |
nombre del dataset (portada). Default: profile['table'] |
source_origin |
de dónde viene el dataset (portada). Default: profile['source'] |
storage |
tecnología de almacenamiento (portada). Default: inferido de source |
generated_at |
fecha de generación (portada/manifiesto). Default: profiled_at/ahora |
description |
frase de descripción del dataset (portada) |
granularity |
"Cada fila es…" (portada). Default: derivado de key_candidates |
quality_criteria |
criterios del score de calidad (portada) |
head_rows |
list[dict] con df.head (overview). Ver §7 |
Un capítulo puede definir y consumir sus propias claves ctx — documenta cuáles en su
docstring.
6. Claves del profile que consume cada capítulo
El TableProfile lo produce profile_table(...)["profile"] (grupo eda). Claves de
nivel superior: table, source, profiled_at, n_rows, n_cols, size_bytes, duplicate_rows, duplicate_pct, null_cell_pct, constant_cols, all_null_cols, quality_score, type_breakdown, key_candidates, columns[], correlations, llm, models, series, caveats.
Cada columns[i]: name, inferred_type, semantic_type, physical_type, distinct_count, unique_pct, null_count, null_pct, empty_count, empty_pct, flags, quality_score, numeric{min,max,mean,median,std,variance,cv,iqr,skew,kurtosis,p1..p99,mode,n_outliers, outlier_pct,zero_pct,negative_pct,distribution_type,histogram[{lo,hi,count}]}, categorical{top[{value,count,pct}],mode,n_distinct,entropy,imbalance,len_min/mean/max}, reexpression, series{...}.
| Capítulo | Claves del profile que consume |
|---|---|
portada |
table, source, profiled_at, n_rows, n_cols, quality_score, key_candidates + ctx |
overview |
columns[].{name,inferred_type,semantic_type,physical_type,null_pct,null_count,categorical.top,numeric.{min,median,max,mean,std}}, head_rows (ver §7) |
num_distr (pendiente) |
columns[] numeric.{histogram,mean,median,std,outlier_pct,...} |
cat_distr (pendiente) |
columns[] categorical.{top,entropy,imbalance} |
calidad (pendiente) |
quality_score, columns[].{quality_score,flags,issues}, duplicate_*, null_cell_pct, constant_cols, all_null_cols |
correlacion (pendiente) |
correlations.pairs[{a,b,value,method}], correlations.levels_caveat |
modelos (pendiente) |
models.{pca,kmeans,outliers,normality} |
analisis_llm (pendiente) |
llm |
timeseries (pendiente) |
series{col:{stationarity,acf_pacf,stl,levels_*}} |
geospatial (pendiente) |
columnas con semantic_type geográfico (lat/lon) |
agregacion (pendiente) |
columns[] + agregados que la fase de cálculo añada |
7. Claves nuevas del profile que la fase de cálculo debe añadir
El TableProfile actual no trae estas claves; el capítulo OVERVIEW las consume y, si
faltan, degrada honestamente (placeholder + derivación de valores reales). Para un
overview completo, la fase de cálculo (otro agente) debe añadir:
profile['head_rows']:list[dict]con las primeras N filas (df.head), una por dict{columna: valor}. Mientras tanto OVERVIEW muestra un placeholder.columns[i]['examples']:listde hasta N valores no nulos crudos de la columna. Mientras tanto OVERVIEW deriva ejemplos decategorical.top[].value(categóricas) y denumeric.{min,median,max}(numéricas) — son valores reales, no inventados.
Sugerencia de implementación (no obligatoria en esta fase): una función del registry que
muestree head_rows/examples desde DuckDB y las inyecte en el profile antes de
renderizar (delegar a fn-constructor, tag eda).
8. Ejemplo COMPLETO de capítulo de referencia (OVERVIEW)
Copia este patrón. Archivo real:
python/functions/datascience/automatic_eda/chapters/overview.py.
from .. import model
CHAPTER_VERSION = "1.0.0"
CHAPTER_ID = "overview"
CHAPTER_TITLE = "Overview"
def _fmt_num(v, d=3):
# ... formateo defensivo (None -> "—", floats compactos) ...
...
def _examples_for(col: dict) -> str:
# 1) col['examples'] si existe; 2) categorical.top[].value;
# 3) numeric.{min,median,max}. Nunca celda vacía ni inventada.
...
def build_overview(profile: dict, ctx: dict):
profile = profile or {}
ctx = ctx or {}
cols = profile.get("columns") or []
if not cols and not (ctx.get("head_rows") or profile.get("head_rows")):
return None # no aplica.
blocks = [
model.Heading(text="Primeras filas (df.head)", level=2),
_head_block(profile, ctx), # DataTable(df.head) o Note si falta head_rows.
]
cols_block = _columns_block(profile) # DataTable: nombre/tipo/nulos/ejemplos.
if cols_block is not None:
blocks.append(model.Heading(text="Diccionario de columnas", level=2))
blocks.append(cols_block)
desc_block = _describe_block(profile) # DataTable: mean/median/min/max/std.
if desc_block is not None:
blocks.append(model.Heading(text="Resumen estadístico numérico", level=2))
blocks.append(desc_block)
return model.Chapter(id=CHAPTER_ID, title=CHAPTER_TITLE,
version=CHAPTER_VERSION, blocks=blocks)
Puntos clave que todo capítulo debe respetar:
- Lectura defensiva:
profile.get(...),or [], comprobarisinstance— nunca asumir que una clave existe ni lanzar. Nonesi no aplica: devuelveNone(oblocksvacíos) cuando el dataset no tiene lo que el capítulo necesita.- No inventar: si falta un dato (p.ej.
df.head), muestra un placeholder honesto o deriva de valores reales del perfil; deja el hueco documentado. - Tablas vía
DataTable: deja que el renderer las parta y repita cabecera; no pre-pagines tú. - Figuras vía
Figure(make=...): pásalas perezosas; las dibuja y escala el renderer.
9. Cómo se prueba un capítulo
from datascience.automatic_eda import build_document, render_pdf, render_pptx
chapters = build_document(profile, ctx={"dataset_name": "..."})
render_pdf(chapters, "reports/x.pdf", {"title": "EDA"})
render_pptx(chapters, "reports/x.pptx", {"title": "EDA"})
O directo desde las funciones públicas con el profile entero (construyen los capítulos):
from datascience import render_automatic_eda_pdf, render_automatic_eda_pptx
render_automatic_eda_pdf(profile, "reports/x.pdf", {"ctx": {...}})
render_automatic_eda_pptx(profile, "reports/x.pptx", {"ctx": {...}})
Añade un test self-contained por capítulo (perfil sintético, sin DuckDB) que verifique
sus bloques presentes y el no-corte (texto largo intacto en la salida). Patrón:
render_automatic_eda_pdf_test.py.
10. Integración futura con profile_table (siguiente fase)
profile_table(emit_pdf=True) usa hoy render_eda_pdf (intacto). En la siguiente fase
se añadirá emit_automatic=True (o se migrará emit_pdf) para que cada EDA emita
siempre PDF + PPTX del motor AutomaticEDA desde el mismo profile:
# Bosquejo de la integración aditiva (NO activar si rompe los tests actuales):
if emit_automatic:
ctx = {"dataset_name": table, "source_origin": db_path, ...}
render_automatic_eda_pdf(prof, os.path.join(report_dir, f"aeda_{table}_{ts}.pdf"),
{"title": f"EDA — {table}", "ctx": ctx})
render_automatic_eda_pptx(prof, os.path.join(report_dir, f"aeda_{table}_{ts}.pptx"),
{"title": f"EDA — {table}", "ctx": ctx})
Hasta entonces los renderers se invocan directamente sobre el profile que
profile_table ya devuelve.