feat(eda): wiring AutomaticEDA — build_eda_render_ctx + pipeline render_automatic_eda + profile_table(emit_automatic)
Conecta el motor AutomaticEDA con los datos crudos para que los 4 capítulos
dependientes de ctx (modelos, timeseries, geospatial, agregacion) salgan
POBLADOS en vez de degradar a una nota.
- build_eda_render_ctx (datascience, impure, dict-no-throw): dado db_path+table
y el TableProfile agregado, construye el ctx con los datos crudos que el
perfil no incluye: raw_numeric {col:[float|None]} alineado por fila (modelos /
geospatial), timeseries_raw {time_col,t,series} vía extract_timeseries_raw,
geo_points {lats,lons} desde el par lat/lon detectado, y db_path/table para el
groupby/pivot push-down de agregacion. Muestrea con LIMIT (no trae la tabla
entera a RAM). Compone detect_time_column / extract_timeseries_raw /
detect_latlon_columns / duckdb_query_readonly (imports lazy para evitar ciclo).
- render_automatic_eda (pipeline): one-shot perfil -> ctx -> PDF + PPTX con los
11 capítulos poblados; devuelve rutas + manifest de versiones por capítulo.
- profile_table: flag aditivo emit_automatic=True emite el AutomaticEDA PDF+PPTX
además del flujo legacy (emit_pdf/render_eda_pdf intacto). Nuevas claves de
retorno aeda_pdf_path / aeda_pptx_path / aeda_manifest_path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user