Ejemplo redes

# ==============================================================================
# CONVERSIÓN FAHRENHEIT → CELSIUS CON REDES NEURONALES
#
# FÓRMULA REAL:   C = (F - 32) × 5/9   ≈   C = F × 0.5556 - 17.778
#
# OBJETIVO: que la red aprenda sola esa fórmula, SIN que nosotros se la digamos.
# Solo le damos pares de datos (F, C) y dejamos que ajuste sus pesos.
#
# MODELOS:
#   Modelo 1 — UNA sola neurona           (1 peso + 1 bias = 2 parámetros)
#   Modelo 2 — Dos capas de 4 neuronas    (más potente, más parámetros)
#
# INSTALACIÓN (una sola vez):
#   pip install tensorflow numpy matplotlib
# ==============================================================================


# ==============================================================================
# PASO 1: Importar librerías
# ==============================================================================

import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

tf.random.set_seed(42)
np.random.seed(42)

print("=" * 60)
print("  REDES NEURONALES: Fahrenheit → Celsius")
print("=" * 60)
print(f"  TensorFlow: {tf.__version__}")


# ==============================================================================
# PASO 2: Crear el dataset — 20 pares (Fahrenheit, Celsius)
# ==============================================================================
#
# Generamos 20 temperaturas en Fahrenheit distribuidas entre -40 °F y 212 °F
# y calculamos su equivalente exacto en Celsius con la fórmula real.
#
# -40 °F es un punto especial: coincide exactamente con -40 °C.
# 212 °F = punto de ebullición del agua = 100 °C.

fahrenheit = np.array([
    -40,  -22,   -4,   14,   32,   50,   68,   77,   86,   95,
    104,  113,  122,  140,  158,  176,  185,  194,  203,  212
], dtype=float)

# Fórmula exacta: C = (F - 32) × 5 / 9
celsius = (fahrenheit - 32) * 5 / 9

print("\n" + "=" * 60)
print("PASO 2: Dataset — 20 pares Fahrenheit / Celsius")
print("=" * 60)
print(f"{'Fahrenheit':>12} │ {'Celsius':>10} │ {'Nota'}")
print("-" * 45)
notas = {-40: "F=C", 32: "Congela", 98.6: "Cuerpo", 212: "Hierve"}
for f, c in zip(fahrenheit, celsius):
    nota = notas.get(f, "")
    print(f"{f:>12.1f} │ {c:>10.4f} │ {nota}")


# ==============================================================================
# PASO 3: Normalizar los datos
# ==============================================================================
#
# Las redes neuronales aprenden mejor cuando los datos están en rangos pequeños
# (cerca de 0). Guardamos la media y desviación para desnormalizar al final.

f_media  = fahrenheit.mean()
f_std    = fahrenheit.std()
c_media  = celsius.mean()
c_std    = celsius.std()

F_norm = (fahrenheit - f_media) / f_std    # Fahrenheit normalizado
C_norm = (celsius    - c_media) / c_std    # Celsius    normalizado

print("\n" + "=" * 60)
print("PASO 3: Normalización (media=0, desv=1)")
print("=" * 60)
print(f"  Fahrenheit → media: {f_media:.2f}, desv: {f_std:.2f}")
print(f"  Celsius    → media: {c_media:.2f}, desv: {c_std:.2f}")


# ==============================================================================
# PASO 4A: MODELO 1 — Una sola neurona
# ==============================================================================
#
# Arquitectura:   [1 entrada F]  →  [1 neurona]  →  [1 salida C]
#
# Esta neurona tiene exactamente DOS parámetros:
#   w (peso / weight): multiplica a F
#   b (bias / sesgo):  suma un valor fijo
#
# La neurona calcula:   salida = w × F + b
#
# ¿Se puede aprender C = F × 0.5556 - 17.778 con una sola neurona?
# SÍ, porque la conversión es una función LINEAL (línea recta).
# Una neurona sin activación no lineal es exactamente eso: una línea.
#
# Sin activation= → la neurona es lineal (el valor por defecto es 'linear')

modelo1 = keras.Sequential([
    layers.Dense(units=1, input_shape=(1,))   # 1 neurona, sin activación no lineal
], name="Una_Neurona")

modelo1.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.05),
    loss="mean_squared_error"   # MSE: error cuadrático medio entre C_pred y C_real
)

print("\n" + "=" * 60)
print("PASO 4A: Modelo 1 — Una sola neurona")
print("=" * 60)
modelo1.summary()

hist1 = modelo1.fit(
    F_norm, C_norm,
    epochs=500,
    verbose=0          # silencioso para no llenar la pantalla
)

loss_final_m1 = hist1.history["loss"][-1]
print(f"\n  ✅ Entrenamiento completado — Loss final: {loss_final_m1:.8f}")


# ==============================================================================
# PASO 4B: MODELO 2 — Dos capas de 4 neuronas
# ==============================================================================
#
# Arquitectura:
#   [1 entrada]  →  [4 neuronas ReLU]  →  [4 neuronas ReLU]  →  [1 salida]
#
# Parámetros:
#   Capa oculta 1: (1 entrada × 4 neuronas) + 4 bias = 8
#   Capa oculta 2: (4 entradas × 4 neuronas) + 4 bias = 20
#   Capa salida:   (4 entradas × 1 neurona)  + 1 bias = 5
#   TOTAL: 33 parámetros
#
# Para un problema LINEAL como este, esta arquitectura es "excesiva",
# pero sirve perfectamente para comparar comportamiento y mostrar los pesos.
#
# ReLU: si el valor es negativo → 0; si es positivo → lo deja pasar.
# La combinación de varias ReLU puede aproximar cualquier función.

modelo2 = keras.Sequential([
    layers.Dense(units=4, activation="relu", input_shape=(1,)),  # capa oculta 1
    layers.Dense(units=4, activation="relu"),                     # capa oculta 2
    layers.Dense(units=1)                                         # capa de salida (lineal)
], name="Dos_Capas_4_Neuronas")

modelo2.compile(
    optimizer=keras.optimizers.Adam(learning_rate=0.05),
    loss="mean_squared_error"
)

print("\n" + "=" * 60)
print("PASO 4B: Modelo 2 — Dos capas de 4 neuronas")
print("=" * 60)
modelo2.summary()

hist2 = modelo2.fit(
    F_norm, C_norm,
    epochs=500,
    verbose=0
)

loss_final_m2 = hist2.history["loss"][-1]
print(f"\n  ✅ Entrenamiento completado — Loss final: {loss_final_m2:.8f}")


# ==============================================================================
# PASO 5: Evaluar el rendimiento de ambos modelos
# ==============================================================================

def evaluar_modelo(modelo, F_norm, celsius_real, f_media, f_std, c_media, c_std):
    """
    Evalúa un modelo comparando sus predicciones con los valores reales.
    Desnormaliza tanto la predicción como el valor real para mostrar °C verdaderos.
    """
    C_pred_norm = modelo.predict(F_norm, verbose=0).flatten()

    # Desnormalizamos para obtener grados Celsius reales
    C_pred_real = C_pred_norm * c_std + c_media

    errores_abs = np.abs(C_pred_real - celsius_real)
    mae  = errores_abs.mean()     # Error Absoluto Medio (MAE) en °C
    rmse = np.sqrt(((C_pred_real - celsius_real) ** 2).mean())  # Raíz del Error Cuadrático Medio

    return C_pred_real, mae, rmse

C_pred1, mae1, rmse1 = evaluar_modelo(modelo1, F_norm, celsius, f_media, f_std, c_media, c_std)
C_pred2, mae2, rmse2 = evaluar_modelo(modelo2, F_norm, celsius, f_media, f_std, c_media, c_std)

print("\n" + "=" * 60)
print("PASO 5: Rendimiento comparado")
print("=" * 60)
print(f"\n  {'Modelo':<28} {'MAE (°C)':>10} {'RMSE (°C)':>12}")
print("  " + "-" * 52)
print(f"  {'Modelo 1: Una neurona':<28} {mae1:>10.6f} {rmse1:>12.6f}")
print(f"  {'Modelo 2: Dos capas × 4':<28} {mae2:>10.6f} {rmse2:>12.6f}")
print("\n  MAE  = error promedio en grados Celsius")
print("  RMSE = error promedio (penaliza más los errores grandes)")
print("\n  Un MAE < 0.5 °C es excelente para uso práctico.")

# Tabla predicciones vs real
print("\n  Tabla de predicciones (primeras 10 muestras):")
print(f"  {'F (°F)':>8} │ {'C real':>8} │ {'M1 pred':>8} │ {'M1 err':>8} │ "
      f"{'M2 pred':>8} │ {'M2 err':>8}")
print("  " + "-" * 65)
for i in range(10):
    print(f"  {fahrenheit[i]:>8.1f} │ {celsius[i]:>8.4f} │ {C_pred1[i]:>8.4f} │ "
          f"{abs(C_pred1[i]-celsius[i]):>8.4f} │ {C_pred2[i]:>8.4f} │ "
          f"{abs(C_pred2[i]-celsius[i]):>8.4f}")


# ==============================================================================
# PASO 6: Mostrar los pesos de cada neurona
# ==============================================================================
#
# Los pesos son los NÚMEROS que la red aprendió ajustando sus parámetros.
# Para Modelo 1 (una neurona lineal), deberían aproximarse a:
#   w ≈ 1.0  (en escala normalizada)
#   b ≈ 0.0  (la fórmula C = F × 0.5556 - 17.78 se transforma en la escala norm.)
#
# Los pesos reales en escala original se pueden calcular así:
#   pendiente = w × (c_std / f_std)
#   intercepto = b × c_std + c_media - w × (c_std / f_std) × f_media

print("\n" + "=" * 60)
print("PASO 6: Pesos aprendidos por cada neurona")
print("=" * 60)

# ---- MODELO 1: UNA NEURONA ----
print("\n  ── MODELO 1: Una sola neurona ──")
pesos_m1, bias_m1 = modelo1.layers[0].get_weights()

# get_weights() devuelve una lista: [matriz_de_pesos, vector_de_bias]
# Para 1 neurona con 1 entrada: pesos shape=(1,1), bias shape=(1,)
w = pesos_m1[0][0]     # el único peso
b = bias_m1[0]         # el único bias

# Convertir de escala normalizada a escala real (°C / °F)
pendiente    = w * (c_std / f_std)
intercepto   = b * c_std + c_media - w * (c_std / f_std) * f_media

print(f"\n  Neurona 1:")
print(f"    Peso  (w) en escala norm.:  {w:>10.6f}")
print(f"    Bias  (b) en escala norm.:  {b:>10.6f}")
print(f"\n  Traducido a escala real °C = w×°F + b:")
print(f"    Pendiente aprendida:  {pendiente:>10.6f}  (real: 0.555556)")
print(f"    Intercepto aprendido: {intercepto:>10.6f}  (real: -17.777778)")
print(f"\n  Error de la pendiente:  {abs(pendiente - 0.555556):>10.8f} °C/°F")
print(f"  Error del intercepto:   {abs(intercepto - (-17.777778)):>10.8f} °C")

# ---- MODELO 2: DOS CAPAS DE 4 NEURONAS ----
print("\n\n  ── MODELO 2: Dos capas de 4 neuronas ──")

nombres_capas = ["Capa oculta 1 (ReLU)", "Capa oculta 2 (ReLU)", "Capa de salida (lineal)"]

for idx, (capa, nombre) in enumerate(zip(modelo2.layers, nombres_capas)):
    pesos, bias = capa.get_weights()
    # pesos shape: (n_entradas, n_neuronas)
    # bias  shape: (n_neuronas,)
    n_entradas, n_neuronas = pesos.shape

    print(f"\n  ┌─ {nombre}")
    print(f"  │  Entradas: {n_entradas}   Neuronas: {n_neuronas}")
    print(f"  │")

    for j in range(n_neuronas):
        print(f"  │  Neurona {j+1}:")
        for i in range(n_entradas):
            print(f"  │    w[entrada_{i+1}→neurona_{j+1}] = {pesos[i][j]:>10.6f}")
        print(f"  │    bias[neurona_{j+1}]           = {bias[j]:>10.6f}")
        if j < n_neuronas - 1:
            print(f"  │")
    print(f"  └{'─' * 50}")


# ==============================================================================
# PASO 7: Gráficos
# ==============================================================================

fig, axes = plt.subplots(2, 2, figsize=(15, 11))
fig.suptitle("Redes Neuronales: Aprender Fahrenheit → Celsius", fontsize=15, fontweight="bold")

VERDE   = "#27AE60"
NARANJA = "#E67E22"
AZUL    = "#2980B9"
ROJO    = "#E74C3C"

# ---- Gráfico 1: Curvas de pérdida ----
ax = axes[0, 0]
ax.plot(hist1.history["loss"], color=NARANJA, linewidth=2, label="Modelo 1: 1 neurona")
ax.plot(hist2.history["loss"], color=AZUL,    linewidth=2, label="Modelo 2: 2 capas × 4")
ax.set_title("Curva de pérdida (MSE) durante el entrenamiento")
ax.set_xlabel("Época")
ax.set_ylabel("MSE (escala normalizada)")
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_yscale("log")   # escala logarítmica para ver mejor la convergencia

# ---- Gráfico 2: Predicciones vs real ----
ax = axes[0, 1]
ax.plot(fahrenheit, celsius,  color=ROJO,    linewidth=2.5, linestyle="--",
        label="Fórmula real", zorder=5)
ax.scatter(fahrenheit, C_pred1, color=NARANJA, s=80, marker="o",
           label="Modelo 1: 1 neurona", zorder=4)
ax.scatter(fahrenheit, C_pred2, color=AZUL,    s=60, marker="^",
           label="Modelo 2: 2 capas × 4", zorder=3)
ax.set_title("Predicciones vs valores reales")
ax.set_xlabel("Temperatura (°F)")
ax.set_ylabel("Temperatura (°C)")
ax.legend()
ax.grid(True, alpha=0.3)

# ---- Gráfico 3: Error absoluto por muestra ----
ax = axes[1, 0]
x_idx = np.arange(len(fahrenheit))
ancho = 0.35
bars1 = ax.bar(x_idx - ancho/2, np.abs(C_pred1 - celsius), ancho,
               color=NARANJA, alpha=0.85, label="Modelo 1: 1 neurona")
bars2 = ax.bar(x_idx + ancho/2, np.abs(C_pred2 - celsius), ancho,
               color=AZUL, alpha=0.85, label="Modelo 2: 2 capas × 4")
ax.set_title("Error absoluto por muestra (°C)")
ax.set_xlabel("Índice de muestra")
ax.set_ylabel("|Predicción − Real|  (°C)")
ax.set_xticks(x_idx)
ax.set_xticklabels([f"{f:.0f}" for f in fahrenheit], rotation=45, ha="right", fontsize=8)
ax.legend()
ax.grid(True, alpha=0.3, axis="y")

# ---- Gráfico 4: Visualización de pesos del Modelo 1 ----
ax = axes[1, 1]
categorias  = ["Peso (w)\n(escala norm.)", "Bias (b)\n(escala norm.)",
               "Pendiente real\n(°C/°F)", "Intercepto real\n(°C)"]
valores_aprendidos = [w,          b,          pendiente,       intercepto]
valores_reales     = [None,       None,        0.555556,       -17.777778]
colores = [NARANJA, NARANJA, VERDE, VERDE]

bars = ax.bar(categorias, valores_aprendidos, color=colores, alpha=0.8, edgecolor="white")

# Líneas horizontales con los valores teóricos (solo donde aplica)
ax.axhline(0.555556,    xmin=0.49, xmax=0.74, color=ROJO, linewidth=2.5,
           linestyle="--", label="Valor teórico")
ax.axhline(-17.777778,  xmin=0.74, xmax=1.0,  color=ROJO, linewidth=2.5,
           linestyle="--")

for bar, val in zip(bars, valores_aprendidos):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.15 * np.sign(val + 0.001),
            f"{val:.4f}", ha="center", va="bottom" if val >= 0 else "top", fontsize=9, fontweight="bold")

ax.set_title("Modelo 1: Pesos aprendidos vs teóricos")
ax.set_ylabel("Valor del parámetro")
ax.axhline(0, color="black", linewidth=0.8)
ax.legend()
ax.grid(True, alpha=0.3, axis="y")

plt.tight_layout()
plt.savefig("fahrenheit_celsius_redes.png", dpi=150, bbox_inches="tight")
plt.show()


# ==============================================================================
# RESUMEN FINAL
# ==============================================================================

print("\n" + "=" * 60)
print("RESUMEN FINAL")
print("=" * 60)
print(f"\n  Fórmula real:       C = F × 0.555556 − 17.777778")
print(f"  Fórmula aprendida:  C = F × {pendiente:.6f} − {abs(intercepto):.6f}")
print(f"\n  {'Modelo':<28} {'Parámetros':>12} {'MAE (°C)':>10}")
print("  " + "-" * 52)
print(f"  {'Modelo 1: Una neurona':<28} {'2':>12} {mae1:>10.6f}")
print(f"  {'Modelo 2: Dos capas × 4':<28} {'33':>12} {mae2:>10.6f}")
print(f"\n  ✅ Código completado. Gráfico guardado: fahrenheit_celsius_redes.png")

Ejemplo K-means

# ── Importaciones ─────────────────────────────────────────────────────
import numpy as np                  # para operaciones numéricas
import matplotlib.pyplot as plt     # para hacer gráficos
from sklearn.cluster import KMeans  # el algoritmo K-Means
from sklearn.datasets import make_blobs  # generador de datos de ejemplo


# ── PASO 1: Crear datos de ejemplo ────────────────────────────────────
#
# make_blobs() genera puntos artificiales agrupados en 'manchas' (blobs).
# Es como inventarse datos ya organizados para practicar.
#
# n_samples  = cuántos puntos queremos generar en total
# centers    = cuántos grupos/manchas queremos que tenga
# cluster_std = qué tan dispersos están los puntos dentro de cada grupo
#               (número pequeño = puntos muy juntos, número grande = dispersos)
# random_state = semilla aleatoria (garantiza que siempre salga igual)
#
X, y_real = make_blobs(
    n_samples=300,    # 300 puntos en total
    centers=4,        # distribuidos en 4 grupos
    cluster_std=0.8,  # puntos bastante juntos en cada grupo
    random_state=42   # para reproducibilidad
)
#
# X tiene forma (300, 2): 300 filas (puntos) y 2 columnas (coordenadas x, y)
# y_real contiene el grupo real de cada punto (solo para comparar después)


# ── PASO 2: Crear el modelo K-Means ───────────────────────────────────
#
# Aquí solo DEFINIMOS el modelo, todavía no lo entrenamos.
# Parámetros importantes:
#
# n_clusters  = el K: cuántos grupos queremos encontrar
#               Elegimos 4 porque sabemos que los datos tienen 4 grupos.
#               En la vida real no sabremos esto y usaremos el Método del Codo.
#
# n_init      = cuántas veces reinicia el algoritmo con centroides distintos.
#               Por defecto prueba 10 inicializaciones y elige la mejor.
#               Más intentos = más fiable, pero más lento.
#
# random_state = semilla para que el resultado sea siempre el mismo.
#
kmeans = KMeans(
    n_clusters=4,
    n_init=10,
    random_state=42
)


# ── PASO 3: Entrenar el modelo ─────────────────────────────────────────
#
# .fit(X) ejecuta el algoritmo K-Means sobre nuestros datos X.
# Internamente hace todos los pasos descritos antes:
# coloca centroides → asigna puntos → recalcula centroides → repite.
#
kmeans.fit(X)


# ── PASO 4: Obtener los resultados ────────────────────────────────────
#
# Después del entrenamiento, el modelo guarda sus resultados en atributos:
#
# .labels_          → array con el número de cluster asignado a cada punto
#                     Ejemplo: [0, 2, 1, 0, 3, 1, 2, ...] (un número por punto)
#
# .cluster_centers_ → array con las coordenadas de los K centroides finales
#                     Tiene forma (K, n_características)
#
# .inertia_         → el valor WCSS final (qué tan compactos son los clusters)
#
etiquetas  = kmeans.labels_
centroides = kmeans.cluster_centers_
inercia    = kmeans.inertia_

print(f'Inercia (WCSS): {inercia:.2f}')
print(f'Centroides encontrados:')
print(centroides)


# ── PASO 5: Visualizar los resultados ─────────────────────────────────
#
# Hacemos un scatter plot (nube de puntos).
# Cada punto se colorea según el cluster al que fue asignado.
# Los centroides se marcan con una X grande en rojo.
#
plt.figure(figsize=(8, 6))

# scatter() dibuja los puntos:
# X[:, 0] = primera columna (coordenada x de todos los puntos)
# X[:, 1] = segunda columna (coordenada y de todos los puntos)
# c=etiquetas → el color de cada punto depende de su cluster
# cmap='viridis' → la paleta de colores a usar
# alpha=0.7 → transparencia (0=invisible, 1=sólido)
plt.scatter(X[:, 0], X[:, 1], c=etiquetas, cmap='viridis', s=50, alpha=0.7)

# Dibujamos los centroides con una X roja más grande:
# centroides[:, 0] = coordenadas x de todos los centroides
# centroides[:, 1] = coordenadas y de todos los centroides
# marker='X' → símbolo de X
# zorder=5   → se dibuja por encima de los puntos
plt.scatter(centroides[:, 0], centroides[:, 1],
            c='red', marker='X', s=200, zorder=5, label='Centroides')

plt.title('K-Means: Resultado del clustering')
plt.xlabel('Característica 1')
plt.ylabel('Característica 2')
plt.legend()
plt.colorbar(label='Número de cluster')
plt.tight_layout()
plt.show()

Regresión ( de nuevo)

# ==============================================================================
# 1. IMPORTACIÓN DE LIBRERÍAS
# ==============================================================================
# Pandas es la librería estrella para manejar datos en formato de tabla (como un Excel).
import pandas as pd

# De la librería 'sklearn' (scikit-learn), importamos herramientas específicas:
# 'train_test_split' nos servirá para dividir nuestros datos en dos grupos (entrenamiento y examen).
from sklearn.model_selection import train_test_split    

# 'StandardScaler' se usa para "normalizar" o "escalar" los datos (ponerlos todos en una misma escala).
from sklearn.preprocessing import StandardScaler        

# 'LogisticRegression' es el algoritmo que creará nuestro modelo de clasificación.
from sklearn.linear_model import LogisticRegression     

# Importamos las métricas para evaluar qué tan bueno es nuestro modelo al final:
from sklearn.metrics import (
    accuracy_score,        # Exactitud: Porcentaje total de aciertos sobre el total.
    precision_score,       # Precisión: De los que el modelo dijo que eran positivos, ¿cuántos lo eran de verdad?
    recall_score,          # Exhaustividad: De los positivos reales que había, ¿cuántos fue capaz de detectar?
    f1_score,              # Puntuación F1: Un equilibrio o "media" entre precisión y recall.
    confusion_matrix,      # Matriz de confusión: Una tabla que muestra dónde acertó y dónde se equivocó.
    classification_report  # Un resumen de texto que junta todas las métricas anteriores.
)

# ==============================================================================
# 2. PREPARACIÓN DE LOS DATOS INICIALES
# ==============================================================================
# Creamos dos listas de Python con nuestros datos brutos.
# 'horas_entrenamiento' es lo que queremos usar para predecir (la causa).
horas_entrenamiento = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
# 'clasificado' es el resultado (el efecto): 0 significa "No clasificado" y 1 significa "Clasificado".
clasificado = [0,0,0,0,0,0,0,1,0,0,1,1,0,1,1,1,1,1,0,1]

# Verificamos que ambas listas tengan la misma cantidad de elementos (en este caso, 20).
# Esto es vital: cada alumno/caso debe tener sus horas y su resultado correspondiente.
print("Cantidad de datos de horas:", len(horas_entrenamiento))
print("Cantidad de datos de clasificación:", len(clasificado))

# ==============================================================================
# 3. CREACIÓN DEL DATAFRAME (LA TABLA DE DATOS)
# ==============================================================================
# Juntamos las dos listas en una estructura de tabla organizada gracias a Pandas.
df = pd.DataFrame({
    "horas": horas_entrenamiento,
    "clasificado": clasificado
})
# Imprimimos la tabla en consola para ver visualmente cómo quedan las filas y columnas.
print("\n--- Nuestra tabla de datos (DataFrame) ---")
print(df)

# Separamos la tabla en dos partes fundamentales para el Machine Learning:
# X: Las variables "predictoras" o características (en mayúscula por convención matemática).
# Usamos doble corchete [[ ]] porque los modelos de sklearn siempre esperan una matriz de dos dimensiones para las X.
X = df[["horas"]]

# y: La variable "objetivo" o lo que queremos predecir (en minúscula por ser un solo vector).
y = df["clasificado"]

# ==============================================================================
# 4. DIVISIÓN EN ENTRENAMIENTO (TRAIN) Y PRUEBA (TEST)
# ==============================================================================
# Imagina que preparas a un alumno para un examen: no le das las preguntas del examen para estudiar.
# Por eso dividimos los datos: un grupo para estudiar (Train) y otro oculto para evaluar (Test).
# 'test_size=0.2' significa que dejamos el 20% de los datos para el examen (4 filas) y el 80% para estudiar (16 filas).
# 'random_state=0' es una "semilla" para que el reparto aleatorio sea siempre el mismo si volvemos a ejecutar el código.
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)

# ==============================================================================
# 5. NORMALIZACIÓN / ESCALADO DE DATOS
# ==============================================================================
# A los algoritmos les cuesta más aprender si los números son gigantescos o están en escalas muy diferentes.
# El escalador ajusta los datos para que tengan una media de 0 y una desviación de 1 (los "centra").

escalador = StandardScaler()

# ¡OJO aquí! Calculamos la escala con los datos de entrenamiento y los transformamos.
# Es fundamental guardar el resultado de vuelta en las variables 'X_train' y 'X_test'.
X_train = escalador.fit_transform(X_train)

# Con los datos de test (el examen) SOLO transformamos, no recalculamos la escala, 
# para no hacer "trampa" y mantener las reglas del grupo de entrenamiento.
X_test = escalador.transform(X_test)

# ==============================================================================
# 6. CREACIÓN Y ENTRENAMIENTO DEL MODELO
# ==============================================================================
# Llamamos al algoritmo de Regresión Logística (pese a llamarse regresión, sirve para clasificar 0s y 1s).
modelo = LogisticRegression()

# Entrenamos el modelo con la función '.fit()'. 
# Aquí es donde el modelo "estudia" la relación entre las horas (X_train) y si clasificó o no (y_train).
modelo.fit(X_train, y_train)
print("\n¡El modelo ha sido entrenado con éxito!")

# ==============================================================================
# 7. PREDICCIÓN Y EVALUACIÓN (EL EXAMEN)
# ==============================================================================
# Le pedimos al modelo que intente adivinar el resultado de las horas del grupo de examen (X_test).
# El modelo NO conoce las respuestas reales (y_test), solo ve las horas.
y_pred = modelo.predict(X_test)

# Ahora comparamos las respuestas reales (y_test) con las que el modelo predijo (y_pred).
exactitud = accuracy_score(y_test, y_pred)

# Mostramos el resultado final por pantalla. 
# Si da 1.0 significa 100% de aciertos en el examen; si da 0.5 significa un 50%, etc.
print("\n--- Evaluación del Modelo ---")
print(f"La exactitud (Accuracy) del modelo en el examen es de: {exactitud}")

Normalización test train

import pandas as pd
from sklearn.preprocessing import StandardScaler

datos = {
    'superficie':   [75, 95, 60, 120, 85, 50, 110, 70, 140, 65, 90, 100],
    'habitaciones': [3, 3, 2, 4, 3, 1, 4, 2, 5, 2, 3, 4],
    'banios':       [1, 2, 1, 2, 2, 1, 3, 1, 3, 1, 2, 2],
    'antiguedad':   [20, 10, 35, 5, 15, 40, 8, 25, 2, 30, 12, 7],
    'dist_centro':  [5.2, 3.8, 8.1, 2.1, 4.5, 9.0, 3.2, 6.7, 1.5, 7.4, 4.0, 2.8],
}
df = pd.DataFrame(datos)

print('─── ANTES ───')
print(df)
print(f'Medias:    {df.mean().round(2).to_dict()}')
print(f'Desv.tip.: {df.std().round(2).to_dict()}')

# ── Aplicar Standard Scaler ──────────────────────────────────────
scaler = StandardScaler()
X_std = scaler.fit_transform(df)
df_std = pd.DataFrame(X_std, columns=df.columns)

print('─── DESPUÉS (Standard Scaler) ───')
print(df_std.round(3))
print(f'Medias tras escalar:    {df_std.mean().round(6).to_dict()}')
print(f'Desv.tip. tras escalar: {df_std.std().round(3).to_dict()}')
# Medias ≈ 0.0  |  Desviaciones típicas ≈ 1.0

# ── Ver los parámetros aprendidos ────────────────────────────────
print('Medias aprendidas (mean_):', scaler.mean_.round(3))
print('Desv. típ. aprendidas (scale_):', scaler.scale_.round(3))

# ── Uso CORRECTO con train/test ──────────────────────────────────
from sklearn.model_selection import train_test_split
X_train, X_test = train_test_split(df, test_size=0.2, random_state=42)

scaler2 = StandardScaler()
X_train_std = scaler2.fit_transform(X_train)  # aprende media/std de TRAIN
X_test_std  = scaler2.transform(X_test)       # aplica misma media/std a TEST

Normalización

import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler

# ── Dataset ─────────────────────────────────────────────────────
datos = {
    'superficie':   [75, 95, 60, 120, 85, 50, 110, 70, 140, 65, 90, 100],
    'habitaciones': [3, 3, 2, 4, 3, 1, 4, 2, 5, 2, 3, 4],
    'banios':       [1, 2, 1, 2, 2, 1, 3, 1, 3, 1, 2, 2],
    'antiguedad':   [20, 10, 35, 5, 15, 40, 8, 25, 2, 30, 12, 7],
    'dist_centro':  [5.2, 3.8, 8.1, 2.1, 4.5, 9.0, 3.2, 6.7, 1.5, 7.4, 4.0, 2.8],
}
df = pd.DataFrame(datos)
cols = df.columns.tolist()

# ── Aplicar ambos métodos ────────────────────────────────────────
mm_scaler  = MinMaxScaler()
std_scaler = StandardScaler()

df_mm  = pd.DataFrame(mm_scaler.fit_transform(df),  columns=cols)
df_std = pd.DataFrame(std_scaler.fit_transform(df), columns=cols)

# ── Comparativa visual en consola ────────────────────────────────
separador = '─' * 65

print(separador)
print('ORIGINAL (valores sin normalizar)')
print(separador)
print(df.to_string(index=False))
print(f'  Min: {df.min().to_dict()}')
print(f'  Max: {df.max().to_dict()}')

print()
print(separador)
print('MIN-MAX SCALER (rango [0, 1])')
print(separador)
print(df_mm.round(3).to_string(index=False))
print(f'  Min: {df_mm.min().round(3).to_dict()}')
print(f'  Max: {df_mm.max().round(3).to_dict()}')

print()
print(separador)
print('STANDARD SCALER (media≈0, desv.típ.≈1)')
print(separador)
print(df_std.round(3).to_string(index=False))
print(f'  Medias:    {df_std.mean().round(4).to_dict()}')
print(f'  Desv.tip.: {df_std.std().round(3).to_dict()}')

# ── Tabla resumen por variable ───────────────────────────────────
print()
print(separador)
print('RESUMEN POR VARIABLE')
print(separador)
for col in cols:
    print(f'{col:>14} | original: [{df[col].min():.1f}, {df[col].max():.1f}]                    | min-max: [{df_mm[col].min():.3f}, {df_mm[col].max():.3f}]                    | z-score: [{df_std[col].min():.3f}, {df_std[col].max():.3f}]')

Regresiones con train y test

# ─────────────────────────────────────────────────────────────────────────────
# REGRESIÓN LOGÍSTICA — Predicción de aprobados/suspensos
# ─────────────────────────────────────────────────────────────────────────────
# La regresión LOGÍSTICA se usa cuando queremos predecir una CATEGORÍA (0 o 1),
# no un número continuo. Aquí queremos saber: ¿aprobará o suspenderá?
# ─────────────────────────────────────────────────────────────────────────────

import pandas as pd
from sklearn.linear_model import LogisticRegression   # el modelo de clasificación
from sklearn.model_selection import train_test_split  # para dividir los datos


# ── 1. DATASET ────────────────────────────────────────────────────────────────
# Cada lista representa una columna con los datos de 15 estudiantes.
# La columna 'aprobo' es la que queremos predecir: 1 = aprobó, 0 = suspendió.

datos = {
    'horas_estudio': [1.5, 3.0, 2.0, 4.5, 1.0, 3.5, 2.5, 0.5, 4.0, 2.0, 3.0, 1.5, 5.0, 2.5, 1.0],
    'asistencia':    [72, 90, 65, 95, 60, 88, 78, 55, 92, 70, 85, 68, 98, 75, 62],
    'nota_parcial':  [4.5, 7.2, 5.0, 8.8, 3.9, 7.5, 6.1, 3.0, 8.2, 5.5, 7.0, 4.8, 9.5, 6.3, 4.0],
    'horas_suenio':  [6, 8, 7, 8, 5, 7, 6, 5, 8, 7, 8, 6, 9, 7, 6],
    'aprobo':        [0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0]
}

# Convertimos el diccionario en un DataFrame (tabla) de pandas.
# Es la estructura más cómoda para trabajar con datos en Python.
df = pd.DataFrame(datos)


# ── 2. SEPARAR X e y ──────────────────────────────────────────────────────────
# X = variables predictoras (las "pistas" que el modelo usará para aprender)
# y = variable objetivo (lo que queremos predecir)
#
# Nota: X lleva VARIAS columnas → usamos dobles corchetes df[[ ]]
#       y lleva UNA sola columna  → usamos un corchete df[ ]

X = df[['horas_estudio', 'asistencia', 'nota_parcial', 'horas_suenio']]
y = df['aprobo']


# ── 3. DIVIDIR EN ENTRENAMIENTO Y TEST ────────────────────────────────────────
# No podemos evaluar el modelo con los mismos datos con los que aprendió
# (sería como darle las respuestas del examen antes del examen).
# Guardamos un 20% de los datos para comprobar después si el modelo acierta.
#
# X_train, y_train → datos con los que el modelo va a aprender (80%)
# X_test,  y_test  → datos que guardamos para evaluar al final  (20%)
# random_state=42  → fija el azar para que la división sea siempre la misma

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


# ── 4. CREAR Y ENTRENAR EL MODELO ─────────────────────────────────────────────
# LogisticRegression() crea el modelo (aún no sabe nada).


modelo = LogisticRegression()

# .fit() es donde ocurre el aprendizaje: el modelo analiza X_train e y_train
# y ajusta sus parámetros internos para predecir lo mejor posible.
# IMPORTANTE: solo usamos los datos de ENTRENAMIENTO aquí, nunca X_test.

modelo.fit(X_train, y_train)


# ── 5. PREDECIR ESTUDIANTES NUEVOS ────────────────────────────────────────────
# Aquí probamos el modelo con estudiantes que NO estaban en el dataset.
# Cada fila es un estudiante: [horas_estudio, asistencia, nota_parcial, horas_suenio]
#
# ⚠ OJO: el tercer estudiante tiene horas_suenio=1 (parece un error en los datos,
#         debería ser algo como 6, 7 u 8). El modelo igualmente hace una predicción,
#         pero el resultado puede no ser realista.

estudiante_nuevo = [
    [3.5, 82, 6.8, 7],   # estudiante 1: estudia bastante, buena nota parcial
    [2.0, 60, 6.0, 6],   # estudiante 2: perfil intermedio
    [4.0, 50, 5.0, 8],   # estudiante 3: estudia mucho pero baja asistencia
]

# predict_proba() devuelve la PROBABILIDAD de pertenecer a cada clase.
# Para cada estudiante devuelve dos números: [P(suspende), P(aprueba)]
# Ambos suman siempre 1.0 (100%).
# Ejemplo: [0.30, 0.70] → 30% de suspender, 70% de aprobar

prob = modelo.predict_proba(estudiante_nuevo)

# predict() devuelve directamente la CLASE predicha: 0 (suspenso) o 1 (aprobado).
# Internamente aplica un umbral de 0.5: si P(aprueba) > 0.5 → predice 1.

clase = modelo.predict(estudiante_nuevo)

# Accedemos a la probabilidad de suspender del SEGUNDO estudiante (índice 1).
# prob tiene forma [estudiante][clase]: prob[1][0] = P(suspende) del estudiante 2.
print(prob[1][0])

# Imprime la clase predicha de los tres estudiantes: algo como [1 0 1]
print(clase)

# Recorremos todos los estudiantes e imprimimos sus probabilidades de forma legible.
# round(...*100) convierte 0.73 en 73 para mostrarlo como porcentaje entero.
for probabilidad in prob:
    print(
        f"Probabilidad de suspender {round(probabilidad[0] * 100)}% "
        f"y de aprobar {round(probabilidad[1] * 100)}%"
    )


# ── 6. EVALUAR EL MODELO SOBRE LOS DATOS DE TEST ──────────────────────────────
# Ahora usamos X_test (las características que el modelo NO vio al entrenar)
# para ver si predice correctamente. Comparamos con y_test (los valores reales).

# y_pred contiene la clase predicha (0 o 1) para cada fila de X_test
y_pred = modelo.predict(X_test)

# y_pred_proba contiene las probabilidades [P(0), P(1)] para cada fila de X_test
y_pred_proba = modelo.predict_proba(X_test)

# Clases predichas: [1 0 1 ...] → lo que el modelo cree que va a pasar
print(y_pred)

# Probabilidades: [[0.3, 0.7], [0.8, 0.2], ...] → con qué seguridad lo predice
print(y_pred_proba)

# Clases reales: los valores verdaderos de y_test que guardamos al principio.
# Comparando y_pred con y_test podemos calcular si el modelo acierta o falla.
print(y_test)

El tuétano de las regresiones

# =============================================================================
# COMPARATIVA: REGRESIÓN LOGÍSTICA vs REGRESIÓN LINEAL
# ¿En qué se diferencian? ¿Cuándo usar cada una?
# =============================================================================
#
# IDEA GENERAL DEL EJERCICIO:
# Tenemos 10 alumnos con sus horas de estudio.
# Vamos a entrenar DOS modelos distintos con los mismos datos:
#
#   Modelo 1 — Regresión LOGÍSTICA:
#     Pregunta: ¿Aprueba o suspende? → Responde con 0 o 1 (categoría)
#
#   Modelo 2 — Regresión LINEAL:
#     Pregunta: ¿Qué nota sacará? → Responde con un número decimal (valor continuo)
#
# Esto ilustra la diferencia clave entre CLASIFICACIÓN y REGRESIÓN en ML.
# =============================================================================


# ─────────────────────────────────────────────────────────────────────────────
# IMPORTACIONES
# ─────────────────────────────────────────────────────────────────────────────
import numpy as np
# numpy nos permite trabajar con arrays numéricos de forma eficiente.
# Lo usamos aquí para crear los datos de entrada y darles el formato correcto.

from sklearn.linear_model import LogisticRegression, LinearRegression
# De scikit-learn importamos los dos modelos que vamos a comparar:
#   - LogisticRegression: clasifica (aprueba / suspende)
#   - LinearRegression:   predice un número continuo (la nota)

from sklearn.model_selection import train_test_split
# Herramienta para dividir los datos en entrenamiento y prueba.
# En este ejercicio no la usamos porque los datos son muy pocos (solo 10),
# pero se importa para recordar que en proyectos reales SIEMPRE hay que usarla.

from sklearn.metrics import accuracy_score
# Función para medir el porcentaje de aciertos de un clasificador.
# Igual que train_test_split, se importa como recordatorio de buenas prácticas,
# aunque en este ejercicio simplificado no la aplicamos.


# =============================================================================
# PASO 1 — PREPARAR LOS DATOS
# =============================================================================

# Horas de estudio de 10 alumnos (la variable de entrada, llamada X)
# Cada número representa cuántas horas estudió ese alumno.
X = np.array([1, 2, 2, 3, 4, 5, 6, 7, 8, 9]).reshape(-1, 1)
#
# ¿Qué hace .reshape(-1, 1)?
# ─────────────────────────
# scikit-learn espera que X tenga forma de TABLA (filas × columnas),
# aunque tengamos una sola columna.
#
# Sin reshape, X sería una lista plana:  [1, 2, 2, 3, ...]  → forma (10,)
# Con reshape(-1, 1), X se convierte en una columna vertical:
#
#   [[1],
#    [2],
#    [2],
#    [3],   ← cada alumno ocupa una fila
#    ...]
#
# El -1 le dice a numpy: "calcula tú cuántas filas hacen falta".
# El 1 indica que queremos 1 columna.
# Resultado: shape (10, 1) → 10 filas, 1 columna.


# Etiquetas para el Modelo 1 — Regresión LOGÍSTICA
# 0 = suspende, 1 = aprueba
y = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
#
# Alumno 1 estudió 1 hora  → suspende (0)
# Alumno 2 estudió 2 horas → suspende (0)
# Alumno 3 estudió 2 horas → suspende (0)
# Alumno 4 estudió 3 horas → suspende (0)
# Alumno 5 estudió 4 horas → suspende (0)
# Alumno 6 estudió 5 horas → aprueba  (1)
# Alumno 7 estudió 6 horas → aprueba  (1)
# Alumno 8 estudió 7 horas → aprueba  (1)
# Alumno 9 estudió 8 horas → aprueba  (1)
# Alumno 10 estudió 9 horas → aprueba (1)
#
# El patrón es claro: a partir de 5 horas, los alumnos aprueban.
# El modelo logístico tiene que aprender ese umbral.


# =============================================================================
# PASO 2 — MODELO 1: REGRESIÓN LOGÍSTICA (clasificación)
# =============================================================================
#
# La regresión logística responde preguntas de tipo SÍ/NO, o en este caso
# APRUEBA/SUSPENDE. Internamente calcula la probabilidad de que el resultado
# sea 1 (aprueba), y si esa probabilidad supera el 50%, predice 1.
#
# Ejemplo: para un alumno con 3 horas podría calcular:
#   probabilidad de aprobar = 15%  → predice 0 (suspende)
#
# Para un alumno con 7 horas:
#   probabilidad de aprobar = 92%  → predice 1 (aprueba)

modelo = LogisticRegression()
# Creamos el modelo. Por ahora es una "caja vacía", aún no ha aprendido nada.

modelo.fit(X, y)
# .fit() es el entrenamiento.
# El modelo analiza todos los pares (horas_estudio, resultado)
# y ajusta sus parámetros internos para aprender el patrón:
# "a partir de X horas, la probabilidad de aprobar supera el 50%".


# ── Predicción con el Modelo 1 ────────────────────────────────────────────
print("=== MODELO 1: Regresión Logística (¿aprueba o suspende?) ===")
print(modelo.predict([[3]]))
# Le preguntamos al modelo: ¿un alumno que estudia 3 horas aprueba o suspende?
#
# [[3]] → los corchetes dobles son necesarios porque el modelo espera
# una tabla, no un número suelto. Es la misma lógica que el reshape anterior.
#
# Resultado esperado: [0]  → el modelo predice que SUSPENDE.
# Esto tiene sentido: con solo 3 horas, los datos de entrenamiento
# mostraban que ese alumno suspendería.

print(modelo.predict_proba([[3]]))
# predict_proba nos da la probabilidad de cada clase, no solo la decisión.
# Devuelve dos números: [P(suspende), P(aprueba)]
# Por ejemplo: [0.82, 0.18] → 82% de probabilidad de suspender, 18% de aprobar.
# Útil cuando no queremos solo un sí/no, sino el grado de certeza del modelo.


# =============================================================================
# PASO 3 — DATOS PARA EL MODELO 2
# =============================================================================

# Notas numéricas de los mismos 10 alumnos (la nueva variable objetivo)
notas = np.array([3, 3, 4, 4, 5, 5, 6, 6, 8, 9])
#
# Alumno 1 (1 hora)  → nota 3
# Alumno 2 (2 horas) → nota 3
# Alumno 3 (2 horas) → nota 4
# Alumno 4 (3 horas) → nota 4
# Alumno 5 (4 horas) → nota 5
# Alumno 6 (5 horas) → nota 5
# Alumno 7 (6 horas) → nota 6
# Alumno 8 (7 horas) → nota 6
# Alumno 9 (8 horas) → nota 8
# Alumno 10 (9 horas) → nota 9
#
# Aquí ya no es aprueba/suspende, sino la nota EXACTA.
# El modelo lineal tiene que aprender cuánto sube la nota por cada hora más.


# =============================================================================
# PASO 4 — MODELO 2: REGRESIÓN LINEAL (predicción de un valor continuo)
# =============================================================================
#
# La regresión lineal ajusta una LÍNEA RECTA a los datos.
# Busca la ecuación:   nota = a × horas + b
#
# Donde:
#   a (pendiente)  = cuántos puntos sube la nota por cada hora extra
#   b (intercepto) = nota base si el alumno estudiara 0 horas
#
# A diferencia del modelo logístico, la salida puede ser CUALQUIER número
# decimal: 4.2, 5.87, 7.3... No está limitada a 0 y 1.

modelo_2 = LinearRegression()
# Creamos el segundo modelo, también vacío de momento.

modelo_2.fit(X, notas)
# Entrenamos el modelo con las mismas horas de estudio (X)
# pero ahora la variable objetivo son las notas numéricas.
#
# Internamente calcula la recta que mejor se ajusta a los datos,
# minimizando el error total entre las notas reales y las predichas.


# ── Predicción con el Modelo 2 ────────────────────────────────────────────
print("\n=== MODELO 2: Regresión Lineal (¿qué nota sacará?) ===")
print(modelo_2.predict([[3]]))
# Le preguntamos: ¿qué nota obtendrá un alumno que estudia 3 horas?
#
# Resultado esperado: algo cercano a 4.0 o 4.2 (un número decimal).
# No devuelve 0 o 1, sino un valor numérico que representa la nota estimada.


# =============================================================================
# RESUMEN: ¿EN QUÉ SE DIFERENCIAN LOS DOS MODELOS?
# =============================================================================
#
#  ┌──────────────────────┬─────────────────────┬──────────────────────────┐
#  │                      │ Regresión LOGÍSTICA  │ Regresión LINEAL         │
#  ├──────────────────────┼─────────────────────┼──────────────────────────┤
#  │ Tipo de problema     │ Clasificación        │ Regresión                │
#  │ Pregunta que responde│ ¿Categoría A o B?    │ ¿Qué valor numérico?     │
#  │ Salida del modelo    │ 0 o 1 (clase)        │ Número decimal (nota)    │
#  │ En este ejercicio    │ Suspende / Aprueba   │ Nota entre 0 y 10        │
#  │ Función interna      │ Sigmoide (0 a 1)     │ Recta y = ax + b         │
#  │ Métrica habitual     │ Accuracy             │ MSE, RMSE, R²            │
#  └──────────────────────┴─────────────────────┴──────────────────────────┘
#
# REGLA PRÁCTICA:
#   → Si la respuesta es una CATEGORÍA (sí/no, color, especie...) → Logística
#   → Si la respuesta es un NÚMERO CONTINUO (precio, temperatura...) → Lineal
# =============================================================================

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.
"""

Regresión logística

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

#1. DATOS
# Horas de estudio de 10 alumnos
X = np.array([1, 2, 2, 3, 4, 5, 6, 7, 8, 9]).reshape(-1, 1)

# 0 = suspenso, 1 = aprobado
y = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1])
print(X)

# Dividir train y test

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

# Entreno el modelo

# Creamos el modelo
modelo = LogisticRegression()

# Entrenamos el modelo con los datos de entrenamiento
modelo.fit(X_train, y_train)

# Medimos que tal funciona nuestro modelo
# Predecimos con los datos de prueba
predicciones = modelo.predict(X_test)
print('Predicciones:', predicciones)

# Calculamos la precisión
precision = accuracy_score(y_test, predicciones)
print(f'Precisión del modelo: {precision * 100:.1f}%')

# Predecir un alumno nuevo: ¿aprobará alguien que estudió 5 horas?
nuevo_alumno = np.array([[5]])
print('¿Aprueba con 5 horas?', modelo.predict(nuevo_alumno))
print('Probabilidad:', modelo.predict_proba(nuevo_alumno))

Ejemplo completo

# =============================================================================
#  EJERCICIO GUIADO: Regresión Lineal Multivariable
#  Dataset: Propinas en un restaurante (tips)
#  Nivel: Principiante / Intermedio
# =============================================================================
#
#  CONTEXTO DEL PROBLEMA
#  ─────────────────────
#  En el ejercicio anterior usamos solo el total de la cuenta para predecir
#  la propina. ¡Pero hay más información disponible!
#
#  PREGUNTA: ¿Podemos predecir mejor la propina usando VARIAS variables?
#
#  Esto es lo que hace la REGRESIÓN LINEAL MULTIVARIABLE:
#  aprender cómo influye cada variable de entrada en la propina.
#
#  La fórmula pasa de tener una sola X a varias:
#
#    tip = b₀ + b₁×total_bill + b₂×size
#
#  donde cada bᵢ (coeficiente) indica cuánto influye esa variable.
#
#  Al final del ejercicio sabrás:
#   · Seleccionar múltiples variables numéricas
#   · Normalizar los datos para que las variables estén en la misma escala
#   · Entrenar un modelo de regresión multivariable
#   · Comparar su rendimiento con el modelo simple anterior
#   · Interpretar los coeficientes de cada variable
#   · Predecir la propina para una comanda nueva con múltiples datos
#
#  VARIABLES DEL DATASET
#  ─────────────────────
#   total_bill → importe total de la cuenta en dólares      [numérica]
#   tip        → propina dejada por el cliente               [numérica, objetivo]
#   sex        → sexo del cliente que paga                   [categórica]
#   smoker     → si la mesa es de fumadores                  [categórica]
#   day        → día de la semana                            [categórica]
#   time       → Lunch / Dinner                              [categórica]
#   size       → número de personas en la mesa               [numérica]
#
#  En este ejercicio usaremos: total_bill y size.
#
# =============================================================================


# ─────────────────────────────────────────────────────────────────────────────
#  PASO 0 — IMPORTACIONES
# ─────────────────────────────────────────────────────────────────────────────
#
#  Ejecuta este bloque primero. Si alguna librería no está instalada:
#      pip install seaborn scikit-learn matplotlib

import seaborn as sns
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 LinearRegression
from sklearn.preprocessing import StandardScaler          # ← nueva importación
from sklearn.metrics import (
    r2_score,
    mean_absolute_error,
    mean_squared_error,
    root_mean_squared_error
)


# ─────────────────────────────────────────────────────────────────────────────
#  PASO 1 — CARGAR EL DATASET
# ─────────────────────────────────────────────────────────────────────────────
#
#  Cargamos el mismo dataset de propinas que en el ejercicio anterior.

print("=" * 60)
print("  PASO 1: Carga del dataset")
print("=" * 60)

df = sns.load_dataset("tips")

# ─────────────────────────────────────────────────────────────────────────────
#  PASO 3 — SELECCIONAR X e y
# ─────────────────────────────────────────────────────────────────────────────
#
#  Ahora X tiene DOS columnas (de ahí el nombre "multivariable"):
#     X = [total_bill, size]
#     y = tip  (lo mismo que antes)
#
#  💡 PISTA: Usa doble corchete para seleccionar varias columnas:
#       X = df[["total_bill", "size"]]
#
#  🛠️  TU TURNO:
#     1. Define X con las dos variables indicadas.
#     2. Define y como la columna "tip".
#     3. Imprime X.shape e y.shape.
#        ¿Qué diferencia ves en X respecto al ejercicio de regresión simple?

print("\n" + "=" * 60)
print("  PASO 3: Seleccionar X e y")
print("=" * 60)

# --- ESCRIBE TU CÓDIGO AQUÍ ---
X = df[["total_bill", "size"]] # ¿Qué variables? total_bill y size
y = df["tip"]



# ─────────────────────────────────────────────────────────────────────────────
#  PASO 4 — DIVIDIR EN ENTRENAMIENTO Y TEST
# ─────────────────────────────────────────────────────────────────────────────
#
#  Dividimos ANTES de normalizar para evitar "data leakage":
#  si normalizamos con todos los datos, el modelo vería información
#  del conjunto de test durante el entrenamiento (hace trampa).
#
#  Orden correcto: dividir → normalizar con train → aplicar a test.
#
#  💡 PISTA: train_test_split(X, y, test_size=0.2, random_state=42)
#
#  🛠️  TU TURNO:
#     Aplica train_test_split y guarda los cuatro conjuntos:
#         X_train, X_test, y_train, y_test
#     Imprime cuántas filas tiene cada parte.

print("\n" + "=" * 60)
print("  PASO 4: División train / test  (80% / 20%)")
print("=" * 60)

# --- ESCRIBE TU CÓDIGO AQUÍ -
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)


# ─────────────────────────────────────────────────────────────────────────────
#  PASO 5 — NORMALIZACIÓN (StandardScaler)
# ─────────────────────────────────────────────────────────────────────────────
#
#  PROBLEMA: total_bill va de 3 a 51 $, pero size va de 1 a 6 personas.
#  Son escalas muy distintas. Si no normalizamos, el modelo puede dar
#  más "peso" a total_bill simplemente porque sus valores son más grandes,
#  no porque sea más importante.
#
#  StandardScaler convierte cada variable para que tenga:
#     · media = 0
#     · desviación típica = 1
#
#  Fórmula:  z = (x − media) / desviación_típica
#
#  ⚠️  REGLA IMPORTANTE: el scaler se "aprende" (fit) SOLO con X_train.
#     Luego se aplica (transform) tanto a X_train como a X_test.
#     Nunca hagas fit con X_test: estarías mirando datos que no deberías ver.
#
#  💡 PISTA:
#       scaler = StandardScaler()
#       X_train_sc = scaler.fit_transform(X_train)   # aprende Y transforma
#       X_test_sc  = scaler.transform(X_test)         # solo transforma
#
#  🛠️  TU TURNO:
#     1. Crea el scaler y aplícalo según el ejemplo de arriba.
#     2. Imprime la media y desviación típica que aprendió el scaler.
#        Pista: scaler.mean_  y  scaler.scale_
#     3. Comprueba que X_train_sc tiene media ≈ 0 y std ≈ 1 por columna.
#        Pista: pd.DataFrame(X_train_sc, columns=X.columns).describe().round(2)

print("\n" + "=" * 60)
print("  PASO 5: Normalización con StandardScaler")
print("=" * 60)


# --- ESCRIBE TU CÓDIGO AQUÍ ---
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_test_sc  = scaler.transform(X_test)


# ─────────────────────────────────────────────────────────────────────────────
#  PASO 6 — ENTRENAR EL MODELO
# ─────────────────────────────────────────────────────────────────────────────
#
#  Entrenamos con X_train_sc (los datos normalizados), no con X_train.
#
#  Con dos variables normalizadas, la fórmula aprendida será:
#    tip = b₀ + b₁×total_bill_sc + b₂×size_sc
#
#  Ahora los coeficientes b₁ y b₂ SÍ son directamente comparables:
#  el mayor indica la variable que más influye en la propina.
#
#  💡 PISTA:
#       modelo = LinearRegression()
#       modelo.fit(X_train_sc, y_train)
#
#  🛠️  TU TURNO:
#     1. Crea el modelo y entrénalo con los datos normalizados.
#     2. Imprime los coeficientes junto al nombre de cada variable.
#        Pista: zip(X.columns, modelo.coef_)
#     3. ¿Qué variable tiene el coeficiente más alto?
#        Después de normalizar, eso sí que indica cuál importa más.

print("\n" + "=" * 60)
print("  PASO 6: Entrenamiento del modelo")
print("=" * 60)

# --- ESCRIBE TU CÓDIGO AQUÍ ---
modelo = LinearRegression()
modelo.fit(X_train_sc, y_train)

# ─────────────────────────────────────────────────────────────────────────────
#  PASO 7 — EVALUAR EL MODELO
# ─────────────────────────────────────────────────────────────────────────────
#
#  Calcula las predicciones sobre X_test_sc y luego las tres métricas:
#     · R²   → ¿qué porcentaje de la variación explica el modelo?
#     · MAE  → error medio en dólares
#     · RMSE → igual que MAE pero penaliza más los errores grandes
#
#  💡 PISTA: El modelo simple (solo total_bill, sin normalizar) obtenía R² ≈ 0.46.
#            ¿Mejora al usar dos variables normalizadas?
#
#  🛠️  TU TURNO:
#     1. Obtén y_pred con modelo.predict(X_test_sc).
#     2. Calcula r2, mae y rmse.
#     3. Imprime los resultados y compáralos con el modelo simple.

print("\n" + "=" * 60)
print("  PASO 7: Evaluación del modelo")
print("=" * 60)

# --- ESCRIBE TU CÓDIGO AQUÍ ---
y_pred = modelo.predict(X_test_sc)
r2   = r2_score(y_test, y_pred)
mae  = mean_absolute_error(y_test, y_pred)
rmse = root_mean_squared_error(y_test, y_pred)

print(f"  R²   = {r2:.4f}")
print(f"  MAE  = {mae:.4f} $")
print(f"  RMSE = {rmse:.4f} $")
print("\n  ¿Mejoró respecto al modelo simple (R² ≈ 0.46)?")