---
name: build_eda_render_ctx
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def build_eda_render_ctx(db_path: str, table: str, profile: dict, backend: str = 'duckdb', sample: int = 5000, base_ctx: dict = None) -> dict"
description: "Constructor del `ctx` de datos crudos del motor AutomaticEDA. Dado un db_path+table (DuckDB o Postgres) y el TableProfile AGREGADO ya calculado por profile_table, produce el dict ctx que los renderers (render_automatic_eda_pdf/_pptx -> build_document(profile, ctx)) pasan a los capitulos que necesitan DATOS CRUDOS no presentes en el perfil agregado: modelos (project_clusters_2d en vivo), timeseries, geospatial y agregacion (groupby/pivot push-down). NO trae tablas enteras a RAM: muestrea con LIMIT sample y delega el push-down de la serie en extract_timeseries_raw. Construye el lector read-only query_fn(sql)->dict igual que profile_table (closure sobre duckdb_query_readonly / pg_query). Estilo dict-no-throw del grupo eda: NUNCA lanza; si una pieza falla, degrada esa clave a ausente/[] y sigue. Devuelve el ctx dict directamente (NO un wrapper {status,...}); se pasa tal cual como meta={'ctx': <ese dict>}. Claves de datos que produce: raw_numeric (muestra cruda alineada por fila), timeseries_raw (fechas+series), geo_points (lats/lons) y db_path+table para el push-down de agregacion. Respeta base_ctx: parte de una copia y solo AÑADE las claves de datos; las de presentacion (dataset_name, source_origin, ...) no se pisan."
tags: [eda, datascience, automatic-eda, render, ctx, extraction, read-only, duckdb, postgres, python]
uses_functions: [detect_time_column_py_datascience, extract_timeseries_raw_py_datascience, detect_latlon_columns_py_datascience, duckdb_query_readonly_py_infra, pg_query_py_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
  - name: db_path
    desc: "ruta al archivo DuckDB, o DSN PostgreSQL si backend='postgres'. Se guarda tal cual en ctx['db_path'] (el capitulo agregacion lo usa para el groupby/pivot push-down via DuckDB) y se inyecta en el closure query_fn. No se valida aqui: si la base no existe, las queries devuelven status error y las claves de datos se omiten."
  - name: table
    desc: "nombre de la tabla. Se escapa con comillas dobles en las queries (raw_numeric y timeseries) y se guarda en ctx['table']."
  - name: profile
    desc: "TableProfile AGREGADO producido por profile_table. Solo se lee su clave `columns` (lista de ColumnProfile dict con name / inferred_type / numeric.{min,max} / semantic_type). Lectura defensiva: si no es dict o no tiene columns, se trata como []. NO se traen las filas crudas de aqui — se muestrean de la base."
  - name: backend
    desc: "'duckdb' (default) o 'postgres'. Selecciona el lector read-only del registry (duckdb_query_readonly / pg_query). Cualquier otro valor devuelve el base_ctx tal cual, SIN añadir claves de datos (ni siquiera db_path/table)."
  - name: sample
    desc: "maximo de filas a muestrear (clausula LIMIT) tanto para raw_numeric (una sola query SELECT de las numericas) como para timeseries_raw (max_rows de extract_timeseries_raw). Default 5000. Acota memoria y tiempo de render."
  - name: base_ctx
    desc: "dict opcional con claves de PRESENTACION ya preparadas (dataset_name, source_origin, ...). Se parte de una copia y NO se pisan sus claves; solo se añaden las de datos. Default None -> {}."
output: "El dict `ctx` directamente (NO un wrapper {status,...}); se pasa tal cual como meta={'ctx': <ese dict>} a render_automatic_eda_pdf/pptx. Nunca lanza. Para backends validos contiene SIEMPRE db_path + table, y opcionalmente: raw_numeric {col:[float|None,...]} (muestra cruda alineada por fila; omitida si no hay numericas o falla la query), timeseries_raw {time_col, t:[iso...], series:{col:[float|None,...]}} (solo si hay columna temporal + numericas y trae filas), geo_points {lats:[...], lons:[...]} (solo si se detecta par lat/lon y ambas estan en raw_numeric). Ante fallo global devuelve al menos {**base_ctx, 'db_path': db_path, 'table': table}. Backend desconocido -> base_ctx tal cual sin claves de datos."
tested: true
tests: ["test_db_path_y_table_en_ctx", "test_raw_numeric_con_columnas_numericas", "test_timeseries_raw_con_fecha", "test_geo_points_con_latlon", "test_sin_fecha_no_hay_timeseries", "test_base_ctx_preservado"]
test_file_path: "python/functions/datascience/build_eda_render_ctx_test.py"
file_path: "python/functions/datascience/build_eda_render_ctx.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience import build_eda_render_ctx, render_automatic_eda_pdf
from datascience import profile_table  # opcional: para obtener el TableProfile

# 1) Perfil agregado de la tabla (push-down, sin RAM).
prof = profile_table("data/ventas.duckdb", "ventas_geo", write_report=False)["profile"]

# 2) ctx de datos crudos para los capitulos (muestrea con LIMIT, no carga todo).
ctx = build_eda_render_ctx(
    "data/ventas.duckdb", "ventas_geo", prof,
    backend="duckdb", sample=5000,
    base_ctx={"dataset_name": "Ventas con geolocalizacion"},
)
# ctx == {
#   "dataset_name": "Ventas con geolocalizacion",   # preservado del base_ctx
#   "db_path": "data/ventas.duckdb", "table": "ventas_geo",
#   "raw_numeric": {"ventas": [1200.5, ...], "lat": [40.41, ...], "lon": [-3.70, ...]},
#   "timeseries_raw": {"time_col": "fecha", "t": ["2024-01-01", ...], "series": {...}},
#   "geo_points": {"lats": [40.41, ...], "lons": [-3.70, ...]},
# }

# 3) Se entrega tal cual a los renderers via meta={"ctx": ctx}.
render_automatic_eda_pdf(prof, "reports/eda.pdf", meta={"ctx": ctx})
```

## Cuando usarla

Justo antes de renderizar un AutomaticEDA (PDF o PPTX), cuando ya tienes el
TableProfile AGREGADO de `profile_table` pero los capitulos de modelos,
timeseries, geospatial y agregacion necesitan DATOS CRUDOS que el perfil
agregado no lleva (la muestra numerica alineada por fila, la serie cronologica,
el par lat/lon, y el db_path/table para el push-down del groupby/pivot). Es el
puente entre el perfil agregado y `build_document(profile, ctx)`: una sola
llamada produce el `ctx` completo muestreando con `LIMIT` en vez de cargar la
tabla entera en memoria.

## Gotchas

- **Impura**: lee de la base de datos a traves de `query_fn` (closure sobre
  `duckdb_query_readonly` / `pg_query`). No abre conexiones fuera de esos
  wrappers del registry. Estilo dict-no-throw del grupo `eda`: NUNCA lanza; ante
  cualquier fallo (query, deteccion, render de una clave) degrada esa clave a
  ausente/`[]` y sigue. Ante un fallo global devuelve al menos
  `{**base_ctx, "db_path": db_path, "table": table}`.
- **`error_type` en el frontmatter es `error_go_core` por convencion del
  registry** (toda funcion impura debe declararlo y el indexer lo exige), pero el
  codigo NO lanza esa excepcion: degrada al ctx parcial. Es metadata, no
  comportamiento.
- **Devuelve el ctx dict directamente, NO un wrapper `{status,...}`**: a
  diferencia de `extract_timeseries_raw` / `profile_table`, esta funcion es el
  ultimo eslabon antes del render y su salida se pasa tal cual como
  `meta={"ctx": <ese dict>}`. No envuelvas su retorno.
- **Backend desconocido**: con un `backend` que no sea `duckdb` ni `postgres`
  devuelve el `base_ctx` tal cual, SIN claves de datos (ni siquiera
  `db_path`/`table`). Comprueba el backend antes si dependes de esas claves.
- **Alineacion por fila de `raw_numeric`**: `raw_numeric[col]` tiene una entrada
  por fila muestreada (un valor no convertible a float queda como `None`, no se
  descarta la fila) porque `project_clusters_2d` descarta filas listwise: todas
  las columnas deben tener la MISMA longitud. `geo_points` se construye desde
  `raw_numeric` para heredar esa alineacion.
- **`geo_points` exige lat/lon en `raw_numeric`**: el par lat/lon solo se adjunta
  si ambas columnas se detectaron (nombre+rango) Y figuran en `raw_numeric`
  (es decir, son numericas en el perfil). Si la tabla guarda lat/lon como texto
  no promovido a numeric, no apareceran; el capitulo geospatial sabe degradar.
- **`timeseries_raw` depende del orden del backend**: hereda el `ORDER BY
  "time_col"` de `extract_timeseries_raw`. Si la columna temporal esta guardada
  como texto no ordenable lexicograficamente (p.ej. `DD/MM/YYYY`), el orden no
  sera el cronologico real — normaliza la columna a date/timestamp antes.
- **`LIMIT sample`**: con tablas grandes obtienes el primer tramo (raw_numeric
  por orden fisico, timeseries por orden cronologico), no un muestreo uniforme.
  Sube `sample` si necesitas mas cobertura.
- **No loguear los datos crudos**: `raw_numeric` / `timeseries_raw` /
  `geo_points` pueden contener datos sensibles. En trazas usa solo conteos y
  nombres de columna, no el ctx completo.
