Codificación de variables categóricas
1 Tipos de datos en machine learning
Antes de codificar, es crucial entender qué tipo de datos tienes. Los modelos de ML solo aceptan números, pero los datos reales vienen en muchas formas.
1.1 Datos numéricos
| Subtipo | Ejemplo | Propiedades |
|---|---|---|
| Discreto | Número de hijos, visitas web | Se cuentan, no se miden |
| Continuo | Peso, temperatura, ingresos | Se miden, no se cuentan |
Los numéricos se expresan en escala de ratio (20 kg es el doble que 10 kg, cero significa ausencia) o escala de intervalo (0°C no significa ausencia de temperatura).
1.2 Datos categóricos
| Subtipo | Ejemplo | ¿Tiene orden? |
|---|---|---|
| Nominal | Color, ciudad, género | No |
| Ordinal | Talla (S\<M\<L), satisfacción (1-5) | Sí |
Nota
Regla clave: Los ordinales conservan su orden al codificar. Los nominales no. Esta distinción determina qué método de codificación usar.
2 El problema de las variables categóricas
Los modelos de machine learning trabajan con números. Pero muchas variables reales son categóricas: color, ciudad, género, tipo de producto. La codificación convierte estas categorías en representaciones numéricas.
La elección del método puede cambiar drásticamente el rendimiento. No hay una solución única: depende del tipo de categoría, del modelo y de la cardinalidad (número de categorías únicas).
| Variable | Categorías | Cardinalidad |
|---|---|---|
| Color | rojo, azul, verde | 3 (baja) |
| Ciudad | Madrid, Barcelona, … | 50 (media) |
| ID de producto | PROD-001, … | 100.000 (alta) |
3 One-Hot Encoding
El one-hot encoding crea una columna binaria (0 o 1) por cada categoría. Es el método más simple y más usado para variables nominales.
3.1 Con scikit-learn
from sklearn.preprocessing import OneHotEncoder
import pandas as pd
data = {
'Ciudad': ['Nueva York', 'Londres', 'París', 'Nueva York', 'París'],
'Color': ['Rojo', 'Azul', 'Verde', 'Verde', 'Rojo'],
'Tamaño': ['Grande', 'Pequeño', 'Mediano', 'Grande', 'Mediano']
}
df = pd.DataFrame(data)
# OneHotEncoder
encoder = OneHotEncoder(sparse_output=False, drop='first')
encoded = encoder.fit_transform(df[['Ciudad', 'Color']])
encoded_df = pd.DataFrame(
encoded,
columns=encoder.get_feature_names_out(['Ciudad', 'Color'])
)
print(encoded_df)
# Ciudad_Londres Ciudad_Nueva York Color_Rojo Color_Verde
# 0 0.0 1.0 1.0 0.0
# 1 1.0 0.0 0.0 0.0
# 2 0.0 0.0 0.0 1.0
# 3 0.0 1.0 0.0 1.0
# 4 0.0 0.0 1.0 0.0
3.2 Con pandas get_dummies
3.3 handle_unknown
Si aparecen categorías nuevas en test que no existían en train:
encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
encoder.fit(df[['Ciudad']])
# Categoría desconocida → todo ceros
print(encoder.transform([['Ciudad_Nueva']]))
# [[0. 0. 0.]]
Tip
Regla general: Usa drop='first' en regresión lineal para evitar la trampa ficticia (ver sección siguiente). Para árboles de decisión, drop=None funciona bien.
4 Label Encoding
Asigna un número entero a cada categoría. Solo una columna, más compacto.
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
df['Color_label'] = le.fit_transform(df['Color'])
print(df[['Color', 'Color_label']])
# Color Color_label
# 0 Rojo 1
# 1 Azul 0
# 2 Verde 2
# 3 Verde 2
# 4 Rojo 1
# Inversión
print(le.inverse_transform([0, 1, 2]))
# ['Azul' 'Rojo' 'Verde']
Advertencia
No uses label encoding para variables nominales. Si asignas Madrid=0, Barcelona=1, Sevilla=2, el modelo interpreta Madrid \< Barcelona \< Sevilla. Solo es válido para ordinales con orden natural.
5 La trampa ficticia
Cuando una variable categórica tiene \(k\) categorías, crear \(k\) columnas dummy + intercepto provoca multicolinealidad perfecta: las columnas se pueden predecir entre sí (suman 1).
5.1 Ejemplo: regresión de altura
Si regresamos altura en función de padres y género, necesitamos una sola columna dummy:
- \(D=0\) → hombre: altura = \(A_1 + B \cdot x\)
- \(D=1\) → mujer: altura = \(A_2 + B \cdot x\)
Si creáramos dos columnas (\(D_{\text{hombre}}\), \(D_{\text{mujer}}\)) con intercepto, las tres variables serían linealmente dependientes.
Importante
Regla: Para \(k\) categorías, usa \(k-1\) columnas dummy si tienes intercepto. Esto equivale a drop='first' en OneHotEncoder o drop_first=True en pd.get_dummies.
5.2 Evitar la trampa
# INCORRECTO: k columnas + intercepto → trampa ficticia
pd.get_dummies(df['Color']) # 3 columnas
# CORRECTO: k-1 columnas + intercepto
pd.get_dummies(df['Color'], drop_first=True) # 2 columnas
La categoría eliminada se convierte en la categoría de referencia: todos los coeficientes se interpretan respecto a ella.
6 Codificación por contraste
Más allá de la codificación dummy, existen esquemas de contraste que permiten interpretaciones específicas. Son útiles en ANOVA y análisis estadístico.
6.1 Codificación de efectos simples
Cada coeficiente representa la diferencia entre la media de esa categoría y la media del nivel de referencia.
from sklearn.preprocessing import OrdinalEncoder
import pandas as pd
import numpy as np
# Implementación manual
categorias = ['Soltero', 'Casado', 'Divorciado']
n = len(categorias)
contrast_matrix = np.zeros((n, n - 1))
for i in range(1, n):
contrast_matrix[i, i - 1] = 1 # Nivel i vs referencia (nivel 0)
df_contrast = pd.DataFrame(contrast_matrix, index=categorias,
columns=[f'Contraste_{i}' for i in range(1, n)])
print(df_contrast)
# Contraste_1 Contraste_2
# Soltero 0.0 0.0
# Casado 1.0 0.0
# Divorciado 0.0 1.0
6.2 Codificación de Helmert
Compara cada nivel con la media de todos los niveles anteriores. Útil para datos ordinales:
from formulaic import contrast coded # Alternativa: statsmodels
# O implementación manual:
categorias = ['Q1', 'Q2', 'Q3', 'Q4']
# Helmert: cada nivel vs media de anteriores
helmert = np.array([
[-3, -1, 1, 3], # codificación clásica
[ 1, -1, -1, 1],
[-1, 3, -3, 1]
]).T / np.sqrt([6, 2, 6]) # Normalizar
df_helmert = pd.DataFrame(helmert, index=categorias,
columns=['H1', 'H2', 'H3'])
print(df_helmert.round(3))
6.3 Cuándo usar cada contraste
| Contraste | Intercepto representa | Cuándo usar |
|---|---|---|
| Dummy (k-1) | Media del nivel de referencia | Regresión lineal general |
| Efectos simples | Media global | Comparar categorías vs referencia |
| Helmert | Media de todas las medias | Datos ordinales |
| Diferencias hacia atrás | Media del nivel anterior | Datos ordenados temporalmente |
7 Target Encoding
Reemplaza cada categoría con la media de la variable objetivo para esa categoría. Ideal para alta cardinalidad.
# Para cada categoría, promedio de y
target_mean = df.groupby('ciudad')['precio'].mean()
df['ciudad_encoded'] = df['ciudad'].map(target_mean)
Importante
Data leakage: El target encoding debe calcularse solo con datos de entrenamiento y aplicarse a validación/test.
8 Binning: discretizar datos continuos
A veces los datos continuos sebenefician de ser convertidos en categorías. KBinsDiscretizer divide variables numéricos en intervalos.
8.1 Estrategias de binning
from sklearn.preprocessing import KBinsDiscretizer
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'edad': np.random.normal(35, 10, 100).clip(18, 80),
'ingresos': np.random.lognormal(10, 1, 100)
})
# Uniform: anchos iguales
kbins_uniform = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='uniform')
df['edad_uniform'] = kbins_uniform.fit_transform(df[['edad']])
# Quantile: cada bin tiene la misma cantidad de datos
kbins_quantile = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='quantile')
df['edad_quantile'] = kbins_quantile.fit_transform(df[['edad']])
# K-means: bins basados en clustering
kbins_kmeans = KBinsDiscretizer(n_bins=5, encode='ordinal', strategy='kmeans')
df['edad_kmeans'] = kbins_kmeans.fit_transform(df[['edad']])
print("Binning por percentiles:")
print(df['edad_quantile'].value_counts().sort_index())
| Estrategia | Cómo divide los bins | Ideal para |
|---|---|---|
| Uniform | Anchos iguales | Distribución uniforme |
| Quantile | Misma cantidad de datos | Distribuciones sesgadas |
| K-means | Centroides de clustering | Grupos naturales |
9 Frequency Encoding
Reemplaza cada categoría con su frecuencia:
10 Resumen
| Método | Columnas | Cardinalidad | Orden | Ideal para |
|---|---|---|---|---|
| One-Hot | k-1 | Baja (\< 15) | No | Cualquier modelo |
| Label | 1 | Cualquiera | Sí | Variables ordinales |
| Target | 1 | Alta (> 15) | No | Tree-based, regresión |
| Frequency | 1 | Cualquiera | No | Cuando la frecuencia importa |
| Binning | 1 | N/A | Sí | Continuos → categorías |
| Contraste | k-1 | Cualquiera | Depende | ANOVA, análisis estadístico |
Tip
En la práctica: Empieza por one-hot si pocas categorías, target encoding si muchas. Para regresión lineal, siempre drop='first' para evitar la trampa ficticia.