feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
+88
View File
@@ -0,0 +1,88 @@
"""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