Files
fn_registry/python/functions/datascience/cramers_v.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

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))