Saltar a contenido

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

encoded_df = pd.get_dummies(df, columns=['Ciudad', 'Color'], drop_first=True)
print(encoded_df)

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:

\[ \text{altura} = A_1 + (A_2 - A_1) \cdot D_{\text{mujer}} + B \cdot \text{altura\_padres} \]
  • \(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.

# CORRECTO
media_train = X_train.groupby('ciudad')['target'].mean()
X_train['ciudad_enc'] = X_train['ciudad'].map(media_train)
X_test['ciudad_enc'] = X_test['ciudad'].map(media_train)

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:

freq = df['ciudad'].value_counts(normalize=True)
df['ciudad_freq'] = df['ciudad'].map(freq)

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.