feat(browser): auto-commit con 178 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
"""Matriz de asociacion unificada para una tabla con columnas de tipos mezclados.
|
||||
|
||||
Funcion pura del grupo eda. Para cada par de columnas elige la metrica de
|
||||
asociacion adecuada al par de tipos (Pearson/Spearman para num-num, Cramer's V
|
||||
para cat-cat, correlation ratio para num-cat) y, ademas, calcula informacion
|
||||
mutua normalizada como medida comun no-lineal para todos los pares. Devuelve la
|
||||
lista de pares evaluados, el subconjunto de pares fuertes y una leyenda de los
|
||||
metodos. Compone las funciones atomicas del registry; no reimplementa metricas.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
from datascience import (
|
||||
correlation_ratio,
|
||||
cramers_v,
|
||||
mutual_info_columns,
|
||||
pearson,
|
||||
spearman_corr,
|
||||
theils_u,
|
||||
)
|
||||
|
||||
# Tipos que, para efectos de asociacion, se tratan como categoricos.
|
||||
_CATEGORICAL_LIKE = {"categorical", "datetime", "boolean", "text"}
|
||||
|
||||
|
||||
def _is_num(v) -> bool:
|
||||
"""True si v es un numero real (int/float) que no es bool ni NaN."""
|
||||
return (
|
||||
isinstance(v, (int, float))
|
||||
and not isinstance(v, bool)
|
||||
and not (isinstance(v, float) and math.isnan(v))
|
||||
)
|
||||
|
||||
|
||||
def _is_numeric_type(t: str) -> bool:
|
||||
return t == "numeric"
|
||||
|
||||
|
||||
def _valid_count(values: list, numeric: bool) -> int:
|
||||
"""Numero de valores validos: numericos finitos si numeric, no-None si cat."""
|
||||
if numeric:
|
||||
return sum(1 for v in values if _is_num(v))
|
||||
return sum(1 for v in values if v is not None)
|
||||
|
||||
|
||||
def _cardinality(values: list) -> int:
|
||||
"""Numero de valores distintos no-None."""
|
||||
return len({v for v in values if v is not None})
|
||||
|
||||
|
||||
def _clean_numeric_pairs(xs: list, ys: list) -> tuple[list, list]:
|
||||
"""Empareja por indice y conserva solo pares con ambos lados numericos."""
|
||||
cx: list[float] = []
|
||||
cy: list[float] = []
|
||||
for x, y in zip(xs, ys):
|
||||
if _is_num(x) and _is_num(y):
|
||||
cx.append(float(x))
|
||||
cy.append(float(y))
|
||||
return cx, cy
|
||||
|
||||
|
||||
def association_matrix(
|
||||
columns: dict,
|
||||
strong_threshold: float = 0.5,
|
||||
top_n: int = 20,
|
||||
) -> dict:
|
||||
"""Construye la matriz de asociacion de una tabla con tipos mezclados.
|
||||
|
||||
Para cada par de columnas (i < j) selecciona la metrica adecuada al par de
|
||||
tipos y calcula tambien informacion mutua normalizada como medida comun:
|
||||
|
||||
- num-num: `pearson` (lineal) y `spearman_corr` (monotonica). El `value`
|
||||
principal es el de mayor valor absoluto; ambos se guardan en `extra`.
|
||||
- cat-cat: `cramers_v` (simetrica) como `value`; `theils_u` en ambas
|
||||
direcciones en `extra` (u_ab = U(a|b), u_ba = U(b|a)).
|
||||
- num-cat: `correlation_ratio(categorias, valores)` como `value`.
|
||||
- Todos los pares: `mutual_info_columns` normalizada en `extra["mi"]`.
|
||||
|
||||
Se saltan los pares donde alguna columna tenga menos de 3 valores validos o
|
||||
sea de tipo `text` con cardinalidad cercana al numero de filas (ruido sin
|
||||
asociacion util). Es una funcion pura: no falla con dict vacio o una sola
|
||||
columna (devuelve `pairs=[]`, `strong=[]`).
|
||||
|
||||
Args:
|
||||
columns: dict {nombre_columna: {"values": list, "type": str}} donde type
|
||||
es uno de "numeric", "categorical", "datetime", "boolean", "text".
|
||||
Los tipos datetime/boolean/text se tratan como categoricos.
|
||||
strong_threshold: umbral en [0, 1]. Un par es "fuerte" si
|
||||
abs(value) >= umbral o extra["mi"] >= umbral.
|
||||
top_n: numero maximo de pares fuertes a devolver, ordenados por
|
||||
relevancia (max(abs(value), mi)) descendente.
|
||||
|
||||
Returns:
|
||||
dict con claves:
|
||||
pairs: lista de todos los pares evaluados, cada uno
|
||||
{a, b, a_type, b_type, method, value, extra}.
|
||||
strong: subconjunto de pairs por encima del umbral, ordenado por
|
||||
relevancia descendente y truncado a top_n.
|
||||
methods_legend: dict {metodo: descripcion}.
|
||||
"""
|
||||
legend = {
|
||||
"pearson": "num-num lineal (Pearson r), signo indica direccion, [-1, 1]",
|
||||
"spearman": "num-num monotonica (Spearman rho), robusta a outliers, [-1, 1]",
|
||||
"cramers_v": "cat-cat simetrica (Cramer's V, sesgo-corregido), [0, 1]",
|
||||
"theils_u": "cat-cat direccional (Theil's U), incertidumbre explicada, [0, 1]",
|
||||
"correlation_ratio": "num-cat (eta), varianza numerica explicada por la categoria, [0, 1]",
|
||||
"mutual_info": "general no-lineal (NMI normalizada) para cualquier par de tipos, [0, 1]",
|
||||
}
|
||||
|
||||
names = list(columns.keys())
|
||||
if len(names) < 2:
|
||||
return {"pairs": [], "strong": [], "methods_legend": legend}
|
||||
|
||||
n_rows = max(
|
||||
(len(columns[name].get("values", [])) for name in names),
|
||||
default=0,
|
||||
)
|
||||
|
||||
def _skip(name: str) -> bool:
|
||||
"""True si la columna no aporta asociacion util (pocos validos o text ruidoso)."""
|
||||
col = columns[name]
|
||||
vals = col.get("values", [])
|
||||
ctype = col.get("type", "categorical")
|
||||
numeric = _is_numeric_type(ctype)
|
||||
if _valid_count(vals, numeric) < 3:
|
||||
return True
|
||||
# Texto de cardinalidad ~ n: identificadores/free-text, sin asociacion util.
|
||||
if ctype == "text" and n_rows > 0 and _cardinality(vals) >= 0.9 * n_rows:
|
||||
return True
|
||||
return False
|
||||
|
||||
pairs: list[dict] = []
|
||||
|
||||
for i in range(len(names)):
|
||||
a_name = names[i]
|
||||
if _skip(a_name):
|
||||
continue
|
||||
a_col = columns[a_name]
|
||||
a_vals = a_col.get("values", [])
|
||||
a_type = a_col.get("type", "categorical")
|
||||
a_numeric = _is_numeric_type(a_type)
|
||||
|
||||
for j in range(i + 1, len(names)):
|
||||
b_name = names[j]
|
||||
if _skip(b_name):
|
||||
continue
|
||||
b_col = columns[b_name]
|
||||
b_vals = b_col.get("values", [])
|
||||
b_type = b_col.get("type", "categorical")
|
||||
b_numeric = _is_numeric_type(b_type)
|
||||
|
||||
extra: dict = {}
|
||||
|
||||
# Medida comun no-lineal para todos los pares.
|
||||
mi = mutual_info_columns(
|
||||
a_vals,
|
||||
b_vals,
|
||||
a_numeric=a_numeric,
|
||||
b_numeric=b_numeric,
|
||||
normalized=True,
|
||||
)
|
||||
extra["mi"] = mi
|
||||
|
||||
if a_numeric and b_numeric:
|
||||
method = "pearson/spearman"
|
||||
cx, cy = _clean_numeric_pairs(a_vals, b_vals)
|
||||
p = pearson(cx, cy)
|
||||
s = spearman_corr(a_vals, b_vals)
|
||||
extra["pearson"] = p
|
||||
extra["spearman"] = s
|
||||
value = p if abs(p) >= abs(s) else s
|
||||
elif (not a_numeric) and (not b_numeric):
|
||||
method = "cramers_v"
|
||||
value = cramers_v(a_vals, b_vals)
|
||||
extra["u_ab"] = theils_u(a_vals, b_vals)
|
||||
extra["u_ba"] = theils_u(b_vals, a_vals)
|
||||
else:
|
||||
method = "correlation_ratio"
|
||||
if a_numeric:
|
||||
# a numerica, b categorica.
|
||||
value = correlation_ratio(b_vals, a_vals)
|
||||
else:
|
||||
# a categorica, b numerica.
|
||||
value = correlation_ratio(a_vals, b_vals)
|
||||
|
||||
pairs.append(
|
||||
{
|
||||
"a": a_name,
|
||||
"b": b_name,
|
||||
"a_type": a_type,
|
||||
"b_type": b_type,
|
||||
"method": method,
|
||||
"value": value,
|
||||
"extra": extra,
|
||||
}
|
||||
)
|
||||
|
||||
def _relevance(pair: dict) -> float:
|
||||
return max(abs(pair["value"]), pair["extra"].get("mi", 0.0))
|
||||
|
||||
strong = [
|
||||
pair
|
||||
for pair in pairs
|
||||
if abs(pair["value"]) >= strong_threshold
|
||||
or pair["extra"].get("mi", 0.0) >= strong_threshold
|
||||
]
|
||||
strong.sort(key=_relevance, reverse=True)
|
||||
strong = strong[:top_n]
|
||||
|
||||
return {"pairs": pairs, "strong": strong, "methods_legend": legend}
|
||||
Reference in New Issue
Block a user