--- id: "0173" title: "EDA: bugs críticos de correctitud estadística (outlier_pct ×100, distribution_type por-skew)" status: resuelto 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). ## Resolucion (2026-06-29, sesion /ausente) Resuelto y verificado con re-corrida del benchmark EDA. Commit principal: caf8c25d. Detalle en reports/ausente-eda-benchmark-2026-06-29.md y temp/eda_benchmark/EVALUATION.md.