En la lección anterior dejamos una rama del ColumnTransformer pendiente: las columnas categóricas. La ciudad del cliente, la categoría del producto o el plan de suscripción de MercaFresh son texto, y los modelos de Machine Learning solo saben operar con números: multiplican, suman distancias, calculan gradientes. "Barcelona" no se puede multiplicar. Codificar es traducir categorías a números, y no es un trámite mecánico: cada método de codificación cuenta al modelo una historia distinta sobre los datos, y elegir mal (por ejemplo, imponer un orden que no existe) introduce patrones falsos que el modelo aprenderá obedientemente. En esta lección verás los métodos principales, sus trampas y una guía clara de cuándo usar cada uno.

Contenido

  1. Por qué los modelos necesitan números
  2. Nominal vs ordinal: la distinción que gobierna todo
  3. Label y ordinal encoding
  4. One-hot encoding
  5. El problema de la alta cardinalidad
  6. Frequency y target encoding
  7. Tabla comparativa y decisión
  8. Caso completo MercaFresh: ciudad, categoría y plan

Por qué los modelos necesitan números

Internamente, casi todos los algoritmos del módulo 4 reducen su trabajo a aritmética sobre matrices: la regresión calcula sumas ponderadas (peso1 * x1 + peso2 * x2 + ...), K-NN mide distancias entre filas, las redes neuronales multiplican matrices. En cuanto una columna contiene "Barcelona", esa maquinaria se detiene: no existe 0.7 * "Barcelona".

La tentación ingenua es asignar números a dedo: Barcelona = 1, Madrid = 2, Valencia = 3. Funciona sintácticamente... y ahí está el peligro: el modelo se creerá los números. Interpretará que Valencia es "el triple" que Barcelona, que Madrid está "entre" ambas, que la media de Barcelona y Valencia es Madrid. Hemos inventado un orden y unas distancias que no existen en la realidad. Toda esta lección gira en torno a evitar ese engaño.

Nominal vs ordinal: la distinción que gobierna todo

Del módulo 2 (lección 02-01) recordarás la clasificación de variables. Para codificar, la distinción crucial es esta:

Tipo ¿Hay orden real? Ejemplos MercaFresh Codificación natural
Nominal No: solo etiquetas ciudad, categoría de producto, método de pago One-hot
Ordinal Sí: orden con significado plan (básico < estándar < premium), satisfacción (baja < media < alta) Ordinal (enteros ordenados)

La pregunta de control: ¿tiene sentido decir que una categoría es "mayor" que otra? Premium > básico, sí; Madrid > Sevilla, no. Responder esta pregunta antes de tocar el teclado evita el 90 % de los errores de codificación.

Label y ordinal encoding

Ambos asignan un entero a cada categoría; la diferencia está en quién controla el orden.

Ordinal encoding: cuando el orden es real

Para el plan de suscripción de MercaFresh, el orden es información: un cliente premium paga más y se comporta distinto. Aquí los enteros ordenados son la codificación correcta, y debemos fijar el orden explícitamente:

import pandas as pd
from sklearn.preprocessing import OrdinalEncoder

df = pd.DataFrame({
    "id_cliente": [101, 104, 105, 106, 107],
    "plan": ["basico", "premium", "estandar", "basico", "premium"],
})

# categories fija el orden: basico=0 < estandar=1 < premium=2
enc = OrdinalEncoder(categories=[["basico", "estandar", "premium"]])
df["plan_cod"] = enc.fit_transform(df[["plan"]])
print(df)
#    id_cliente      plan  plan_cod
# 0         101    basico       0.0
# 1         104   premium       2.0
# 2         105  estandar       1.0
# ...

Sin el parámetro categories, OrdinalEncoder ordena alfabéticamente — que aquí por casualidad coincide, pero con ["bajo", "alto", "medio"] produciría alto=0, bajo=1, medio=2: un desastre silencioso. Con ordinales, dicta siempre el orden tú.

En pandas puro, el equivalente es un mapeo explícito, igual de válido:

orden = {"basico": 0, "estandar": 1, "premium": 2}
df["plan_cod"] = df["plan"].map(orden)

Label encoding: casi solo para la variable objetivo

LabelEncoder de scikit-learn hace lo mismo (categoría → entero) pero está pensado para la variable objetivo (por ejemplo, churn "sí"/"no" → 1/0), no para las características: trabaja con una sola columna y asigna el orden alfabético sin que puedas controlarlo.

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
y = le.fit_transform(["no", "si", "no", "no", "si"])   # [0 1 0 0 1]
print(le.classes_)                                      # ['no' 'si']

El peligro: orden falso sobre nominales

¿Qué pasa si codificamos la ciudad con enteros? Barcelona=0, Madrid=1, Sevilla=2, Valencia=3. Un modelo lineal aprenderá un único coeficiente para "ciudad" y concluirá cosas como "cada paso de ciudad aumenta el churn un 3 %" — una regla absurda que depende del orden alfabético. Algunos modelos de árbol (04-03) sobreviven a esto porque solo hacen cortes, pero como norma general: enteros para nominales = orden falso = patrones inventados. Para nominales, la herramienta es la siguiente.

One-hot encoding

La idea: en lugar de un número por categoría, una columna binaria por categoría. Cada fila tiene un 1 en su columna y 0 en las demás — de ahí el nombre, "un solo encendido".

df2 = pd.DataFrame({
    "id_cliente": [101, 104, 105, 108, 110],
    "ciudad": ["Barcelona", "Barcelona", "Valencia", "Madrid", "Sevilla"],
})

# Con pandas: rápido para explorar
dummies = pd.get_dummies(df2["ciudad"], prefix="ciudad", dtype=int)
print(pd.concat([df2, dummies], axis=1))
#    id_cliente     ciudad  ciudad_Barcelona  ciudad_Madrid  ciudad_Sevilla  ciudad_Valencia
# 0         101  Barcelona                 1              0               0                0
# 1         104  Barcelona                 1              0               0                0
# 2         105   Valencia                 0              0               0                1
# ...

Ya no hay orden ni distancias inventadas: cada ciudad es una dimensión independiente y el modelo aprende un efecto propio para cada una. Es la codificación por defecto para nominales de pocas categorías.

get_dummies vs OneHotEncoder

get_dummies es cómodo, pero tiene un defecto grave en un flujo de ML: no memoriza las categorías. Si el lote del mes que viene no trae ningún cliente de Sevilla, generará una columna menos y el modelo recibirá una matriz con otra forma. OneHotEncoder sigue el patrón fit/transform que ya dominas: aprende las categorías en fit y garantiza siempre las mismas columnas.

from sklearn.preprocessing import OneHotEncoder

ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
X_ciudad = ohe.fit_transform(df2[["ciudad"]])
print(ohe.get_feature_names_out())
# ['ciudad_Barcelona' 'ciudad_Madrid' 'ciudad_Sevilla' 'ciudad_Valencia']
  • handle_unknown="ignore": si en producción aparece un cliente de Bilbao (categoría nunca vista), lo codifica como todo ceros en vez de lanzar un error. Imprescindible en el mundo real.
  • sparse_output=False devuelve una matriz normal; con muchas columnas conviene dejar la salida dispersa (sparse) para ahorrar memoria.
  • Encaja directamente como la rama categórica del ColumnTransformer de la lección anterior.

La trampa de la multicolinealidad, de pasada

Si sabes que un cliente no es de Barcelona, ni de Madrid, ni de Sevilla... ya sabes que es de Valencia: la última columna es redundante (siempre suma 1 con las demás). Esa redundancia, llamada multicolinealidad, molesta a algunos modelos lineales clásicos. El parámetro drop="first" (o drop_first=True en get_dummies) elimina una columna para evitarla. Quédate con la idea y el nombre del parámetro; el porqué profundo pertenece a la regresión lineal (04-01) y a la regularización (07-01).

El problema de la alta cardinalidad

One-hot escala mal. La columna ciudad con 4 valores genera 4 columnas; pero ¿y el código postal de entrega de MercaFresh, con 800 valores distintos? ¿O el id de producto con 5.000?

  • 800 columnas nuevas, casi todas ceros: memoria y tiempo disparados.
  • Categorías con 2 o 3 ejemplos: el modelo no puede aprender nada fiable de ellas (materia prima del overfitting, que formalizaremos en 06-05).

Estrategias frente a la alta cardinalidad:

  1. Agrupar por conocimiento de negocio: códigos postales → provincia o zona de reparto.
  2. Agrupar la cola rara: conservar las categorías frecuentes y fusionar el resto en "otras" (OneHotEncoder(min_frequency=...) lo hace solo).
  3. Cambiar de codificación: frequency o target encoding, que generan una única columna sea cual sea la cardinalidad.
# Agrupar la cola: ciudades con menos de 20 clientes pasan a "otras"
conteo = df_grande["ciudad"].value_counts()
raras = conteo[conteo < 20].index
df_grande["ciudad_agrupada"] = df_grande["ciudad"].replace(raras, "otras")

Frequency y target encoding

Dos técnicas que sustituyen la categoría por un solo número informativo, pensadas para cardinalidad alta. Las presentamos a nivel introductorio: entiende la idea y, sobre todo, el riesgo.

Frequency encoding

Sustituye cada categoría por su frecuencia (cuántas veces aparece, o su proporción):

freq = df_grande["ciudad"].value_counts(normalize=True)
df_grande["ciudad_freq"] = df_grande["ciudad"].map(freq)

Barcelona (40 % de los clientes) se convierte en 0.40; un pueblo con 2 clientes, en 0.0002. Es simple, no explota la dimensionalidad y a veces la frecuencia en sí es predictiva (ciudades grandes tienen mejores tiempos de entrega y menos churn). Limitación: dos categorías con igual frecuencia se vuelven indistinguibles.

Target encoding

Sustituye cada categoría por la media de la variable objetivo en esa categoría: cada ciudad se convierte en su propia tasa de churn.

# Idea conceptual (así NO se debe hacer en un proyecto real, ver el riesgo abajo)
tasa_por_ciudad = df_grande.groupby("ciudad")["churn"].mean()
df_grande["ciudad_target"] = df_grande["ciudad"].map(tasa_por_ciudad)

Es potentísimo —condensa exactamente la relación categoría-objetivo en un número— y por eso mismo es la técnica con más riesgo de fuga de información de todo el preprocesamiento: estamos metiendo la respuesta (el churn) dentro de una característica. Para una ciudad con 3 clientes de los que 2 hicieron churn, la codificación 0.67 es prácticamente chivarle al modelo la etiqueta de esos clientes. Síntomas y precauciones:

  • Evaluación de entrenamiento sospechosamente perfecta y rendimiento pobre en datos nuevos.
  • Calcular las medias solo con datos de entrenamiento (el mismo principio de 03-02, aquí elevado a crítico).
  • Usar suavizado (mezclar la media de la categoría con la media global, con más peso a la global cuanto menos datos tenga la categoría) y esquemas de validación cruzada que verás en 06-03. El TargetEncoder de scikit-learn incorpora estas defensas.

Regla prudente para este curso: one-hot como opción por defecto; target encoding solo cuando la cardinalidad lo exija y con sus salvaguardas puestas.

Tabla comparativa y decisión

Método Columnas generadas ¿Impone orden? Cardinalidad alta Riesgo principal Cuándo usarlo
Ordinal encoding 1 Sí (lo defines tú) Bien Orden mal definido Ordinales reales (plan, nivel)
Label encoding 1 Sí (alfabético) Orden falso Solo la variable objetivo
One-hot 1 por categoría No Mal Explosión de columnas Nominales con pocas categorías (por defecto)
Frequency 1 No Muy bien Colisiones de frecuencia Cardinalidad alta, solución rápida
Target 1 No Muy bien Fuga de información Cardinalidad alta, con suavizado y cuidado
flowchart TD
    A{"¿La variable tiene<br/>orden real?"} -->|Si| B["Ordinal encoding<br/>con orden explicito"]
    A -->|No| C{"¿Pocas categorias?<br/>(orientativo: < 15)"}
    C -->|Si| D["One-hot encoding"]
    C -->|No| E{"¿Puedo agrupar con<br/>criterio de negocio?"}
    E -->|Si| F["Agrupar y one-hot"]
    E -->|No| G["Frequency o target encoding<br/>(con salvaguardas)"]

Caso completo MercaFresh: ciudad, categoría y plan

Apliquemos la decisión a las tres categóricas del dataset de churn:

import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder

clientes = pd.DataFrame({
    "id_cliente": [101, 104, 105, 106, 107, 108],
    "ciudad": ["Barcelona", "Barcelona", "Valencia", "Madrid", "Madrid", "Sevilla"],
    "categoria_favorita": ["frescos", "despensa", "frescos", "bebidas", "frescos", "limpieza"],
    "plan": ["basico", "premium", "estandar", "basico", "premium", "basico"],
    "gasto_total": [1250.5, 890.0, 2100.75, 310.2, 15400.0, 670.4],
})

# Decisiones razonadas:
# - ciudad: nominal, 4 valores            -> one-hot
# - categoria_favorita: nominal, 4 valores -> one-hot
# - plan: ordinal real                     -> ordinal con orden explícito
codificador = ColumnTransformer(transformers=[
    ("nominales", OneHotEncoder(sparse_output=False, handle_unknown="ignore"),
     ["ciudad", "categoria_favorita"]),
    ("ordinales", OrdinalEncoder(categories=[["basico", "estandar", "premium"]]),
     ["plan"]),
    ("numericas", "passthrough", ["gasto_total"]),
])

X = codificador.fit_transform(clientes)
print(codificador.get_feature_names_out())
print(pd.DataFrame(X, columns=codificador.get_feature_names_out()).round(2))

El resultado es una matriz totalmente numérica: 4 columnas de ciudad + 4 de categoría + 1 de plan + el gasto. Observa cómo el ColumnTransformer presentado en 03-03 ha absorbido la codificación como una rama más: el preprocesamiento sigue siendo un único objeto con fit y transform. Nota la limpieza de la ciudad en 03-01: si no hubiéramos unificado "BCN" y " barcelona ", el one-hot habría creado columnas fantasma para cada variante.

Errores Comunes y Consejos

  • Enteros arbitrarios sobre nominales. Ciudad = 1, 2, 3 inventa orden y distancias. Si no puedes decir cuál es "mayor", no uses ordinal.
  • Dejar que OrdinalEncoder ordene alfabéticamente un ordinal real. Pasa siempre categories=[...] con el orden de negocio.
  • Usar get_dummies en producción. Sin memoria de categorías, cada lote puede generar columnas distintas. En flujos de ML, OneHotEncoder con handle_unknown="ignore".
  • One-hot sobre 800 códigos postales. Explosión de columnas y categorías sin datos: agrupa primero o cambia de técnica.
  • Target encoding calculado con todo el dataset. Es la fuga de información en su forma más pura: las medias por categoría se aprenden solo de entrenamiento, con suavizado.
  • Codificar antes de limpiar el texto. "Madrid" y "madrid " generan dos columnas. La normalización de 03-01 va siempre antes que la codificación.
  • Consejo: tras codificar, imprime get_feature_names_out() y revisa que el número y los nombres de columnas son los que esperabas. Los errores de codificación son silenciosos; esta comprobación de 5 segundos los caza casi todos.

Ejercicios

Ejercicio 1

Clasifica cada variable de MercaFresh como nominal u ordinal e indica el método de codificación adecuado: (a) método de pago (tarjeta, PayPal, contra reembolso); (b) valoración de la última entrega (mala, regular, buena, excelente); (c) barrio de entrega en Barcelona (73 barrios); (d) tipo de cliente (particular, empresa).

Ejercicio 2

Codifica con OrdinalEncoder la columna valoracion = ["buena", "mala", "excelente", "regular", "buena"] respetando el orden real. Muestra qué codificación (incorrecta) habría producido el orden alfabético por defecto.

Ejercicio 3

Aplica frequency encoding a la columna barrio = ["Gracia", "Eixample", "Gracia", "Sants", "Eixample", "Gracia", "Raval", "Eixample"] usando proporciones. ¿Qué dos barrios quedan indistinguibles tras la codificación y por qué es una limitación del método?

Soluciones

Ejercicio 1

  • (a) Nominal (ningún método de pago es "mayor") → one-hot (3 categorías, sin problema).
  • (b) Ordinal real (mala < regular < buena < excelente) → ordinal encoding con orden explícito.
  • (c) Nominal de alta cardinalidad (73 valores) → agrupar por distrito si el negocio lo permite; si no, frequency o target encoding con salvaguardas.
  • (d) Nominal binaria → one-hot; con dos categorías basta una sola columna 0/1 (es exactamente el caso donde drop="first" resulta natural).

Ejercicio 2

from sklearn.preprocessing import OrdinalEncoder
import pandas as pd

df = pd.DataFrame({"valoracion": ["buena", "mala", "excelente", "regular", "buena"]})

# Correcto: orden de negocio explícito
enc = OrdinalEncoder(categories=[["mala", "regular", "buena", "excelente"]])
print(enc.fit_transform(df))       # buena=2, mala=0, excelente=3, regular=1

# Incorrecto: orden alfabético por defecto
enc_mal = OrdinalEncoder()
print(enc_mal.fit_transform(df))   # buena=0, excelente=1, mala=2, regular=3

Con el orden alfabético, "mala" (2) quedaría por encima de "excelente" (1): el modelo aprendería que las peores valoraciones son "mayores" que las mejores.

Ejercicio 3

s = pd.Series(["Gracia", "Eixample", "Gracia", "Sants",
               "Eixample", "Gracia", "Raval", "Eixample"])
freq = s.value_counts(normalize=True)
print(s.map(freq))
# Gracia -> 0.375, Eixample -> 0.375, Sants -> 0.125, Raval -> 0.125

Gracia y Eixample colisionan en 0.375, y Sants y Raval en 0.125: barrios distintos se vuelven idénticos para el modelo. Frequency encoding solo conserva "cuán común es la categoría", no su identidad — el precio de comprimir todo en un número.

Conclusión

Ya sabes traducir categorías a números sin mentirle al modelo: ordinal encoding con orden explícito para los ordinales reales, one-hot como opción por defecto para nominales (con OneHotEncoder y handle_unknown="ignore" en flujos serios), estrategias de agrupación o frequency/target encoding para la alta cardinalidad —con el target encoding señalizado como la zona de mayor riesgo de fuga—, y todo integrado en el ColumnTransformer que venimos construyendo. La matriz del churn de MercaFresh es ya completamente numérica.

Pero fíjate en ella: las columnas one-hot valen 0 o 1, el plan va de 0 a 2... y el gasto total llega a 15.400. Para los algoritmos que miden distancias o descienden gradientes, esa disparidad de escalas hace que una sola columna ahogue a todas las demás. Ese es exactamente el problema de la próxima lección: normalización y estandarización.

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