From cb7a7fc1fdaa97070a57ce45dca42e80f6710c34 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Tue, 30 Jun 2026 14:30:31 +0200 Subject: [PATCH] =?UTF-8?q?docs(eda):=20contrato=20de=20cap=C3=ADtulos=20A?= =?UTF-8?q?utomaticEDA=20+=20capability=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_(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) --- docs/automatic_eda_contract.md | 299 +++++++++++++++++++++++++++++++++ docs/capabilities/INDEX.md | 2 +- docs/capabilities/eda.md | 4 + 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 docs/automatic_eda_contract.md diff --git a/docs/automatic_eda_contract.md b/docs/automatic_eda_contract.md new file mode 100644 index 00000000..63e55213 --- /dev/null +++ b/docs/automatic_eda_contract.md @@ -0,0 +1,299 @@ +# 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_pdf` **de forma aditiva** (ese sigue activo en + `profile_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/.py` +que expone **dos** símbolos: + +```python +CHAPTER_VERSION = "1.0.0" # semver de generación del capítulo (ver §4) + +def build_(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_` donde `` es el del módulo y + el de `CHAPTER_ORDER` (§3). Ej.: `chapters/num_distr.py` → `build_num_distr`. +- Devuelve un `model.Chapter(id, title, version=CHAPTER_VERSION, blocks=[...])` o `None`. +- Un capítulo que devuelve `None` o cuyos `blocks` quedan 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`: + +```python +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/.py` y llama `build_`. **Para añadir un capítulo NO se edita +`chapters_registry.py`**: basta crear el módulo `chapters/.py` (con su `` 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 `` 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: ` · v`. +- `ENGINE_VERSION` (en `model.py`) versiona el motor global. +- Al renderizar se escribe `automatic_eda_manifest.json` junto a la salida: + +```json +{ + "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']`: `list` de hasta N valores **no nulos** crudos de la columna. + Mientras tanto OVERVIEW deriva ejemplos de `categorical.top[].value` (categóricas) y de + `numeric.{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`. + +```python +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: + +1. **Lectura defensiva**: `profile.get(...)`, `or []`, comprobar `isinstance` — nunca + asumir que una clave existe ni lanzar. +2. **`None` si no aplica**: devuelve `None` (o `blocks` vacíos) cuando el dataset no tiene + lo que el capítulo necesita. +3. **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. +4. **Tablas vía `DataTable`**: deja que el renderer las parta y repita cabecera; no + pre-pagines tú. +5. **Figuras vía `Figure(make=...)`**: pásalas perezosas; las dibuja y escala el renderer. + +--- + +## 9. Cómo se prueba un capítulo + +```python +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): + +```python +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: + +```python +# 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. diff --git a/docs/capabilities/INDEX.md b/docs/capabilities/INDEX.md index 98e0141e..dbea6af4 100644 --- a/docs/capabilities/INDEX.md +++ b/docs/capabilities/INDEX.md @@ -68,7 +68,7 @@ Indice de grupos de capacidades del registry. Cada grupo agrupa >=3 funciones qu | [consent](consent.md) | 3 | CMP / IAB TCF / data brokers: detectar el CMP de un sitio (Didomi/OneTrust/Sourcepoint/Quantcast), leer `__tcfapi` para contar vendors y propositos, aceptar el banner (selectores + fallback LLM con haiku que localiza Aceptar/Ver socios), y descargar la GVL de IAB para nominar cada broker y que datos recopila. Nacio de `projects/databrokers/` | | [onlyoffice](onlyoffice.md) | 3 | Operar ONLYOFFICE Desktop Editors (binario onlyoffice-desktopeditors) en Linux/X11 desde terminal via instancia aislada (slot HOME=/tmp/oo_): abrir un archivo en ventana propia, cerrar+reabrir para mostrar datos editados en disco (no hay reload nativo, Issue #2313), y matar el proceso del slot. Solo gestiona la ventana, NO edita ni crea archivos. Requiere X11 + wmctrl + xdotool. No confundir con el Document Server (web/Docker) | | [email](email.md) | 21 | Gestionar cuentas de correo por IMAP+SMTP directo (Python stdlib, sin browser ni MCP Gmail): conectar/listar/buscar/leer (imap_*), mutar estado (mark_seen/move/delete/save_draft) por UID, y construir+enviar (email_build_html/smtp_send). Auth user+app-password (NO OAuth; Outlook fuera). Credenciales desde pass, resueltas por la capa app. Complementa al browser (interactivo) — no lo reemplaza | -| [eda](eda.md) | 27 | Exploratory Data Analysis por tabla y base con motor DuckDB + PostgreSQL push-down: perfil base SQL (SUMMARIZE + distinct exacto), estadística numérica/categórica, tipo semántico regex, calidad, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK containment + join graph mermaid), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM (dictionary/PII/limpieza/análisis) y generación de notebook. Orquestadores `profile_table` (backend duckdb/postgres, flags run_models/run_llm) y `profile_database` | +| [eda](eda.md) | 29 | Exploratory Data Analysis por tabla y base con motor DuckDB + PostgreSQL push-down: perfil base SQL (SUMMARIZE + distinct exacto), estadística numérica/categórica, tipo semántico regex, calidad, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK containment + join graph mermaid), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM (dictionary/PII/limpieza/análisis) y generación de notebook. Orquestadores `profile_table` (backend duckdb/postgres, flags run_models/run_llm) y `profile_database` | | [seo](seo.md) | 3 | SEO orientado a datos sobre Google Search Console: autenticar con service account (`gsc_auth`), extraer Search Analytics paginado (`pull_gsc_search_analytics`) y el pipeline de ingesta a DuckDB + espejo Postgres para Metabase (`ingest_gsc_search_analytics`). Cadena de ingesta del proyecto `seo_analytics`; alimenta dashboards de striking distance, CTR opportunities y content decay | | [local-hub](local-hub.md) | 4 | Exponer los procesos locales como subdominios `*.localhost` (via Caddy, sin DNS) y reunirlos en una pantalla principal Glance con estado en vivo, refrescada a diario por dag_engine. Descubre servicios (manifiesto + registry), renderiza Caddyfile + config Glance (puras), y el pipeline `refresh_local_hub` regenera+recarga. Fuente de verdad: `apps/local_hub/local_services.yaml` | | [comfyui-judge](comfyui-judge.md) | 4 | Panel multi-juez de calidad de imagen: estético LAION-V2 (`comfyui_score_aesthetic`, 0-10) + fidelidad CLIP prompt↔imagen (`comfyui_score_clip_alignment`, 0-1) + crítica LLM-vision (`comfyui_critique_image_llm`, good/bad). Agregados por voto mayoría en `comfyui_judge_image`. Gate objetivo para tests/DoD y el bucle de mejora de skills ComfyUI; degrada con gracia si un juez cae. Jueces estético/fidelidad por subproceso al venv ComfyUI (torch+open_clip), crítico via claude-direct | diff --git a/docs/capabilities/eda.md b/docs/capabilities/eda.md index 946fec5f..d569acd6 100644 --- a/docs/capabilities/eda.md +++ b/docs/capabilities/eda.md @@ -71,6 +71,10 @@ Orquestadores one-shot: | `eda_llm_insights_py_datascience` | impure | 1 call LLM sobre el perfil agregado (no filas crudas): data dictionary, resumen, granularidad de fila, PII/RGPD, limpieza, análisis sugeridos. | | `build_eda_notebook_py_datascience` | impure | Genera un `.ipynb` (nbformat v4) que perfila la tabla, listo para lanzar en Jupyter colaborativo. | | `render_eda_pdf_py_datascience` | impure | Renderiza el `TableProfile` a un PDF multipágina **vertical (A5), legible en móvil** (estilo Tufte: histogramas como small multiples, top-k, heatmap de asociación). 4ª salida del workflow, junto a JSON/Markdown/notebook. | +| `render_automatic_eda_pdf_py_datascience` | impure | Motor **AutomaticEDA**: documento por CAPÍTULOS (modelo de bloques independiente del formato) → PDF A5 móvil que **nunca corta** texto/tablas/imágenes (tablas largas se parten repitiendo cabecera) + manifiesto versionado por capítulo. Acepta el `TableProfile` o capítulos del modelo. Aditivo, no reemplaza `render_eda_pdf`. | +| `render_automatic_eda_pptx_py_datascience` | impure | Motor **AutomaticEDA** → PPTX 16:9 para **compartir** desde el mismo documento por capítulos; mismo principio anti-corte (continúa en slide `(cont.)`). Motor `python-pptx`. | + +> **AutomaticEDA** (núcleo nuevo, fase de capítulos): separa contenido (capítulos/bloques) de formato (PDF móvil + PPTX). Para escribir un capítulo nuevo (NUM DISTR, CAT DISTR, CALIDAD, CORRELACIÓN, MODELOS, ANÁLISIS LLM, TIMESERIES, GEOSPATIAL, AGREGACIÓN) lee el contrato: **`docs/automatic_eda_contract.md`**. Código del motor en `python/functions/datascience/automatic_eda/`; capítulos de referencia: `portada`, `overview`. ### Orquestadores (pipelines) | ID | Qué hace |