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