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:
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: extract_null_mask
|
||||
kind: function
|
||||
lang: py
|
||||
domain: datascience
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def extract_null_mask(query_fn, table: str, columns: list, max_rows: int = 5000) -> dict"
|
||||
description: "Extrae la mascara de nulos (1=falta / 0=presente) de una muestra de filas de una tabla, una lista 0/1 por columna alineada por fila, para alimentar el capitulo de calidad / patron de nulos de AutomaticEDA sin que el capitulo toque la base de datos. Recibe un lector read-only inyectado `query_fn(sql) -> dict` (mismo contrato que duckdb_query_readonly / pg_query / el `_q` de profile_table) y NO abre ninguna conexion por su cuenta. Construye UNA sola query que proyecta por cada columna `CASE WHEN \"col\" IS NULL THEN 1 ELSE 0 END` con identificadores escapados y LIMIT. Devuelve dict dict-no-throw: columns (efectivamente leidas, en orden), mask (lista int 0/1 por columna, misma longitud todas) y n. Una celda None se cuenta defensivamente como 1 (falta)."
|
||||
tags: [eda, nulls, missing, datascience, automatic-eda, extraction, read-only, duckdb, postgres, python]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: query_fn
|
||||
desc: "callable lector read-only del backend activo. Recibe un string SQL y devuelve un dict {'status':'ok','rows':[{col:val,...},...]} (mismo contrato que duckdb_query_readonly o el `_q` de profile_table). NO se abre ninguna conexion dentro de la funcion: toda la lectura pasa por query_fn. Si es None -> error."
|
||||
- name: table
|
||||
desc: "nombre de la tabla de la que muestrear la mascara de nulos. Se escapa con comillas dobles en la query. Vacio o None -> status error."
|
||||
- name: columns
|
||||
desc: "lista de nombres de columna a evaluar. Cada una produce una entrada en `mask` con una lista 0/1 paralela por fila (1=IS NULL, 0=presente). Cada nombre se escapa con comillas dobles. Vacia o None -> status error."
|
||||
- name: max_rows
|
||||
desc: "limite de filas a muestrear (clausula LIMIT). Default 5000. Protege frente a tablas enormes; con LIMIT obtienes el primer tramo, no un muestreo uniforme."
|
||||
output: "dict (nunca lanza). En exito: {'status':'ok','table':str,'columns':[str,...] (en orden),'mask':{col:[int 0/1,...],...} (1=falta/IS NULL, 0=presente; todas las listas con misma longitud = n),'n':int}. En error (sin lanzar): {'status':'error','error':str,'table':str,'columns':[],'mask':{},'n':0}. Errores: query_fn None, table vacia, columns vacia, o query_fn devuelve status!='ok' (se propaga su error)."
|
||||
tested: true
|
||||
tests: ["test_golden_mask_alineada", "test_celda_none_cuenta_como_falta", "test_columns_vacia_status_error", "test_query_fn_status_error_propaga", "test_query_fn_none_da_error_sin_reventar", "test_sql_contiene_case_y_limit"]
|
||||
test_file_path: "python/functions/datascience/extract_null_mask_test.py"
|
||||
file_path: "python/functions/datascience/extract_null_mask.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from datascience.extract_null_mask import extract_null_mask
|
||||
from infra import duckdb_query_readonly
|
||||
|
||||
# El lector read-only se inyecta como closure (igual que el `_q` de profile_table).
|
||||
db = "data/clientes.duckdb"
|
||||
def _q(sql):
|
||||
return duckdb_query_readonly(db, sql)
|
||||
|
||||
res = extract_null_mask(_q, "clientes", ["email", "telefono", "edad"])
|
||||
# res == {
|
||||
# "status": "ok",
|
||||
# "table": "clientes",
|
||||
# "columns": ["email", "telefono", "edad"],
|
||||
# "mask": {
|
||||
# "email": [0, 0, 1, 0, ...], # fila 2 sin email
|
||||
# "telefono": [1, 0, 1, 0, ...],
|
||||
# "edad": [0, 0, 0, 1, ...],
|
||||
# },
|
||||
# "n": 5000,
|
||||
# }
|
||||
|
||||
# % de nulos por columna a partir de la muestra:
|
||||
pct = {c: 100 * sum(bits) / max(res["n"], 1) for c, bits in res["mask"].items()}
|
||||
|
||||
# Se entrega al capitulo de calidad sin que este toque la BD:
|
||||
ctx = {"null_mask": res}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando el capitulo de calidad / patron de nulos de AutomaticEDA necesita saber
|
||||
DONDE faltan los valores (no solo cuantos) y NO debe abrir la base de datos por
|
||||
su cuenta: extraes aqui la mascara 0/1 por columna alineada por fila y se la pasas
|
||||
en `ctx['null_mask']`. Usala siempre que quieras detectar co-ocurrencia de nulos
|
||||
(filas que fallan en varias columnas a la vez), calcular el % de nulos sobre una
|
||||
muestra, o pintar un heatmap de missingness reutilizando un unico lector read-only
|
||||
inyectado, en vez de hacer N `COUNT(*) WHERE col IS NULL` por separado.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Impura**: lee de la base de datos a traves de `query_fn`. No abre conexiones
|
||||
por su cuenta — depende por completo del lector inyectado. Sigue el estilo
|
||||
dict-no-throw del grupo `eda`: nunca lanza; ante cualquier fallo devuelve
|
||||
`{"status":"error","error":...}` con `columns=[]`, `mask={}`, `n=0`.
|
||||
- **`error_type` en el frontmatter es `error_go_core` por convencion del registry**
|
||||
(toda funcion impura debe declararlo y el indexer lo exige), pero el codigo
|
||||
NO lanza esa excepcion: degrada al dict de error. Es metadata, no comportamiento.
|
||||
- **Muestra, no censo**: con `LIMIT max_rows` obtienes el primer tramo de filas que
|
||||
devuelva el backend, no un muestreo uniforme ni la tabla entera. El % de nulos
|
||||
derivado es una estimacion sobre esa muestra; para el conteo exacto usa un
|
||||
agregado `COUNT(*)`/`COUNT(col)` aparte.
|
||||
- **Alineacion por fila**: `mask[col][i]` corresponde a la misma fila `i` que
|
||||
`mask[otra_col][i]`. Todas las listas tienen longitud `n`, asi que puedes cruzar
|
||||
columnas por indice (co-ocurrencia de nulos) sin re-alinear.
|
||||
- **Defensa None -> 1**: el SQL ya devuelve 0/1, pero si una celda llega como `None`
|
||||
(CASE no aplicado, columna ausente en la fila, backend que nulifica) se cuenta
|
||||
como 1 (falta). Un valor inesperado no convertible a int se trata como presente (0).
|
||||
- **No loguear los datos crudos**: aunque `mask` es solo 0/1, los nombres de columna
|
||||
pueden revelar el esquema. En trazas usa `n` y el numero de columnas, no el dict
|
||||
completo.
|
||||
Reference in New Issue
Block a user