Al final de la lección anterior nos quedó una matriz totalmente numérica pero con escalas dispares: columnas one-hot que valen 0 o 1 conviviendo con gastos de hasta 15.400 €. Para muchos algoritmos, esa disparidad es un problema serio: una columna con números grandes domina los cálculos y las demás se vuelven invisibles, no porque importen menos, sino por puro accidente de unidades. Normalizar y estandarizar significa poner todas las variables en escalas comparables. En esta lección entenderás por qué la escala importa (y para qué algoritmos), dominarás los tres escaladores fundamentales de scikit-learn —MinMaxScaler, StandardScaler y RobustScaler—, verás su efecto antes y después, y aprenderás la regla de oro que evita fugas: ajustar el escalador solo con datos de entrenamiento.

Contenido

  1. Por qué la escala importa
  2. Qué algoritmos lo necesitan y cuáles no
  3. Min-Max scaling: llevar todo a [0, 1]
  4. Estandarización z-score: StandardScaler
  5. RobustScaler: escalar con outliers en casa
  6. Comparación visual antes y después
  7. Qué escalador usar: tabla de decisión
  8. Fit en train, transform en test

Por qué la escala importa

Tomemos dos clientes de MercaFresh descritos por dos variables: gasto total en euros y antigüedad en años.

  • Cliente A: 1.200 € de gasto, 1 año de antigüedad.
  • Cliente B: 1.150 € de gasto, 9 años de antigüedad.

Para un algoritmo basado en distancias (como el K-NN que veremos en 04-05), la "diferencia" entre ambos es:

import numpy as np

A = np.array([1200, 1])
B = np.array([1150, 9])
print(np.sqrt(((A - B) ** 2).sum()))   # 50.64

La distancia sale 50,64... de los cuales 50 vienen del gasto y solo 0,64 de la antigüedad. Los 8 años de diferencia —enormes en términos de fidelidad del cliente— quedan aplastados por 50 € de diferencia de gasto, que es calderilla. La antigüedad podría eliminarse del dataset y el resultado apenas cambiaría: la unidad de medida, no la importancia real, está decidiendo qué variable manda.

El mismo problema aparece, con otra mecánica, en los algoritmos que aprenden por gradiente (regresión logística 04-02, SVM 04-04, redes neuronales 04-07): cuando las variables tienen escalas muy distintas, la superficie de error se vuelve un valle alargado y estrecho, y el proceso de optimización zigzaguea lentamente en lugar de bajar recto. Escalar redondea el valle y acelera —y estabiliza— el entrenamiento. Los detalles de cada algoritmo pertenecen al módulo 4; aquí basta el mensaje: distancias y gradientes exigen escalas comparables.

Qué algoritmos lo necesitan y cuáles no

Familia ¿Sensible a la escala? Por qué
K-NN (04-05), K-means (05-01) Sí, crítico Calculan distancias entre filas
SVM (04-04) Sí, crítico Distancias al hiperplano + optimización
Regresión lineal/logística con gradiente (04-01, 04-02) Convergencia del gradiente; comparabilidad de coeficientes
Redes neuronales (04-07) Gradientes estables
PCA (05-03) Sí, crítico La varianza grande domina las componentes
Árboles de decisión (04-03), Random Forest, Gradient Boosting (07-02/03) No Solo hacen cortes tipo "¿gasto > 500?"; el corte es el mismo en cualquier escala
Naive Bayes (04-06) En general no Trabaja con probabilidades por variable

Esta tabla te acompañará todo el curso: antes de entrenar cualquier modelo, pregúntate en qué fila cae. Y un recordatorio de la lección 03-02: el KNNImputer también mide distancias, así que también agradece datos escalados.

Min-Max scaling: llevar todo a [0, 1]

La normalización Min-Max transforma cada variable para que su mínimo sea 0 y su máximo 1:

x_escalado = (x - min) / (max - min)
import pandas as pd
from sklearn.preprocessing import MinMaxScaler

clientes = pd.DataFrame({
    "gasto_total": [1250.5, 890.0, 2100.75, 310.2, 15400.0, 670.4, 980.1, 45.9],
    "antiguedad_anios": [3, 1, 6, 2, 9, 4, 1, 5],
    "num_pedidos": [18, 12, 25, 4, 210, 9, 14, 6],
})

mms = MinMaxScaler()
escalado = pd.DataFrame(mms.fit_transform(clientes), columns=clientes.columns)
print(escalado.round(3))
print(mms.data_min_, mms.data_max_)   # lo aprendido en fit

Interpretación directa: 0 es "el cliente que menos", 1 "el que más", 0,5 "a mitad de camino". Ventajas e inconvenientes:

  • A favor: rango acotado y predecible ([0, 1]), interpretación intuitiva, encaja donde se exige un rango fijo (por ejemplo, algunas redes neuronales o imágenes con píxeles 0-255).
  • En contra: extremadamente sensible a outliers. Mira la columna gasto_total: el cliente de 15.400 € se lleva el 1, y todos los demás quedan comprimidos entre 0,00 y 0,13. El outlier ha secuestrado la escala y el resto de clientes son casi indistinguibles entre sí.
  • Además, un dato futuro mayor que el máximo de entrenamiento saldrá > 1: el rango [0, 1] solo está garantizado para los datos con los que se hizo fit.

Estandarización z-score: StandardScaler

La estandarización transforma cada variable restando su media y dividiendo por su desviación estándar:

z = (x - media) / desviacion

¿Te suena? Es exactamente el z-score de la lección 02-02: cada valor pasa a expresarse en "número de desviaciones estándar respecto a la media". Un cliente con z = +2 en gasto está dos desviaciones por encima del gasto medio; uno con z = 0 es exactamente promedio. Lo que allí usábamos como detector de rarezas, aquí se convierte en un idioma común para todas las variables: tras estandarizar, todas tienen media 0 y desviación 1, y "estar 2 desviaciones por encima" significa lo mismo en euros que en años.

from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
estandarizado = pd.DataFrame(ss.fit_transform(clientes), columns=clientes.columns)
print(estandarizado.round(2))
print(estandarizado.mean().round(2))   # ~0 en todas las columnas
print(estandarizado.std().round(2))    # ~1 en todas las columnas
print(ss.mean_, ss.scale_)             # media y desviación aprendidas en fit

Propiedades:

  • No acota el rango: los valores típicos caen entre -3 y +3 (la regla 68-95-99.7 de 02-02), pero un outlier puede salir a z = 6 sin problema.
  • No cambia la forma de la distribución: si el gasto era asimétrico, el gasto estandarizado sigue siendo asimétrico. Escalar no es transformar (eso fue 03-03); son pasos complementarios: primero log1p corrige la forma, después StandardScaler ajusta la escala.
  • Es el escalador por defecto en la práctica: robusto "suficiente" para la mayoría de casos y el que asumen muchos algoritmos.
  • Sigue usando media y desviación, así que los outliers lo perturban (menos que a Min-Max, pero lo hacen): con el cliente de 15.400 € dentro, la media y la desviación del gasto están infladas.

RobustScaler: escalar con outliers en casa

Cuando los outliers son parte legítima del dataset —y en MercaFresh lo son: los clientes-restaurante existen y no queremos eliminarlos—, conviene escalar con estadísticos que los ignoren. RobustScaler usa la mediana y el IQR (el rango intercuartílico de 02-01) en lugar de la media y la desviación:

x_escalado = (x - mediana) / IQR
from sklearn.preprocessing import RobustScaler

rs = RobustScaler()
robusto = pd.DataFrame(rs.fit_transform(clientes), columns=clientes.columns)
print(robusto.round(2))
print(rs.center_, rs.scale_)   # mediana e IQR aprendidos en fit

Como mediana e IQR apenas se mueven aunque haya valores extremos (lo viste en 02-01: son estadísticos robustos), el grueso de los clientes queda bien repartido y el outlier simplemente sale con un valor grande, sin comprimir a los demás. Es la elección natural cuando la limpieza de 03-01 concluyó "estos outliers son reales y se quedan".

Comparación visual antes y después

Nada convence más que verlo. Comparemos los tres escaladores sobre el gasto, con su outlier incluido:

import matplotlib.pyplot as plt

gasto = clientes[["gasto_total"]]
versiones = {
    "Original (EUR)": gasto.values.ravel(),
    "MinMaxScaler": MinMaxScaler().fit_transform(gasto).ravel(),
    "StandardScaler": StandardScaler().fit_transform(gasto).ravel(),
    "RobustScaler": RobustScaler().fit_transform(gasto).ravel(),
}

fig, axes = plt.subplots(1, 4, figsize=(14, 3))
for ax, (titulo, valores) in zip(axes, versiones.items()):
    ax.boxplot(valores, vert=True)
    ax.set_title(titulo, fontsize=10)
plt.tight_layout()
plt.show()

Lo que muestran los cuatro boxplots (los de 02-01, trabajando de nuevo):

  • Original: caja diminuta abajo, outlier lejanísimo arriba. Escala en miles.
  • MinMaxScaler: misma silueta exacta, pero comprimida en [0, 1]: la caja entera ocupa un 13 % del rango. El outlier manda.
  • StandardScaler: misma silueta, centrada en 0; el outlier sale a z ≈ 2,6 y el resto queda apiñado en una franja estrecha por debajo de la media.
  • RobustScaler: la caja central queda bien desplegada alrededor de 0 (de -0,5 a 0,5 aproximadamente, porque el IQR pasa a valer 1) y el outlier se va lejos sin molestar a nadie.

Conclusión visual: escalar nunca cambia la forma de la distribución, solo su regla de medir; lo que distingue a los escaladores es qué tramo de los datos usan como referencia para esa regla.

Qué escalador usar: tabla de decisión

Situación Escalador recomendado Motivo
Caso general, sin outliers graves StandardScaler Estándar de facto; lo asumen muchos algoritmos
Outliers legítimos presentes RobustScaler Mediana e IQR no se dejan arrastrar
Se necesita rango acotado [0, 1] MinMaxScaler Único que lo garantiza (en train)
Variable ya binaria (one-hot) Ninguno Ya está en {0, 1}; escalarla solo resta interpretabilidad
Modelo de árboles (04-03, 07-02/03) Ninguno necesario Los cortes son invariantes a la escala
Distribución muy asimétrica Transformar primero (03-03), escalar después Escalar no corrige la forma

Y la conexión con el flujo que venimos montando: el escalador es un paso más del Pipeline de la rama numérica del ColumnTransformer — imputación → transformación → escalado, en ese orden, mientras las columnas one-hot pasan de largo.

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer

rama_numerica = Pipeline(steps=[
    ("imputar", SimpleImputer(strategy="median")),
    ("escalar", RobustScaler()),
])

Fit en train, transform en test

La regla que cierra la lección es la misma que apareció en 03-02 con la imputación, ahora aplicada al escalado — y con los escaladores es donde más se peca. Todo escalador aprende parámetros en fit: mínimos y máximos, medias y desviaciones, medianas e IQRs. Si esos parámetros se calculan con el dataset completo antes de dividir en entrenamiento y prueba (la división que formalizaremos en 06-01), los datos de prueba habrán dejado su huella en la media o el máximo: fuga de información. La evaluación saldrá un poco mejor de lo que merece, y ni un error ni un aviso te alertará.

El protocolo correcto:

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1) Dividir PRIMERO (los detalles, en 06-01)
X_train, X_test = train_test_split(clientes, test_size=0.25, random_state=42)

# 2) Ajustar el escalador SOLO con entrenamiento
scaler = StandardScaler()
scaler.fit(X_train)

# 3) Transformar ambos conjuntos con LO APRENDIDO EN TRAIN
X_train_esc = scaler.transform(X_train)
X_test_esc = scaler.transform(X_test)

# Atajo equivalente para train: scaler.fit_transform(X_train)

Consecuencia que sorprende la primera vez: el conjunto de prueba escalado no tendrá media exactamente 0 ni desviación exactamente 1 — se ha medido con la regla de train, no con la suya propia. Y así debe ser: en producción, cada cliente nuevo llegará de uno en uno y se escalará con los parámetros congelados del entrenamiento. fit_transform en train, transform a secas en test y en producción: grábatelo como reflejo. Si además todo vive dentro de un Pipeline, el reflejo viene de serie: el pipeline solo hace fit cuando tú se lo pides sobre train.

Errores Comunes y Consejos

  • Hacer fit_transform sobre el conjunto de prueba. Es el error de fuga más común en principiantes: en test (y en producción) solo transform. Si tu test escalado tiene media 0 perfecta, sospecha.
  • Escalar con MinMaxScaler un dataset con outliers. El resto de valores queda comprimido en un rincón del [0, 1]. Con outliers legítimos, RobustScaler.
  • Esperar que escalar arregle la asimetría. Escalar es una operación lineal: la forma del histograma no cambia. Para la forma, las transformaciones de 03-03.
  • Escalar las columnas one-hot. Un 0/1 estandarizado se convierte en valores como -0,58/1,72: se pierde la legibilidad sin ganar nada relevante. Deja las binarias fuera de la rama de escalado en el ColumnTransformer.
  • Escalar la variable objetivo de un problema de clasificación. El churn (0/1) es la etiqueta, no una característica: no se escala.
  • Olvidar guardar el escalador. Los parámetros aprendidos (mean_, scale_...) son parte del modelo: en producción necesitarás el mismo objeto para escalar cada cliente nuevo (lo retomaremos en el módulo 8).
  • Consejo: imprime describe() del resultado escalado. Medias ~0 y desviaciones ~1 (o rangos [0, 1]) en train confirman que todo funcionó; valores absurdos delatan columnas que no debían escalarse.

Ejercicios

Ejercicio 1

Sin ejecutar código: los tiempos de entrega de MercaFresh son [38, 42, 44, 47, 52, 55, 61, 190] minutos (el 190 fue un pedido con la furgoneta averiada). ¿Qué escalador elegirías y por qué? ¿Qué le pasaría al resto de valores con MinMaxScaler?

Ejercicio 2

Estandariza a mano (sin scikit-learn, solo NumPy) la variable antiguedad = [3, 1, 6, 2, 9, 4, 1, 5] y comprueba que el resultado tiene media 0 y desviación 1. Después verifica que StandardScaler da el mismo resultado.

Ejercicio 3

Este código tiene un error de fuga de información. Encuéntralo y corrígelo:

scaler = StandardScaler()
datos_esc = scaler.fit_transform(datos)
X_train, X_test = train_test_split(datos_esc, test_size=0.3)

Soluciones

Ejercicio 1

RobustScaler. El 190 es un outlier con causa conocida (avería) pero presente en los datos; media y desviación (StandardScaler) quedarían infladas por él. Con MinMaxScaler sería peor: 190 se llevaría el 1 y los siete valores normales quedarían comprimidos aproximadamente entre 0,00 y 0,15, casi indistinguibles entre sí. RobustScaler, al usar mediana (49,5) e IQR, despliega bien los valores normales y deja que el 190 salga lejos sin distorsionar a los demás. (Alternativa legítima de 03-01: tratar el 190 como error puntual y corregirlo antes de escalar.)

Ejercicio 2

import numpy as np
from sklearn.preprocessing import StandardScaler

x = np.array([3, 1, 6, 2, 9, 4, 1, 5], dtype=float)

z = (x - x.mean()) / x.std()        # std() de NumPy divide por n, como StandardScaler
print(z.round(3))
print(z.mean().round(10), z.std().round(10))   # 0.0 y 1.0

ss = StandardScaler()
z_sk = ss.fit_transform(x.reshape(-1, 1)).ravel()
print(np.allclose(z, z_sk))          # True

Detalle fino: StandardScaler usa la desviación poblacional (dividir por n), igual que np.std() por defecto; la muestral de pandas (Series.std(), que divide por n-1) daría valores ligeramente distintos.

Ejercicio 3

El fit_transform se ejecuta sobre todos los datos antes de dividir: la media y la desviación aprendidas contienen información de las filas que luego serán de test. Corrección:

X_train, X_test = train_test_split(datos, test_size=0.3, random_state=42)

scaler = StandardScaler()
X_train_esc = scaler.fit_transform(X_train)   # aprende SOLO de train
X_test_esc = scaler.transform(X_test)         # aplica lo aprendido

Primero dividir, después ajustar solo con train, y transformar test con los parámetros de train.

Conclusión

Ya dominas la última pieza puramente mecánica del preprocesamiento: por qué los algoritmos de distancias y gradientes necesitan escalas comparables, cómo MinMaxScaler acota a [0, 1] (pero se rinde ante outliers), cómo StandardScaler convierte cada variable a los z-scores que conociste en 02-02, cómo RobustScaler escala con mediana e IQR cuando los outliers son legítimos, y la regla innegociable de ajustar siempre el escalador solo con datos de entrenamiento. El dataset de churn de MercaFresh está limpio, completo, transformado, codificado y escalado.

¿Está entonces terminado? Técnicamente sí; competitivamente no. Hasta ahora hemos reparado y adaptado las columnas que ya existían. El salto de calidad viene de crear columnas nuevas que condensen conocimiento de negocio: cuánto hace que el cliente no compra, cuánto vale, si su actividad crece o se apaga. Esa es la ingeniería de características, la lección que cierra el módulo — y donde el dataset de MercaFresh alcanzará su forma definitiva.

Curso de Machine Learning

Módulo 1: Introducción al Machine Learning

Módulo 2: Fundamentos de Estadística y Probabilidad

Módulo 3: Preprocesamiento de Datos

Módulo 4: Algoritmos de Machine Learning Supervisado

Módulo 5: Algoritmos de Machine Learning No Supervisado

Módulo 6: Evaluación y Validación de Modelos

Módulo 7: Técnicas Avanzadas y Optimización

Módulo 8: Implementación y Despliegue de Modelos

Módulo 9: Proyectos Prácticos

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados