DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

MLZC25-19. Mutual Information Score: Selección Inteligente de Características para Clasificación

🎯 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
Enter fullscreen mode Exit fullscreen mode

¿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 mejorMutual 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")
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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,)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

======================================================================
Enter fullscreen mode Exit fullscreen mode

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}")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

📊 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()
Enter fullscreen mode Exit fullscreen mode

🔍 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}")
Enter fullscreen mode Exit fullscreen mode

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
--------------------------------------------------------------------------------
Enter fullscreen mode Exit fullscreen mode

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})")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

💡 Ventajas y Limitaciones de MI

✅ Ventajas

  1. Funciona con cualquier tipo de variable

    • Categóricas
    • Numéricas
    • Binarias
  2. Captura relaciones no-lineales

    • No se limita a relaciones lineales
    • Detecta dependencias complejas
  3. Interpretación clara

    • Valores siempre positivos
    • Cero = independencia
  4. No asume distribuciones

    • Distribution-free
    • Robusto a outliers

⚠️ Limitaciones

  1. Sensible al tamaño de muestra

    • Con pocas muestras puede ser inestable
    • Necesita suficientes datos
  2. No captura interacciones

    • MI(X, Y | Z) es diferente
    • Solo ve pares individuales
  3. Requiere discretización para numéricas

    • Puede perder información
    • Elección de bins afecta resultados
  4. 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})")
Enter fullscreen mode Exit fullscreen mode

🎯 Conclusión

El Mutual Information Score es una herramienta poderosa para:

  1. ✅ Evaluar la importancia de variables categóricas
  2. ✅ Complementar el análisis de correlaciones
  3. ✅ Guiar la selección de features
  4. ✅ 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)