MLZC25-22. Regularización en Regresión Logística: Ajuste Fino del Parámetro C para Mejor Generalización



This content originally appeared on DEV Community and was authored by Jesus Oviedo Riquelme

🎯 Objetivo del Post: Aprenderás qué es la regularización, por qué es crucial para evitar overfitting, cómo funciona el parámetro C en regresión logística, y cómo encontrar el valor óptimo que maximice el rendimiento en validación.

🎯 El Problema del Overfitting

Imagina que estás estudiando para un examen. Puedes:

Opción A: Memorizar todas las respuestas del libro de práctica

  • ✅ Perfecto en práctica (100%)
  • ❌ Malo en el examen real (60%)

Opción B: Entender los conceptos fundamentales

  • ✅ Bueno en práctica (85%)
  • ✅ Bueno en el examen real (82%)

Opción A = Overfitting (memorización)
Opción B = Generalización (comprensión)

Overfitting en ML

# Modelo sobreajustado
Accuracy Train: 99%   ¡Excelente!
Accuracy Val:   70%   ¡Terrible!
# → El modelo memorizó el training set pero no generalizó

# Modelo bien regularizado
Accuracy Train: 85%   Bueno
Accuracy Val:   84%   Bueno
# → El modelo aprendió patrones generales

🔧 ¿Qué es la Regularización?

Regularización es una técnica para penalizar la complejidad del modelo, forzándolo a ser más simple y generalizar mejor.

Analogía: Simplificar para Entender Mejor

Sin Regularización Con Regularización
“Si llueve Y es lunes Y es marzo Y el viento sopla del norte Y… → lleva paraguas” “Si llueve → lleva paraguas”
Regla compleja, específica Regla simple, general
Overfitting Generalización

Tipos de Regularización

  1. L1 (Lasso)

    • Penaliza la suma absoluta de coeficientes
    • Algunos coeficientes se vuelven exactamente cero
    • Selección automática de features
  2. L2 (Ridge)Usado en Logistic Regression por defecto

    • Penaliza la suma de cuadrados de coeficientes
    • Coeficientes se vuelven pequeños pero no cero
    • Reduce magnitud de todos los coeficientes
  3. ElasticNet

    • Combina L1 y L2
    • Balance entre ambas técnicas

📊 El Parámetro C en Logistic Regression

En scikit-learn, el parámetro C controla la fuerza de la regularización:

Definición

C = 1 / λ

Donde λ (lambda) es el parámetro de regularización tradicional.

Interpretación de C

Valor de C λ Regularización Efecto
C muy pequeño (0.01) λ muy grande Fuerte Modelo muy simple, puede underfit
C pequeño (0.1) λ grande Fuerte Modelo simple
C medio (1.0) λ medio Moderada Balance
C grande (10) λ pequeño Débil Modelo complejo
C muy grande (100) λ muy pequeño Muy débil Modelo muy complejo, puede overfit

Visualización del Efecto de C

Complejidad del Modelo
    │
    │                                    ╱ Overfitting
    │                              ╱
    │                        ╱
    │                  ╱        
    │            ╱              
    │      ╱                    ← Sweet spot
    │╱ Underfitting              
    └────────────────────────────────────────────
        0.01    0.1     1.0    10     100         C

        ←── Más regularización   Menos regularización ──→

💻 Implementación: Encontrar el Mejor C

Paso 1: Preparar los Datos

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction import DictVectorizer
from sklearn.metrics import accuracy_score

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

# División de datos
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)

y_train = df_train_full['converted'].values
y_val = df_val['converted'].values
y_test = df_test['converted'].values

X_train_df = df_train_full.drop('converted', axis=1).reset_index(drop=True)
X_val_df = df_val.drop('converted', axis=1).reset_index(drop=True)
X_test_df = df_test.drop('converted', axis=1).reset_index(drop=True)

# One-hot encoding
dv = DictVectorizer(sparse=False)
X_train = dv.fit_transform(X_train_df.to_dict(orient='records'))
X_val = dv.transform(X_val_df.to_dict(orient='records'))
X_test = dv.transform(X_test_df.to_dict(orient='records'))

print("✅ Datos preparados")
print(f"   Train: {X_train.shape}")
print(f"   Val:   {X_val.shape}")
print(f"   Test:  {X_test.shape}")

Paso 2: Probar Diferentes Valores de C

print("\nQUESTION 6: BÚSQUEDA DEL MEJOR PARÁMETRO C")
print("=" * 70)

# Valores de C a probar (según el homework)
C_values = [0.01, 0.1, 1, 10, 100]

# Diccionario para guardar resultados
results = {}

print(f"\n{'C':>8} {'Acc Train':>12} {'Acc Val':>12} {'Acc Val (3 dec)':>18} {'Nota'}")
print("-" * 70)

for C in C_values:
    # Entrenar modelo con C específico
    model = LogisticRegression(
        solver='liblinear', 
        C=C, 
        max_iter=1000, 
        random_state=42
    )
    model.fit(X_train, y_train)

    # Evaluar en train y validation
    y_pred_train = model.predict(X_train)
    y_pred_val = model.predict(X_val)

    accuracy_train = accuracy_score(y_train, y_pred_train)
    accuracy_val = accuracy_score(y_val, y_pred_val)

    # Redondear a 3 decimales (según homework)
    accuracy_val_rounded = round(accuracy_val, 3)

    # Guardar resultado
    results[C] = accuracy_val_rounded

    # Determinar nota sobre overfitting
    diff = accuracy_train - accuracy_val
    if diff < 0.02:
        nota = "✓ Bien balanceado"
    elif diff < 0.05:
        nota = "~ Leve overfit"
    else:
        nota = "⚠ Overfit"

    print(f"{C:>8.2f} {accuracy_train:>12.6f} {accuracy_val:>12.6f} "
          f"{accuracy_val_rounded:>18.3f}  {nota}")

print("-" * 70)

Salida esperada:

QUESTION 6: BÚSQUEDA DEL MEJOR PARÁMETRO C
======================================================================

       C    Acc Train       Acc Val  Acc Val (3 dec)  Nota
----------------------------------------------------------------------
    0.01     0.847765     0.845890               0.846  ✓ Bien balanceado
    0.10     0.849260     0.849315               0.849  ✓ Bien balanceado
    1.00     0.849315     0.849315               0.849  ✓ Bien balanceado
   10.00     0.849315     0.849315               0.849  ✓ Bien balanceado
  100.00     0.849315     0.849315               0.849  ✓ Bien balanceado
----------------------------------------------------------------------

Paso 3: Identificar el Mejor C

print("\nANÁLISIS DE RESULTADOS")
print("=" * 70)

# Encontrar el máximo accuracy
max_accuracy = max(results.values())

# Encontrar todos los C con máximo accuracy
best_Cs = [c for c, acc in results.items() if acc == max_accuracy]

# Si hay empate, seleccionar el menor C (más regularización)
best_C = min(best_Cs)

print(f"\nResultados ordenados por accuracy:")
for c, acc in sorted(results.items(), key=lambda x: (-x[1], x[0])):
    marker = " ← MEJOR" if c == best_C else ""
    print(f"  C = {c:>6.2f}  →  Accuracy = {acc:.3f}{marker}")

print(f"\n🎯 Mejor C: {best_C}")
print(f"   Accuracy: {max_accuracy:.3f}")

if len(best_Cs) > 1:
    print(f"\n💡 Nota: Empate entre C = {best_Cs}")
    print(f"   Seleccionamos el menor C ({best_C}) → Más regularización")
    print(f"   Razón: Modelo más simple con el mismo rendimiento")

print(f"\n✅ RESPUESTA QUESTION 6: {best_C}")

Paso 4: Visualizar el Efecto de C

# Preparar datos para visualización
Cs_list = sorted(results.keys())
accuracies = [results[c] for c in Cs_list]

# Crear gráfico
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))

# Gráfico 1: Accuracy vs C (escala logarítmica)
ax1.plot(Cs_list, accuracies, 'o-', linewidth=2, markersize=8, color='steelblue')
ax1.axhline(y=max_accuracy, color='red', linestyle='--', 
            label=f'Máximo: {max_accuracy:.3f}', alpha=0.7)
ax1.set_xscale('log')
ax1.set_xlabel('C (escala logarítmica)', fontweight='bold')
ax1.set_ylabel('Accuracy en Validación', fontweight='bold')
ax1.set_title('Accuracy vs Parámetro C', fontsize=14, fontweight='bold')
ax1.grid(alpha=0.3)
ax1.legend()

# Marcar el mejor C
best_idx = Cs_list.index(best_C)
ax1.plot(best_C, accuracies[best_idx], 'r*', markersize=20, 
         label=f'Mejor C = {best_C}')

# Gráfico 2: Diferencia respecto al mejor
differences = [acc - max_accuracy for acc in accuracies]
colors = ['green' if d == 0 else 'orange' if d > -0.01 else 'red' 
          for d in differences]

ax2.bar(range(len(Cs_list)), differences, color=colors, alpha=0.7)
ax2.set_xticks(range(len(Cs_list)))
ax2.set_xticklabels([f'{c}' for c in Cs_list])
ax2.set_xlabel('C', fontweight='bold')
ax2.set_ylabel('Diferencia vs Máximo', fontweight='bold')
ax2.set_title('Pérdida de Accuracy vs Mejor Modelo', 
              fontsize=14, fontweight='bold')
ax2.axhline(y=0, color='black', linestyle='-', linewidth=1)
ax2.grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

📊 Análisis Detallado del Efecto de C

Comparar Train vs Validation

print("\nCOMPARACIÓN TRAIN vs VALIDATION POR C")
print("=" * 80)

print(f"\n{'C':>8} {'Train Acc':>12} {'Val Acc':>12} {'Diferencia':>12} {'Interpretación'}")
print("-" * 80)

for C in C_values:
    model = LogisticRegression(solver='liblinear', C=C, 
                               max_iter=1000, random_state=42)
    model.fit(X_train, y_train)

    acc_train = accuracy_score(y_train, model.predict(X_train))
    acc_val = accuracy_score(y_val, model.predict(X_val))
    diff = acc_train - acc_val

    if diff < 0.01:
        interp = "Excelente generalización"
    elif diff < 0.03:
        interp = "Buena generalización"
    elif diff < 0.05:
        interp = "Leve overfitting"
    else:
        interp = "Overfitting significativo"

    print(f"{C:>8.2f} {acc_train:>12.6f} {acc_val:>12.6f} "
          f"{diff:>12.6f}  {interp}")

Analizar Coeficientes

print("\nEFECTO DE C EN LOS COEFICIENTES")
print("=" * 70)

# Entrenar modelos con C extremos
model_low_C = LogisticRegression(solver='liblinear', C=0.01, 
                                  max_iter=1000, random_state=42)
model_high_C = LogisticRegression(solver='liblinear', C=100, 
                                   max_iter=1000, random_state=42)

model_low_C.fit(X_train, y_train)
model_high_C.fit(X_train, y_train)

# Comparar magnitud de coeficientes
coef_low = model_low_C.coef_[0]
coef_high = model_high_C.coef_[0]

print(f"\nEstadísticas de coeficientes:")
print(f"\n{'Métrica':<25} {'C = 0.01':>15} {'C = 100':>15}")
print("-" * 60)
print(f"{'Media absoluta':<25} {np.mean(np.abs(coef_low)):>15.6f} "
      f"{np.mean(np.abs(coef_high)):>15.6f}")
print(f"{'Máximo absoluto':<25} {np.max(np.abs(coef_low)):>15.6f} "
      f"{np.max(np.abs(coef_high)):>15.6f}")
print(f"{'Std de coeficientes':<25} {np.std(coef_low):>15.6f} "
      f"{np.std(coef_high):>15.6f}")

print(f"\n💡 Observación:")
print(f"   C pequeño (0.01) → Coeficientes más pequeños (más regularización)")
print(f"   C grande (100) → Coeficientes más grandes (menos regularización)")

🎯 Evaluación Final en Test Set

Una vez encontrado el mejor C, evaluamos en el conjunto de test:

print("\nEVALUACIÓN FINAL EN TEST SET")
print("=" * 70)

# Entrenar modelo final con el mejor C
final_model = LogisticRegression(
    solver='liblinear', 
    C=best_C, 
    max_iter=1000, 
    random_state=42
)
final_model.fit(X_train, y_train)

# Evaluar en todos los conjuntos
y_pred_train_final = final_model.predict(X_train)
y_pred_val_final = final_model.predict(X_val)
y_pred_test_final = final_model.predict(X_test)

acc_train_final = accuracy_score(y_train, y_pred_train_final)
acc_val_final = accuracy_score(y_val, y_pred_val_final)
acc_test_final = accuracy_score(y_test, y_pred_test_final)

print(f"\nModelo final (C = {best_C}):")
print(f"  Accuracy Train: {acc_train_final:.6f} ({acc_train_final*100:.2f}%)")
print(f"  Accuracy Val:   {acc_val_final:.6f} ({acc_val_final*100:.2f}%)")
print(f"  Accuracy Test:  {acc_test_final:.6f} ({acc_test_final*100:.2f}%)")

# Verificar consistencia
if abs(acc_val_final - acc_test_final) < 0.02:
    print(f"\n✅ Excelente: Val y Test son muy similares")
    print(f"   El modelo generaliza bien a datos nuevos")
elif abs(acc_val_final - acc_test_final) < 0.05:
    print(f"\n✓ Bueno: Val y Test son razonablemente similares")
else:
    print(f"\n⚠ Advertencia: Val y Test difieren significativamente")
    print(f"   Puede haber sobreajuste en validation")

💡 Mejores Prácticas en Regularización

✅ DO (Hacer)

  1. Probar múltiples valores de C
   C_values = [0.001, 0.01, 0.1, 1, 10, 100, 1000]
  1. Usar validación cruzada para mayor robustez
   from sklearn.model_selection import GridSearchCV
   param_grid = {'C': [0.01, 0.1, 1, 10, 100]}
   grid_search = GridSearchCV(LogisticRegression(), param_grid, cv=5)
  1. Evaluar múltiples métricas
   # No solo accuracy, también precision, recall, F1
  1. Verificar en test set al final
   # Usar test solo una vez, al final

❌ DON’T (No Hacer)

  1. No usar validation set
   # ❌ MAL: Optimizar en train
   # ✅ BIEN: Optimizar en validation
  1. Optimizar en test set
   # ❌ MAL: Elegir C basándose en test
   # Esto introduce data leakage
  1. Ignorar la diferencia train-val
   # Si train >> val → overfitting
   # Necesitas más regularización (C menor)
  1. No considerar el contexto
   # Balance entre interpretabilidad y rendimiento

📝 Código Completo para Referencia

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.feature_extraction import DictVectorizer
from sklearn.metrics import accuracy_score

# 1. Preparar 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
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)

y_train = df_train_full['converted'].values
y_val = df_val['converted'].values

X_train_df = df_train_full.drop('converted', axis=1)
X_val_df = df_val.drop('converted', axis=1)

# 3. One-hot encoding
dv = DictVectorizer(sparse=False)
X_train = dv.fit_transform(X_train_df.to_dict(orient='records'))
X_val = dv.transform(X_val_df.to_dict(orient='records'))

# 4. Probar diferentes C
C_values = [0.01, 0.1, 1, 10, 100]
results = {}

for C in C_values:
    model = LogisticRegression(solver='liblinear', C=C, max_iter=1000, random_state=42)
    model.fit(X_train, y_train)

    y_pred_val = model.predict(X_val)
    accuracy_val = accuracy_score(y_val, y_pred_val)
    accuracy_val_rounded = round(accuracy_val, 3)

    results[C] = accuracy_val_rounded
    print(f"C={C}: accuracy = {accuracy_val_rounded}")

# 5. Encontrar mejor C
max_accuracy = max(results.values())
best_C = min([c for c, acc in results.items() if acc == max_accuracy])

print(f"\nMejor C: {best_C} (accuracy: {max_accuracy:.3f})")

🎯 Resumen del Homework Completo

Ahora tenemos todas las respuestas:

print("\n" + "=" * 70)
print("RESUMEN COMPLETO - HOMEWORK 3: CLASIFICACIÓN")
print("=" * 70)

print(f"\nQuestion 1 - Moda de 'industry':")
print(f"  Respuesta: retail")

print(f"\nQuestion 2 - Mayor correlación:")
print(f"  Respuesta: annual_income y interaction_count")

print(f"\nQuestion 3 - Mayor Mutual Information Score:")
print(f"  Respuesta: employment_status")

print(f"\nQuestion 4 - Accuracy de Regresión Logística:")
print(f"  Respuesta: 0.85")

print(f"\nQuestion 5 - Feature menos útil:")
print(f"  Respuesta: employment_status")

print(f"\nQuestion 6 - Mejor valor de C:")
print(f"  Respuesta: {best_C}")

print("\n" + "=" * 70)
print("✅ HOMEWORK COMPLETADO")
print("=" * 70)

🎓 Conclusión

En este post final aprendimos sobre regularización:

  1. ✅ Qué es la regularización y por qué previene overfitting
  2. ✅ Cómo funciona el parámetro C en regresión logística
  3. ✅ Cómo encontrar el mejor C mediante búsqueda sistemática
  4. ✅ Cómo evaluar el modelo final en test set

Puntos clave:

  • 🎯 C controla la fuerza de regularización (C pequeño = más regularización)
  • 📊 Siempre optimizar en validation, no en test
  • 🔍 Si hay empate, elegir el C más pequeño (modelo más simple)
  • ⚖ Balance entre train y validation accuracy es crucial

🚀 Siguientes Pasos

¡Felicitaciones! Has completado el Homework 3 de Clasificación. Ahora puedes:

  1. ✅ Experimentar con otros datasets de clasificación
  2. ✅ Probar otros algoritmos (Random Forest, Gradient Boosting)
  3. ✅ Explorar técnicas avanzadas (SMOTE para desbalance, ensemble methods)
  4. ✅ Aplicar lo aprendido a problemas reales de tu trabajo

¿Qué valor de C encontraste óptimo? ¿Notaste diferencias significativas entre valores? ¡Comparte tus resultados!


This content originally appeared on DEV Community and was authored by Jesus Oviedo Riquelme