feat(eda): capítulo MISSINGNESS — patrones de datos faltantes (co-ocurrencia + MCAR/MAR)

Añade el capítulo `missingness` al motor AutomaticEDA, complemento natural de
`calidad`: donde calidad reporta cuánto falta por columna, este capítulo analiza
el PATRÓN de los nulos — dónde faltan y si las columnas faltan juntas
(co-ocurrencia de ausencias), la señal que distingue MCAR de MAR antes de imputar.

Capítulo (`chapters/missingness.py`), registrado en `chapters_registry.py` justo
tras `calidad`:
- Resumen global: % de celdas faltantes, columnas con nulos, filas completas vs
  incompletas.
- Ranking por columna (tabla + barras horizontales).
- Co-ocurrencia: correlación de las máscaras is-null entre columnas (heatmap +
  tabla de los pares que co-faltan, con co-faltantes y Jaccard).
- Patrones de fila más frecuentes (estilo matriz de missingno).
- Lectura MCAR/MAR exploratoria (heurística por correlación/solape de ausencias,
  no confirmatoria), que cita la evidencia concreta.
- Términos de glosario clicables: missingness, MCAR, MAR.

La máscara is-null por fila de TODAS las columnas (numéricas y categóricas) se
construye con un push-down DuckDB sobre ctx['db_path']/table (mismo patrón que el
capítulo agregación), con fallback a ctx['raw_numeric'] cuando no hay BD. Activa
solo si la tabla tiene nulos; si no, devuelve None.

Funciones nuevas del grupo `eda` (dominio datascience):
- extract_null_mask (impura): máscara is-null por fila vía query_fn.
- missingness_overview (pura): resumen global + filas completas/incompletas.
- missingness_correlation (pura): correlación de ausencias + pares + Jaccard,
  reutiliza pearson.
- missingness_row_patterns (pura): patrones de fila más comunes.
- missingness_corr_heatmap_figure / missingness_rank_bar_figure (impuras): figuras.

Verificado: EDA de titanic genera el capítulo en PDF + PPTX + MD con Cabin 77.1%,
Age 19.9% y la co-ocurrencia Age↔Cabin (158 filas). Suite completa de AutomaticEDA
+ render_automatic_eda en verde (125 passed); tests por función y por capítulo;
fn index sin error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 20:38:39 +02:00
parent a1e2e3567c
commit 7fa19d65db
21 changed files with 2624 additions and 0 deletions
@@ -0,0 +1,93 @@
---
id: missingness_rank_bar_figure_py_datascience
name: missingness_rank_bar_figure
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def missingness_rank_bar_figure(names, pcts, title=\"% de valores faltantes por columna\") -> \"matplotlib.figure.Figure\""
description: "Construye una figura matplotlib de barras horizontales que ordena las columnas de un dataset por su porcentaje de valores faltantes (0-100), la mayor arriba, etiquetando cada barra con su NN.N% al final. Usa ax.barh, eje X fijo 0-100 y labels truncados a ~22 chars. Devuelve un matplotlib.figure.Figure listo para rasterizar por el renderer del informe EDA (capítulo de datos faltantes). Backend Agg sin pyplot global; defensivo ante listas vacías, longitudes desiguales o valores no numéricos (nunca lanza)."
tags: [eda, missing, missingness, ranking, bar, barh, matplotlib, figure, visualization, datascience, impure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [matplotlib]
example: |
from datascience.missingness_rank_bar_figure import missingness_rank_bar_figure
names = ["edad", "ingresos", "ciudad", "email"]
pcts = [12.5, 40.0, 3.2, 0.0]
fig = missingness_rank_bar_figure(names, pcts, title="% de valores faltantes por columna")
tested: true
tests:
- "test_returns_figure_with_axes"
- "test_sorted_descending_largest_on_top"
- "test_empty_lists_do_not_raise_and_returns_figure"
- "test_xlim_is_zero_to_hundred"
- "test_length_mismatch_and_non_numeric_are_handled"
test_file_path: "python/functions/datascience/missingness_rank_bar_figure_test.py"
file_path: "python/functions/datascience/missingness_rank_bar_figure.py"
params:
- name: names
desc: "Lista de nombres de columna. Puede venir vacía (devuelve figura \"sin datos faltantes\"). Los items se convierten a str y se truncan a ~22 chars con elipsis para las etiquetas del eje Y; los originales no se mutan."
- name: pcts
desc: "Lista paralela a names con el % de nulos en [0,100]. Valores None, NaN o no numéricos se coercen a 0.0 y los negativos se recortan a 0. Si len(names) != len(pcts) se recorta al menor de ambos para no romper."
- name: title
desc: "Título de la figura. Se trunca a ~60 chars con elipsis si es muy largo. Default \"% de valores faltantes por columna\"."
output: "Un matplotlib.figure.Figure (figsize 6.4 x alto adaptativo según nº de barras, dpi 150) con un Axes de barras horizontales (ax.barh) ordenadas por % descendente, la mayor arriba. Eje X fijado a [0,100] con label \"% faltante\", etiquetas del eje Y truncadas a ~22 chars, y cada barra anotada con su NN.N% al final. Si names o pcts vienen vacíos devuelve una Figure con texto centrado \"sin datos faltantes\"; cualquier error inesperado se captura y devuelve una Figure con el mensaje de error (nunca lanza). El caller rasteriza/cierra la figura; la función no la muestra ni la guarda."
---
## Ejemplo
```python
from datascience.missingness_rank_bar_figure import missingness_rank_bar_figure
# % de nulos por columna (p. ej. (df.isnull().mean() * 100).
names = ["edad", "ingresos", "ciudad", "email"]
pcts = [12.5, 40.0, 3.2, 0.0]
fig = missingness_rank_bar_figure(
names,
pcts,
title="% de valores faltantes por columna",
)
# ingresos (40.0%) queda arriba; email (0.0%) abajo.
# El renderer del informe lo rasteriza; aquí solo persistimos para inspección.
fig.savefig("/tmp/missingness_rank.png")
```
## Cuando usarla
Úsala al abrir el capítulo de datos faltantes de un informe EDA para responder
"¿qué columnas están más incompletas?" de un vistazo. Pásale los nombres de
columna y el % de nulos de cada una (`(df.isnull().mean() * 100).round(1)`); la
función se encarga de ordenar de mayor a menor y poner la peor arriba. Es la
pareja "magnitud" del heatmap de co-ocurrencia: las barras dicen *cuánto* falta
en cada columna, el heatmap dice *si esas ausencias están relacionadas* entre
columnas.
## Gotchas
- **Impura por matplotlib.** Toca la maquinaria de render. Usa el backend `Agg`
y la API orientada a objetos `Figure`/`add_subplot` — NUNCA `pyplot.*` aquí,
para no tocar el estado global ni filtrar figuras entre llamadas. `pyplot` NO
es thread-safe; esta función evita ese riesgo construyendo el `Figure`
directamente, así que es segura de llamar en bucle desde el renderer.
- **El caller cierra la figura.** Devuelve el `Figure` pero no lo muestra ni lo
guarda. Quien la consume debe rasterizarla y luego liberarla
(`matplotlib.pyplot.close(fig)`) para no acumular memoria en lotes grandes.
- **Espera porcentajes 0-100, no fracciones 0-1.** El eje X está fijado a
`[0, 100]`. Si pasas fracciones (`0.4` en vez de `40.0`) las barras saldrán
pegadas al origen. Multiplica por 100 antes de llamar.
- **Alto adaptativo.** La altura de la figura crece con el número de barras
(hasta un tope) para que reports con muchas columnas sigan legibles; aun así,
conviene filtrar a las columnas con algún nulo antes de llamar para no listar
decenas de barras a 0%.
- **Defensiva, nunca lanza.** Listas vacías, longitudes desiguales, valores
`None`/`NaN`/no numéricos o cualquier error inesperado se manejan sin propagar:
en el peor caso devuelve una `Figure` con "sin datos faltantes" o con el texto
del error. No envuelvas la llamada en try/except por miedo a un raise — no lo
hay.