Los 5 pasos de la regresión

# Los 5 pasos de la regresión

En un mundo obsesionado con avanzar cada vez más rápido, *Los 5 pasos de la regresión* propone una idea aparentemente contradictoria: para progresar de verdad, primero debemos aprender a regresar. Regresar a los fundamentos, a los patrones que gobiernan nuestra mente y a las preguntas esenciales que dan sentido a nuestra existencia. Este libro combina programación en Python, desarrollo personal y reflexión espiritual en una propuesta única destinada a quienes desean comprender tanto las máquinas como a sí mismos.

El autor parte de un concepto tomado de la ciencia de datos: la regresión. En programación, una regresión busca descubrir relaciones ocultas entre variables para poder comprender y predecir el comportamiento de un sistema. En la vida ocurre algo similar. Nuestros pensamientos, emociones y decisiones no aparecen de manera aislada; responden a patrones que pueden ser observados, analizados y transformados.

El primer paso, **Observar los datos**, enseña a contemplar la realidad sin prejuicios. Igual que un programador examina un conjunto de datos antes de construir un modelo, el lector aprende a identificar hábitos, creencias y circunstancias que influyen en su vida cotidiana. La observación rigurosa se convierte en una forma de autoconocimiento.

El segundo paso, **Limpiar el ruido**, muestra cómo distinguir la información relevante de las distracciones. En Python, los datos incompletos o erróneos pueden arruinar cualquier análisis. Del mismo modo, los miedos heredados, las expectativas ajenas y las narrativas falsas distorsionan nuestra percepción del mundo. El proceso de depuración se transforma aquí en una práctica espiritual.

El tercer paso, **Encontrar las variables ocultas**, invita a explorar aquello que no siempre es visible. Muchas veces buscamos respuestas en el exterior cuando las causas reales se encuentran en dimensiones más profundas de nuestra personalidad. El libro conecta conceptos estadísticos con enseñanzas filosóficas y tradiciones espirituales para mostrar cómo descubrir esos factores invisibles.

El cuarto paso, **Entrenar el modelo**, explica que ningún cambio ocurre de forma instantánea. Tanto en programación como en la vida, el aprendizaje requiere repetición, paciencia y corrección constante. Los ejercicios prácticos en Python sirven como metáfora de la disciplina personal necesaria para construir una identidad más consciente y resiliente.

El quinto paso, **Predecir con humildad**, aborda la gran lección de toda regresión: ningún modelo es perfecto. Podemos mejorar nuestras estimaciones, comprender tendencias y tomar mejores decisiones, pero siempre existirá incertidumbre. Esta aceptación de los límites se convierte en una fuente de serenidad y sabiduría.

A lo largo de la obra, el lector encontrará ejemplos accesibles de programación en Python, explicados para principiantes, junto con ejercicios de reflexión y prácticas de atención consciente. El objetivo no es formar únicamente programadores ni únicamente buscadores espirituales, sino personas capaces de pensar con claridad y actuar con propósito.

Lejos de presentar la tecnología y la espiritualidad como mundos opuestos, el libro demuestra que ambos comparten una misma aspiración: comprender patrones ocultos. Allí donde el programador busca relaciones entre variables, el ser humano busca significado. Ambas búsquedas pueden enriquecerse mutuamente.

*Los 5 pasos de la regresión* es una invitación a mirar la propia vida como un conjunto de datos en constante evolución. No promete fórmulas mágicas ni respuestas definitivas, sino una metodología para aprender, corregir errores, adaptarse al cambio y encontrar sentido en medio de la complejidad. Un manual para quienes desean programar mejor, vivir mejor y comprender mejor el misterio de ser humanos.

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)?")


Regresión lineal multivariable

import numpy as np # Operaciones numéricas y arrays
import pandas as pd # Manejo de tablas de datos (DataFrames)
import matplotlib.pyplot as plt # Gráficas
from sklearn.model_selection import train_test_split # Dividir datos en tren/test
from sklearn.linear_model import LinearRegression # Nuestro modelo
from sklearn.preprocessing import StandardScaler # Normalizar datos
from sklearn.metrics import mean_squared_error, r2_score

np.random.seed(42)  # Fijamos semilla para reproducibilidad
n = 200         # Número de pisos en nuestro dataset

# Cada columna representa una característica del piso
data = pd.DataFrame({
    'metros2'     : np.random.randint(30, 150, n),   # Entre 30 y 150 m²
    'habitaciones': np.random.randint(1,  6, n),   # Entre 1 y 5 habitaciones
    'planta'      : np.random.randint(0,  10, n),   # Entre planta 0 y 9
    'antiguedad'  : np.random.randint(0,  50, n),   # Años de antigüedad
})

# Fórmula ficticia para el precio (así sabemos cuál es la verdad)
# precio = 50k + 2000×m² + 8000×hab + 500×planta - 300×antigüedad + ruido
ruido = np.random.normal(0, 10000, n)  # Variación aleatoria realista
data['precio'] = (
    50000
    + 2000 * data['metros2']
    + 8000 * data['habitaciones']
    +  500 * data['planta']
    -  300 * data['antiguedad']
    + ruido
)

print(data.head())  # Ver las primeras filas

# Aquí tenemos un dataset de 200 pisos con sus precios aleatorio pero siguiendo
# una fórmula que podemos predecir

# ─── Separar variables de entrada (X) y salida (y) ─────────────
X = data.drop(columns=['precio'])  # Todo excepto el precio
y = data['precio']          # Solo el precio (lo que queremos predecir)

# ─── Dividir en entrenamiento (80%) y test (20%) ─────────────
X_train, X_test, y_train, y_test = train_test_split(
    X, y,                   # Datos de entrada y salida
    test_size=0.2,          # 20% para test
    random_state=42          # Semilla para resultados reproducibles
)

print(f'Datos de entrenamiento: {len(X_train)} pisos')
print(f'Datos de test:          {len(X_test)} pisos')
# → Datos de entrenamiento: 160 pisos
# → Datos de test:          40 pisos

# ─── Normalizar los datos ──────────────────────────────────────
scaler = StandardScaler()  # Creamos el normalizador

# fit_transform: aprende la media y std DE LOS DATOS DE ENTRENAMIENTO
# y luego transforma. NUNCA uses fit en los datos de test.
X_train_scaled = scaler.fit_transform(X_train)

# transform solo: aplica la misma transformación al test
# (sin volver a aprender — eso sería trampa)
X_test_scaled  = scaler.transform(X_test)

modelo = LinearRegression()  # Instanciar el modelo

# .fit() es donde ocurre el 'aprendizaje':
# el modelo encuentra los mejores coeficientes β
modelo.fit(X_train_scaled, y_train)

# Ver los coeficientes aprendidos
print('Intercepto (β₀):', modelo.intercept_)
for nombre, coef in zip(X.columns, modelo.coef_):
    print(f'  {nombre:<15}: {coef:>10.2f}')

y_pred = modelo.predict(X_test_scaled)  # Predicciones

# R² (R-cuadrado): qué porcentaje de la variación explica el modelo
# Va de 0 a 1. Cuanto más cerca de 1, mejor.
r2  = r2_score(y_test, y_pred)

# RMSE: error promedio en las mismas unidades que y (euros)
# Fácil de interpretar: 'me equivoco en promedio X euros'
mse  = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)

print(f'R²   = {r2:.4f}')  # p.ej. R² = 0.9812
print(f'RMSE = {rmse:.2f} €')  # p.ej. RMSE = 9.847,23 €
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- Gráfico 1: Predicciones vs Valores reales ---
axes[0].scatter(y_test, y_pred, alpha=0.5, color='steelblue')
# La línea perfecta (y = x) sirve como referencia visual
axes[0].plot([y_test.min(), y_test.max()],
         [y_test.min(), y_test.max()], 'r--', label='Predicción perfecta')
axes[0].set_xlabel('Precio real (€)')
axes[0].set_ylabel('Precio predicho (€)')
axes[0].set_title(f'Predicciones vs. Valores reales\nR² = {r2:.4f}')
axes[0].legend()

# --- Gráfico 2: Residuos (errores del modelo) ---
residuos = y_test - y_pred  # Diferencia entre real y predicho
axes[1].scatter(y_pred, residuos, alpha=0.5, color='coral')
axes[1].axhline(0, color='black', linestyle='--')  # Línea en 0
axes[1].set_xlabel('Precio predicho (€)')
axes[1].set_ylabel('Residuo (real - predicho)')
axes[1].set_title('Análisis de Residuos\n(idealmente dispersos sin patrón)')

plt.tight_layout()  # Ajusta márgenes automáticamente
plt.savefig('resultados_regresion.png', dpi=150, bbox_inches='tight')
plt.show()

# ─── Predecir el precio de un piso nuevo ───────────────────────

# Creamos un DataFrame con las características del piso nuevo
# ¡Importante: usar los mismos nombres de columna que en el entrenamiento!
piso_nuevo = pd.DataFrame([{
    'metros2'     : 90,   # 90 metros cuadrados
    'habitaciones': 3,   # 3 habitaciones
    'planta'      : 5,   # Planta 5
    'antiguedad'  : 10,  # 10 años de antigüedad
}])

# Normalizar usando el MISMO scaler que entrenamos (no fit_transform)
piso_nuevo_scaled = scaler.transform(piso_nuevo)

# Predecir
precio_estimado = modelo.predict(piso_nuevo_scaled)[0]
print(f'Precio estimado: {precio_estimado:,.0f} €')
# → Precio estimado: 250.500 €  (resultado aproximado)

Gráficos

import matplotlib.pyplot as plt

# --- DATOS ---
# Estos son los datos que queremos representar.
# Cada elemento de 'ventas' corresponde a un mes.
meses  = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun']
ventas = [120,   145,   132,   178,   165,   200]
ventas2 = [150,   125,   162,   128,   105,   230]

# plt.subplots() crea dos cosas a la vez:
#   fig  → la figura (el lienzo completo)
#   ax   → los ejes (la zona de dibujo)
fig, ax = plt.subplots()

# --- DIBUJAR LA LÍNEA ---
# ax.plot(eje_x, eje_y) dibuja la línea.
# color     → color de la línea (nombre o código hex)
# linewidth → grosor de la línea
# marker    → símbolo en cada punto ('o' = círculo)
ax.plot(meses, ventas, color='blueviolet', linewidth=2, marker='p',label="Tienda A")
ax.plot(meses, ventas2,mfc='green', color='tomato', linewidth=2, marker='s', linestyle='--', label="Tienda B")
ax.legend(loc='upper left', fontsize=10)
# --- ETIQUETAS Y TÍTULO ---
ax.set_title('Ventas mensuales')   # Título de la gráfica
ax.set_xlabel('Mes')               # Etiqueta del eje horizontal
ax.set_ylabel('Unidades vendidas') # Etiqueta del eje vertical

# --- CUADRÍCULA ---
# La cuadrícula facilita leer los valores.
# linestyle='--' → líneas discontinuas
# alpha=0.4      → 40% de opacidad (líneas sutiles)
ax.grid(True, linestyle='--', alpha=0.4)

# --- MOSTRAR ---
plt.tight_layout()  # Ajusta márgenes automáticamente
plt.savefig('ejemplo.png', dpi=150, bbox_inches='tight')

# plt.show() abre la ventana y muestra el resultado
plt.show()