763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
115 lines
3.8 KiB
Python
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))
|