---
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 — <table>'."
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.
