Comparativa de Modelos de Regresión: ML vs Redes Neuronales
Comparación de SGDRegressor, Random Forest, Gradient Boosting y MLP en regresión tabular sobre California Housing.
Comparativa completa de modelos de regresión: ML clásico vs redes neuronales
En este notebook construiremos, entrenaremos y evaluaremos varios modelos para resolver un problema de regresión supervisada realista: predecir el valor medio de vivienda usando el dataset California Housing.
Objetivo didáctico
La meta es entender, en un mismo flujo de trabajo, cómo cambian el rendimiento y el comportamiento de modelos de distinta complejidad:
- Modelo lineal entrenado con gradiente (
SGDRegressor). - Random Forest Regressor (ensamble de árboles por bagging).
- Gradient Boosting Regressor (ensamble secuencial por boosting).
- Red neuronal simple (pocas capas/parámetros).
- Red neuronal profunda (más capacidad representacional).
Además, compararemos métricas, curvas de entrenamiento/validación y diagnósticos de error para decidir qué enfoque funciona mejor en este caso.
Fundamentos matemáticos y computacionales (visión práctica)
1) Problema de regresión
Queremos aproximar una función:
$$ \hat{y} = f_ heta(x) $$
donde $x$ son variables de entrada (demografía, geografía, etc.) y $y$ es una variable continua (valor medio de vivienda).
2) Función de pérdida
Usaremos principalmente el error cuadrático medio (MSE):
$$ \mathcal{L}( heta)=\frac{1}{n}\sum_{i=1}^{n}(y_i-\hat{y}_i)^2 $$
- Penaliza más los errores grandes.
- Es derivable, por lo que encaja muy bien con optimización por gradiente.
En la comparación final también veremos:
- MAE (error absoluto medio), más robusto a outliers.
- RMSE (raíz de MSE), en las mismas unidades de la variable objetivo.
- R², proporción de varianza explicada.
3) Modelos clásicos vs redes neuronales
- Lineal (SGDRegressor): aprende una relación global aproximadamente lineal.
- Random Forest: promedia muchos árboles entrenados sobre subconjuntos aleatorios (reduce varianza).
- Gradient Boosting: añade árboles secuencialmente para corregir errores previos (reduce sesgo).
- Redes neuronales: componen transformaciones no lineales y pueden modelar interacciones complejas.
4) Curvas de aprendizaje
Tras cada entrenamiento veremos la evolución en train/val de MSE o RMSE frente a épocas/iteraciones. Esto permite detectar:
- Underfitting (alto error en train y val).
- Overfitting (train baja mucho, val empeora).
- Regiones de entrenamiento estables.
Dataset y librerías
- Dataset:
fetch_california_housingde scikit-learn. - Tarea: regresión tabular con 8 variables numéricas.
- Modelos:
SGDRegressor,RandomForestRegressor,GradientBoostingRegressor, y dos MLP en PyTorch.
Nota pedagógica: el dataset no es trivial (hay no linealidades), pero tampoco excesivamente complejo, por lo que es ideal para comparar enfoques de forma clara.
# Configuración general y librerías
# (Si falta alguna librería en tu entorno, instala la correspondiente y vuelve a ejecutar)
import warnings
warnings.filterwarnings('ignore')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import SGDRegressor
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
# Estilo de gráficos
sns.set_theme(style='whitegrid', context='notebook')
plt.rcParams['figure.figsize'] = (9, 5)
# Reproducibilidad
SEED = 42
np.random.seed(SEED)
torch.manual_seed(SEED)
<torch._C.Generator at 0x753918190390>
1) Carga de datos y EDA inicial
Vamos a inspeccionar el dataset para entender dimensiones, variables, rangos y posibles señales de complejidad (asimetrías, correlaciones parciales, etc.).
# Cargamos California Housing y construimos un DataFrame
housing = fetch_california_housing(as_frame=True)
df = housing.frame.copy()
target_col = 'MedHouseVal' # objetivo (en cientos de miles de dólares)
print('Shape:', df.shape)
print('\nColumnas:', list(df.columns))
df.head()
Shape: (20640, 9) Columnas: ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population', 'AveOccup', 'Latitude', 'Longitude', 'MedHouseVal']
| MedInc | HouseAge | AveRooms | AveBedrms | Population | AveOccup | Latitude | Longitude | MedHouseVal | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 8.3252 | 41.0 | 6.984127 | 1.023810 | 322.0 | 2.555556 | 37.88 | -122.23 | 4.526 |
| 1 | 8.3014 | 21.0 | 6.238137 | 0.971880 | 2401.0 | 2.109842 | 37.86 | -122.22 | 3.585 |
| 2 | 7.2574 | 52.0 | 8.288136 | 1.073446 | 496.0 | 2.802260 | 37.85 | -122.24 | 3.521 |
| 3 | 5.6431 | 52.0 | 5.817352 | 1.073059 | 558.0 | 2.547945 | 37.85 | -122.25 | 3.413 |
| 4 | 3.8462 | 52.0 | 6.281853 | 1.081081 | 565.0 | 2.181467 | 37.85 | -122.25 | 3.422 |
# Revisión rápida de tipos y valores faltantes
print(df.info())
print('\nNulos por columna:')
print(df.isnull().sum())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 20640 entries, 0 to 20639 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 MedInc 20640 non-null float64 1 HouseAge 20640 non-null float64 2 AveRooms 20640 non-null float64 3 AveBedrms 20640 non-null float64 4 Population 20640 non-null float64 5 AveOccup 20640 non-null float64 6 Latitude 20640 non-null float64 7 Longitude 20640 non-null float64 8 MedHouseVal 20640 non-null float64 dtypes: float64(9) memory usage: 1.4 MB None Nulos por columna: MedInc 0 HouseAge 0 AveRooms 0 AveBedrms 0 Population 0 AveOccup 0 Latitude 0 Longitude 0 MedHouseVal 0 dtype: int64
# Estadística descriptiva para identificar escalas y dispersión
df.describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| MedInc | 20640.0 | 3.870671 | 1.899822 | 0.499900 | 2.563400 | 3.534800 | 4.743250 | 15.000100 |
| HouseAge | 20640.0 | 28.639486 | 12.585558 | 1.000000 | 18.000000 | 29.000000 | 37.000000 | 52.000000 |
| AveRooms | 20640.0 | 5.429000 | 2.474173 | 0.846154 | 4.440716 | 5.229129 | 6.052381 | 141.909091 |
| AveBedrms | 20640.0 | 1.096675 | 0.473911 | 0.333333 | 1.006079 | 1.048780 | 1.099526 | 34.066667 |
| Population | 20640.0 | 1425.476744 | 1132.462122 | 3.000000 | 787.000000 | 1166.000000 | 1725.000000 | 35682.000000 |
| AveOccup | 20640.0 | 3.070655 | 10.386050 | 0.692308 | 2.429741 | 2.818116 | 3.282261 | 1243.333333 |
| Latitude | 20640.0 | 35.631861 | 2.135952 | 32.540000 | 33.930000 | 34.260000 | 37.710000 | 41.950000 |
| Longitude | 20640.0 | -119.569704 | 2.003532 | -124.350000 | -121.800000 | -118.490000 | -118.010000 | -114.310000 |
| MedHouseVal | 20640.0 | 2.068558 | 1.153956 | 0.149990 | 1.196000 | 1.797000 | 2.647250 | 5.000010 |
# Histogramas: observamos distribuciones y posibles asimetrías
_ = df.hist(bins=30, figsize=(14, 10), edgecolor='black')
plt.suptitle('Distribución de variables (EDA)', y=1.02)
plt.tight_layout()
plt.show()
# Matriz de correlación para detectar relaciones lineales aproximadas
corr = df.corr(numeric_only=True)
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', square=True)
plt.title('Matriz de correlación')
plt.tight_layout()
plt.show()
# Relación entre variables clave y la variable objetivo
fig, axes = plt.subplots(1, 3, figsize=(17, 4))
sns.scatterplot(data=df.sample(3000, random_state=SEED), x='MedInc', y=target_col, alpha=0.5, ax=axes[0])
axes[0].set_title('Ingreso medio vs valor vivienda')
sns.scatterplot(data=df.sample(3000, random_state=SEED), x='AveRooms', y=target_col, alpha=0.5, ax=axes[1])
axes[1].set_title('Habitaciones medias vs valor vivienda')
sns.scatterplot(data=df.sample(3000, random_state=SEED), x='Latitude', y=target_col, alpha=0.5, ax=axes[2])
axes[2].set_title('Latitud vs valor vivienda')
plt.tight_layout()
plt.show()
2) Preparación de datos
Separaremos en train/validación/test para evaluar de forma rigurosa:
- Train: ajuste de parámetros.
- Validación: selección de hiperparámetros y control de sobreajuste.
- Test: estimación final no sesgada.
Para modelos sensibles a escala (lineales y redes) aplicaremos StandardScaler.
# Separación de variables
X = df.drop(columns=[target_col]).values
y = df[target_col].values
# Split estratificado no aplica aquí (objetivo continuo), usamos split aleatorio reproducible
X_train_full, X_test, y_train_full, y_test = train_test_split(
X, y, test_size=0.15, random_state=SEED
)
X_train, X_val, y_train, y_val = train_test_split(
X_train_full, y_train_full, test_size=0.1765, random_state=SEED
)
# 0.1765 de 0.85 aprox = 0.15 -> resultado final ~70/15/15
print('Train:', X_train.shape, 'Val:', X_val.shape, 'Test:', X_test.shape)
# Escalado (solo fit con train para evitar fuga de información)
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_val_sc = scaler.transform(X_val)
X_test_sc = scaler.transform(X_test)
Train: (14447, 8) Val: (3097, 8) Test: (3096, 8)
3) Funciones auxiliares de evaluación y visualización
def regression_metrics(y_true, y_pred):
"""Devuelve métricas estándar de regresión en un diccionario."""
mse = mean_squared_error(y_true, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_true, y_pred)
r2 = r2_score(y_true, y_pred)
return {'MSE': mse, 'RMSE': rmse, 'MAE': mae, 'R2': r2}
def plot_learning_curves(train_values, val_values, title, ylabel='RMSE'):
"""Pinta curvas train/val frente a iteraciones/épocas."""
epochs = np.arange(1, len(train_values) + 1)
plt.figure(figsize=(8, 4.5))
plt.plot(epochs, train_values, label='Train')
plt.plot(epochs, val_values, label='Validación')
plt.xlabel('Época / iteración')
plt.ylabel(ylabel)
plt.title(title)
plt.legend()
plt.tight_layout()
plt.show()
4) Modelo 1 — Regresión lineal con SGD
Este modelo actúa como baseline lineal optimizado por gradiente. Entrenamos por épocas con partial_fit para poder monitorizar curvas train/val.
sgd = SGDRegressor(
loss='squared_error',
penalty='l2',
alpha=1e-4,
learning_rate='invscaling',
eta0=0.01,
power_t=0.25,
random_state=SEED
)
n_epochs = 80
sgd_train_rmse, sgd_val_rmse = [], []
for epoch in range(n_epochs):
# partial_fit permite simular entrenamiento por épocas
sgd.partial_fit(X_train_sc, y_train)
pred_train = sgd.predict(X_train_sc)
pred_val = sgd.predict(X_val_sc)
sgd_train_rmse.append(np.sqrt(mean_squared_error(y_train, pred_train)))
sgd_val_rmse.append(np.sqrt(mean_squared_error(y_val, pred_val)))
plot_learning_curves(sgd_train_rmse, sgd_val_rmse, 'SGDRegressor - Curva de aprendizaje', ylabel='RMSE')
sgd_test_pred = sgd.predict(X_test_sc)
sgd_metrics = regression_metrics(y_test, sgd_test_pred)
print('Métricas test SGD:', sgd_metrics)
Métricas test SGD: {'MSE': 0.5456287887633794, 'RMSE': np.float64(0.7386668997345011), 'MAE': 0.5381823389180139, 'R2': 0.5836508721145055}
5) Modelo 2 — Random Forest Regressor
Random Forest no entrena por épocas clásicas, pero podemos observar su evolución añadiendo árboles progresivamente (warm_start=True).
rf = RandomForestRegressor(
n_estimators=1,
max_depth=None,
min_samples_leaf=2,
random_state=SEED,
n_jobs=-1,
warm_start=True
)
rf_train_rmse, rf_val_rmse = [], []
num_trees_progress = list(range(10, 210, 10))
for n_trees in num_trees_progress:
rf.set_params(n_estimators=n_trees)
rf.fit(X_train, y_train)
pred_train = rf.predict(X_train)
pred_val = rf.predict(X_val)
rf_train_rmse.append(np.sqrt(mean_squared_error(y_train, pred_train)))
rf_val_rmse.append(np.sqrt(mean_squared_error(y_val, pred_val)))
plot_learning_curves(rf_train_rmse, rf_val_rmse, 'Random Forest - Evolución al añadir árboles', ylabel='RMSE')
rf_test_pred = rf.predict(X_test)
rf_metrics = regression_metrics(y_test, rf_test_pred)
print('Métricas test Random Forest:', rf_metrics)
Métricas test Random Forest: {'MSE': 0.2636236411832535, 'RMSE': np.float64(0.513442928847261), 'MAE': 0.33517824870248347, 'R2': 0.7988385595536363}
6) Modelo 3 — Gradient Boosting Regressor
El boosting construye árboles de manera secuencial. Esto nos permite observar su progreso natural mediante staged_predict.
gbr = GradientBoostingRegressor(
n_estimators=250,
learning_rate=0.05,
max_depth=3,
subsample=0.9,
random_state=SEED
)
gbr.fit(X_train, y_train)
gbr_train_rmse, gbr_val_rmse = [], []
for pred_train, pred_val in zip(gbr.staged_predict(X_train), gbr.staged_predict(X_val)):
gbr_train_rmse.append(np.sqrt(mean_squared_error(y_train, pred_train)))
gbr_val_rmse.append(np.sqrt(mean_squared_error(y_val, pred_val)))
plot_learning_curves(gbr_train_rmse, gbr_val_rmse, 'Gradient Boosting - Curva de aprendizaje', ylabel='RMSE')
gbr_test_pred = gbr.predict(X_test)
gbr_metrics = regression_metrics(y_test, gbr_test_pred)
print('Métricas test Gradient Boosting:', gbr_metrics)
Métricas test Gradient Boosting: {'MSE': 0.277516426013637, 'RMSE': np.float64(0.5267982782941085), 'MAE': 0.3635442549539332, 'R2': 0.7882374898022757}
7) Modelos neuronales en PyTorch
Entrenaremos dos arquitecturas:
- Red neuronal simple: 1 capa oculta pequeña.
- Red neuronal profunda: varias capas, BatchNorm y Dropout.
Para ambas mostraremos curvas de pérdida (MSE) y RMSE en train/val.
# Conversión a tensores
X_train_t = torch.tensor(X_train_sc, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)
X_val_t = torch.tensor(X_val_sc, dtype=torch.float32)
y_val_t = torch.tensor(y_val, dtype=torch.float32).view(-1, 1)
X_test_t = torch.tensor(X_test_sc, dtype=torch.float32)
y_test_t = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)
train_ds = TensorDataset(X_train_t, y_train_t)
val_ds = TensorDataset(X_val_t, y_val_t)
train_loader = DataLoader(train_ds, batch_size=128, shuffle=True)
val_loader = DataLoader(val_ds, batch_size=256, shuffle=False)
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print('Dispositivo:', device)
Dispositivo: cuda
# Definimos arquitecturas
class SimpleRegressor(nn.Module):
def __init__(self, in_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, 32),
nn.ReLU(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.net(x)
class DeepRegressor(nn.Module):
def __init__(self, in_features):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_features, 128),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.Dropout(0.15),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.10),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1)
)
def forward(self, x):
return self.net(x)
def train_torch_model(model, train_loader, val_loader, lr=1e-3, epochs=120):
"""Entrena un modelo PyTorch y devuelve historial de loss/RMSE."""
model = model.to(device)
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
history = {
'train_loss': [], 'val_loss': [],
'train_rmse': [], 'val_rmse': []
}
for epoch in range(epochs):
# ----- Entrenamiento -----
model.train()
train_losses = []
train_preds, train_targets = [], []
for xb, yb in train_loader:
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
preds = model(xb)
loss = criterion(preds, yb)
loss.backward()
optimizer.step()
train_losses.append(loss.item())
train_preds.append(preds.detach().cpu().numpy())
train_targets.append(yb.detach().cpu().numpy())
# ----- Validación -----
model.eval()
val_losses = []
val_preds, val_targets = [], []
with torch.no_grad():
for xb, yb in val_loader:
xb, yb = xb.to(device), yb.to(device)
preds = model(xb)
loss = criterion(preds, yb)
val_losses.append(loss.item())
val_preds.append(preds.cpu().numpy())
val_targets.append(yb.cpu().numpy())
# Cálculo de métricas por época
train_preds_np = np.vstack(train_preds).ravel()
train_targets_np = np.vstack(train_targets).ravel()
val_preds_np = np.vstack(val_preds).ravel()
val_targets_np = np.vstack(val_targets).ravel()
train_loss = float(np.mean(train_losses))
val_loss = float(np.mean(val_losses))
train_rmse = np.sqrt(mean_squared_error(train_targets_np, train_preds_np))
val_rmse = np.sqrt(mean_squared_error(val_targets_np, val_preds_np))
history['train_loss'].append(train_loss)
history['val_loss'].append(val_loss)
history['train_rmse'].append(train_rmse)
history['val_rmse'].append(val_rmse)
return model, history
7.1 Red neuronal simple
simple_model = SimpleRegressor(in_features=X_train_sc.shape[1])
simple_model, simple_hist = train_torch_model(simple_model, train_loader, val_loader, lr=1e-3, epochs=120)
# Curvas de pérdida
plot_learning_curves(simple_hist['train_loss'], simple_hist['val_loss'], 'NN simple - Loss (MSE)', ylabel='MSE')
# Curvas de RMSE
plot_learning_curves(simple_hist['train_rmse'], simple_hist['val_rmse'], 'NN simple - RMSE', ylabel='RMSE')
# Evaluación en test
simple_model.eval()
with torch.no_grad():
simple_test_pred = simple_model(X_test_t.to(device)).cpu().numpy().ravel()
simple_metrics = regression_metrics(y_test, simple_test_pred)
print('Métricas test NN simple:', simple_metrics)
Métricas test NN simple: {'MSE': 0.3201096736787496, 'RMSE': np.float64(0.5657823553971524), 'MAE': 0.3903933918560722, 'R2': 0.755736159439242}
7.2 Red neuronal profunda
deep_model = DeepRegressor(in_features=X_train_sc.shape[1])
deep_model, deep_hist = train_torch_model(deep_model, train_loader, val_loader, lr=8e-4, epochs=180)
# Curvas de pérdida
plot_learning_curves(deep_hist['train_loss'], deep_hist['val_loss'], 'NN profunda - Loss (MSE)', ylabel='MSE')
# Curvas de RMSE
plot_learning_curves(deep_hist['train_rmse'], deep_hist['val_rmse'], 'NN profunda - RMSE', ylabel='RMSE')
# Evaluación en test
deep_model.eval()
with torch.no_grad():
deep_test_pred = deep_model(X_test_t.to(device)).cpu().numpy().ravel()
deep_metrics = regression_metrics(y_test, deep_test_pred)
print('Métricas test NN profunda:', deep_metrics)
Métricas test NN profunda: {'MSE': 0.2828019704740204, 'RMSE': np.float64(0.5317912846916734), 'MAE': 0.3517370297639561, 'R2': 0.7842042865113199}
8) Comparativa global de métricas
results = pd.DataFrame([
{'Modelo': 'SGDRegressor (lineal)', **sgd_metrics},
{'Modelo': 'Random Forest', **rf_metrics},
{'Modelo': 'Gradient Boosting', **gbr_metrics},
{'Modelo': 'NN simple (PyTorch)', **simple_metrics},
{'Modelo': 'NN profunda (PyTorch)', **deep_metrics},
]).sort_values('RMSE')
results
| Modelo | MSE | RMSE | MAE | R2 | |
|---|---|---|---|---|---|
| 1 | Random Forest | 0.263624 | 0.513443 | 0.335178 | 0.798839 |
| 2 | Gradient Boosting | 0.277516 | 0.526798 | 0.363544 | 0.788237 |
| 4 | NN profunda (PyTorch) | 0.282802 | 0.531791 | 0.351737 | 0.784204 |
| 3 | NN simple (PyTorch) | 0.320110 | 0.565782 | 0.390393 | 0.755736 |
| 0 | SGDRegressor (lineal) | 0.545629 | 0.738667 | 0.538182 | 0.583651 |
# Visualización comparativa de RMSE, MAE y R2
fig, axes = plt.subplots(1, 3, figsize=(18, 4.5))
sns.barplot(data=results, x='RMSE', y='Modelo', ax=axes[0], palette='Blues_r')
axes[0].set_title('Comparación RMSE (menor es mejor)')
sns.barplot(data=results, x='MAE', y='Modelo', ax=axes[1], palette='Greens_r')
axes[1].set_title('Comparación MAE (menor es mejor)')
sns.barplot(data=results, x='R2', y='Modelo', ax=axes[2], palette='Purples')
axes[2].set_title('Comparación R² (mayor es mejor)')
plt.tight_layout()
plt.show()
9) Diagnóstico visual del mejor modelo
Tomaremos el modelo con menor RMSE y analizaremos:
- predicción vs valor real,
- distribución de residuales,
- posibles sesgos sistemáticos.
# Selección automática del mejor modelo por RMSE
best_model_name = results.iloc[0]['Modelo']
print('Mejor modelo según RMSE:', best_model_name)
pred_map = {
'SGDRegressor (lineal)': sgd_test_pred,
'Random Forest': rf_test_pred,
'Gradient Boosting': gbr_test_pred,
'NN simple (PyTorch)': simple_test_pred,
'NN profunda (PyTorch)': deep_test_pred,
}
best_pred = pred_map[best_model_name]
residuals = y_test - best_pred
fig, axes = plt.subplots(1, 3, figsize=(17, 4.5))
# Real vs predicho
axes[0].scatter(y_test, best_pred, alpha=0.5)
min_v, max_v = y_test.min(), y_test.max()
axes[0].plot([min_v, max_v], [min_v, max_v], 'r--')
axes[0].set_xlabel('Valor real')
axes[0].set_ylabel('Predicción')
axes[0].set_title(f'Real vs predicho ({best_model_name})')
# Histograma de residuales
sns.histplot(residuals, bins=40, kde=True, ax=axes[1])
axes[1].set_title('Distribución de residuales')
axes[1].set_xlabel('Residual (y_real - y_pred)')
# Residuales vs predicción
axes[2].scatter(best_pred, residuals, alpha=0.4)
axes[2].axhline(0, color='red', linestyle='--')
axes[2].set_xlabel('Predicción')
axes[2].set_ylabel('Residual')
axes[2].set_title('Residual vs predicción')
plt.tight_layout()
plt.show()
Mejor modelo según RMSE: Random Forest
10) Mini-bloque de pruebas rápidas (sanity checks)
Estas comprobaciones ayudan al alumno a validar que el experimento tiene sentido:
- métricas en rangos razonables,
- predicciones finitas,
- comparación coherente entre modelos.
# Tests simples de coherencia
assert all(np.isfinite(results['RMSE'])), 'Hay RMSE no finitos'
assert all(np.isfinite(results['MAE'])), 'Hay MAE no finitos'
assert all(np.isfinite(results['R2'])), 'Hay R2 no finitos'
# El modelo lineal suele ser competitivo pero normalmente peor que ensambles en este dataset
print('RMSE lineal:', float(results[results['Modelo']=='SGDRegressor (lineal)']['RMSE']))
print('RMSE mejor modelo:', float(results.iloc[0]['RMSE']))
print('✅ Sanity checks completados correctamente')
RMSE lineal: 0.7386668997345011 RMSE mejor modelo: 0.513442928847261 ✅ Sanity checks completados correctamente
Conclusiones y siguientes pasos
Conclusiones principales
- En regresión tabular con no linealidades, los ensambles de árboles (Random Forest / Gradient Boosting) suelen ser baselines muy fuertes.
- El modelo lineal (SGD) aporta una referencia clara: si queda lejos, indica que la relación entrada-salida no es principalmente lineal.
- Las redes neuronales pueden igualar o superar a modelos clásicos, pero requieren más ajuste (arquitectura, regularización, learning rate, epochs).
- Las curvas train/val son clave para interpretar el proceso: ayudan a detectar sobreajuste y a decidir cuándo parar.
Qué podrías probar ahora
- Búsqueda de hiperparámetros (grid/random/bayesiana).
- Early stopping para la red profunda.
- Transformaciones de variables (por ejemplo log en algunas features).
- Modelos adicionales: XGBoost/LightGBM/CatBoost.
- Ensambles híbridos (stacking entre árboles y redes).
- Validación cruzada K-fold para mayor robustez estadística.
Idea final: no existe un “mejor modelo universal”. Lo correcto es comparar con método, entender el problema y elegir según rendimiento + coste + interpretabilidad.