Files
fn_registry/python/functions/datascience/extract_null_mask.md
T
egutierrez 7fa19d65db 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>
2026-06-30 20:38:39 +02:00

5.8 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
extract_null_mask function py datascience 1.0.0 impure def extract_null_mask(query_fn, table: str, columns: list, max_rows: int = 5000) -> dict 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).
eda
nulls
missing
datascience
automatic-eda
extraction
read-only
duckdb
postgres
python
false error_go_core
name desc
query_fn 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 desc
table 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 desc
columns 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 desc
max_rows limite de filas a muestrear (clausula LIMIT). Default 5000. Protege frente a tablas enormes; con LIMIT obtienes el primer tramo, no un muestreo uniforme.
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). true
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
python/functions/datascience/extract_null_mask_test.py python/functions/datascience/extract_null_mask.py

Ejemplo

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.