feat(infra): auto-commit con 56 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-21 14:22:55 +02:00
parent c1071a82b3
commit 32c7336bf6
56 changed files with 5307 additions and 100 deletions
+100 -48
View File
@@ -1,80 +1,132 @@
# eda — Exploratory Data Analysis por tabla
# eda — Exploratory Data Analysis por tabla y base
Grupo de capacidad para perfilar tablas y entender datasets nuevos rápido, repetible y sin reinventar lógica. Motor **DuckDB SQL push-down**: los agregados (`SUMMARIZE`, `COUNT DISTINCT`, 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, outliers).
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).
El orquestador one-shot es `profile_table_py_pipelines`: "hazme un EDA de esta tabla" → un `TableProfile` completo + report markdown + JSON sidecar en `reports/`.
Orquestadores one-shot:
- `profile_table_py_pipelines` — "hazme un EDA de esta tabla" → `TableProfile` completo + report markdown + JSON. Flags `run_models` (modelos baratos) y `run_llm` (interpretación LLM).
- `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: `SUMMARIZE` push-down → esqueleto del `TableProfile` con perfil base por columna (tipo inferido, nulls, distinct exacto ≤200k filas, flags). Reusa `duckdb_query_readonly`. |
| `describe_numeric_py_datascience` | pure | Bloque `numeric` sobre una muestra: min/max/mean/median/mode/std/cv, percentiles p1-p99, IQR, skew, kurtosis, outliers, %zeros/%neg, tipo de distribución, histograma. |
| `summarize_categorical_py_datascience` | pure | Bloque `categorical`: top-k frecuencias, mode, distinct, entropía de Shannon (bits), imbalance, longitudes. |
| `infer_semantic_type_py_datascience` | pure | Tipo semántico por regex (email/url/ip/uuid/iban/currency/datetime/integer/decimal/...) sin LLM. Primera pasada barata. |
| `column_quality_score_py_datascience` | pure | Score de calidad 0-100 (completeness/validity/consistency) + issues legibles para un `ColumnProfile`. |
| `render_eda_markdown_py_datascience` | pure | `TableProfile` → report markdown autosuficiente (Overview, Columnas, Numéricas con sparkline ASCII, Categóricas, Calidad). |
| `summary_stats_py_datascience` | pure | Descriptiva mínima (n, mean, median, p25, p75) de una lista de floats. |
| `profile_table_py_pipelines` | pipeline | Orquestador end-to-end: compone todo lo anterior, promueve tipos VARCHAR→numeric/datetime por contenido, y emite `TableProfile` + report markdown + JSON. |
| `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`. |
### 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. |
### Orquestadores (pipelines)
| ID | Qué hace |
|---|---|
| `profile_table_py_pipelines` | EDA de una tabla end-to-end, `backend="duckdb"` (default) o `"postgres"` (base + correlación + `run_models` + `run_llm`) → JSON + markdown. |
| `profile_database_py_pipelines` | EDA de una base entera: todas las tablas + FK + join graph. |
## Contrato de datos
Todas las funciones producen/consumen el mismo shape (dict JSON), lo que desacopla cálculo, render y (futuro) LLM:
```
TableProfile = {
table, source, profiled_at, n_rows, n_cols, size_bytes,
duplicate_rows, duplicate_pct, constant_cols:[str], all_null_cols:[str],
null_cell_pct, type_breakdown:{numeric,categorical,datetime,text,boolean},
columns:[ColumnProfile], correlations, key_candidates:[str],
quality_score, llm, models
}
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}
ColumnProfile = {
name, physical_type, inferred_type, # numeric|categorical|datetime|boolean|text|id
semantic_type, count, n_rows, null_count, null_pct, empty_count, empty_pct,
distinct_count, unique_pct, # *_pct son FRACCIONES 0-1; el render las muestra ×100
flags:[constant|possible_id|high_cardinality|mostly_null],
quality_score,
numeric: {min,max,mean,median,mode,std,variance,cv,p1,p5,p25,p50,p75,p95,p99,iqr,
skew,kurtosis,n_outliers,outlier_pct,zero_pct,negative_pct,distribution_type,
histogram:[{lo,hi,count}]} | None,
categorical: {top:[{value,count,pct}],mode,mode_pct,n_distinct,entropy,imbalance,
len_mean,len_min,len_max} | None,
datetime: {min,max,range_days,granularity,n_gaps,future_pct,monotonic} | None
}
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}
# *_pct son FRACCIONES 0-1; el render las muestra ×100
correlations = {pairs:[{a,b,a_type,b_type,method,value,extra}], strong:[...], methods_legend}
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 de una tabla DuckDB en una línea (escribe `reports/eda_<table>_<ts>.md` + `.json`):
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(os.path.expanduser("~/.fn_freelance/freelance.duckdb"), "freelance_projects")
print(r["status"], r["report_md_path"])
r = profile_table("/ruta/datos.duckdb", "clientes", run_models=True, run_llm=True)
prof = r["profile"]
print(prof["type_breakdown"], "key_candidates:", prof["key_candidates"], "calidad:", prof["quality_score"])
print(r["report_md_path"]) # reports/eda_clientes_<ts>.md
print(prof["correlations"]["strong"]) # pares correlacionados
print(prof["models"]["kmeans"]["best_k"]) # segmentos
print(prof["llm"]["row_meaning"]) # qué representa 1 fila
```
La promoción de tipo por contenido resuelve el caso típico de scrapers/CSV donde los números y fechas llegan como `VARCHAR`: `bids` ('10','20') se detecta `integer` y se perfila como numérica (mean/median/percentiles); `scraped_at` se detecta `datetime_iso`.
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**: solo metadata SQL + una muestra (`sample`, default 5000) por columna. Para distribución exacta de una columna enorme, sube `sample` o consulta SQL directa.
- **Distinct exacto solo hasta 200k filas**; por encima usa aproximado (HyperLogLog) capado a nº de filas.
- **Solo DuckDB** por ahora (CSV/Parquet/Excel entran gratis vía `read_csv_auto`/`read_parquet`/`read_xlsx` cargándolos antes a DuckDB). PostgreSQL y BigQuery requieren adaptador (pendiente).
- **No es estadística inferencial ni modelado**: es perfilado descriptivo. Correlaciones, modelos baratos (PCA/KMeans/IsolationForest) y capa LLM son fases siguientes del grupo.
- **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.
- **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.
## Roadmap (fases siguientes)
## Estado
- **Correlación / asociación**: Spearman, Cramér's V, Theil's U, correlation ratio η², Mutual Information, VIF → `correlations` del `TableProfile`.
- **Relaciones inter-tabla**: FK inference por containment, cardinalidad de relación, join graph (mermaid), star-schema hints → `profile_database`.
- **Modelos baratos** (flag `--models`, sklearn/scipy): PCA 2D, KMeans + silhouette, Isolation Forest, feature importance, tests de normalidad, tendencia temporal.
- **Capa LLM** (flag `--llm`, grupo `claude-direct`): data dictionary, resumen ejecutivo (qué es 1 fila + granularidad), flag PII/RGPD, limpieza sugerida, análisis sugeridos.
- **Entrega notebook**: analysis Jupyter auto-generado y ejecutado en el navegador colaborativo.
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).