---
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.