Clase 5 · Regresión lineal y regularización

#@title Estilo de la clase (ejecutar, no hace falta leer) {display-mode: "form"}
from IPython.display import HTML, display
display(HTML(r'''
<style>
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;600&family=Amiri:wght@400;700&display=swap');
.rendered_html, .markdown, .cell .text_cell_render { font-family:'Work Sans',system-ui,sans-serif; color:#122535; }
.rendered_html h1,.rendered_html h2,.rendered_html h3 { font-family:'Amiri',Georgia,serif; color:#00529B; }
.rendered_html h2 { border-bottom:2px solid #00529B; padding-bottom:.2em; }
.rendered_html a { color:#00529B; }
.rendered_html table th { background:#00529B; color:#fff; }
.rendered_html h1,.rendered_html h2,.rendered_html h3 { scroll-margin-top:16px; }
</style>
'''))

Analítica de Datos · Maestría en Ciencias del Comportamiento · Universidad de San Andrés

Primavera 2026 · 05/09/2026

Abrir en Colab

La pregunta de hoy es ¿cuánto bienestar laboral podemos predecir a partir del salario?, y de ahí en adelante, ¿cuánto mejora si usamos más variables?

La idea que queremos ver:

El modelo que mejor ajusta los datos que ya viste (entrenamiento) no es el que mejor predice los que vienen (testeo). En la Clase 4 el parámetro era K, la cantidad de vecinos. Hoy es la cantidad de variables que le metemos al modelo.

# Paso Qué hacemos
1 Armar la tabla unir nimbus_clima con nimbus_salario
2 La recta y el RSS mover β₀ y β₁ a mano y mirar los residuos
3 Mínimos cuadrados ✏️ programar la fórmula, sin sklearn
4 Medir el ajuste ✏️ RSE y R², a mano y después con sklearn
5 El número honesto ✏️ tu propio modelo, medido en entrenamiento y testeo
6 Regularización ✏️ Ridge y Lasso cuando sobran variables

Cómo se usa. Las celdas se corren con Shift+Enter, de arriba hacia abajo. Si te salteás una, las de abajo pueden fallar. Las cuatro consignas tienen huecos para completar. Buscá los espacios ___ o “___”

Ojo con los dos “bienestar”. El de hoy es bienestar_laboral, un índice de 0 a 100 de la encuesta de clima 2026. No es el bienestar diario del piloto de la fruta (escala 1 a 7) de las clases pasadas. Son dos variables distintas.

1. Armar la tabla

Dos tablas. clima tiene la encuesta de este año, una fila por empleado, con el índice de bienestar y 19 variables más. salario es el panel de sueldos, con una fila por empleado y por año, así que nos quedamos con 2025, que es el sueldo que tenían cuando contestaron.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

BASE = "https://raw.githubusercontent.com/tomdamelio/analitica_de_datos_alumnos/main/data/toy-nimbus/"

clima = pd.read_csv(BASE + "nimbus_clima.csv")
salario = pd.read_csv(BASE + "nimbus_salario.csv")

salario_2025 = salario[salario["anio"] == 2025][["empleado_id", "salario_mensual"]]
datos = clima.merge(salario_2025, on="empleado_id")

# El salario en millones deja los números en una escala cómoda de leer.
datos["salario"] = datos["salario_mensual"] / 1_000_000

# Las dos variables sueltas: se usan en casi todas las celdas de acá en adelante.
x = datos["salario"].to_numpy()
y = datos["bienestar_laboral"].to_numpy()

def dibujar_nube(ax):
    """Dibuja la nube de puntos con los ejes ya nombrados."""
    ax.scatter(x, y, s=14, alpha=0.35, color="#33404a")
    ax.set_xlabel("Salario mensual (millones de $)")
    ax.set_ylabel("Bienestar laboral (0-100)")
    ax.set_ylim(0, 100)

print(f"{len(datos)} empleados")
print(f"correlación salario / bienestar: {datos['salario'].corr(datos['bienestar_laboral']):.3f}")
datos[["empleado_id", "salario", "bienestar_laboral"]].head(3)
600 empleados
correlación salario / bienestar: 0.572
empleado_id salario bienestar_laboral
0 1 1.301 66.2
1 2 1.329 56.8
2 3 1.255 54.8

Cada punto es una persona: su sueldo en el eje horizontal, lo que contestó en la encuesta en el vertical. Dos cosas que vuelven más adelante. La nube sube, y eso es lo que va a capturar una recta. Y es ancha: para un mismo sueldo hay gente con 40 y gente con 80 de bienestar.

fig, ax = plt.subplots(figsize=(7, 4.5))
dibujar_nube(ax)
ax.set_title("Nimbus: 600 empleados")
plt.tight_layout()
plt.show()

2. La recta y el RSS

Una recta son dos números: la ordenada al origen β₀ y la pendiente β₁.

\[\hat{y} = \beta_0 + \beta_1 x\]

El residuo de una persona es lo que la recta que ajustamos le erra al valor real (el valor real menos nuestra predicción), y el RSS es la suma de todos esos residuos al cuadrado. Es un solo número que dice qué tan mala es una recta: cuanto más chico, mejor.

\[e_i = y_i - \hat{y}_i \qquad \text{RSS} = \sum_{i=1}^{n} e_i^2\]

Elevar al cuadrado no es un capricho. Si los sumaras tal cual, los errores con signo positivo y los negativos se cancelarían, y una recta muy mala podría dar RSS cero.

Jugemos un poco. Abajo hay un gráfico interactivo. Tratá de bajar el RSS lo más que puedas moviendo los sliders, y anotá el mejor valor que consigas junto con los dos β del título. Los vamos a comparar contra la solución exacta.

Un detalle de construcción: el slider dice “altura”, que es dónde queda la recta en el salario promedio, y no β₀. Como nadie cobra cero, mover β₀ directo correría la recta fuera del gráfico en vez de girarla. β₀ se despeja de la altura y es el que ves en el título.

from ipywidgets import interact, FloatSlider

media_x = x.mean()

def rss(b0, b1):
    """Suma de los residuos al cuadrado de la recta b0 + b1*x."""
    return float(((y - (b0 + b1 * x)) ** 2).sum())

def recta_a_ojo(altura=65.0, b1=45.0):
    """La recta que definen altura y b1, con sus residuos y su RSS."""
    b0 = altura - b1 * media_x
    fig, ax = plt.subplots(figsize=(7, 4.5))
    ax.vlines(x, y, b0 + b1 * x, color="#B4232E", lw=0.5, alpha=0.5)
    dibujar_nube(ax)
    grilla = np.linspace(x.min(), x.max(), 100)
    ax.plot(grilla, b0 + b1 * grilla, color="#00529B", lw=2.5)
    ax.set_title(f"β₀ = {b0:.1f}   β₁ = {b1:.1f}   RSS = {rss(b0, b1):,.0f}")
    plt.tight_layout()
    plt.show()

interact(
    recta_a_ojo,
    altura=FloatSlider(value=65, min=0, max=100, step=1, description="altura"),
    b1=FloatSlider(value=45, min=-40, max=140, step=1, description="β₁"),
);

Vas a notar dos cosas. Que se mejora bastante rápido al principio, y que después te trabás: movés un slider y empeora, movés el otro y empeora, y no sabés para dónde seguir. En la próxima sección vamos a ver como lidiamos con esta optimización efectivamente.

3. Parámetros de la recta, a mano

Buscar a ojo el RSS más chico es lento y no tiene garantía. Para este problema hay una fórmula exacta:

\[\hat{\beta}_1 = \frac{\sum_i (x_i - \bar{x})(y_i - \bar{y})}{\sum_i (x_i - \bar{x})^2} \qquad \hat{\beta}_0 = \bar{y} - \hat{\beta}_1 \bar{x}\]

donde \(\bar{x}\) y \(\bar{y}\) son los promedios. \(\sum_i\) es la suma de todo un conjunto de valores, en este caso recorriendo \(i\). La parte de arriba de la fracción de \(\hat{\beta}_1\) da cuenta de que tanto covarían ambas variables. El denominador es cuánto se dispersan los salarios, y es lo que define la escala de la pendiente.

La segunda fórmula es cómo se obtiene la ordenada al origen a partir de los promedios y de la pendiente que ya tenemos.

#@title ✏️ Consigna 1 {display-mode: "form"}
from IPython.display import HTML, display
display(HTML('''
<div style="background-color:#d4edda;border:1px solid #c3e6cb;color:#155724;border-radius:4px;padding:0.75rem 1.25rem;">
    <b>✏️ Consigna 1</b>:
    <ul>
    <li>Completá <code>minimos_cuadrados(x, y)</code> con las dos fórmulas de arriba.</li>
    </ul>
    <br>
    <i>Tips</i>:
    <ul>
    <li><i>Usá los promedios <code>mx</code> y <code>my</code>, ya calculados en la primera línea.</i></li>
    <li><i>β₀ se despeja de <code>my = b0 + b1 * mx</code>, la recta pasando por (mx, my).</i></li>
    </ul>
</div>
'''))
✏️ Consigna 1:
  • Completá minimos_cuadrados(x, y) con las dos fórmulas de arriba.

Tips:
  • Usá los promedios mx y my, ya calculados en la primera línea.
  • β₀ se despeja de my = b0 + b1 * mx, la recta pasando por (mx, my).

En la sección 4 comparamos este resultado contra sklearn: si ahí coinciden, programaste bien.

def minimos_cuadrados(x, y):
    """Devuelve (b0, b1) de la recta que minimiza el RSS.

    Son las ecuaciones (3.4) de James y otros, capítulo 3.
    """
    mx, my = x.mean(), y.mean()
    # TODO: arriba, cuánto se aparta cada uno de SU promedio; abajo, sólo el de x
    b1 = ((x - mx) * (y - ___)).sum() / ((x - ___) ** 2).sum()
    # TODO: la recta pasa por (mx, my). Despejá b0 de my = b0 + b1 * mx
    b0 = my - ___ * mx
    return float(b0), float(b1)

b0_ols, b1_ols = minimos_cuadrados(x, y)
rss_ols = rss(b0_ols, b1_ols)

print(f"β₀ = {b0_ols:.2f}")
print(f"β₁ = {b1_ols:.2f}")
print(f"RSS = {rss_ols:,.0f}")

Poné abajo los dos valores que habías conseguido a ojo. La celda dibuja tu recta contra la exacta y compara los dos RSS.

#@title Tu recta contra la de mínimos cuadrados {display-mode: "form"}
b0_a_ojo = 0.0   #@param {type:"number"}
b1_a_ojo = 45.0  #@param {type:"number"}

fig, ax = plt.subplots(figsize=(7.5, 4.5))
dibujar_nube(ax)
grilla = np.linspace(x.min(), x.max(), 100)
ax.plot(grilla, b0_a_ojo + b1_a_ojo * grilla, color="#C8622A", lw=2.5, ls="--",
        label=f"a ojo: RSS = {rss(b0_a_ojo, b1_a_ojo):,.0f}")
ax.plot(grilla, b0_ols + b1_ols * grilla, color="#00529B", lw=2.5,
        label=f"mínimos cuadrados: RSS = {rss_ols:,.0f}")
ax.legend(loc="lower right")
ax.set_title("La recta elegida a ojo contra la exacta")
plt.tight_layout()
plt.show()

4. Medir el ajuste: RSE y R²

El RSS por sí solo no se puede leer, porque al ser una suma el tamaño va a depender no solo de la magnitud del error, sino también de cuánta gente o datos individuales haya. Dos medidas son de especial ayuda.

  1. El RSE es el error típico, en las unidades de la respuesta.
  2. El compara que tanto mejor es nuestro modelo contra un modelo que predecir siempre el promedio. Este modelo tiene un RSS propio, el TSS (la suma de los residuos si predecimos siempre el promedio).

\[\text{RSE} = \sqrt{\frac{\text{RSS}}{n - 2}} \qquad R^2 = 1 - \frac{\text{RSS}}{\text{TSS}} \qquad \text{TSS} = \sum_i (y_i - \bar{y})^2\]

#@title ✏️ Consigna 2 {display-mode: "form"}
from IPython.display import HTML, display
display(HTML('''
<div style="background-color:#d4edda;border:1px solid #c3e6cb;color:#155724;border-radius:4px;padding:0.75rem 1.25rem;">
    <b>✏️ Consigna 2</b>:
    <ul>
    <li>Calculá el RSE y el R² de la recta de mínimos cuadrados.</li>
    </ul>
    <br>
    <i>Tips</i>:
    <ul>
    <li><i>El denominador del RSE no es <code>n</code>: es <code>n</code> menos la cantidad de parámetros que estimamos (β₀ y β₁).</i></li>
    <li><i>El R² es <code>1 - RSS/TSS</code>. El TSS ya está calculado.</i></li>
    </ul>
</div>
'''))
✏️ Consigna 2:
  • Calculá el RSE y el R² de la recta de mínimos cuadrados.

Tips:
  • El denominador del RSE no es n: es n menos la cantidad de parámetros que estimamos (β₀ y β₁).
  • El R² es 1 - RSS/TSS. El TSS ya está calculado.
n = len(y)
tss = float(((y - y.mean()) ** 2).sum())

# TODO: ¿cuántos parámetros estimamos en una regresión simple?
rse = np.sqrt(rss_ols / (n - ___))
# TODO: el R² compara el error de nuestra recta contra el del modelo del promedio
r2 = 1 - ___ / tss

print(f"TSS = {tss:,.0f}")
print(f"RSE = {rse:.2f} puntos de bienestar")
print(f"R²  = {r2:.3f}")

Nueve puntos de error sobre un índice que va de 11 a 97, y un tercio de la variación explicada.

Ese 0,33 es un resultado razonable en ciencias del comportamiento, no uno malo. Pensalo, estamos prediciendo cómo se siente alguien en su trabajo a partir de un solo número. Todo lo demás que influye no está en la tabla y va al residuo. Un R² de 0,9 en datos de personas suele ser señal de que algo está mal (recordá lo que dijimos la clase pasada: Si el modelo da demasiado bien, quizás hay que preocuparse).

Lo mismo con sklearn

Ahora en tres líneas. Ya entendimos la lógica, y ahora sklearn nos facilita el código. Verifiquemos que da exactamente lo mismo.

from sklearn.linear_model import LinearRegression

X = datos[["salario"]]            # sklearn espera una tabla, no un vector
modelo = LinearRegression().fit(X, y)

print(f"sklearn  b0 = {modelo.intercept_:.4f}   b1 = {modelo.coef_[0]:.4f}   R2 = {modelo.score(X, y):.4f}")
print(f"a mano   b0 = {b0_ols:.4f}   b1 = {b1_ols:.4f}   R2 = {r2:.4f}")
sklearn  b0 = -31.0448   b1 = 70.2997   R2 = 0.3274
a mano   b0 = -31.0448   b1 = 70.2997   R2 = 0.3274

5. El número honesto: varias variables, entrenamiento y testeo

La encuesta tiene 19 predictores además del salario. Estos son los candidatos, cada uno solo, con su R².

CANDIDATAS = ["salario", "apoyo_equipo", "reconocimiento", "autonomia",
              "horas_extra_semana", "dias_home_office", "bono_anual_pct",
              "reuniones_semana", "mensajes_chat_dia", "puntualidad_pct",
              "horas_capacitacion", "distancia_oficina_km"]

def r2_de(columnas, tabla=None):
    """R² del modelo que usa esas columnas."""
    tabla = datos if tabla is None else tabla
    objetivo = tabla["bienestar_laboral"]
    return LinearRegression().fit(tabla[columnas], objetivo).score(tabla[columnas], objetivo)

solas = pd.Series({c: r2_de([c]) for c in CANDIDATAS}).sort_values(ascending=False)
solas.round(3)
salario                 0.327
bono_anual_pct          0.306
apoyo_equipo            0.160
horas_extra_semana      0.082
reconocimiento          0.076
autonomia               0.007
horas_capacitacion      0.006
reuniones_semana        0.004
dias_home_office        0.002
puntualidad_pct         0.001
mensajes_chat_dia       0.000
distancia_oficina_km    0.000
dtype: float64

Hay tres que explican algo y el resto está cerca de cero. Dos puntos entrelazados antes de seguir:

Los R² no se suman. Salario, apoyo del equipo y reconocimiento explican 0,327 + 0,160 + 0,076 por separado, pero juntas no van a llegar a esa suma, porque la información que brindan se superpone. El que se siente apoyado por su equipo suele ser el mismo que se siente reconocido, así que ese pedazo de información se cuenta una sola vez.

Un coeficiente no se lee solo. El bono anual parece un predictor fuerte con su R² de 0,306, pero en Nimbus el bono es un porcentaje del sueldo. Su correlación con el salario es 0,966. Al lado del salario, su coeficiente se desploma de +1,845 a +0,010. Es casi la misma información, repetida.

tres = ["salario", "apoyo_equipo", "reconocimiento"]
print(f"sumando los tres R2 por separado:   {solas[tres].sum():.3f}")
print(f"el modelo con las tres juntas:      {r2_de(tres):.3f}")

solo_bono = LinearRegression().fit(datos[["bono_anual_pct"]], y)
con_salario = LinearRegression().fit(datos[["bono_anual_pct", "salario"]], y)
print(f"\nbeta del bono, solo:            {solo_bono.coef_[0]:+.3f}")
print(f"beta del bono, con el salario:  {con_salario.coef_[0]:+.3f}")
print(f"correlación bono / salario:     {datos['bono_anual_pct'].corr(datos['salario']):.3f}")
sumando los tres R2 por separado:   0.564
el modelo con las tres juntas:      0.522

beta del bono, solo:            +1.845
beta del bono, con el salario:  +0.010
correlación bono / salario:     0.966

El problema de medir sobre los mismos datos

Todo lo anterior lo medimos sobre las mismas 600 personas con las que armamos el modelo. Es como hacer un examen con el machete a la vista.

La solución es la de la Clase 4: apartamos un pedazo antes de empezar, no lo vemos, y recién al final hacemos una prueba ahí.

#@title ✏️ Consigna 3 {display-mode: "form"}
from IPython.display import HTML, display
display(HTML('''
<div style="background-color:#d4edda;border:1px solid #c3e6cb;color:#155724;border-radius:4px;padding:0.75rem 1.25rem;">
    <b>✏️ Consigna 3</b>:
    <ul>
    <li>Armá tu modelo: elegí al menos tres variables de <code>CANDIDATAS</code> en <code>mis_variables</code>.</li>
    <li>Partí la tabla dejando el 30% para testeo.</li>
    <li>Evaluá tu modelo en los dos lados.</li>
    </ul>
    <br>
    <i>Tips</i>:
    <ul>
    <li><i>Usá <code>train_test_split(datos, test_size=..., random_state=42)</code>.</i></li>
    </ul>
</div>
'''))
✏️ Consigna 3:
  • Armá tu modelo: elegí al menos tres variables de CANDIDATAS en mis_variables.
  • Partí la tabla dejando el 30% para testeo.
  • Evaluá tu modelo en los dos lados.

Tips:
  • Usá train_test_split(datos, test_size=..., random_state=42).

No hace falta que busques el mejor de todos. Buscá uno que te parezca razonable. random_state=42 es para que a todos les dé lo mismo y podamos comparar en clase.

from sklearn.model_selection import train_test_split

# TODO: poné acá las variables que elegiste. Al menos tres, todas de CANDIDATAS.
mis_variables = ["___", "___", "___"]

# TODO: qué proporción va a testeo
entrenamiento, testeo = train_test_split(datos, test_size=___, random_state=42)

mio = LinearRegression().fit(entrenamiento[mis_variables], entrenamiento["bienestar_laboral"])
r2_train_mio = mio.score(entrenamiento[mis_variables], entrenamiento["bienestar_laboral"])
# TODO: el número honesto se mide en el conjunto que el modelo NO vio
r2_test_mio = mio.score(___[mis_variables], ___["bienestar_laboral"])

print(f"tu modelo: {len(mis_variables)} variables")
print(f"  filas de entrenamiento: {len(entrenamiento)}   de testeo: {len(testeo)}")
print(f"  R2 en entrenamiento: {r2_train_mio:.3f}")
print(f"  R2 en testeo:        {r2_test_mio:.3f}")
print(f"  Diferencia:               {r2_train_mio - r2_test_mio:+.3f}")

Ahora los tres modelos sobre la misma partición: sólo el salario, el tuyo, y uno con todas las variables de la tabla. Mirá la última columna, la diferencia entre entrenamiento y testeo.

TODAS = [c for c in datos.columns
         if c not in ["empleado_id", "bienestar_laboral", "salario_mensual"]]

def evaluar(columnas):
    """Ajusta en entrenamiento y devuelve (R2 entrenamiento, R2 testeo)."""
    objetivo = entrenamiento["bienestar_laboral"]
    m = LinearRegression().fit(entrenamiento[columnas], objetivo)
    return (m.score(entrenamiento[columnas], objetivo),
            m.score(testeo[columnas], testeo["bienestar_laboral"]))

filas = []
for nombre, columnas in [("sólo el salario", ["salario"]),
                         ("el tuyo", mis_variables),
                         ("TODAS", TODAS)]:
    r2_train, r2_test = evaluar(columnas)
    filas.append({"modelo": nombre, "variables": len(columnas),
                  "train": r2_train, "test": r2_test, "diferencia": r2_train - r2_test})

pd.DataFrame(filas).set_index("modelo").round(3)
variables train test diferencia
modelo
sólo el salario 1 0.346 0.267 0.078
el tuyo 6 0.631 0.586 0.045
TODAS 19 0.641 0.557 0.084

Ahí está la clase entera en una tabla. Con todas las variables el R² de entrenamiento es el más alto de los tres, y también el de la diferencia más grande. Básicamente, es el que más podría engañarnos de que funciona. Si elegiste bien las variables, tu modelo le gana en testeo usando la tercera parte.

Agregar variables siempre mejora el número de entrenamiento. Hay que mirar el otro.

6. Regularización: penalizar en vez de descartar

Elegir variables a mano es lento y obliga a decidir de a una si cada variable entra o sale. Cuando hay pocos datos para muchas variables, hay una opción complementaria. Podemos dejar las variables dentro del modelo, y penalizar los coeficientes para que el modelo no se entusiasme con ninguna.

\[\text{Ridge:}\ \ \text{RSS} + \lambda \sum_j \beta_j^2 \qquad \text{Lasso:}\ \ \text{RSS} + \lambda \sum_j |\beta_j|\]

#@title ✏️ Consigna 4 {display-mode: "form"}
from IPython.display import HTML, display
display(HTML('''
<div style="background-color:#d4edda;border:1px solid #c3e6cb;color:#155724;border-radius:4px;padding:0.75rem 1.25rem;">
    <b>✏️ Consigna 4</b>:
    <ul>
    <li>Entrená los tres modelos (mínimos cuadrados, Ridge y Lasso) con sólo 30 personas.</li>
    <li>Compará su R² sobre el resto de los datos.</li>
    </ul>
    <br>
    <i>Tips</i>:
    <ul>
    <li><i>Usá <code>train_size=30</code> al partir los datos.</i></li>
    <li><i>Para Ridge usá <code>alpha=10</code>; para Lasso, <code>alpha=0.5</code>.</i></li>
    </ul>
</div>
'''))
✏️ Consigna 4:
  • Entrená los tres modelos (mínimos cuadrados, Ridge y Lasso) con sólo 30 personas.
  • Compará su R² sobre el resto de los datos.

Tips:
  • Usá train_size=30 al partir los datos.
  • Para Ridge usá alpha=10; para Lasso, alpha=0.5.

alpha es el λ de la cuenta de arriba. El StandardScaler del pipeline es lo que estandariza antes de penalizar.

from sklearn.linear_model import Lasso, Ridge
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

# TODO: cuántas personas usamos para entrenar
chico, resto = train_test_split(datos, train_size=___, random_state=11)

# TODO: alpha es el λ de la cuenta de arriba. Probá 10 para Ridge y 0.5 para Lasso.
recetas = {"mínimos cuadrados": LinearRegression(),
           "Ridge": Ridge(alpha=___),
           "Lasso": Lasso(alpha=___, max_iter=50_000)}

for nombre, receta in recetas.items():
    m = make_pipeline(StandardScaler(), receta).fit(chico[TODAS], chico["bienestar_laboral"])
    print(f"{nombre:>18}: R2 de testeo = {m.score(resto[TODAS], resto['bienestar_laboral']):+.3f}")

Con 30 personas y 19 variables, mínimos cuadrados da un R² negativo, lo que quiere decir que predice peor que decir el promedio. Ridge y Lasso, con exactamente los mismos 30, siguen funcionando.

Hoja de referencia

Qué Fórmula En Python
Residuo \(e_i = y_i - \hat{y}_i\) y - modelo.predict(X)
RSS \(\sum_i e_i^2\) ((y - pred) ** 2).sum()
RSE \(\sqrt{\text{RSS}/(n-2)}\) np.sqrt(rss / (n - 2))
\(1 - \text{RSS}/\text{TSS}\) modelo.score(X, y)
Ajustar LinearRegression().fit(X, y)
Partir train_test_split(datos, test_size=0.3, random_state=42)
Ridge RSS \(+ \lambda \sum \beta_j^2\) Ridge(alpha=10)
Lasso RSS \(+ \lambda \sum \lvert\beta_j\rvert\) Lasso(alpha=0.5)

Antes de regularizar, siempre StandardScaler. Si no, la variable que viene en millones y la que viene del 1 al 10 pagan penalizaciones que no son comparables.