763e06c127
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.4 KiB
Python
75 lines
2.4 KiB
Python
"""Cramer's V: asociacion simetrica entre dos columnas categoricas pareadas.
|
|
|
|
Funcion pura del grupo eda. Mide la fuerza de asociacion entre dos variables
|
|
categoricas (0 = independientes, 1 = asociacion perfecta) usando la estadistica
|
|
chi-cuadrado de la tabla de contingencia, con la correccion de sesgo de
|
|
Bergsma-Wicher para tablas pequenas.
|
|
"""
|
|
|
|
from collections import Counter
|
|
|
|
|
|
def cramers_v(a: list, b: list) -> float:
|
|
"""Calcula Cramer's V (con correccion de sesgo) entre dos categoricas.
|
|
|
|
Empareja `a` y `b` posicion a posicion, descarta los pares donde cualquiera
|
|
de los dos sea None, construye la tabla de contingencia y devuelve la V de
|
|
Cramer corregida (Bergsma-Wicher), clampada a [0, 1].
|
|
|
|
Args:
|
|
a: lista de valores categoricos (hashables; None se descarta).
|
|
b: lista de valores categoricos pareada con `a` (mismo criterio).
|
|
|
|
Returns:
|
|
float en [0, 1]: 0.0 si hay menos de 2 pares validos o menos de 2
|
|
categorias distintas en alguna de las dos variables; en otro caso la V
|
|
de Cramer corregida. Nunca devuelve None ni lanza excepcion.
|
|
"""
|
|
# Empareja y descarta pares con None en cualquiera de los dos lados.
|
|
pairs = [
|
|
(x, y)
|
|
for x, y in zip(a, b)
|
|
if x is not None and y is not None
|
|
]
|
|
n = len(pairs)
|
|
if n < 2:
|
|
return 0.0
|
|
|
|
rows = sorted({x for x, _ in pairs}, key=repr)
|
|
cols = sorted({y for _, y in pairs}, key=repr)
|
|
r = len(rows)
|
|
k = len(cols)
|
|
if r < 2 or k < 2:
|
|
return 0.0
|
|
|
|
row_idx = {v: i for i, v in enumerate(rows)}
|
|
col_idx = {v: j for j, v in enumerate(cols)}
|
|
|
|
cell = Counter((row_idx[x], col_idx[y]) for x, y in pairs)
|
|
row_tot = [0.0] * r
|
|
col_tot = [0.0] * k
|
|
for (i, j), c in cell.items():
|
|
row_tot[i] += c
|
|
col_tot[j] += c
|
|
|
|
# chi2 = sum((obs - exp)^2 / exp) sobre toda la tabla.
|
|
chi2 = 0.0
|
|
for i in range(r):
|
|
for j in range(k):
|
|
obs = cell.get((i, j), 0)
|
|
exp = row_tot[i] * col_tot[j] / n
|
|
if exp > 0.0:
|
|
diff = obs - exp
|
|
chi2 += diff * diff / exp
|
|
|
|
phi2 = chi2 / n
|
|
# Correccion de sesgo Bergsma-Wicher.
|
|
phi2corr = max(0.0, phi2 - (r - 1) * (k - 1) / (n - 1))
|
|
rcorr = r - (r - 1) ** 2 / (n - 1)
|
|
kcorr = k - (k - 1) ** 2 / (n - 1)
|
|
|
|
denom = max(1e-12, min(kcorr - 1.0, rcorr - 1.0))
|
|
v = (phi2corr / denom) ** 0.5
|
|
# Clampa a [0, 1] por seguridad numerica.
|
|
return max(0.0, min(1.0, v))
|