---
title: "Tratamiento de Ruido, Suavizado y Discretización"
pagetitle: "Tratamiento de Ruido, Suavizado y Discretización"
open-graph:
description: "Distingue ruido de valores atípicos y aplica discretización por igual ancho y suavizado a una serie simulada y a datos de BigMart Sales."
twitter-card:
description: "Distingue ruido de valores atípicos y aplica discretización por igual ancho y suavizado a una serie simulada y a datos de BigMart Sales."
description: "Distingue ruido de valores atípicos y aplica discretización por igual ancho y suavizado a una serie simulada y a datos de BigMart Sales."
format:
html:
toc: true
toc-depth: 3
toc-title: "Contenido"
code-fold: true
code-tools: true
theme: cosmo
highlight-style: github
execute:
echo: true
warning: false
message: false
jupyter: python3
---
{{< include _labkit.qmd >}}
En la ciencia de datos y la ingeniería de *machine learning*, nos enfrentamos constantemente al desafío de lidiar con datos imperfectos. Los datos provenientes del mundo real rara vez son limpios y perfectos; suelen estar contaminados por diversas fuentes de perturbación. Una de estas fuentes principales es el **ruido**.
En este capítulo distinguimos ruido, errores de captura y valores atípicos; después usamos ejemplos pequeños para entender suavizado y discretización. BigMart Sales sirve como contexto de variables, mientras que la serie diaria que aparece más adelante es **simulada**: el conjunto de datos utilizado aquí no proporciona una secuencia temporal diaria de ventas.
Para comprender cómo afecta el ruido a nuestros modelos, es fundamental distinguir entre dos conceptos que a menudo se confunden en la práctica profesional: el **ruido** y los **outliers** (valores atípicos).
## Ruido y valores atípicos
* **Ruido[^ruidoblanco]:** variación que dificulta reconocer el patrón que interesa. Puede surgir de mediciones imprecisas, variabilidad natural o variables explicativas que aún no hemos observado. No siempre se puede ni conviene eliminar.
* **Valor atípico[^atipico]:** observación inusual respecto de la mayoría. Puede ser válida o indicar un error; hay que investigar su origen antes de corregirla.
## Visibilidad de productos y una serie simulada
En **BigMart Sales**, los registros con `Item_Visibility = 0` merecen una revisión: un cero podría ser legítimo, una convención de captura o un dato faltante codificado. No podemos deducir su causa sin documentación del proceso de medición. Para practicar con ventas diarias usaremos una serie **sintética** con tendencia conocida y perturbaciones añadidas; así podremos ver cuándo un filtro mejora la lectura de la señal y cuándo borra picos reales. Un modelo que memoriza fluctuaciones accidentales puede sufrir sobreajuste[^2].
---
## Ejemplo trabajado: discretización y suavizado a mano
Para desmitificar el funcionamiento de los algoritmos computacionales de suavizado y discretización, desarrollaremos los cálculos de forma completamente manual.
Consideremos un conjunto numérico ordenado de 12 pesos ficticios de productos (**Item_Weight**). Es un ejemplo didáctico, no una muestra extraída del archivo BigMart:
$$ X = [4, 8, 9, 15, 21, 21, 24, 25, 26, 28, 29, 34] $$
### Discretización por igual ancho (*equal-width binning*)
Divide el rango de los datos en $k$ intervalos del mismo tamaño:
1. Número de bins: $k = 3$.
2. Rango: $\min(X) = 4$, $\max(X) = 34$.
3. Ancho del bin ($W$):
$$ W = \frac{\max(X) - \min(X)}{k} = \frac{34 - 4}{3} = \frac{30}{3} = 10 $$
4. Intervalos:
- Bin 1: $[4, 14)$
- Bin 2: $[14, 24)$
- Bin 3: $[24, 34]$
5. Asignación de elementos:
- Bin 1: $\{4, 8, 9\}$ (3 elementos)
- Bin 2: $\{15, 21, 21\}$ (3 elementos)
- Bin 3: $\{24, 25, 26, 28, 29, 34\}$ (6 elementos)
### Discretización por igual frecuencia (*equal-frequency binning*)
Asigna el mismo número de observaciones $N/k$ a cada bin:
1. $N = 12, k = 3 \implies 12 / 3 = 4$ elementos por bin.
2. Asignación ordenada:
- Bin 1: $\{4, 8, 9, 15\}$
- Bin 2: $\{21, 21, 24, 25\}$
- Bin 3: $\{26, 28, 29, 34\}$
### Suavizado de datos dentro de los bins
Tomando los bins de igual frecuencia:
**Suavizado por Medias del Bin.**
- **Bin 1 $\{4, 8, 9, 15\}$**: $\mu_1 = \frac{4+8+9+15}{4} = \frac{36}{4} = 9.0 \implies [9.0, 9.0, 9.0, 9.0]$
- **Bin 2 $\{21, 21, 24, 25\}$**: $\mu_2 = \frac{21+21+24+25}{4} = \frac{91}{4} = 22.75 \implies [22.75, 22.75, 22.75, 22.75]$
- **Bin 3 $\{26, 28, 29, 34\}$**: $\mu_3 = \frac{26+28+29+34}{4} = \frac{117}{4} = 29.25 \implies [29.25, 29.25, 29.25, 29.25]$
Señal final suavizada: $[9.0, 9.0, 9.0, 9.0, 22.75, 22.75, 22.75, 22.75, 29.25, 29.25, 29.25, 29.25]$
**Suavizado por Medianas del Bin.**
- **Bin 1**: Mediana de $\{4, 8, 9, 15\} = \frac{8 + 9}{2} = 8.5 \implies [8.5, 8.5, 8.5, 8.5]$
- **Bin 2**: Mediana de $\{21, 21, 24, 25\} = \frac{21 + 24}{2} = 22.5 \implies [22.5, 22.5, 22.5, 22.5]$
- **Bin 3**: Mediana de $\{26, 28, 29, 34\} = \frac{28 + 29}{2} = 28.5 \implies [28.5, 28.5, 28.5, 28.5]$
**Suavizado por Límites del Bin (Boundaries).**
Cada valor se reemplaza por el extremo del bin (mínimo o máximo) más cercano:
- **Bin 1** (Límites: 4 y 15):
- 4 está en el límite: 4
- 8 está más cerca de 4 ($|8-4|=4$) que de 15 ($|8-15|=7$): 4
- 9 está más cerca de 4 ($|9-4|=5$) que de 15 ($|9-15|=6$): 4
- 15 está en el límite: 15
- Resultado Bin 1: $[4, 4, 4, 15]$
- **Bin 2** (Límites: 21 y 25): 21 y 21 ya son el límite inferior; 24 está más cerca de 25 ($1$ frente a $3$). Resultado: $[21, 21, 25, 25]$.
- **Bin 3** (Límites: 26 y 34): 28 está más cerca de 26 ($2$ frente a $6$) y 29 también ($3$ frente a $5$). Resultado: $[26, 26, 26, 34]$.
**¿Cuánta información se pierde?** Una medida simple es la suma de cuadrados entre cada valor y su sustituto. Para el bin 1: con la media, $(4-9)^2+(8-9)^2+(9-9)^2+(15-9)^2=62$; con los límites, $0+16+25+0=41$. Ningún método es mejor en general: la media minimiza esta suma **dentro** de cada bin cuando el sustituto es un único valor[^mediaopt], mientras que los límites usan dos valores y conservan datos que sí existieron.
### Suavizado por regresión lineal manual
Tomemos 5 observaciones con ruido de ventas de un producto en función de la semana:
$(1, 2.1), (2, 3.9), (3, 6.2), (4, 7.8), (5, 10.3)$
1. $\bar{x} = \frac{1+2+3+4+5}{5} = 3.0$
2. $\bar{y} = \frac{2.1+3.9+6.2+7.8+10.3}{5} = \frac{30.3}{5} = 6.06$
3. $\sum (x_i - \bar{x})^2 = (-2)^2 + (-1)^2 + 0^2 + 1^2 + 2^2 = 4 + 1 + 0 + 1 + 4 = 10.0$
4. $\sum (x_i - \bar{x})(y_i - \bar{y})$:
- $(1-3)(2.1-6.06) = (-2)(-3.96) = 7.92$
- $(2-3)(3.9-6.06) = (-1)(-2.16) = 2.16$
- $(3-3)(6.2-6.06) = 0$
- $(4-3)(7.8-6.06) = (1)(1.74) = 1.74$
- $(5-3)(10.3-6.06) = (2)(4.24) = 8.48$
- Suma = $7.92 + 2.16 + 0 + 1.74 + 8.48 = 20.30$
5. Pendiente $\beta_1 = \frac{20.30}{10.0} = 2.03$
6. Intercepto $\beta_0 = \bar{y} - \beta_1 \bar{x} = 6.06 - (2.03 \times 3.0) = 6.06 - 6.09 = -0.03$
7. Predicciones suavizadas ($\hat{y}_i = 2.03 x_i - 0.03$):
- $\hat{y}_1 = 2.00$ (original: 2.1)
- $\hat{y}_2 = 4.03$ (original: 3.9)
- $\hat{y}_3 = 6.06$ (original: 6.2)
- $\hat{y}_4 = 8.09$ (original: 7.8)
- $\hat{y}_5 = 10.12$ (original: 10.3)
---
## Formalización: modelos y filtros
### Modelo aditivo de señal y ruido
Un modelo posible representa cada observación $y_i$ como señal subyacente $f(x_i)$ más un error $\epsilon_i$:
$$ y_i = f(x_i) + \epsilon_i $$
Bajo un supuesto simplificador, los errores tienen media cero y varianza constante[^homocedasticidad]:
$$ \mathbb{E}[\epsilon_i] = 0, \quad \operatorname{Var}(\epsilon_i) = \sigma^2 $$
### Relación señal-ruido (SNR)
Si conocemos o estimamos por separado la potencia de la señal y la del error, la relación señal-ruido puede expresarse en decibelios:
$$ \text{SNR}_{\text{dB}} = 10 \log_{10}\left(\frac{P_{\text{señal}}}{P_{\text{ruido}}}\right) $$
### Compromiso entre suavidad y detalle
Una ventana pequeña conserva variaciones rápidas, incluido el ruido. Una ventana grande reduce fluctuaciones, pero también retrasa o aplana cambios reales. Este compromiso tiene una forma matemática precisa para la media móvil centrada con $2m+1$ observaciones.
**Varianza (lo que ganamos).** Si los errores son ruido blanco[^ruidoblanco] con varianza $\sigma^2$, la media de $2m+1$ de ellos tiene varianza
$$
\operatorname{Var}\!\left(\frac{1}{2m+1}\sum_{j=-m}^{m}\epsilon_{t+j}\right)=\frac{\sigma^2}{2m+1},
$$
porque las varianzas de variables independientes se suman y la constante sale al cuadrado. Con $\sigma=0.4$ y una ventana de 15 días, la desviación del ruido que sobrevive es $0.4/\sqrt{15}\approx0.10$.
**Sesgo (lo que pagamos).** Si la señal se curva, promediar vecinos la aplana. Con una expansión de Taylor[^taylor] de $f$ alrededor de $t$, los términos lineales se cancelan por simetría y queda
$$
\mathbb{E}[\hat y_t]-f(t)\approx\frac{f''(t)}{2}\cdot\frac{1}{2m+1}\sum_{j=-m}^{m}j^2=\frac{m(m+1)}{6}\,f''(t).
$$
El sesgo crece aproximadamente con $m^2$ y es mayor donde la curvatura $|f''|$ es grande: en picos y valles. El error cuadrático medio[^ecm] suma ambos efectos, $\text{ECM}=\text{sesgo}^2+\text{varianza}$; por eso la curva de error frente a la ventana tiene forma de U y existe una ventana intermedia óptima.
La media exponencial con $\alpha=2/(w+1)$ (el parámetro `span=w` de pandas) tiene, en régimen estable, varianza $\frac{\alpha}{2-\alpha}\sigma^2=\sigma^2/w$: la misma reducción que una media móvil de $w$ observaciones, pero usando solo el pasado.
### Filtros de suavizado comunes
Si una serie fue muestreada con poca frecuencia, algunos cambios rápidos pueden confundirse con patrones lentos (*aliasing*)[^5]. Los filtros siguientes responden a preguntas distintas:
1. **Media móvil centrada (SMA):**
$$ \hat{y}_t = \frac{1}{2m+1}\sum_{j=-m}^m y_{t+j} $$
Usa observaciones futuras; sirve para exploración retrospectiva, pero no para predecir en tiempo real.
2. **Suavizado Exponencial (EWMA):**
$$ \hat{y}_t = \alpha y_t + (1-\alpha) \hat{y}_{t-1}, \quad \alpha \in [0, 1] $$
3. **Filtro de Savitzky-Golay:** ajusta un polinomio local por mínimos cuadrados. Puede conservar mejor algunos rasgos de la curva que una media móvil, según la ventana y el orden del polinomio; la versión centrada también usa observaciones futuras.
---
## Laboratorio interactivo: suavizado y discretización
**Objetivo.** Comparar cómo cambian los grupos al modificar el número de intervalos y comprobar en una serie simulada el compromiso sesgo–varianza de la sección anterior.
1. Con tres intervalos, compara ancho igual y frecuencia igual. Comprueba que los 12 precios quedan asignados una sola vez y describe qué método produce grupos más equilibrados.
2. Aplica media, mediana y límites del intervalo. La «pérdida» es la suma de cuadrados entre cada precio y su valor suavizado. ¿Qué método pierde menos? ¿Por qué la pérdida siempre disminuye al aumentar $k$?
3. En la serie, activa el pico de promoción y usa la media móvil centrada con ventana 3, 11 y 31. Observa el pico y ubica cada ventana en la curva «error frente a ventana». ¿Dónde está el mínimo de la U?
4. Duplica el ruido. ¿La ventana óptima se hace más grande o más pequeña? Relaciónalo con las fórmulas de varianza y sesgo.
**Entrega breve.** Anota una decisión de transformación con su propósito y un riesgo. La línea negra es la señal verdadera solo porque **simulamos** los datos; en un caso real no la conoceríamos.
::: {.content-visible when-format="html"}
```{=html}
<div class="plab" id="noise-lab">
<p class="plab-eyebrow">Laboratorio · Discretización</p>
<h3 class="plab-title">Doce precios, k intervalos</h3>
<p class="plab-sub">Precios ficticios de <code>Item_MRP</code>: 35, 48, 52, 98, 105, 112, 145, 160, 185, 210, 240, 250.</p>
<div class="plab-controls">
<div class="plab-field" style="flex:2 1 230px">Partición
<div class="plab-seg" id="nb-type"><button type="button" data-value="width" aria-pressed="true">Igual ancho</button><button type="button" data-value="freq">Igual frecuencia</button></div>
</div>
<label>Número de intervalos k: <b id="nb-k-v"></b><input type="range" id="nb-k" min="2" max="6" value="3"></label>
<div class="plab-field" style="flex:2 1 260px">Suavizado dentro del intervalo
<div class="plab-seg" id="nb-smooth"><button type="button" data-value="none" aria-pressed="true">Ninguno</button><button type="button" data-value="mean">Media</button><button type="button" data-value="median">Mediana</button><button type="button" data-value="boundary">Límites</button></div>
</div>
</div>
<svg id="nb-chart" class="wide" role="img" aria-label="Precios en una recta con los cortes de los intervalos"></svg>
<div class="plab-stats">
<div class="plab-stat"><span>Tamaños de los grupos</span><strong id="nb-sizes"></strong></div>
<div class="plab-stat"><span>Precios asignados</span><strong id="nb-total"></strong></div>
<div class="plab-stat"><span>Pérdida Σ(x − x̂)²</span><strong id="nb-loss"></strong></div>
</div>
</div>
<div class="plab" id="filter-lab">
<p class="plab-eyebrow">Laboratorio · Filtros</p>
<h3 class="plab-title">Ventas diarias simuladas: ¿cuánto suavizar?</h3>
<p class="plab-sub">Serie de 120 días = tendencia estacional + ruido gaussiano (+ un pico real de promoción, opcional).</p>
<div class="plab-controls">
<div class="plab-field" style="flex:2 1 250px">Filtro
<div class="plab-seg" id="nf-type"><button type="button" data-value="sma" aria-pressed="true">Media móvil centrada</button><button type="button" data-value="ewma">Media exponencial</button></div>
</div>
<label>Ventana w: <b id="nf-w-v"></b><input type="range" id="nf-w" min="1" max="41" step="2" value="11"></label>
<label>Desviación del ruido σ: <b id="nf-s-v"></b><input type="range" id="nf-s" min="0.1" max="1.2" step="0.05" value="0.4"></label>
<label style="flex:1 1 180px;flex-direction:row;align-items:center;gap:.4rem"><input type="checkbox" id="nf-spike" checked> Pico real de promoción (día 70)</label>
</div>
<div class="plab-grid">
<div class="plab-panel"><h4>Serie y filtro</h4>
<svg id="nf-series" role="img" aria-label="Serie simulada con la señal verdadera y el filtro"></svg>
<div class="plab-legend"><span><i class="dot" style="background:var(--lab-axis)"></i>observado</span><span><i style="background:var(--lab-text)"></i>señal verdadera</span><span><i style="background:var(--lab-s1)"></i>filtrado</span></div>
</div>
<div class="plab-panel"><h4>Error frente a ventana (misma serie)</h4>
<svg id="nf-curve" role="img" aria-label="Raíz del error cuadrático medio según la ventana"></svg>
<div class="plab-legend"><span><i style="background:var(--lab-s1)"></i>media móvil</span><span><i style="background:var(--lab-s2)"></i>media exponencial</span><span><i class="dot" style="background:var(--lab-s5)"></i>ventana actual</span></div>
</div>
</div>
<div class="plab-stats">
<div class="plab-stat"><span>RMSE sin filtrar</span><strong id="nf-raw"></strong></div>
<div class="plab-stat"><span>RMSE con filtro</span><strong id="nf-rmse"></strong></div>
<div class="plab-stat"><span>Pico: real / filtrado</span><strong id="nf-peak"></strong></div>
<div class="plab-stat"><span>Mejor ventana (media móvil)</span><strong id="nf-best"></strong></div>
</div>
<p class="plab-note" id="nf-note" aria-live="polite"></p>
</div>
<script>
(() => {
const K = window.LabKit;
// ---------- Discretización ----------
const nb = document.getElementById('noise-lab'), $b = s => nb.querySelector(s);
const prices = [35, 48, 52, 98, 105, 112, 145, 160, 185, 210, 240, 250];
const type = K.segmented($b('#nb-type'), () => renderBins()), smooth = K.segmented($b('#nb-smooth'), () => renderBins());
function bins(k) {
if (type.get() === 'width') {
const lo = prices[0], hi = prices[prices.length - 1], w = (hi - lo) / k;
const edges = Array.from({ length: k + 1 }, (_, i) => lo + i * w);
return { edges, groups: edges.slice(0, -1).map((e, i) => prices.filter(p => p >= e && (i === k - 1 ? p <= edges[i + 1] : p < edges[i + 1]))) };
}
const groups = [], base = Math.floor(prices.length / k), extra = prices.length % k; let off = 0;
for (let i = 0; i < k; i++) { const n = base + (i < extra ? 1 : 0); groups.push(prices.slice(off, off + n)); off += n; }
const edges = [prices[0], ...groups.slice(1).map((g, i) => (groups[i][groups[i].length - 1] + g[0]) / 2), prices[prices.length - 1]];
return { edges, groups };
}
function smoothGroup(g) {
if (!g.length) return [];
const m = smooth.get();
if (m === 'mean') return g.map(() => K.mean(g));
if (m === 'median') return g.map(() => K.median(g));
if (m === 'boundary') return g.map(v => (v - g[0] <= g[g.length - 1] - v) ? g[0] : g[g.length - 1]);
return g.slice();
}
function renderBins() {
const k = +$b('#nb-k').value; $b('#nb-k-v').textContent = k;
const { edges, groups } = bins(k), sm = groups.map(smoothGroup), on = smooth.get() !== 'none';
const c = K.chart($b('#nb-chart'), { width: 640, height: on ? 190 : 140, x: [20, 265], y: [0, on ? 2 : 1], margin: { left: 16, right: 16, top: 8, bottom: 34 } });
c.axes({ xTicks: 8, yTicks: 0, grid: false, xLabel: 'Item_MRP ($)' });
const cls = ['s1', 's2', 's3', 's4', 's5', 's1'];
edges.forEach(e => c.line(e, 0, e, on ? 2 : 1, 'muted line thin dash'));
const yTop = on ? 1.45 : .5;
groups.forEach((g, i) => g.forEach((v, j) => {
c.dot(v, yTop, 6, cls[i] + ' dot');
if (on) { c.line(v, yTop - .12, sm[i][j], .55, cls[i] + ' line thin').style.opacity = .6; c.dot(sm[i][j], .45, 5, cls[i] + ' dot'); }
}));
c.text(20, yTop, 'original', { dx: 2, dy: -12 });
if (on) c.text(20, .45, 'suavizado', { dx: 2, dy: 20 });
$b('#nb-sizes').textContent = groups.map(g => g.length).join(' · ');
$b('#nb-total').textContent = `${K.sum(groups.map(g => g.length))} de ${prices.length}`;
$b('#nb-loss').textContent = on ? K.fmt(K.sum(groups.flatMap((g, i) => g.map((v, j) => (v - sm[i][j]) ** 2))), 1) : '—';
}
$b('#nb-k').addEventListener('input', renderBins);
renderBins();
// ---------- Filtros ----------
const nf = document.getElementById('filter-lab'), $f = s => nf.querySelector(s);
const N = 120, rand = K.rng(3), z = Array.from({ length: N }, () => rand.normal());
const ftype = K.segmented($f('#nf-type'), () => renderFilter());
const signal = spike => Array.from({ length: N }, (_, t) => 3 + Math.sin(t / 12) + .5 * Math.sin(t / 4) + (spike ? 2.2 * Math.exp(-((t - 70) ** 2) / 8) : 0));
const sma = (y, w) => { const h = (w - 1) / 2; return y.map((_, t) => { let s = 0, n = 0; for (let j = t - h; j <= t + h; j++) if (j >= 0 && j < y.length) { s += y[j]; n++; } return s / n; }); };
const ewma = (y, w) => { const a = 2 / (w + 1); let s = y[0]; return y.map((v, t) => (s = t ? a * v + (1 - a) * s : v)); };
const rmse = (a, b) => Math.sqrt(K.mean(a.map((v, i) => (v - b[i]) ** 2)));
function renderFilter() {
const w = +$f('#nf-w').value, sig = +$f('#nf-s').value, spike = $f('#nf-spike').checked, kind = ftype.get();
$f('#nf-w-v').textContent = w + (w === 1 ? ' (sin suavizar)' : ' días');
$f('#nf-s-v').textContent = K.fmt(sig, 2);
const f = signal(spike), y = f.map((v, t) => v + sig * z[t]);
const filt = kind === 'sma' ? sma(y, w) : ewma(y, w);
const c = K.chart($f('#nf-series'), { width: 420, height: 280, x: [0, N - 1], y: [0, 7.5], margin: { left: 36, right: 12, top: 12, bottom: 38 } });
c.axes({ xTicks: 6, yTicks: 5, xLabel: 'día', yLabel: 'ventas (miles)' });
y.forEach((v, t) => c.dot(t, v, 1.8, 'muted'));
const tl = c.path(f.map((v, t) => [t, v]), 'line dash'); tl.style.stroke = 'var(--lab-text)';
c.path(filt.map((v, t) => [t, v]), 's1 line');
// Curva de error para todas las ventanas: sobre la misma realización del ruido.
const ws = Array.from({ length: 21 }, (_, i) => 2 * i + 1);
const eS = ws.map(v => rmse(sma(y, v), f)), eE = ws.map(v => rmse(ewma(y, v), f));
const top = Math.max(...eS, ...eE) * 1.1;
const k = K.chart($f('#nf-curve'), { width: 420, height: 280, x: [1, 41], y: [0, top], margin: { left: 44, right: 12, top: 12, bottom: 38 } });
k.axes({ xTicks: 8, yTicks: 5, xLabel: 'ventana w', yLabel: 'RMSE frente a la señal', yFmt: v => K.fmt(v, 2) });
k.path(ws.map((v, i) => [v, eS[i]]), 's1 line'); k.path(ws.map((v, i) => [v, eE[i]]), 's2 line');
const cur = rmse(filt, f); k.dot(w, cur, 6, 's5 dot');
const bi = eS.indexOf(Math.min(...eS));
k.line(ws[bi], 0, ws[bi], top, 's1 line thin dash');
$f('#nf-raw').textContent = K.fmt(rmse(y, f), 3);
$f('#nf-rmse').textContent = K.fmt(cur, 3);
$f('#nf-peak').textContent = spike ? `${K.fmt(f[70], 2)} / ${K.fmt(filt[70], 2)}` : '—';
$f('#nf-best').textContent = `w = ${ws[bi]}`;
const note = $f('#nf-note');
note.className = 'plab-note' + (w > ws[bi] + 6 ? ' warn' : w < ws[bi] - 6 ? '' : ' good');
note.textContent = w > ws[bi] + 6 ? `Ventana demasiado grande: domina el sesgo. ${spike ? `El pico real (${K.fmt(f[70], 2)}) queda en ${K.fmt(filt[70], 2)}.` : 'La curva se aplana en los valles y crestas.'}`
: w < ws[bi] - 6 ? `Ventana pequeña: domina la varianza; el filtro todavía sigue buena parte del ruido (σ/√w ≈ ${K.fmt(sig / Math.sqrt(w), 2)}).`
: `Cerca del mínimo de la U: sesgo y varianza están equilibrados para este nivel de ruido.${kind === 'ewma' ? ' La media exponencial usa solo el pasado, así que además se retrasa.' : ' Recuerda que la media centrada usa días futuros.'}`;
}
nf.querySelectorAll('input').forEach(i => i.addEventListener('input', renderFilter));
renderFilter();
})();
</script>
```
:::
::: {.content-visible when-format="pdf"}
*(El laboratorio interactivo con precios y ventas simuladas está disponible en la versión web del libro. Las preguntas y los datos de ejemplo anteriores también pueden resolverse sin los controles.)*
:::
---
## Implementación computacional con Python
```{python}
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from scipy.signal import savgol_filter
# 1. Serie sintética de demanda; no son ventas observadas de BigMart
rng = np.random.default_rng(42)
dias = np.linspace(0, 10, 200)
demanda_real = np.sin(dias) + 0.5 * np.sin(3 * dias) + 3.0
ruido = rng.normal(0, 0.4, size=len(dias))
ventas_ruidosas = demanda_real + ruido
df_ventas = pd.DataFrame({'dia': dias, 'demanda_real': demanda_real, 'ventas_ruidosas': ventas_ruidosas})
# 2. Técnicas de Suavizado
df_ventas['sma_15'] = df_ventas['ventas_ruidosas'].rolling(window=15, center=True, min_periods=1).mean()
df_ventas['ewma_15'] = df_ventas['ventas_ruidosas'].ewm(span=15, adjust=False).mean()
df_ventas['savgol'] = savgol_filter(df_ventas['ventas_ruidosas'], window_length=15, polyorder=3)
# 3. Discretización de Precios con pandas
precios_bigmart = [35, 48, 52, 98, 105, 112, 145, 160, 185, 210, 240, 250]
df_precios = pd.DataFrame({'Item_MRP': precios_bigmart})
df_precios['equal_width'] = pd.cut(df_precios['Item_MRP'], bins=3)
df_precios['equal_freq'] = pd.qcut(df_precios['Item_MRP'], q=3)
print("--- Discretización de Item_MRP ---")
print(df_precios.head(6))
```
### Visualización del suavizado con Plotly
```{python}
fig = go.Figure()
for columna, etiqueta, color, estilo in [
('ventas_ruidosas', 'Observaciones ruidosas', '#a6a6a6', 'solid'),
('demanda_real', 'Señal conocida (simulación)', '#222222', 'dash'),
('sma_15', 'Media móvil centrada', '#d62728', 'solid'),
('ewma_15', 'Media exponencial causal', '#2ca02c', 'solid'),
('savgol', 'Savitzky-Golay centrado', '#1f77b4', 'solid'),
]:
fig.add_trace(go.Scatter(x=df_ventas['dia'], y=df_ventas[columna],
mode='lines', name=etiqueta,
line=dict(color=color, dash=estilo)))
fig.update_layout(title='Comparación de filtros sobre una serie sintética',
xaxis_title='Tiempo simulado', yaxis_title='Demanda simulada',
template='plotly_white', height=460)
fig.show()
```
---
## Prácticas para usar en proyectos reales
::: {.callout-important title="Regla de oro en producción: Prevención de fuga de datos"}
Aprende los límites de discretización con datos de entrenamiento y aplícalos después a validación, prueba y producción. Registra cómo se tratan valores fuera del rango aprendido. Para series temporales, valida respetando el orden cronológico y evita filtros centrados cuando la aplicación exige decisiones en tiempo real.
:::
::: {.callout-tip title="Mejores prácticas en la industria: Cuándo discretizar"}
* **Conservar el valor continuo** suele ser una referencia razonable: discretizar pierde resolución[^1]. Comprueba con validación si los intervalos añaden valor al modelo y al uso de negocio.
* **Discretizar** puede facilitar reglas interpretables o niveles contractuales. En algunos modelos de riesgo se usan transformaciones como *Weight of Evidence*[^4]; deben aprenderse y validarse sin fuga de la variable objetivo.
:::
---
## Notas de términos
[^1]: **Pérdida de resolución por discretización:** valores distintos pasan a compartir una categoría. Esto puede reducir detalle útil, aunque en algunos problemas mejore robustez o interpretabilidad.
[^2]: **Sobreajuste al ruido estocástico (Overfitting):** Ocurre cuando un algoritmo de ML asimila las fluctuaciones aleatorias de la señal como si fueran patrones explicables del fenómeno.
[^atipico]: **Valor atípico:** observación distante de la mayoría según una referencia explícita (por ejemplo, un rango intercuartílico). Ser atípico no implica ser incorrecto.
[^homocedasticidad]: **Homocedasticidad:** varianza del error aproximadamente constante en los distintos valores de la variable explicativa.
[^4]: **Weight of Evidence (WOE):** codificación de categorías mediante el logaritmo de la razón entre proporciones de dos clases. Requiere reglas para categorías sin observaciones de alguna clase.
[^5]: **Aliasing:** efecto por el cual una señal muestreada con frecuencia insuficiente puede aparentar una frecuencia distinta. El criterio de Nyquist se aplica a señales con banda de frecuencias limitada y muestreo regular.
[^ruidoblanco]: **Ruido blanco:** sucesión de errores con media cero, la misma varianza y sin correlación entre instantes distintos. Es un modelo idealizado: en ventas reales el error de hoy suele parecerse al de ayer (autocorrelación), y entonces promediar reduce la varianza menos de lo que indica $\sigma^2/(2m+1)$.
[^taylor]: **Expansión de Taylor:** aproximación de una función suave cerca de un punto mediante sus derivadas: $f(t+j)\approx f(t)+j f'(t)+\tfrac{j^2}{2}f''(t)$. Al promediar $j=-m,\dots,m$, el término con $j$ se anula por simetría y sobrevive el de $j^2$.
[^ecm]: **Error cuadrático medio (ECM):** $\mathbb{E}[(\hat y_t-f(t))^2]$. Sumando y restando $\mathbb{E}[\hat y_t]$ se descompone en $\text{sesgo}^2+\text{varianza}$: una idea que reaparece al elegir la complejidad de cualquier modelo.
[^mediaopt]: **La media minimiza la suma de cuadrados:** $\sum_i (x_i-c)^2$ es una parábola en $c$ cuya derivada, $-2\sum_i(x_i-c)$, se anula en $c=\bar x$. La mediana minimiza, en cambio, la suma de valores absolutos $\sum_i|x_i-c|$.