Saltar a contenido

Escalado y normalización

1 ¿Por qué escalar las características?

Muchos algoritmos de machine learning son sensibles a la escala de las variables. Si una variable tiene un rango de 0 a 1.000.000 y otra de 0 a 1, la primera dominará la función de pérdida simplemente por su magnitud, no por su importancia real.

1.1 Algoritmos que necesitan escalado

Algoritmo ¿Necesita escalado? Razón
KNN Sí Basado en distancias euclidianas
SVM Sí El hiperplano depende de la magnitud de las features
Redes neuronales Sí Los gradientes dependen de la escala de entrada
Regresión lineal/logística Recomendado Acelera convergencia, mejora interpretabilidad
Árboles de decisión No Solo usa umbrales, la escala no afecta
Random Forest No Misma razón que los árboles
XGBoost No Basado en árboles

Nota

Regla simple: Si el algoritmo usa distancias o gradientes, escala. Si usa solo umbrales (árboles), no es necesario.

2 Normalización (Min-Max)

La normalización comprime todos los valores al rango \([0, 1]\):

\[ x_{\text{scaled}} = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \]
from sklearn.preprocessing import MinMaxScaler
import numpy as np

X = np.array([[100, 0.5], [200, 1.0], [300, 1.5], [400, 2.0]])

scaler = MinMaxScaler()
X_normalized = scaler.fit_transform(X)

print("Original:\n", X)
print("Normalizado:\n", X_normalized)
print("Rango por columna:", X_normalized.min(axis=0), "-", X_normalized.max(axis=0))

2.1 Cuándo usar Min-Max

  • Cuando conoces los límites exactos de tus datos.
  • Cuando necesitas valores en un rango específico (por ejemplo, imágenes en [0, 1]).
  • Cuando la distribución no es gaussiana.

Advertencia

Sensible a outliers. Si tienes un valor atípico extremo, comprime todos los demás valores a un rango muy pequeño. Ejemplo: si la mayoría de edades están entre 20-60 pero hay una persona de 120 años, todos los valores se comprimen en [0, 0.45].

3 Estandarización (Z-score)

La estandarización transforma los datos para que tengan media 0 y desviación estándar 1:

\[ x_{\text{scaled}} = \frac{x - \mu}{\sigma} \]

donde \(\mu\) es la media y \(\sigma\) la desviación estándar.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_standardized = scaler.fit_transform(X)

print("Original - Media:", X.mean(axis=0), "Std:", X.std(axis=0))
print("Estandarizado - Media:", X_standardized.mean(axis=0).round(10), "Std:", X_standardized.std(axis=0).round(4))

3.1 Cuándo usar Z-score

  • Cuando la distribución es aproximadamente gaussiana.
  • Cuando tienes outliers y quieres minimizar su impacto.
  • Para la mayoría de algoritmos de ML, es la opción por defecto recomendada.

Tip

¿Por qué es más robusta a outliers? Porque usa la media y desviación estándar, que se distribuyen más uniformemente que los percentiles (min/max). Un outlier afecta la media, pero no comprime todos los demás valores a un rango tiny como Min-Max.

4 Estandarización robusta

La estandarización robusta usa la mediana y el rango intercuartílico (IQR) en lugar de media y desviación estándar, lo que la hace completamente inmune a outliers:

\[ x_{\text{scaled}} = \frac{x - \text{mediana}}{Q_3 - Q_1} \]
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()
X_robust = scaler.fit_transform(X)

print("RobustScaler:\n", X_robust)

Cuándo usar: Cuando tus datos tienen muchos outliers y no puedes (o no quieres) eliminarlos antes de escalar.

5 Máxima magnitud (MaxAbs)

Escala cada feature por su máximo valor absoluto, manteniendo el rango en \([-1, 1]\):

\[ x_{\text{scaled}} = \frac{x}{|x_{\max}|} \]
from sklearn.preprocessing import MaxAbsScaler

scaler = MaxAbsScaler()
X_maxabs = scaler.fit_transform(X)

print("MaxAbsScaler:\n", X_maxabs)

Ventaja: Preserva la dispersión de datos dispersos (sparse data). Ideal para TF-IDF y datos de texto.

6 Comparación visual

import matplotlib.pyplot as plt
import numpy as np
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler

# Datos con outlier
np.random.seed(42)
X_normal = np.random.randn(100, 1) * 10 + 50
X_outlier = np.array([[200]])  # outlier
X_data = np.vstack([X_normal, X_outlier])

scalers = {
    'Original': None,
    'Min-Max': MinMaxScaler(),
    'Standard': StandardScaler(),
    'Robust': RobustScaler()
}

fig, axes = plt.subplots(1, 4, figsize=(16, 4))
for ax, (name, scaler) in zip(axes, scalers.items()):
    if scaler is None:
        X_plot = X_data
    else:
        X_plot = scaler.fit_transform(X_data)
    ax.hist(X_plot, bins=30, edgecolor='black', alpha=0.7)
    ax.set_title(name)
    ax.set_xlabel('Valor')
    ax.set_ylabel('Frecuencia')
plt.tight_layout()

7 Escalado en pipelines

Importante

Regla de oro: fit_transform() solo en entrenamiento, transform() en validación/test. Si calculas la media y desviación estándar con todos los datos, se filtra información del test al entrenamiento (data leakage).

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

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # fit + transform
X_test_scaled = scaler.transform(X_test)        # solo transform

7.1 Integrado con Pipeline

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC

pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('svm', SVC(kernel='rbf'))
])

# Se aplica el escalado correctamente en cross-validation
from sklearn.model_selection import cross_val_score
scores = cross_val_score(pipe, X, y, cv=5)

8 Normalización L1/L2 (por fila)

Nota

No confundir con estandarización. La normalización L1/L2 se aplica por fila (a cada vector de features individualmente). La estandarización (Z-score) se aplica por columna (a cada feature en todo el dataset).

La normalización L2 transforma cada fila para que tenga norma unitaria (magnitud = 1). Es especialmente útil para calcular similitud del coseno en procesamiento de texto y sistemas de recomendación.

8.1 Similitud del coseno

\[ \cos(\theta) = \frac{A \cdot B}{\|A\| \cdot \|B\|} \]

Si los vectores ya están normalizados con L2 (\(\|A\| = \|B\| = 1\)), la fórmula se simplifica a solo el producto punto.

from sklearn.preprocessing import Normalizer, normalize
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
import numpy as np

# Datos de ejemplo (cada fila = un documento)
df = pd.DataFrame({
    'palabra_1': [1, 2, 3],
    'palabra_2': [4, 5, 6],
    'palabra_3': [7, 8, 9]
})

# Normalización L2 (por fila)
normalizer = Normalizer(norm='l2')
df_norm = pd.DataFrame(normalizer.fit_transform(df), columns=df.columns)
print("Normalizado L2:")
print(df_norm.round(4))

# Similitud del coseno
sim = cosine_similarity(df_norm)
print(f"\nSimilitud coseno doc 0 vs doc 1: {sim[0, 1]:.4f}")

8.2 Normas disponibles

Norma Fórmula Cuándo usar
L1 \(\|x\|_1 = \sum \|x_i\|\) Preservar linealidad, sparse features
L2 \(\|x\|_2 = \sqrt{\sum x_i^2}\) Similitud del coseno (texto, recomendación)
Max \(\max(\|x_i\|)\) Preservar el valor absoluto máximo
# L1: suma de valores absolutos = 1
df_l1 = pd.DataFrame(normalize(df, norm='l1'), columns=df.columns)

# L2: raíz cuadrada de suma de cuadrados = 1
df_l2 = pd.DataFrame(normalize(df, norm='l2'), columns=df.columns)

# Max: valor absoluto máximo = 1
df_max = pd.DataFrame(normalize(df, norm='max'), columns=df.columns)

9 Power Transformer: hacer los datos más gaussianos

Muchos modelos asumen distribución gaussiana. El PowerTransformer aplica transformaciones de potencia para hacer que los datos sesgados se parezcan más a una normal.

9.1 Box-Cox vs. Yeo-Johnson

Método Requisito Ventaja
Box-Cox Datos estrictamente positivos (\(x > 0\)) Más interpretable
Yeo-Johnson Acepta ceros y negativos Más flexible
from sklearn.preprocessing import PowerTransformer
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Datos con distribución sesgada (exponencial)
np.random.seed(42)
data = pd.DataFrame({
    'ingresos': np.random.exponential(scale=50000, size=1000),
    'deuda': np.random.chisquare(df=2, size=1000) * 10000
})

print("Sesgo original:")
print(data.skew().round(2))
# ingresos    1.98
# deuda       2.03

# Aplicar Yeo-Johnson (acepta negativos)
pt = PowerTransformer(method='yeo-johnson')
data_transformed = pd.DataFrame(
    pt.fit_transform(data), columns=data.columns
)

print("\nSesgo tras transformación:")
print(data_transformed.skew().round(2))
# ingresos   -0.02
# deuda      -0.01

Tip

¿Cuándo usar PowerTransformer? Cuando tus features tienen distribución sesgada (colas pesadas) y vas a usar modelos sensibles a la distribución: regresión lineal, regresión logística, SVM lineal. Para árboles de decisión no es necesario.

10 Resumen

Método Fórmula Rango Robusto a outliers Ideal para
Min-Max \((x - \min)/(\max - \min)\) [0, 1] No Datos acotados, imágenes
Z-score \((x - \mu)/\sigma\) ~[-3, 3] Parcialmente La mayoría de algoritmos
Robust \((x - \text{med})/IQR\) Variable Sí Datos con outliers
MaxAbs $x / x_{\max} $ [-1, 1]
L2 (Normalizer) \(x / \|x\|_2\) Unitario No Similitud del coseno
Power Transformer Yeo-Johnson / Box-Cox ~gaussiana Parcialmente Datos sesgados

En la práctica: Usa StandardScaler como punto de partida. Si tienes muchos outliers, prueba RobustScaler. Si trabajas con datos dispersos o texto, usa Normalizer. Si tus features están sesgadas, usa PowerTransformer antes del escalado.