7ac69ab4fb
Bloque del grupo eda (sesion ausente EDA-benchmark): - 8 funciones nuevas: adf_kpss_stationarity, acf_pacf, stl_decompose, to_returns, fdr_correction, suggest_reexpression, exploratory_caveats, render_eda_pdf - integracion: profile_table (run_series, emit_pdf), association_matrix (FDR Benjamini-Hochberg), render_eda_markdown (secciones series/reexpresion/caveats) - slash commands /eda y /capitulos - issues 0173-0177: mejoras del /eda derivadas del benchmark sobre 12 datasets reales (outlier_pct x100, periodo estacional, FK inference, render models, tipos id-like) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
170 lines
12 KiB
Markdown
170 lines
12 KiB
Markdown
# eda — Exploratory Data Analysis por tabla y base
|
||
|
||
Grupo de capacidad para perfilar tablas y bases de datos completas y entender datasets nuevos rápido, repetible y sin reinventar lógica. Motor **DuckDB SQL push-down**: los agregados (`SUMMARIZE`, `COUNT DISTINCT`, `corr()`, percentiles) se calculan en SQL sin traer las filas a RAM; solo una muestra pequeña baja a Python para lo estadístico fino (skew, kurtosis, histograma, correlación mixta, modelos).
|
||
|
||
Orquestadores one-shot:
|
||
- `profile_table_py_pipelines` — "hazme un EDA de esta tabla" → `TableProfile` completo + report markdown + JSON (+ PDF móvil con `emit_pdf`). Flags `run_models` (modelos baratos), `run_llm` (interpretación LLM), `run_series` (análisis de serie temporal por columna numérica) y `emit_pdf` (PDF vertical legible en móvil). Re-expresión sugerida por columna y avisos exploratorios se añaden siempre.
|
||
- `profile_database_py_pipelines` — "hazme un EDA de esta base" → perfila todas las tablas + infiere FK + join graph (mermaid).
|
||
|
||
> Cuando Enmanuel pide un EDA, el flujo acordado es: perfilar con este grupo, escribir el report, y **generar un analysis Jupyter lanzado en el navegador colaborativo y ejecutado por Claude** para verlo en vivo. Ver la memoria `eda-workflow-registry` y la regla `notebook_collaboration.md`.
|
||
|
||
## Funciones
|
||
|
||
### Perfilado base (tabla y columna)
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `summarize_table_duckdb_py_datascience` | impure | Corazón (DuckDB): `SUMMARIZE` push-down + `COUNT DISTINCT` exacto (≤200k filas) → esqueleto del `TableProfile`. |
|
||
| `summarize_table_pg_py_datascience` | impure | Adaptador PostgreSQL: mismo esqueleto `TableProfile` vía SQL push-down (information_schema + count/distinct/min/max/avg/stddev/percentile_cont). |
|
||
| `describe_numeric_py_datascience` | pure | Bloque numérico: min/max/mean/median/std/cv, p1-p99, IQR, skew, kurtosis, outliers, distribución, histograma. |
|
||
| `summarize_categorical_py_datascience` | pure | top-k frecuencias, mode, distinct, entropía, imbalance, longitudes. |
|
||
| `infer_semantic_type_py_datascience` | pure | Tipo semántico por regex (email/url/ip/uuid/iban/currency/datetime/...). |
|
||
| `column_quality_score_py_datascience` | pure | Score 0-100 (completeness/validity/consistency) + issues. |
|
||
| `render_eda_markdown_py_datascience` | pure | `TableProfile` → report markdown con sparklines ASCII. |
|
||
| `summary_stats_py_datascience` | pure | Descriptiva mínima (n, mean, median, p25, p75). |
|
||
|
||
### Correlación / asociación
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `pearson_py_datascience` | pure | Correlación lineal num↔num (preexistente). |
|
||
| `spearman_corr_py_datascience` | pure | Correlación de rangos (monotónica no lineal) num↔num. |
|
||
| `cramers_v_py_datascience` | pure | Asociación simétrica cat↔cat (corrección Bergsma-Wicher). |
|
||
| `theils_u_py_datascience` | pure | Asociación direccional U(a\|b) cat↔cat. |
|
||
| `correlation_ratio_py_datascience` | pure | η: cuánto explica una categórica a una numérica. |
|
||
| `mutual_info_columns_py_datascience` | pure | Información mutua (no lineal, general) entre cualquier par. |
|
||
| `association_matrix_py_datascience` | pure | Matriz unificada: elige métrica por par de tipos + pares fuertes. |
|
||
| `correlation_matrix_duckdb_py_datascience` | impure | Matriz Pearson push-down (`corr()` SQL) para muchas filas. |
|
||
|
||
### Relaciones inter-tabla
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `infer_fk_containment_duckdb_py_datascience` | impure | Infiere FK candidatas por containment de valores (inclusion coefficient). |
|
||
| `build_join_graph_py_datascience` | pure | FK candidates → grafo (roles fact/dimension) + diagrama Mermaid. |
|
||
|
||
### Modelos baratos (flag `run_models`)
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `pca_explained_py_datascience` | pure | PCA: varianza explicada + loadings + proyección. |
|
||
| `kmeans_segments_py_datascience` | pure | Segmentos naturales, auto-k por silhouette. |
|
||
| `isolation_forest_outliers_py_datascience` | pure | Outliers multivariante (filas anómalas). |
|
||
| `normality_tests_py_datascience` | pure | Jarque-Bera + D'Agostino + Shapiro → ¿normal? |
|
||
| `trend_slope_py_datascience` | pure | Tendencia de una serie (up/down/flat) por regresión lineal. |
|
||
| `run_eda_models_py_datascience` | pure | Wrapper: compone PCA + KMeans + IsolationForest + normalidad → bloque `models`. |
|
||
|
||
### Series temporales (flag `run_series`)
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `adf_kpss_stationarity_py_datascience` | pure | Estacionariedad por consenso ADF + KPSS (hipótesis nulas opuestas) → veredicto `stationary`/`non_stationary`/`inconclusive` + aviso de correlación espuria. |
|
||
| `acf_pacf_py_datascience` | pure | ACF + PACF con bandas de confianza + lags significativos + Ljung-Box (¿ruido blanco?). Detecta autocorrelación que infla los p-valores OLS. |
|
||
| `stl_decompose_py_datascience` | pure | Descomposición STL (tendencia/estacional/resto) + fuerza de tendencia y estacional de Hyndman. Auto-infiere el periodo por autocorrelación. |
|
||
| `to_returns_py_datascience` | pure | Convierte una serie de niveles (precios) a retornos log/simples. Los niveles no son estacionarios; los retornos sí (unidad correcta para correlacionar/modelar). |
|
||
|
||
### Rigor y disciplina exploratoria
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `fdr_correction_py_datascience` | pure | Corrige p-valores por comparaciones múltiples (Benjamini-Hochberg FDR / Bonferroni FWER) → controla el data-mining bias. Ya integrada en `association_matrix`. |
|
||
| `suggest_reexpression_py_datascience` | pure | Escalera de potencias de Tukey: qué transformación (log/sqrt/Yeo-Johnson/...) simetriza mejor una columna numérica según su skew y dominio. No la ejecuta, la sugiere. |
|
||
| `exploratory_caveats_py_datascience` | pure | Genera las advertencias de que el EDA es exploratorio (correlación≠causalidad, overfitting in-sample, comparaciones múltiples, outliers, muestra pequeña, MNAR) según lo que el perfil realmente contiene. |
|
||
|
||
### Capa LLM y entrega
|
||
| ID | Pureza | Qué hace |
|
||
|---|---|---|
|
||
| `eda_llm_insights_py_datascience` | impure | 1 call LLM sobre el perfil agregado (no filas crudas): data dictionary, resumen, granularidad de fila, PII/RGPD, limpieza, análisis sugeridos. |
|
||
| `build_eda_notebook_py_datascience` | impure | Genera un `.ipynb` (nbformat v4) que perfila la tabla, listo para lanzar en Jupyter colaborativo. |
|
||
| `render_eda_pdf_py_datascience` | impure | Renderiza el `TableProfile` a un PDF multipágina **vertical (A5), legible en móvil** (estilo Tufte: histogramas como small multiples, top-k, heatmap de asociación). 4ª salida del workflow, junto a JSON/Markdown/notebook. |
|
||
|
||
### Orquestadores (pipelines)
|
||
| ID | Qué hace |
|
||
|---|---|
|
||
| `profile_table_py_pipelines` | EDA de una tabla end-to-end, `backend="duckdb"` (default) o `"postgres"` (base + correlación con FDR + `run_models` + `run_llm` + `run_series` + re-expresión + caveats) → JSON + markdown (+ PDF móvil con `emit_pdf`). |
|
||
| `profile_database_py_pipelines` | EDA de una base entera: todas las tablas + FK + join graph. |
|
||
|
||
## Contrato de datos
|
||
|
||
```
|
||
TableProfile = {table, source, profiled_at, n_rows, n_cols, size_bytes,
|
||
duplicate_rows, duplicate_pct, constant_cols, all_null_cols, null_cell_pct,
|
||
type_breakdown:{numeric,categorical,datetime,text,boolean},
|
||
columns:[ColumnProfile], correlations, key_candidates, quality_score, llm, models,
|
||
series:{<col>:SeriesBlock}|None, # solo con run_series
|
||
caveats:{n, caveats:[{id,topic,message,reference}], note}} # siempre
|
||
|
||
ColumnProfile = {name, physical_type, inferred_type, semantic_type, count, n_rows,
|
||
null_count, null_pct, empty_count, empty_pct, distinct_count, unique_pct,
|
||
flags:[constant|possible_id|high_cardinality|mostly_null], quality_score,
|
||
numeric:{...}|None, categorical:{...}|None, datetime:{...}|None,
|
||
reexpression:{recommended,ladder_power,reason,alternatives,skew}|None, # cols numéricas
|
||
series:SeriesBlock|None} # solo con run_series
|
||
# *_pct son FRACCIONES 0-1; el render las muestra ×100
|
||
|
||
SeriesBlock = {order_col, ordered, n, stationarity:{adf,kpss,verdict,warning},
|
||
acf_pacf:{acf,pacf,significant_acf_lags,ljung_box,is_autocorrelated},
|
||
stl:{period,trend_strength,seasonal_strength,...},
|
||
to_returns:{...}|absent, levels_suggested:bool}
|
||
|
||
correlations = {pairs:[{a,b,a_type,b_type,method,value,extra,p_value,
|
||
p_value_adjusted,significant}], strong:[...], methods_legend,
|
||
multiple_testing:{method,alpha,n_tests,n_rejected}} # p-valores corregidos por FDR
|
||
models = {n_numeric_cols, pca, kmeans, outliers, normality, note}
|
||
llm = {summary, row_meaning, dictionary:[{column,description,business_meaning,unit}],
|
||
pii:[{column,kind,severity}], cleaning:[str], analyses:[str]}
|
||
```
|
||
|
||
## Ejemplo canónico
|
||
|
||
EDA completo de una tabla (estadística + correlación + modelos + LLM + report):
|
||
|
||
```python
|
||
import sys, os
|
||
sys.path.insert(0, os.path.join("python", "functions"))
|
||
from pipelines.profile_table import profile_table
|
||
|
||
r = profile_table(
|
||
"/ruta/datos.duckdb", "clientes",
|
||
run_models=True, run_llm=True, run_series=True, emit_pdf=True,
|
||
)
|
||
prof = r["profile"]
|
||
print(r["report_md_path"]) # reports/eda_clientes_<ts>.md
|
||
print(r["pdf_path"]) # reports/eda_clientes_<ts>.pdf (móvil)
|
||
print(prof["correlations"]["strong"]) # pares fuertes Y significativos tras FDR
|
||
print(prof["models"]["kmeans"]["best_k"]) # segmentos
|
||
print(prof["series"]["precio"]["stationarity"]["verdict"]) # ¿serie estacionaria?
|
||
print(prof["columns"][0]["reexpression"]["recommended"]) # transformación sugerida
|
||
print(prof["caveats"]["caveats"][0]["message"]) # aviso exploratorio general
|
||
print(prof["llm"]["row_meaning"]) # qué representa 1 fila
|
||
```
|
||
|
||
EDA de una base entera con relaciones:
|
||
|
||
```python
|
||
from pipelines.profile_database import profile_database
|
||
r = profile_database("/ruta/datos.duckdb") # todas las tablas
|
||
print(r["db_profile"]["join_graph"]["mermaid"]) # diagrama de relaciones FK
|
||
```
|
||
|
||
Notebook ejecutable:
|
||
|
||
```python
|
||
from datascience import build_eda_notebook
|
||
build_eda_notebook("/ruta/datos.duckdb", "clientes", "/tmp/eda.ipynb", run_models=True)
|
||
```
|
||
|
||
## Fronteras
|
||
|
||
- **NO carga la tabla entera a RAM**: metadata SQL + muestra por columna/filas (`sample`, default 5000).
|
||
- **Distinct exacto hasta 200k filas**; por encima aproximado capado.
|
||
- **Correlación de tabla** se calcula sobre la muestra de filas alineadas; excluye columnas id-like (alta cardinalidad) para evitar asociación espuria. `correlation_matrix_duckdb` ofrece Pearson push-down exacto a escala si hace falta.
|
||
- **Modelos** (`run_models`) requieren ≥2 columnas numéricas para PCA/KMeans/IsolationForest; normalidad funciona con 1.
|
||
- **LLM** (`run_llm`) hace 1 llamada (haiku) y envía solo el perfil agregado, nunca filas crudas; requiere token OAuth de Claude.
|
||
- **Series** (`run_series`) trata cada columna numérica como serie temporal: si hay una columna datetime se ordena por ella, si no por el orden físico de filas. Necesita ≥8 puntos válidos por columna; STL exige ≥2 periodos. La sugerencia de retornos (`to_returns`) solo aparece en columnas estrictamente positivas y no claramente estacionarias (series de niveles/precios).
|
||
- **PDF** (`emit_pdf`) genera un PDF A5 vertical legible en móvil junto al report markdown vía `render_eda_pdf` (matplotlib `PdfPages`, sin dependencias nuevas).
|
||
- **Correlaciones**: los p-valores de cada par se corrigen por comparaciones múltiples (FDR Benjamini-Hochberg) dentro de `association_matrix`; un par solo entra en `strong` si supera el umbral de magnitud Y es significativo tras la corrección.
|
||
- **Fuentes**: DuckDB nativo (CSV/Parquet/Excel cargándolos antes a DuckDB) y **PostgreSQL** (`backend="postgres"`, DSN vía `resolve_pg_dsn`). BigQuery pendiente. `profile_database` (multi-tabla + FK) es solo DuckDB por ahora.
|
||
|
||
## Estado
|
||
|
||
Implementado y validado end-to-end (152 tests verdes): perfilado base, correlación/asociación (Pearson/Spearman/Cramér's V/Theil's U/η/MI), relaciones inter-tabla (FK + join graph), modelos baratos (PCA/KMeans/IsolationForest/normalidad/tendencia), capa LLM y generación de notebook.
|
||
|
||
Validado sobre PostgreSQL real (tablas del Metabase local del proyecto captacion_clientes).
|
||
|
||
Pendiente: adaptador BigQuery; `profile_database` multi-tabla para PostgreSQL (hoy solo DuckDB); perfil fino de columnas datetime (`profile_datetime`); excluir columnas numéricas `possible_id` de la matriz de asociación (hoy solo se excluyen las categóricas id-like).
|