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