31  Regresión y clasificadores lineales

Autor/a

Diego Villalba

Fecha de publicación

19 de mayo de 2026

El aprendizaje supervisado estudia problemas en los que se dispone de observaciones de entrada y de una variable objetivo asociada. A partir de un conjunto de entrenamiento, el objetivo es aprender una función capaz de predecir la respuesta de nuevas observaciones. En términos generales, se parte de datos de la forma \[ \mathcal{D}=\{(\mathbf{x}_i,y_i)\}_{i=1}^n, \] donde \(\mathbf{x}_i\in\mathbb{R}^p\) representa un vector de características y \(y_i\) representa la respuesta observada. Cuando \(y_i\) es una magnitud continua, el problema se formula como regresión; cuando \(y_i\) representa una clase discreta, se formula como clasificación (Hastie, Tibshirani, y Friedman 2009; bishop2006pattern?).

Las presentaciones base introducen dos familias de modelos deterministas centrales en aprendizaje supervisado: la regresión lineal, usada para predecir variables numéricas, y los clasificadores lineales, usados para separar clases mediante hiperplanos de decisión. En este capítulo se reorganizan y amplían esas ideas para construir una exposición autocontenida: primero se estudia la regresión lineal simple y múltiple; después se revisan métricas de evaluación para modelos de regresión; finalmente se introducen el discriminante lineal de Fisher y el perceptrón como ejemplos fundamentales de clasificadores lineales.

La importancia pedagógica de estos modelos no se limita a su simplicidad. La regresión lineal permite discutir estimación, residuos, error cuadrático, interpretación de coeficientes y problemas de colinealidad. Los clasificadores lineales permiten introducir funciones discriminantes, fronteras de decisión, separabilidad lineal, márgenes y aprendizaje iterativo. Muchas técnicas modernas, incluyendo redes neuronales profundas, máquinas de soporte vectorial y modelos generalizados, conservan ideas estructurales que aparecen por primera vez en estos modelos (Goodfellow, Bengio, y Courville 2016; vapnik1998statistical?).

1 Regresión: predicción de variables continuas

En un problema de regresión, la variable objetivo toma valores reales. Ejemplos típicos son el precio de una casa, la temperatura de una ciudad, el ingreso mensual de una persona o el número esperado de citas médicas. La tarea consiste en aprender una función \(f\) tal que \[ \widehat{y}=f(\mathbf{x}) \] sea una aproximación razonable del valor observado \(y\).

Definición. Un problema de regresión supervisada consiste en estimar una función \(f:\mathcal{X}\to\mathbb{R}\) a partir de un conjunto de pares entrada–salida \(\mathcal{D}=\{(\mathbf{x}_i,y_i)\}_{i=1}^n\), de modo que las predicciones \(\widehat{y}_i=f(\mathbf{x}_i)\) aproximen los valores observados \(y_i\): \[ \widehat{y}_i=f(\mathbf{x}_i),\qquad y_i\in\mathbb{R}. \tag{1}\]

La regresión puede abordarse con modelos de distinta complejidad. Entre los más frecuentes se encuentran la regresión lineal, la regresión polinómica, los árboles de regresión, los bosques aleatorios y los métodos de boosting como XGBoost (Breiman 2001; Chen y Guestrin 2016). Sin embargo, la regresión lineal ocupa un lugar especial porque es interpretable, tiene una solución analítica bajo ciertas condiciones y permite estudiar con claridad la relación entre variables explicativas y variable dependiente.

1.1 Regresión simple y regresión múltiple

La regresión lineal simple usa una sola variable explicativa. Su forma básica es \[ \widehat{y}=\beta_0+\beta_1 x, \] donde \(\beta_0\) es el intercepto y \(\beta_1\) es la pendiente. La pendiente indica el cambio esperado en \(y\) cuando \(x\) aumenta una unidad, manteniendo fijo todo lo demás. En el caso simple no hay más variables que controlar.

La regresión lineal múltiple generaliza esta idea a varias variables explicativas: \[ y=\beta_0+\beta_1x_1+\beta_2x_2+\cdots+\beta_px_p+\varepsilon. \] Aquí \(\varepsilon\) representa el término de error, es decir, la parte de la respuesta que no es explicada por la combinación lineal de variables. Esta formulación permite modelar situaciones como el precio de una casa a partir de su tamaño, número de habitaciones y antigüedad.

Definición. El modelo de regresión lineal múltiple expresa la respuesta \(y_i\) como una combinación lineal de \(p\) variables explicativas más un término de error: \[ y_i=\beta_0+\sum_{j=1}^{p}\beta_jx_{ij}+\varepsilon_i, \qquad i=1,\ldots,n. \tag{2}\]

Por ejemplo, si se desea predecir la nota final de un estudiante usando horas de estudio, asistencia y tareas entregadas, un modelo posible es \[ \widehat{\text{nota}}=20+5(\text{horas})+0.3(\text{asistencia})+2(\text{tareas}). \] Esta expresión no debe leerse como una ley universal, sino como un modelo estimado o propuesto. El coeficiente de horas, por ejemplo, sugiere que cada hora adicional de estudio se asocia con un aumento esperado de cinco puntos en la nota, manteniendo constantes la asistencia y las tareas.

Regresión lineal simple — explorador interactivo
Ajusta β₀ y β₁ y observa cómo cambian los residuos y las métricas en tiempo real

2 Representación matricial de la regresión lineal

La notación matricial simplifica la estimación y permite conectar la regresión lineal con álgebra lineal, optimización y aprendizaje automático. Para \(n\) observaciones y \(p\) variables explicativas, se define \[ \mathbf{y}=\begin{bmatrix}y_1\\y_2\\\vdots\\y_n\end{bmatrix}, \qquad \mathbf{X}=\begin{bmatrix} 1 & x_{11} & x_{12} & \cdots & x_{1p}\\ 1 & x_{21} & x_{22} & \cdots & x_{2p}\\ \vdots & \vdots & \vdots & \ddots & \vdots\\ 1 & x_{n1} & x_{n2} & \cdots & x_{np} \end{bmatrix}, \qquad \boldsymbol{\beta}=\begin{bmatrix}\beta_0\\\beta_1\\\vdots\\\beta_p\end{bmatrix}. \] La primera columna de unos permite incorporar el intercepto. Con esta notación, el modelo se escribe como \[ \mathbf{y}=\mathbf{X}\boldsymbol{\beta}+\boldsymbol{\varepsilon}. \]

Definición. La matriz de diseño \(\mathbf{X}\in\mathbb{R}^{n\times(p+1)}\) contiene una fila por observación y una columna por término del modelo, incluyendo una columna inicial de unos para el intercepto: \[ \mathbf{y}=\mathbf{X}\boldsymbol{\beta}+\boldsymbol{\varepsilon}. \tag{3}\]

La estimación por mínimos cuadrados ordinarios busca el vector \(\boldsymbol{\beta}\) que minimiza la suma de errores cuadráticos entre los valores observados y los valores predichos (legendre1805nouvelles?; gauss1809theoria?): \[ \min_{\boldsymbol{\beta}}\;\|\mathbf{y}-\mathbf{X}\boldsymbol{\beta}\|_2^2. \] Si \(\mathbf{X}^\top\mathbf{X}\) es invertible, la solución analítica es \[ \widehat{\boldsymbol{\beta}}=(\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}. \]

Definición. El estimador de mínimos cuadrados ordinarios es el vector de coeficientes que minimiza la norma cuadrática de los residuos: \[ \widehat{\boldsymbol{\beta}} =\arg\min_{\boldsymbol{\beta}}\|\mathbf{y}-\mathbf{X}\boldsymbol{\beta}\|_2^2. \tag{4}\]

Una vez estimado el modelo, los valores predichos y los residuos se calculan como \[ \widehat{\mathbf{y}}=\mathbf{X}\widehat{\boldsymbol{\beta}}, \qquad \widehat{\boldsymbol{\varepsilon}}=\mathbf{y}-\widehat{\mathbf{y}}. \] Los residuos son esenciales para evaluar el ajuste: un buen modelo no solo tiene errores pequeños, sino errores sin patrones sistemáticos evidentes.

Mostrar código
import numpy as np
import plotly.graph_objects as go

# Datos del explorador interactivo reutilizados
XD = np.array([1,2,3,4,5,6,7,8,9,10], dtype=float)
YD = np.array([2.1,4.3,5.8,7.9,10.2,11.8,14.0,16.1,17.9,20.2])
N = len(XD)

# Solución OLS
xbar, ybar = XD.mean(), YD.mean()
b1_opt = np.dot(XD - xbar, YD - ybar) / np.dot(XD - xbar, XD - xbar)
b0_opt = ybar - b1_opt * xbar

# Grid de parámetros
b0_vals = np.linspace(b0_opt - 4, b0_opt + 4, 60)
b1_vals = np.linspace(b1_opt - 0.8, b1_opt + 0.8, 60)
B0, B1 = np.meshgrid(b0_vals, b1_vals)

# SCE en cada punto del grid
Yhat = B0[:, :, None] + B1[:, :, None] * XD[None, None, :]
SSE = np.sum((YD[None, None, :] - Yhat) ** 2, axis=2)

fig = go.Figure()

fig.add_trace(go.Surface(
    x=B0, y=B1, z=SSE,
    colorscale="Blues",
    opacity=0.82,
    colorbar=dict(title="SCE"),
    showscale=True,
    hovertemplate="β₀=%{x:.2f}<br>β₁=%{y:.2f}<br>SCE=%{z:.1f}<extra></extra>"
))

# Mínimo
sse_opt = float(np.sum((YD - (b0_opt + b1_opt * XD))**2))
fig.add_trace(go.Scatter3d(
    x=[b0_opt], y=[b1_opt], z=[sse_opt],
    mode="markers+text",
    marker=dict(size=8, color="#dc2626", symbol="diamond"),
    text=["OLS óptimo"],
    textposition="top center",
    textfont=dict(color="#dc2626", size=12),
    name="Mínimo OLS"
))

fig.update_layout(
    title="Superficie de pérdida SCE(β₀, β₁)",
    scene=dict(
        xaxis_title="β₀ (intercepto)",
        yaxis_title="β₁ (pendiente)",
        zaxis_title="SCE",
        camera=dict(eye=dict(x=1.6, y=-1.6, z=1.0))
    ),
    height=500,
    template="plotly_white"
)
fig.show()
Figura 1: Superficie de la función de pérdida SCE(β₀, β₁) para regresión simple. El mínimo global (marcado en rojo) corresponde a la solución OLS. La superficie tiene forma de paraboloide, lo que garantiza un único mínimo global.

2.1 Condición de invertibilidad y colinealidad

La fórmula cerrada de mínimos cuadrados requiere que \(\mathbf{X}^\top\mathbf{X}\) sea invertible. Esto ocurre cuando las columnas de \(\mathbf{X}\) son linealmente independientes. Si una variable explicativa puede escribirse como combinación lineal exacta de otras, la matriz pierde rango y la solución deja de ser única. Este problema se conoce como colinealidad perfecta. En la práctica, también puede aparecer multicolinealidad aproximada, que no impide calcular una solución pero puede hacer que los coeficientes sean inestables.

Cuando \(\mathbf{X}^\top\mathbf{X}\) no es invertible, existen varias estrategias: eliminar variables redundantes, transformar variables, usar la pseudoinversa de Moore–Penrose o introducir regularización, como en regresión ridge (hoerl1970ridge?; golub2013matrix?).

3 Ejemplo práctico: regresión lineal múltiple en Python

Consideremos el ejemplo de estudiantes con tres variables explicativas: horas de estudio, porcentaje de asistencia y número de tareas entregadas. El objetivo es predecir la nota final.

import numpy as np
import pandas as pd

# Datos de ejemplo
datos = pd.DataFrame({
    "horas": [2, 4, 6, 8],
    "asistencia": [60, 70, 80, 90],
    "tareas": [4, 5, 6, 7],
    "nota": [56, 71, 86, 101]
})

X = datos[["horas", "asistencia", "tareas"]].to_numpy(dtype=float)
y = datos["nota"].to_numpy(dtype=float)

# Agregamos la columna de unos para el intercepto
X_design = np.column_stack([np.ones(len(X)), X])

# Estimación por mínimos cuadrados usando la pseudoinversa
beta_hat = np.linalg.pinv(X_design) @ y
beta_hat
array([-0.30737122,  3.23062328,  0.78455555,  0.69319799])

El uso de np.linalg.pinv es deliberado: en ejemplos pequeños o con variables altamente correlacionadas, la pseudoinversa es más estable que invertir directamente \(\mathbf{X}^\top\mathbf{X}\). Para obtener predicciones y residuos:

y_hat = X_design @ beta_hat
residuos = y - y_hat

resultado = datos.copy()
resultado["prediccion"] = y_hat
resultado["residuo"] = residuos
resultado
horas asistencia tareas nota prediccion residuo
0 2 60 4 56 56.0 0.0
1 4 70 5 71 71.0 0.0
2 6 80 6 86 86.0 0.0
3 8 90 7 101 101.0 0.0

En este ejemplo, las variables están construidas de modo que el ajuste puede ser prácticamente exacto. En datos reales, esto rara vez ocurre. Un error de entrenamiento igual a cero puede ser señal de una relación determinista, de un conjunto demasiado pequeño o de sobreajuste.

4 Métricas de evaluación para regresión

Evaluar un modelo de regresión implica comparar valores observados \(y_i\) con predicciones \(\widehat{y}_i\). Las métricas más comunes resumen los residuos \(e_i=y_i-\widehat{y}_i\).

Definición. La suma de errores cuadráticos mide la discrepancia total entre observaciones y predicciones elevando cada residuo al cuadrado: \[ \mathrm{SSE}=\sum_{i=1}^{n}(y_i-\widehat{y}_i)^2. \tag{5}\]

El error cuadrático medio o MSE promedia la suma de errores cuadráticos: \[ \mathrm{MSE}=\frac{1}{n}\sum_{i=1}^{n}(y_i-\widehat{y}_i)^2. \] Su raíz cuadrada es el RMSE: \[ \mathrm{RMSE}=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\widehat{y}_i)^2}. \] El RMSE está en las mismas unidades que la variable objetivo y penaliza fuertemente los errores grandes debido al cuadrado.

Definición. El error absoluto medio mide la magnitud promedio del error sin considerar su signo: \[ \mathrm{MAE}=\frac{1}{n}\sum_{i=1}^{n}|y_i-\widehat{y}_i|. \tag{6}\]

El MAE suele ser más interpretable que el RMSE, porque indica cuánto se equivoca el modelo en promedio. Además, es menos sensible a errores extremos. En general se cumple que \(\mathrm{RMSE}\geq \mathrm{MAE}\); si la diferencia entre ambos es grande, esto sugiere que existen algunos errores de magnitud considerable.

Otra métrica importante es el coeficiente de determinación: \[ R^2=1-\frac{\sum_{i=1}^{n}(y_i-\widehat{y}_i)^2}{\sum_{i=1}^{n}(y_i-\bar{y})^2}, \qquad \bar{y}=\frac{1}{n}\sum_{i=1}^n y_i. \] En mínimos cuadrados ordinarios con intercepto, \(R^2\) suele interpretarse como la proporción de variabilidad de la variable dependiente explicada por el modelo. Un valor cercano a 1 indica alto ajuste; un valor cercano a 0 indica que el modelo no mejora sustancialmente a la predicción constante \(\bar{y}\) (Hastie, Tibshirani, y Friedman 2009).

Definición. El coeficiente de determinación compara la variación no explicada por el modelo contra la variación total respecto a la media: \[ R^2=1-\frac{\mathrm{SSE}}{\mathrm{SST}} =1-\frac{\sum_{i=1}^{n}(y_i-\widehat{y}_i)^2}{\sum_{i=1}^{n}(y_i-\bar{y})^2}. \tag{7}\]

Métricas de error — sensibilidad a valores atípicos
Desplaza el control para ver cómo MAE y RMSE reaccionan de manera diferente ante errores grandes

4.1 Cálculo de métricas en Python

from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

mae = mean_absolute_error(y, y_hat)
rmse = np.sqrt(mean_squared_error(y, y_hat))
r2 = r2_score(y, y_hat)
sse = np.sum((y - y_hat) ** 2)

pd.DataFrame({
    "metrica": ["SSE", "MAE", "RMSE", "R2"],
    "valor": [sse, mae, rmse, r2]
})
metrica valor
0 SSE 0.0
1 MAE 0.0
2 RMSE 0.0
3 R2 1.0

En una evaluación rigurosa, estas métricas deben calcularse sobre datos no usados para entrenar el modelo. Separar entrenamiento y prueba permite estimar la capacidad de generalización. También es recomendable inspeccionar gráficamente los residuos.

Mostrar código
import plotly.graph_objects as go
import numpy as np

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=y_hat,
    y=residuos,
    mode="markers",
    marker=dict(color="#2563eb", size=10, opacity=0.8,
                line=dict(width=1, color="#1e3a6e")),
    name="Residuo",
    hovertemplate="Predicción: %{x:.2f}<br>Residuo: %{y:.2f}<extra></extra>"
))

# Línea de referencia en cero
fig.add_hline(y=0, line_dash="dash", line_color="#94a3b8", line_width=1.5)

# Banda de ±1 desviación estándar
std_r = float(np.std(residuos))
fig.add_hrect(y0=-std_r, y1=std_r, fillcolor="#2563eb", opacity=0.06,
              line_width=0, annotation_text="±1 SD", annotation_position="top right")

fig.update_layout(
    title="Residuos vs. valores predichos",
    xaxis_title="Predicción ŷᵢ",
    yaxis_title="Residuo eᵢ = yᵢ − ŷᵢ",
    template="plotly_white",
    showlegend=False,
    height=380
)
fig.show()
Figura 2: Residuos vs. valores predichos. Un buen modelo muestra residuos distribuidos aleatoriamente alrededor de cero sin patrones sistemáticos.

Un patrón curvo en esta gráfica puede indicar que la relación entre variables no es lineal. Una dispersión creciente puede sugerir heterocedasticidad. Residuos con valores extremos pueden revelar observaciones atípicas o problemas de medición.

5 Ventajas y limitaciones de la regresión lineal

La regresión lineal tiene varias ventajas. Es un método común para modelar datos numéricos, es computacionalmente eficiente, permite interpretar coeficientes y ofrece una base sólida para entender modelos más complejos. Además, cuando los supuestos estadísticos son razonables, permite construir intervalos de confianza y pruebas de significancia para los coeficientes (montgomery2012introduction?).

Sin embargo, también tiene limitaciones. Supone que la relación entre características y respuesta puede aproximarse mediante una combinación lineal. No maneja datos faltantes automáticamente. Las variables categóricas requieren codificación previa, por ejemplo mediante variables indicadoras. Además, los coeficientes pueden volverse inestables cuando hay multicolinealidad o cuando el número de variables se acerca al número de observaciones.

En aplicaciones predictivas, el modelo debe evaluarse con datos externos al entrenamiento. En aplicaciones explicativas, además deben revisarse supuestos como independencia de errores, varianza constante, ausencia de colinealidad severa y especificación adecuada de la forma funcional.

6 Más allá de la linealidad: árboles de regresión

Un árbol de regresión predice valores numéricos dividiendo recursivamente el espacio de características en regiones más pequeñas. En cada región, asigna como predicción un valor constante, típicamente el promedio de los valores observados en esa región (breiman1984classification?). Esta idea contrasta con la regresión lineal: en lugar de una sola fórmula global, el árbol aprende reglas locales.

Definición. Un árbol de regresión particiona el espacio de entrada en regiones disjuntas \(R_1,\ldots,R_M\) y predice en cada región el promedio de las respuestas de entrenamiento que caen en ella: \[ \widehat{f}(\mathbf{x})=\sum_{m=1}^{M}c_m\mathbf{1}\{\mathbf{x}\in R_m\}, \qquad c_m=\frac{1}{|R_m|}\sum_{i:\mathbf{x}_i\in R_m}y_i. \tag{8}\]

Los árboles capturan relaciones no lineales e interacciones entre variables, pero pueden sobreajustar si crecen demasiado. Por ello, se suelen combinar en métodos de ensamble, como random forests o gradient boosting (Breiman 2001; friedman2001greedy?).

from sklearn.tree import DecisionTreeRegressor

arbol = DecisionTreeRegressor(max_depth=2, random_state=42)
arbol.fit(X, y)

pred_arbol = arbol.predict(X)
pd.DataFrame({
    "nota_real": y,
    "pred_arbol": pred_arbol
})
nota_real pred_arbol
0 56.0 56.0
1 71.0 71.0
2 86.0 86.0
3 101.0 101.0

7 De regresión a clasificación lineal

En clasificación, la variable objetivo no es continua sino categórica. En el caso binario, suele codificarse como \(y\in\{-1,+1\}\) o \(y\in\{0,1\}\). Un clasificador lineal decide la clase de una observación usando una función discriminante de la forma \[ g(\mathbf{x})=\mathbf{w}^\top\mathbf{x}+w_0. \] La frontera de decisión es el conjunto de puntos para los cuales \(g(\mathbf{x})=0\). En dos dimensiones, esta frontera es una recta; en más dimensiones, es un hiperplano.

Definición. Un clasificador lineal binario asigna una etiqueta a partir del signo de una función afín: \[ \widehat{y}=\operatorname{sign}(\mathbf{w}^\top\mathbf{x}+w_0), \qquad \widehat{y}\in\{-1,+1\}. \tag{9}\]

Geométricamente, \(\mathbf{w}\) es perpendicular a la frontera de decisión. Cambiar \(w_0\) desplaza el hiperplano sin cambiar su orientación. Esta interpretación permite conectar clasificadores lineales con proyecciones, márgenes y separación de clases.

Frontera de decisión lineal — explorador interactivo
Ajusta w₁, w₂ y w₀ para rotar y desplazar el hiperplano de decisión g(x)=w₁x₁+w₂x₂+w₀
Clase +1 Clase −1 Mal clasificado

8 Discriminante lineal de Fisher

El discriminante lineal de Fisher busca una dirección de proyección que separe bien dos clases. La idea central es proyectar los datos sobre una recta y elegir la dirección en la que las medias de las clases queden lejos entre sí, mientras que los puntos de cada clase queden compactos (fisher1936use?).

Sean dos clases \(\omega_1\) y \(\omega_2\) con conjuntos de muestras \(\mathcal{X}_1\) y \(\mathcal{X}_2\). Sus medias son \[ \mathbf{m}_1=\frac{1}{n_1}\sum_{\mathbf{x}\in\mathcal{X}_1}\mathbf{x}, \qquad \mathbf{m}_2=\frac{1}{n_2}\sum_{\mathbf{x}\in\mathcal{X}_2}\mathbf{x}. \] Las matrices de dispersión intra-clase son \[ \mathbf{S}_1=\sum_{\mathbf{x}\in\mathcal{X}_1}(\mathbf{x}-\mathbf{m}_1)(\mathbf{x}-\mathbf{m}_1)^\top, \qquad \mathbf{S}_2=\sum_{\mathbf{x}\in\mathcal{X}_2}(\mathbf{x}-\mathbf{m}_2)(\mathbf{x}-\mathbf{m}_2)^\top. \] La dispersión intra-clase total es \[ \mathbf{S}_W=\mathbf{S}_1+\mathbf{S}_2, \] y la dispersión entre clases puede escribirse como \[ \mathbf{S}_B=(\mathbf{m}_1-\mathbf{m}_2)(\mathbf{m}_1-\mathbf{m}_2)^\top. \]

Definición. El criterio de Fisher mide la razón entre separación entre clases y dispersión dentro de las clases después de proyectar por una dirección \(\mathbf{w}\): \[ J_F(\mathbf{w})=\frac{\mathbf{w}^\top\mathbf{S}_B\mathbf{w}}{\mathbf{w}^\top\mathbf{S}_W\mathbf{w}}. \tag{10}\]

Maximizar este cociente produce una dirección proporcional a \[ \mathbf{w}^*\propto \mathbf{S}_W^{-1}(\mathbf{m}_1-\mathbf{m}_2), \] cuando \(\mathbf{S}_W\) es invertible. Si no lo es, se puede usar pseudoinversa o regularización. El resultado no fija por sí solo el umbral de clasificación; solo determina la dirección de proyección. Una vez proyectado un punto como \(z=\mathbf{w}^{*\top}\mathbf{x}\), se elige un umbral \(t\) y se clasifica según si \(z>t\) o \(z\leq t\).

Discriminante Lineal de Fisher — explorador interactivo
Rota la dirección de proyección y observa cómo cambia la separación entre clases
Clase 1 Clase 2 Dirección w Dirección óptima

8.1 Implementación de Fisher en Python

import numpy as np

rng = np.random.default_rng(7)
X1 = rng.normal(loc=[1.5, 2.0], scale=[0.7, 0.5], size=(40, 2))
X2 = rng.normal(loc=[4.0, 3.2], scale=[0.8, 0.6], size=(40, 2))

m1 = X1.mean(axis=0)
m2 = X2.mean(axis=0)

S1 = (X1 - m1).T @ (X1 - m1)
S2 = (X2 - m2).T @ (X2 - m2)
SW = S1 + S2

w = np.linalg.pinv(SW) @ (m1 - m2)
w = w / np.linalg.norm(w)

# Proyecciones
z1 = X1 @ w
z2 = X2 @ w
threshold = 0.5 * (z1.mean() + z2.mean())

threshold, w
(np.float64(-3.614274223853112), array([-0.77598175, -0.63075535]))
Mostrar código
import plotly.graph_objects as go
import numpy as np

# Escalar el vector w para visualización
scale = 1.8
w_viz = w * scale

# Calcular proyecciones sobre la dirección w para cada clase
z1 = X1 @ w
z2 = X2 @ w
# Pies de perpendicular: foot_i = z_i * w (proyección en el espacio original)
feet1 = np.outer(z1, w)
feet2 = np.outer(z2, w)

fig = go.Figure()

# Líneas de proyección (drop lines) — tenues
for i in range(len(X1)):
    fig.add_trace(go.Scatter(
        x=[X1[i, 0], feet1[i, 0]], y=[X1[i, 1], feet1[i, 1]],
        mode="lines", line=dict(color="#93c5fd", width=0.6),
        showlegend=False, hoverinfo="skip"
    ))
for i in range(len(X2)):
    fig.add_trace(go.Scatter(
        x=[X2[i, 0], feet2[i, 0]], y=[X2[i, 1], feet2[i, 1]],
        mode="lines", line=dict(color="#fca5a5", width=0.6),
        showlegend=False, hoverinfo="skip"
    ))

# Puntos clase 1
fig.add_trace(go.Scatter(
    x=X1[:, 0], y=X1[:, 1],
    mode="markers",
    marker=dict(color="#2563eb", size=8, opacity=0.85,
                line=dict(width=0.8, color="#1e3a6e")),
    name="Clase 1"
))

# Puntos clase 2
fig.add_trace(go.Scatter(
    x=X2[:, 0], y=X2[:, 1],
    mode="markers",
    marker=dict(color="#dc2626", size=8, opacity=0.85,
                line=dict(width=0.8, color="#7f1d1d")),
    name="Clase 2"
))

# Eje de proyección (dirección w escalada, pasando por el origen)
t_vals = np.linspace(-2.5, 2.5, 2)
axis_x = t_vals * w[0]
axis_y = t_vals * w[1]
fig.add_trace(go.Scatter(
    x=axis_x, y=axis_y,
    mode="lines",
    line=dict(color="#16a34a", width=2, dash="dot"),
    name="Eje de Fisher",
    hoverinfo="skip"
))

# Flecha (vector w)
fig.add_annotation(
    ax=0, ay=0,
    x=w_viz[0], y=w_viz[1],
    xref="x", yref="y", axref="x", ayref="y",
    showarrow=True,
    arrowhead=3, arrowsize=1.4, arrowwidth=2.5,
    arrowcolor="#16a34a"
)

# Medias de cada clase
m1 = X1.mean(axis=0)
m2 = X2.mean(axis=0)
for m, col, name in [(m1, "#2563eb", "μ₁"), (m2, "#dc2626", "μ₂")]:
    fig.add_trace(go.Scatter(
        x=[m[0]], y=[m[1]],
        mode="markers+text",
        marker=dict(symbol="diamond", size=13, color=col,
                    line=dict(width=1.5, color="white")),
        text=[name], textposition="top right",
        textfont=dict(size=13, color=col),
        showlegend=False, hoverinfo="skip"
    ))

fig.update_layout(
    title="Dirección de proyección de Fisher",
    xaxis_title="x₁",
    yaxis_title="x₂",
    template="plotly_white",
    height=440,
    legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="right", x=1)
)
fig.show()
Figura 3: Dirección de proyección óptima de Fisher (flecha verde) sobre las dos nubes de datos. La dirección maximiza la separación entre medias relativa a la dispersión intra-clase.

En problemas aplicados, el umbral puede elegirse según el costo de los errores. Si los falsos negativos son más graves que los falsos positivos, se puede desplazar el umbral para aumentar sensibilidad. Una curva ROC permite estudiar el intercambio entre tasa de verdaderos positivos y tasa de falsos positivos para distintos umbrales (fawcett2006introduction?).

Mostrar código
import plotly.graph_objects as go
import numpy as np

# Proyecciones
z1_plot = X1 @ w
z2_plot = X2 @ w

z_min = min(z1_plot.min(), z2_plot.min()) - 0.5
z_max = max(z1_plot.max(), z2_plot.max()) + 0.5
z_grid = np.linspace(z_min, z_max, 300)

def gauss_pdf(z, mu, sigma):
    return np.exp(-0.5 * ((z - mu) / sigma) ** 2) / (sigma * np.sqrt(2 * np.pi))

mu1, sig1 = z1_plot.mean(), z1_plot.std()
mu2, sig2 = z2_plot.mean(), z2_plot.std()

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=z_grid, y=gauss_pdf(z_grid, mu1, sig1),
    fill="tozeroy", fillcolor="rgba(37,99,235,0.18)",
    line=dict(color="#2563eb", width=2),
    name="Clase 1"
))
fig.add_trace(go.Scatter(
    x=z_grid, y=gauss_pdf(z_grid, mu2, sig2),
    fill="tozeroy", fillcolor="rgba(220,38,38,0.18)",
    line=dict(color="#dc2626", width=2),
    name="Clase 2"
))

# Rug plots
fig.add_trace(go.Scatter(
    x=z1_plot, y=np.full_like(z1_plot, -0.02),
    mode="markers", marker=dict(symbol="line-ns", size=10,
                                color="#2563eb", line=dict(width=1.5, color="#2563eb")),
    name="Puntos clase 1", showlegend=False
))
fig.add_trace(go.Scatter(
    x=z2_plot, y=np.full_like(z2_plot, -0.02),
    mode="markers", marker=dict(symbol="line-ns", size=10,
                                color="#dc2626", line=dict(width=1.5, color="#dc2626")),
    name="Puntos clase 2", showlegend=False
))

# Umbral
fig.add_vline(x=threshold, line_dash="dash", line_color="#475569", line_width=1.8,
              annotation_text="umbral", annotation_position="top right")

fig.update_layout(
    title="Distribuciones proyectadas sobre la dirección de Fisher",
    xaxis_title="z = wᵀx",
    yaxis_title="Densidad",
    template="plotly_white",
    height=380,
    legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="right", x=1)
)
fig.show()
Figura 4: Distribuciones proyectadas sobre la dirección óptima de Fisher. El umbral de clasificación (línea vertical) divide el eje en las dos regiones de decisión.

9 El perceptrón

El perceptrón, propuesto por Rosenblatt, es uno de los primeros modelos de aprendizaje para clasificación binaria (rosenblatt1958perceptron?). A diferencia de la regresión lineal con solución cerrada, el perceptrón aprende de manera iterativa: revisa ejemplos, detecta errores y actualiza sus pesos.

Se usa una función de decisión \[ \widehat{y}=\operatorname{sign}(\mathbf{w}^\top\mathbf{x}+b), \] con etiquetas \(y_i\in\{-1,+1\}\). Si una muestra está mal clasificada, entonces \[ y_i(\mathbf{w}^\top\mathbf{x}_i+b)\leq 0. \] El algoritmo corrige los parámetros mediante \[ \mathbf{w}\leftarrow \mathbf{w}+\eta y_i\mathbf{x}_i, \qquad b\leftarrow b+\eta y_i, \] donde \(\eta>0\) es la tasa de aprendizaje.

Definición. El algoritmo del perceptrón actualiza sus pesos cuando encuentra una observación mal clasificada: \[ \mathbf{w}^{(t+1)}=\mathbf{w}^{(t)}+\eta y_i\mathbf{x}_i, \qquad b^{(t+1)}=b^{(t)}+\eta y_i. \tag{11}\]

El teorema de convergencia del perceptrón establece que, si los datos son linealmente separables, el algoritmo encuentra una solución en un número finito de actualizaciones (novikoff1962convergence?; minsky1969perceptrons?). Si los datos no son linealmente separables, el algoritmo puede no converger, por lo que en la práctica se fija un número máximo de épocas o se usan variantes con regularización y funciones de pérdida suaves.

Definición. Un conjunto de datos binario es linealmente separable si existe un vector \(\mathbf{w}\) y un sesgo \(b\) tales que todas las observaciones quedan correctamente clasificadas con margen positivo: \[ y_i(\mathbf{w}^\top\mathbf{x}_i+b)>0, \qquad i=1,\ldots,n. \tag{12}\]

Algoritmo del perceptrón — animación paso a paso
Observa cómo el perceptrón corrige la frontera de decisión cada vez que comete un error
Clase +1 Clase −1 Punto activo

9.1 Implementación del perceptrón desde cero

import numpy as np

class PerceptronBinario:
    def __init__(self, lr=1.0, max_epochs=100):
        self.lr = lr
        self.max_epochs = max_epochs
        self.w = None
        self.b = 0.0
        self.errors_ = []

    def fit(self, X, y):
        X = np.asarray(X, dtype=float)
        y = np.asarray(y, dtype=float)
        self.w = np.zeros(X.shape[1])
        self.b = 0.0

        for _ in range(self.max_epochs):
            errores = 0
            for xi, yi in zip(X, y):
                if yi * (np.dot(self.w, xi) + self.b) <= 0:
                    self.w += self.lr * yi * xi
                    self.b += self.lr * yi
                    errores += 1
            self.errors_.append(errores)
            if errores == 0:
                break
        return self

    def decision_function(self, X):
        return np.asarray(X) @ self.w + self.b

    def predict(self, X):
        return np.where(self.decision_function(X) >= 0, 1, -1)

Probemos el algoritmo con datos linealmente separables:

X_pos = rng.normal(loc=[3, 3], scale=0.4, size=(25, 2))
X_neg = rng.normal(loc=[1, 1], scale=0.4, size=(25, 2))
X_clf = np.vstack([X_pos, X_neg])
y_clf = np.hstack([np.ones(len(X_pos)), -np.ones(len(X_neg))])

per = PerceptronBinario(lr=1.0, max_epochs=50)
per.fit(X_clf, y_clf)

pred = per.predict(X_clf)
accuracy = np.mean(pred == y_clf)
accuracy, per.w, per.b, per.errors_
(np.float64(1.0), array([0.5950622 , 0.78519092]), np.float64(-3.0), [4, 3, 0])
Mostrar código
import plotly.graph_objects as go
import numpy as np

epocas = list(range(1, len(per.errors_) + 1))

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=epocas, y=per.errors_,
    mode="lines+markers",
    line=dict(color="#7c3aed", width=2.5),
    marker=dict(size=8, color="#7c3aed",
                line=dict(width=1.5, color="white")),
    name="Errores",
    hovertemplate="Época %{x}<br>Errores: %{y}<extra></extra>"
))

# Área bajo la curva para énfasis visual
fig.add_trace(go.Scatter(
    x=epocas, y=per.errors_,
    fill="tozeroy", fillcolor="rgba(124,58,237,0.10)",
    line=dict(width=0), showlegend=False, hoverinfo="skip"
))

converged_epoch = next((i+1 for i, e in enumerate(per.errors_) if e == 0), None)
if converged_epoch:
    fig.add_vline(x=converged_epoch, line_dash="dash", line_color="#16a34a",
                  line_width=1.8,
                  annotation_text=f"Convergencia (época {converged_epoch})",
                  annotation_position="top right",
                  annotation_font_color="#16a34a")

fig.update_layout(
    title="Errores de clasificación por época — Perceptrón",
    xaxis_title="Época",
    yaxis_title="Número de errores",
    template="plotly_white",
    height=360,
    showlegend=False
)
fig.show()
Figura 5: Número de ejemplos mal clasificados por época durante el entrenamiento del perceptrón. La convergencia a cero indica separabilidad lineal del conjunto de datos.

La secuencia de errores permite observar si el modelo converge. En un conjunto linealmente separable, los errores deben llegar a cero. En datos no separables, la curva puede oscilar.

10 Comparación conceptual: regresión lineal, Fisher y perceptrón

Aunque regresión lineal, Fisher y perceptrón son modelos lineales, responden a objetivos distintos. La regresión lineal aproxima una variable continua minimizando errores cuadráticos. El discriminante de Fisher busca una dirección de proyección que maximice separación relativa entre clases. El perceptrón busca un hiperplano que clasifique correctamente ejemplos binarios, actualizando sus parámetros cuando comete errores.

Modelo Tipo de tarea Objetivo principal Salida
Regresión lineal Regresión Minimizar error cuadrático Valor real
Fisher LDA binario Clasificación Maximizar separación proyectada Clase según umbral
Perceptrón Clasificación Corregir errores de clasificación Etiqueta binaria

La conexión común es la geometría lineal. Todos construyen combinaciones lineales de las características. Por ello, su desempeño depende fuertemente de la representación de entrada. Variables mal escaladas, redundantes o irrelevantes pueden afectar la estimación, la orientación del hiperplano y la estabilidad del aprendizaje.

11 Buenas prácticas en documentación reproducible con Quarto

Al convertir una presentación en capítulo técnico, conviene transformar diapositivas breves en explicaciones progresivas. Un capítulo reproducible debe incluir definiciones formales, ejemplos ejecutables, interpretación de resultados y referencias. En Quarto, los bloques de código permiten que el análisis sea verificable y actualizable. Para proyectos docentes o técnicos, una organización mínima recomendable es:

  1. introducir el concepto con intuición;
  2. formalizarlo con notación matemática;
  3. mostrar un ejemplo pequeño calculable a mano;
  4. implementar el método en Python;
  5. evaluar resultados e interpretar limitaciones.

Esta estructura evita que el capítulo sea solo una transcripción de diapositivas y lo convierte en material de estudio reutilizable.

12 Conclusiones

La regresión lineal y los clasificadores lineales son modelos fundamentales porque condensan ideas centrales del aprendizaje supervisado: representación de datos, estimación de parámetros, funciones objetivo, interpretación geométrica, evaluación de desempeño y generalización. La regresión lineal muestra cómo ajustar una función continua minimizando residuos. Sus métricas, como MAE, RMSE y \(R^2\), permiten evaluar el error desde distintas perspectivas. El discriminante lineal de Fisher introduce el principio de separar clases mediante una proyección óptima. El perceptrón muestra cómo una máquina puede aprender iterativamente a corregir errores.

Aunque estos modelos pueden ser insuficientes para relaciones altamente no lineales, siguen siendo puntos de referencia indispensables. Su simplicidad permite diagnosticar problemas de datos, construir líneas base interpretables y comprender métodos más avanzados. En ciencia de datos, una buena práctica consiste en comenzar con modelos simples, evaluarlos rigurosamente y aumentar la complejidad solo cuando los datos y el problema lo justifican.