🏭 Caso de Uso

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.

🐍 Python 📓 Jupyter Notebook

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:

  1. Modelo lineal entrenado con gradiente (SGDRegressor).
  2. Random Forest Regressor (ensamble de árboles por bagging).
  3. Gradient Boosting Regressor (ensamble secuencial por boosting).
  4. Red neuronal simple (pocas capas/parámetros).
  5. 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.
  • , 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_housing de 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.

[1]
# 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.).

[2]
# 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
[3]
# 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
[4]
# 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
[5]
# 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()
Output
[6]
# 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()
Output
[7]
# 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()
Output

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.

[8]
# 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

[9]
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.

[10]
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)
Output
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).

[11]
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)
Output
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.

[12]
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)
Output
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.

[13]
# 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
[14]
# 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)
[15]
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

[16]
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)
Output
Output
Métricas test NN simple: {'MSE': 0.3201096736787496, 'RMSE': np.float64(0.5657823553971524), 'MAE': 0.3903933918560722, 'R2': 0.755736159439242}

7.2 Red neuronal profunda

[17]
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)
Output
Output
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

[18]
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
[19]
# 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()
Output

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.
[20]
# 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
Output

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.
[21]
# 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

  1. En regresión tabular con no linealidades, los ensambles de árboles (Random Forest / Gradient Boosting) suelen ser baselines muy fuertes.
  2. El modelo lineal (SGD) aporta una referencia clara: si queda lejos, indica que la relación entrada-salida no es principalmente lineal.
  3. Las redes neuronales pueden igualar o superar a modelos clásicos, pero requieren más ajuste (arquitectura, regularización, learning rate, epochs).
  4. 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.