--- name: render_eda_pdf kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def render_eda_pdf(profile: dict, out_path: str, title: str = None) -> dict" description: "Renderiza un TableProfile del grupo eda en un PDF multipágina portátil pensado para LEER Y EXPLORAR EN EL MÓVIL. Páginas A5 retrato, una columna, tipografía grande; diseño Tufte (alto data-ink ratio, histogramas reales como small multiples, barras top-k, heatmap de asociación, integridad de ejes desde 0). Lee todo el profile defensivamente con .get y sólo renderiza las secciones presentes; bloques nuevos del profile (models, caveats, ...) se vuelcan genéricamente (forward-compatible). dict-no-throw: nunca lanza, devuelve {pdf_path, n_pages, note}. Motor matplotlib PdfPages, cero dependencias nuevas." tags: [eda, pdf, render, report, mobile, tufte, visualization, matplotlib, profiling, datascience, python] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [os, textwrap, datetime, matplotlib, numpy] params: - name: profile desc: "TableProfile dict del grupo de capacidad eda (el dict que profile_table devuelve bajo la clave 'profile'). Puede tener muchas claves ausentes o None; un profile None/vacío genera igualmente un PDF de 1 página. Claves consumidas: table, source, profiled_at, n_rows, n_cols, size_bytes, duplicate_rows/_pct, null_cell_pct, quality_score, type_breakdown, constant_cols, all_null_cols, key_candidates, columns[] (con numeric.histogram [{lo,hi,count}], categorical.top [{value,count,pct}], quality_score, flags/issues), correlations.pairs [{a,b,value}], llm. Cualquier otra clave de nivel superior se vuelca en una página forward-compat." - name: out_path desc: "ruta del archivo PDF de salida. Los directorios padre se crean si faltan." - name: title desc: "título opcional para la portada. Por defecto 'EDA — '." output: "dict (nunca lanza): {pdf_path: str, n_pages: int, note: str}. En éxito pdf_path es la ruta escrita, n_pages el número de páginas generadas y note un resumen ('N páginas', con detalle de las secciones omitidas si alguna falló). En error fatal de escritura pdf_path es None y note explica la causa." tested: true tests: ["test_golden_genera_pdf_multipagina", "test_edge_profile_vacio_no_revienta", "test_edge_profile_none_no_revienta", "test_edge_solo_numericas", "test_forward_compat_seccion_desconocida"] test_file_path: "python/functions/datascience/render_eda_pdf_test.py" file_path: "python/functions/datascience/render_eda_pdf.py" --- ## Ejemplo ```python from datascience import render_eda_pdf # TableProfile mínimo (en la práctica viene de profile_table(...)["profile"]). profile = { "table": "ventas", "source": "data/ventas.csv", "n_rows": 1000, "n_cols": 2, "null_cell_pct": 0.02, "quality_score": 92.5, "type_breakdown": {"numeric": 1, "categorical": 1}, "columns": [ { "name": "precio", "inferred_type": "numeric", "quality_score": 95.0, "numeric": { "min": 1.0, "max": 100.0, "median": 40.0, "mean": 42.5, "std": 12.3, "outlier_pct": 1.2, "histogram": [ {"lo": 0.0, "hi": 25.0, "count": 100}, {"lo": 25.0, "hi": 50.0, "count": 500}, {"lo": 50.0, "hi": 75.0, "count": 300}, {"lo": 75.0, "hi": 100.0, "count": 50}, ], }, }, { "name": "categoria", "inferred_type": "categorical", "quality_score": 99.0, "categorical": { "entropy": 1.05, "top": [ {"value": "neumaticos", "count": 500, "pct": 0.5}, {"value": "aceite", "count": 300, "pct": 0.3}, {"value": "filtros", "count": 200, "pct": 0.2}, ], }, }, ], } res = render_eda_pdf(profile, "reports/eda_ventas.pdf", title="EDA — ventas") print(res) # -> {'pdf_path': 'reports/eda_ventas.pdf', 'n_pages': 5, 'note': '5 páginas'} ``` ## Cuando usarla Cuando quieras una **4ª salida portátil del EDA para revisar en el teléfono**: después de `profile_table(...)`, pásale el `profile` resultante para emitir un PDF que el usuario recibe y explora desde el móvil, sin abrir notebooks ni markdown. Úsala como capa de presentación del grupo `eda` (junto al report markdown, el JSON sidecar y el notebook Jupyter): histogramas reales en small multiples, barras top-k de las categóricas, heatmap de correlaciones y una portada con el score de calidad, todo maquetado para pantalla pequeña con criterios de Tufte (alto data-ink ratio, ejes honestos desde 0). No recalcula nada del perfil — sólo lo dibuja. ## Gotchas - **Impura**: escribe un archivo en `out_path` (crea los directorios padre). Usa el backend headless `Agg` de matplotlib, así que corre en agentes/CI sin display. - **Nunca lanza** (dict-no-throw): cada sección se construye aislada; si una falla, se omite y se anota en `note`, pero el PDF se genera igual. Un profile `None`/`{}` produce un PDF de 1 página válido. - **Forward-compatible**: sólo conoce un conjunto fijo de claves de nivel superior; cualquier bloque nuevo del profile (p.ej. `models`, `caveats`, series temporales que añadan otras funciones del grupo) se vuelca en una página genérica "Otras secciones" en vez de perderse o romper. No asume claves que quizá no existan. - **Registro en el package**: el `## Ejemplo` usa `from datascience import render_eda_pdf`, que requiere que la función esté añadida al `__init__.py` del paquete (lo hace `fn index` + la integración del orquestador). El test importa el módulo directo (`from render_eda_pdf import render_eda_pdf`) para no depender de ese registro. - **Histograma real, no ASCII**: necesita `numeric.histogram` como lista de bins `{lo, hi, count}` (el formato que emite `describe_numeric`). Si una columna numérica no trae histograma, esa columna se salta en la página de distribuciones. - **Heatmap de correlaciones**: reconstruye la matriz simétrica desde `correlations.pairs` (`{a, b, value}`); anota los valores en celda sólo si hay ≤8 columnas para no saturar la pantalla del móvil. - **PDF con texto seleccionable** (`pdf.fonttype=42`, TrueType embebido), legible y buscable en visores móviles.