feat(eda): funciones de agregación/OLAP para AutomaticEDA (groupby/pivot push-down + selección LLM)

Cuatro funciones nuevas del grupo eda que nutren el capítulo AGREGACION:
- select_groupby_keys (pure): elige categóricas agrupables + numéricas medida desde el TableProfile.
- groupby_stats_duckdb (impure): GROUP BY push-down en DuckDB (count/mean/median/std/min/max por grupo).
- pivot_table_duckdb (impure): pivot A×B push-down, limitado a top filas/cols para no cortar.
- suggest_aggregations_llm (impure): el LLM elige las agregaciones interesantes con fallback determinista.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:33:55 +02:00
parent 415154d9a3
commit 96da9e3015
13 changed files with 2146 additions and 0 deletions
@@ -0,0 +1,92 @@
---
name: pivot_table_duckdb
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def pivot_table_duckdb(db_path: str, table: str, index: str, columns: str, value: str, agg: str = 'mean', top_rows: int = 10, top_cols: int = 8) -> dict"
description: "Pivot table (index x columns -> agg(value)) calculada con push-down SQL en DuckDB (GROUP BY en el motor, sin traer filas a RAM) y recortada a las top_rows filas y top_cols columnas con mas observaciones para que quepa entera en un PDF movil / slide PPTX sin cortarse. Version push-down para tablas grandes de la funcion pura `pivot` (que pivota list[dict] en memoria)."
tags: [eda, pivot, duckdb, aggregate, datascience, push-down, report]
uses_functions: [duckdb_query_readonly_py_infra]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: []
params:
- name: db_path
desc: "Ruta al archivo DuckDB. Debe existir; el modo read_only NO crea la base."
- name: table
desc: "Nombre de la tabla a pivotar. Se interpola citado con dobles comillas (DuckDB no admite parametros para identificadores)."
- name: index
desc: "Columna cuyos valores forman las filas de la pivot (eje vertical)."
- name: columns
desc: "Columna cuyos valores forman las columnas de la pivot (eje horizontal)."
- name: value
desc: "Columna numerica a agregar en cada celda. Ignorada cuando agg='count'."
- name: agg
desc: "Funcion de agregacion: mean, sum, count, min, max, median. mean->avg(), count->COUNT(*). Otro valor devuelve {status:'error'}."
- name: top_rows
desc: "Numero maximo de filas a conservar, elegidas por mayor numero de observaciones (suma de COUNT(*) por valor de index). Default 10."
- name: top_cols
desc: "Numero maximo de columnas a conservar, elegidas por mayor numero de observaciones (suma de COUNT(*) por valor de columns). Default 8."
output: "dict. En exito {status:'ok', index, columns, value, agg, row_labels:[...], col_labels:[...], matrix:[[...]], truncated_rows:bool, truncated_cols:bool, note:str}. matrix tiene len(row_labels) filas y cada fila len(col_labels) celdas (valor agregado o None si la combinacion no existe). truncated_* indica si hubo mas filas/columnas que el top. En error {status:'error', error:str} (no lanza)."
tested: true
tests: ["pivot mean labels y celda conocida", "pivot trunca a top rows y top cols", "pivot count no necesita value real", "pivot db inexistente devuelve error sin lanzar", "pivot agg invalido devuelve error"]
test_file_path: "python/functions/datascience/pivot_table_duckdb_test.py"
file_path: "python/functions/datascience/pivot_table_duckdb.py"
---
## Ejemplo
```python
import duckdb
from datascience import pivot_table_duckdb
# Tabla DuckDB de prueba estilo titanic: sex x pclass -> mean(fare).
db = "/tmp/pivot_demo.duckdb"
con = duckdb.connect(db)
con.execute(
"CREATE TABLE titanic AS SELECT * FROM (VALUES "
"('male',1,211.3),('female',1,151.5),('male',3,7.9),"
"('female',3,16.7),('male',1,52.0),('female',2,41.6)"
") t(sex, pclass, fare)"
)
con.close()
res = pivot_table_duckdb(db, "titanic", index="sex", columns="pclass", value="fare", agg="mean")
print(res["status"]) # ok
print(res["row_labels"]) # ['female', 'male'] (orden por nº de observaciones desc; empate -> etiqueta)
print(res["col_labels"]) # [1, 3, 2] (pclass=1 tiene 3 obs, pclass=3 -> 2, pclass=2 -> 1)
print(res["matrix"]) # [[151.5, 16.7, 41.6], [131.65, 7.9, None]] (male/pclass=2 no existe -> None)
```
## Cuando usarla
Cuando quieres una pivot table (`index` x `columns` -> `agg(value)`) de una tabla
DuckDB con MUCHAS filas y necesitas que el resultado quepa entero en un informe: un
PDF abierto en el movil o un slide PPTX, donde una matriz de 50x30 se cortaria. La
agregacion se hace push-down en el motor (no traes las filas a RAM) y el resultado se
limita a las `top_rows` x `top_cols` combinaciones con mas observaciones. Encaja en el
flujo `eda` para resumir el cruce de dos categoricas (sexo x clase, region x producto)
contra una metrica. Para pivotar un `list[dict]` ya cargado en memoria usa la funcion
pura `pivot_py_datascience`; esta es la version push-down sobre disco.
## Gotchas
- Funcion impura: lee un archivo DuckDB del disco (read_only, nunca lo modifica).
- Recorta a `top_rows` x `top_cols` por numero de observaciones (suma de `COUNT(*)`),
NO por magnitud del valor agregado. Si habia mas filas/columnas, `truncated_rows` /
`truncated_cols` quedan en True y esas combinaciones NO aparecen en la matriz.
- Las celdas sin datos (combinacion `index` x `columns` que no existe en la tabla) se
rellenan con `None`, no con 0: distinguir "cero medido" de "sin observaciones".
- `agg='count'` cuenta filas por celda con `COUNT(*)` e ignora `value` (puedes pasar
cualquier nombre de columna). Para el resto de aggs, `value` debe ser una columna
numerica real o la query fallara con `{status:'error'}`.
- `agg` solo admite mean, sum, count, min, max, median; cualquier otro valor devuelve
`{status:'error'}` sin tocar la base.
- Orden de `row_labels` / `col_labels`: por numero de observaciones descendente, con
desempate estable por etiqueta. No es orden alfabetico ni el de aparicion.
- La query se ejecuta con `sandbox=False` en `duckdb_query_readonly` (uso interno
confiable: el SQL lo construye esta funcion, no un cliente externo).