Files
fn_registry/python/functions/datascience/mutual_info_columns.py
T
egutierrez 763e06c127 feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 18:22:23 +02:00

115 lines
3.8 KiB
Python

"""Informacion mutua entre dos columnas pareadas (relaciones lineales y no lineales).
Funcion pura del grupo eda. Mide la dependencia estadistica general entre dos
columnas (numericas, categoricas o mezcla), capturando relaciones de cualquier
forma -- no solo lineales como Pearson. Es la metrica "general" de la matriz de
asociacion: complementa a `pearson` (solo lineal num-num) y `cramers_v` (solo
cat-cat).
"""
import math
from collections import Counter
import numpy as np
from sklearn.metrics import mutual_info_score
def _discretize(values: list, numeric: bool, bins: int) -> list:
"""Discretiza una columna a etiquetas enteras.
Columnas numericas -> `bins` cubos por cuantiles (np.digitize sobre los
bordes de cuantil). Columnas categoricas -> factorizacion valor->id.
"""
if numeric:
arr = np.asarray(values, dtype=float)
# Bordes interiores por cuantiles (excluye 0 y 1 para usar digitize).
qs = np.linspace(0.0, 1.0, bins + 1)[1:-1]
if qs.size == 0:
# bins <= 1 -> todo cae en un unico cubo.
return [0] * len(arr)
edges = np.quantile(arr, qs)
# Bordes unicos: cuantiles repetidos (columnas con poca variacion)
# colapsan en menos cubos, lo cual es correcto (menos entropia).
edges = np.unique(edges)
return list(np.digitize(arr, edges))
# Categorica: mapa valor -> id entero, en orden de aparicion.
ids: dict = {}
out = []
for v in values:
if v not in ids:
ids[v] = len(ids)
out.append(ids[v])
return out
def _entropy(labels: list) -> float:
"""Entropia de Shannon (nats) de una secuencia de etiquetas."""
n = len(labels)
if n == 0:
return 0.0
h = 0.0
for c in Counter(labels).values():
p = c / n
h -= p * math.log(p)
return h
def mutual_info_columns(
a: list,
b: list,
a_numeric: bool = False,
b_numeric: bool = False,
bins: int = 10,
normalized: bool = True,
) -> float:
"""Informacion mutua entre dos columnas pareadas posicion a posicion.
Empareja `a` y `b`, descarta los pares donde cualquiera de los dos sea None,
discretiza cada columna (numericas por cuantiles, categoricas por
factorizacion) y calcula la informacion mutua. Captura relaciones de
cualquier forma (lineal o no, num-num, cat-cat, num-cat).
Args:
a: lista de valores de la primera columna (None se descarta).
b: lista de valores pareada con `a` (mismo criterio).
a_numeric: si True, `a` se discretiza en `bins` cuantiles; si False se
factoriza como categorica.
b_numeric: idem para `b`.
bins: numero de cubos por cuantiles para columnas numericas.
normalized: si True devuelve la NMI = MI / sqrt(H(a)*H(b)) en [0, 1]
(1 = dependencia total). Si False devuelve la MI cruda en nats.
Returns:
float. NMI en [0, 1] si normalized; MI en nats (>= 0) si no. Devuelve
0.0 si hay menos de 2 pares validos o si alguna columna discretizada
tiene entropia 0 (constante) bajo normalized. Nunca None ni excepcion.
"""
pairs = [
(x, y)
for x, y in zip(a, b)
if x is not None and y is not None
]
if len(pairs) < 2:
return 0.0
a_vals = [x for x, _ in pairs]
b_vals = [y for _, y in pairs]
a_disc = _discretize(a_vals, a_numeric, bins)
b_disc = _discretize(b_vals, b_numeric, bins)
mi = float(mutual_info_score(a_disc, b_disc))
if not normalized:
return max(0.0, mi)
ha = _entropy(a_disc)
hb = _entropy(b_disc)
if ha <= 0.0 or hb <= 0.0:
# Alguna columna es constante -> no hay informacion compartida medible.
return 0.0
nmi = mi / math.sqrt(ha * hb)
# Clampa a [0, 1] por seguridad numerica.
return max(0.0, min(1.0, nmi))