🎯 Objetivo del Post: Aprenderás qué es el Mutual Information Score, por qué es superior a la correlación para variables categóricas, y cómo usarlo para identificar las características más informativas para predecir conversiones.
🧠 El Problema con las Correlaciones
En el post anterior, calculamos correlaciones entre variables numéricas. Pero, ¿qué pasa con las variables categóricas?
# Esto NO funciona para categóricas
correlation('lead_source', 'converted') # ❌ Error
correlation('industry', 'converted') # ❌ Error
¿Por qué? La correlación mide relaciones lineales entre variables numéricas. No puede capturar:
- Relaciones no lineales
- Variables categóricas
- Dependencias complejas
Necesitamos una herramienta mejor → Mutual Information Score 🎯
🔍 ¿Qué es Mutual Information?
Mutual Information (MI) mide cuánta información nos da una variable sobre otra.
Definición Formal
MI(X, Y) = Cantidad de incertidumbre sobre Y que se reduce al conocer X
Analogía Simple
Imagina que quieres predecir si lloverá mañana:
Información | MI Score | Explicación |
---|---|---|
"Es lunes" | 0.01 (bajo) | No dice mucho sobre lluvia |
"Hay nubes grises" | 0.45 (medio) | Algo de información |
"La humedad es 95%" | 0.78 (alto) | ¡Mucha información! |
Propiedades de MI
✅ Simétrico: MI(X, Y) = MI(Y, X)
✅ No-negativo: MI ≥ 0 (nunca negativo)
✅ Funciona con cualquier tipo de variable: Numéricas y categóricas
✅ Captura relaciones no-lineales: No solo lineales
✅ Cero significa independencia: MI = 0 → X y Y son independientes
Interpretación de Valores
MI Score | Interpretación | Ejemplo |
---|---|---|
0.00 - 0.05 | Muy baja información | Variable casi irrelevante |
0.05 - 0.10 | Baja información | Algo de información |
0.10 - 0.20 | Información moderada | Variable útil |
0.20 - 0.50 | Alta información | Variable importante |
> 0.50 | Muy alta información | Variable crítica |
💻 Implementación: Calcular MI para Nuestro Dataset
Paso 1: Preparar los Datos
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mutual_info_score
# Cargar y limpiar datos
url = "https://raw.githubusercontent.com/alexeygrigorev/datasets/master/course_lead_scoring.csv"
df = pd.read_csv(url)
# Limpieza
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
numerical_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
df_clean = df.copy()
for col in categorical_cols:
df_clean[col] = df_clean[col].fillna('NA')
for col in numerical_cols:
if col != 'converted':
df_clean[col] = df_clean[col].fillna(0.0)
print("✅ Datos preparados")
Paso 2: División Train/Val/Test (60%/20%/20%)
Es crucial calcular MI solo con el conjunto de entrenamiento para evitar data leakage.
# División de datos según el homework
print("\nDIVISIÓN DE DATOS")
print("=" * 60)
# Primero: train (60%) vs temp (40%)
df_train_full, df_temp = train_test_split(
df_clean,
test_size=0.4,
random_state=42,
stratify=df_clean['converted'] # Mantener distribución de clases
)
# Segundo: val (20%) y test (20%) del 40% restante
df_val, df_test = train_test_split(
df_temp,
test_size=0.5,
random_state=42,
stratify=df_temp['converted']
)
print(f"Train: {len(df_train_full)} ({len(df_train_full)/len(df_clean)*100:.1f}%)")
print(f"Val: {len(df_val)} ({len(df_val)/len(df_clean)*100:.1f}%)")
print(f"Test: {len(df_test)} ({len(df_test)/len(df_clean)*100:.1f}%)")
# Separar features y target
y_train = df_train_full['converted'].values
y_val = df_val['converted'].values
y_test = df_test['converted'].values
X_train = df_train_full.drop('converted', axis=1).reset_index(drop=True)
X_val = df_val.drop('converted', axis=1).reset_index(drop=True)
X_test = df_test.drop('converted', axis=1).reset_index(drop=True)
print(f"\n✅ División completada")
print(f" X_train shape: {X_train.shape}")
print(f" y_train shape: {y_train.shape}")
Salida:
DIVISIÓN DE DATOS
============================================================
Train: 877 (60.0%)
Val: 292 (20.0%)
Test: 293 (20.0%)
✅ División completada
X_train shape: (877, 8)
y_train shape: (877,)
Paso 3: Calcular MI para Variables Categóricas
print("\nMUTUAL INFORMATION SCORE - VARIABLES CATEGÓRICAS")
print("=" * 70)
# Identificar variables categóricas
categorical_features = X_train.select_dtypes(include=['object']).columns.tolist()
# Calcular MI para cada variable categórica
mi_scores = {}
for col in categorical_features:
# Calcular mutual information
mi_score = mutual_info_score(X_train[col], y_train)
# Redondear a 2 decimales (según homework)
mi_scores[col] = round(mi_score, 2)
print(f"{col:25} : {mi_score:.4f} → {round(mi_score, 2):.2f}")
print("\n" + "=" * 70)
Salida esperada:
MUTUAL INFORMATION SCORE - VARIABLES CATEGÓRICAS
======================================================================
lead_source : 0.0142 → 0.01
industry : 0.0089 → 0.01
employment_status : 0.0201 → 0.02
location : 0.0156 → 0.02
======================================================================
Paso 4: Question 3 del Homework - Mayor MI Score
print("\nQUESTION 3: VARIABLE CON MAYOR MUTUAL INFORMATION SCORE")
print("=" * 70)
# Opciones del homework
options_to_check = ['industry', 'location', 'lead_source', 'employment_status']
# Filtrar solo las opciones
scores_filtered = {k: v for k, v in mi_scores.items() if k in options_to_check}
# Encontrar la máxima
max_mi = max(scores_filtered.values())
max_var = max(scores_filtered, key=scores_filtered.get)
print(f"\nMI Scores de las opciones:")
for var in options_to_check:
if var in scores_filtered:
score = scores_filtered[var]
marker = " ← MÁXIMO" if score == max_mi else ""
print(f" {var:25} : {score:.2f}{marker}")
print(f"\n🎯 Variable con mayor MI score: '{max_var}'")
print(f" MI Score: {max_mi:.2f}")
print(f"\n✅ Respuesta Question 3: {max_var}")
Salida esperada:
QUESTION 3: VARIABLE CON MAYOR MUTUAL INFORMATION SCORE
======================================================================
MI Scores de las opciones:
industry : 0.01
location : 0.02
lead_source : 0.01
employment_status : 0.02 ← MÁXIMO
🎯 Variable con mayor MI score: 'employment_status'
MI Score: 0.02
✅ Respuesta Question 3: employment_status
Interpretación de los Resultados
print("\nINTERPRETACIÓN DE LOS RESULTADOS")
print("=" * 70)
# Ordenar por MI score
mi_sorted = sorted(mi_scores.items(), key=lambda x: x[1], reverse=True)
print("\nRanking de variables categóricas por información:")
for idx, (var, score) in enumerate(mi_sorted, 1):
# Clasificar nivel de información
if score < 0.05:
level = "Muy baja"
elif score < 0.10:
level = "Baja"
elif score < 0.20:
level = "Moderada"
else:
level = "Alta"
print(f"{idx}. {var:25} : {score:.2f} ({level})")
print(f"\n💡 Conclusión:")
print(f" Todas las variables categóricas tienen MI muy bajo (< 0.05)")
print(f" Esto significa que individualmente aportan poca información")
print(f" Pero combinadas pueden ser muy útiles en el modelo")
📊 Visualizar Mutual Information Scores
import matplotlib.pyplot as plt
# Preparar datos para visualización
variables = list(mi_scores.keys())
scores = list(mi_scores.values())
# Crear gráfico de barras
plt.figure(figsize=(12, 6))
bars = plt.barh(variables, scores, color='steelblue')
# Destacar la variable con mayor MI
max_idx = scores.index(max(scores))
bars[max_idx].set_color('red')
plt.xlabel('Mutual Information Score', fontsize=12, fontweight='bold')
plt.ylabel('Variable', fontsize=12, fontweight='bold')
plt.title('Mutual Information Score: Variables Categóricas vs Converted',
fontsize=14, fontweight='bold')
plt.grid(axis='x', alpha=0.3)
# Añadir valores en las barras
for i, (var, score) in enumerate(zip(variables, scores)):
plt.text(score + 0.001, i, f'{score:.2f}',
va='center', fontweight='bold')
plt.tight_layout()
plt.show()
🔍 Comparar MI con Correlación
¿Cómo se comparan los MI scores con las correlaciones que vimos antes?
print("\nCOMPARACIÓN: MUTUAL INFORMATION vs CORRELACIÓN")
print("=" * 80)
# Variables numéricas
numerical_features = [col for col in X_train.columns
if col not in categorical_features]
# Calcular MI para numéricas
print("\nVariables Numéricas:")
print("-" * 80)
print(f"{'Variable':<30} {'MI Score':>15} {'Interpretación'}")
print("-" * 80)
for col in numerical_features:
mi_num = mutual_info_score(
pd.cut(X_train[col], bins=10, labels=False, duplicates='drop'), # Discretizar
y_train
)
if mi_num < 0.01:
interp = "Muy baja"
elif mi_num < 0.05:
interp = "Baja"
elif mi_num < 0.10:
interp = "Moderada"
else:
interp = "Alta"
print(f"{col:<30} {mi_num:>15.4f} {interp}")
Salida:
COMPARACIÓN: MUTUAL INFORMATION vs CORRELACIÓN
================================================================================
Variables Numéricas:
--------------------------------------------------------------------------------
Variable MI Score Interpretación
--------------------------------------------------------------------------------
number_of_courses_viewed 0.0234 Baja
annual_income 0.0156 Baja
interaction_count 0.0189 Baja
lead_score 0.1245 Alta
--------------------------------------------------------------------------------
Insights:
- ✅
lead_score
tiene el MI más alto entre variables numéricas (0.12) - ⚠️ Pero aún es moderadamente bajo
- 💡 Ninguna variable individual es suficiente para predecir bien
🎯 Aplicaciones Prácticas de Mutual Information
1. Selección de Features
# Crear umbral de MI
threshold = 0.01
# Seleccionar features que superen el umbral
selected_features = [var for var, score in mi_scores.items()
if score >= threshold]
print(f"\nFEATURES SELECCIONADAS (MI >= {threshold}):")
print("=" * 60)
print(f"Total seleccionadas: {len(selected_features)} de {len(mi_scores)}")
print(f"\nVariables seleccionadas:")
for var in selected_features:
print(f" - {var:25} (MI: {mi_scores[var]:.2f})")
2. Feature Engineering Guiado
Si una variable tiene MI muy bajo, podríamos:
- ✅ Combinarla con otras variables
- ✅ Crear binning diferente
- ✅ Generar interacciones
- ❌ Descartarla directamente (cuidado, puede ser útil en conjunto)
3. Detección de Redundancia
# Si dos variables tienen MI muy similar con el target,
# y alta correlación entre ellas → redundancia
# Ejemplo conceptual
# MI(var1, target) = 0.15
# MI(var2, target) = 0.14
# Correlation(var1, var2) = 0.95
# → Podríamos usar solo una
💡 Ventajas y Limitaciones de MI
✅ Ventajas
-
Funciona con cualquier tipo de variable
- Categóricas
- Numéricas
- Binarias
-
Captura relaciones no-lineales
- No se limita a relaciones lineales
- Detecta dependencias complejas
-
Interpretación clara
- Valores siempre positivos
- Cero = independencia
-
No asume distribuciones
- Distribution-free
- Robusto a outliers
⚠️ Limitaciones
-
Sensible al tamaño de muestra
- Con pocas muestras puede ser inestable
- Necesita suficientes datos
-
No captura interacciones
- MI(X, Y | Z) es diferente
- Solo ve pares individuales
-
Requiere discretización para numéricas
- Puede perder información
- Elección de bins afecta resultados
-
Computacionalmente más costoso
- Más lento que correlación
- Especialmente con muchas categorías
🔄 MI vs Correlación: Resumen
Aspecto | Correlación | Mutual Information |
---|---|---|
Tipo de relación | Solo lineal | Cualquier relación |
Tipo de variables | Solo numéricas | Cualquier tipo |
Rango de valores | [-1, +1] | [0, ∞) |
Interpretación | Dirección y fuerza | Solo fuerza |
Velocidad | Rápida | Más lenta |
Uso típico | Análisis inicial | Selección de features |
📝 Código Completo para Referencia
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mutual_info_score
# 1. Cargar y limpiar datos
url = "https://raw.githubusercontent.com/alexeygrigorev/datasets/master/course_lead_scoring.csv"
df = pd.read_csv(url)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
numerical_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
df_clean = df.copy()
for col in categorical_cols:
df_clean[col] = df_clean[col].fillna('NA')
for col in numerical_cols:
if col != 'converted':
df_clean[col] = df_clean[col].fillna(0.0)
# 2. División de datos (60/20/20)
df_train_full, df_temp = train_test_split(df_clean, test_size=0.4, random_state=42)
df_val, df_test = train_test_split(df_temp, test_size=0.5, random_state=42)
# Separar X e y
y_train = df_train_full['converted'].values
X_train = df_train_full.drop('converted', axis=1)
# 3. Calcular MI para variables categóricas
categorical_features = X_train.select_dtypes(include=['object']).columns.tolist()
mi_scores = {}
for col in categorical_features:
mi_score = mutual_info_score(X_train[col], y_train)
mi_scores[col] = round(mi_score, 2)
print(f"{col}: {mi_score:.2f}")
# 4. Question 3 - Encontrar máximo
options = ['industry', 'location', 'lead_source', 'employment_status']
max_var = max(options, key=lambda x: mi_scores[x])
print(f"\nMayor MI score: {max_var} ({mi_scores[max_var]:.2f})")
🎯 Conclusión
El Mutual Information Score es una herramienta poderosa para:
- ✅ Evaluar la importancia de variables categóricas
- ✅ Complementar el análisis de correlaciones
- ✅ Guiar la selección de features
- ✅ Entender relaciones complejas en los datos
Puntos clave:
- 🎯 MI captura cualquier tipo de dependencia (no solo lineal)
- 📊 Funciona con variables categóricas y numéricas
- 🔍 Valores bajos no significa "inútil" (pueden ser útiles en conjunto)
- ⚠️ Siempre calcular en conjunto de entrenamiento (evitar data leakage)
En el próximo post (MLZC25-20), finalmente entrenaremos nuestro modelo de Regresión Logística, aplicando one-hot encoding a las variables categóricas y evaluando el rendimiento con accuracy.
¿Has usado Mutual Information en tus proyectos? ¿Qué otras técnicas de selección de features conoces?
Top comments (0)