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:
@@ -0,0 +1,158 @@
|
||||
---
|
||||
id: select_groupby_keys_py_datascience
|
||||
name: select_groupby_keys
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def select_groupby_keys(profile: dict, max_keys: int = 3, max_card: int = 20, max_measures: int = 4) -> dict"
|
||||
description: "Elige deterministicamente las columnas categoricas mas interesantes para GROUP BY, las numericas medida y pares pivote a partir de un TableProfile del grupo eda. Respaldo cuantitativo para el capitulo de agregacion/OLAP de un EDA. Funcion pura, no muta el input, nunca lanza."
|
||||
tags: [eda, aggregation, groupby, olap, profiling, datascience]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
example: |
|
||||
from datascience import select_groupby_keys
|
||||
profile = {
|
||||
"n_rows": 891,
|
||||
"key_candidates": ["passenger_id"],
|
||||
"columns": [
|
||||
{"name": "sex", "inferred_type": "categorical", "distinct_count": 2,
|
||||
"unique_pct": 0.002, "null_pct": 0.0, "flags": [],
|
||||
"categorical": {"imbalance": 1.8}, "numeric": None},
|
||||
{"name": "pclass", "inferred_type": "categorical", "distinct_count": 3,
|
||||
"unique_pct": 0.003, "null_pct": 0.0, "flags": [],
|
||||
"categorical": {"imbalance": 2.5}, "numeric": None},
|
||||
{"name": "fare", "inferred_type": "numeric", "distinct_count": 200,
|
||||
"unique_pct": 0.2, "null_pct": 0.0, "flags": [],
|
||||
"numeric": {"std": 49.7, "cv": 1.54}, "categorical": None},
|
||||
],
|
||||
}
|
||||
select_groupby_keys(profile)
|
||||
# {"group_keys": [{"col": "sex", ...}, {"col": "pclass", ...}],
|
||||
# "measures": ["fare"],
|
||||
# "pivots": [{"index": "sex", "columns": "pclass", "value": "fare"}],
|
||||
# "note": "2 clave(s) de grupo: sex, pclass; 1 medida(s): fare; 1 pivot(s)."}
|
||||
tested: true
|
||||
tests:
|
||||
- "test_titanic_picks_good_cats_excludes_id_and_constant"
|
||||
- "test_titanic_measures_exclude_id_constant_and_keep_numerics"
|
||||
- "test_titanic_generates_one_pivot"
|
||||
- "test_empty_profile_returns_all_empty_and_does_not_crash"
|
||||
- "test_none_profile_does_not_crash"
|
||||
- "test_only_numerics_yields_empty_group_keys_and_no_pivots"
|
||||
- "test_high_cardinality_and_max_card_are_excluded"
|
||||
- "test_max_keys_limits_group_keys"
|
||||
- "test_three_keys_cap_pivots_to_two"
|
||||
- "test_does_not_mutate_input"
|
||||
test_file_path: "python/functions/datascience/select_groupby_keys_test.py"
|
||||
file_path: "python/functions/datascience/select_groupby_keys.py"
|
||||
params:
|
||||
- name: profile
|
||||
desc: >
|
||||
TableProfile dict del grupo eda (p.ej. salida de summarize_table_duckdb).
|
||||
Se lee de forma defensiva (.get / or [] / isinstance). Claves usadas:
|
||||
columns (list[ColumnProfile]), key_candidates (list de nombres de columna
|
||||
o dicts {name}), n_rows. Cada ColumnProfile usa: name, inferred_type
|
||||
("numeric"|"categorical"|"datetime"|"text"|"boolean"), distinct_count,
|
||||
unique_pct (0..1), null_pct (0..1), flags (list[str], reconoce
|
||||
"possible_id"/"high_cardinality"/"constant"), numeric ({std, cv, ...}|None)
|
||||
y categorical ({imbalance, mode_pct, ...}|None).
|
||||
- name: max_keys
|
||||
desc: "Numero maximo de claves de grupo (group_keys) a devolver. Default 3."
|
||||
- name: max_card
|
||||
desc: >
|
||||
Cardinalidad maxima (distinct_count) que una columna categorica puede
|
||||
tener para seguir siendo candidata a clave de grupo. Default 20.
|
||||
- name: max_measures
|
||||
desc: "Numero maximo de columnas medida (nombres) a devolver. Default 4."
|
||||
output: >
|
||||
dict con group_keys (list de {col, cardinality, score} ordenada por score
|
||||
desc), measures (list[str] de nombres de columnas numericas ordenadas por
|
||||
dispersion), pivots (list de {index, columns, value}, hasta 2 pares
|
||||
categorica x categorica con la primera measure como valor) y note (str,
|
||||
resumen corto en espanol de lo elegido). Ante profile vacio/None devuelve
|
||||
todas las listas vacias y una note descriptiva; nunca lanza.
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
from datascience import select_groupby_keys
|
||||
|
||||
# TableProfile estilo titanic: 2 categoricas buenas, 1 numerica medida,
|
||||
# 1 id secuencial (descartado) y un key_candidate declarado.
|
||||
profile = {
|
||||
"n_rows": 891,
|
||||
"key_candidates": ["passenger_id"],
|
||||
"columns": [
|
||||
{"name": "sex", "inferred_type": "categorical", "distinct_count": 2,
|
||||
"unique_pct": 0.002, "null_pct": 0.0, "flags": [],
|
||||
"categorical": {"imbalance": 1.8}, "numeric": None},
|
||||
{"name": "pclass", "inferred_type": "categorical", "distinct_count": 3,
|
||||
"unique_pct": 0.003, "null_pct": 0.0, "flags": [],
|
||||
"categorical": {"imbalance": 2.5}, "numeric": None},
|
||||
{"name": "fare", "inferred_type": "numeric", "distinct_count": 200,
|
||||
"unique_pct": 0.2, "null_pct": 0.0, "flags": [],
|
||||
"numeric": {"std": 49.7, "cv": 1.54}, "categorical": None},
|
||||
{"name": "passenger_id", "inferred_type": "numeric", "distinct_count": 891,
|
||||
"unique_pct": 1.0, "null_pct": 0.0, "flags": ["possible_id"],
|
||||
"numeric": {"std": 257.4, "cv": 0.58}, "categorical": None},
|
||||
],
|
||||
}
|
||||
|
||||
select_groupby_keys(profile)
|
||||
# {
|
||||
# "group_keys": [
|
||||
# {"col": "sex", "cardinality": 2, "score": 0.5556},
|
||||
# {"col": "pclass", "cardinality": 3, "score": 0.4},
|
||||
# ],
|
||||
# "measures": ["fare"], # passenger_id excluido (id secuencial)
|
||||
# "pivots": [{"index": "sex", "columns": "pclass", "value": "fare"}],
|
||||
# "note": "2 clave(s) de grupo: sex, pclass; 1 medida(s): fare; 1 pivot(s).",
|
||||
# }
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando hayas perfilado una tabla con el grupo `eda` (p.ej.
|
||||
`summarize_table_duckdb`) y necesites decidir, sin mirar los datos, por qué
|
||||
columnas merece la pena agrupar (GROUP BY) y qué métricas numéricas agregar:
|
||||
el respaldo cuantitativo del capítulo de agregación/OLAP de un AutomaticEDA, o
|
||||
para proponer pivotes en un dashboard. Es la capa de selección sobre el
|
||||
TableProfile crudo: lee el perfil, ordena candidatos de forma determinista y
|
||||
no toca los datos.
|
||||
|
||||
## Notas
|
||||
|
||||
Función pura, sin I/O ni dependencias externas (solo stdlib), no muta
|
||||
`profile`. Lectura defensiva total (`.get`, `or []`, `isinstance`): un `{}` o
|
||||
`None` produce `{"group_keys": [], "measures": [], "pivots": [], "note": ...}`
|
||||
y nunca lanza.
|
||||
|
||||
Criterios de selección (deterministas):
|
||||
|
||||
- **group_keys** — candidatas con `inferred_type` en `("categorical","boolean")`.
|
||||
Se descartan las que estén en `key_candidates`, con flag
|
||||
`possible_id`/`high_cardinality`/`constant`, con `distinct_count` fuera de
|
||||
`[2, max_card]`, o all-null (`null_pct >= 0.999`). `score = card_score *
|
||||
balance_score`: `card_score` mantiene un plateau para cardinalidad moderada
|
||||
(2..12) y decae hacia `max_card`; `balance_score = 1/imbalance` usando
|
||||
`categorical.imbalance` si está, aproximando con `mode_pct` si no, o un valor
|
||||
neutro (0.5) en último caso. Devuelve hasta `max_keys`, ordenadas por score
|
||||
desc (empates por orden de columna).
|
||||
- **measures** — candidatas con `inferred_type` en
|
||||
`("numeric","integer","float")`. Se descartan id-like (flag `possible_id` y
|
||||
`unique_pct >= 0.99`) y constantes (`numeric.std` == 0 o None). Se rankean por
|
||||
dispersión informativa: `abs(cv)` si está, si no `abs(std)`. Devuelve hasta
|
||||
`max_measures` **nombres** (strings).
|
||||
- **pivots** — hasta 2 pares `(group_keys[i].col, group_keys[j].col)` con i<j y
|
||||
la primera measure como valor. Vacío si hay menos de 2 group_keys.
|
||||
|
||||
Caveat de ranking de measures: mezclar `cv` (adimensional) con `std` (en
|
||||
unidades de la columna) cuando una columna carece de `cv` puede dar órdenes
|
||||
poco comparables entre columnas; se prefiere `cv` siempre que esté disponible.
|
||||
Reference in New Issue
Block a user