---
title: "Regresión y clasificadores lineales"
author: "Diego Villalba"
date: today
lang: es
format:
html:
toc: true
toc-depth: 3
toc-title: "Contenido"
number-sections: true
code-fold: false
code-tools: true
code-summary: "Mostrar código"
fig-align: center
theme: cosmo
highlight-style: github
smooth-scroll: true
pdf:
toc: true
toc-depth: 3
number-sections: true
documentclass: scrbook
papersize: letter
fontsize: 11pt
geometry: margin=2.5cm
keep-tex: false
execute:
echo: true
warning: false
message: false
cache: false
bibliography: references_example.bib
crossref:
fig-title: "Figura"
tbl-title: "Tabla"
eq-prefix: "Ec."
---
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** [@hastie2009elements; @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 [@goodfellow2016deep; @vapnik1998statistical].
## 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$.
::: {.callout-note appearance="minimal"}
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}.
$$ {#eq-regression-problem}
:::
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 [@breiman2001random; @chen2016xgboost]. 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.
### 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.
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-multiple-linear-regression}
:::
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.
```{=html}
<div id="lr-root" style="font-family:Georgia,serif;background:#f8f9fb;border:1px solid #d8dee6;border-radius:8px;padding:18px 22px;margin:2em 0;">
<div style="text-align:center;margin-bottom:10px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">Regresión lineal simple — explorador interactivo</span><br>
<span style="font-size:0.81em;color:#666;">Ajusta β₀ y β₁ y observa cómo cambian los residuos y las métricas en tiempo real</span>
</div>
<canvas id="lr-cv" style="display:block;margin:0 auto;border:1px solid #dde;border-radius:4px;"></canvas>
<div style="display:flex;gap:20px;margin:12px 0 8px;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label style="font-size:0.83em;color:#444;">β₀ (intercepto) = <strong id="lr-b0v">5.00</strong></label>
<input type="range" id="lr-b0" min="-6" max="22" step="0.1" value="5.0"
style="width:100%;accent-color:#1a3a5c;" oninput="lrDraw()">
</div>
<div style="flex:1;min-width:200px;">
<label style="font-size:0.83em;color:#444;">β₁ (pendiente) = <strong id="lr-b1v">0.50</strong></label>
<input type="range" id="lr-b1" min="-1" max="5" step="0.05" value="0.5"
style="width:100%;accent-color:#1a3a5c;" oninput="lrDraw()">
</div>
</div>
<div id="lr-metrics" style="text-align:center;font-size:0.83em;padding:6px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-bottom:10px;min-height:28px;"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;">
<button class="lrbtn" onclick="lrOLS()">Solución OLS optima</button>
<button class="lrbtn" onclick="lrToggleRes()">Mostrar / ocultar residuos</button>
<button class="lrbtn" onclick="lrReset()">Reiniciar</button>
</div>
</div>
<style>
.lrbtn{padding:5px 14px;border:1px solid #aab;background:#fff;border-radius:4px;cursor:pointer;
font-family:Georgia,serif;font-size:0.81em;}.lrbtn:hover{background:#eef2f7;}
</style>
<script>
(function(){
const XD=[1,2,3,4,5,6,7,8,9,10];
const YD=[2.1,4.3,5.8,7.9,10.2,11.8,14.0,16.1,17.9,20.2];
const N=XD.length;
const xbar=XD.reduce((a,b)=>a+b,0)/N;
const ybar=YD.reduce((a,b)=>a+b,0)/N;
const sxy=XD.reduce((s,x,i)=>s+(x-xbar)*(YD[i]-ybar),0);
const sxx=XD.reduce((s,x)=>s+(x-xbar)**2,0);
const OB1=sxy/sxx, OB0=ybar-OB1*xbar; // OLS solution
let showRes=true;
const cv=document.getElementById('lr-cv');
const ctx=cv.getContext('2d');
const DPR=window.devicePixelRatio||1;
const W=640, H=275;
cv.width=W*DPR; cv.height=H*DPR; cv.style.width=W+'px'; cv.style.height=H+'px';
ctx.scale(DPR,DPR);
const PL=56,PR=14,PT=14,PB=36;
const PW=W-PL-PR, PH=H-PT-PB;
const xMn=0,xMx=11,yMn=-3,yMx=23;
const px=x=>PL+(x-xMn)/(xMx-xMn)*PW;
const py=y=>PT+PH-(y-yMn)/(yMx-yMn)*PH;
function mets(b0,b1){
const yh=XD.map(x=>b0+b1*x);
const sse=yh.reduce((s,v,i)=>s+(YD[i]-v)**2,0);
const sst=YD.reduce((s,v)=>s+(v-ybar)**2,0);
return{yh, mse:sse/N, rmse:Math.sqrt(sse/N), r2:1-sse/sst};
}
window.lrDraw=function(){
const b0=+document.getElementById('lr-b0').value;
const b1=+document.getElementById('lr-b1').value;
document.getElementById('lr-b0v').textContent=b0.toFixed(2);
document.getElementById('lr-b1v').textContent=b1.toFixed(2);
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#fff'; ctx.fillRect(0,0,W,H);
// Grid
ctx.strokeStyle='#eaecf0'; ctx.lineWidth=0.8;
for(let x=1;x<=10;x++){ctx.beginPath();ctx.moveTo(px(x),PT);ctx.lineTo(px(x),PT+PH);ctx.stroke();}
for(let y=0;y<=20;y+=4){ctx.beginPath();ctx.moveTo(PL,py(y));ctx.lineTo(PL+PW,py(y));ctx.stroke();}
// Axes
ctx.strokeStyle='#aaa'; ctx.lineWidth=1.1;
ctx.beginPath();ctx.moveTo(PL,PT);ctx.lineTo(PL,PT+PH);ctx.stroke();
ctx.beginPath();ctx.moveTo(PL,py(0));ctx.lineTo(PL+PW,py(0));ctx.stroke();
// Tick labels
ctx.fillStyle='#888'; ctx.font='11px Georgia'; ctx.textAlign='center';
for(let x=2;x<=10;x+=2) ctx.fillText(x,px(x),PT+PH+16);
ctx.textAlign='right';
for(let y=0;y<=20;y+=4) ctx.fillText(y,PL-6,py(y)+4);
// Axis titles
ctx.fillStyle='#666'; ctx.font='12px Georgia'; ctx.textAlign='center';
ctx.fillText('x (horas de estudio)', PL+PW/2, H-2);
ctx.save(); ctx.translate(13,PT+PH/2); ctx.rotate(-Math.PI/2);
ctx.fillText('y (nota)', 0, 0); ctx.restore();
const m=mets(b0,b1);
// Residuals
if(showRes){
XD.forEach((xi,i)=>{
const yh=m.yh[i];
// Shaded residual rectangle
const x0=px(xi), y0=py(YD[i]), y1=py(yh);
const h=y1-y0, side=Math.min(Math.abs(h)*0.4,14);
ctx.fillStyle='rgba(180,30,30,0.08)';
ctx.fillRect(x0-side,Math.min(y0,y1),side,Math.abs(h));
// Residual line
ctx.beginPath(); ctx.moveTo(x0,y0); ctx.lineTo(x0,y1);
ctx.strokeStyle='rgba(180,30,30,0.65)'; ctx.lineWidth=1.8;
ctx.setLineDash([]); ctx.stroke();
});
}
// Ghost OLS line when not at solution
const atOLS = Math.abs(b0-OB0)<0.08 && Math.abs(b1-OB1)<0.04;
if(!atOLS){
ctx.beginPath();
ctx.moveTo(px(0.4),py(OB0+OB1*0.4));
ctx.lineTo(px(10.6),py(OB0+OB1*10.6));
ctx.strokeStyle='rgba(30,140,80,0.3)'; ctx.lineWidth=1.8;
ctx.setLineDash([7,4]); ctx.stroke(); ctx.setLineDash([]);
ctx.fillStyle='rgba(30,140,80,0.55)'; ctx.font='10px Georgia'; ctx.textAlign='left';
ctx.fillText('OLS óptima', px(10.6)+4, py(OB0+OB1*10.6)+4);
}
// User's regression line
ctx.beginPath();
ctx.moveTo(px(0.3),py(b0+b1*0.3));
ctx.lineTo(px(10.7),py(b0+b1*10.7));
ctx.strokeStyle='#1a3a5c'; ctx.lineWidth=2.3; ctx.setLineDash([]); ctx.stroke();
// Line equation label
const labY=b0+b1*6;
if(labY>yMn+1&&labY<yMx-1){
ctx.fillStyle='#1a3a5c'; ctx.font='bold 11px Georgia'; ctx.textAlign='left';
ctx.fillText(`ŷ = ${b0>=0?'':'-'}${Math.abs(b0).toFixed(2)} ${b1>=0?'+':'-'} ${Math.abs(b1).toFixed(2)}·x`, px(6.2), py(labY)-7);
}
// Data points
XD.forEach((xi,i)=>{
ctx.beginPath(); ctx.arc(px(xi),py(YD[i]),5.5,0,Math.PI*2);
ctx.fillStyle='#c0392b'; ctx.fill();
ctx.strokeStyle='#922b21'; ctx.lineWidth=1; ctx.stroke();
});
// Metrics bar
const r2c=m.r2>0.96?'#1a7a3a':m.r2>0.70?'#b7770d':'#c0392b';
document.getElementById('lr-metrics').innerHTML=
`MSE = <strong>${m.mse.toFixed(3)}</strong>
·
RMSE = <strong>${m.rmse.toFixed(3)}</strong>
·
R² = <strong style="color:${r2c};">${m.r2.toFixed(4)}</strong>
${atOLS?' <span style="color:#1a7a3a;">← mínimo global OLS ✓</span>':''}`;
};
window.lrOLS=function(){
document.getElementById('lr-b0').value=OB0.toFixed(2);
document.getElementById('lr-b1').value=OB1.toFixed(2);
lrDraw();
};
window.lrToggleRes=function(){ showRes=!showRes; lrDraw(); };
window.lrReset=function(){
document.getElementById('lr-b0').value='5.0';
document.getElementById('lr-b1').value='0.5';
showRes=true; lrDraw();
};
lrDraw();
})();
</script>
```
## 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}.
$$
::: {.callout-note appearance="minimal"}
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}.
$$ {#eq-matrix-linear-model}
:::
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}.
$$
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-ols-definition}
:::
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.
```{python}
#| label: fig-ols-superficie
#| fig-cap: "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."
#| code-fold: true
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()
```
### 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].
## 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.
```{python}
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
```
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:
```{python}
y_hat = X_design @ beta_hat
residuos = y - y_hat
resultado = datos.copy()
resultado["prediccion"] = y_hat
resultado["residuo"] = residuos
resultado
```
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.
## 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$.
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-sse}
:::
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.
::: {.callout-note appearance="minimal"}
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|.
$$ {#eq-mae}
:::
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}$ [@hastie2009elements].
::: {.callout-note appearance="minimal"}
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}.
$$ {#eq-r2}
:::
```{=html}
<div id="met-wrap" style="font-family:Georgia,serif;max-width:700px;margin:2em auto;background:#f9fafc;border:1px solid #d0d7e3;border-radius:10px;padding:20px 24px;box-shadow:0 2px 12px rgba(30,60,120,.07);">
<div style="text-align:center;margin-bottom:12px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">Métricas de error — sensibilidad a valores atípicos</span><br>
<span style="font-size:0.81em;color:#666;">Desplaza el control para ver cómo MAE y RMSE reaccionan de manera diferente ante errores grandes</span>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap;">
<label style="font-size:0.83em;color:#444;">Error del punto atípico: <strong id="met-ov">0.0</strong> unidades</label>
<input id="met-sl" type="range" min="0" max="9" step="0.1" value="0"
style="flex:1;min-width:160px;accent-color:#c0392b;">
</div>
<canvas id="met-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;margin-bottom:10px;"></canvas>
<div id="met-info" style="font-size:0.82em;text-align:center;padding:6px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;min-height:22px;"></div>
</div>
<script>
(function(){
/* 10 base residuals; last one = the controllable outlier */
const BASE=[0.4,-0.3,0.7,-0.5,0.2,-0.6,0.8,-0.2,0.5,-0.4];
const N=BASE.length;
const cv=document.getElementById('met-cv');
const W=660, H=240, DPR=window.devicePixelRatio||1;
cv.width=W*DPR; cv.height=H*DPR;
cv.style.width=W+'px'; cv.style.height=H+'px';
const ctx=cv.getContext('2d'); ctx.scale(DPR,DPR);
/* two panels */
const LP={l:46,r:W/2-8,t:24,b:210};
const RP={l:W/2+8,r:W-12,t:24,b:210};
const LW=LP.r-LP.l, LH=LP.b-LP.t;
const RW=RP.r-RP.l, RH=RP.b-RP.t;
function draw(outlier){
const res=[...BASE]; res[N-1]=outlier;
const mae =res.reduce((s,r)=>s+Math.abs(r),0)/N;
const mse =res.reduce((s,r)=>s+r*r,0)/N;
const rmse=Math.sqrt(mse);
ctx.clearRect(0,0,W,H);
/* ── left: residual dot plot ── */
ctx.fillStyle='#f8f9fb'; ctx.fillRect(LP.l,LP.t,LW,LH);
const maxAbs=Math.max(1.5,Math.abs(outlier)*1.1);
const zy=LP.t+LH/2;
const ys=y=>zy-y*(LH/2)/maxAbs;
/* reference lines */
[-1,1].forEach(v=>{
ctx.strokeStyle='#dde';ctx.lineWidth=0.8;ctx.setLineDash([4,3]);
ctx.beginPath();ctx.moveTo(LP.l,ys(v));ctx.lineTo(LP.r,ys(v));ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle='#bbb';ctx.font='9px Georgia,serif';ctx.textAlign='right';
ctx.fillText(v>0?'+1':'-1',LP.l-3,ys(v)+3);
});
/* zero line */
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1.3;
ctx.beginPath();ctx.moveTo(LP.l,zy);ctx.lineTo(LP.r,zy);ctx.stroke();
ctx.fillStyle='#aaa';ctx.font='9px Georgia,serif';ctx.textAlign='right';
ctx.fillText('0',LP.l-3,zy+3);
/* panel label */
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Distribución de residuos eᵢ',LP.l+LW/2,LP.t-7);
ctx.fillStyle='#888';ctx.font='10px Georgia,serif';
ctx.fillText('Observación i',LP.l+LW/2,LP.b+15);
/* residual sticks and dots */
const xStep=LW/(N+1);
res.forEach((r,i)=>{
const xx=LP.l+(i+1)*xStep;
const yy=ys(r);
const isOut=(i===N-1);
ctx.strokeStyle=isOut?'#c0392b':'rgba(37,99,235,0.55)';
ctx.lineWidth=isOut?2.2:1.5;
ctx.beginPath();ctx.moveTo(xx,zy);ctx.lineTo(xx,yy);ctx.stroke();
ctx.beginPath();ctx.arc(xx,yy,isOut?6:4,0,2*Math.PI);
ctx.fillStyle=isOut?'#c0392b':'#2563eb';ctx.fill();
ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
});
if(Math.abs(outlier)>0.5){
const oxo=LP.l+N*xStep, oyo=ys(outlier);
ctx.fillStyle='#c0392b';ctx.font='bold 9px Georgia,serif';ctx.textAlign='center';
ctx.fillText('atípico',oxo,outlier>0?Math.max(oyo-8,LP.t+4):Math.min(oyo+14,LP.b-2));
}
/* ── right: metric bars ── */
ctx.fillStyle='#f8f9fb'; ctx.fillRect(RP.l,RP.t,RW,RH);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Métricas de error',RP.l+RW/2,RP.t-7);
const metrics=[
{lbl:'MAE',val:mae,col:'#2563eb',sub:'media |eᵢ|'},
{lbl:'RMSE',val:rmse,col:'#16a34a',sub:'√ media eᵢ²'},
{lbl:'MSE',val:mse,col:'#e05c2a',sub:'media eᵢ²'},
];
const maxV=Math.max(...metrics.map(m=>m.val))*1.12;
const bW=RW*0.2;
metrics.forEach((m,i)=>{
const bx=RP.l+RW*(i+1)/(metrics.length+1)-bW/2;
const bh=Math.min((m.val/maxV)*(RH-30),RH-30);
const by=RP.b-bh-4;
ctx.fillStyle=m.col+'bb';ctx.fillRect(bx,by,bW,bh);
ctx.strokeStyle=m.col;ctx.lineWidth=1.5;ctx.strokeRect(bx,by,bW,bh);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText(m.val.toFixed(3),bx+bW/2,by-5);
ctx.fillStyle=m.col;ctx.font='bold 12px Georgia,serif';
ctx.fillText(m.lbl,bx+bW/2,RP.b+12);
ctx.fillStyle='#888';ctx.font='9px Georgia,serif';
ctx.fillText(m.sub,bx+bW/2,RP.b+23);
});
/* info line */
const ratio=mae>0?(rmse/mae).toFixed(2):'—';
document.getElementById('met-info').innerHTML=
`MAE = <strong style="color:#2563eb">${mae.toFixed(3)}</strong> · `+
`RMSE = <strong style="color:#16a34a">${rmse.toFixed(3)}</strong> · `+
`MSE = <strong style="color:#e05c2a">${mse.toFixed(3)}</strong> · `+
`<span style="color:#777">RMSE/MAE = <strong>${ratio}</strong> `+
`<em>(cerca de 1.0 sin atípicos; crece con valores extremos)</em></span>`;
}
document.getElementById('met-sl').addEventListener('input',function(){
const v=parseFloat(this.value);
document.getElementById('met-ov').textContent=v.toFixed(1);
draw(v);
});
draw(0);
})();
</script>
```
### Cálculo de métricas en Python
```{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]
})
```
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.
```{python}
#| label: fig-residuos
#| fig-cap: "Residuos vs. valores predichos. Un buen modelo muestra residuos distribuidos aleatoriamente alrededor de cero sin patrones sistemáticos."
#| code-fold: true
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()
```
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.
## 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.
## 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.
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-regression-tree}
:::
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 [@breiman2001random; @friedman2001greedy].
```{python}
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
})
```
## 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.
::: {.callout-note appearance="minimal"}
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\}.
$$ {#eq-linear-classifier}
:::
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.
```{=html}
<div id="clf-wrap" style="font-family:Georgia,serif;max-width:700px;margin:2em auto;background:#f9fafc;border:1px solid #d0d7e3;border-radius:10px;padding:20px 24px;box-shadow:0 2px 12px rgba(30,60,120,.07);">
<div style="text-align:center;margin-bottom:12px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">Frontera de decisión lineal — explorador interactivo</span><br>
<span style="font-size:0.81em;color:#666;">Ajusta w₁, w₂ y w₀ para rotar y desplazar el hiperplano de decisión g(x)=w₁x₁+w₂x₂+w₀</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px;">
<div>
<label style="font-size:0.82em;color:#444;">w₁ = <strong id="clf-w1v">1.0</strong></label>
<input id="clf-w1" type="range" min="-3" max="3" step="0.1" value="1"
style="width:100%;accent-color:#1a3a5c;">
</div>
<div>
<label style="font-size:0.82em;color:#444;">w₂ = <strong id="clf-w2v">1.0</strong></label>
<input id="clf-w2" type="range" min="-3" max="3" step="0.1" value="1"
style="width:100%;accent-color:#1a3a5c;">
</div>
<div>
<label style="font-size:0.82em;color:#444;">w₀ = <strong id="clf-w0v">-6.0</strong></label>
<input id="clf-w0" type="range" min="-8" max="4" step="0.1" value="-6"
style="width:100%;accent-color:#1a3a5c;">
</div>
</div>
<div id="clf-acc" style="text-align:center;font-size:0.83em;padding:5px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-bottom:10px;min-height:24px;"></div>
<canvas id="clf-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
<div style="margin-top:8px;display:flex;gap:18px;justify-content:center;font-size:0.79em;color:#555;">
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#2563eb;margin-right:4px;vertical-align:middle;"></span>Clase +1</span>
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#e05c2a;margin-right:4px;vertical-align:middle;"></span>Clase −1</span>
<span><span style="display:inline-block;width:11px;height:11px;border-radius:2px;border:2px solid #dc2626;background:transparent;margin-right:4px;vertical-align:middle;"></span>Mal clasificado</span>
</div>
</div>
<script>
(function(){
/* ── datos: clase +1 (arriba-izq), clase -1 (abajo-der) ── */
const Cp=[[1.2,3.8],[2.0,4.2],[1.5,3.0],[0.8,4.5],[2.3,3.5],[1.0,2.8],[1.8,4.8],[2.5,4.0]];
const Cn=[[4.5,1.2],[5.0,0.8],[3.8,1.5],[5.2,2.0],[4.0,0.5],[5.5,1.8],[4.8,0.3],[3.5,2.2]];
const cv=document.getElementById('clf-cv');
const W=660, H=340, DPR=window.devicePixelRatio||1;
cv.width=W*DPR; cv.height=H*DPR;
cv.style.width=W+'px'; cv.style.height=H+'px';
const ctx=cv.getContext('2d'); ctx.scale(DPR,DPR);
const PL=46,PR=12,PT=18,PB=36;
const PW=W-PL-PR, PH=H-PT-PB;
const xMn=0,xMx=6.5,yMn=0,yMx=5.8;
const sx=x=>PL+(x-xMn)/(xMx-xMn)*PW;
const sy=y=>PT+PH-(y-yMn)/(yMx-yMn)*PH;
const ixs=cx=>xMn+(cx-PL)/PW*(xMx-xMn);
const iys=cy=>yMn+(1-(cy-PT)/PH)*(yMx-yMn);
function draw(){
const w1=+document.getElementById('clf-w1').value;
const w2=+document.getElementById('clf-w2').value;
const w0=+document.getElementById('clf-w0').value;
document.getElementById('clf-w1v').textContent=w1.toFixed(1);
document.getElementById('clf-w2v').textContent=w2.toFixed(1);
document.getElementById('clf-w0v').textContent=w0.toFixed(1);
ctx.clearRect(0,0,W,H);
/* background shading by column */
for(let cx=PL;cx<PL+PW;cx+=3){
const xd=ixs(cx);
if(Math.abs(w2)>1e-4){
const yb=-(w1*xd+w0)/w2;
const yb_cl=Math.max(yMn,Math.min(yMx,yb));
const cyb=sy(yb_cl);
const posAbove=w2>0;
ctx.fillStyle=posAbove?'rgba(37,99,235,0.07)':'rgba(224,92,42,0.07)';
ctx.fillRect(cx,PT,3,Math.max(0,cyb-PT));
ctx.fillStyle=posAbove?'rgba(224,92,42,0.07)':'rgba(37,99,235,0.07)';
ctx.fillRect(cx,cyb,3,Math.max(0,PT+PH-cyb));
} else {
const xb=Math.abs(w1)>1e-4?-w0/w1:Infinity;
const posRight=w1>0;
const isRight=(xd>xb);
ctx.fillStyle=(posRight===isRight)?'rgba(37,99,235,0.07)':'rgba(224,92,42,0.07)';
ctx.fillRect(cx,PT,3,PH);
}
}
/* grid */
ctx.strokeStyle='#e5e9f0';ctx.lineWidth=0.7;
for(let gx=0;gx<=6;gx++){ctx.beginPath();ctx.moveTo(sx(gx),PT);ctx.lineTo(sx(gx),PT+PH);ctx.stroke();}
for(let gy=0;gy<=5;gy++){ctx.beginPath();ctx.moveTo(PL,sy(gy));ctx.lineTo(PL+PW,sy(gy));ctx.stroke();}
/* axes */
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1.2;
ctx.beginPath();ctx.moveTo(PL,PT);ctx.lineTo(PL,PT+PH);ctx.lineTo(PL+PW,PT+PH);ctx.stroke();
/* tick labels */
ctx.fillStyle='#888';ctx.font='11px Georgia,serif';ctx.textAlign='center';
for(let gx=0;gx<=6;gx++) ctx.fillText(gx,sx(gx),PT+PH+14);
ctx.textAlign='right';
for(let gy=0;gy<=5;gy++) ctx.fillText(gy,PL-5,sy(gy)+4);
ctx.fillStyle='#666';ctx.font='11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('x₁',PL+PW/2,H-4);
ctx.save();ctx.translate(14,PT+PH/2);ctx.rotate(-Math.PI/2);ctx.fillText('x₂',0,0);ctx.restore();
/* decision boundary */
const score=x=>w1*x[0]+w2*x[1]+w0;
if(Math.abs(w2)>1e-4){
const y0=-(w1*0+w0)/w2, y1=-(w1*6.5+w0)/w2;
ctx.strokeStyle='#1a2e45';ctx.lineWidth=2.5;ctx.setLineDash([7,4]);
ctx.beginPath();ctx.moveTo(sx(0),sy(y0));ctx.lineTo(sx(6.5),sy(y1));ctx.stroke();
ctx.setLineDash([]);
/* label */
const midX=3.25, midY=-(w1*midX+w0)/w2;
if(midY>yMn&&midY<yMx){
const eq=`g(x)=${w1>=0?'':'-'}${Math.abs(w1).toFixed(1)}x₁${w2>=0?'+':'-'}${Math.abs(w2).toFixed(1)}x₂${w0>=0?'+':'-'}${Math.abs(w0).toFixed(1)}=0`;
ctx.fillStyle='#1a2e45';ctx.font='bold 10px Georgia,serif';ctx.textAlign='center';
ctx.fillText(eq,sx(midX),sy(midY)-8);
}
} else if(Math.abs(w1)>1e-4){
const xb=-w0/w1;
ctx.strokeStyle='#1a2e45';ctx.lineWidth=2.5;ctx.setLineDash([7,4]);
ctx.beginPath();ctx.moveTo(sx(xb),PT);ctx.lineTo(sx(xb),PT+PH);ctx.stroke();
ctx.setLineDash([]);
}
/* points */
function drawPts(pts,label){
const yLbl=label===1?1:-1;
pts.forEach(p=>{
const s=score(p);
const pred=s>0?1:-1;
const wrong=(pred!==yLbl);
const col=label===1?'#2563eb':'#e05c2a';
ctx.beginPath();ctx.arc(sx(p[0]),sy(p[1]),6,0,2*Math.PI);
ctx.fillStyle=wrong?'rgba(255,255,255,0.9)':col+'cc';ctx.fill();
ctx.strokeStyle=wrong?'#dc2626':col;
ctx.lineWidth=wrong?2.5:1.5;ctx.stroke();
if(wrong){
ctx.strokeStyle='#dc2626';ctx.lineWidth=2;
const cx2=sx(p[0]),cy2=sy(p[1]),r=8;
ctx.beginPath();ctx.moveTo(cx2-r,cy2-r);ctx.lineTo(cx2+r,cy2+r);ctx.stroke();
ctx.beginPath();ctx.moveTo(cx2+r,cy2-r);ctx.lineTo(cx2-r,cy2+r);ctx.stroke();
}
});
}
drawPts(Cp,1); drawPts(Cn,-1);
/* accuracy */
const all=[...Cp.map(p=>({p,y:1})),...Cn.map(p=>({p,y:-1}))];
const correct=all.filter(({p,y})=>(score(p)>0?1:-1)===y).length;
const acc=(correct/all.length*100).toFixed(0);
const acol=correct===all.length?'#16a34a':correct>all.length*0.6?'#ca8a04':'#dc2626';
document.getElementById('clf-acc').innerHTML=
`Clasificados correctamente: <strong style="color:${acol}">${correct}/${all.length}</strong>  ·  `+
`Exactitud: <strong style="color:${acol}">${acc}%</strong> · `+
`<span style="color:#888">Frontera: ${w1>=0?'':'-'}${Math.abs(w1).toFixed(1)}x₁ ${w2>=0?'+':'-'} ${Math.abs(w2).toFixed(1)}x₂ ${w0>=0?'+':'-'} ${Math.abs(w0).toFixed(1)} = 0</span>`;
}
['clf-w1','clf-w2','clf-w0'].forEach(id=>document.getElementById(id).addEventListener('input',draw));
draw();
})();
</script>
```
## 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.
$$
::: {.callout-note appearance="minimal"}
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}}.
$$ {#eq-fisher-criterion}
:::
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$.
```{=html}
<div id="lda-wrap" style="font-family:Georgia,serif;max-width:700px;margin:2em auto;background:#f9fafc;border:1px solid #d0d7e3;border-radius:10px;padding:22px 26px;box-shadow:0 2px 12px rgba(30,60,120,.07);">
<div style="text-align:center;margin-bottom:14px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">Discriminante Lineal de Fisher — explorador interactivo</span><br>
<span style="font-size:0.81em;color:#666;">Rota la dirección de proyección y observa cómo cambia la separación entre clases</span>
</div>
<div style="display:flex;align-items:center;gap:14px;margin-bottom:10px;flex-wrap:wrap;">
<label style="font-size:0.83em;color:#444;">Ángulo de proyección θ = <strong id="lda-angv">0°</strong></label>
<input id="lda-ang" type="range" min="0" max="180" value="0" step="1"
style="flex:1;min-width:140px;accent-color:#2563eb;">
<button id="lda-opt-btn" onclick="ldaOptimal()"
style="padding:5px 13px;background:#1a2e45;color:#fff;border:none;border-radius:5px;cursor:pointer;font-size:0.82em;">
Dirección óptima de Fisher
</button>
</div>
<div id="lda-jval" style="text-align:center;font-size:0.83em;padding:5px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-bottom:10px;min-height:26px;"></div>
<canvas id="lda-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
<div style="margin-top:8px;display:flex;gap:18px;justify-content:center;font-size:0.79em;color:#555;">
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#2563eb;margin-right:4px;vertical-align:middle;"></span>Clase 1</span>
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#e05c2a;margin-right:4px;vertical-align:middle;"></span>Clase 2</span>
<span><span style="display:inline-block;width:18px;height:3px;background:#16a34a;margin-right:4px;vertical-align:middle;"></span>Dirección w</span>
<span><span style="display:inline-block;width:18px;height:3px;background:#9333ea;margin-right:4px;vertical-align:middle;border-top:2px dashed #9333ea;"></span>Dirección óptima</span>
</div>
</div>
<script>
(function(){
/* ── datos ─────────────────────────────────────────────── */
const C1=[[-0.11,1.68],[0.68,2.39],[1.29,1.52],[1.54,2.71],[2.18,1.94],
[0.91,1.14],[1.82,2.55],[0.42,2.10],[1.66,1.38],[2.37,2.13],
[1.12,2.88],[0.75,1.81],[1.41,2.02],[2.09,1.59],[0.58,2.44],
[1.94,2.25],[1.23,1.72],[0.87,2.63],[1.57,1.95],[2.24,1.46]];
const C2=[[3.12,2.54],[4.36,3.71],[3.87,2.92],[4.68,3.45],[3.55,4.12],
[4.91,2.83],[3.29,3.58],[4.14,4.27],[4.52,3.18],[3.76,2.71],
[5.08,3.89],[3.43,3.25],[4.79,2.64],[3.98,4.04],[4.23,3.37],
[5.31,3.52],[3.61,2.89],[4.47,4.18],[3.84,3.06],[4.12,3.73]];
/* ── within-class scatter SW ────────────────────────────── */
function mean(pts){ const n=pts.length; let mx=0,my=0; pts.forEach(p=>{mx+=p[0];my+=p[1];}); return [mx/n,my/n]; }
function scatter(pts,m){ let s00=0,s01=0,s11=0; pts.forEach(p=>{const dx=p[0]-m[0],dy=p[1]-m[1];s00+=dx*dx;s01+=dx*dy;s11+=dy*dy;}); return [[s00,s01],[s01,s11]]; }
const m1=mean(C1), m2=mean(C2);
const S1=scatter(C1,m1), S2=scatter(C2,m2);
const SW=[[S1[0][0]+S2[0][0],S1[0][1]+S2[0][1]],[S1[1][0]+S2[1][0],S1[1][1]+S2[1][1]]];
/* ── 2×2 inverse ─────────────────────────────────────────── */
function inv2(M){ const det=M[0][0]*M[1][1]-M[0][1]*M[1][0]; return [[M[1][1]/det,-M[0][1]/det],[-M[1][0]/det,M[0][0]/det]]; }
const SWinv=inv2(SW);
const dm=[m2[0]-m1[0],m2[1]-m1[1]];
const wopt_raw=[SWinv[0][0]*dm[0]+SWinv[0][1]*dm[1], SWinv[1][0]*dm[0]+SWinv[1][1]*dm[1]];
const wopt_norm=Math.sqrt(wopt_raw[0]**2+wopt_raw[1]**2);
const wopt=[wopt_raw[0]/wopt_norm, wopt_raw[1]/wopt_norm];
const optAngle=Math.round((Math.atan2(wopt[1],wopt[0])*180/Math.PI+360)%180);
/* ── canvas setup ────────────────────────────────────────── */
const cv=document.getElementById('lda-cv');
const W=680, H=480, DPR=window.devicePixelRatio||1;
cv.width=W*DPR; cv.height=H*DPR;
cv.style.width=W+'px'; cv.style.height=H+'px';
const ctx=cv.getContext('2d'); ctx.scale(DPR,DPR);
/* layout: top 2D scatter, bottom 1D projection */
const SC={l:44,r:16,t:16,b:210}; // scatter margins
const PR={l:44,r:16,t:H-196,b:H-56}; // projection strip
const SCW=W-SC.l-SC.r, SCH=H-SC.t-SC.b;
const PRW=W-PR.l-PR.r, PRH=PR.b-PR.t;
/* data range */
const allX=[...C1.map(p=>p[0]),...C2.map(p=>p[0])];
const allY=[...C1.map(p=>p[1]),...C2.map(p=>p[1])];
const xMn=Math.min(...allX)-0.5, xMx=Math.max(...allX)+0.5;
const yMn=Math.min(...allY)-0.5, yMx=Math.max(...allY)+0.5;
const sx=x=>SC.l+(x-xMn)/(xMx-xMn)*SCW;
const sy=y=>SC.t+SCH-(y-yMn)/(yMx-yMn)*SCH;
/* ── state ───────────────────────────────────────────────── */
let curAng=0; // degrees
/* ── J(w) criterion ──────────────────────────────────────── */
function computeJ(w){
const proj=pts=>pts.map(p=>p[0]*w[0]+p[1]*w[1]);
const p1=proj(C1), p2=proj(C2);
const mu1=p1.reduce((a,v)=>a+v,0)/p1.length;
const mu2=p2.reduce((a,v)=>a+v,0)/p2.length;
const v1=p1.reduce((a,v)=>a+(v-mu1)**2,0)/p1.length;
const v2=p2.reduce((a,v)=>a+(v-mu2)**2,0)/p2.length;
return {J:(mu1-mu2)**2/(v1+v2+1e-9), mu1, mu2, v1, v2, p1, p2};
}
/* ── draw ────────────────────────────────────────────────── */
function draw(){
ctx.clearRect(0,0,W,H);
const ang=curAng*Math.PI/180;
const w=[Math.cos(ang),Math.sin(ang)];
const {J,mu1,mu2,v1,v2,p1,p2}=computeJ(w);
const Jopt=computeJ(wopt).J;
const ratio=Math.min(J/Jopt,1);
/* ── 2D scatter panel ── */
/* background */
ctx.fillStyle='#f8f9fb'; ctx.fillRect(SC.l,SC.t,SCW,SCH);
/* grid */
ctx.strokeStyle='#e5e9f0'; ctx.lineWidth=0.8;
for(let gx=Math.ceil(xMn);gx<=xMx;gx++){
ctx.beginPath();ctx.moveTo(sx(gx),SC.t);ctx.lineTo(sx(gx),SC.t+SCH);ctx.stroke();
}
for(let gy=Math.ceil(yMn);gy<=yMx;gy++){
ctx.beginPath();ctx.moveTo(SC.l,sy(gy));ctx.lineTo(SC.l+SCW,sy(gy));ctx.stroke();
}
/* axes */
ctx.strokeStyle='#9aa3b5'; ctx.lineWidth=1.2;
ctx.beginPath();ctx.moveTo(SC.l,SC.t);ctx.lineTo(SC.l,SC.t+SCH);ctx.lineTo(SC.l+SCW,SC.t+SCH);ctx.stroke();
/* axis labels */
ctx.fillStyle='#666';ctx.font='11px Georgia,serif';ctx.textAlign='center';
for(let gx=Math.ceil(xMn);gx<=xMx;gx++){
ctx.fillText(gx,sx(gx),SC.t+SCH+13);
}
ctx.textAlign='right';
for(let gy=Math.ceil(yMn);gy<=yMx;gy++){
ctx.fillText(gy,SC.l-5,sy(gy)+4);
}
/* optimal direction (dashed purple) */
const ctrX=(sx(xMn)+sx(xMx))/2, ctrY=(sy(yMn)+sy(yMx))/2;
const optAng=optAngle*Math.PI/180;
const rLen=Math.min(SCW,SCH)*0.46;
ctx.setLineDash([5,4]); ctx.strokeStyle='#9333ea'; ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(ctrX-Math.cos(optAng)*rLen, ctrY+Math.sin(optAng)*rLen);
ctx.lineTo(ctrX+Math.cos(optAng)*rLen, ctrY-Math.sin(optAng)*rLen);
ctx.stroke(); ctx.setLineDash([]);
/* projection axis (green) */
const axLen=Math.min(SCW,SCH)*0.48;
const axX1=ctrX-Math.cos(ang)*axLen, axY1=ctrY+Math.sin(ang)*axLen;
const axX2=ctrX+Math.cos(ang)*axLen, axY2=ctrY-Math.sin(ang)*axLen;
ctx.strokeStyle='#16a34a'; ctx.lineWidth=2.2;
ctx.beginPath();ctx.moveTo(axX1,axY1);ctx.lineTo(axX2,axY2);ctx.stroke();
/* arrowhead */
const ah=8;
ctx.fillStyle='#16a34a';
ctx.save();ctx.translate(axX2,axY2);ctx.rotate(-ang);
ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(-ah,-ah/2.5);ctx.lineTo(-ah,ah/2.5);ctx.closePath();ctx.fill();
ctx.restore();
ctx.fillStyle='#16a34a';ctx.font='bold 11px Georgia,serif';ctx.textAlign='left';
ctx.fillText('w',axX2+6,axY2-4);
/* projection drop-lines */
function projectPt(p){
const t=p[0]*w[0]+p[1]*w[1];
return {t, fx:ctrX+(t*w[0]-ctrX*0+0)*1, fy:ctrY };
}
// compute origin on axis in data coords
const originT=0;
// foot of perpendicular from point p onto axis
function foot(p){
const t=p[0]*w[0]+p[1]*w[1];
return { px:sx(t*w[0]), py:sy(t*w[1]) };
// more precisely: foot in screen coords
}
function footScreen(p){
const t=p[0]*w[0]+p[1]*w[1];
// point on axis = ctr + t*(cos,-sin)*scale ... but scale is tricky cross-unit
// easier: foot = ctrScreen + t_screen * w_screen_dir
const tScaled=t*(axLen/(Math.abs(w[0])*(xMx-xMn)/SCW*0.5+Math.abs(w[1])*(yMx-yMn)/SCH*0.5));
// simplest correct formula using actual screen ax direction
const dx=axX2-axX1, dy=axY2-axY1;
const axLen2=Math.sqrt(dx*dx+dy*dy);
const ux=dx/axLen2, uy=dy/axLen2;
const px_s=sx(p[0]), py_s=sy(p[1]);
const proj2=(px_s-axX1)*ux+(py_s-axY1)*uy;
return { fx:axX1+proj2*ux, fy:axY1+proj2*uy, t:proj2 };
}
ctx.lineWidth=0.8; ctx.strokeStyle='rgba(100,120,160,0.35)';
[...C1,...C2].forEach(p=>{
const {fx,fy}=footScreen(p);
ctx.beginPath();ctx.moveTo(sx(p[0]),sy(p[1]));ctx.lineTo(fx,fy);ctx.stroke();
});
/* data points */
function drawPts(pts,col){
pts.forEach(p=>{
ctx.beginPath();ctx.arc(sx(p[0]),sy(p[1]),4.5,0,2*Math.PI);
ctx.fillStyle=col+'cc';ctx.fill();
ctx.strokeStyle=col;ctx.lineWidth=1.2;ctx.stroke();
});
}
drawPts(C1,'#2563eb'); drawPts(C2,'#e05c2a');
/* ── 1D projection strip ── */
ctx.fillStyle='#f0f4fa';
ctx.fillRect(PR.l,PR.t,PRW,PRH);
ctx.strokeStyle='#c5cfe0';ctx.lineWidth=1;
ctx.strokeRect(PR.l,PR.t,PRW,PRH);
ctx.fillStyle='#8892a4';ctx.font='10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Proyecciones 1D (eje w)',PR.l+PRW/2,PR.t-5);
/* map projection values to strip x */
const allProj=[...p1,...p2];
const pMn=Math.min(...allProj)-0.5, pMx=Math.max(...allProj)+0.5;
const ps=t=>PR.l+(t-pMn)/(pMx-pMn)*PRW;
/* axis in strip */
const midY=PR.t+PRH/2;
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1.2;
ctx.beginPath();ctx.moveTo(PR.l,midY);ctx.lineTo(PR.l+PRW,midY);ctx.stroke();
/* ticks */
ctx.fillStyle='#888';ctx.font='10px Georgia,serif';ctx.textAlign='center';
for(let tv=Math.ceil(pMn);tv<=pMx;tv++){
ctx.beginPath();ctx.moveTo(ps(tv),midY-4);ctx.lineTo(ps(tv),midY+4);ctx.strokeStyle='#aaa';ctx.lineWidth=1;ctx.stroke();
ctx.fillText(tv.toFixed(0),ps(tv),midY+15);
}
/* projected points */
function drawProj1D(projs,col,yRow){
projs.forEach(t=>{
ctx.beginPath();ctx.arc(ps(t),yRow,4,0,2*Math.PI);
ctx.fillStyle=col+'bb';ctx.fill();
ctx.strokeStyle=col;ctx.lineWidth=1;ctx.stroke();
});
}
drawProj1D(p1,'#2563eb',midY-18);
drawProj1D(p2,'#e05c2a',midY+18);
/* class means on strip */
const pm1=p1.reduce((a,v)=>a+v,0)/p1.length;
const pm2=p2.reduce((a,v)=>a+v,0)/p2.length;
function drawMeanTick(t,col,yRow){
ctx.strokeStyle=col;ctx.lineWidth=2.5;
ctx.beginPath();ctx.moveTo(ps(t),yRow-6);ctx.lineTo(ps(t),yRow+6);ctx.stroke();
ctx.fillStyle=col;ctx.font='bold 10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('μ',ps(t),yRow-9);
}
drawMeanTick(pm1,'#1d4ed8',midY-18);
drawMeanTick(pm2,'#b94018',midY+18);
/* threshold midpoint */
const thr=(pm1+pm2)/2;
ctx.strokeStyle='#374151';ctx.lineWidth=1.8;ctx.setLineDash([4,3]);
ctx.beginPath();ctx.moveTo(ps(thr),PR.t+4);ctx.lineTo(ps(thr),PR.b-4);ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle='#374151';ctx.font='10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('t',ps(thr),PR.t+2);
/* ── J(w) metric display ── */
const Jcolor=ratio>0.85?'#16a34a':ratio>0.5?'#ca8a04':'#dc2626';
document.getElementById('lda-jval').innerHTML=
`J(w) = <strong style="color:${Jcolor}">${J.toFixed(3)}</strong>`+
` | J<sub>óptimo</sub> = <strong>${Jopt.toFixed(3)}</strong>`+
` | Eficiencia = <strong style="color:${Jcolor}">${(ratio*100).toFixed(1)}%</strong>`+
` | θ = ${curAng}° (θ<sub>opt</sub> = ${optAngle}°)`;
/* ── panel label ── */
ctx.fillStyle='#1a2e45';ctx.font='bold 12px Georgia,serif';ctx.textAlign='left';
ctx.fillText('Espacio 2D original',SC.l+4,SC.t+14);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';
ctx.fillText('Proyección sobre w',PR.l+4,PR.t+13);
}
/* ── controls ─────────────────────────────────────────────── */
const slider=document.getElementById('lda-ang');
const angLabel=document.getElementById('lda-angv');
slider.addEventListener('input',function(){
curAng=parseInt(this.value);
angLabel.textContent=curAng+'°';
draw();
});
window.ldaOptimal=function(){
curAng=optAngle;
slider.value=optAngle;
angLabel.textContent=optAngle+'°';
draw();
};
draw();
})();
</script>
```
### Implementación de Fisher en Python
```{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
```
```{python}
#| label: fig-fisher-proyeccion
#| fig-cap: "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."
#| code-fold: true
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()
```
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].
```{python}
#| label: fig-fisher-1d
#| fig-cap: "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."
#| code-fold: true
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()
```
## 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.
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-perceptron-update}
:::
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.
::: {.callout-note appearance="minimal"}
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.
$$ {#eq-linear-separability}
:::
```{=html}
<div id="per-wrap" style="font-family:Georgia,serif;max-width:700px;margin:2em auto;background:#f9fafc;border:1px solid #d0d7e3;border-radius:10px;padding:20px 24px;box-shadow:0 2px 12px rgba(30,60,120,.07);">
<div style="text-align:center;margin-bottom:12px;">
<span style="font-size:1.05em;font-weight:bold;color:#1a2e45;">Algoritmo del perceptrón — animación paso a paso</span><br>
<span style="font-size:0.81em;color:#666;">Observa cómo el perceptrón corrige la frontera de decisión cada vez que comete un error</span>
</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:10px;flex-wrap:wrap;">
<button onclick="perStep()" id="per-btn-step"
style="padding:6px 16px;background:#1a2e45;color:#fff;border:none;border-radius:5px;cursor:pointer;font-family:Georgia,serif;font-size:0.83em;">
Siguiente actualización
</button>
<button onclick="perEpoch()"
style="padding:6px 16px;background:#374151;color:#fff;border:none;border-radius:5px;cursor:pointer;font-family:Georgia,serif;font-size:0.83em;">
Época completa
</button>
<button onclick="perReset()"
style="padding:6px 16px;background:#fff;color:#374151;border:1px solid #aab;border-radius:5px;cursor:pointer;font-family:Georgia,serif;font-size:0.83em;">
Reiniciar
</button>
</div>
<div id="per-status" style="text-align:center;font-size:0.82em;padding:5px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-bottom:10px;min-height:24px;"></div>
<canvas id="per-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
<div id="per-log" style="margin-top:10px;font-size:0.79em;color:#555;background:#fff;border:1px solid #e0e5ed;border-radius:5px;padding:8px 12px;min-height:36px;max-height:100px;overflow-y:auto;"></div>
<div style="margin-top:8px;display:flex;gap:18px;justify-content:center;font-size:0.79em;color:#555;">
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#2563eb;margin-right:4px;vertical-align:middle;"></span>Clase +1</span>
<span><span style="display:inline-block;width:11px;height:11px;border-radius:50%;background:#e05c2a;margin-right:4px;vertical-align:middle;"></span>Clase −1</span>
<span><span style="display:inline-block;width:11px;height:11px;border-radius:2px;border:2px solid #f59e0b;background:#fef3c7;margin-right:4px;vertical-align:middle;"></span>Punto activo</span>
</div>
</div>
<script>
(function(){
/* ── datos linealmente separables ── */
const DATA=[
{x:[1.0,1.0],y:1},{x:[1.5,0.6],y:1},{x:[0.7,1.5],y:1},
{x:[1.3,1.8],y:1},{x:[0.5,0.9],y:1},{x:[1.8,1.3],y:1},
{x:[0.9,2.0],y:1},{x:[1.2,0.4],y:1},
{x:[3.5,3.2],y:-1},{x:[4.0,3.8],y:-1},{x:[3.2,4.0],y:-1},
{x:[4.2,3.0],y:-1},{x:[3.8,4.2],y:-1},{x:[3.0,3.6],y:-1},
{x:[4.5,3.5],y:-1},{x:[3.3,2.8],y:-1}
];
const N=DATA.length;
/* ── canvas ── */
const cv=document.getElementById('per-cv');
const W=660, H=320, DPR=window.devicePixelRatio||1;
cv.width=W*DPR; cv.height=H*DPR;
cv.style.width=W+'px'; cv.style.height=H+'px';
const ctx=cv.getContext('2d'); ctx.scale(DPR,DPR);
const PL=46,PR=12,PT=18,PB=36;
const PW=W-PL-PR, PH=H-PT-PB;
const xMn=0,xMx=5.5,yMn=0,yMx=5;
const sx=x=>PL+(x-xMn)/(xMx-xMn)*PW;
const sy=y=>PT+PH-(y-yMn)/(yMx-yMn)*PH;
/* ── state ── */
let w=[0,0], b=0;
let ptr=0, epochN=0, stepN=0, done=false;
let activeIdx=-1, logLines=[];
const sign=v=>v>0?1:-1; // sign(0)=+1
function predict(x){ return sign(w[0]*x[0]+w[1]*x[1]+b); }
function draw(){
ctx.clearRect(0,0,W,H);
/* background shading */
if(w[0]!==0||w[1]!==0){
for(let cx=PL;cx<PL+PW;cx+=3){
const xd=xMn+(cx-PL)/PW*(xMx-xMn);
if(Math.abs(w[1])>1e-6){
const yb=-(w[0]*xd+b)/w[1];
const yb_cl=Math.max(yMn,Math.min(yMx,yb));
const cyb=sy(yb_cl);
const posAbove=w[1]>0;
ctx.fillStyle=posAbove?'rgba(37,99,235,0.06)':'rgba(224,92,42,0.06)';
ctx.fillRect(cx,PT,3,Math.max(0,cyb-PT));
ctx.fillStyle=posAbove?'rgba(224,92,42,0.06)':'rgba(37,99,235,0.06)';
ctx.fillRect(cx,cyb,3,Math.max(0,PT+PH-cyb));
} else {
const xb=Math.abs(w[0])>1e-6?-b/w[0]:Infinity;
ctx.fillStyle=(xd>xb)===(w[0]>0)?'rgba(37,99,235,0.06)':'rgba(224,92,42,0.06)';
ctx.fillRect(cx,PT,3,PH);
}
}
}
/* grid */
ctx.strokeStyle='#e5e9f0';ctx.lineWidth=0.7;
for(let gx=0;gx<=5;gx++){ctx.beginPath();ctx.moveTo(sx(gx),PT);ctx.lineTo(sx(gx),PT+PH);ctx.stroke();}
for(let gy=0;gy<=5;gy++){ctx.beginPath();ctx.moveTo(PL,sy(gy));ctx.lineTo(PL+PW,sy(gy));ctx.stroke();}
/* axes */
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1.2;
ctx.beginPath();ctx.moveTo(PL,PT);ctx.lineTo(PL,PT+PH);ctx.lineTo(PL+PW,PT+PH);ctx.stroke();
ctx.fillStyle='#888';ctx.font='11px Georgia,serif';ctx.textAlign='center';
for(let gx=0;gx<=5;gx++) ctx.fillText(gx,sx(gx),PT+PH+14);
ctx.textAlign='right';
for(let gy=0;gy<=4;gy++) ctx.fillText(gy,PL-5,sy(gy)+4);
ctx.fillStyle='#666';ctx.font='11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('x₁',PL+PW/2,H-4);
ctx.save();ctx.translate(14,PT+PH/2);ctx.rotate(-Math.PI/2);ctx.fillText('x₂',0,0);ctx.restore();
/* decision boundary */
if(w[0]!==0||w[1]!==0){
if(Math.abs(w[1])>1e-6){
const y0=-(w[0]*xMn+b)/w[1], y1=-(w[0]*xMx+b)/w[1];
ctx.strokeStyle='#1a2e45';ctx.lineWidth=2.5;ctx.setLineDash([7,4]);
ctx.beginPath();ctx.moveTo(sx(xMn),sy(y0));ctx.lineTo(sx(xMx),sy(y1));ctx.stroke();
ctx.setLineDash([]);
} else if(Math.abs(w[0])>1e-6){
const xb=-b/w[0];
ctx.strokeStyle='#1a2e45';ctx.lineWidth=2.5;ctx.setLineDash([7,4]);
ctx.beginPath();ctx.moveTo(sx(xb),PT);ctx.lineTo(sx(xb),PT+PH);ctx.stroke();
ctx.setLineDash([]);
}
/* w vector label */
ctx.fillStyle='#1a2e45';ctx.font='10px Georgia,serif';ctx.textAlign='left';
ctx.fillText(`w=[${w[0].toFixed(1)}, ${w[1].toFixed(1)}] b=${b.toFixed(1)}`,PL+4,PT+14);
} else {
ctx.fillStyle='#aaa';ctx.font='italic 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('(w=0, b=0 — frontera no definida)',PL+PW/2,PT+PH/2);
}
/* data points */
DATA.forEach((d,i)=>{
const isActive=(i===activeIdx);
const wrong=(predict(d.x)!==d.y);
const col=d.y===1?'#2563eb':'#e05c2a';
const r=isActive?8:6;
ctx.beginPath();ctx.arc(sx(d.x[0]),sy(d.x[1]),r,0,2*Math.PI);
if(isActive){
ctx.fillStyle='#fef3c7';ctx.fill();
ctx.strokeStyle='#f59e0b';ctx.lineWidth=2.5;ctx.stroke();
} else if(wrong&&(w[0]!==0||w[1]!==0||b!==0)){
ctx.fillStyle='rgba(255,255,255,0.85)';ctx.fill();
ctx.strokeStyle='#dc2626';ctx.lineWidth=2;ctx.stroke();
} else {
ctx.fillStyle=col+'cc';ctx.fill();
ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.stroke();
}
});
/* status */
const correct=DATA.filter(d=>predict(d.x)===d.y).length;
const acol=done?'#16a34a':correct<N*0.6?'#dc2626':'#ca8a04';
const wStr=`w=[${w[0].toFixed(1)},${w[1].toFixed(1)}], b=${b.toFixed(1)}`;
document.getElementById('per-status').innerHTML=
done
? `<strong style="color:#16a34a">Convergió</strong> en ${stepN} actualiz. / ${epochN} épocas — todos los puntos correctos ✓`
: `Época ${epochN} · Actualización ${stepN} · `+
`Correctos: <strong style="color:${acol}">${correct}/${N}</strong> · ${wStr}`;
}
function logPush(msg){
logLines.unshift(msg);
if(logLines.length>12) logLines.pop();
document.getElementById('per-log').innerHTML=logLines.join('<br>');
}
function findNext(){
/* scan starting from ptr, find first misclassified */
for(let k=0;k<N;k++){
const i=(ptr+k)%N;
if(predict(DATA[i].x)!==DATA[i].y) return i;
}
return -1;
}
window.perStep=function(){
if(done) return;
const i=findNext();
if(i===-1){ done=true; draw(); return; }
activeIdx=i;
const d=DATA[i];
const oldPred=predict(d.x);
/* update */
w[0]+=d.y*d.x[0];
w[1]+=d.y*d.x[1];
b+=d.y;
stepN++;
if(i<ptr&&i<ptr%N) epochN++;
if(i+1>=N&&ptr<=i) epochN++;
ptr=(i+1)%N;
logPush(`Paso ${stepN}: punto (${d.x[0].toFixed(1)},${d.x[1].toFixed(1)}) `+
`predijo ${oldPred>0?'+1':'−1'} (real ${d.y>0?'+1':'−1'}) → `+
`w=[${w[0].toFixed(1)},${w[1].toFixed(1)}], b=${b.toFixed(1)}`);
const allOK=DATA.every(d=>predict(d.x)===d.y);
if(allOK){ done=true; epochN++; }
draw();
};
window.perEpoch=function(){
if(done) return;
epochN++;
let updates=0;
DATA.forEach((d,i)=>{
if(predict(d.x)!==d.y){
activeIdx=i;
w[0]+=d.y*d.x[0];
w[1]+=d.y*d.x[1];
b+=d.y;
stepN++;
updates++;
}
});
if(updates===0||DATA.every(d=>predict(d.x)===d.y)){ done=true; }
logPush(`Época ${epochN}: ${updates} actualiz. — w=[${w[0].toFixed(1)},${w[1].toFixed(1)}], b=${b.toFixed(1)}`);
activeIdx=-1;
draw();
};
window.perReset=function(){
w=[0,0];b=0;ptr=0;epochN=0;stepN=0;done=false;activeIdx=-1;logLines=[];
document.getElementById('per-log').innerHTML='';
draw();
};
draw();
logPush('Iniciando con w=[0,0], b=0 — haz clic en "Siguiente actualización"');
})();
</script>
```
### Implementación del perceptrón desde cero
```{python}
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:
```{python}
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_
```
```{python}
#| label: fig-perceptron-errores
#| fig-cap: "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."
#| code-fold: true
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()
```
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.
## 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.
## 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.
## 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.