Saltar al contenido
Numerus Scriptum por Edison Achalma

Métricas de performance en ML Python

Evaluación de modelos predictivos con métricas de regresión y clasificación implementadas en scikit-learn.

46 min de lectura PDF de esta lección(196 KB) DOCX (252 KB)

En esta octava guía exploraremos el problema de predicción y las métricas fundamentales para evaluar modelos de clasificación. Estos conceptos son esenciales para evaluar modelos de machine learning aplicados a problemas económicos como predicción de incumplimiento crediticio, clasificación de empresas, predicción de pobreza, entre otros.

1 El problema de predicción

1.1 Concepto fundamental

En economía y ciencias sociales frecuentemente necesitamos predecir eventos futuros basándonos en información histórica:

  • ¿Un cliente pagará o no pagará su crédito?
  • ¿Una empresa quebrará o sobrevivirá?
  • ¿Un hogar saldrá o permanecerá en pobreza?
  • ¿Un estudiante completará o abandonará sus estudios?
  • ¿Un producto se venderá o no se venderá?
python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report
from sklearn.metrics import roc_curve, auc, precision_recall_curve
from sklearn.model_selection import train_test_split

# Configuración de gráficos
plt.style.use('seaborn-v0_8-darkgrid')
plt.rcParams['figure.figsize'] = (10, 6)

1.2 Flujo de decisión con predicción

python
print("""
FLUJO DE TOMA DE DECISIONES CON PREDICCIÓN

1. RECOLECCIÓN DE DATOS
   ├─ Variables explicativas (características)
   └─ Variable objetivo (lo que queremos predecir)
   
2. ENTRENAMIENTO DEL MODELO
   ├─ Aprender patrones de los datos históricos
   └─ Establecer relación entre X (predictores) y Y (objetivo)
   
3. PREDICCIÓN
   ├─ Aplicar modelo a nuevos casos
   └─ Obtener probabilidades o clasificaciones
   
4. EVALUACIÓN
   ├─ Medir qué tan buenas son las predicciones
   └─ Usar métricas apropiadas
   
5. DECISIÓN
   ├─ Usar predicción para tomar acción
   └─ Ej: Aprobar/rechazar crédito, intervenir/no intervenir
""")

FLUJO DE TOMA DE DECISIONES CON PREDICCIÓN

1. RECOLECCIÓN DE DATOS
   ├─ Variables explicativas (características)
   └─ Variable objetivo (lo que queremos predecir)

2. ENTRENAMIENTO DEL MODELO
   ├─ Aprender patrones de los datos históricos
   └─ Establecer relación entre X (predictores) y Y (objetivo)

3. PREDICCIÓN
   ├─ Aplicar modelo a nuevos casos
   └─ Obtener probabilidades o clasificaciones

4. EVALUACIÓN
   ├─ Medir qué tan buenas son las predicciones
   └─ Usar métricas apropiadas

5. DECISIÓN
   ├─ Usar predicción para tomar acción
   └─ Ej: Aprobar/rechazar crédito, intervenir/no intervenir

2 Clasificación binaria

2.1 Problema de admisión académica

python
# Crear dataset de ejemplo: admisión a programa de posgrado
np.random.seed(42)

# Datos simulados
n_estudiantes = 200

datos_admision = pd.DataFrame({
    'puntaje_examen': np.random.randint(500, 800, n_estudiantes),
    'gpa': np.random.uniform(2.0, 4.0, n_estudiantes),
    'experiencia_laboral': np.random.randint(0, 8, n_estudiantes),
    'publicaciones': np.random.randint(0, 5, n_estudiantes)
})

# Generar variable de admisión (1 = admitido, 0 = no admitido)
# Basado en una combinación de factores
score = (datos_admision['puntaje_examen'] - 500) * 0.002 + \
        datos_admision['gpa'] * 0.15 + \
        datos_admision['experiencia_laboral'] * 0.08 + \
        datos_admision['publicaciones'] * 0.10 + \
        np.random.normal(0, 0.2, n_estudiantes)

datos_admision['admitido'] = (score > 1.0).astype(int)

print("Dataset de admisiones:")
print(datos_admision.head(10))
print(f"\nDistribución de admisiones:")
print(datos_admision['admitido'].value_counts())
print(f"\nTasa de admisión: {datos_admision['admitido'].mean()*100:.1f}%")
Dataset de admisiones:
   puntaje_examen       gpa  experiencia_laboral  publicaciones  admitido
0             602  3.355129                    6              3         1
1             770  2.033176                    5              4         1
2             606  3.024186                    1              4         1
3             571  2.452992                    4              4         1
4             688  3.290346                    2              4         1
5             520  2.348733                    1              4         0
6             602  3.381875                    2              2         0
7             621  2.773471                    6              3         1
8             714  3.873460                    0              4         1
9             587  2.275042                    1              3         0

Distribución de admisiones:
admitido
1    140
0     60
Name: count, dtype: int64

Tasa de admisión: 70.0%

2.2 Entrenar modelo de clasificación

python
# Separar variables predictoras y objetivo
X = datos_admision[['puntaje_examen', 'gpa', 'experiencia_laboral', 'publicaciones']]
y = datos_admision['admitido']

# Dividir en conjunto de entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42
)

print(f"Tamaño conjunto entrenamiento: {len(X_train)}")
print(f"Tamaño conjunto prueba: {len(X_test)}")

# Entrenar modelo de regresión logística
modelo = LogisticRegression(random_state=42)
modelo.fit(X_train, y_train)

print("\nModelo entrenado exitosamente")
print(f"Coeficientes del modelo:")
for variable, coef in zip(X.columns, modelo.coef_[0]):
    print(f"  {variable}: {coef:.4f}")
Tamaño conjunto entrenamiento: 140
Tamaño conjunto prueba: 60

Modelo entrenado exitosamente
Coeficientes del modelo:
  puntaje_examen: 0.0089
  gpa: 0.7248
  experiencia_laboral: 0.5510
  publicaciones: 0.8955

3 Thresholding (Umbralización)

3.1 Concepto de probabilidades

python
# Obtener probabilidades de predicción
probabilidades = modelo.predict_proba(X_test)

print("Probabilidades de predicción (primeras 10 observaciones):")
print("\nÍndice | P(No admitido) | P(Admitido) | Predicción")
print("-" * 55)

for i in range(10):
    pred = "Admitido" if probabilidades[i, 1] > 0.5 else "No admitido"
    print(f"{i:6d} | {probabilidades[i, 0]:14.4f} | {probabilidades[i, 1]:11.4f} | {pred}")

# Crear DataFrame con resultados
resultados = pd.DataFrame({
    'prob_no_admitido': probabilidades[:, 0],
    'prob_admitido': probabilidades[:, 1],
    'real': y_test.values
})

print("\n\nEstadísticas de probabilidades:")
print(resultados['prob_admitido'].describe())
Probabilidades de predicción (primeras 10 observaciones):

Índice | P(No admitido) | P(Admitido) | Predicción
-------------------------------------------------------
     0 |         0.2196 |      0.7804 | Admitido
     1 |         0.1008 |      0.8992 | Admitido
     2 |         0.4035 |      0.5965 | Admitido
     3 |         0.2008 |      0.7992 | Admitido
     4 |         0.0295 |      0.9705 | Admitido
     5 |         0.7350 |      0.2650 | No admitido
     6 |         0.0866 |      0.9134 | Admitido
     7 |         0.5659 |      0.4341 | No admitido
     8 |         0.0246 |      0.9754 | Admitido
     9 |         0.1647 |      0.8353 | Admitido


Estadísticas de probabilidades:
count    60.000000
mean      0.685291
std       0.275099
min       0.078084
25%       0.498738
50%       0.780514
75%       0.928637
max       0.998200
Name: prob_admitido, dtype: float64

3.2 Efecto del umbral

python
# Experimentar con diferentes umbrales
umbrales = [0.3, 0.5, 0.7, 0.9]

print("EFECTO DEL UMBRAL EN LA CLASIFICACIÓN")
print("=" * 70)

for umbral in umbrales:
    # Aplicar umbral
    predicciones = (probabilidades[:, 1] > umbral).astype(int)
    
    # Contar predicciones
    n_positivos = predicciones.sum()
    tasa_positivos = (n_positivos / len(predicciones)) * 100
    
    print(f"\nUmbral: {umbral}")
    print(f"  Predicciones positivas: {n_positivos} ({tasa_positivos:.1f}%)")
    print(f"  Predicciones negativas: {len(predicciones) - n_positivos}")
EFECTO DEL UMBRAL EN LA CLASIFICACIÓN
======================================================================

Umbral: 0.3
  Predicciones positivas: 53 (88.3%)
  Predicciones negativas: 7

Umbral: 0.5
  Predicciones positivas: 45 (75.0%)
  Predicciones negativas: 15

Umbral: 0.7
  Predicciones positivas: 34 (56.7%)
  Predicciones negativas: 26

Umbral: 0.9
  Predicciones positivas: 18 (30.0%)
  Predicciones negativas: 42

3.3 Visualizar distribución de probabilidades

python
# Crear gráfico de distribución de probabilidades
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Histograma por clase real
for clase, nombre in [(0, 'No admitido'), (1, 'Admitido')]:
    mask = resultados['real'] == clase
    axes[0].hist(resultados.loc[mask, 'prob_admitido'], 
                 bins=20, alpha=0.6, label=nombre)

axes[0].axvline(x=0.5, color='red', linestyle='--', label='Umbral = 0.5')
axes[0].set_xlabel('Probabilidad de admisión')
axes[0].set_ylabel('Frecuencia')
axes[0].set_title('Distribución de probabilidades por clase real')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Scatter plot
colores = ['red' if x == 0 else 'green' for x in resultados['real']]
axes[1].scatter(range(len(resultados)), 
                resultados['prob_admitido'],
                c=colores, alpha=0.6)
axes[1].axhline(y=0.5, color='blue', linestyle='--', label='Umbral = 0.5')
axes[1].set_xlabel('Índice de observación')
axes[1].set_ylabel('Probabilidad de admisión')
axes[1].set_title('Probabilidades predichas vs clase real')
axes[1].legend(['Umbral 0.5', 'No admitido', 'Admitido'])
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('distribucion_probabilidades.png', dpi=300, bbox_inches='tight')
print("\nGráfico guardado como 'distribucion_probabilidades.png'")

Gráfico guardado como 'distribucion_probabilidades.png'

4 Matriz de confusión

4.1 Concepto y estructura

La matriz de confusión es una tabla que describe el desempeño de un modelo de clasificación.

python
print("""
MATRIZ DE CONFUSIÓN - ESTRUCTURA

                    Predicción
                 Negativo  Positivo
               ┌──────────┬──────────┐
Real  Negativo │    TN    │    FP    │
               ├──────────┼──────────┤
      Positivo │    FN    │    TP    │
               └──────────┴──────────┘

Donde:
- TN (True Negative): Negativos correctamente identificados
- FP (False Positive): Negativos incorrectamente clasificados como positivos
- FN (False Negative): Positivos incorrectamente clasificados como negativos
- TP (True Positive): Positivos correctamente identificados
""")

MATRIZ DE CONFUSIÓN - ESTRUCTURA

                    Predicción
                 Negativo  Positivo
               ┌──────────┬──────────┐
Real  Negativo │    TN    │    FP    │
               ├──────────┼──────────┤
      Positivo │    FN    │    TP    │
               └──────────┴──────────┘

Donde:
- TN (True Negative): Negativos correctamente identificados
- FP (False Positive): Negativos incorrectamente clasificados como positivos
- FN (False Negative): Positivos incorrectamente clasificados como negativos
- TP (True Positive): Positivos correctamente identificados

4.2 Calcular matriz de confusión

python
# Predicciones con umbral 0.5
y_pred = modelo.predict(X_test)

# Calcular matriz de confusión
cm = confusion_matrix(y_test, y_pred)

print("MATRIZ DE CONFUSIÓN")
print("=" * 50)
print(f"\n                 Predicción")
print(f"              No admitido  Admitido")
print(f"Real No admitido    {cm[0,0]:4d}       {cm[0,1]:4d}")
print(f"     Admitido       {cm[1,0]:4d}       {cm[1,1]:4d}")

# Extraer componentes
TN, FP, FN, TP = cm.ravel()

print(f"\n\nComponentes de la matriz:")
print(f"  Verdaderos Negativos (TN): {TN}")
print(f"  Falsos Positivos (FP): {FP}")
print(f"  Falsos Negativos (FN): {FN}")
print(f"  Verdaderos Positivos (TP): {TP}")
MATRIZ DE CONFUSIÓN
==================================================

                 Predicción
              No admitido  Admitido
Real No admitido      14         11
     Admitido          1         34


Componentes de la matriz:
  Verdaderos Negativos (TN): 14
  Falsos Positivos (FP): 11
  Falsos Negativos (FN): 1
  Verdaderos Positivos (TP): 34

4.3 Función personalizada para matriz de confusión

python
def calcular_matriz_confusion(y_real, y_pred):
    """
    Calcula matriz de confusión y sus componentes
    """
    # Convertir a arrays booleanos
    y_real = np.array(y_real).astype(bool)
    y_pred = np.array(y_pred).astype(bool)
    
    # Calcular componentes
    TP = (y_real & y_pred).sum()
    TN = (~y_real & ~y_pred).sum()
    FP = (~y_real & y_pred).sum()
    FN = (y_real & ~y_pred).sum()
    
    # Crear matriz
    matriz = np.array([[TN, FP],
                       [FN, TP]])
    
    return matriz, TP, TN, FP, FN

# Usar función personalizada
matriz, TP, TN, FP, FN = calcular_matriz_confusion(y_test, y_pred)

print("Matriz de confusión (función personalizada):")
print(matriz)
print(f"\nVerificación: TP={TP}, TN={TN}, FP={FP}, FN={FN}")
Matriz de confusión (función personalizada):
[[14 11]
 [ 1 34]]

Verificación: TP=34, TN=14, FP=11, FN=1

4.4 Visualizar matriz de confusión

python
def graficar_matriz_confusion(cm, nombres_clases=['No', 'Sí']):
    """
    Visualiza la matriz de confusión
    """
    fig, ax = plt.subplots(figsize=(8, 6))
    
    im = ax.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)
    ax.figure.colorbar(im, ax=ax)
    
    ax.set(xticks=np.arange(cm.shape[1]),
           yticks=np.arange(cm.shape[0]),
           xticklabels=nombres_clases,
           yticklabels=nombres_clases,
           ylabel='Clase Real',
           xlabel='Clase Predicha',
           title='Matriz de Confusión')
    
    # Anotar celdas con valores
    fmt = 'd'
    thresh = cm.max() / 2.
    for i in range(cm.shape[0]):
        for j in range(cm.shape[1]):
            ax.text(j, i, format(cm[i, j], fmt),
                   ha="center", va="center",
                   color="white" if cm[i, j] > thresh else "black",
                   fontsize=20)
    
    plt.tight_layout()
    return fig

# Graficar
fig = graficar_matriz_confusion(cm, nombres_clases=['No admitido', 'Admitido'])
plt.savefig('matriz_confusion.png', dpi=300, bbox_inches='tight')
print("Matriz de confusión guardada como 'matriz_confusion.png'")
Matriz de confusión guardada como 'matriz_confusion.png'

5 Métricas de clasificación

5.1 Resumen de métricas principales

python
print("""
MÉTRICAS DE CLASIFICACIÓN

1. ACCURACY (Exactitud)
   - Proporción total de predicciones correctas
   - Fórmula: (TP + TN) / (TP + TN + FP + FN)
   - Cuándo usar: Clases balanceadas
   
2. PRECISION (Precisión)
   - De las predicciones positivas, cuántas son correctas
   - Fórmula: TP / (TP + FP)
   - Cuándo usar: Costo alto de falsos positivos
   - Ejemplo: Diagnóstico médico (evitar alarmas falsas)
   
3. RECALL (Sensibilidad/Exhaustividad)
   - De los casos positivos reales, cuántos detectamos
   - Fórmula: TP / (TP + FN)
   - Cuándo usar: Costo alto de falsos negativos
   - Ejemplo: Detección de fraude (no perder casos reales)
   
4. F1-SCORE
   - Media armónica de precision y recall
   - Fórmula: 2 * (Precision * Recall) / (Precision + Recall)
   - Cuándo usar: Balance entre precision y recall
   
5. SPECIFICITY (Especificidad)
   - De los casos negativos reales, cuántos detectamos
   - Fórmula: TN / (TN + FP)
""")

MÉTRICAS DE CLASIFICACIÓN

1. ACCURACY (Exactitud)
   - Proporción total de predicciones correctas
   - Fórmula: (TP + TN) / (TP + TN + FP + FN)
   - Cuándo usar: Clases balanceadas

2. PRECISION (Precisión)
   - De las predicciones positivas, cuántas son correctas
   - Fórmula: TP / (TP + FP)
   - Cuándo usar: Costo alto de falsos positivos
   - Ejemplo: Diagnóstico médico (evitar alarmas falsas)

3. RECALL (Sensibilidad/Exhaustividad)
   - De los casos positivos reales, cuántos detectamos
   - Fórmula: TP / (TP + FN)
   - Cuándo usar: Costo alto de falsos negativos
   - Ejemplo: Detección de fraude (no perder casos reales)

4. F1-SCORE
   - Media armónica de precision y recall
   - Fórmula: 2 * (Precision * Recall) / (Precision + Recall)
   - Cuándo usar: Balance entre precision y recall

5. SPECIFICITY (Especificidad)
   - De los casos negativos reales, cuántos detectamos
   - Fórmula: TN / (TN + FP)

6 Accuracy (Exactitud)

6.1 Cálculo e interpretación

python
def calcular_accuracy(y_real, y_pred):
    """Calcula accuracy"""
    _, TP, TN, FP, FN = calcular_matriz_confusion(y_real, y_pred)
    accuracy = (TP + TN) / (TP + TN + FP + FN)
    return accuracy

# Calcular accuracy
accuracy = calcular_accuracy(y_test, y_pred)
accuracy_sklearn = modelo.score(X_test, y_test)

print(f"Accuracy (función propia): {accuracy:.4f} ({accuracy*100:.2f}%)")
print(f"Accuracy (sklearn): {accuracy_sklearn:.4f} ({accuracy_sklearn*100:.2f}%)")

print(f"\nInterpretación:")
print(f"El modelo clasifica correctamente el {accuracy*100:.1f}% de los casos")
Accuracy (función propia): 0.8000 (80.00%)
Accuracy (sklearn): 0.8000 (80.00%)

Interpretación:
El modelo clasifica correctamente el 80.0% de los casos

6.2 Limitaciones del accuracy

python
# Ejemplo con dataset desbalanceado
print("\nEJEMPLO: LIMITACIÓN DEL ACCURACY CON CLASES DESBALANCEADAS")
print("=" * 70)

# Dataset muy desbalanceado (95% negativos, 5% positivos)
n = 1000
y_desbalanceado = np.array([0]*950 + [1]*50)

# Modelo que siempre predice negativo
y_pred_siempre_negativo = np.zeros(n)

acc = calcular_accuracy(y_desbalanceado, y_pred_siempre_negativo)
print(f"\nModelo que predice siempre 'No':")
print(f"Accuracy: {acc:.4f} ({acc*100:.1f}%)")
print("\n¡Accuracy alto pero modelo inútil!")
print("No detecta ningún caso positivo.")

# Matriz de confusión
cm_desbalanceado = confusion_matrix(y_desbalanceado, y_pred_siempre_negativo)
print(f"\nMatriz de confusión:")
print(cm_desbalanceado)
print("\nObservación: TP = 0 (no detecta ningún positivo)")

EJEMPLO: LIMITACIÓN DEL ACCURACY CON CLASES DESBALANCEADAS
======================================================================

Modelo que predice siempre 'No':
Accuracy: 0.9500 (95.0%)

¡Accuracy alto pero modelo inútil!
No detecta ningún caso positivo.

Matriz de confusión:
[[950   0]
 [ 50   0]]

Observación: TP = 0 (no detecta ningún positivo)

7 Precision (Precisión)

7.1 Cálculo e interpretación

python
def calcular_precision(y_real, y_pred):
    """Calcula precision"""
    _, TP, TN, FP, FN = calcular_matriz_confusion(y_real, y_pred)
    
    if TP + FP == 0:
        return 0.0
    
    precision = TP / (TP + FP)
    return precision

# Calcular precision
precision = calcular_precision(y_test, y_pred)

print(f"Precision: {precision:.4f} ({precision*100:.2f}%)")
print(f"\nInterpretación:")
print(f"De los estudiantes que el modelo predice como 'admitidos',")
print(f"el {precision*100:.1f}% realmente son admitidos.")
print(f"\nFalsos positivos: {FP} estudiantes")
print("(Predichos como admitidos pero en realidad no lo fueron)")
Precision: 0.7556 (75.56%)

Interpretación:
De los estudiantes que el modelo predice como 'admitidos',
el 75.6% realmente son admitidos.

Falsos positivos: 11 estudiantes
(Predichos como admitidos pero en realidad no lo fueron)

7.2 Ejemplo económico: Aprobación de créditos

python
print("\n\nEJEMPLO ECONÓMICO: APROBACIÓN DE CRÉDITOS")
print("=" * 70)

# Simular decisiones de crédito
n_creditos = 1000
y_real_pagara = np.random.choice([0, 1], n_creditos, p=[0.2, 0.8])  # 80% pagan
y_pred_aprobar = np.random.choice([0, 1], n_creditos, p=[0.15, 0.85])  # Modelo aprueba 85%

# Calcular métricas
_, TP, TN, FP, FN = calcular_matriz_confusion(y_real_pagara, y_pred_aprobar)
precision_credito = calcular_precision(y_real_pagara, y_pred_aprobar)

print(f"\nResultados de {n_creditos} solicitudes:")
print(f"  Créditos aprobados por modelo: {TP + FP}")
print(f"  De estos, pagarán: {TP} ({precision_credito*100:.1f}%)")
print(f"  De estos, NO pagarán: {FP} (Pérdida por falsos positivos)")

# Calcular pérdida
monto_promedio = 10000
perdida_por_fp = FP * monto_promedio

print(f"\n\nAnálisis financiero:")
print(f"  Monto promedio por crédito: S/ {monto_promedio:,}")
print(f"  Falsos positivos: {FP}")
print(f"  Pérdida estimada: S/ {perdida_por_fp:,}")
print(f"\nConlusión: Alta precision reduce pérdidas por morosidad")


EJEMPLO ECONÓMICO: APROBACIÓN DE CRÉDITOS
======================================================================

Resultados de 1000 solicitudes:
  Créditos aprobados por modelo: 847
  De estos, pagarán: 680 (80.3%)
  De estos, NO pagarán: 167 (Pérdida por falsos positivos)


Análisis financiero:
  Monto promedio por crédito: S/ 10,000
  Falsos positivos: 167
  Pérdida estimada: S/ 1,670,000

Conlusión: Alta precision reduce pérdidas por morosidad

8 Recall (Sensibilidad)

8.1 Cálculo e interpretación

python
def calcular_recall(y_real, y_pred):
    """Calcula recall (sensibilidad)"""
    _, TP, TN, FP, FN = calcular_matriz_confusion(y_real, y_pred)
    
    if TP + FN == 0:
        return 0.0
    
    recall = TP / (TP + FN)
    return recall

# Calcular recall
recall = calcular_recall(y_test, y_pred)

print(f"Recall: {recall:.4f} ({recall*100:.2f}%)")
print(f"\nInterpretación:")
print(f"De los estudiantes que realmente fueron admitidos,")
print(f"el modelo detectó correctamente al {recall*100:.1f}%.")
print(f"\nFalsos negativos: {FN} estudiantes")
print("(No fueron identificados como admitidos cuando sí lo fueron)")
Recall: 0.9714 (97.14%)

Interpretación:
De los estudiantes que realmente fueron admitidos,
el modelo detectó correctamente al 97.1%.

Falsos negativos: 118 estudiantes
(No fueron identificados como admitidos cuando sí lo fueron)

8.2 Ejemplo económico: Detección de fraude

python
print("\n\nEJEMPLO ECONÓMICO: DETECCIÓN DE FRAUDE")
print("=" * 70)

# Simular transacciones
n_trans = 10000
y_real_fraude = np.random.choice([0, 1], n_trans, p=[0.98, 0.02])  # 2% fraude
y_pred_fraude = np.random.choice([0, 1], n_trans, p=[0.97, 0.03])  # Modelo detecta 3%

# Calcular métricas
_, TP, TN, FP, FN = calcular_matriz_confusion(y_real_fraude, y_pred_fraude)
recall_fraude = calcular_recall(y_real_fraude, y_pred_fraude)
precision_fraude = calcular_precision(y_real_fraude, y_pred_fraude)

print(f"\nResultados de {n_trans:,} transacciones:")
print(f"  Fraudes reales: {TP + FN}")
print(f"  Fraudes detectados: {TP} ({recall_fraude*100:.1f}% de los reales)")
print(f"  Fraudes no detectados: {FN} ¡CRÍTICO!")

# Calcular pérdida
monto_promedio_fraude = 5000
perdida_por_fn = FN * monto_promedio_fraude

print(f"\n\nAnálisis financiero:")
print(f"  Monto promedio por fraude: S/ {monto_promedio_fraude:,}")
print(f"  Fraudes no detectados (FN): {FN}")
print(f"  Pérdida por fraudes no detectados: S/ {perdida_por_fn:,}")

# Costo de investigar falsos positivos
costo_investigacion = 100
costo_fp = FP * costo_investigacion
print(f"\n  Alertas falsas (FP): {FP}")
print(f"  Costo de investigar falsas alarmas: S/ {costo_fp:,}")

print(f"\n\nConclusión: En detección de fraude, es MÁS IMPORTANTE el Recall")
print("(No queremos perder fraudes reales, aunque tengamos falsas alarmas)")


EJEMPLO ECONÓMICO: DETECCIÓN DE FRAUDE
======================================================================

Resultados de 10,000 transacciones:
  Fraudes reales: 198
  Fraudes detectados: 4 (2.0% de los reales)
  Fraudes no detectados: 194 ¡CRÍTICO!


Análisis financiero:
  Monto promedio por fraude: S/ 5,000
  Fraudes no detectados (FN): 194
  Pérdida por fraudes no detectados: S/ 970,000

  Alertas falsas (FP): 301
  Costo de investigar falsas alarmas: S/ 30,100


Conclusión: En detección de fraude, es MÁS IMPORTANTE el Recall
(No queremos perder fraudes reales, aunque tengamos falsas alarmas)

9 F1-Score

9.1 Cálculo e interpretación

python
def calcular_f1(y_real, y_pred):
    """Calcula F1-score"""
    precision = calcular_precision(y_real, y_pred)
    recall = calcular_recall(y_real, y_pred)
    
    if precision + recall == 0:
        return 0.0
    
    f1 = 2 * (precision * recall) / (precision + recall)
    return f1

# Calcular F1
f1 = calcular_f1(y_test, y_pred)

print(f"Métricas del modelo:")
print(f"  Precision: {precision:.4f}")
print(f"  Recall: {recall:.4f}")
print(f"  F1-Score: {f1:.4f}")

print(f"\n\nInterpretación del F1-Score:")
print("El F1-Score es la media armónica de Precision y Recall.")
print(f"Un F1 de {f1:.2f} indica un {'buen' if f1 > 0.7 else 'regular' if f1 > 0.5 else 'bajo'} balance entre ambas métricas.")
Métricas del modelo:
  Precision: 0.7556
  Recall: 0.9714
  F1-Score: 0.8500


Interpretación del F1-Score:
El F1-Score es la media armónica de Precision y Recall.
Un F1 de 0.85 indica un buen balance entre ambas métricas.

9.2 Trade-off entre Precision y Recall

python
# Demostrar trade-off
umbrales = [0.1, 0.3, 0.5, 0.7, 0.9]

print("\n\nTRADE-OFF: PRECISION VS RECALL")
print("=" * 70)
print(f"{'Umbral':<10} {'Precision':<12} {'Recall':<12} {'F1-Score':<12}")
print("-" * 70)

for umbral in umbrales:
    y_pred_umbral = (probabilidades[:, 1] > umbral).astype(int)
    
    prec = calcular_precision(y_test, y_pred_umbral)
    rec = calcular_recall(y_test, y_pred_umbral)
    f1_temp = calcular_f1(y_test, y_pred_umbral)
    
    print(f"{umbral:<10.1f} {prec:<12.4f} {rec:<12.4f} {f1_temp:<12.4f}")

print("\nObservación:")
print("- Umbral bajo → Mayor Recall (detecta más positivos) pero menor Precision")
print("- Umbral alto → Mayor Precision (menos falsas alarmas) pero menor Recall")


TRADE-OFF: PRECISION VS RECALL
======================================================================
Umbral     Precision    Recall       F1-Score    
----------------------------------------------------------------------
0.1        0.5932       1.0000       0.7447      
0.3        0.6415       0.9714       0.7727      
0.5        0.7556       0.9714       0.8500      
0.7        0.9118       0.8857       0.8986      
0.9        1.0000       0.5143       0.6792      

Observación:
- Umbral bajo → Mayor Recall (detecta más positivos) pero menor Precision
- Umbral alto → Mayor Precision (menos falsas alarmas) pero menor Recall

10 Curva Precision-Recall

10.1 Generar curva Precision-Recall

python
def calcular_curva_precision_recall(y_real, probabilidades):
    """
    Calcula curva Precision-Recall para diferentes umbrales
    """
    # Obtener umbrales únicos
    umbrales = np.sort(np.unique(probabilidades))
    
    precision_list = []
    recall_list = []
    
    for umbral in umbrales:
        y_pred = (probabilidades >= umbral).astype(int)
        
        prec = calcular_precision(y_real, y_pred)
        rec = calcular_recall(y_real, y_pred)
        
        precision_list.append(prec)
        recall_list.append(rec)
    
    return np.array(recall_list), np.array(precision_list), umbrales

# Calcular curva
recall_curva, precision_curva, umbrales_pr = calcular_curva_precision_recall(
    y_test, probabilidades[:, 1]
)

# También usar sklearn
precision_sklearn, recall_sklearn, _ = precision_recall_curve(
    y_test, probabilidades[:, 1]
)

10.2 Visualizar curva Precision-Recall

python
# Crear gráfico
fig, ax = plt.subplots(figsize=(10, 8))

# Curva propia
ax.plot(recall_curva, precision_curva, 'b-', linewidth=2, 
        label='Curva PR (implementación propia)')

# Curva sklearn
ax.plot(recall_sklearn, precision_sklearn, 'r--', linewidth=2, alpha=0.7,
        label='Curva PR (sklearn)')

# Línea de referencia (clasificador aleatorio)
proporcion_positivos = y_test.mean()
ax.axhline(y=proporcion_positivos, color='gray', linestyle='--', 
           label=f'Clasificador aleatorio ({proporcion_positivos:.2f})')

# Punto del modelo actual (umbral 0.5)
ax.plot(recall, precision, 'go', markersize=12, 
        label=f'Umbral 0.5 (P={precision:.2f}, R={recall:.2f})')

ax.set_xlabel('Recall (Sensibilidad)', fontsize=12)
ax.set_ylabel('Precision (Precisión)', fontsize=12)
ax.set_title('Curva Precision-Recall', fontsize=14, fontweight='bold')
ax.legend(loc='best')
ax.grid(True, alpha=0.3)
ax.set_xlim([0, 1.05])
ax.set_ylim([0, 1.05])

plt.tight_layout()
plt.savefig('curva_precision_recall.png', dpi=300, bbox_inches='tight')
print("Curva Precision-Recall guardada como 'curva_precision_recall.png'")
Curva Precision-Recall guardada como 'curva_precision_recall.png'

11 Curva ROC y AUC

11.1 Concepto de ROC

python
print("""
CURVA ROC (Receiver Operating Characteristic)

La curva ROC grafica:
- Eje X: False Positive Rate (FPR) = FP / (FP + TN)
- Eje Y: True Positive Rate (TPR) = TP / (TP + FN)  [mismo que Recall]

Interpretación:
- Diagonal (línea punteada): Clasificador aleatorio (AUC = 0.5)
- Curva sobre diagonal: Mejor que aleatorio
- Más cerca de esquina superior izquierda: Mejor modelo
- AUC (Area Under Curve): Métrica resumen
  * AUC = 1.0: Clasificador perfecto
  * AUC = 0.5: Clasificador aleatorio
  * AUC < 0.5: Peor que aleatorio
""")

CURVA ROC (Receiver Operating Characteristic)

La curva ROC grafica:
- Eje X: False Positive Rate (FPR) = FP / (FP + TN)
- Eje Y: True Positive Rate (TPR) = TP / (TP + FN)  [mismo que Recall]

Interpretación:
- Diagonal (línea punteada): Clasificador aleatorio (AUC = 0.5)
- Curva sobre diagonal: Mejor que aleatorio
- Más cerca de esquina superior izquierda: Mejor modelo
- AUC (Area Under Curve): Métrica resumen
  * AUC = 1.0: Clasificador perfecto
  * AUC = 0.5: Clasificador aleatorio
  * AUC < 0.5: Peor que aleatorio

11.2 Calcular TPR y FPR

python
def calcular_tpr(y_real, y_pred):
    """Calcula True Positive Rate (TPR) = Recall"""
    return calcular_recall(y_real, y_pred)

def calcular_fpr(y_real, y_pred):
    """Calcula False Positive Rate (FPR)"""
    _, TP, TN, FP, FN = calcular_matriz_confusion(y_real, y_pred)
    
    if FP + TN == 0:
        return 0.0
    
    fpr = FP / (FP + TN)
    return fpr

def calcular_curva_roc(y_real, probabilidades):
    """
    Calcula curva ROC para diferentes umbrales
    """
    umbrales = np.sort(np.unique(probabilidades))
    
    tpr_list = []
    fpr_list = []
    
    for umbral in umbrales:
        y_pred = (probabilidades >= umbral).astype(int)
        
        tpr = calcular_tpr(y_real, y_pred)
        fpr = calcular_fpr(y_real, y_pred)
        
        tpr_list.append(tpr)
        fpr_list.append(fpr)
    
    return np.array(fpr_list), np.array(tpr_list), umbrales

# Calcular curva ROC
fpr_curva, tpr_curva, umbrales_roc = calcular_curva_roc(
    y_test, probabilidades[:, 1]
)

# También usar sklearn
fpr_sklearn, tpr_sklearn, _ = roc_curve(y_test, probabilidades[:, 1])

# Calcular AUC
roc_auc = auc(fpr_sklearn, tpr_sklearn)

print(f"AUC (Area Under Curve): {roc_auc:.4f}")
AUC (Area Under Curve): 0.9440

11.3 Visualizar curva ROC

python
# Crear gráfico
fig, ax = plt.subplots(figsize=(10, 8))

# Curva ROC
ax.plot(fpr_sklearn, tpr_sklearn, 'b-', linewidth=2,
        label=f'Curva ROC (AUC = {roc_auc:.3f})')

# Línea diagonal (clasificador aleatorio)
ax.plot([0, 1], [0, 1], 'r--', linewidth=2, label='Clasificador aleatorio (AUC = 0.5)')

# Punto del modelo actual
fpr_actual = calcular_fpr(y_test, y_pred)
tpr_actual = calcular_tpr(y_test, y_pred)
ax.plot(fpr_actual, tpr_actual, 'go', markersize=12,
        label=f'Umbral 0.5 (FPR={fpr_actual:.2f}, TPR={tpr_actual:.2f})')

ax.set_xlabel('False Positive Rate (FPR)', fontsize=12)
ax.set_ylabel('True Positive Rate (TPR)', fontsize=12)
ax.set_title('Curva ROC', fontsize=14, fontweight='bold')
ax.legend(loc='lower right')
ax.grid(True, alpha=0.3)
ax.set_xlim([0, 1])
ax.set_ylim([0, 1])

# Área sombreada bajo la curva
ax.fill_between(fpr_sklearn, tpr_sklearn, alpha=0.2, label='AUC')

plt.tight_layout()
plt.savefig('curva_roc.png', dpi=300, bbox_inches='tight')
print("Curva ROC guardada como 'curva_roc.png'")
Curva ROC guardada como 'curva_roc.png'

11.4 Interpretación del AUC

python
def interpretar_auc(auc_score):
    """Interpreta el valor de AUC"""
    if auc_score >= 0.9:
        return "Excelente"
    elif auc_score >= 0.8:
        return "Muy bueno"
    elif auc_score >= 0.7:
        return "Bueno"
    elif auc_score >= 0.6:
        return "Regular"
    else:
        return "Pobre"

interpretacion = interpretar_auc(roc_auc)

print(f"\n\nINTERPRETACIÓN DEL MODELO")
print("=" * 70)
print(f"AUC: {roc_auc:.4f} - {interpretacion}")
print(f"\nSignificado:")
print(f"Si tomamos un caso positivo y uno negativo al azar,")
print(f"el modelo asignará una probabilidad mayor al caso positivo")
print(f"el {roc_auc*100:.1f}% de las veces.")


INTERPRETACIÓN DEL MODELO
======================================================================
AUC: 0.9440 - Excelente

Significado:
Si tomamos un caso positivo y uno negativo al azar,
el modelo asignará una probabilidad mayor al caso positivo
el 94.4% de las veces.

12 Aplicaciones económicas

12.1 Aplicación 1: Predicción de incumplimiento crediticio

python
print("\n\nAPLICACIÓN: PREDICCIÓN DE INCUMPLIMIENTO CREDITICIO")
print("=" * 70)

# Generar datos simulados
np.random.seed(42)
n_clientes = 1000

datos_credito = pd.DataFrame({
    'edad': np.random.randint(18, 70, n_clientes),
    'ingreso_mensual': np.random.uniform(1000, 10000, n_clientes),
    'monto_credito': np.random.uniform(5000, 100000, n_clientes),
    'historial_pagos': np.random.randint(1, 10, n_clientes),
    'num_creditos_activos': np.random.randint(0, 5, n_clientes),
    'ratio_deuda_ingreso': np.random.uniform(0.1, 0.8, n_clientes)
})

# Variable objetivo: incumplimiento (1 = sí, 0 = no)
score_riesgo = (
    -datos_credito['ingreso_mensual'] * 0.0001 +
    datos_credito['monto_credito'] * 0.00001 +
    datos_credito['ratio_deuda_ingreso'] * 2 +
    -datos_credito['historial_pagos'] * 0.15 +
    datos_credito['num_creditos_activos'] * 0.2 +
    np.random.normal(0, 0.3, n_clientes)
)

datos_credito['incumplimiento'] = (score_riesgo > 0.5).astype(int)

print(f"\nDataset de créditos:")
print(f"  Total clientes: {n_clientes}")
print(f"  Incumplimientos: {datos_credito['incumplimiento'].sum()} ({datos_credito['incumplimiento'].mean()*100:.1f}%)")

# Dividir datos
X_credito = datos_credito.drop('incumplimiento', axis=1)
y_credito = datos_credito['incumplimiento']

X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(
    X_credito, y_credito, test_size=0.3, random_state=42
)

# Entrenar modelo
modelo_credito = LogisticRegression(random_state=42, max_iter=1000)
modelo_credito.fit(X_train_c, y_train_c)

# Predecir
y_pred_c = modelo_credito.predict(X_test_c)
prob_incumplimiento = modelo_credito.predict_proba(X_test_c)

# Calcular métricas
cm_credito = confusion_matrix(y_test_c, y_pred_c)
_, TP_c, TN_c, FP_c, FN_c = calcular_matriz_confusion(y_test_c, y_pred_c)

accuracy_c = calcular_accuracy(y_test_c, y_pred_c)
precision_c = calcular_precision(y_test_c, y_pred_c)
recall_c = calcular_recall(y_test_c, y_pred_c)
f1_c = calcular_f1(y_test_c, y_pred_c)

# Curva ROC
fpr_c, tpr_c, _ = roc_curve(y_test_c, prob_incumplimiento[:, 1])
auc_c = auc(fpr_c, tpr_c)

print(f"\n\nRESULTADOS DEL MODELO")
print("=" * 70)
print(f"\nMatriz de Confusión:")
print(f"                    Predicción")
print(f"              Pagará  Incumplirá")
print(f"Real  Pagará    {TN_c:4d}      {FP_c:4d}")
print(f"   Incumplirá   {FN_c:4d}      {TP_c:4d}")

print(f"\n\nMétricas:")
print(f"  Accuracy:  {accuracy_c:.4f} ({accuracy_c*100:.1f}%)")
print(f"  Precision: {precision_c:.4f} ({precision_c*100:.1f}%)")
print(f"  Recall:    {recall_c:.4f} ({recall_c*100:.1f}%)")
print(f"  F1-Score:  {f1_c:.4f}")
print(f"  AUC:       {auc_c:.4f}")

# Análisis de costos
monto_promedio = datos_credito['monto_credito'].mean()
tasa_recuperacion = 0.30  # Se recupera 30% en promedio

costo_fn = FN_c * monto_promedio * (1 - tasa_recuperacion)  # No detectar incumplimiento
costo_fp = FP_c * 500  # Costo de revisar caso que no es incumplimiento

print(f"\n\nANÁLISIS DE COSTOS")
print("=" * 70)
print(f"  Monto promedio por crédito: S/ {monto_promedio:,.2f}")
print(f"\n  Falsos Negativos (FN): {FN_c}")
print(f"    → Incumplimientos no detectados")
print(f"    → Pérdida estimada: S/ {costo_fn:,.2f}")
print(f"\n  Falsos Positivos (FP): {FP_c}")
print(f"    → Clientes buenos rechazados o revisados")
print(f"    → Costo de revisión: S/ {costo_fp:,.2f}")
print(f"\n  Costo total: S/ {costo_fn + costo_fp:,.2f}")

print(f"\n\nRECOMENDACIÓN:")
if recall_c < 0.7:
    print("⚠ RECALL BAJO: Estamos perdiendo muchos incumplimientos.")
    print("  Considerar reducir umbral para detectar más casos de riesgo.")
if precision_c < 0.5:
    print("⚠ PRECISION BAJA: Muchas falsas alarmas.")
    print("  Considerar aumentar umbral o mejorar features.")


APLICACIÓN: PREDICCIÓN DE INCUMPLIMIENTO CREDITICIO
======================================================================

Dataset de créditos:
  Total clientes: 1000
  Incumplimientos: 500 (50.0%)


RESULTADOS DEL MODELO
======================================================================

Matriz de Confusión:
                    Predicción
              Pagará  Incumplirá
Real  Pagará     124        19
   Incumplirá     22       135


Métricas:
  Accuracy:  0.8633 (86.3%)
  Precision: 0.8766 (87.7%)
  Recall:    0.8599 (86.0%)
  F1-Score:  0.8682
  AUC:       0.9521


ANÁLISIS DE COSTOS
======================================================================
  Monto promedio por crédito: S/ 52,452.51

  Falsos Negativos (FN): 22
    → Incumplimientos no detectados
    → Pérdida estimada: S/ 807,768.67

  Falsos Positivos (FP): 19
    → Clientes buenos rechazados o revisados
    → Costo de revisión: S/ 9,500.00

  Costo total: S/ 817,268.67


RECOMENDACIÓN:

12.2 Aplicación 2: Clasificación de empresas por riesgo de quiebra

python
print("\n\n\nAPLICACIÓN: CLASIFICACIÓN DE RIESGO DE QUIEBRA EMPRESARIAL")
print("=" * 70)

# Generar datos de empresas
np.random.seed(42)
n_empresas = 500

datos_empresas = pd.DataFrame({
    'activos_totales': np.random.uniform(100000, 10000000, n_empresas),
    'pasivos_totales': np.random.uniform(50000, 8000000, n_empresas),
    'ventas_anuales': np.random.uniform(200000, 15000000, n_empresas),
    'utilidad_neta': np.random.uniform(-500000, 2000000, n_empresas),
    'flujo_caja': np.random.uniform(-200000, 1000000, n_empresas),
    'años_operacion': np.random.randint(1, 30, n_empresas)
})

# Calcular ratios financieros
datos_empresas['ratio_liquidez'] = datos_empresas['activos_totales'] / datos_empresas['pasivos_totales']
datos_empresas['margen_utilidad'] = datos_empresas['utilidad_neta'] / datos_empresas['ventas_anuales']
datos_empresas['ratio_endeudamiento'] = datos_empresas['pasivos_totales'] / datos_empresas['activos_totales']

# Variable objetivo: quiebra (1 = sí, 0 = no)
score_quiebra = (
    -datos_empresas['ratio_liquidez'] * 0.5 +
    -datos_empresas['margen_utilidad'] * 2 +
    datos_empresas['ratio_endeudamiento'] * 3 +
    -datos_empresas['flujo_caja'] * 0.000001 +
    -datos_empresas['años_operacion'] * 0.05 +
    np.random.normal(0, 0.5, n_empresas)
)

datos_empresas['quiebra'] = (score_quiebra > 1.0).astype(int)

print(f"\nDataset de empresas:")
print(f"  Total empresas: {n_empresas}")
print(f"  Quiebras: {datos_empresas['quiebra'].sum()} ({datos_empresas['quiebra'].mean()*100:.1f}%)")

# Seleccionar features
features_empresas = ['ratio_liquidez', 'margen_utilidad', 'ratio_endeudamiento',
                     'flujo_caja', 'años_operacion']

X_empresas = datos_empresas[features_empresas]
y_empresas = datos_empresas['quiebra']

# Dividir datos
X_train_e, X_test_e, y_train_e, y_test_e = train_test_split(
    X_empresas, y_empresas, test_size=0.3, random_state=42
)

# Entrenar modelo
modelo_quiebra = LogisticRegression(random_state=42, max_iter=1000)
modelo_quiebra.fit(X_train_e, y_train_e)

# Evaluar
y_pred_e = modelo_quiebra.predict(X_test_e)
prob_quiebra = modelo_quiebra.predict_proba(X_test_e)

# Métricas
accuracy_e = calcular_accuracy(y_test_e, y_pred_e)
precision_e = calcular_precision(y_test_e, y_pred_e)
recall_e = calcular_recall(y_test_e, y_pred_e)
f1_e = calcular_f1(y_test_e, y_pred_e)

fpr_e, tpr_e, _ = roc_curve(y_test_e, prob_quiebra[:, 1])
auc_e = auc(fpr_e, tpr_e)

print(f"\n\nMÉTRICAS DEL MODELO")
print("=" * 70)
print(f"  Accuracy:  {accuracy_e:.4f}")
print(f"  Precision: {precision_e:.4f}")
print(f"  Recall:    {recall_e:.4f}")
print(f"  F1-Score:  {f1_e:.4f}")
print(f"  AUC:       {auc_e:.4f}")

# Importancia de variables
print(f"\n\nIMPORTANCIA DE VARIABLES (Coeficientes)")
print("=" * 70)
coeficientes = pd.DataFrame({
    'Variable': features_empresas,
    'Coeficiente': modelo_quiebra.coef_[0]
}).sort_values('Coeficiente', key=abs, ascending=False)

for _, row in coeficientes.iterrows():
    signo = "↑ Mayor riesgo" if row['Coeficiente'] > 0 else "↓ Menor riesgo"
    print(f"  {row['Variable']:<25} {row['Coeficiente']:>8.4f}  {signo}")

print(f"\n\nAPLICACIÓN PRÁCTICA:")
print("Este modelo puede usarse para:")
print("  1. Sistema de alerta temprana de quiebra")
print("  2. Decisiones de inversión o préstamos corporativos")
print("  3. Priorización de auditorías o intervenciones")
print("  4. Análisis de cartera de inversiones")



APLICACIÓN: CLASIFICACIÓN DE RIESGO DE QUIEBRA EMPRESARIAL
======================================================================

Dataset de empresas:
  Total empresas: 500
  Quiebras: 197 (39.4%)


MÉTRICAS DEL MODELO
======================================================================
  Accuracy:  0.9333
  Precision: 0.9048
  Recall:    0.9344
  F1-Score:  0.9194
  AUC:       0.9854


IMPORTANCIA DE VARIABLES (Coeficientes)
======================================================================
  ratio_liquidez             -3.2044  ↓ Menor riesgo
  ratio_endeudamiento         2.6668  ↑ Mayor riesgo
  margen_utilidad            -2.3090  ↓ Menor riesgo
  años_operacion             -0.0526  ↓ Menor riesgo
  flujo_caja                 -0.0000  ↓ Menor riesgo


APLICACIÓN PRÁCTICA:
Este modelo puede usarse para:
  1. Sistema de alerta temprana de quiebra
  2. Decisiones de inversión o préstamos corporativos
  3. Priorización de auditorías o intervenciones
  4. Análisis de cartera de inversiones

13 Ejercicios prácticos

13.1 Ejercicio 1: Optimizar umbral según costo

python
print("\n\n\nEJERCICIO: OPTIMIZACIÓN DE UMBRAL SEGÚN COSTOS")
print("=" * 70)

def encontrar_umbral_optimo(y_real, probabilidades, costo_fp, costo_fn):
    """
    Encuentra el umbral óptimo que minimiza costos totales
    """
    umbrales = np.linspace(0.1, 0.9, 50)
    
    mejores_resultados = {
        'umbral': None,
        'costo_total': float('inf'),
        'precision': 0,
        'recall': 0,
        'fp': 0,
        'fn': 0
    }
    
    resultados = []
    
    for umbral in umbrales:
        y_pred = (probabilidades >= umbral).astype(int)
        
        _, TP, TN, FP, FN = calcular_matriz_confusion(y_real, y_pred)
        
        costo_total = FP * costo_fp + FN * costo_fn
        
        prec = calcular_precision(y_real, y_pred)
        rec = calcular_recall(y_real, y_pred)
        
        resultados.append({
            'umbral': umbral,
            'costo_total': costo_total,
            'precision': prec,
            'recall': rec,
            'fp': FP,
            'fn': FN
        })
        
        if costo_total < mejores_resultados['costo_total']:
            mejores_resultados = {
                'umbral': umbral,
                'costo_total': costo_total,
                'precision': prec,
                'recall': rec,
                'fp': FP,
                'fn': FN
            }
    
    return mejores_resultados, pd.DataFrame(resultados)

# Aplicar al modelo de crédito
costo_por_fp = 500  # Costo de revisar un caso falso positivo
costo_por_fn = monto_promedio * 0.7  # 70% del monto no recuperado

resultado_optimo, df_resultados = encontrar_umbral_optimo(
    y_test_c, 
    prob_incumplimiento[:, 1],
    costo_por_fp,
    costo_por_fn
)

print(f"\nCostos considerados:")
print(f"  Costo por Falso Positivo: S/ {costo_por_fp:,.2f}")
print(f"  Costo por Falso Negativo: S/ {costo_por_fn:,.2f}")

print(f"\n\nUMBRAL ÓPTIMO ENCONTRADO:")
print("=" * 70)
print(f"  Umbral óptimo: {resultado_optimo['umbral']:.3f}")
print(f"  Costo total: S/ {resultado_optimo['costo_total']:,.2f}")
print(f"  Precision: {resultado_optimo['precision']:.4f}")
print(f"  Recall: {resultado_optimo['recall']:.4f}")
print(f"  Falsos Positivos: {resultado_optimo['fp']}")
print(f"  Falsos Negativos: {resultado_optimo['fn']}")

# Comparar con umbral 0.5
_, TP_05, TN_05, FP_05, FN_05 = calcular_matriz_confusion(y_test_c, y_pred_c)
costo_umbral_05 = FP_05 * costo_por_fp + FN_05 * costo_por_fn

print(f"\n\nCOMPARACIÓN CON UMBRAL 0.5:")
print(f"  Costo con umbral 0.5: S/ {costo_umbral_05:,.2f}")
print(f"  Ahorro con umbral óptimo: S/ {costo_umbral_05 - resultado_optimo['costo_total']:,.2f}")

# Graficar
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))

# Gráfico de costos
ax1.plot(df_resultados['umbral'], df_resultados['costo_total'], 'b-', linewidth=2)
ax1.axvline(x=resultado_optimo['umbral'], color='r', linestyle='--', 
            label=f"Umbral óptimo ({resultado_optimo['umbral']:.3f})")
ax1.axvline(x=0.5, color='g', linestyle='--', alpha=0.5, label='Umbral 0.5')
ax1.set_xlabel('Umbral')
ax1.set_ylabel('Costo Total (S/)')
ax1.set_title('Costo Total vs Umbral')
ax1.legend()
ax1.grid(True, alpha=0.3)

# Gráfico de Precision vs Recall
ax2.plot(df_resultados['umbral'], df_resultados['precision'], 'b-', 
         linewidth=2, label='Precision')
ax2.plot(df_resultados['umbral'], df_resultados['recall'], 'r-', 
         linewidth=2, label='Recall')
ax2.axvline(x=resultado_optimo['umbral'], color='g', linestyle='--', alpha=0.5,
            label='Umbral óptimo')
ax2.set_xlabel('Umbral')
ax2.set_ylabel('Métrica')
ax2.set_title('Precision y Recall vs Umbral')
ax2.legend()
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('optimizacion_umbral.png', dpi=300, bbox_inches='tight')
print("\nGráfico guardado como 'optimizacion_umbral.png'")



EJERCICIO: OPTIMIZACIÓN DE UMBRAL SEGÚN COSTOS
======================================================================

Costos considerados:
  Costo por Falso Positivo: S/ 500.00
  Costo por Falso Negativo: S/ 36,716.76


UMBRAL ÓPTIMO ENCONTRADO:
======================================================================
  Umbral óptimo: 0.165
  Costo total: S/ 105,433.52
  Precision: 0.7078
  Recall: 0.9873
  Falsos Positivos: 64
  Falsos Negativos: 2


COMPARACIÓN CON UMBRAL 0.5:
  Costo con umbral 0.5: S/ 817,268.67
  Ahorro con umbral óptimo: S/ 711,835.15

Gráfico guardado como 'optimizacion_umbral.png'

14 Conclusión

En esta guía hemos explorado predicción y métricas de performance:

El problema de predicción

Predecir eventos futuros basándose en datos históricos para tomar mejores decisiones.

Matriz de confusión

Herramienta fundamental que muestra TP, TN, FP, FN para evaluar clasificadores.

Métricas de clasificación

  • Accuracy: Proporción de predicciones correctas
  • Precision: De las predicciones positivas, cuántas son correctas
  • Recall: De los casos positivos reales, cuántos detectamos
  • F1-Score: Balance entre precision y recall

Curvas de evaluación

  • Curva Precision-Recall: Para evaluar trade-off entre precision y recall
  • Curva ROC: Para evaluar capacidad discriminativa del modelo
  • AUC: Métrica resumen de la curva ROC

15 Próximos pasos

En la siguiente guía (Guía 9: Métodos de ML para Clasificación) exploraremos:

  • Regresión logística
  • K-Nearest Neighbors (KNN)
  • Árboles de decisión
  • Random Forests
  • Clasificación multi-clase

Estos algoritmos nos permitirán construir modelos predictivos más sofisticados para problemas económicos complejos.

16 Recursos adicionales

Para profundizar en métricas de clasificación:

  • Scikit-learn Metrics: scikit-learn.org/stable/modules/model_evaluation.html
  • Understanding ROC Curves: developers.google.com/machine-learning/crash-course/classification/roc-and-auc
  • Precision-Recall trade-off: machinelearningmastery.com
  • Aplicaciones en economía: papers en credit scoring, fraud detection

Cómo citar

Achalma, E. (2021, 29 de noviembre). Métricas de performance en ML Python. Numerus Scriptum. https://numerus-scriptum.netlify.app/cursos/python/prediccion-y-metrica-de-performance-con-python/

Esc
elegir abrir Esc cerrar