Files
fn_registry/python/functions/datascience/render_automatic_eda_pdf.md
T
egutierrez 9cdde4a341 feat(eda): núcleo AutomaticEDA — documento por capítulos + renderers PDF/PPTX anti-corte
Introduce la capa intermedia entre el contenido de un EDA y su formato de
salida. Un documento es una lista de capítulos versionados; cada capítulo es
un conjunto ordenado de bloques (heading, markdown, kv_table, data_table,
figure, image, caption, note) independientes del formato.

Núcleo (paquete de soporte python/functions/datascience/automatic_eda/):
- model.py: dataclasses de bloques + Chapter, normalizadores defensivos
  (aceptan dataclass o dict, nunca lanzan), ENGINE_VERSION y el manifiesto
  por capítulo (automatic_eda_manifest.json).
- text_layout.py: medición/wrapping por rejilla de caracteres compartida.
- chapters_registry.py: CHAPTER_ORDER pre-declarado + build_document con
  auto-discovery de capítulos por convención (permite añadir capítulos en
  paralelo sin editar el registro).
- render_pdf_impl.py: paginador A5 retrato móvil que MIDE cada bloque y nunca
  corta: texto a líneas completas, tablas largas partidas por filas repitiendo
  cabecera, figuras/imágenes escaladas para caber enteras. Pie versionado por
  capítulo.
- render_pptx_impl.py: mismo principio sobre slides 16:9 (continúa en slide
  "(cont.)"; tablas repiten cabecera; figuras exportadas a PNG escaladas).
- chapters/portada.py y chapters/overview.py: capítulos de referencia. Portada
  con nombre, rótulo Automatic-EDA, fuente, almacenamiento (inferido de
  source), fecha europea, filas×cols, descripción, granularidad y calidad con
  criterios. Overview con df.head (placeholder honesto si falta head_rows),
  diccionario de columnas (tipo/nulos/ejemplos) y describe numérico.

Funciones públicas del registry (grupo eda, dict-no-throw):
- render_automatic_eda_pdf / render_automatic_eda_pptx: aceptan capítulos o un
  TableProfile (construyen los capítulos con build_document) y escriben el
  manifiesto. Aditivas — no reemplazan render_eda_pdf.

Tests self-contained (sin DuckDB) para ambos renderers: golden (portada +
overview), partición de tablas largas repitiendo cabecera, no-corte de celdas
y markdown largos, profile None/{} válido de 1 página/slide, y error path en
directorio no escribible. 23 tests verdes (incluye los previos de
render_eda_pdf, intactos).

Dependencia nueva python-pptx>=1.0.2 declarada en python/pyproject.toml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 14:30:31 +02:00

7.4 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
render_automatic_eda_pdf function py datascience 1.0.0 impure def render_automatic_eda_pdf(chapters_or_profile, out_path: str, meta: dict = None) -> dict Renderiza un documento AutomaticEDA por CAPÍTULOS (modelo de bloques independiente del formato) en un PDF A5 retrato pensado para LEER EN EL MÓVIL. Acepta una lista de capítulos del modelo o directamente un TableProfile del grupo eda (en cuyo caso construye los capítulos canónicos con build_document). El paginador MIDE cada bloque y NUNCA corta nada: el texto se envuelve a líneas completas, las tablas largas se parten por filas REPITIENDO la cabecera, figuras e imágenes se escalan para caber enteras. Cada capítulo empieza en página nueva con pie 'Capítulo · vX.Y.Z' y se escribe un manifiesto automatic_eda_manifest.json junto a la salida para seguimiento por capítulo. dict-no-throw: nunca lanza, devuelve {path, n_pages, chapters, manifest_path, note}. Motor matplotlib PdfPages. Aditivo: NO reemplaza render_eda_pdf.
eda
pdf
render
report
mobile
automatic-eda
chapters
versioned
no-cut
pagination
matplotlib
datascience
python
false error_go_core
os
matplotlib
datascience.automatic_eda
name desc
chapters_or_profile una lista de capítulos del modelo AutomaticEDA (dataclasses Chapter o dicts {id,title,version,blocks}) O un TableProfile dict del grupo eda. Si es un TableProfile, los capítulos canónicos se construyen con build_document(profile, meta['ctx']). Un capítulo es {id,title,version,blocks}; un bloque es uno de: heading, markdown, kv_table, data_table, figure, image, caption, note. Lectura defensiva: cualquier cosa no reconocida se degrada a Note, nunca lanza.
name desc
out_path ruta del archivo PDF de salida. Los directorios padre se crean si faltan. Si está en un directorio no escribible (p.ej. /proc/...) devuelve {path:None, note:<causa>} sin lanzar.
name desc
meta dict opcional. Claves: title (título de portada/pie), ctx (contexto de presentación pasado a los builders de capítulo cuando se da un profile: dataset_name, source_origin, storage, generated_at, description, granularity, quality_criteria, head_rows...), manifest_path (override; por defecto automatic_eda_manifest.json junto a out_path), write_manifest (False para no escribirlo), generated_at.
dict (nunca lanza): {path: str|None, n_pages: int, chapters: list[{id,version,n_pages}], manifest_path: str|None, note: str}. En éxito path es la ruta escrita, n_pages el total de páginas, chapters el desglose por capítulo para el manifiesto. En error fatal path es None y note explica la causa. true
test_golden_profile_genera_pdf_portada_y_overview
test_edge_tabla_larga_parte_repitiendo_cabecera
test_edge_celda_larga_no_se_corta
test_no_corta_texto_markdown
test_edge_profile_none_y_vacio_un_pagina
test_error_path_directorio_no_escribible_no_revienta
python/functions/datascience/render_automatic_eda_pdf_test.py python/functions/datascience/render_automatic_eda_pdf.py

Ejemplo

from datascience import render_automatic_eda_pdf

# Caso 1: directamente desde un TableProfile del grupo eda.
# profile = profile_table(db, "ventas", backend="duckdb")["profile"]
profile = {
    "table": "ventas", "source": "/data/ventas.csv",
    "n_rows": 1000, "n_cols": 2, "quality_score": 92.5,
    "columns": [
        {"name": "precio", "inferred_type": "numeric", "null_pct": 0.01,
         "null_count": 10,
         "numeric": {"mean": 42.5, "median": 40.0, "min": 1.0, "max": 100.0,
                     "std": 12.3}},
        {"name": "categoria", "inferred_type": "categorical", "null_pct": 0.0,
         "categorical": {"top": [{"value": "neumaticos", "count": 500},
                                 {"value": "aceite", "count": 300}]}},
    ],
}
res = render_automatic_eda_pdf(
    profile, "reports/ventas_aeda.pdf",
    {"title": "EDA — ventas",
     "ctx": {"dataset_name": "Ventas", "source_origin": "ERP export",
             "description": "Líneas de venta del ERP.",
             "granularity": "Cada fila es una línea de venta."}})
print(res["n_pages"], res["chapters"], res["manifest_path"])
# -> 3 [{'id':'portada','version':'1.0.0','n_pages':1},
#       {'id':'overview','version':'1.0.0','n_pages':2}] reports/automatic_eda_manifest.json

# Caso 2: desde capítulos construidos a mano (modelo de bloques).
from datascience.automatic_eda.model import Chapter, Heading, DataTable
ch = Chapter(id="resumen", title="Resumen", version="1.0.0", blocks=[
    Heading("Tabla", 1),
    DataTable(header=["col", "valor"], rows=[["a", "1"], ["b", "2"]]),
])
render_automatic_eda_pdf([ch], "reports/manual.pdf")

Cuando usarla

Cuando quieras el PDF móvil del nuevo motor AutomaticEDA por capítulos (portada

  • overview + los capítulos que existan): después de profile_table(...), pásale el profile y obtienes un PDF A5 retrato versionado por capítulo, con manifiesto. Úsala como capa de presentación PDF del grupo eda cuando necesites garantía de no-corte (texto, tablas e imágenes nunca recortados) y versionado por capítulo para mejora continua. Es el reemplazo evolutivo de render_eda_pdf: comparte estética Tufte/móvil pero separa contenido (capítulos/bloques) de formato (renderer), de modo que el mismo documento se emite también como PPTX (render_automatic_eda_pptx). Para añadir un capítulo nuevo, ver docs/capabilities/automatic_eda.md.

Gotchas

  • Impura: escribe el PDF en out_path (crea los directorios padre) y, salvo meta['write_manifest']=False, un automatic_eda_manifest.json junto a la salida. Backend headless Agg de matplotlib (corre en agentes/CI sin display).
  • Nunca lanza (dict-no-throw): un bloque o capítulo que falle se omite y se anota en note; el PDF se genera igual. Un profile None/{} produce un PDF de 1 página válido. out_path no escribible → {path: None, note: <causa>}.
  • No corta nada: el paginador mide cada bloque con una rejilla de caracteres (sobre-estima ligeramente, nunca afirma que algo cabe cuando se desbordaría). El texto se envuelve a líneas completas (sin cortar a media palabra), las tablas largas se parten por filas repitiendo la cabecera, las celdas con texto largo se envuelven dentro de su columna (la fila crece), y figuras/imágenes se escalan para caber enteras (nunca se recortan).
  • Tablas muy anchas: con muchas columnas (>10) cada columna se estrecha y su texto se envuelve en varias líneas (sigue sin perderse). El reparto por columnas-en-grupos para tablas muy anchas es una mejora pendiente (ver capability page).
  • head_rows / examples: el capítulo Overview muestra df.head desde ctx['head_rows']/profile['head_rows'] y ejemplos no-nulos desde columns[i]['examples']; si el profile no los trae (hoy no los trae), degrada con un placeholder honesto y deriva los ejemplos de los valores reales del perfil (top categóricos, min/median/max numéricos). Documentado en el contrato.
  • Registro en el package: el ## Ejemplo usa from datascience import render_automatic_eda_pdf (añadido al __init__.py); el test importa el módulo directo para no depender de ese registro.
  • Fechas en UI europeas: la portada formatea la fecha como DD/MM/AAAA HH:mm.