Условие
В датасете есть категориальные фичи разной природы:
city— 5000 уникальных значений;device_type— 4 значения (mobile/tablet/desktop/tv);user_segment— 10 значений с порядком (S → M → L → XL);product_id— 1М уникальных;referrer_domain— 50К уникальных, длинный хвост.
Выберите способ кодирования для каждой фичи под (а) логистическую регрессию; (б) градиентный бустинг (CatBoost/LightGBM). Объясните выбор и риски.
Решение
Способы
| Способ | Размерность | Логрег | Бустинг | Утечка target |
|---|---|---|---|---|
| One-Hot Encoding | взрыв | ок при ≤ 50 | ок при ≤ 50-100 | нет |
| Ordinal (label encoding) | 1 | плохо | нормально (но не оптимально) | нет |
| Target / Mean encoding | 1 | хорошо | хорошо | да — нужна защита |
| Frequency / Count encoding | 1 | средне | хорошо | нет |
| Hashing trick | задаётся | средне | хорошо для high-card | нет |
| Embeddings (NN/DL) | задаётся | n/a | n/a (но FFM/факторизация) | нет |
По фичам
device_type (4 уник.): One-Hot для логрег. Для бустинга — нативная категориальная (CatBoost / LightGBM).
user_segment (10, порядок): Ordinal (S=1, M=2, L=3, XL=4). И для логрег, и для бустинга. Если поведение монотонно — отлично.
city (5000):
- Логрег: Target encoding с регуляризацией (sklearn-contrib
TargetEncoderилиcategory_encoders). Альтернатива — частотное. - Бустинг: нативная категориальная или target encoding со smoothing.
product_id (1М):
- Логрег: Hashing trick (например, 2¹⁵ корзин) или embeddings, если переходим в FFM/DL.
- Бустинг: чаще не подаём raw
product_id— заменяем агрегатами (CTR_30d, avg_price, n_views).
referrer_domain (50K, длинный хвост):
- Группировать редкие в
OTHER:df[col].value_counts() < 100 → 'OTHER'. - Дальше target encoding или frequency.
Target encoding с защитой от утечки
from category_encoders import TargetEncoder
# CV-based encoding (out-of-fold)
from sklearn.model_selection import KFold
import numpy as np, pandas as pd
def oof_target_encode(X, y, col, n_splits=5, smoothing=10):
kf = KFold(n_splits=n_splits, shuffle=True, random_state=0)
out = np.full(len(X), np.nan)
for tr, va in kf.split(X):
prior = y.iloc[tr].mean()
stats = y.iloc[tr].groupby(X.iloc[tr][col]).agg(['mean', 'count'])
smooth = (stats['mean'] * stats['count'] + prior * smoothing) / (stats['count'] + smoothing)
out[va] = X.iloc[va][col].map(smooth).fillna(prior).values
return outsmoothing затягивает редкие категории к prior — защищает от переобучения на маленьких группах.
Что делать с unseen в test
# при .transform на test
encoded = X_test[col].map(encoding_dict).fillna(global_prior)CatBoost: ordered target encoding
CatBoost делает target encoding онлайн — для каждой строки использует только предыдущие строки. Это уже out-of-fold-like — отсюда устойчивость и не нужно вручную энкодить.
Подводные камни
- One-Hot на 5000 значений в логрег → разреженная матрица 5000 колонок, переобучение и медленный fit. Использовать sparse + L1.
- Mean encoding без CV → leakage; в проде Gini падает на 5-10 п.п.
- Ordinal без порядка (city → произвольные числа) для логрег = бред: модель решит, что «10 > 1». Только для деревьев и осторожно.
- CatBoost категориальная фича = строка, а LightGBM —
categorydtype или явный список колонок. - Хеш-коллизии на hashing trick: 2¹⁵ для 1М значений = 30+ значений на bucket. Часто всё равно работает для логрег с регуляризацией.
product_idподавать как число в логрег / линейку = leakage в худшем виде (модель «думает», что 12345 > 100).- Test-only категории: после fit encoder может не знать значение — обязательно fill prior.
Эталонный ответ
device_type(4): One-Hot для логрег, native cat для бустинга.user_segment(10, порядок): Ordinal для обоих.city(5000): Target encoding с CV+smoothing для логрег; native cat или target enc для бустинга.product_id(1М): Hashing trick для логрег; агрегаты (CTR, avg_price) для бустинга.referrer_domain(50К, хвост): группировать редкие → OTHER, потом target encoding.
Ключи: target encoding делать out-of-fold (или через CatBoost ordered), unseen в test → fill prior, ordinal — только при настоящем порядке.