763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
"""Theil's U (uncertainty coefficient) direccional entre dos columnas categoricas.
|
|
|
|
U(a|b) mide cuanta incertidumbre de `a` se elimina conociendo `b`, normalizado a
|
|
[0, 1]. Es ASIMETRICO: theils_u(a, b) != theils_u(b, a) en general, lo que lo
|
|
distingue de medidas simetricas como Cramer's V y permite detectar dependencias
|
|
direccionales (p.ej. ciudad -> pais).
|
|
"""
|
|
|
|
import math
|
|
from collections import Counter
|
|
|
|
|
|
def _entropy(counts: list) -> float:
|
|
"""Entropia de Shannon (base natural) de una lista de conteos.
|
|
|
|
Args:
|
|
counts: conteos por categoria (enteros >= 0).
|
|
|
|
Returns:
|
|
entropia en nats; 0.0 si no hay observaciones.
|
|
"""
|
|
total = sum(counts)
|
|
if total == 0:
|
|
return 0.0
|
|
h = 0.0
|
|
for c in counts:
|
|
if c > 0:
|
|
p = c / total
|
|
h -= p * math.log(p)
|
|
return h
|
|
|
|
|
|
def theils_u(a: list, b: list) -> float:
|
|
"""Theil's U direccional U(a|b): incertidumbre de `a` explicada por `b`.
|
|
|
|
Calcula la fraccion de la entropia de la distribucion marginal de `a` que se
|
|
elimina al condicionar sobre los valores de `b`. Es una medida de asociacion
|
|
ASIMETRICA en [0, 1]:
|
|
|
|
- U(a|b) = 1.0 -> conocer `b` determina por completo `a`.
|
|
- U(a|b) = 0.0 -> `b` no aporta nada sobre `a` (independencia).
|
|
|
|
Las entropias usan la misma base (logaritmo natural), por lo que la base se
|
|
cancela en el cociente y el resultado es independiente de ella.
|
|
|
|
Args:
|
|
a: columna categorica objetivo (cuya incertidumbre se mide).
|
|
b: columna categorica condicionante (el conocimiento que se aporta).
|
|
Ambas listas se emparejan por indice; los pares con algun None se
|
|
descartan antes de calcular.
|
|
|
|
Returns:
|
|
Theil's U(a|b) como float en [0.0, 1.0]. Devuelve 0.0 (nunca None ni
|
|
excepcion) si hay menos de 2 pares validos o si H(a) == 0 (es decir, `a`
|
|
ya es constante y no hay incertidumbre que eliminar).
|
|
"""
|
|
# Empareja por indice y descarta pares con algun None.
|
|
pairs = [
|
|
(av, bv)
|
|
for av, bv in zip(a, b)
|
|
if av is not None and bv is not None
|
|
]
|
|
if len(pairs) < 2:
|
|
return 0.0
|
|
|
|
# H(a): entropia de la distribucion marginal de a.
|
|
a_counts = Counter(av for av, _ in pairs)
|
|
h_a = _entropy(list(a_counts.values()))
|
|
if h_a == 0.0:
|
|
return 0.0
|
|
|
|
# H(a|b) = suma_b p(b) * H(a | b=valor).
|
|
by_b: dict = {}
|
|
for av, bv in pairs:
|
|
by_b.setdefault(bv, Counter())[av] += 1
|
|
total = len(pairs)
|
|
h_a_given_b = 0.0
|
|
for bv, a_sub in by_b.items():
|
|
p_b = sum(a_sub.values()) / total
|
|
h_a_given_b += p_b * _entropy(list(a_sub.values()))
|
|
|
|
u = (h_a - h_a_given_b) / h_a
|
|
# Clampa a [0, 1] para absorber errores de redondeo en coma flotante.
|
|
if u < 0.0:
|
|
return 0.0
|
|
if u > 1.0:
|
|
return 1.0
|
|
return u
|