Cuando una variable categórica tiene muchísimas categorías (códigos de cliente, ciudades, SKU…), el One-Hot Encoding tradicional explota el número de columnas, consume memoria y puede sobre ajustar. En este post vemos tres enfoques prácticos para tratar la alta cardinalidad sin perder el control:
- 1) OHE “domado” que agrupa categorías raras.
- 2) Target/Mean Encoding dentro de un pipeline (sin fuga de objetivo).
- 3) Feature Hashing (hashing trick) para un espacio fijo y ligero.
0- Dataset de ejemplo
Como ejemplo, usaremos un dataset ficticio con una variable categórica de alta cardinalidad (city con más de 400 valores diferentes), dos categóricas normales y un par de numéricas. Objetivo binario y.

La variable categórica city tiene 463 valores diferentes:

1- One-Hot “domado”: agrupar raras e ignorar desconocidas
La idea de este método consiste en mantener OHE pero limitar columnas, agrupando categorías raras y controlando desconocidos. Desde scikit-learn 1.1, en la función OneHotEncoder() podemos usar min_frequency max_categories y handle_unknown="infrequent_if_exist" para mandar las infrecuentes a un único “cajón”.
ohe_hi = OneHotEncoder(
handle_unknown="infrequent_if_exist",
min_frequency=0.01, # agrupa categorías <1% en 'infrequent'
sparse_output=True
)
ohe_lo = OneHotEncoder(handle_unknown="ignore", sparse_output=True)
pre_ohe = ColumnTransformer([
("num", "passthrough", num_cols),
("ohe_hi", ohe_hi, cat_cols_hi),
("ohe_lo", ohe_lo, cat_cols_lo),
], remainder="drop")
pipe_ohe = Pipeline([
("pre", pre_ohe),
("clf", base_clf)
])
auc_ohe = cross_val_score(pipe_ohe, X, y, cv=cv, scoring=auc, n_jobs=-1).mean()
print(f"OHE domado | AUC CV: {auc_ohe:.3f}")
– min_frequency/max_categories: Estos parámetros permiten agrupar las categorías menos frecuentes de una característica en una única columna, lo que reduce la dimensionalidad y mejora la estabilidad del modelo.
– handle_unknown="infrequent_if_exist": Si una categoría no vista durante el entrenamiento aparece en los nuevos datos, se codifica asignándole el valor de la categoría infrecuente agrupada, si es que esta existe.
Hemos obtenido esto:

Es el “one-hot domado” de city: hemos convertido la ciudad en muchas columnas binarias (city_C007, city_C008…), y cada fila tiene un 1 solo en la columna de su ciudad y 0 en las demás. Como la cardinalidad es muy alta, nos quedamos con un conjunto limitado de ciudades frecuentes (en la captura solo se ve una parte), evitando así la explosión de columnas y permitiendo ignorar valores no vistos en inferencia.
¿Cuándo usaremos One-Hot “domado”?: si queremos interpretabilidad por dummies y la cardinalidad no es brutal, o podemos agrupar “la larga cola” sin perder señal.
2 – Target/Mean Encoding (con pipeline, sin fuga)
Aquí la idea es reemplazar cada categoría por la media del objetivo para esa categoría, pero suavizada hacia la media global para evitar sobreajuste. En scikit-learn esto se hace con TargetEncoder. Si lo metemos dentro de un Pipeline y entrenamos con validación cruzada, el encoder aprende solo con los datos del pliegue de entrenamiento y codifica el de validación con medias calculadas en los otros pliegues (cross-fitting), de modo que no se “cuela” información del objetivo en la validación.
from sklearn.preprocessing import TargetEncoder
te_hi = TargetEncoder(cols=cat_cols_hi, smoothing=10, min_samples_leaf=20)
te_lo = TargetEncoder(cols=cat_cols_lo, smoothing=5, min_samples_leaf=10)
# Empaquetamos dos TE: uno para city y otro para segment/deposit
pre_te = ColumnTransformer([
("num", "passthrough", num_cols),
("te_hi", te_hi, cat_cols_hi),
("te_lo", te_lo, cat_cols_lo),
], remainder="drop")
pipe_te = Pipeline([
("pre", pre_te),
("clf", base_clf)
])
auc_te = cross_val_score(pipe_te, X, y, cv=cv, scoring=auc, n_jobs=-1).mean()
print(f"Target Encoding | AUC CV: {auc_te:.3f}")

Aquí hemos aplicado Target Encoding a city: cada ciudad se sustituye por un único número city_te, que es la media del objetivo para esa ciudad suavizada hacia la media global (evita sobreajuste). lead_time y prev_cancel se muestran solo como contexto: en vez de cientos de dummies por ciudad, ahora cada fila tiene una sola columna numérica que resume el efecto de city.
El smoothing evita que categorías con pocos ejemplos “se peguen” al 0 o 1. Si vamos justos de datos, podemos considerar el calibrar probabilidades al final (isotónica/Platt) para umbrales más estables.
3 – Feature Hashing: espacio fijo y sin fit
Piensa en esto así: para cada fila creas pequeñas “etiquetas” de texto con la forma columna=valor (por ejemplo, segment=Online TA, deposit=Refundable). Esas etiquetas se pasan a FeatureHasher, que aplica una función hash y las coloca en un vector de tamaño fijo (N columnas) siempre en las mismas posiciones para las mismas etiquetas. No necesita fit ni guardar un diccionario (por eso es rápido y consume poca memoria) y se lleva bien con muchísimas categorías o categorías nuevas. La pega: puede haber colisiones (dos etiquetas distintas que caen en la misma columna) y se pierde algo de interpretabilidad, justo igual que el “hashing trick” clásico en NLP (p. ej., HashingVectorizer).
def to_token_list(df, cols):
return (df[cols].astype(str)
.apply(lambda r: [f"{c}={r[c]}" for c in cols], axis=1))
hasher = FeatureHasher(n_features=2**14, input_type="string")
hash_pipe = Pipeline([
("features", ColumnTransformer([
("num", "passthrough", num_cols),
("hash", Pipeline([
("tok", FunctionTransformer(to_token_list, kw_args={"cols": cat_cols_hi+cat_cols_lo})),
("fh", FunctionTransformer(lambda X: hasher.transform(X), accept_sparse=True))
]), cat_cols_hi+cat_cols_lo)
], remainder="drop")),
("clf", base_clf)
])
auc_hash = cross_val_score(hash_pipe, X, y, cv=cv, scoring=auc, n_jobs=-1).mean()
print(f"Feature Hashing | AUC CV: {auc_hash:.3f}")

Esto es la salida de Feature Hashing: las 3 columnas categóricas (city, segment, deposit) se han transformado en 16 columnas fijas (h00–h15).
En cada fila solo hay unos pocos valores distintos de 0 (−1, 1…), que son los “buckets” donde han caído sus tokens; así evitamos crear miles de dummies a costa de posibles colisiones.
Algunos consejos para el feature hashing:
- Elegir
n_featuressegún memoria/latencia. - Saber que existe colisión de hash: pequeñas, pero reales; mitigarlo subiendo
n_features. - Ideal cuando la cardinalidad es descomunal o el schema cambia a menudo.
Conclusión
Si necesitamos columnas interpretables y el número de categorías es manejable, usaremos OHE con categorías raras agrupadas (parámetros min_frequency / max_categories).
Si prima el desempeño y tenemos datos suficientes, Target/Mean Encoding suele capturar mejor la señal.
Si la cardinalidad es enorme o el esquema cambia, Feature Hashing nos ofrece un espacio fijo y sin fit, con muy buena relación señal/recursos.