---
title: "Introducción al preprocesamiento y la calidad de datos"
pagetitle: "Introducción al preprocesamiento y la calidad de datos"
open-graph:
description: "Introduce la calidad de datos y el preprocesamiento con un ejemplo manual de BigMart: limpieza, estandarización y codificación one-hot."
twitter-card:
description: "Introduce la calidad de datos y el preprocesamiento con un ejemplo manual de BigMart: limpieza, estandarización y codificación one-hot."
description: "Introduce la calidad de datos y el preprocesamiento con un ejemplo manual de BigMart: limpieza, estandarización y codificación one-hot."
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 >}}
Preprocesar datos es convertir registros capturados para operar un negocio en entradas coherentes para una pregunta analítica. Antes de transformar una columna, preguntamos qué representa, cómo se obtuvo y qué información estará disponible cuando se use el modelo. La limpieza ayuda a evitar errores conocidos; no garantiza buenas predicciones.
En BigMart, `LF` y `Low Fat` pueden significar lo mismo, un peso negativo requiere revisión y un precio extraordinario podría ser un error o un producto especial. Una regla automática no puede decidir lo último sin contexto. Tampoco todos los modelos necesitan el mismo tratamiento: algunos aceptan valores ausentes o categorías directamente, mientras que otros requieren imputación o codificación.[^modelos]
::: {.callout-note title="Antes de limpiar"}
Conserva una copia del valor original y registra la regla aplicada. Convierte en ausente un valor imposible según una regla de negocio documentada; investiga un valor atípico plausible antes de eliminarlo.
:::
## Ejemplo: de una tabla cruda a variables utilizables
Esta muestra **inventada para aprender** contiene problemas de completitud, validez y consistencia. Los límites de peso y precio que usaremos son supuestos del ejercicio, no reglas generales de BigMart.
| Producto | `Item_Weight` | `Item_MRP` | `Outlet_Establishment_Year` | `Item_Fat_Content` |
|---|---:|---:|---|---|
| FDA15 | 9.30 | 249.80 | 1999 | Low Fat |
| DRC01 | -5.00 | 0.00 | 09-Jan-2009 | low fat |
| FDN15 | NA | 1200000.00 | 1998 | LF |
| FDX07 | 19.20 | NA | 14/02/1998 | Regular |
| NCD19 | 200.00 | 53.86 | 1985-01-20 | reg |
El orden de trabajo es: definir qué valores violan reglas del dominio; unificar etiquetas y formatos; separar entrenamiento y prueba; **ajustar** las estadísticas y categorías con entrenamiento; **aplicar** esas mismas transformaciones a ambos conjuntos. El identificador del producto no es una medida continua: aquí lo conservamos para auditoría, pero no lo enviamos al modelo.
### Una transformación a mano
Consideremos tres pesos de entrenamiento: $10$, ausente y $20$. Si elegimos imputación por media, calculamos $(10+20)/2=15$ con los dos pesos observados. La columna resultante es $(10,15,20)$. Su media es $15$ y su desviación estándar poblacional[^desv] es $\sqrt{\tfrac{(10-15)^2+(15-15)^2+(20-15)^2}{3}}=\sqrt{50/3}\approx4.082$. Al estandarizarla obtenemos $\big(\tfrac{10-15}{4.082},\,0,\,\tfrac{20-15}{4.082}\big)\approx(-1.225,0,1.225)$.
Ahora llega un producto nuevo de prueba con peso $40$. Si por descuido «ajustamos» con las cuatro filas, la media de imputación pasa a $(10+20+40)/3\approx23.33$: el valor que asignamos a una fila **de entrenamiento** depende de un dato que no deberíamos haber visto. Esta es la idea de fuga que formalizaremos enseguida y que el laboratorio permite manipular.
Si las categorías son `Low Fat`, `LF` y `Regular`, primero unificamos `LF` con `Low Fat`. Después, una codificación *one hot*[^onehot] da las columnas `Fat_Low` y `Fat_Regular`:
| Peso estandarizado | `Fat_Low` | `Fat_Regular` |
|---:|---:|---:|
| -1.225 | 1 | 0 |
| 0 | 1 | 0 |
| 1.225 | 0 | 1 |
Esta matriz solo ilustra un posible preprocesador. El escalado es útil para muchos modelos sensibles a magnitudes o distancias, pero no es un requisito universal.[^escalado]
## Formalización: reglas, parámetros y calidad
Un pipeline es una composición[^composicion] ordenada $T=T_k\circ\cdots\circ T_1$. Algunas etapas aplican reglas establecidas por el dominio; otras aprenden parámetros[^parametro] $\theta$ a partir de datos. Para evaluar un modelo, ajustamos $\hat\theta=\operatorname{fit}(X_{\mathrm{train}})$ y usamos el mismo estado en $T_{\hat\theta}(X_{\mathrm{train}})$ y $T_{\hat\theta}(X_{\mathrm{test}})$. Ajustar con $X_{\mathrm{test}}$ incorpora información de evaluación al entrenamiento, una forma de *fuga de datos*.[^fuga]
Un preprocesador suele combinar comprobaciones de calidad distintas:
| Dimensión | Pregunta práctica | Ejemplo de control |
|---|---|---|
| Completitud | ¿Faltan valores requeridos? | Proporción de `Item_Weight` ausente |
| Validez | ¿Respeta tipos y rangos acordados? | Peso positivo dentro de un rango de negocio |
| Consistencia | ¿Coinciden representaciones equivalentes? | `LF` y `Low Fat` |
| Unicidad | ¿Hay registros duplicados de una entidad? | Producto y tienda repetidos sin justificación |
| Exactitud | ¿Se aproxima el registro a la realidad? | Contrastar un precio con la fuente original |
| Oportunidad | ¿Llegó a tiempo para la decisión? | Retraso entre venta y captura |
Para una columna requerida, una medida simple de completitud es $C=1-\frac{n_{\mathrm{ausentes}}}{n}$. Las demás dimensiones necesitan reglas y fuentes de referencia propias; una sola puntuación no las resume.
::: {.callout-tip title="Práctica en equipos de datos"}
Documenta el esquema y las reglas de calidad que afectan al modelo, valida los lotes nuevos y observa cómo cambian faltantes, rangos y categorías con el tiempo. Una alerta debe conducir a investigar la causa; una variación de distribución por sí sola no demuestra pérdida de rendimiento.[^drift]
:::
## Laboratorio: ¿qué cambia si ajustamos con datos de prueba?
**Objetivo.** Identificar qué estadísticas pertenecen al entrenamiento y observar cómo se altera una imputación cuando se usan datos de prueba para calcularlas.
**Hipótesis.** Con pesos observados de entrenamiento $10$ y $20$, la media para imputar debe ser $15$. Si se incorpora el peso $40$ de prueba al ajuste, la media cambia.
**Pasos.** Con «Imputar peso» activo, compara ambos modos de ajuste y observa cómo se mueve la línea naranja. Activa «Estandarizar precio» y anota μ y σ en cada modo. Por último, activa «Codificar tienda»: la fila de prueba pertenece a una tienda *Hypermarket* que no aparece en entrenamiento. ¿Qué columnas existen en cada modo y cuál de las dos matrices podría producirse realmente en producción?
::: {.content-visible when-format="html"}
```{=html}
<div class="plab" id="intro-lab">
<p class="plab-eyebrow">Laboratorio · Ajustar y aplicar</p>
<h3 class="plab-title">Simulador de entrenamiento y prueba</h3>
<p class="plab-sub">Tres filas de entrenamiento y una de prueba. Activa transformaciones y cambia qué filas se usan para <b>aprender</b> sus parámetros.</p>
<div class="plab-controls">
<div class="plab-field" style="flex:2 1 260px">Filas usadas en <code>.fit()</code>
<div class="plab-seg" id="intro-fit"><button type="button" data-value="train" aria-pressed="true">Solo entrenamiento</button><button type="button" data-value="all">Entrenamiento + prueba</button></div>
</div>
<div class="plab-field" style="flex:3 1 320px">Transformaciones
<div style="display:flex;flex-wrap:wrap;gap:.3rem 1rem;color:var(--lab-text)">
<label><input type="checkbox" id="lab-clean" checked> Unificar grasa (regla fija)</label>
<label><input type="checkbox" id="lab-impute" checked> Imputar peso (aprende media)</label>
<label><input type="checkbox" id="lab-scale"> Estandarizar precio (aprende μ, σ)</label>
<label><input type="checkbox" id="lab-ohe"> Codificar tienda (aprende categorías)</label>
</div>
</div>
</div>
<div class="plab-grid" style="grid-template-columns:1fr">
<div class="plab-panel"><h4>Parámetros aprendidos</h4>
<svg id="intro-chart" class="wide" role="img" aria-label="Pesos y media aprendida para imputar"></svg>
<div class="plab-legend"><span><i class="dot" style="background:var(--lab-s1)"></i>peso de entrenamiento</span><span><i class="dot" style="background:var(--lab-s5)"></i>peso de prueba</span><span><i style="background:var(--lab-s2)"></i>media usada para imputar</span></div>
<p id="lab-parameters" aria-live="polite" style="font-size:.85rem;margin:.5rem 0 0"></p>
</div>
<div class="plab-panel"><h4>Matriz resultante</h4><div class="plab-scroll" id="lab-table"></div></div>
</div>
<p class="plab-note" id="lab-feedback" aria-live="polite"></p>
</div>
<script>
(() => {
const K = window.LabKit, root = document.getElementById('intro-lab');
const $ = s => root.querySelector(s);
const data = [
{ set: 'Entrenamiento', weight: 10, fat: 'Low Fat', mrp: 100, outlet: 'Supermarket' },
{ set: 'Entrenamiento', weight: 20, fat: 'LF', mrp: 150, outlet: 'Grocery' },
{ set: 'Entrenamiento', weight: null, fat: 'reg', mrp: 200, outlet: 'Supermarket' },
{ set: 'Prueba', weight: 40, fat: 'Regular', mrp: 250, outlet: 'Hypermarket' }
];
const fitMode = K.segmented($('#intro-fit'), () => update());
const on = id => $(id).checked;
function update() {
const all = fitMode.get() === 'all';
const fit = all ? data : data.filter(d => d.set === 'Entrenamiento');
const wMean = K.mean(fit.filter(d => d.weight !== null).map(d => d.weight));
const pMean = K.mean(fit.map(d => d.mrp)), pSd = K.sd(fit.map(d => d.mrp));
const cats = [...new Set(fit.map(d => d.outlet))];
const clean = on('#lab-clean'), impute = on('#lab-impute'), scale = on('#lab-scale'), ohe = on('#lab-ohe');
const c = K.chart($('#intro-chart'), { width: 640, height: 150, x: [0, 45], y: [0, 1], margin: { left: 14, right: 14, top: 26, bottom: 34 } });
c.axes({ xTicks: 5, yTicks: 0, grid: false, xLabel: 'Item_Weight (kg)' });
data.filter(d => d.weight !== null).forEach(d => { c.dot(d.weight, .45, 7, (d.set === 'Prueba' ? 's5' : 's1') + ' dot'); c.text(d.weight, .45, d.set === 'Prueba' ? 'prueba' : 'train', { 'text-anchor': 'middle', dy: 20 }); });
c.line(wMean, 0, wMean, 1, 's2 line');
c.text(wMean, 1, `media = ${K.fmt(wMean, 2)}`, { 'text-anchor': 'middle', dy: -8, class: 's2' });
$('#lab-parameters').textContent = `Precio: μ = ${K.fmt(pMean, 2)}, σ = ${K.fmt(pSd, 2)}. Categorías de tienda aprendidas: ${cats.join(', ')}.`;
const head = '<tr><th>Conjunto</th><th>Peso</th><th>Grasa</th><th>Precio</th>' + (ohe ? '<th>Supermarket</th><th>Grocery</th>' + (all ? '<th>Hypermarket</th>' : '') : '<th>Tienda</th>') + '</tr>';
const body = data.map(d => {
const fat = clean ? ({ LF: 'Low Fat', reg: 'Regular' }[d.fat] || d.fat) : d.fat;
const w = d.weight === null && impute ? wMean : d.weight;
const price = scale ? (d.mrp - pMean) / pSd : d.mrp;
const cols = all ? ['Supermarket', 'Grocery', 'Hypermarket'] : ['Supermarket', 'Grocery'];
const outlet = ohe ? cols.map(k => `<td>${+(d.outlet === k)}</td>`).join('') : `<td>${d.outlet}</td>`;
const hl = d.weight === null && impute ? ' class="hl"' : '';
return `<tr><td>${d.set}</td><td${hl}>${w === null ? 'NA' : K.fmt(w, 2)}</td><td>${fat}</td><td>${K.fmt(price, 2)}</td>${outlet}</tr>`;
}).join('');
$('#lab-table').innerHTML = `<table class="plab-table"><thead>${head}</thead><tbody>${body}</tbody></table>`;
const fb = $('#lab-feedback');
if (all) { fb.className = 'plab-note bad'; fb.textContent = `Fuga de datos: la fila de prueba movió la media de imputación de 15.00 a ${K.fmt(wMean, 2)}${scale ? ', cambió μ y σ del precio' : ''}${ohe ? ' y creó la columna Hypermarket, que el modelo nunca debió conocer' : ''}. La evaluación ya no es independiente.`; }
else { fb.className = 'plab-note good'; fb.textContent = `Parámetros aprendidos solo con entrenamiento y aplicados sin reajuste a la prueba.${ohe ? ' La tienda «Hypermarket» de prueba es desconocida: se codifica con ceros (handle_unknown="ignore").' : ''} La regla que unifica «LF» y «reg» no aprende nada de los datos, así que no genera fuga.`; }
}
root.querySelectorAll('input').forEach(i => i.addEventListener('change', update));
update();
})();
</script>
```
:::
::: {.content-visible when-format="pdf"}
En el simulador web se comparan las medias $15$ y $(10+20+40)/3\approx23.33$. Con la primera se imputa el peso faltante sin usar la fila de prueba.
:::
**Comprobación.** La media con prueba es $23.33$; la media de entrenamiento es $15$. El simulador muestra además que el precio de prueba también altera los parámetros del escalado si se incluye en `.fit()`.
**Reflexión.** ¿Por qué una transformación que usa reglas fijas para `LF` puede aplicarse a prueba, mientras que la media de imputación debe aprenderse en entrenamiento? ¿Cambiaría la conclusión si el conjunto de prueba tuviera cientos de filas?
## Implementación con `pandas` y `scikit-learn`
El ejemplo aplica reglas del ejercicio a peso y precio, luego imputa, escala y codifica categorías. Separamos filas antes de ajustar el `ColumnTransformer`. Usamos pocas filas para poder inspeccionar el resultado, no para estimar rendimiento predictivo.
```{python}
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, RobustScaler, StandardScaler
X_raw = pd.DataFrame({
'Item_Identifier': ['FDA15', 'DRC01', 'FDN15', 'FDX07', 'NCD19', 'FDP10', 'FDZ20'],
'Item_Weight': [9.30, -5.00, np.nan, 19.20, 200.00, 12.0, 10.5],
'Item_MRP': [249.80, 0.00, 1200000.0, np.nan, 53.86, 180.0, 190.0],
'Item_Fat_Content': ['Low Fat', 'low fat', 'LF', 'Regular', 'reg', 'LF', 'Unknown'],
})
# División ilustrativa explícita: las dos últimas filas simulan datos futuros.
X_train = X_raw.iloc[:5].copy()
X_test = X_raw.iloc[5:].copy()
display(X_raw)
```
Estas clases conservan el formato tabular que recibe `ColumnTransformer` y no modifican el original. Las reglas de rango son decisiones del ejemplo; en un proyecto real requieren revisión con especialistas del dominio.
```{python}
class RangeToMissing(BaseEstimator, TransformerMixin):
def __init__(self, lower, upper):
self.lower = lower
self.upper = upper
def fit(self, X, y=None):
return self
def transform(self, X):
result = X.copy()
column = result.columns[0]
values = pd.to_numeric(result[column], errors='coerce')
result[column] = values.where(values.between(self.lower, self.upper))
return result
def get_feature_names_out(self, input_features=None):
return np.asarray(input_features, dtype=object)
class FatContentStandardizer(BaseEstimator, TransformerMixin):
def fit(self, X, y=None):
return self
def transform(self, X):
result = X.copy()
column = result.columns[0]
normalized = result[column].astype('string').str.strip().str.lower()
result[column] = normalized.map({
'lf': 'Low Fat', 'low fat': 'Low Fat',
'reg': 'Regular', 'regular': 'Regular',
}).fillna('Unknown').astype(str)
return result
def get_feature_names_out(self, input_features=None):
return np.asarray(input_features, dtype=object)
weight_pipeline = Pipeline([
('range', RangeToMissing(1, 50)),
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler()),
])
price_pipeline = Pipeline([
('range', RangeToMissing(1, 1000)),
('imputer', SimpleImputer(strategy='median')),
('scaler', RobustScaler()),
])
fat_pipeline = Pipeline([
('normalize', FatContentStandardizer()),
('encode', OneHotEncoder(handle_unknown='ignore', sparse_output=False)),
])
preprocessor = ColumnTransformer([
('weight', weight_pipeline, ['Item_Weight']),
('price', price_pipeline, ['Item_MRP']),
('fat', fat_pipeline, ['Item_Fat_Content']),
])
train_matrix = preprocessor.fit_transform(X_train)
test_matrix = preprocessor.transform(X_test)
columns = preprocessor.get_feature_names_out()
display(pd.DataFrame(train_matrix, columns=columns, index=X_train['Item_Identifier']))
display(pd.DataFrame(test_matrix, columns=columns, index=X_test['Item_Identifier']))
```
El precio de $1{,}200{,}000$ se convierte en ausente por la regla **supuestamente válida en este ejercicio** y se imputa antes de escalar. `RobustScaler` usa mediana y rango intercuartílico; no corrige por sí mismo un valor erróneo.[^robust] La categoría `Unknown` de prueba se codifica según las categorías aprendidas en entrenamiento; `handle_unknown='ignore'` evita un error si aparece una categoría nueva, aunque requiere seguimiento.[^onehot]
::: {.callout-important title="Evaluación sin fuga de datos"}
Separa entrenamiento y prueba antes de aprender medias, medianas, escalas o categorías. En validación cruzada, coloca preprocesador y estimador en un mismo `Pipeline` para que cada pliegue ajuste solo con sus filas de entrenamiento.[^fuga]
:::
## Para recordar
- Define el significado y las reglas de calidad antes de modificar valores.
- Comprueba las transformaciones con un ejemplo pequeño y conserva trazabilidad.
- Ajusta parámetros con entrenamiento; aplica el resultado a prueba y a datos nuevos.
- Vigila cambios de esquema y distribución después del despliegue, y comprueba su efecto con métricas del caso de uso.
[^modelos]: La necesidad de codificar o imputar depende del estimador y de su implementación; esta unidad muestra un flujo tabular general, no una obligación para todo modelo.
[^onehot]: *One hot encoding* crea una columna indicadora por categoría. [Documentación de `OneHotEncoder`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.OneHotEncoder.html).
[^escalado]: `StandardScaler` calcula parámetros con el conjunto usado en `fit`; los modelos basados en árboles suelen ser menos sensibles a la escala. [Documentación de preprocesamiento de scikit-learn](https://scikit-learn.org/stable/modules/preprocessing.html).
[^fuga]: Fuga de datos: información que no estaría disponible al entrenar influye en alguna etapa del ajuste o de la selección del modelo. [Errores comunes y prácticas recomendadas de scikit-learn](https://scikit-learn.org/stable/common_pitfalls.html#data-leakage).
[^drift]: Un cambio en $P(X)$ describe deriva de las variables de entrada; una variación en $P(Y\mid X)$ describe cambio de la relación con la respuesta. La relevancia depende de la tarea y de la evidencia disponible.
[^robust]: [Documentación de `RobustScaler`](https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.RobustScaler.html).
[^desv]: **Desviación estándar poblacional:** raíz del promedio de las desviaciones al cuadrado respecto a la media, dividiendo entre $n$. Es la convención de `StandardScaler`; la versión muestral divide entre $n-1$.
[^composicion]: **Composición de funciones:** $(T_2\circ T_1)(X)=T_2(T_1(X))$; se aplica primero $T_1$ y su salida alimenta a $T_2$. El orden importa: imputar y luego escalar no da, en general, lo mismo que escalar y luego imputar.
[^parametro]: **Parámetro aprendido frente a regla fija:** una media o una lista de categorías son *estadísticos* calculados con datos; si cambian las filas, cambian ellos. Una regla como «`LF` significa `Low Fat`» no depende de las filas y puede aplicarse a cualquier conjunto sin riesgo de fuga.