---
title: "Clasificadores bayesianos"
author: "Diego Villalba"
date: today
lang: es
format:
html:
toc: true
toc-depth: 3
toc-title: "Contenido"
number-sections: true
code-fold: true
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: referencias_bayesianos.bib
crossref:
fig-title: "Figura"
tbl-title: "Tabla"
eq-prefix: "Ec."
---
Los métodos de aprendizaje supervisado pueden verse desde dos perspectivas complementarias. En una perspectiva determinista, el objetivo es aprender directamente una función de decisión $f(\mathbf{x})$ que asigne cada observación a una clase, produzca una predicción continua o aproxime una relación funcional. En una perspectiva probabilística, en cambio, el objetivo es modelar explícitamente la incertidumbre: no sólo se decide una clase, sino que se estima qué tan plausible es cada clase dada la evidencia observada. Esto se traduce en que, si no observamos niguna caracteristica del objeto, la mejor decisión que podemos tomar depende de la probabilidad previa de cada clase.
Para ilustrar este hecho, supongamos que somos participantes dentro del famoso juego *En familia con chabelo*, donde se nos da la oportunidad de ganar un auto, para esto debemos de escoger dentro de 3 posibles puertas donde detrás se encuentra el auto o una cabra. Si no se nos da ninguna pista previa es razonable pensar que contamos con una probabilidad de $\frac{1}{3}$ de ganar el auto, por lo que la mejor decisión es escoger una puerta al azar.
{width="65%"}
Asumiendo que nuestra elección es la puerta 1, como sucede en muchas ocasiones el presentador nos hará notar que detras de la puerta tres hay una cabra, con lo cual es natural preguntarse: ¿Bajo que condiciones tendremos la mejor probabilidad de ganar?, es común pensar que dado que sabemos que detrás de las puerta de nuestra elección hay o una cabra o un auto la probabilidad de encntrar nuestro premio tras estas 2 puertas es de $\frac{1}{2}$, sin embargo nuestra intución en ese caso sería incorrecta.
{width="65%"}
Para ver con más detalle por qué conviene cambiar de puerta, hagamos las cuentas paso a paso. Antes de que Chabelo abra una puerta, el auto puede estar en cualquiera de las tres puertas con la misma probabilidad:
$$
P(A_1)=P(A_2)=P(A_3)=\frac{1}{3}
$$
donde $A_i$ significa que el auto está detrás de la puerta $i$.
Ahora bien, Chabelo sabe dónde está el auto y siempre abre una puerta que tiene una cabra. Además, si tiene dos puertas con cabra disponibles, asumimos que escoge una de ellas al azar. Entonces las posibilidades son las siguientes:
Las ramas donde cambiar gana valen por 2 porque tienen probabilidad $\frac{1}{3}$, mientras que las ramas donde cambiar pierde valen $\frac{1}{6}$. Esto ocurre porque si el auto está en nuestra puerta inicial, Chabelo tiene dos puertas posibles para abrir y divide ese caso en dos ramas de probabilidad $\frac{1}{6}$. En cambio, si el auto está en una puerta distinta a la nuestra, Chabelo no tiene libertad: sólo puede abrir la única puerta restante con cabra. Por eso cada una de esas ramas conserva probabilidad $\frac{1}{3}$, equivalente a dos partes de tamaño $\frac{1}{6}$.
```{=html}
<div id="cgv3" style="font-family:'Segoe UI',system-ui,sans-serif;max-width:940px;margin:1.5rem auto;background:#f8fafc;border:1px solid #e2e8f0;border-radius:14px;padding:1.4rem 1.8rem;">
<div style="margin-bottom:1.1rem;display:flex;align-items:center;gap:.8rem;flex-wrap:wrap;">
<span style="font-weight:700;color:#1e293b;font-size:.95rem;">Elige tu puerta inicial:</span>
<div id="cgv3-btns" style="display:flex;gap:6px;"></div>
</div>
<div style="margin-bottom:1rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:.6rem 1rem;font-size:.86rem;color:#1e40af;">
El árbol muestra <strong>todos los caminos posibles</strong> del juego.
Las ramas más probables (p = 1/3) contribuyen 2 unidades al conteo final;
las menos probables (p = 1/6) contribuyen 1. Las ramas donde cambiar gana pesan 2 porque Chabelo estaba obligado a abrir una sola puerta. El total es 6 estados de igual peso.
</div>
<svg id="cgv3-svg" viewBox="0 0 920 490" xmlns="http://www.w3.org/2000/svg"
style="width:100%;display:block;border-radius:10px;background:white;border:1px solid #f1f5f9;"></svg>
<div id="cgv3-counter" style="margin-top:.9rem;"></div>
</div>
<script>
(function () {
var cgv3Choice = 1;
// Leaf x-positions for 4 columns
var LX = [105, 315, 580, 805];
var RX = 460; // root x
function getLeaves(d) {
var oth = [1,2,3].filter(function(x){ return x!==d; }).sort(function(a,b){ return a-b; });
return [
{ auto:d, chabelo:oth[0], switchTo:oth[1], switchWins:false, dots:1, pFrac:'1/6', edgeP:'1/2' },
{ auto:d, chabelo:oth[1], switchTo:oth[0], switchWins:false, dots:1, pFrac:'1/6', edgeP:'1/2' },
{ auto:oth[0], chabelo:oth[1], switchTo:oth[0], switchWins:true, dots:2, pFrac:'1/3', edgeP:'1' },
{ auto:oth[1], chabelo:oth[0], switchTo:oth[1], switchWins:true, dots:2, pFrac:'1/3', edgeP:'1' },
];
}
function bez(x1,y1,x2,y2) {
var my=(y1+y2)/2;
return 'M'+x1+','+y1+' C'+x1+','+my+' '+x2+','+my+' '+x2+','+y2;
}
function edge(x1,y1,x2,y2,col,sw) {
return '<path d="'+bez(x1,y1,x2,y2)+'" stroke="'+col
+'" stroke-width="'+(sw||2.2)+'" fill="none" stroke-linecap="round"/>';
}
function edgeLbl(x1,y1,x2,y2,txt,col) {
var mx=(x1+x2)/2, my=(y1+y2)/2;
return '<rect x="'+(mx-24)+'" y="'+(my-10)+'" width="48" height="18" rx="5" fill="white"/>'
+'<text x="'+mx+'" y="'+(my+4)+'" text-anchor="middle" font-size="11.5" '
+'font-family="\'Segoe UI\',sans-serif" font-weight="600" fill="'+(col||'#475569')+'">'+txt+'</text>';
}
function bx(cx,cy,bw,bh,lines,bg,fg) {
var x=cx-bw/2, y=cy-bh/2, lh=18;
var th=lines.length*lh, sy=cy-th/2+lh*0.72;
var s='<g>';
s+='<rect x="'+x+'" y="'+y+'" width="'+bw+'" height="'+bh+'" rx="8" fill="'+bg+'"/>';
lines.forEach(function(l,i){
s+='<text x="'+cx+'" y="'+(sy+i*lh)+'" text-anchor="middle" font-family="\'Segoe UI\',sans-serif" '
+'font-size="'+(i===0?13:12)+'" font-weight="'+(i===0?'700':'400')+'" fill="'+fg+'">'+l+'</text>';
});
return s+'</g>';
}
function render(choice) {
var leaves = getLeaves(choice);
var oth = [1,2,3].filter(function(x){ return x!==choice; }).sort(function(a,b){ return a-b; });
var l1x = [(LX[0]+LX[1])/2, LX[2], LX[3]]; // [210, 580, 805]
// Y levels
var Yr = 44, BHr = 46; // root
var Yl = 152, BHl = 48; // L1 (auto position)
var Yc = 284, BHc = 58; // L2 (Chabelo)
var Yo = 402, BHo = 46; // outcome banner
var Yd = 466; // dots row
var Yw = 488; // ×N weight labels
var BW = 160;
var svg = '';
// ── background column tints ──
// Left group (auto=choice): light red
var lx0 = LX[0]-BW/2-12, lx1 = LX[1]+BW/2+12;
svg += '<rect x="'+lx0+'" y="'+(Yr-28)+'" width="'+(lx1-lx0)+'" height="'+(Yw-Yr+28+12)+'" rx="10" fill="#fef2f2" opacity="0.6"/>';
// Right group (auto=oth): light green
var lx2 = LX[2]-BW/2-12, lx3 = LX[3]+BW/2+12;
svg += '<rect x="'+lx2+'" y="'+(Yr-28)+'" width="'+(lx3-lx2)+'" height="'+(Yw-Yr+28+12)+'" rx="10" fill="#f0fdf4" opacity="0.6"/>';
// ── edges root → L1 (with prob labels) ──
var rootBot = Yr+BHr/2, l1Top = Yl-BHl/2;
[0,1,2].forEach(function(j){
svg += edge(RX, rootBot, l1x[j], l1Top, '#94a3b8');
svg += edgeLbl(RX, rootBot, l1x[j], l1Top, '1/3');
});
// Edges L1 to L2 (Chabelo)
var l1Bot = Yl+BHl/2, l2Top = Yc-BHc/2;
svg += edge(l1x[0], l1Bot, LX[0], l2Top, '#94a3b8');
svg += edgeLbl(l1x[0], l1Bot, LX[0], l2Top, '1/2');
svg += edge(l1x[0], l1Bot, LX[1], l2Top, '#94a3b8');
svg += edgeLbl(l1x[0], l1Bot, LX[1], l2Top, '1/2');
svg += edge(l1x[1], l1Bot, LX[2], l2Top, '#94a3b8');
svg += edgeLbl(l1x[1], l1Bot, LX[2], l2Top, '1');
svg += edge(l1x[2], l1Bot, LX[3], l2Top, '#94a3b8');
svg += edgeLbl(l1x[2], l1Bot, LX[3], l2Top, '1');
// Edges L2 to outcome (with joint probability)
var l2Bot = Yc+BHc/2, outTop = Yo-BHo/2;
leaves.forEach(function(lf,i){
var col = lf.switchWins ? '#16a34a' : '#dc2626';
svg += edge(LX[i], l2Bot, LX[i], outTop, col, 2.8);
svg += edgeLbl(LX[i], l2Bot, LX[i], outTop, 'p='+lf.pFrac, col);
});
// Nodes
// Root
svg += bx(RX, Yr, 200, BHr, ['Elegiste la puerta '+choice], '#6366f1', '#fff');
// L1
[0,1,2].forEach(function(j){
var lbl = j===0 ? ['Auto en puerta '+choice, 'p = 1/3']
: ['Auto en puerta '+oth[j-1], 'p = 1/3'];
svg += bx(l1x[j], Yl, BW, BHl, lbl, '#475569', '#fff');
});
// L2 (Chabelo)
leaves.forEach(function(lf,i){
svg += bx(LX[i], Yc, BW, BHc,
['Chabelo abre', 'puerta '+lf.chabelo], '#0284c7', '#fff');
});
// Outcome banners
leaves.forEach(function(lf,i){
var col = lf.switchWins ? '#16a34a' : '#dc2626';
var changeTxt = lf.switchWins ? 'Cambiar: GANAS' : 'Cambiar: PIERDES';
var stayTxt = lf.switchWins ? 'Quedarte: PIERDES' : 'Quedarte: GANAS';
var x=LX[i]-BW/2, y=Yo-BHo/2;
svg += '<rect x="'+x+'" y="'+y+'" width="'+BW+'" height="'+BHo+'" rx="8" fill="'+col+'"/>';
svg += '<text x="'+LX[i]+'" y="'+(y+17)+'" text-anchor="middle" font-size="13.5" '
+'font-family="\'Segoe UI\',sans-serif" fill="white" font-weight="700">'+changeTxt+'</text>';
svg += '<text x="'+LX[i]+'" y="'+(y+33)+'" text-anchor="middle" font-size="11" '
+'font-family="\'Segoe UI\',sans-serif" fill="rgba(255,255,255,.9)">'+stayTxt+'</text>';
});
// Dots row in SVG: each dot represents 1/6 of the sample space.
var dotR = 12, dotGap = 7;
leaves.forEach(function(lf,i){
var col = lf.switchWins ? '#16a34a' : '#dc2626';
var totalW = lf.dots*(dotR*2) + (lf.dots-1)*dotGap;
var sx = LX[i] - totalW/2 + dotR;
for (var d=0; d<lf.dots; d++){
svg += '<circle cx="'+(sx+d*(dotR*2+dotGap))+'" cy="'+Yd+'" r="'+dotR
+'" fill="'+col+'" stroke="white" stroke-width="2.5"/>';
}
// weight label
var wlbl = lf.dots===1 ? 'x1' : 'x2';
svg += '<text x="'+LX[i]+'" y="'+Yw+'" text-anchor="middle" font-size="11" '
+'font-family="\'Segoe UI\',sans-serif" font-weight="700" fill="'+col+'">'+wlbl+'</text>';
});
// Bracket / count annotation
var bktY = Yd + dotR + 6;
// red bracket over cols 0-1
var bktLx = LX[0]-dotR-4, bktRx = LX[1]+dotR+4;
svg += '<line x1="'+bktLx+'" y1="'+bktY+'" x2="'+bktRx+'" y2="'+bktY+'" stroke="#dc2626" stroke-width="1.5" stroke-dasharray="3 2"/>';
// green bracket over cols 2-3
var bktLx2 = LX[2]-dotR-4, bktRx2 = LX[3]+dotR+4+8;
svg += '<line x1="'+bktLx2+'" y1="'+bktY+'" x2="'+bktRx2+'" y2="'+bktY+'" stroke="#16a34a" stroke-width="1.5" stroke-dasharray="3 2"/>';
document.getElementById('cgv3-svg').innerHTML = svg;
// ── dot counter strip (HTML below SVG) ──
var dotsHTML = '';
// 2 red
for (var r=0; r<2; r++){
dotsHTML += '<div style="width:36px;height:36px;border-radius:50%;background:#dc2626;'
+ 'border:3px solid white;box-shadow:0 1px 4px rgba(0,0,0,.18);'
+ 'display:flex;align-items:center;justify-content:center;'
+ 'color:white;font-size:.8rem;font-weight:700;">'+(r+1)+'</div>';
}
dotsHTML += '<div style="width:1px;height:36px;background:#d1d5db;margin:0 6px;"></div>';
// 4 green
for (var g=0; g<4; g++){
dotsHTML += '<div style="width:36px;height:36px;border-radius:50%;background:#16a34a;'
+ 'border:3px solid white;box-shadow:0 1px 4px rgba(0,0,0,.18);'
+ 'display:flex;align-items:center;justify-content:center;'
+ 'color:white;font-size:.8rem;font-weight:700;">'+(g+3)+'</div>';
}
document.getElementById('cgv3-counter').innerHTML =
'<div style="display:flex;align-items:center;gap:1.2rem;padding:.85rem 1.1rem;'
+'background:white;border:1px solid #e2e8f0;border-radius:10px;flex-wrap:wrap;">'
+'<div style="font-size:.88rem;font-weight:700;color:#1e293b;min-width:160px;">'
+'Conteo de estados<br><span style="font-weight:400;color:#64748b;">(cada circulo = 1/6 del espacio muestral)</span></div>'
+'<div style="display:flex;gap:6px;align-items:center;">'+ dotsHTML +'</div>'
+'<div style="border-left:2px solid #e2e8f0;padding-left:1.1rem;font-size:.9rem;line-height:1.6;">'
+'<div><span style="color:#dc2626;font-weight:700;">2 de 6</span>'
+' -> pierdes cambiando = 1/3</div>'
+'<div><span style="color:#16a34a;font-weight:700;">4 de 6</span>'
+' -> <strong>ganas cambiando = 2/3</strong></div>'
+'</div>'
+'</div>';
}
window.cgv3SetChoice = function(d) {
cgv3Choice = d;
[1,2,3].forEach(function(i){
var b=document.getElementById('cgv3-b'+i);
var on=i===d;
b.style.background = on?'#6366f1':'white';
b.style.color = on?'white':'#374151';
b.style.borderColor = on?'#6366f1':'#cbd5e1';
});
render(d);
};
var btns = document.getElementById('cgv3-btns');
[1,2,3].forEach(function(i){
var b=document.createElement('button');
b.id='cgv3-b'+i;
b.textContent='Puerta '+i;
b.setAttribute('onclick','cgv3SetChoice('+i+')');
b.style.cssText='padding:6px 18px;border-radius:8px;border:2px solid #cbd5e1;background:white;'
+'color:#374151;cursor:pointer;font-size:.9rem;font-weight:600;transition:all .15s;';
btns.appendChild(b);
});
cgv3SetChoice(1);
})();
</script>
```
Como en el ejemplo Chabelo abre la puerta 3 y muestra una cabra, debemos quedarnos únicamente con los casos compatibles con esa información. Es decir, descartamos todos los casos donde Chabelo abre la puerta 2 o donde el auto estaba detrás de la puerta 3.
Los casos posibles después de observar que Chabelo abrió la puerta 3 son:
| Caso | Ubicación del auto | Probabilidad inicial | Probabilidad de que Chabelo abra la puerta 3 | Probabilidad conjunta |
|---|---:|---:|---:|---:|
| 1 | Puerta 1 | $\frac{1}{3}$ | $\frac{1}{2}$ | $\frac{1}{3}\cdot\frac{1}{2}=\frac{1}{6}$ |
| 2 | Puerta 2 | $\frac{1}{3}$ | $1$ | $\frac{1}{3}\cdot 1=\frac{1}{3}$ |
| 3 | Puerta 3 | $\frac{1}{3}$ | $0$ | $\frac{1}{3}\cdot 0=0$ |
La probabilidad total de que Chabelo abra la puerta 3 es:
$$
P(\text{Chabelo abre puerta 3})
=
\frac{1}{6}+\frac{1}{3}+0
$$
$$
P(\text{Chabelo abre puerta 3})
=
\frac{1}{6}+\frac{2}{6}
=
\frac{3}{6}
=
\frac{1}{2}
$$
Ahora actualizamos las probabilidades usando la información nueva. Primero, calculemos la probabilidad de que el auto esté en la puerta 1 dado que Chabelo abrió la puerta 3:
$$
P(A_1 \mid \text{Chabelo abre puerta 3})
=
\frac{P(A_1 \cap \text{Chabelo abre puerta 3})}
{P(\text{Chabelo abre puerta 3})}
$$
$$
P(A_1 \mid \text{Chabelo abre puerta 3})
=
\frac{\frac{1}{6}}{\frac{1}{2}}
=
\frac{1}{6}\cdot\frac{2}{1}
=
\frac{2}{6}
=
\frac{1}{3}
$$
Por lo tanto, si nos quedamos con la puerta 1, la probabilidad de ganar sigue siendo:
$$
P(\text{ganar si nos quedamos})
=
\frac{1}{3}
$$
Ahora calculemos la probabilidad de que el auto esté en la puerta 2 dado que Chabelo abrió la puerta 3:
$$
P(A_2 \mid \text{Chabelo abre puerta 3})
=
\frac{P(A_2 \cap \text{Chabelo abre puerta 3})}
{P(\text{Chabelo abre puerta 3})}
$$
$$
P(A_2 \mid \text{Chabelo abre puerta 3})
=
\frac{\frac{1}{3}}{\frac{1}{2}}
=
\frac{1}{3}\cdot\frac{2}{1}
=
\frac{2}{3}
$$
Por lo tanto, si cambiamos a la puerta 2, la probabilidad de ganar es:
$$
P(\text{ganar si cambiamos})
=
\frac{2}{3}
$$
La clave está en que la puerta que elegimos inicialmente conserva su probabilidad original de $\frac{1}{3}$. La información nueva no reparte la probabilidad en partes iguales entre las dos puertas cerradas. Más bien, la probabilidad que antes estaba distribuida entre las dos puertas que no escogimos se concentra en la única puerta no abierta por Chabelo.
En resumen:
$$
P(\text{ganar si nos quedamos})
=
\frac{1}{3}
$$
$$
P(\text{ganar si cambiamos})
=
\frac{2}{3}
$$
Así que la mejor estrategia es cambiar de puerta. La pista de Chabelo no es una simple eliminación de una opción: es evidencia nueva que cambia nuestras probabilidades posteriores. Esta es precisamente la idea central del razonamiento bayesiano: actualizar nuestras creencias iniciales cuando observamos nueva información.
::: {.callout-note appearance="minimal"}
En esta simulación podemos observar como las probabilidades del problema a lo largo de las diferentes realizaciones convergen a los valores teóricos de $\frac{1}{3}$ para la estrategia de quedarse y $\frac{2}{3}$ para la estrategia de cambiar.
```{python}
import random
import plotly.graph_objects as go
def simular_monty_hall_plotly(n_simulaciones=10_000):
puertas = [1, 2, 3]
gana_quedandose = 0
gana_cambiando = 0
probabilidades_quedandose = []
probabilidades_cambiando = []
simulaciones = []
for i in range(1, n_simulaciones + 1):
# El auto se coloca al azar detrás de una puerta
puerta_auto = random.choice(puertas)
# El participante siempre escoge inicialmente la puerta 1
eleccion_inicial = 1
# Chabelo abre una puerta que no fue elegida y que no tiene el auto
puertas_posibles_para_abrir = [
puerta
for puerta in puertas
if puerta != eleccion_inicial and puerta != puerta_auto
]
puerta_abierta = random.choice(puertas_posibles_para_abrir)
# Si el participante cambia, escoge la única puerta cerrada restante
puerta_cambio = [
puerta
for puerta in puertas
if puerta != eleccion_inicial and puerta != puerta_abierta
][0]
# Estrategia 1: quedarse con la puerta inicial
if eleccion_inicial == puerta_auto:
gana_quedandose += 1
# Estrategia 2: cambiar de puerta
if puerta_cambio == puerta_auto:
gana_cambiando += 1
# Probabilidades acumuladas hasta la simulación actual
probabilidades_quedandose.append(gana_quedandose / i)
probabilidades_cambiando.append(gana_cambiando / i)
simulaciones.append(i)
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=simulaciones,
y=probabilidades_quedandose,
mode="lines",
name="Quedarse con la puerta inicial",
line=dict(color="#1f77b4", width=2)
)
)
fig.add_trace(
go.Scatter(
x=simulaciones,
y=probabilidades_cambiando,
mode="lines",
name="Cambiar de puerta",
line=dict(color="#d62728", width=2)
)
)
fig.add_hline(
y=1/3,
line_dash="dash",
line_color="#1f77b4",
annotation_text="Valor teórico: 1/3",
annotation_position="bottom"
)
fig.add_hline(
y=2/3,
line_dash="dash",
line_color="#d62728",
annotation_text="Valor teórico: 2/3",
annotation_position="bottom"
)
fig.update_layout(
title="Evolución de las probabilidades estimadas en el problema de Monty Hall",
xaxis_title="Número de simulaciones",
yaxis_title="Probabilidad acumulada de ganar",
template="plotly_white",
yaxis=dict(range=[0, 1]),
legend=dict(
title="Estrategia",
orientation="h",
yanchor="top",
y=-0.2,
xanchor="center",
x=0.5
)
)
fig.show()
print(f"Simulaciones realizadas: {n_simulaciones}")
print(f"Probabilidad final de ganar quedándose: {probabilidades_quedandose[-1]:.4f}")
print(f"Probabilidad final de ganar cambiando: {probabilidades_cambiando[-1]:.4f}")
simular_monty_hall_plotly(n_simulaciones=10_000)
```
:::
Esta es la diferencia central en los clasificadores bayesianos, la decisión se deriva de probabilidades a priori, densidades condicionales y probabilidades posteriores [@duda2001pattern; @bishop2006pattern]. Por ejemplo, si una caja contiene monedas de dos tipos y una de ellas es más frecuente, la decisión con menor probabilidad de error antes de observar la moneda es escoger la clase más probable. Sin embargo, una vez que se observa una característica, como el peso, la decisión debe incorporar esa evidencia. La pregunta deja de ser “¿qué clase es más frecuente?” y pasa a ser “¿qué clase es más probable dado el valor observado?”.
:::{.column-margin}
Si tuvieramos que clasificar entre monedas de un centavo y monedas de diez centavos, antes de observar el peso, la mejor decisión sería escoger la clase más frecuente. Sin embargo, una vez que se observa el peso, la decisión debe incorporar esa evidencia. La pregunta deja de ser “¿qué clase es más frecuente?” y pasa a ser “¿qué clase es más probable dado el valor observado?”.
{width="70%"}
:::
Los clasificadores bayesianos usan el teorema de Bayes para decidir a qué clase pertenece una observación. Para cada clase $\omega_i$, se considera qué tan probable es observar los datos $\mathbf{x}$ si esa clase fuera la correcta, junto con qué tan probable era esa clase antes de ver los datos. Con esta información se calcula la probabilidad de cada clase después de observar $\mathbf{x}$. La decisión más sencilla consiste en elegir la clase con mayor probabilidad posterior; si algunos errores son más costosos que otros, se puede modificar la decisión para tomar en cuenta esos costos [@duda2001pattern; @jaynes2003probability].
Este capítulo desarrolla la clasificación bayesiana desde sus fundamentos hasta algunas variantes prácticas: Naïve Bayes, clasificadores gaussianos, modelos de Markov para secuencias discretas, decisión bayesiana con costos y el criterio minimax cuando las probabilidades previas son inciertas. Los ejemplos computacionales están escritos en Python y buscan conectar la teoría con implementaciones reproducibles.
## Vista probabilística de la clasificación
Sea $\mathbf{x}\in\mathbb{R}^d$ una observación y sea $\Omega=\{\omega_1,\dots,\omega_c\}$ el conjunto de clases posibles. En clasificación probabilística se supone que $\mathbf{x}$ no determina de manera absoluta la clase, sino que modifica nuestras creencias sobre ella. Antes de observar $\mathbf{x}$, la información disponible está representada por las probabilidades previas $P(\omega_i)$. Después de observar $\mathbf{x}$, la información relevante se resume en las probabilidades posteriores $P(\omega_i\mid \mathbf{x})$.
::: {.callout-note appearance="minimal"}
Definición. La probabilidad posterior de la clase $\omega_i$ dada una observación $\mathbf{x}$ se obtiene mediante el teorema de Bayes:
$$
P(\omega_i\mid \mathbf{x}) = \frac{p(\mathbf{x}\mid \omega_i)P(\omega_i)}{p(\mathbf{x})}
$$ {#eq-bayes-posterior}
:::
En la ecuación @eq-bayes-posterior, $p(\mathbf{x}\mid\omega_i)$ es la densidad condicional de la observación bajo la clase $\omega_i$, $P(\omega_i)$ es la probabilidad previa de esa clase y $p(\mathbf{x})$ es la densidad marginal de la observación:
$$
p(\mathbf{x}) = \sum_{j=1}^c p(\mathbf{x}\mid\omega_j)P(\omega_j).
$$
Como $p(\mathbf{x})$ no depende de la clase que se compara, muchas reglas de decisión pueden escribirse en términos de la cantidad no normalizada $p(\mathbf{x}\mid\omega_i)P(\omega_i)$.
#### La decisión de clasificación {.unnumbered}
Si no hay observaciones, una regla razonable para dos clases es escoger la clase más probable a priori:
$$
\text{decidir }\omega_1 \quad \text{si} \quad P(\omega_1)>P(\omega_2).
$$
El error esperado de esta regla, cuando sólo existen dos clases, es
$$
P(e)=\min\{P(\omega_1),P(\omega_2)\}.
$$
Una vez observada $\mathbf{x}$, la decisión cambia: se debe comparar $P(\omega_1\mid\mathbf{x})$ contra $P(\omega_2\mid\mathbf{x})$. Esta distinción es la esencia de la clasificación bayesiana: la evidencia observada actualiza las probabilidades de las clases.
::: {.callout-note appearance="minimal"}
Definición. La regla de máxima probabilidad posterior, o regla MAP, asigna la observación $\mathbf{x}$ a la clase con posterior máxima:
$$
\hat{\omega}(\mathbf{x}) = \arg\max_{\omega_i\in\Omega} P(\omega_i\mid\mathbf{x})
$$ {#eq-map-rule}
:::
### Ejemplo básico: El dilema del Cine
::: columns
::: {.column width="55%"}
**Esta persona dejó caer su boleto en el pasillo.**
¿Le dices?
> “Disculpe, señora”
o
> “Disculpe, señor”
**Tienes que hacer una conjetura.**
:::
::: {.column width="45%"}
{width="70%"}
:::
:::
Para responder de la manera mas acertada posible podemos hacer uso del teorema de Bayes, para esto definamos el evento $H$ como “hombre” y el evento $M$ como “mujer”, mientras que la evidencia observada es el tipo de cabello de la persona. Entonces, la probabilidad de que la persona sea hombre o mujer dado el tipo de cabello se calcula con Bayes:
$$
P(\text{sexo}\mid \text{cabello})
=
\frac{
P(\text{cabello}\mid \text{sexo})P(\text{sexo})
}{
\sum_{\text{sexo}} P(\text{sexo})P(\text{cabello}\mid \text{sexo})
}
$$
Donde bajo una asumpción justa podemos suponer que dentro del cine:
::: columns
::: {.column width="50%"}
**De cada 100 mujeres en el cine**
- 50 tienen cabello corto
- 50 tienen cabello largo
:::
::: {.column width="50%"}
**De cada 100 hombres en el cine**
- 96 tienen cabello corto
- 4 tienen cabello largo
:::
:::
Hagamos los calculos paso a paso, si asumimos que la proporción de hombres y mujeres en el cine es la misma, entonces las probabilidades previas son:
$$P(H)=P(M)=0.5$$
Mientras que las verosimilitudes son:
$$P(\text{cabello largo}\mid H)=0.04$$
$$P(\text{cabello largo}\mid M)=0.50$$
teniendo esto, Entonces los numeradores son:
$$
P(\text{largo}\mid H)P(H)=0.04(0.5)=0.02
$$
$$
P(\text{largo}\mid M)P(M)=0.50(0.5)=0.25
$$
Entonces:
$$
P(H\mid \text{cabello largo}) = 0.02A \approx 0.07
$$
$$
P(M\mid \text{cabello largo}) = 0.25A \approx 0.93
$$
donde
$$
A=\frac{1}{0.02+0.25}
$$
Así, bajo la regla de máxima probabilidad posterior, la decisión es:
$$
\Rightarrow \textbf{Decisión: mujer}
$$
::: {.callout-note}
La idea central es que no decidimos solo por la característica observada, sino por cómo esa característica modifica nuestras creencias previas sobre cada clase.
:::
### Dilema en el cine: el contexto importa
Es importante notar que la decisión anterior depende de las probabilidades previas. Si el contexto cambia, las probabilidades previas también cambian, lo que puede llevar a una decisión diferente incluso con la misma evidencia observada. Por ejemplo, si esta situación ocurre mientras estás formado en la fila del baño de hombres, la proporción de hombres y mujeres cambia drásticamente, lo que afecta las probabilidades previas y, por ende, la decisión bayesiana.
::: columns
::: {.column width="50%"}
¿Le dices?
> “Disculpe, señora”
o
> “Disculpe, señor”
:::
::: {.column width="50%"}
{width="80%"}
:::
:::
Ahora las probabilidades previas cambian:
$$
P(H)=0.98,
\qquad
P(M)=0.02
$$
donde $H$ representa “hombre” y $M$ representa “mujer”. De nuevo asumiendo de manera justa que dentro de la fila:
::: columns
::: {.column width="50%"}
**De cada 2 mujeres en la fila**
- 1 tiene cabello corto
- 1 tiene cabello largo
:::
::: {.column width="50%"}
**De cada 98 hombres en la fila**
- 94 tienen cabello corto
- 4 tienen cabello largo
:::
:::
De los datos anteriores:
$$
P(\text{cabello largo}\mid H)=\frac{4}{98}
$$
$$
P(\text{cabello largo}\mid M)=\frac{1}{2}
$$
Calculamos los numeradores de Bayes:
$$
P(\text{cabello largo}\mid H)P(H)
=
\frac{4}{98}(0.98)
=
0.04
$$
$$
P(\text{cabello largo}\mid M)P(M)
=
\frac{1}{2}(0.02)
=
0.01
$$
La constante de normalización es:
$$
B=
\frac{1}{0.04+0.01}
=
20
$$
Entonces, bajo la misma regla de máxima probabilidad posterior, las probabilidades posteriores son:
$$
P(H\mid \text{cabello largo})
=
0.04B
=
0.04(20)
=
0.80
$$
$$
P(M\mid \text{cabello largo})
=
0.01B
=
0.01(20)
=
0.20
$$
Como:
$$
P(H\mid \text{cabello largo})
>
P(M\mid \text{cabello largo})
$$
la decisión bayesiana es:
$$
\Rightarrow \textbf{Decisión: hombre}
$$
::: {.callout-important}
La misma evidencia observada (cabello largo) puede llevar a una decisión distinta cuando cambian las probabilidades previas.
En clasificación bayesiana, el contexto modifica la inferencia.
:::
### Implementación básica: algoritmo general de un clasificador bayesiano
A continuación se muestra una implementación básica de un clasificador bayesiano para cualquier número de clases. El código calcula las probabilidades posteriores y toma la decisión de clasificación basada en la clase con mayor probabilidad posterior.
```{python}
#| echo: true
#| code-fold: false
import numpy as np
def clasificador_bayesiano(verosimilitudes, priors, clases):
"""
Clasificador bayesiano para cualquier número de clases.
verosimilitudes: p(x | omega_i) para cada clase.
priors: P(omega_i) para cada clase.
clases: nombres de las clases.
"""
verosimilitudes = np.array(verosimilitudes, dtype=float)
priors = np.array(priors, dtype=float)
clases = np.array(clases)
evidencia = np.sum(verosimilitudes * priors)
posteriores = (verosimilitudes * priors) / evidencia
indice_decision = np.argmax(posteriores)
decision = clases[indice_decision]
return posteriores, decision
clases = ["normal", "enfermedad"]
# p(x | omega_i): qué tan probable es observar x en cada clase
verosimilitudes = [0.2, 0.4]
# P(omega_i): probabilidad previa de cada clase
priors = [0.9, 0.1]
posteriores, decision = clasificador_bayesiano(
verosimilitudes,
priors,
clases
)
for clase, posterior in zip(clases, posteriores):
print(f"P({clase} | x) = {posterior:.3f}")
print("Decisión:", decision)
```
En este ejemplo, aunque la verosimilitud bajo la clase de enfermedad es mayor, el prior de la clase normal domina la decisión de error mínimo. Esto ilustra una idea importante: una evidencia local no se interpreta de manera aislada, sino en relación con la frecuencia previa de cada clase.
```{=html}
<div id="bayes-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;">Actualización bayesiana — prior → posterior</span><br>
<span style="font-size:0.81em;color:#666;">Ajusta el prior P(ω₁) y las verosimilitudes p(x|ω) para ver cómo cambia la decisión</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px;">
<div>
<label style="font-size:0.8em;color:#444;">P(ω₁) prior = <strong id="bpv">0.90</strong></label><br>
<input id="bps" type="range" min="0.05" max="0.95" step="0.05" value="0.90" style="width:100%;accent-color:#1a3a5c;">
</div>
<div>
<label style="font-size:0.8em;color:#444;">p(x|ω₁) = <strong id="bl1v">0.20</strong></label><br>
<input id="bl1" type="range" min="0.01" max="0.99" step="0.01" value="0.20" style="width:100%;accent-color:#2563eb;">
</div>
<div>
<label style="font-size:0.8em;color:#444;">p(x|ω₂) = <strong id="bl2v">0.40</strong></label><br>
<input id="bl2" type="range" min="0.01" max="0.99" step="0.01" value="0.40" style="width:100%;accent-color:#dc2626;">
</div>
</div>
<div id="bayes-info" 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:22px;"></div>
<canvas id="bayes-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
</div>
<script>
(function(){
const cv=document.getElementById('bayes-cv');
const W=680,H=260,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 C1='#2563eb',C2='#dc2626';
function bar(x,y,w,h,col,alpha){
ctx.globalAlpha=alpha||1;
ctx.fillStyle=col;ctx.fillRect(x,y,w,h);
ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.globalAlpha=1;
ctx.strokeRect(x,y,w,h);
}
function draw(){
const pw1=parseFloat(document.getElementById('bps').value);
const lk1=parseFloat(document.getElementById('bl1').value);
const lk2=parseFloat(document.getElementById('bl2').value);
const pw2=1-pw1;
const evidence=lk1*pw1+lk2*pw2;
const post1=lk1*pw1/evidence;
const post2=lk2*pw2/evidence;
const lr=lk1/lk2;
ctx.clearRect(0,0,W,H);
const panW=W/3-16,pH=H-60,base=H-30,pad=24;
// Left: priors
const ph1L=pw1*pH,ph2L=pw2*pH;
bar(pad,base-ph1L,panW*0.42,ph1L,C1,0.8);
bar(pad+panW*0.48,base-ph2L,panW*0.42,ph2L,C2,0.8);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Priors',pad+panW*0.45,22);
ctx.fillStyle=C1;ctx.font='10px Georgia,serif';
ctx.fillText('P(ω₁)='+pw1.toFixed(2),pad+panW*0.21,base-ph1L-5);
ctx.fillStyle=C2;ctx.fillText('P(ω₂)='+pw2.toFixed(2),pad+panW*0.69,base-ph2L-5);
ctx.fillStyle='#888';ctx.font='9px Georgia,serif';
ctx.fillText('ω₁',pad+panW*0.21,base+12);ctx.fillText('ω₂',pad+panW*0.69,base+12);
// Middle: likelihoods
const mx=W/3+8;
const ph1M=lk1*pH,ph2M=lk2*pH;
bar(mx,base-ph1M,panW*0.42,ph1M,C1,0.8);
bar(mx+panW*0.48,base-ph2M,panW*0.42,ph2M,C2,0.8);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Verosimilitudes',mx+panW*0.45,22);
ctx.fillStyle=C1;ctx.font='10px Georgia,serif';
ctx.fillText('p(x|ω₁)='+lk1.toFixed(2),mx+panW*0.21,base-ph1M-5);
ctx.fillStyle=C2;ctx.fillText('p(x|ω₂)='+lk2.toFixed(2),mx+panW*0.69,base-ph2M-5);
ctx.fillStyle='#888';ctx.font='9px Georgia,serif';
ctx.fillText('ω₁',mx+panW*0.21,base+12);ctx.fillText('ω₂',mx+panW*0.69,base+12);
// Right: posteriors
const rx=2*W/3+8;
const ph1R=post1*pH,ph2R=post2*pH;
bar(rx,base-ph1R,panW*0.42,ph1R,C1,0.9);
bar(rx+panW*0.48,base-ph2R,panW*0.42,ph2R,C2,0.9);
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Posteriores',rx+panW*0.45,22);
ctx.fillStyle=C1;ctx.font='10px Georgia,serif';
ctx.fillText('P(ω₁|x)='+post1.toFixed(3),rx+panW*0.21,base-ph1R-5);
ctx.fillStyle=C2;ctx.fillText('P(ω₂|x)='+post2.toFixed(3),rx+panW*0.69,base-ph2R-5);
ctx.fillStyle='#888';ctx.font='9px Georgia,serif';
ctx.fillText('ω₁',rx+panW*0.21,base+12);ctx.fillText('ω₂',rx+panW*0.69,base+12);
// Decision box
const dec=post1>post2?'ω₁':'ω₂';
const decCol=post1>post2?C1:C2;
ctx.strokeStyle='#e0e5ed';ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(W/3,35);ctx.lineTo(W/3,H-20);ctx.stroke();
ctx.beginPath();ctx.moveTo(2*W/3,35);ctx.lineTo(2*W/3,H-20);ctx.stroke();
document.getElementById('bayes-info').innerHTML=
`Razón de verosimilitudes ℓ(x) = <strong>${lr.toFixed(3)}</strong> · `+
`Umbral = P(ω₂)/P(ω₁) = <strong>${(pw2/pw1).toFixed(3)}</strong> · `+
`Decisión MAP: <strong style="color:${decCol}">${dec}</strong>`;
}
['bps','bl1','bl2'].forEach(id=>{
const el=document.getElementById(id);
const vid={'bps':'bpv','bl1':'bl1v','bl2':'bl2v'}[id];
el.addEventListener('input',function(){
document.getElementById(vid).textContent=parseFloat(this.value).toFixed(2);
draw();
});
});
draw();
})();
</script>
```
## Clasificador bayesiano de error mínimo
La regla MAP tiene una interpretación óptima cuando todos los errores tienen el mismo costo: minimiza la probabilidad promedio de clasificación incorrecta. Para verlo, consideremos inicialmente dos clases. Si se asigna $\mathbf{x}$ a $\omega_1$, el error condicional es $P(\omega_2\mid\mathbf{x})$; si se asigna a $\omega_2$, el error condicional es $P(\omega_1\mid\mathbf{x})$. Por tanto, para minimizar el error en cada punto $\mathbf{x}$ se elige la clase con mayor posterior.
::: {.callout-note appearance="minimal"}
Definición. El error condicional de clasificación para dos clases es
$$
P(e\mid\mathbf{x})=
\begin{cases}
P(\omega_2\mid\mathbf{x}), & \text{si se decide }\omega_1,\\
P(\omega_1\mid\mathbf{x}), & \text{si se decide }\omega_2.
\end{cases}
$$ {#eq-conditional-error}
:::
El error promedio se obtiene integrando sobre todo el espacio de observaciones:
$$
P(e)=\int P(e\mid\mathbf{x})p(\mathbf{x})\,d\mathbf{x}.
$$
Dado que $p(\mathbf{x})\geq 0$, minimizar el error promedio se logra minimizando el error condicional punto a punto. Así, para dos clases:
$$
P(\omega_1\mid\mathbf{x})>P(\omega_2\mid\mathbf{x})
\quad \Longrightarrow \quad
\mathbf{x}\in\omega_1.
$$
### Ejemplo en Python: decisión de error mínimo
El siguiente código muestra la regla de error mínimo para varias observaciones. Para cada observación se tienen las verosimilitudes $p(\mathbf{x}\mid\omega_i)$ bajo dos clases. Después se calculan las posteriores, se elige la clase con mayor posterior y se reporta el error condicional mínimo.
```{python}
#| code-fold: false
import numpy as np
clases = np.array(["normal", "enfermedad"])
# Probabilidades previas P(omega_i)
priors = np.array([0.8, 0.2])
# Cada fila representa una observación distinta.
# Las columnas contienen p(x | normal) y p(x | enfermedad).
verosimilitudes = np.array([
[0.50, 0.10],
[0.35, 0.25],
[0.20, 0.45],
[0.10, 0.60],
[0.40, 0.30]
])
# Numerador de Bayes: p(x | omega_i) P(omega_i)
puntajes_no_normalizados = verosimilitudes * priors
# Evidencia p(x), calculada para cada observación
evidencia = puntajes_no_normalizados.sum(axis=1, keepdims=True)
# Posteriores P(omega_i | x)
posteriores = puntajes_no_normalizados / evidencia
# Regla de error mínimo: elegir la clase con mayor posterior
indices_decision = np.argmax(posteriores, axis=1)
decisiones = clases[indices_decision]
# El error condicional mínimo es 1 menos la posterior más grande
errores_condicionales = 1 - np.max(posteriores, axis=1)
for i, (post, decision, error) in enumerate(
zip(posteriores, decisiones, errores_condicionales),
start=1
):
print(f"Observación {i}")
print(f" P(normal | x) = {post[0]:.3f}")
print(f" P(enfermedad | x) = {post[1]:.3f}")
print(f" Decisión de error mínimo: {decision}")
print(f" Error condicional mínimo: {error:.3f}\n")
```
En este ejemplo, una observación se clasifica como `enfermedad` sólo cuando su posterior supera a la posterior de `normal`. No basta con que $p(\mathbf{x}\mid\text{enfermedad})$ sea grande de manera aislada: también importa el prior de cada clase. Por eso la decisión se basa en el producto $p(\mathbf{x}\mid\omega_i)P(\omega_i)$ y no únicamente en la verosimilitud.
### Formas equivalentes de la regla de decisión
Usando el teorema de Bayes, la comparación de posteriores puede escribirse como una comparación de productos entre verosimilitud y prior:
$$
p(\mathbf{x}\mid\omega_1)P(\omega_1) > p(\mathbf{x}\mid\omega_2)P(\omega_2).
$$
Para dos clases, también puede expresarse mediante el cociente de verosimilitudes.
::: {.callout-note appearance="minimal"}
Definición. El cociente de verosimilitudes entre dos clases se define como
$$
\ell(\mathbf{x})=\frac{p(\mathbf{x}\mid\omega_1)}{p(\mathbf{x}\mid\omega_2)}
$$ {#eq-likelihood-ratio}
:::
La regla de decisión se vuelve
$$
\ell(\mathbf{x}) \gtrless_{\omega_2}^{\omega_1} \frac{P(\omega_2)}{P(\omega_1)}.
$$
Si se trabaja en escala logarítmica, se evita el subdesbordamiento numérico y se transforman productos en sumas:
$$
\log \ell(\mathbf{x}) = \log p(\mathbf{x}\mid\omega_1)-\log p(\mathbf{x}\mid\omega_2).
$$
Esta forma es especialmente útil en alta dimensión, en secuencias largas y en modelos donde las densidades son productos de muchas probabilidades pequeñas.
## Clasificador Naïve Bayes
En alta dimensión, estimar la densidad conjunta $p(\mathbf{x}\mid\omega_i)$ puede ser difícil. Si $\mathbf{x}=(x_1,\dots,x_d)$, una densidad conjunta general requiere modelar dependencias entre todas las variables. Por ejemplo, en clasificación de textos no basta con preguntar si aparece una palabra: un documento puede contener cientos o miles de términos, y cada término puede aparecer varias veces.
Naïve Bayes simplifica el problema suponiendo independencia condicional de las características dada la clase [@mccallum1998comparison; @murphy2012machine]. La palabra *naïve* no significa que el método sea inútil, sino que usa una suposición fuerte: una vez que conocemos la clase, tratamos a las características como si aportaran evidencia de manera separada.
::: {.callout-note appearance="minimal"}
Definición. El supuesto de independencia condicional de Naïve Bayes establece que
$$
p(\mathbf{x}\mid\omega_i)=p(x_1,x_2,\dots,x_d\mid\omega_i)=\prod_{k=1}^{d}p(x_k\mid\omega_i)
$$ {#eq-naive-bayes-assumption}
:::
Con este supuesto, la regla MAP se escribe como:
$$
\hat{\omega}(\mathbf{x})=\arg\max_i \left[\log P(\omega_i)+\sum_{k=1}^{d}\log p(x_k\mid\omega_i)\right].
$$
Esta expresión tiene una lectura sencilla. El término $\log P(\omega_i)$ representa qué tan probable era la clase antes de observar el documento. La suma $\sum_{k=1}^{d}\log p(x_k\mid\omega_i)$ acumula la evidencia que aportan las características observadas. La clase elegida es aquella que obtiene la puntuación total más alta.
```{python}
#| label: fig-nb-independencia
#| fig-cap: "Comparación geométrica entre el supuesto gaussiano general (QDA, covarianza completa) y Naïve Bayes gaussiano (covarianza diagonal). QDA permite elipses rotadas arbitrariamente; NB solo puede representar elipses alineadas con los ejes."
#| code-fold: true
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
rng = np.random.default_rng(7)
# Datos con correlación entre variables
cov_full = np.array([[1.2, 0.85], [0.85, 0.9]])
cov_diag = np.diag(np.diag(cov_full)) # versión diagonal (NB)
mu1 = np.array([-1.5, -1.0])
mu2 = np.array([1.5, 1.0])
X1 = rng.multivariate_normal(mu1, cov_full, 80)
X2 = rng.multivariate_normal(mu2, cov_full, 80)
def ellipse_points(mu, cov, n_std=2, n_pts=120):
"""Elipse de confianza via descomposición espectral."""
vals, vecs = np.linalg.eigh(cov)
t = np.linspace(0, 2 * np.pi, n_pts)
circle = np.stack([np.cos(t), np.sin(t)], axis=1)
ellipse = circle @ (vecs * np.sqrt(vals) * n_std).T + mu
return ellipse[:, 0], ellipse[:, 1]
def grid_posterior(X1, X2, cov, xlim, ylim, n=80):
"""Probabilidad posterior P(clase 1 | x) en un grid 2D."""
def mvn(X, mu, cov):
D = X.shape[1]
diff = X - mu
inv_cov = np.linalg.inv(cov)
sign, logdet = np.linalg.slogdet(cov)
maha = np.einsum('ij,jk,ik->i', diff, inv_cov, diff)
return np.exp(-0.5 * maha - 0.5 * (D * np.log(2 * np.pi) + logdet))
xg = np.linspace(xlim[0], xlim[1], n)
yg = np.linspace(ylim[0], ylim[1], n)
Xg, Yg = np.meshgrid(xg, yg)
pts = np.c_[Xg.ravel(), Yg.ravel()]
mu1_ = X1.mean(axis=0)
mu2_ = X2.mean(axis=0)
p1 = mvn(pts, mu1_, cov)
p2 = mvn(pts, mu2_, cov)
post = p1 / (p1 + p2 + 1e-300)
return xg, yg, post.reshape(n, n)
xlim = (-5, 5); ylim = (-5, 5)
xg, yg, Z_full = grid_posterior(X1, X2, cov_full, xlim, ylim)
xg, yg, Z_diag = grid_posterior(X1, X2, cov_diag, xlim, ylim)
mu1_est = X1.mean(axis=0)
mu2_est = X2.mean(axis=0)
fig = make_subplots(
rows=1, cols=2,
subplot_titles=["QDA (covarianza completa)", "Naïve Bayes (covarianza diagonal)"]
)
for col, cov, Z, title in [(1, cov_full, Z_full, "QDA"), (2, cov_diag, Z_diag, "NB")]:
fig.add_trace(go.Contour(
x=xg, y=yg, z=Z,
colorscale=[[0, "#4C72B0"], [0.5, "white"], [1, "#DD8452"]],
opacity=0.40,
showscale=False,
contours=dict(showlines=True, start=0.1, end=0.9, size=0.2,
coloring="fill")
), row=1, col=col)
for X, mu_e, c_col, name in [
(X1, mu1_est, "#2563eb", "Clase 1"),
(X2, mu2_est, "#dc2626", "Clase 2")
]:
fig.add_trace(go.Scatter(
x=X[:, 0], y=X[:, 1],
mode="markers",
marker=dict(color=c_col, size=5, opacity=0.55),
name=name, showlegend=(col == 1)
), row=1, col=col)
ex, ey = ellipse_points(mu_e, cov if col == 1 else np.diag(np.diag(cov_full)))
fig.add_trace(go.Scatter(
x=ex, y=ey, mode="lines",
line=dict(color=c_col, width=2.2, dash="solid"),
showlegend=False
), row=1, col=col)
fig.update_layout(
title="Supuesto de independencia condicional: QDA vs Naïve Bayes",
template="plotly_white",
height=420,
legend=dict(orientation="h", yanchor="bottom", y=1.08, xanchor="right", x=1)
)
fig.update_xaxes(range=xlim, title_text="x₁")
fig.update_yaxes(range=ylim, title_text="x₂")
fig.show()
```
### Naïve Bayes multinomial para texto
Una de las versiones más usadas en clasificación de documentos es Naïve Bayes multinomial. En este caso, cada documento se representa como un vector de conteos:
$$
\mathbf{x}=(x_1,x_2,\dots,x_d),
$$
donde $x_k$ indica cuántas veces aparece la palabra $k$ del vocabulario en el documento. Si $\theta_{ik}$ representa la probabilidad de observar la palabra $k$ dentro de documentos de la clase $\omega_i$, entonces el modelo asigna la siguiente puntuación:
$$
g_i(\mathbf{x})
=
\log P(\omega_i)
+
\sum_{k=1}^{d}x_k\log \theta_{ik}.
$$
La decisión se toma escogiendo la clase con mayor puntuación:
$$
\hat{\omega}(\mathbf{x})
=
\arg\max_i g_i(\mathbf{x}).
$$
En problemas de texto, las probabilidades $\theta_{ik}$ se estiman contando palabras en los documentos de entrenamiento. Si $N_{ik}$ es el número de veces que aparece la palabra $k$ en documentos de la clase $\omega_i$, una estimación directa sería:
$$
\theta_{ik}
=
\frac{N_{ik}}{\sum_{r=1}^{d}N_{ir}}.
$$
El problema es que si una palabra nunca aparece en una clase durante el entrenamiento, entonces $N_{ik}=0$ y su probabilidad estimada sería cero. Como el modelo multiplica probabilidades, una sola palabra con probabilidad cero puede anular toda la puntuación de una clase. Para evitarlo se usa suavizado de Laplace:
$$
\theta_{ik}
=
\frac{N_{ik}+\alpha}{\sum_{r=1}^{d}N_{ir}+\alpha d}.
$$
Cuando $\alpha=1$, se interpreta como si agregáramos una aparición ficticia de cada palabra en cada clase. Esto evita probabilidades cero y vuelve más estable al clasificador.
::: {.callout-tip}
En la práctica se trabaja con logaritmos, no con productos directos. Esto evita problemas numéricos cuando se multiplican muchas probabilidades pequeñas y convierte el producto de evidencias en una suma de evidencias.
:::
### Ejemplo manual: clasificación de un documento corto
Supongamos que queremos clasificar documentos en dos clases: `spam` y `ham`. Después de entrenar el modelo, podríamos obtener las siguientes probabilidades:
| Palabra | $P(\text{palabra}\mid\text{spam})$ | $P(\text{palabra}\mid\text{ham})$ |
|---|---:|---:|
| free | $0.30$ | $0.05$ |
| prize | $0.25$ | $0.05$ |
| project | $0.05$ | $0.30$ |
Si los priors son iguales,
$$
P(\text{spam})=P(\text{ham})=0.5,
$$
y observamos el documento:
$$
\mathbf{x}=\text{``free project prize''},
$$
entonces las puntuaciones logarítmicas son:
$$
g_{\text{spam}}(\mathbf{x})
=
\log(0.5)+\log(0.30)+\log(0.05)+\log(0.25),
$$
$$
g_{\text{ham}}(\mathbf{x})
=
\log(0.5)+\log(0.05)+\log(0.30)+\log(0.05).
$$
Aunque la palabra `project` favorece a `ham`, las palabras `free` y `prize` favorecen a `spam`. El clasificador suma toda la evidencia y decide por la clase con mayor puntuación final.
```{python}
#| label: fig-nb-palabras
#| fig-cap: "Verosimilitudes por clase para el ejemplo spam/ham. Las barras muestran P(palabra|clase) para cada término del vocabulario. Las palabras con mayor diferencia entre barras son las más discriminativas."
#| code-fold: true
import numpy as np
import plotly.graph_objects as go
palabras = ["free", "prize", "project", "meeting", "offer", "money", "update", "win"]
p_spam = np.array([0.30, 0.25, 0.05, 0.03, 0.18, 0.22, 0.02, 0.15])
p_ham = np.array([0.05, 0.05, 0.30, 0.28, 0.06, 0.04, 0.20, 0.02])
# Log-likelihood ratio como barra de fondo (discriminatividad)
llr = np.log(p_spam / p_ham)
fig = go.Figure()
fig.add_trace(go.Bar(
name="P(palabra | spam)",
x=palabras, y=p_spam,
marker_color="#dc2626",
marker_line=dict(color="#7f1d1d", width=1),
opacity=0.85
))
fig.add_trace(go.Bar(
name="P(palabra | ham)",
x=palabras, y=p_ham,
marker_color="#2563eb",
marker_line=dict(color="#1e3a6e", width=1),
opacity=0.85
))
# Anotación de log-ratio sobre cada palabra
for i, (palabra, ratio) in enumerate(zip(palabras, llr)):
color = "#dc2626" if ratio > 0 else "#2563eb"
symbol = "▲" if ratio > 0 else "▼"
fig.add_annotation(
x=palabra,
y=max(p_spam[i], p_ham[i]) + 0.015,
text=f"{symbol} {abs(ratio):.2f}",
showarrow=False,
font=dict(size=10, color=color),
yanchor="bottom"
)
fig.update_layout(
title="Verosimilitudes por clase — ejemplo spam/ham",
xaxis_title="Palabra",
yaxis_title="P(palabra | clase)",
barmode="group",
template="plotly_white",
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="right", x=1),
annotations=[dict(
x=0.5, y=-0.18, xref="paper", yref="paper",
text="▲/▼ = log P(spam)/P(ham). Positivo → favorece spam; negativo → favorece ham",
showarrow=False, font=dict(size=11, color="#64748b")
)]
)
fig.show()
```
### Ejemplo en Python: Naïve Bayes para texto
```{python}
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
texts = [
"free money offer now",
"win money free prize",
"meeting schedule project update",
"project discussion and meeting notes",
"free prize offer",
"schedule the project meeting"
]
labels = ["spam", "spam", "ham", "ham", "spam", "ham"]
model = make_pipeline(
CountVectorizer(),
MultinomialNB(alpha=1.0)
)
model.fit(texts, labels)
new_docs = ["free project prize", "meeting update schedule"]
print(model.predict(new_docs))
print(model.predict_proba(new_docs))
```
El parámetro `alpha=1.0` introduce suavizado de Laplace. Este suavizado evita que una palabra ausente en el entrenamiento de una clase anule completamente la probabilidad de un documento.
También podemos inspeccionar el vocabulario aprendido y las probabilidades estimadas por clase:
```{python}
import numpy as np
vectorizer = model.named_steps["countvectorizer"]
clf = model.named_steps["multinomialnb"]
vocabulario = vectorizer.get_feature_names_out()
for clase, log_probs in zip(clf.classes_, clf.feature_log_prob_):
print(f"\nClase: {clase}")
for palabra, log_prob in zip(vocabulario, log_probs):
print(f"{palabra:12s} P(palabra | clase) = {np.exp(log_prob):.3f}")
```
Este segundo bloque muestra de manera explícita qué aprendió el modelo: para cada clase, estima qué palabras son más probables. Por eso Naïve Bayes suele ser fácil de interpretar en texto. Una palabra como `free` debería tener mayor probabilidad bajo `spam`, mientras que una palabra como `meeting` debería tener mayor probabilidad bajo `ham`.
::: {.callout-important}
Naïve Bayes no necesita que el supuesto de independencia sea perfectamente cierto para clasificar bien. Lo importante es que las puntuaciones relativas separen razonablemente las clases. Por eso puede funcionar bien incluso cuando las palabras de un documento claramente no son independientes entre sí.
:::
### Variantes comunes de Naïve Bayes
La misma idea puede adaptarse a distintos tipos de características:
| Variante | Tipo de característica | Ejemplo de uso |
|---|---|---|
| Bernoulli Naïve Bayes | Presencia o ausencia de una característica | Si una palabra aparece o no aparece |
| Multinomial Naïve Bayes | Conteos discretos | Frecuencias de palabras en documentos |
| Gaussian Naïve Bayes | Variables continuas | Mediciones físicas, señales o atributos numéricos |
La diferencia entre estas variantes está en cómo se modela $p(x_k\mid\omega_i)$. El principio de decisión sigue siendo el mismo: estimar una probabilidad por clase, sumar la evidencia de las características y elegir la clase con mayor posterior.
Aunque el supuesto de independencia suele ser falso, el clasificador puede funcionar muy bien en tareas de texto, filtrado de spam y clasificación de documentos, porque para decidir no siempre se necesita estimar perfectamente las probabilidades; basta con obtener fronteras de decisión útiles [@mccallum1998comparison].
## Clasificadores bayesianos gaussianos
Una familia muy importante de clasificadores bayesianos se obtiene cuando las densidades condicionales de clase se modelan mediante distribuciones normales. Este supuesto es razonable cuando las características observadas resultan de la agregación de muchos factores independientes o débilmente dependientes, de acuerdo con la intuición asociada al teorema central del límite [@wasserman2004all; @bishop2006pattern].
::: {.callout-note appearance="minimal"}
Definición. Una variable aleatoria continua unidimensional $x$ tiene distribución normal con media $\mu$ y varianza $\sigma^2$, denotada $\mathcal{N}(\mu,\sigma^2)$, si su densidad es
$$
p(x)=\frac{1}{\sqrt{2\pi\sigma^2}}\exp\left[-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2\right]
$$ {#eq-normal-univariate}
:::
En dimensión $d$, la distribución normal multivariada queda determinada por un vector de medias $\boldsymbol{\mu}$ y una matriz de covarianza $\boldsymbol{\Sigma}$.
::: {.callout-note appearance="minimal"}
Definición. Una observación $\mathbf{x}\in\mathbb{R}^d$ sigue una distribución normal multivariada $\mathcal{N}(\boldsymbol{\mu},\boldsymbol{\Sigma})$ si
$$
p(\mathbf{x}) = \frac{1}{(2\pi)^{d/2}|\boldsymbol{\Sigma}|^{1/2}}
\exp\left[-\frac{1}{2}(\mathbf{x}-\boldsymbol{\mu})^T\boldsymbol{\Sigma}^{-1}(\mathbf{x}-\boldsymbol{\mu})\right]
$$ {#eq-normal-multivariate}
:::
### Funciones discriminantes gaussianas
Si cada clase $\omega_i$ tiene una densidad condicional gaussiana
$$
p(\mathbf{x}\mid\omega_i)=\mathcal{N}(\mathbf{x};\boldsymbol{\mu}_i,\boldsymbol{\Sigma}_i),
$$
entonces la decisión MAP puede obtenerse maximizando la función discriminante
$$
g_i(\mathbf{x}) = \log p(\mathbf{x}\mid\omega_i) + \log P(\omega_i).
$$
Eliminando términos constantes comunes a todas las clases:
$$
g_i(\mathbf{x}) = -\frac{1}{2}\log|\boldsymbol{\Sigma}_i|
-\frac{1}{2}(\mathbf{x}-\boldsymbol{\mu}_i)^T\boldsymbol{\Sigma}_i^{-1}(\mathbf{x}-\boldsymbol{\mu}_i)
+\log P(\omega_i).
$$
Cuando las matrices de covarianza son distintas entre clases, la frontera $g_i(\mathbf{x})=g_j(\mathbf{x})$ es cuadrática. Este caso se conoce como análisis discriminante cuadrático, o QDA [@hastie2009elements].
### Casos especiales
#### Covarianza esférica común y priors iguales
Si $\boldsymbol{\Sigma}_i=\sigma^2\mathbf{I}$ para todas las clases y los priors son iguales, entonces maximizar $g_i(\mathbf{x})$ equivale a minimizar la distancia euclidiana a la media de la clase:
$$
\hat{\omega}(\mathbf{x}) = \arg\min_i \|\mathbf{x}-\boldsymbol{\mu}_i\|^2.
$$
Este caso corresponde a un clasificador de distancia mínima o template matching.
#### Covarianza común y priors arbitrarios
Si $\boldsymbol{\Sigma}_i=\boldsymbol{\Sigma}$ para todas las clases, pero los priors no necesariamente son iguales, la función discriminante se vuelve lineal:
$$
g_i(\mathbf{x}) = \mathbf{w}_i^T\mathbf{x}+b_i,
$$
con
$$
\mathbf{w}_i=\boldsymbol{\Sigma}^{-1}\boldsymbol{\mu}_i,
\qquad
b_i=-\frac{1}{2}\boldsymbol{\mu}_i^T\boldsymbol{\Sigma}^{-1}\boldsymbol{\mu}_i+\log P(\omega_i).
$$
Este caso corresponde al análisis discriminante lineal, o LDA [@fisher1936use; @hastie2009elements]. El término $\log P(\omega_i)$ desplaza la frontera de decisión hacia la clase con menor prior, porque se requiere más evidencia para asignar una observación a una clase poco frecuente.
### Ejemplo en Python: clasificador gaussiano con covarianza común
```{python}
#| code-fold: false
import numpy as np
from sklearn.datasets import make_blobs
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
X, y = make_blobs(
n_samples=600,
centers=[[-2, 0], [2, 1]],
cluster_std=[1.2, 1.2],
random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
classes = np.unique(y_train)
means = np.array([X_train[y_train == k].mean(axis=0) for k in classes])
priors = np.array([np.mean(y_train == k) for k in classes])
# Estimación de covarianza común por máxima verosimilitud agrupada.
S = np.zeros((X_train.shape[1], X_train.shape[1]))
for k, mu in zip(classes, means):
Xk = X_train[y_train == k]
centered = Xk - mu
S += centered.T @ centered
S /= len(X_train)
S_inv = np.linalg.inv(S)
W = means @ S_inv.T
b = np.array([
-0.5 * mu.T @ S_inv @ mu + np.log(pi)
for mu, pi in zip(means, priors)
])
scores = X_test @ W.T + b
y_pred = classes[np.argmax(scores, axis=1)]
print("Accuracy:", accuracy_score(y_test, y_pred))
print(confusion_matrix(y_test, y_pred))
```
Este código implementa la regla discriminante lineal sin llamar directamente a una clase de `scikit-learn`. La implementación deja visible la estructura bayesiana: estimación de medias, covarianza común, priors y maximización de $g_i(\mathbf{x})$.
```{=html}
<div id="lda-g-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 gaussiana (LDA)</span><br>
<span style="font-size:0.81em;color:#666;">Mueve las medias, la desviación estándar y el prior para ver cómo se desplaza la frontera de decisión</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;margin-bottom:10px;">
<div><label style="font-size:0.79em;color:#444;">μ₁ = <strong id="gmu1v">-2</strong></label><br>
<input id="gmu1" type="range" min="-5" max="0" step="0.5" value="-2" style="width:100%;accent-color:#2563eb;"></div>
<div><label style="font-size:0.79em;color:#444;">μ₂ = <strong id="gmu2v">2</strong></label><br>
<input id="gmu2" type="range" min="0" max="5" step="0.5" value="2" style="width:100%;accent-color:#dc2626;"></div>
<div><label style="font-size:0.79em;color:#444;">σ = <strong id="gsigv">1.2</strong></label><br>
<input id="gsig" type="range" min="0.5" max="3" step="0.1" value="1.2" style="width:100%;accent-color:#6b7280;"></div>
<div><label style="font-size:0.79em;color:#444;">P(ω₁) = <strong id="gprv">0.5</strong></label><br>
<input id="gpr" type="range" min="0.05" max="0.95" step="0.05" value="0.5" style="width:100%;accent-color:#1a3a5c;"></div>
</div>
<div id="ldag-info" 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:22px;"></div>
<canvas id="ldag-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
</div>
<script>
(function(){
const cv=document.getElementById('ldag-cv');
const W=680,H=280,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 PAD={l:40,r:20,t:30,b:36};
const CW=W-PAD.l-PAD.r,CH=H-PAD.t-PAD.b;
const xMn=-8,xMx=8;
const sx=x=>PAD.l+(x-xMn)/(xMx-xMn)*CW;
const sy=y=>PAD.t+CH-y*CH;
const C1='#2563eb',C2='#dc2626';
function gauss(x,mu,sig){return Math.exp(-0.5*((x-mu)/sig)**2)/(sig*Math.sqrt(2*Math.PI));}
function draw(){
const mu1=parseFloat(document.getElementById('gmu1').value);
const mu2=parseFloat(document.getElementById('gmu2').value);
const sig=parseFloat(document.getElementById('gsig').value);
const p1=parseFloat(document.getElementById('gpr').value);
const p2=1-p1;
// Decision boundary: w*(x-x*) = 0 → x* = (mu1+mu2)/2 + sig²/(mu2-mu1)*ln(p2/p1)
let xstar=null;
if(Math.abs(mu2-mu1)>0.01){
xstar=(mu1+mu2)/2+sig*sig/(mu2-mu1)*Math.log(p2/p1);
}
const N=400;
const step=(xMx-xMn)/N;
let maxY=0;
for(let i=0;i<=N;i++){
const x=xMn+i*step;
const y=Math.max(gauss(x,mu1,sig)*p1,gauss(x,mu2,sig)*p2);
if(y>maxY)maxY=y;
}
maxY*=1.15;
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#f8f9fb';ctx.fillRect(PAD.l,PAD.t,CW,CH);
ctx.strokeStyle='#e8ecf3';ctx.lineWidth=0.8;
[-6,-4,-2,0,2,4,6].forEach(x=>{
ctx.beginPath();ctx.moveTo(sx(x),PAD.t);ctx.lineTo(sx(x),PAD.t+CH);ctx.stroke();
});
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1.2;
ctx.beginPath();ctx.moveTo(PAD.l,PAD.t);ctx.lineTo(PAD.l,PAD.t+CH);ctx.lineTo(PAD.l+CW,PAD.t+CH);ctx.stroke();
ctx.fillStyle='#888';ctx.font='10px Georgia,serif';ctx.textAlign='center';
[-6,-4,-2,0,2,4,6].forEach(x=>ctx.fillText(x,sx(x),PAD.t+CH+14));
// Shaded regions
if(xstar!==null){
const xs=Math.max(xMn,Math.min(xMx,xstar));
ctx.fillStyle=C1+'18';
ctx.fillRect(PAD.l,PAD.t,sx(xs)-PAD.l,CH);
ctx.fillStyle=C2+'18';
ctx.fillRect(sx(xs),PAD.t,PAD.l+CW-sx(xs),CH);
}
// Gaussian curves filled
function fillGauss(mu,sig,p,col){
ctx.beginPath();ctx.moveTo(sx(xMn),PAD.t+CH);
for(let i=0;i<=N;i++){
const x=xMn+i*step;
const y=gauss(x,mu,sig)*p/maxY;
ctx.lineTo(sx(x),sy(y));
}
ctx.lineTo(sx(xMx),PAD.t+CH);ctx.closePath();
ctx.fillStyle=col+'55';ctx.fill();
ctx.strokeStyle=col;ctx.lineWidth=2;ctx.stroke();
}
fillGauss(mu1,sig,p1,C1);
fillGauss(mu2,sig,p2,C2);
// Decision boundary
if(xstar!==null&&xstar>xMn&&xstar<xMx){
ctx.strokeStyle='#f59e0b';ctx.lineWidth=2.5;ctx.setLineDash([6,4]);
ctx.beginPath();ctx.moveTo(sx(xstar),PAD.t+4);ctx.lineTo(sx(xstar),PAD.t+CH);ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle='#f59e0b';ctx.font='bold 10px Georgia,serif';ctx.textAlign='left';
ctx.fillText('x*='+xstar.toFixed(2),sx(xstar)+4,PAD.t+14);
}
// Mean markers
[mu1,mu2].forEach((m,i)=>{
const col=i===0?C1:C2;
ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.setLineDash([3,3]);
ctx.beginPath();ctx.moveTo(sx(m),PAD.t);ctx.lineTo(sx(m),PAD.t+CH);ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle=col;ctx.font='bold 10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('μ'+(i+1),sx(m),PAD.t+10);
});
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Densidades condicionales y frontera de decisión',PAD.l+CW/2,PAD.t-10);
const info=xstar!==null?
`Frontera x* = (μ₁+μ₂)/2 + σ²/(μ₂−μ₁)·ln(P(ω₂)/P(ω₁)) = <strong>${xstar.toFixed(3)}</strong>`:
`Las medias coinciden — no hay frontera definida`;
document.getElementById('ldag-info').innerHTML=info;
}
['gmu1','gmu2','gsig','gpr'].forEach(id=>{
const vid={gmu1:'gmu1v',gmu2:'gmu2v',gsig:'gsigv',gpr:'gprv'}[id];
document.getElementById(id).addEventListener('input',function(){
document.getElementById(vid).textContent=parseFloat(this.value).toFixed(id==='gpr'?2:1);
draw();
});
});
draw();
})();
</script>
```
### Ejemplo interactivo 2: dos gaussianas en 1D
El siguiente ejemplo muestra el caso más simple de clasificación gaussiana: una sola variable $x$ y dos clases. Cada clase se modela con una distribución normal distinta:
$$
p(x\mid\omega_1)=\mathcal{N}(x;\mu_1,\sigma_1^2),
\qquad
p(x\mid\omega_2)=\mathcal{N}(x;\mu_2,\sigma_2^2).
$$
Para clasificar un valor observado $x$, no comparamos únicamente las densidades $p(x\mid\omega_i)$. La regla MAP compara las densidades ponderadas por sus probabilidades previas:
$$
p(x\mid\omega_1)P(\omega_1)
\quad\text{contra}\quad
p(x\mid\omega_2)P(\omega_2).
$$
La frontera de decisión aparece en los valores de $x$ donde ambas cantidades son iguales. En esos puntos el clasificador está indiferente entre las dos clases:
$$
p(x\mid\omega_1)P(\omega_1)
=
p(x\mid\omega_2)P(\omega_2).
$$
El control interactivo modifica $P(\omega_1)$. Cuando aumenta el prior de una clase, esa clase necesita menos evidencia local para ser seleccionada. Por eso la frontera se desplaza hacia la otra clase: se requiere evidencia más fuerte para vencer a la clase que ahora es más probable a priori.
```{python}
#| label: fig-gaussian-1d
#| fig-cap: "Densidades, posterior y frontera de decisión para dos clases gaussianas."
import numpy as np
import plotly.graph_objects as go
x = np.linspace(-5, 7, 600)
mu1, s1 = 0.0, 1.0
mu2, s2 = 2.2, 1.4
def normal_pdf(x, mu, sigma):
return np.exp(-0.5*((x-mu)/sigma)**2)/(sigma*np.sqrt(2*np.pi))
frames = []
prior_grid = np.linspace(0.1, 0.9, 9)
for p1 in prior_grid:
p2 = 1 - p1
f1 = normal_pdf(x, mu1, s1)
f2 = normal_pdf(x, mu2, s2)
un1 = f1 * p1
un2 = f2 * p2
post1 = un1 / (un1 + un2)
post2 = 1 - post1
score = un1 - un2
crossing_idx = np.where(np.diff(np.sign(score)) != 0)[0]
boundary_x = x[crossing_idx] if len(crossing_idx) > 0 else np.array([])
boundary_y = np.interp(boundary_x, x, np.maximum(un1, un2))
frames.append(go.Frame(
data=[
go.Scatter(x=x, y=un1, mode="lines", name="p(x|ω1)P(ω1)"),
go.Scatter(x=x, y=un2, mode="lines", name="p(x|ω2)P(ω2)"),
go.Scatter(x=x, y=post1, mode="lines", name="P(ω1|x)", yaxis="y2"),
go.Scatter(x=x, y=post2, mode="lines", name="P(ω2|x)", yaxis="y2"),
go.Scatter(
x=boundary_x,
y=boundary_y,
mode="markers",
name="frontera MAP",
marker=dict(size=9, symbol="x", color="black")
)
],
name=f"P(ω1)={p1:.1f}"
))
fig = go.Figure(data=frames[4].data, frames=frames)
fig.update_layout(
title="Dos clases gaussianas en 1D: el prior desplaza la decisión",
xaxis_title="x",
yaxis=dict(title="p(x | ωᵢ)P(ωᵢ)"),
yaxis2=dict(title="posterior", overlaying="y", side="right", range=[0, 1]),
height=500,
legend=dict(orientation="h", y=-0.25),
margin=dict(b=120),
updatemenus=[dict(
type="buttons",
buttons=[dict(label="Animar", method="animate", args=[None])]
)],
sliders=[dict(
steps=[dict(method="animate", args=[[fr.name], {"mode":"immediate"}], label=fr.name) for fr in frames]
)]
)
fig.show()
```
La gráfica muestra tres ideas importantes. Primero, las curvas $p(x\mid\omega_i)P(\omega_i)$ son las cantidades que realmente se comparan para decidir. Segundo, las posteriores $P(\omega_i\mid x)$ cambian suavemente con $x$: cerca de la media de una clase, su posterior suele aumentar. Tercero, si las varianzas son distintas, puede aparecer más de una frontera de decisión, porque las curvas gaussianas pueden cruzarse en más de un punto.
### Clasificación gaussiana en 2D
En dos dimensiones, cada observación ya no es un número sino un vector:
$$
\mathbf{x}=(x_1,x_2)^T.
$$
Cada clase se describe con tres elementos:
- Un centro $\boldsymbol{\mu}_i$.
- Una matriz de covarianza $\Sigma_i$.
- Un prior $P(\omega_i)$.
El centro $\boldsymbol{\mu}_i$ indica dónde se concentra la clase. La matriz $\Sigma_i$ indica la forma de la nube: qué tan dispersa está, si está alargada en alguna dirección y si las variables $x_1$ y $x_2$ están correlacionadas. El prior $P(\omega_i)$ desplaza la decisión hacia la clase menos frecuente, porque se necesita más evidencia para asignar una observación a una clase con menor probabilidad previa.
La función discriminante para cada clase es:
$$
g_i(\mathbf{x})
=
\log p(\mathbf{x}\mid\omega_i)
+
\log P(\omega_i).
$$
La frontera se obtiene donde las dos clases tienen la misma puntuación:
$$
g_1(\mathbf{x})=g_2(\mathbf{x}).
$$
Si las covarianzas son iguales, la frontera es lineal. Si las covarianzas son distintas, la frontera puede ser curva, porque cada clase mide la cercanía a su centro con una geometría diferente.
### Ejemplo interactivo 3: frontera en 2D
El siguiente ejemplo simula dos nubes gaussianas en el plano. El fondo indica la región donde gana cada clase y la línea de contorno muestra los puntos donde $g_1(\mathbf{x})-g_2(\mathbf{x})=0$. A un lado de esa frontera se decide $\omega_1$ y al otro lado se decide $\omega_2$.
```{python}
#| label: fig-gaussian-2d
#| fig-cap: "Frontera MAP para dos clases gaussianas en dos dimensiones."
import numpy as np
import plotly.graph_objects as go
np.random.seed(7)
n = 180
mu1 = np.array([-1.2, 0.0])
mu2 = np.array([1.2, 0.6])
S1 = np.array([[1.0, 0.55], [0.55, 1.0]])
S2 = np.array([[1.2, -0.45], [-0.45, 0.75]])
X1 = np.random.multivariate_normal(mu1, S1, n)
X2 = np.random.multivariate_normal(mu2, S2, n)
xx, yy = np.meshgrid(np.linspace(-5, 5, 180), np.linspace(-4, 5, 180))
G = np.c_[xx.ravel(), yy.ravel()]
def log_gauss(X, mu, S):
invS = np.linalg.inv(S)
diff = X - mu
q = np.sum(diff @ invS * diff, axis=1)
return -0.5*q - 0.5*np.log(np.linalg.det(S))
p1 = 0.5
score = log_gauss(G, mu1, S1) + np.log(p1) - log_gauss(G, mu2, S2) - np.log(1-p1)
Z = score.reshape(xx.shape)
decision_region = (Z > 0).astype(int)
fig = go.Figure()
fig.add_trace(go.Contour(
x=xx[0],
y=yy[:,0],
z=decision_region,
contours=dict(start=0, end=1, size=1, coloring="heatmap"),
colorscale=[[0, "rgba(214,39,40,0.18)"], [1, "rgba(31,119,180,0.18)"]],
showscale=False,
line=dict(width=0),
name="región de decisión"
))
fig.add_trace(go.Contour(
x=xx[0],
y=yy[:,0],
z=Z,
contours=dict(start=0, end=0, size=1),
showscale=False,
line=dict(color="black", width=3),
name="frontera MAP"
))
fig.add_trace(go.Scatter(x=X1[:,0], y=X1[:,1], mode="markers", name="ω1", opacity=0.75))
fig.add_trace(go.Scatter(x=X2[:,0], y=X2[:,1], mode="markers", name="ω2", opacity=0.75))
fig.update_layout(
title="Frontera MAP en 2D con covarianzas distintas",
xaxis_title="x1",
yaxis_title="x2",
height=520,
legend=dict(orientation="h", y=-0.15),
margin=dict(b=90)
)
fig.show()
```
La frontera no depende sólo de qué media esté más cerca en distancia euclidiana. También depende de la orientación y dispersión de cada nube. Por ejemplo, una clase muy dispersa puede asignar probabilidad razonable a puntos alejados de su centro, mientras que una clase muy compacta exige que los puntos estén cerca de su media. Esa diferencia aparece en la forma curva de la frontera.
### Interpretación geométrica
La cantidad
$$
(\mathbf{x}-\boldsymbol{\mu}_i)^T\Sigma_i^{-1}(\mathbf{x}-\boldsymbol{\mu}_i)
$$
es una distancia de Mahalanobis.
Significa que el modelo no mide sólo cercanía euclidiana, sino cercanía relativa a la forma de la nube de datos.
::: {.callout-tip}
La covarianza codifica orientación, escala y correlación.
:::
## ¿Cómo evaluar el supuesto gaussiano?
El supuesto de normalidad rara vez debe aceptarse sin diagnóstico. En aplicaciones reales conviene combinar razonamiento sustantivo, visualización y pruebas estadísticas. La visualización suele ser más informativa que una prueba aislada, porque permite detectar asimetrías, colas pesadas, multimodalidad o valores atípicos.
Algunas herramientas útiles son:
- histogramas y estimaciones de densidad;
- diagramas de caja;
- gráficos Q-Q;
- pruebas de bondad de ajuste, como Kolmogorov--Smirnov, Shapiro--Wilk o pruebas basadas en $\chi^2$.
```{python}
#| label: fig-qq-normal
#| fig-cap: "Gráfico Q-Q comparando cuantiles empíricos de una muestra aleatoria contra cuantiles teóricos de una normal estándar. Los puntos sobre la línea diagonal indican buen ajuste gaussiano."
#| code-fold: true
import numpy as np
import plotly.graph_objects as go
rng = np.random.default_rng(123)
x = rng.normal(loc=0, scale=1, size=300)
# Cuantiles teóricos y empíricos (implementación manual, sin scipy)
n = len(x)
x_sorted = np.sort(x)
# Probabilidades usando la fórmula de Filliben (i-0.375)/(n+0.25)
probs = (np.arange(1, n + 1) - 0.375) / (n + 0.25)
# Cuantiles teóricos de N(0,1) usando la aproximación racional de Abramowitz & Stegun
def norm_ppf(p):
"""Aproximación del cuantil normal estándar para 0 < p < 1."""
p = np.clip(p, 1e-10, 1 - 1e-10)
sign = np.where(p < 0.5, -1.0, 1.0)
pp = np.where(p < 0.5, p, 1 - p)
t = np.sqrt(-2 * np.log(pp))
c0, c1, c2 = 2.515517, 0.802853, 0.010328
d1, d2, d3 = 1.432788, 0.189269, 0.001308
num = c0 + c1 * t + c2 * t**2
den = 1 + d1 * t + d2 * t**2 + d3 * t**3
return sign * (t - num / den)
q_teoricos = norm_ppf(probs)
# Línea de referencia: regresión sobre cuartiles Q1 y Q3
q25_emp = np.percentile(x_sorted, 25)
q75_emp = np.percentile(x_sorted, 75)
q25_teo = norm_ppf(np.array([0.25]))[0]
q75_teo = norm_ppf(np.array([0.75]))[0]
slope = (q75_emp - q25_emp) / (q75_teo - q25_teo)
intercept = q25_emp - slope * q25_teo
ref_x = np.array([q_teoricos.min(), q_teoricos.max()])
ref_y = intercept + slope * ref_x
fig = go.Figure()
fig.add_trace(go.Scatter(
x=q_teoricos, y=x_sorted,
mode="markers",
marker=dict(color="#2563eb", size=5, opacity=0.6,
line=dict(width=0.5, color="#1e3a6e")),
name="Cuantiles empíricos",
hovertemplate="Q teórico: %{x:.3f}<br>Q empírico: %{y:.3f}<extra></extra>"
))
fig.add_trace(go.Scatter(
x=ref_x, y=ref_y,
mode="lines",
line=dict(color="#dc2626", width=2, dash="dash"),
name="Línea de referencia"
))
fig.update_layout(
title="Gráfico Q-Q frente a la normal estándar",
xaxis_title="Cuantiles teóricos N(0,1)",
yaxis_title="Cuantiles empíricos",
template="plotly_white",
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="right", x=1)
)
fig.show()
# Prueba de normalidad manual (estadístico de correlación de cuantiles)
r_qq = float(np.corrcoef(q_teoricos, x_sorted)[0, 1])
print(f"Correlación Q-Q (≈1 indica normalidad): r = {r_qq:.4f}")
```
Una advertencia importante es que las pruebas de normalidad son sensibles al tamaño de muestra. Con muestras grandes pueden detectar desviaciones pequeñas sin relevancia práctica; con muestras pequeñas pueden carecer de potencia. Por ello, el diagnóstico debe vincularse con el objetivo de clasificación.
## Clasificación bayesiana de secuencias discretas
Los clasificadores bayesianos no se limitan a variables continuas. También pueden aplicarse a datos discretos estructurados, como texto, secuencias genómicas o series de símbolos. En biología computacional, un ejemplo clásico es la identificación de islas CpG en el genoma humano [@durbin1998biological].
Las islas CpG son regiones cortas del ADN donde el dinucleótido CG aparece con mayor frecuencia que en otras regiones. Una definición determinista clásica considera regiones de longitud mayor a 200 pares de bases, contenido $G+C$ superior a 50% y razón observada/esperada de CpG mayor a 0.60 [@gardiner1987cpg]. Sin embargo, una perspectiva probabilística permite construir modelos generativos para regiones CpG y no CpG, y decidir con base en la razón de verosimilitudes.
::: {.callout-note appearance="minimal"}
Definición. El contenido $G+C$ de una secuencia de longitud $N$ se define como
$$
\%GC = \frac{N(C)+N(G)}{N}
$$ {#eq-gc-content}
:::
::: {.callout-note appearance="minimal"}
Definición. La razón CpG observada/esperada puede escribirse como
$$
\text{CpG ratio}=\frac{N(CpG)/N}{(N(C)/N)(N(G)/N)}
$$ {#eq-cpg-ratio}
:::
### Cadenas de Markov de primer orden
Una cadena de Markov de primer orden modela una secuencia suponiendo que el símbolo actual depende sólo del símbolo inmediatamente anterior. Para ADN, el alfabeto es $\mathcal{A}=\{A,C,G,T\}$.
::: {.callout-note appearance="minimal"}
Definición. Una secuencia de variables aleatorias $X_1,X_2,\dots,X_n$ forma una cadena de Markov de primer orden si
$$
P(X_n=x_n\mid X_1=x_1,\dots,X_{n-1}=x_{n-1})=P(X_n=x_n\mid X_{n-1}=x_{n-1})
$$ {#eq-markov-property}
:::
Las probabilidades de transición se denotan
$$
a_{st}=P(X_i=t\mid X_{i-1}=s),
$$
donde $s,t\in\mathcal{A}$. Para una secuencia $\mathbf{x}=(x_1,
\dots,x_L)$, su probabilidad bajo una cadena de Markov es
$$
P(\mathbf{x}) = P(x_1)\prod_{i=2}^{L} a_{x_{i-1}x_i}.
$$
### Log-odds para discriminar secuencias
Supongamos que se entrenan dos cadenas de Markov: una para islas CpG, denotada $+$, y otra para regiones no CpG, denotada $-$. La puntuación log-odds de una secuencia es
::: {.callout-note appearance="minimal"}
Definición. La puntuación log-odds entre dos modelos de Markov para una secuencia $\mathbf{x}$ es
$$
S(\mathbf{x})=\log\frac{P(\mathbf{x}\mid +)}{P(\mathbf{x}\mid -)}
=\sum_{i=2}^{L}\log\frac{a^+_{x_{i-1}x_i}}{a^-_{x_{i-1}x_i}}
$$ {#eq-markov-log-odds}
:::
Si $S(\mathbf{x})$ supera un umbral, la secuencia se clasifica como CpG; si no, como no CpG. El umbral puede fijarse por priors, por costos de error o por validación empírica.
### Ejemplo en Python: puntuación de una secuencia de ADN
```{python}
#| code-fold: false
import numpy as np
alphabet = ["A", "C", "G", "T"]
idx = {s: i for i, s in enumerate(alphabet)}
A_plus = np.array([
[0.180, 0.274, 0.426, 0.120],
[0.171, 0.368, 0.274, 0.188],
[0.161, 0.339, 0.375, 0.125],
[0.079, 0.355, 0.384, 0.182],
])
A_minus = np.array([
[0.300, 0.205, 0.285, 0.210],
[0.322, 0.298, 0.078, 0.302],
[0.248, 0.246, 0.298, 0.208],
[0.177, 0.239, 0.292, 0.292],
])
def markov_log_odds(seq, A_pos, A_neg):
score = 0.0
for prev, curr in zip(seq[:-1], seq[1:]):
i, j = idx[prev], idx[curr]
score += np.log(A_pos[i, j] / A_neg[i, j])
return score
seq = "ACGCGCGTACGCGT"
score = markov_log_odds(seq, A_plus, A_minus)
normalized_score = score / (len(seq) - 1)
print("Score:", score)
print("Length-normalized score:", normalized_score)
print("Decision:", "CpG" if score > 0 else "non-CpG")
```
La normalización por longitud permite comparar secuencias de tamaños distintos. En problemas reales se recomienda estimar las matrices de transición con suavizado, por ejemplo mediante pseudo-conteos, para evitar probabilidades cero.
```{=html}
<div id="cpg-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;">Log-odds de Markov — clasificador CpG</span><br>
<span style="font-size:0.81em;color:#666;">El mapa muestra log(a⁺ₛₜ / a⁻ₛₜ) por dinucleótido. Haz clic en dinucleótidos para construir una secuencia y acumular la puntuación log-odds.</span>
</div>
<canvas id="cpg-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;cursor:pointer;"></canvas>
<div style="display:flex;gap:10px;margin-top:10px;flex-wrap:wrap;align-items:center;">
<div id="cpg-seq" style="font-size:0.82em;background:#fff;border:1px solid #e0e5ed;border-radius:5px;padding:4px 10px;flex:1;min-height:22px;color:#1a2e45;font-family:monospace;word-break:break-all;">Secuencia: (vacía)</div>
<button onclick="(function(){document.getElementById('cpg-seq').textContent='Secuencia: (vacía)';window._cpgSeq=[];window._cpgScore=0;document.getElementById('cpg-score').innerHTML='Puntuación = <strong>0.000</strong>';document.getElementById('cpg-dec').textContent='—';})()" style="font-size:0.8em;padding:3px 10px;border:1px solid #d0d7e3;border-radius:5px;background:#fff;cursor:pointer;">Reiniciar</button>
</div>
<div id="cpg-score" style="text-align:center;font-size:0.85em;padding:5px 10px;background:#fff;border:1px solid #e0e5ed;border-radius:5px;margin-top:8px;">Puntuación = <strong>0.000</strong>   Decisión: <span id="cpg-dec">—</span></div>
</div>
<script>
(function(){
const ALF=['A','C','G','T'];
const AP=[[0.180,0.274,0.426,0.120],[0.171,0.368,0.274,0.188],[0.161,0.339,0.375,0.125],[0.079,0.355,0.384,0.182]];
const AM=[[0.300,0.205,0.285,0.210],[0.322,0.298,0.078,0.302],[0.248,0.246,0.298,0.208],[0.177,0.239,0.292,0.292]];
const cv=document.getElementById('cpg-cv');
const W=680,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);
window._cpgSeq=[];window._cpgScore=0;
// Precompute log-ratios
const LR=AP.map((row,i)=>row.map((v,j)=>Math.log(v/AM[i][j])));
const allVals=LR.flat();
const lrMin=Math.min(...allVals),lrMax=Math.max(...allVals);
function lerp(t,a,b){return a+t*(b-a);}
function heatCol(v){
const t=(v-lrMin)/(lrMax-lrMin);
if(t<0.5){const s=t*2;return`rgb(${Math.round(lerp(s,220,248))},${Math.round(lerp(s,38,200))},${Math.round(lerp(s,38,200))})`;}
else{const s=(t-0.5)*2;return`rgb(${Math.round(lerp(s,248,22))},${Math.round(lerp(s,200,163))},${Math.round(lerp(s,200,74))})`;}
}
const CELL=62,PAD_L=54,PAD_T=54;
function draw(){
ctx.clearRect(0,0,W,H);
ctx.fillStyle='#f8f9fb';ctx.fillRect(0,0,W,H);
ctx.fillStyle='#1a2e45';ctx.font='bold 12px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Siguiente nucleótido (columna) →',PAD_L+CELL*2,PAD_T-36);
ctx.fillStyle='#555';ctx.font='10px Georgia,serif';
ctx.fillText('Nucleótido anterior (fila) ↓',22,PAD_T+CELL*2);
ALF.forEach((s,i)=>{
ctx.fillStyle='#1a2e45';ctx.font='bold 12px Georgia,serif';ctx.textAlign='center';
ctx.fillText(s,PAD_L+i*CELL+CELL/2,PAD_T-14);
ctx.fillText(s,PAD_L-18,PAD_T+i*CELL+CELL/2+4);
ALF.forEach((t,j)=>{
const v=LR[i][j];
const bx=PAD_L+j*CELL,by=PAD_T+i*CELL;
ctx.fillStyle=heatCol(v);ctx.fillRect(bx,by,CELL-2,CELL-2);
ctx.strokeStyle='#fff';ctx.lineWidth=1.5;ctx.strokeRect(bx,by,CELL-2,CELL-2);
ctx.fillStyle=Math.abs(v)>0.3?'#fff':'#1a2e45';
ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText(v.toFixed(2),bx+CELL/2-1,by+CELL/2+4);
ctx.font='9px Georgia,serif';ctx.fillText(s+t,bx+CELL/2-1,by+12);
});
});
// Color legend
const lx=PAD_L+4*CELL+16,ly=PAD_T+8,lh=4*CELL-16,lw=18;
for(let i=0;i<lh;i++){
const t=1-i/lh;
const v=lrMin+t*(lrMax-lrMin);
ctx.fillStyle=heatCol(v);ctx.fillRect(lx,ly+i,lw,1);
}
ctx.strokeStyle='#9aa3b5';ctx.lineWidth=1;ctx.strokeRect(lx,ly,lw,lh);
ctx.fillStyle='#444';ctx.font='9px Georgia,serif';ctx.textAlign='left';
ctx.fillText(lrMax.toFixed(2),lx+lw+3,ly+6);
ctx.fillText('0',lx+lw+3,ly+lh/2+4);
ctx.fillText(lrMin.toFixed(2),lx+lw+3,ly+lh);
ctx.fillStyle='#555';ctx.font='9px Georgia,serif';ctx.textAlign='center';
ctx.fillText('log-odds',lx+lw/2,ly+lh+14);
}
cv.addEventListener('click',function(e){
const rect=cv.getBoundingClientRect();
const px=(e.clientX-rect.left),py=(e.clientY-rect.top);
const col=Math.floor((px-PAD_L)/CELL),row=Math.floor((py-PAD_T)/CELL);
if(col<0||col>3||row<0||row>3)return;
const seq=window._cpgSeq;
const last=seq.length?seq[seq.length-1]:null;
const cur=ALF[col];
if(last!==null){
const i=ALF.indexOf(last),j=ALF.indexOf(cur);
window._cpgScore+=LR[i][j];
}
seq.push(cur);
const seqStr=seq.join('');
document.getElementById('cpg-seq').textContent='Secuencia: '+seqStr;
const sc=window._cpgScore;
const dec=sc>0?'<strong style="color:#16a34a">CpG ✓</strong>':'<strong style="color:#dc2626">no-CpG ✗</strong>';
document.getElementById('cpg-score').innerHTML=
`Puntuación acumulada = <strong>${sc.toFixed(3)}</strong>  ·  Decisión: `+dec;
});
draw();
})();
</script>
```
## Clasificadores bayesianos de riesgo mínimo
La decisión de error mínimo supone que todos los errores cuestan lo mismo. Sin embargo, en muchas aplicaciones esto no es cierto. En diagnóstico médico, por ejemplo, un falso negativo puede ser mucho más costoso que un falso positivo. En detección de fraude, bloquear una transacción legítima y dejar pasar una transacción fraudulenta tampoco tienen el mismo costo. Por ello se introduce una función de pérdida.
::: {.callout-note appearance="minimal"}
Definición. La pérdida $\lambda(\alpha_i,\omega_j)$ representa el costo de tomar la decisión $\alpha_i$ cuando el verdadero estado de la naturaleza es $\omega_j$:
$$
\lambda_{ij}=\lambda(\alpha_i,\omega_j)
$$ {#eq-loss-function}
:::
Dada una observación $\mathbf{x}$, el riesgo condicional de tomar la decisión $\alpha_i$ es la pérdida esperada bajo la distribución posterior de las clases.
::: {.callout-note appearance="minimal"}
Definición. El riesgo condicional de decidir $\alpha_i$ dado $\mathbf{x}$ es
$$
R(\alpha_i\mid\mathbf{x})=\sum_{j=1}^{c}\lambda(\alpha_i,\omega_j)P(\omega_j\mid\mathbf{x})
$$ {#eq-conditional-risk}
:::
La regla de riesgo mínimo selecciona la decisión con menor riesgo condicional:
$$
\alpha^*(\mathbf{x})=\arg\min_i R(\alpha_i\mid\mathbf{x}).
$$
Cuando $\lambda_{ii}=0$ y $\lambda_{ij}=1$ para $i\neq j$, la regla de riesgo mínimo se reduce a la regla de error mínimo. Es decir, la clasificación MAP es un caso particular de la teoría de decisión bayesiana [@berger1985statistical; @duda2001pattern].
### Dos clases con matriz de pérdidas
Para dos clases y dos decisiones, la matriz de pérdidas puede escribirse como
$$
\begin{array}{c|cc}
& \omega_1 & \omega_2\\
\hline
\alpha_1 & \lambda_{11} & \lambda_{12}\\
\alpha_2 & \lambda_{21} & \lambda_{22}
\end{array}
$$
La decisión $\alpha_1$ se toma si
$$
\lambda_{11}P(\omega_1\mid\mathbf{x})+
\lambda_{12}P(\omega_2\mid\mathbf{x})
<
\lambda_{21}P(\omega_1\mid\mathbf{x})+
\lambda_{22}P(\omega_2\mid\mathbf{x}).
$$
De manera equivalente, el umbral del cociente de verosimilitudes se modifica por los costos:
$$
\ell(\mathbf{x})
\gtrless_{\alpha_2}^{\alpha_1}
\frac{P(\omega_2)}{P(\omega_1)}
\cdot
\frac{\lambda_{12}-\lambda_{22}}{\lambda_{21}-\lambda_{11}}.
$$
### Ejemplo en Python: error mínimo frente a riesgo mínimo
```{python}
px_w1 = 0.2
px_w2 = 0.4
p_w1 = 0.9
p_w2 = 0.1
posterior, _ = clasificador_bayesiano(
verosimilitudes=[px_w1, px_w2],
priors=[p_w1, p_w2],
clases=["normal", "enfermedad"]
)
post_w1, post_w2 = posterior
# Matriz de pérdida:
# alpha_1 = decidir normal, alpha_2 = decidir enfermedad
# omega_1 = normal, omega_2 = enfermedad
loss = np.array([
[0, 6],
[1, 0]
])
risks = loss @ posterior
print("Posterior:", posterior)
print("Conditional risks:", risks)
print("Minimum-error decision:", "normal" if post_w1 > post_w2 else "enfermedad")
print("Minimum-risk decision:", "normal" if np.argmin(risks) == 0 else "enfermedad")
```
El resultado muestra que la decisión de error mínimo puede ser “normal”, mientras que la decisión de riesgo mínimo puede ser “enfermedad” si el costo de no detectar la enfermedad es suficientemente alto. Esta diferencia es fundamental en sistemas de decisión sensibles al costo.
```{python}
#| label: fig-umbral-riesgo
#| fig-cap: "Riesgo condicional de cada acción como función de la probabilidad posterior P(ω₂|x). La intersección de las dos curvas define el umbral óptimo de decisión bajo la matriz de pérdida especificada."
#| code-fold: true
import numpy as np
import plotly.graph_objects as go
# Matriz de pérdida: λ₁₂=6 (falso negativo), λ₂₁=1 (falso positivo)
lam12 = 6.0 # costo de decidir "normal" cuando hay enfermedad
lam21 = 1.0 # costo de decidir "enfermedad" cuando es normal
post2 = np.linspace(0, 1, 300) # P(ω₂|x)
post1 = 1 - post2
# R(α₁|x) = λ₁₂·P(ω₂|x) → riesgo de decidir "normal"
# R(α₂|x) = λ₂₁·P(ω₁|x) → riesgo de decidir "enfermedad"
R1 = lam12 * post2
R2 = lam21 * post1
# Umbral: R1 = R2 → lam12·p2 = lam21·(1-p2) → p2* = lam21/(lam12+lam21)
threshold = lam21 / (lam12 + lam21)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=post2, y=R1,
mode="lines",
line=dict(color="#2563eb", width=2.5),
name="R(α₁|x) = decidir normal",
hovertemplate="P(ω₂|x)=%{x:.3f}<br>Riesgo=%{y:.3f}<extra></extra>"
))
fig.add_trace(go.Scatter(
x=post2, y=R2,
mode="lines",
line=dict(color="#dc2626", width=2.5),
name="R(α₂|x) = decidir enfermedad",
hovertemplate="P(ω₂|x)=%{x:.3f}<br>Riesgo=%{y:.3f}<extra></extra>"
))
# Región donde conviene α₁ (decidir normal)
fig.add_vrect(x0=0, x1=threshold, fillcolor="#2563eb", opacity=0.07,
line_width=0, annotation_text="decidir normal",
annotation_position="top left",
annotation_font=dict(color="#2563eb", size=11))
# Región donde conviene α₂ (decidir enfermedad)
fig.add_vrect(x0=threshold, x1=1, fillcolor="#dc2626", opacity=0.07,
line_width=0, annotation_text="decidir enfermedad",
annotation_position="top right",
annotation_font=dict(color="#dc2626", size=11))
# Umbral
fig.add_vline(x=threshold, line_dash="dash", line_color="#475569", line_width=1.8,
annotation_text=f"p* = λ₂₁/(λ₁₂+λ₂₁) = {threshold:.3f}",
annotation_position="bottom right",
annotation_font=dict(size=11))
fig.update_layout(
title=f"Riesgo condicional por acción (λ₁₂={lam12:.0f}, λ₂₁={lam21:.0f})",
xaxis_title="P(ω₂|x) — probabilidad posterior de enfermedad",
yaxis_title="Riesgo condicional R(αᵢ|x)",
template="plotly_white",
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.01, xanchor="right", x=1)
)
fig.show()
```
```{=html}
<div id="risk-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;">Riesgo mínimo vs error mínimo</span><br>
<span style="font-size:0.81em;color:#666;">Ajusta los costos de la matriz de pérdida y la posterior P(ω₂|x) para ver cuándo ambas reglas difieren</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px;">
<div>
<label style="font-size:0.8em;color:#444;">λ₁₂ (falso negativo) = <strong id="rl12v">6</strong></label><br>
<input id="rl12" type="range" min="1" max="20" step="1" value="6" style="width:100%;accent-color:#dc2626;">
</div>
<div>
<label style="font-size:0.8em;color:#444;">λ₂₁ (falso positivo) = <strong id="rl21v">1</strong></label><br>
<input id="rl21" type="range" min="1" max="10" step="1" value="1" style="width:100%;accent-color:#2563eb;">
</div>
<div>
<label style="font-size:0.8em;color:#444;">P(ω₂|x) = <strong id="rpv">0.17</strong></label><br>
<input id="rpp" type="range" min="0.01" max="0.99" step="0.01" value="0.17" style="width:100%;accent-color:#7c3aed;">
</div>
</div>
<div id="risk-info" 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:22px;"></div>
<canvas id="risk-cv" style="display:block;width:100%;border-radius:6px;border:1px solid #e0e5ed;background:#fff;"></canvas>
</div>
<script>
(function(){
const cv=document.getElementById('risk-cv');
const W=680,H=280,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);
function draw(){
const l12=parseFloat(document.getElementById('rl12').value); // cost decide normal when disease
const l21=parseFloat(document.getElementById('rl21').value); // cost decide disease when normal
const post2=parseFloat(document.getElementById('rpp').value);
const post1=1-post2;
// l11=l22=0 (correct decisions cost 0)
// R(α₁|x) = λ₁₁*post1 + λ₁₂*post2 = 0 + l12*post2
// R(α₂|x) = λ₂₁*post1 + λ₂₂*post2 = l21*post1 + 0
const R1=l12*post2; // risk of deciding α₁ (normal)
const R2=l21*post1; // risk of deciding α₂ (disease)
const riskDec=R1<R2?'α₁ (normal)':'α₂ (enfermedad)';
const mapDec=post1>post2?'α₁ (normal)':'α₂ (enfermedad)';
const agree=riskDec===mapDec;
const C1='#2563eb',C2='#dc2626';
ctx.clearRect(0,0,W,H);
const base=H-44,maxH=H-80,pad=60,groupW=W/2-pad*1.2;
// Group 1: decision α₁ (normal)
const bw=groupW*0.4;
// show decomposed: only l12*post2 matters (l11=0)
const barH1a=Math.min((l12*post2/(l12+l21))*maxH,maxH);
const bx1=pad+groupW*0.1;
ctx.fillStyle=C2+'99';ctx.fillRect(bx1,base-barH1a,bw,barH1a);
ctx.strokeStyle=C2;ctx.lineWidth=1.5;ctx.strokeRect(bx1,base-barH1a,bw,barH1a);
ctx.fillStyle='#1a2e45';ctx.font='10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('λ₁₂·P(ω₂|x)',bx1+bw/2,base-barH1a-6);
ctx.fillText('= '+R1.toFixed(3),bx1+bw/2,base-barH1a-18);
ctx.fillStyle='#1a2e45';ctx.font='bold 12px Georgia,serif';
ctx.fillText('R(α₁ | x) [decidir normal]',pad+groupW*0.5,base+22);
ctx.fillText('R₁ = '+R1.toFixed(3),pad+groupW*0.5,base+37);
// Group 2: decision α₂ (disease)
const barH2a=Math.min((l21*post1/(l12+l21))*maxH,maxH);
const bx2=W/2+pad+groupW*0.1;
ctx.fillStyle=C1+'99';ctx.fillRect(bx2,base-barH2a,bw,barH2a);
ctx.strokeStyle=C1;ctx.lineWidth=1.5;ctx.strokeRect(bx2,base-barH2a,bw,barH2a);
ctx.fillStyle='#1a2e45';ctx.font='10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('λ₂₁·P(ω₁|x)',bx2+bw/2,base-barH2a-6);
ctx.fillText('= '+R2.toFixed(3),bx2+bw/2,base-barH2a-18);
ctx.fillStyle='#1a2e45';ctx.font='bold 12px Georgia,serif';
ctx.fillText('R(α₂ | x) [decidir enf.]',W/2+pad+groupW*0.5,base+22);
ctx.fillText('R₂ = '+R2.toFixed(3),W/2+pad+groupW*0.5,base+37);
// Highlight winner
const winX=R1<R2?bx1:bx2,winH=R1<R2?barH1a:barH2a,winW=bw;
ctx.strokeStyle='#f59e0b';ctx.lineWidth=3;
ctx.strokeRect(winX-3,base-winH-3,winW+6,winH+6);
ctx.fillStyle='#f59e0b';ctx.font='bold 10px Georgia,serif';ctx.textAlign='center';
ctx.fillText('★ mínimo',winX+winW/2,base-winH-12);
// Divider
ctx.strokeStyle='#e0e5ed';ctx.lineWidth=1.5;
ctx.beginPath();ctx.moveTo(W/2,30);ctx.lineTo(W/2,H-20);ctx.stroke();
// Title
ctx.fillStyle='#1a2e45';ctx.font='bold 11px Georgia,serif';ctx.textAlign='center';
ctx.fillText('Riesgos condicionales R(αᵢ|x)',W/2,18);
const col=agree?'#16a34a':'#dc2626';
const thrStr='λ₂₁/λ₁₂ · P(ω₁)/P(ω₂) = '+(l21/l12*(post1/post2)).toFixed(3);
document.getElementById('risk-info').innerHTML=
`Umbral ℓ(x) ≷ <strong>${thrStr}</strong>  · `+
`MAP: <strong>${mapDec}</strong>  · `+
`Riesgo mínimo: <strong>${riskDec}</strong>  · `+
`<span style="color:${col}">${agree?'✓ Coinciden':'✗ Difieren'}</span>`;
}
['rl12','rl21','rpp'].forEach(id=>{
const vid={rl12:'rl12v',rl21:'rl21v',rpp:'rpv'}[id];
document.getElementById(id).addEventListener('input',function(){
document.getElementById(vid).textContent=parseFloat(this.value).toFixed(id==='rpp'?2:0);
draw();
});
});
draw();
})();
</script>
```
## Minimizar un tipo de error condicionado al otro
En algunos problemas no se busca minimizar una suma ponderada de errores, sino controlar explícitamente un tipo de error mientras se minimiza otro. Esta idea aparece en pruebas de hipótesis, detección de señales y clasificación con restricciones operativas. El caso clásico es minimizar la probabilidad de falso negativo sujeto a una cota sobre la probabilidad de falso positivo, o viceversa. La regla resultante se relaciona con el lema de Neyman--Pearson [@neyman1933problem].
Para dos clases, sea $\mathcal{R}_1$ la región donde se decide $\omega_1$ y $\mathcal{R}_2$ la región donde se decide $\omega_2$. Las probabilidades de error condicionales pueden escribirse como
$$
P_1(e)=\int_{\mathcal{R}_2}p(\mathbf{x}\mid\omega_1)d\mathbf{x},
\qquad
P_2(e)=\int_{\mathcal{R}_1}p(\mathbf{x}\mid\omega_2)d\mathbf{x}.
$$
Si se desea minimizar $P_1(e)$ sujeto a $P_2(e)=\epsilon$, se obtiene una regla basada en umbrales del cociente de verosimilitudes:
$$
\frac{p(\mathbf{x}\mid\omega_1)}{p(\mathbf{x}\mid\omega_2)} \gtrless \lambda.
$$
El valor de $\lambda$ se ajusta para cumplir la restricción sobre el error. En términos prácticos, esto equivale a desplazar el umbral de decisión hasta alcanzar una sensibilidad, especificidad, tasa de falsos positivos o tasa de falsos negativos deseada.
### 8.1 Ejemplo en Python: elegir umbral por restricción de falsos positivos
```{python}
from sklearn.metrics import roc_curve
rng = np.random.default_rng(7)
# Scores altos favorecen la clase positiva.
y_true = np.r_[np.zeros(500), np.ones(500)]
scores = np.r_[rng.normal(0, 1, 500), rng.normal(2, 1, 500)]
fpr, tpr, thresholds = roc_curve(y_true, scores)
max_fpr = 0.05
valid = np.where(fpr <= max_fpr)[0]
best_idx = valid[np.argmax(tpr[valid])]
print("Threshold:", thresholds[best_idx])
print("FPR:", fpr[best_idx])
print("TPR:", tpr[best_idx])
```
Este enfoque es más adecuado que maximizar accuracy cuando una clase es rara o cuando los errores tienen consecuencias asimétricas.
## Criterio minimax cuando los priors son desconocidos
La regla bayesiana clásica requiere conocer o estimar los priors $P(\omega_i)$. Cuando estos priors son inciertos, una alternativa conservadora es el criterio minimax. La idea es escoger la regla de decisión que minimiza el peor riesgo posible bajo los priors admisibles [@berger1985statistical; @duda2001pattern].
::: {.callout-note appearance="minimal"}
Definición. Una regla minimax selecciona la decisión o regla de decisión $\alpha$ que minimiza el máximo riesgo posible:
$$
\alpha_{\text{minimax}} = \arg\min_{\alpha}\max_{\pi\in\Pi} R(\alpha;\pi)
$$ {#eq-minimax-rule}
:::
Aquí $\pi$ representa el vector de priors y $\Pi$ el conjunto de priors considerados plausibles. En problemas de dos clases, puede estudiarse el riesgo como función de $P(\omega_1)$, ya que $P(\omega_2)=1-P(\omega_1)$. La regla minimax evita depender de una estimación puntual del prior y protege contra escenarios adversos. Su desventaja es que puede ser demasiado conservadora si sí existe información confiable sobre las frecuencias reales de clase.
## Recomendaciones prácticas
La clasificación bayesiana es conceptualmente elegante, pero su desempeño depende de la calidad del modelo probabilístico. Algunas recomendaciones prácticas son:
1. Estimar priors con cuidado. En datos desbalanceados, los priors empíricos pueden reflejar sesgos de muestreo y no prevalencias reales.
2. Trabajar en escala logarítmica para evitar problemas numéricos.
3. Validar supuestos distribucionales cuando se usan modelos gaussianos.
4. Usar suavizado en modelos discretos, especialmente en texto y secuencias.
5. Elegir la métrica o función de pérdida de acuerdo con el problema real, no sólo con accuracy.
6. Separar conceptualmente probabilidad, decisión y costo: una buena estimación probabilística no implica automáticamente una buena regla de decisión si el umbral no corresponde al costo operativo.
## Conclusiones
Los clasificadores bayesianos proporcionan un marco unificado para tomar decisiones bajo incertidumbre. La regla MAP minimiza la probabilidad de error cuando los errores tienen el mismo costo, mientras que la regla de riesgo mínimo permite incorporar pérdidas asimétricas. Naïve Bayes muestra cómo construir clasificadores prácticos en alta dimensión mediante una suposición de independencia condicional. Los clasificadores gaussianos muestran cómo los supuestos sobre las densidades condicionales determinan la geometría de las fronteras de decisión: fronteras lineales bajo covarianza común y fronteras cuadráticas bajo covarianzas distintas. Por otro lado, los modelos de Markov muestran que el mismo principio puede aplicarse a secuencias discretas con dependencia entre símbolos.
La idea central del capítulo es que clasificar no consiste únicamente en trazar una frontera, sino en articular tres elementos: un modelo de cómo se generan los datos, una actualización probabilística de las creencias y una regla de decisión adaptada al objetivo. Esta separación hace que los métodos bayesianos sean especialmente útiles en problemas donde la incertidumbre, los costos de error y la interpretabilidad son componentes esenciales del sistema de aprendizaje.