Regresión logística spam

"""
Ejemplo didáctico de Regresión Logística
========================================

Objetivo:
Predecir si un correo es SPAM (1) o NO SPAM (0).

Este ejemplo está pensado para enseñar:

1. Qué es un dataset.
2. Por qué se divide en entrenamiento (train) y prueba (test).
3. Cómo aprende un modelo de regresión logística.
4. Qué significa predecir.
5. Cómo interpretar las métricas.

Dataset:
- 30 correos ficticios.
- Variables:
    * num_palabras_promocionales
    * num_enlaces
    * porcentaje_mayusculas
    * spam (objetivo)

La variable spam vale:
    1 = spam
    0 = no spam
"""

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (
    accuracy_score,
    confusion_matrix,
    classification_report
)

# ------------------------------------------------------------------
# 1. DATASET
# ------------------------------------------------------------------

datos = [
    [0,0,5,0],
    [1,0,4,0],
    [0,1,8,0],
    [2,0,6,0],
    [1,1,7,0],
    [0,0,3,0],
    [2,1,10,0],
    [1,0,12,0],
    [0,1,6,0],
    [2,0,8,0],
    [5,3,45,1],
    [6,4,55,1],
    [4,2,40,1],
    [7,5,60,1],
    [5,4,50,1],
    [6,3,48,1],
    [8,5,70,1],
    [4,3,42,1],
    [7,4,65,1],
    [5,2,46,1],
    [1,1,15,0],
    [2,1,18,0],
    [3,1,22,0],
    [3,2,25,0],
    [4,1,28,0],
    [4,2,30,1],
    [3,3,35,1],
    [2,2,20,0],
    [5,1,32,1],
    [1,2,18,0]
]

df = pd.DataFrame(
    datos,
    columns=[
        "num_palabras_promocionales",
        "num_enlaces",
        "porcentaje_mayusculas",
        "spam"
    ]
)

print("="*70)
print("DATASET COMPLETO")
print("="*70)
print(df)

# ------------------------------------------------------------------
# 2. SEPARAR VARIABLES DE ENTRADA Y SALIDA
# ------------------------------------------------------------------

X = df[
    [
        "num_palabras_promocionales",
        "num_enlaces",
        "porcentaje_mayusculas"
    ]
]

y = df["spam"]

# ------------------------------------------------------------------
# 3. DIVISIÓN TRAIN / TEST
# ------------------------------------------------------------------

"""
¿Por qué dividimos los datos?

Imagina que un profesor enseña las respuestas exactas de un examen
a un alumno y luego le pone exactamente el mismo examen.

Sacar un 10 no demostraría que ha aprendido.

Con Machine Learning ocurre lo mismo.

TRAIN:
    Datos que el modelo utiliza para aprender.

TEST:
    Datos que el modelo NO ha visto nunca.

Si funciona bien en TEST, tenemos más confianza en que
generaliza correctamente.
"""

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.30,
    random_state=42
)

print("\n")
print("="*70)
print("DIVISIÓN TRAIN / TEST")
print("="*70)
print(f"Filas para entrenamiento: {len(X_train)}")
print(f"Filas para prueba:        {len(X_test)}")

print("Estos datos los hemos reservado para test")
print(X_test)
print("Y estas son los datos reales")
print(y_test)
# ------------------------------------------------------------------
# 4. ENTRENAMIENTO
# ------------------------------------------------------------------

modelo = LogisticRegression()
modelo.fit(X_train, y_train)

print("\nModelo entrenado.")

# ------------------------------------------------------------------
# 5. PREDICCIÓN
# ------------------------------------------------------------------

"""
¿Qué significa predecir?

El modelo observa las características de un correo que nunca
ha visto y estima:

P(spam)

Es decir, la probabilidad de que sea spam.

Después convierte esa probabilidad en una clase:
    >= 0.5  -> spam
    <  0.5  -> no spam
"""

probabilidades = modelo.predict_proba(X_test)

predicciones = modelo.predict(X_test)

print("\n")
print("="*70)
print("PREDICCIONES")
print("="*70)

for i in range(len(X_test)):
    prob_no_spam = probabilidades[i][0]
    prob_spam = probabilidades[i][1]

    print(
        f"Correo {i+1:2d} | "
        f"P(No Spam)={prob_no_spam:.3f} | "
        f"P(Spam)={prob_spam:.3f} | "
        f"Predicción={predicciones[i]} | "
        f"Real={list(y_test)[i]}"
    )

print("Los datos reales vs. predichos:")
print(y_test.tolist())
print(predicciones)
# ------------------------------------------------------------------
# 6. MÉTRICAS
# ------------------------------------------------------------------

accuracy = accuracy_score(y_test, predicciones)

print("\n")
print("="*70)
print("ACCURACY")
print("="*70)
print(f"Accuracy = {accuracy:.3f}")

"""
Accuracy:

(Número de aciertos) / (Número total de casos)

Ejemplo:
Si el modelo acierta 8 de 10 correos:

Accuracy = 8/10 = 0.80 = 80%
"""

print("\n")
print("="*70)
print("MATRIZ DE CONFUSIÓN")
print("="*70)

cm = confusion_matrix(y_test, predicciones)
print(cm)

"""
Matriz de confusión:

                Predijo No Spam   Predijo Spam

Real No Spam         VN              FP
Real Spam            FN              VP

VN = Verdadero Negativo
FP = Falso Positivo
FN = Falso Negativo
VP = Verdadero Positivo

FP:
    Correo normal marcado como spam.

FN:
    Correo spam que el modelo dejó pasar.
"""

print("\n")
print("="*70)
print("CLASSIFICATION REPORT")
print("="*70)

print(classification_report(y_test, predicciones))

"""
Precision:
    De todos los correos marcados como spam,
    ¿cuántos eran realmente spam?

Recall:
    De todos los spam reales,
    ¿cuántos encontró el modelo?

F1:
    Media armónica entre Precision y Recall.

Support:
    Número de ejemplos de cada clase.
"""

# ------------------------------------------------------------------
# 7. EJEMPLO NUEVO
# ------------------------------------------------------------------

nuevo_correo = pd.DataFrame(
    [[6, 4, 58]],
    columns=[
        "num_palabras_promocionales",
        "num_enlaces",
        "porcentaje_mayusculas"
    ]
)

prob = modelo.predict_proba(nuevo_correo)[0][1]

print("\n")
print("="*70)
print("EJEMPLO DE CORREO NUEVO")
print("="*70)
print(nuevo_correo)
print(f"Probabilidad de spam: {prob:.3f}")
print(f"Clasificación final: {modelo.predict(nuevo_correo)[0]}")

"""
Conclusión:

Entrenamiento:
    El modelo aprende patrones.

Prueba:
    Verificamos si esos patrones funcionan en datos nuevos.

Predicción:
    Aplicamos lo aprendido a correos nunca vistos.

Métricas:
    Cuantifican si el modelo está funcionando bien.
"""

Publicado por

Juan Pablo Fuentes

Formador de programación y bases de datos