feat(eda): series temporales + rigor anti-data-mining + PDF movil + /eda + benchmark issues
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>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
---
|
||||
id: "0173"
|
||||
title: "EDA: bugs críticos de correctitud estadística (outlier_pct ×100, distribution_type por-skew)"
|
||||
status: pendiente
|
||||
type: bugfix
|
||||
domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: alta
|
||||
depends: []
|
||||
blocks: []
|
||||
related: ["0174", "0175", "0176", "0177", "0068"]
|
||||
created: 2026-06-29
|
||||
updated: 2026-06-29
|
||||
tags: [eda, datascience, profile_table, render_eda_markdown, describe_numeric, benchmark]
|
||||
---
|
||||
# 0173 — EDA: bugs críticos de correctitud estadística
|
||||
|
||||
## Contexto
|
||||
|
||||
Un benchmark adversarial del workflow `/eda` sobre 12 datasets reales (29/06/2026,
|
||||
`temp/eda_benchmark/EVALUATION.md`) detectó que los estadísticos descriptivos base son
|
||||
correctos, pero el **porcentaje de outliers que el report markdown muestra es imposible**
|
||||
(supera el 100%, hasta 336%), engañando a un lector no experto con apariencia de autoridad.
|
||||
|
||||
Hallazgos cubiertos por este issue:
|
||||
|
||||
| Hallazgo | Severidad | Evidencia del benchmark |
|
||||
|---|---|---|
|
||||
| H1 — `outlier_pct` por-columna >100% en el report markdown | crítico | wine-red `chlorides` 193.87%, `density` 112.57% (skew 0.07); titanic `SibSp` 336.70%, `Fare` 224.47%; seattle `precipitation` 253.25% |
|
||||
| H11 — `distribution_type` por-skew etiqueta mal discretas/ordinales/multimodales | bajo | wine `quality` (6 valores) → "normal-ish"; precios BTC multimodales → "normal-ish" (skew 0.45) |
|
||||
|
||||
### Causa raíz de H1 (verificada en código, READ-ONLY)
|
||||
|
||||
`EVALUATION.md` propuso "corregir la fórmula en `describe_numeric`". **Eso es incorrecto.** Al
|
||||
leer el código:
|
||||
|
||||
- `python/functions/datascience/describe_numeric.py:113` calcula
|
||||
`outlier_pct = 100.0 * n_outliers / n` — ya en escala 0-100 y acotado a [0,100]. **Está bien.**
|
||||
- `python/functions/datascience/render_eda_markdown.py:203-204` renderiza ese valor con
|
||||
`_fmt_pct(val)`, y `_fmt_pct` (líneas 31-44) hace `num * 100` porque **asume que su input es
|
||||
una fracción 0-1**. Resultado: **doble ×100** (un 1.94 real se muestra como 193.87%).
|
||||
- El PDF (`render_eda_pdf.py:296`) usa `_fmt_num(outlier_pct, 1) + "%"` sin multiplicar — por eso
|
||||
el PDF muestra el outlier_pct correcto y el markdown no. El bug es **exclusivo del renderer
|
||||
markdown**.
|
||||
|
||||
El factor "19-40×" que observó el evaluador se debe a que comparaba contra outliers IQR (3-10%),
|
||||
mientras `describe_numeric` usa z-score (umbral 3.0, da menos outliers); pero el mecanismo del bug
|
||||
es el doble ×100, no la fórmula.
|
||||
|
||||
## Tareas
|
||||
|
||||
1. **H1 (fix de 1 línea):** en `python/functions/datascience/render_eda_markdown.py:203-204`,
|
||||
sustituir `_fmt_pct(val)` por un formateo que NO multiplique (p.ej. `f"{_fmt_num(val, 2)}%"`),
|
||||
porque `numeric.outlier_pct` ya viene en escala 0-100. **No tocar** `describe_numeric.py` (su
|
||||
fórmula es correcta).
|
||||
2. Auditar el resto de `render_eda_markdown.py` por si otro campo en escala 0-100 pasa por
|
||||
`_fmt_pct` (los `*_pct` del perfil base sí son fracciones 0-1 y deben seguir con `_fmt_pct`;
|
||||
solo `numeric.outlier_pct` está en escala 0-100). Documentar en el docstring de `describe_numeric`
|
||||
que `outlier_pct` está en 0-100 para evitar la confusión a futuro.
|
||||
3. **H11:** en `python/functions/datascience/detect_distribution_type.py`, no etiquetar por skew
|
||||
solamente: usar también nº de modos / cardinalidad y, cuando esté disponible, el test de
|
||||
normalidad Jarque-Bera (`normality_tests.py`, ya expuesto en `models.normality` vía
|
||||
`run_eda_models`). Una variable discreta/ordinal/multimodal no debe salir "normal-ish".
|
||||
4. Añadir/extender tests unitarios: `describe_numeric_test.py` (outlier_pct en [0,100]),
|
||||
`render_eda_markdown_test.py` (un perfil con `outlier_pct=7.0` renderiza `"7.00%"`, no `"700%"`),
|
||||
y un test de `detect_distribution_type` (discreta de 6 valores no se etiqueta "normal-ish"). Nota:
|
||||
hoy NO existe `detect_distribution_type_test.py` en `python/functions/datascience/` — hay que
|
||||
crearlo (a confirmar el nombre canónico al implementar; el resto de tests citados sí existen).
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: outlier_pct en rango | e2e | re-correr `profile_table` sobre `temp/eda_benchmark/datasets/.../wine-red` y leer el `.md` | `chlorides`/`density` muestran `outlier_pct` en [0,100]% (no 193.87% / 112.57%) |
|
||||
| Edge: skew alto real | unit | `describe_numeric_test.py` con datos de cola fuerte | `outlier_pct` ≤ 100 y coherente con n_outliers/n |
|
||||
| Edge: discreta ordinal | unit | `detect_distribution_type_test.py` con 6 valores discretos | NO etiqueta "normal-ish" |
|
||||
| Error: input vacío/no numérico | unit | `describe_numeric([])` | claves None, sin crash (contrato actual preservado) |
|
||||
| Mecánica | — | `./fn run describe_numeric_py_datascience`, `./fn run render_eda_markdown_py_datascience` | tests verdes; `fn index` limpio |
|
||||
|
||||
Re-correr el benchmark sobre wine-red y titanic y confirmar que ningún `outlier_pct` supera 100%.
|
||||
|
||||
## Notas
|
||||
|
||||
Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md` (consolidación del benchmark). H1 es el fix de
|
||||
mayor ratio impacto/esfuerzo del lote (una línea elimina los números imposibles que más minan la
|
||||
confianza del report). Hermanos: 0174 (series), 0175 (relational), 0176 (render), 0177 (tipos).
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
id: "0174"
|
||||
title: "EDA series temporales: período estacional roto + correlación de niveles + to_returns ciego"
|
||||
status: pendiente
|
||||
type: bugfix
|
||||
domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: alta
|
||||
depends: []
|
||||
blocks: []
|
||||
related: ["0173", "0175", "0176", "0177"]
|
||||
created: 2026-06-29
|
||||
updated: 2026-06-29
|
||||
tags: [eda, datascience, stl_decompose, profile_table, to_returns, series, benchmark]
|
||||
---
|
||||
# 0174 — EDA series temporales: período estacional + correlación de niveles
|
||||
|
||||
## Contexto
|
||||
|
||||
El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) confirmó que la
|
||||
estacionariedad (ADF+KPSS), la autocorrelación (Ljung-Box) y el aviso de espuriedad
|
||||
Granger-Newbold están **bien** (verificados a mano con `statsmodels`). Pero el **detector de
|
||||
período estacional está roto**, lo que produce falsos negativos de estacionalidad, y la
|
||||
correlación de precios se calcula sobre niveles (espuria para uso financiero).
|
||||
|
||||
Hallazgos cubiertos:
|
||||
|
||||
| Hallazgo | Severidad | Evidencia del benchmark |
|
||||
|---|---|---|
|
||||
| H2 — período estacional sale `2` casi siempre → `seasonal_strength=0` | crítico | seattle `temp_max` reporta "sin estacionalidad" (`period=2`); STL real con `period=365` da fuerza estacional **0.843**. UNRATE (mensual) debería usar 12, no 2 |
|
||||
| H8 — correlación de precios sobre niveles marcada `sig=sí` | medio-alto | aapl/btc `Close–Open=0.998 sig=sí`: espuria por construcción (niveles autocorrelados no estacionarios) |
|
||||
| H13 — `to_returns` sugerido ciegamente a temperatura (sin sentido físico) | bajo | seattle `temp_max`: "convertir a retornos"; debería ser "diferencias" |
|
||||
|
||||
### Causa raíz H2 (verificada en código, READ-ONLY)
|
||||
|
||||
`python/functions/datascience/stl_decompose.py:34-58` (`_infer_period`) busca el lag entre 2 y
|
||||
`max_period` que maximiza la autocorrelación **cruda** de la serie. En cualquier serie con
|
||||
tendencia (precios, temperatura), la autocorrelación decae monótonamente desde el lag mínimo, así
|
||||
que **el lag 2 casi siempre gana** → `period=2` espurio y un STL con componente estacional que es
|
||||
ruido (`seasonal_strength≈0`). Además, `python/functions/pipelines/profile_table.py:175`
|
||||
(`_build_series_block`) llama `stl_decompose(series_vals)` **sin pasar el período**, pese a que el
|
||||
pipeline ya conoce la columna de orden temporal (`order_col`) y podría derivar la frecuencia.
|
||||
|
||||
## Tareas
|
||||
|
||||
1. **H2 — arreglar la inferencia de período** en `stl_decompose.py:34-58`. Opciones (preferir la
|
||||
robusta): (a) detrend antes de autocorrelar; (b) buscar picos en el periodograma/FFT en vez del
|
||||
primer lag; (c) **derivar el período de la frecuencia del índice datetime** (mensual→12,
|
||||
diario→7 y/o 365) — la señal más fiable.
|
||||
2. **H2 — pasar el período desde el pipeline:** en `profile_table.py:_build_series_block`, cuando
|
||||
exista `order_col` datetime, inferir la frecuencia del índice y pasar `period=` explícito a
|
||||
`stl_decompose`. Si no se puede determinar un período fiable, que `stl_decompose` **no reporte
|
||||
`seasonal_strength=0`** como conclusión: devolver `note` "período no determinado" (ya hay una
|
||||
rama así en `:139-145`; extenderla a los casos que hoy caen en `period=2`).
|
||||
3. **H8 — correlación sobre retornos para series no estacionarias:** en la sección de correlaciones
|
||||
de `profile_table.py:346-384`, cuando una columna sea una serie no estacionaria de niveles
|
||||
(verdict `non_stationary`/`inconclusive`, ya detectado), correlacionar sobre retornos/diferencias
|
||||
(`to_returns`, ya importado) o marcar esos pares de niveles como "posible espuria" junto a la
|
||||
tabla. El aviso global existe pero está lejos de los números.
|
||||
4. **H13 — retornos vs diferencias por semántica:** en `profile_table.py:189` / `to_returns.py`,
|
||||
elegir "retornos" (financiero, estrictamente positivo multiplicativo) vs "diferencias" (físico,
|
||||
aditivo) según la naturaleza, o usar "diferencias" por defecto cuando no haya señal financiera.
|
||||
5. Tests: `stl_decompose_test.py` (serie sintética mensual con estacionalidad anual → período
|
||||
correcto y `seasonal_strength` alta; serie con tendencia sin estacionalidad → nota, no
|
||||
`period=2`); cobertura de `_build_series_block` con `order_col` datetime.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: estacionalidad anual | e2e | re-correr `profile_table` con `run_series=True` sobre seattle `temp_max` | `seasonal_strength ≈ 0.84` con período ≈ 365 (NO "sin estacionalidad", NO `period=2`) |
|
||||
| Edge: serie mensual | unit | `stl_decompose_test.py` serie mensual sintética con ciclo 12 | período inferido 12 y fuerza estacional alta |
|
||||
| Edge: sin estacionalidad | unit | `stl_decompose_test.py` serie con solo tendencia | `note` "período no determinado", NO `seasonal_strength=0` como conclusión |
|
||||
| Error: serie corta | unit | `stl_decompose([...]<2*period)` | nota "serie corta", sin crash (contrato actual) |
|
||||
| H8 | e2e | re-correr `profile_table` sobre aapl/btc | pares de niveles no estacionarios marcados como posible espuria o correlación sobre retornos |
|
||||
| Mecánica | — | `./fn run stl_decompose_py_datascience`; `fn index` | tests verdes; índice limpio |
|
||||
|
||||
Re-correr el benchmark sobre seattle, fred-unrate, aapl y btc y confirmar que la estacionalidad se
|
||||
detecta donde existe y no se inventa donde no.
|
||||
|
||||
## Notas
|
||||
|
||||
Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. H2 es el segundo bloqueante de fiabilidad: un
|
||||
"sin estacionalidad" donde la hay es un falso negativo que un decisor creería. La estacionariedad ya
|
||||
funciona — no tocarla. Hermanos: 0173, 0175, 0176, 0177.
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
id: "0175"
|
||||
title: "EDA relational: precisión de FK inference (falsos positivos) + filtrar VIEWs + test ATTACH"
|
||||
status: pendiente
|
||||
type: bugfix
|
||||
domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: alta
|
||||
depends: []
|
||||
blocks: []
|
||||
related: ["0173", "0174", "0176", "0177"]
|
||||
created: 2026-06-29
|
||||
updated: 2026-06-29
|
||||
tags: [eda, datascience, infer_fk_containment_duckdb, build_join_graph, profile_database, duckdb, benchmark]
|
||||
---
|
||||
# 0175 — EDA relational: precisión de FK inference + filtrar VIEWs
|
||||
|
||||
## Contexto
|
||||
|
||||
El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) confirmó que la inferencia de
|
||||
claves foráneas a nivel de base es **inútil por falsos positivos masivos** y que las VISTAS se
|
||||
perfilan como tablas base. El join graph resultante necesita filtrado manual para ser legible.
|
||||
|
||||
Hallazgos cubiertos:
|
||||
|
||||
| Hallazgo | Severidad | Evidencia del benchmark |
|
||||
|---|---|---|
|
||||
| H3 — FK inference por contención: 10-20× falsos positivos | crítico | chinook 111 candidatas vs ~11 reales; sakila 565 vs ~30. Casos absurdos: `InvoiceLine.Quantity→Album.AlbumId`, `Genre.GenreId→{Album,Artist,Customer,…}` |
|
||||
| H5 — VIEWs perfiladas como tablas base | alto | sakila `n_tables=21` incluye 5 VISTAS (`customer_list`, `film_list` 5462 filas, `staff_list`, `sales_by_store`, `sales_by_film_category`) + `film_text` (FTS, 0 filas) |
|
||||
| H10 — coste relacional gastado en computar FK falsas | medio | sakila 31.82s: la mayoría en INTERSECT de los 565 pares candidatos, casi todos falsos |
|
||||
| H14 — bug `sqlite_master does not exist` tras ATTACH (ya parcheado, falta test) | bajo (resuelto) | `_run.log`: `profile_database` falló con `Catalog Error: src.sqlite_master`; re-run posterior `ok` |
|
||||
|
||||
### Causa raíz (verificada en código, READ-ONLY)
|
||||
|
||||
- `python/functions/datascience/infer_fk_containment_duckdb.py:217-285` emite una FK candidata si
|
||||
`inclusion(A⊆B) ≥ min_inclusion` **y** B "parece clave" (unicidad ≥0.95). **No usa el nombre de
|
||||
la columna**, que es la señal más fuerte de FK (`AlbumId→Album.AlbumId`), ni excluye columnas
|
||||
no-clave (cantidades, importes) como ORIGEN. Enteros pequeños (`GenreId` 1..25) están contenidos
|
||||
en casi todo → ruido.
|
||||
- `python/functions/pipelines/profile_database.py:155-159` lista tablas con `duckdb_list_tables`
|
||||
sin filtrar `table_type` → perfila VIEWs y tablas FTS como base (H5), lo que infla el universo de
|
||||
pares y multiplica las FK falsas (relaciona H10).
|
||||
- H10 es el **mismo cambio** que H3: filtrar candidatos por nombre **antes** del INTERSECT reduce
|
||||
pares (más rápido) y falsos positivos (más preciso) a la vez.
|
||||
|
||||
## Tareas
|
||||
|
||||
1. **H3+H10 — señal de nombre en `infer_fk_containment_duckdb.py:217-285`:** antes de lanzar el
|
||||
INTERSECT, exigir coincidencia/patrón de nombre entre origen y destino (`from_col` casa con
|
||||
`to_table`/`to_col`, patrón `<X>Id → <X>.<X>Id`; case-insensitive). Excluir como ORIGEN columnas
|
||||
claramente no-clave (cantidades, importes, flags) por heurística de nombre/tipo. Esto poda el
|
||||
O(tablas²×columnas²) y elimina la mayoría de los falsos positivos. Validar mejor la cardinalidad
|
||||
(los `1:1` imposibles del benchmark).
|
||||
2. **H5 — filtrar VIEWs** antes de perfilar e inferir FK: filtrar `table_type='BASE TABLE'` vía
|
||||
`information_schema.tables` / `duckdb_tables()`. Decidir (a confirmar al implementar) si el filtro
|
||||
va como flag nuevo en `duckdb_list_tables` (infra, reutilizable) o en `profile_database.py` tras
|
||||
listar. Preferir el flag en `duckdb_list_tables` si no rompe consumidores.
|
||||
3. **H3 — propagar al join graph:** verificar que `build_join_graph.py` recibe la lista ya filtrada
|
||||
y que el diagrama Mermaid resultante es legible (sin nodos VIEW ni aristas espurias).
|
||||
4. **H14 — test de regresión:** añadir test (en `profile_database_test.py` o
|
||||
`infer_fk_containment_duckdb_test.py`) que haga `ATTACH` de una base SQLite pequeña en DuckDB y
|
||||
perfile, confirmando que se usa `information_schema`/`duckdb_tables()` y nunca `sqlite_master`.
|
||||
(A confirmar: localizar la función que hace el ATTACH —probablemente `summarize_table_duckdb.py`
|
||||
o una primitiva infra `duckdb_*`— para cubrirla.)
|
||||
5. Tests: casos sintéticos con tablas que tengan columnas tipo `XId` (FK real) y columnas de
|
||||
cantidad contenidas en claves (falso positivo) → confirmar que solo emite las reales.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: FK reales sin ruido | e2e | re-correr `profile_database` sobre chinook | ~11 FK candidatas (no 111); incluyen `Album.ArtistId→Artist.ArtistId`, `Invoice.CustomerId→Customer.CustomerId`; NO incluyen `InvoiceLine.Quantity→Album.AlbumId` |
|
||||
| Edge: VIEWs excluidas | e2e | re-correr `profile_database` sobre sakila | `n_tables` cuenta solo BASE TABLE (sin `customer_list`/`film_list`/…); FK candidatas ≪ 565 |
|
||||
| Edge: cantidad vs clave | unit | `infer_fk_containment_duckdb_test.py` con columna `Quantity` contenida en una clave | NO emite FK desde `Quantity` |
|
||||
| Error: ATTACH SQLite | unit | test de regresión ATTACH SQLite→DuckDB | perfila sin `sqlite_master does not exist`; usa information_schema |
|
||||
| Rendimiento (H10) | e2e | medir duración de `profile_database` sobre sakila | menor que el baseline 31.82s (menos INTERSECT) |
|
||||
| Mecánica | — | `./fn run infer_fk_containment_duckdb_py_datascience`, `./fn run profile_database_py_pipelines`; `fn index` | tests verdes; índice limpio |
|
||||
|
||||
Re-correr el benchmark sobre chinook y sakila y confirmar que las FK reales son distinguibles del
|
||||
ruido y que las VIEWs no se cuentan como tablas.
|
||||
|
||||
## Notas
|
||||
|
||||
Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. Tres síntomas (H3/H5/H10) con un núcleo común:
|
||||
la capa de inferencia de relaciones inter-tabla. Atacarlos juntos en una rama; filtrar VIEWs reduce
|
||||
el universo de pares y filtrar candidatos por nombre arregla precisión y velocidad a la vez. H14 ya
|
||||
está parcheado en producción; este issue solo añade el test de regresión que faltaba.
|
||||
Hermanos: 0173, 0174, 0176, 0177.
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
id: "0176"
|
||||
title: "EDA render: models/series/caveats en markdown+PDF + PDF para profile_database"
|
||||
status: pendiente
|
||||
type: feature
|
||||
domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: ["0173", "0174", "0175", "0177"]
|
||||
created: 2026-06-29
|
||||
updated: 2026-06-29
|
||||
tags: [eda, datascience, render_eda_markdown, render_eda_pdf, profile_database, pdf, benchmark]
|
||||
---
|
||||
# 0176 — EDA render: models/series/caveats en markdown+PDF + PDF para profile_database
|
||||
|
||||
## Contexto
|
||||
|
||||
El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) confirmó que la información de
|
||||
modelos (PCA/KMeans) está completa en el JSON pero **no llega legible a ningún formato**, y que el
|
||||
análisis relacional no tiene salida móvil (PDF). El tercio final del PDF queda ilegible.
|
||||
|
||||
Hallazgos cubiertos:
|
||||
|
||||
| Hallazgo | Severidad | Evidencia del benchmark |
|
||||
|---|---|---|
|
||||
| H4 — `models` omitido en Markdown; `models`/`series`/`caveats` como dict crudo truncado en PDF | alto | wine-red `.md` (12 numéricas, PCA valioso) → cero menciones de models. PDF aapl: `- pca: {'n_components': 2, …` cortado a media línea |
|
||||
| H9 — `profile_database` no genera PDF | medio | chinook y sakila con `pdf=null`; análisis relacional solo en Markdown |
|
||||
|
||||
### Causa raíz (verificada en código, READ-ONLY)
|
||||
|
||||
- `python/functions/datascience/render_eda_markdown.py`: tiene formatters para `series` (`:337`) y
|
||||
`caveats` (`:407`), pero **no para `models`** → el bloque PCA/KMeans nunca se renderiza en MD.
|
||||
- `python/functions/datascience/render_eda_pdf.py:50-55`: `_KNOWN_TOP_KEYS` **no incluye** `models`,
|
||||
`series` ni `caveats`, así que caen en `_generic_pages` (`:479-495`) → `_wrap_value` →
|
||||
`str(dict)` truncado a 60-64 chars. Por eso esas tres secciones salen como dict crudo en el PDF.
|
||||
- `python/functions/pipelines/profile_database.py:205-218`: solo escribe MD+JSON, nunca invoca
|
||||
`render_eda_pdf`; no tiene param `emit_pdf`.
|
||||
|
||||
## Tareas
|
||||
|
||||
1. **H4 — markdown:** añadir una sección `## Modelos` (PCA/KMeans/outliers/normalidad) a
|
||||
`render_eda_markdown.py`, formateando `models.pca` (varianza explicada, top loadings, acumulada),
|
||||
`models.kmeans` (best_k, silhouette, tamaños de cluster) y `models.outliers` como tablas legibles.
|
||||
2. **H4 — PDF:** en `render_eda_pdf.py`, añadir builders dedicados para `models`, `series` y
|
||||
`caveats` (tablas/listas, no `str(dict)`) y registrarlos en `_KNOWN_TOP_KEYS` + en la lista
|
||||
`builders` (`:595-604`) para sacarlos del volcado genérico. Mantener el contrato dict-no-throw
|
||||
(una sección que falle no aborta el PDF).
|
||||
3. **Unificar renderers:** asegurar que MD y PDF cubren el mismo conjunto de secciones (`models`,
|
||||
`series`, `caveats`) para que no diverjan otra vez.
|
||||
4. **H9 — PDF relational:** añadir un renderer PDF DB-level (puede ser una variante en
|
||||
`render_eda_pdf.py` o una función nueva) con: portada de la base, resumen de tablas, join graph
|
||||
filtrado (tras 0175), y FK candidatas. Añadir param `emit_pdf` a `profile_database.py` que lo
|
||||
invoque y devuelva `pdf_path`.
|
||||
5. Tests: `render_eda_markdown_test.py` (perfil con `models` → aparece sección Modelos);
|
||||
`render_eda_pdf_test.py` (perfil con `models`/`series`/`caveats` → NO aparecen como `str(dict)`;
|
||||
`n_pages` incrementa); test de `profile_database(emit_pdf=True)` → `pdf_path` no nulo, PDF válido.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: models en MD | e2e | re-correr `profile_table(run_models=True)` sobre wine-red y leer el `.md` | sección `## Modelos` con PCA (varianza explicada) y KMeans (silhouette) legibles |
|
||||
| Golden: PDF legible | e2e | re-correr sobre aapl y `pdftotext` del PDF | `models`/`series`/`caveats` como tablas, sin `{'n_components': 2, …` truncado |
|
||||
| Edge: perfil sin models | unit | `render_eda_markdown_test.py`/`render_eda_pdf_test.py` con `models=None` | sección omitida limpiamente, sin crash |
|
||||
| Edge: PDF relational | e2e | `profile_database(emit_pdf=True)` sobre chinook | `pdf_path` no nulo; PDF con resumen de tablas + join graph |
|
||||
| Error: sección corrupta | unit | `render_eda_pdf` con una sección con tipo inesperado | esa sección se omite con nota; PDF sigue válido (≥1 página) |
|
||||
| Mecánica | — | `./fn run render_eda_markdown_py_datascience`, `./fn run render_eda_pdf_py_datascience`; `fn index` | tests verdes; índice limpio |
|
||||
|
||||
Re-correr el benchmark sobre un single-table con modelos (wine-red) y sobre un relational (chinook)
|
||||
y confirmar que models llega al MD y al PDF, y que `profile_database` emite PDF.
|
||||
|
||||
## Notas
|
||||
|
||||
Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. Tipo `feature` porque, además de arreglar el
|
||||
volcado crudo (H4, fix), añade un renderer PDF relational nuevo (H9). La información ya existe en el
|
||||
JSON; este issue solo la hace legible en las dos salidas pensadas para humanos. Hermanos: 0173, 0174,
|
||||
0175, 0177.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
id: "0177"
|
||||
title: "EDA tipos: id secuencial fuera de correlación/PCA + η² espurio por cardinalidad + re-expresión no-continuas"
|
||||
status: pendiente
|
||||
type: bugfix
|
||||
domain:
|
||||
- registry-quality
|
||||
scope: registry-only
|
||||
priority: media
|
||||
depends: []
|
||||
blocks: []
|
||||
related: ["0173", "0174", "0175", "0176"]
|
||||
created: 2026-06-29
|
||||
updated: 2026-06-29
|
||||
tags: [eda, datascience, profile_table, association_matrix, correlation_ratio, run_eda_models, suggest_reexpression, benchmark]
|
||||
---
|
||||
# 0177 — EDA tipos: id secuencial fuera de correlación/PCA + η² espurio
|
||||
|
||||
## Contexto
|
||||
|
||||
El benchmark `/eda` (29/06/2026, `temp/eda_benchmark/EVALUATION.md`) **refutó** el riesgo temido
|
||||
(que el EDA excluyera columnas financieras `Open/Close/High/Low/Volume` por marcarlas id-like: NO
|
||||
ocurre, aparecen en todo). Pero detectó el **problema inverso**: el flag `possible_id` es cosmético
|
||||
y no excluye lo que sí debería (índices secuenciales), y la razón de correlación η² da artefactos
|
||||
≈1 por cardinalidad.
|
||||
|
||||
Hallazgos cubiertos:
|
||||
|
||||
| Hallazgo | Severidad | Evidencia del benchmark |
|
||||
|---|---|---|
|
||||
| H7 — `possible_id` no excluye id secuencial (`PassengerId`) de correlación ni de PCA/KMeans | medio-alto | titanic `PassengerId–Cabin` η²=0.897 `sig=sí`; `models.pca.n_features=7` incluye `PassengerId`, `Survived`, `Pclass` |
|
||||
| H6 — `correlation_ratio` (η²) ≈1 espurio cuando la categórica tiene cardinalidad ≈ n | alto | titanic `Ticket–Fare=1 sig=sí` (`Ticket` 681 distintos/891); aapl/btc/seattle/fred `Date–* =1` |
|
||||
| H12 — `suggest_reexpression` sugiere fila para binarias/ordinales/ids (aunque sea `none`) | bajo | titanic `Survived` (0/1), `Pclass` (ordinal), `PassengerId` (id) listadas |
|
||||
|
||||
### Causa raíz (verificada en código, READ-ONLY)
|
||||
|
||||
- `python/functions/pipelines/profile_table.py:356-361` (`_skip_for_assoc`) excluye de la matriz de
|
||||
asociación las columnas id-like **categóricas/text** (`possible_id`/`high_cardinality`), pero **no**
|
||||
excluye numéricas secuenciales (`PassengerId` es numérica con `possible_id`) ni columnas datetime.
|
||||
El `assoc_input` resultante se pasa tal cual a `run_eda_models` (`:391`), así que el id secuencial,
|
||||
el target binario y el ordinal entran como features de PCA/KMeans.
|
||||
- H6: `correlation_ratio.py` calcula η² sin guard de cardinalidad; cuando cada grupo tiene ~1
|
||||
observación (categórica de cardinalidad ≈ n), la varianza intra-grupo ≈0 → η²≈1 trivialmente. El
|
||||
FDR no protege (artefacto determinista, no azar).
|
||||
- H12: `suggest_reexpression` (llamado en `profile_table.py:300` para toda numérica) no salta
|
||||
binarias/ordinales/ids.
|
||||
|
||||
## Tareas
|
||||
|
||||
1. **H7 — distinguir id secuencial de float continuo:** en la detección de tipos
|
||||
(`summarize_table_duckdb.py` / lógica de `possible_id`) o en `profile_table.py`, marcar
|
||||
"índice entero secuencial/monótono" distinto de "float continuo de alta cardinalidad". El primero
|
||||
se excluye de correlación y de PCA/KMeans; el segundo se mantiene (precios). **Nunca** excluir
|
||||
floats continuos.
|
||||
2. **H7 — excluir no-features de los modelos:** en `_skip_for_assoc` (y/o en `run_eda_models.py`)
|
||||
excluir de PCA/KMeans los ids secuenciales, binarias, ordinales y el target evidente, además de
|
||||
las categóricas id-like que ya se excluyen.
|
||||
3. **H6 — guard de cardinalidad en η²:** en `correlation_ratio.py` (y/o al construir los pares en
|
||||
`association_matrix.py`/`profile_table.py`), no computar η² si la categórica tiene cardinalidad
|
||||
cercana a `n` o tamaño de grupo medio ≈1; excluir columnas datetime/id de los pares categóricos.
|
||||
4. **H12 — saltar no-continuas en re-expresión:** en `suggest_reexpression.py` (o en la llamada de
|
||||
`profile_table.py:300`), no emitir fila de re-expresión para binarias/ordinales/ids.
|
||||
5. Tests: `correlation_ratio_test.py` (categórica cardinalidad≈n → no η²≈1 espurio);
|
||||
`run_eda_models_test.py` (id secuencial/target/ordinal no entran como features);
|
||||
`suggest_reexpression_test.py` (binaria/ordinal/id → sin sugerencia).
|
||||
|
||||
## Definition of Done
|
||||
|
||||
| Escenario | Tipo | Comando / evidencia | Resultado esperado |
|
||||
|---|---|---|---|
|
||||
| Golden: id secuencial fuera | e2e | re-correr `profile_table(run_models=True)` sobre titanic | `PassengerId` NO aparece en correlaciones ni en `models.pca.features`; floats continuos (precios en aapl/btc) SÍ se conservan |
|
||||
| Golden: η² sin artefacto | e2e | re-correr sobre titanic | `Ticket–Fare` y `Date–*` NO aparecen como par fuerte η²=1 |
|
||||
| Edge: float continuo | unit | `correlation_ratio_test.py` / detección de tipos | columna float de alta cardinalidad (precio) se mantiene en correlación |
|
||||
| Edge: re-expresión | unit | `suggest_reexpression_test.py` con binaria/ordinal/id | sin fila de re-expresión |
|
||||
| Error: solo numéricas | unit | `run_eda_models` con assoc_input vacío tras filtrar | sin crash; bloque models coherente |
|
||||
| Mecánica | — | `./fn run correlation_ratio_py_datascience`, `./fn run run_eda_models_py_datascience`, `./fn run suggest_reexpression_py_datascience`; `fn index` | tests verdes; índice limpio |
|
||||
|
||||
Re-correr el benchmark sobre titanic (id secuencial + η² espurio) y sobre aapl/btc (confirmar que
|
||||
los floats financieros NO se excluyen) y verificar ambos comportamientos.
|
||||
|
||||
## Notas
|
||||
|
||||
Issue derivado de `temp/eda_benchmark/EDA_ISSUES.md`. El warning "grave" del benchmark (excluir
|
||||
columnas financieras) quedó **refutado**: este issue arregla el problema inverso real (no excluir
|
||||
ids secuenciales) sin tocar el tratamiento correcto de los floats continuos. Hermanos: 0173, 0174,
|
||||
0175, 0176.
|
||||
Reference in New Issue
Block a user