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