Feature Engineering
Генерация признаков, обработка категорий, текстов и временных рядов.
Feature Engineering — данные важнее модели
Можно взять простую логистическую регрессию с хорошими признаками и обойти глубокую нейросеть на сырых данных. Это не теория — на Kaggle-соревнованиях feature engineering решает 50-80% успеха. Модель умеет находить закономерности, но ты должен дать ей правильный «словарь» для описания мира.
Аналогия: представь, что ты объясняешь другу, как выбрать квартиру. Ты не даёшь ему сырой адрес «ул. Ленина, 42» — ты говоришь: «10 минут до метро, второй этаж, свежий ремонт, тихий район». Каждый из этих признаков — результат трансформации сырых данных в полезную информацию. Feature engineering — это ровно то же самое, но для модели.
Большая картина: от сырых данных к фичам
Feature engineering — это не одно действие, а целый пайплайн между сырыми данными и моделью:
Шаг 1. Сырые данные (Raw Data). Таблица из базы данных: числа, строки, даты, пропуски, JSON-поля. Модель не может с этим работать напрямую. Шаг 2. Очистка (Cleaning). Убираем дубликаты, обрабатываем пропуски, фиксим типы данных. Доход «-999» — это не минус 999 тысяч, а код пропуска. Шаг 3. Трансформация (Transform). Нормализуем числа, кодируем категории, извлекаем фичи из дат и текстов. Создаём новые признаки (например, «возраст аккаунта в днях» из даты регистрации). Шаг 4. Отбор (Selection). Убираем шум — признаки, которые не помогают или мешают. Корреляция, feature importance, L1-регуляризация. Шаг 5. Модель. Чистые, информативные фичи → модель учится быстрее и точнее.

Дальше разберём каждый тип данных отдельно: числа, категории, текст, время. Потом — отбор признаков и работа с пропусками.
Числовые признаки: нормализация и трансформации
У тебя признак «доход» от 10 000 до 10 000 000, а «возраст» от 18 до 80. Линейная модель будет думать, что доход в тысячу раз важнее — просто потому что числа больше. Нормализация приводит признаки к одному масштабу, чтобы модель оценивала их честно.
StandardScaler — вычитаем среднее, делим на стандартное отклонение. Результат: среднее = 0, σ = 1. Стандартный выбор для линейных моделей и нейросетей.
μ — среднее значение признака, σ — стандартное отклонение. Считаются ТОЛЬКО на train
MinMaxScaler — сжимает значения в диапазон [0, 1]. Полезен, когда нужны ограниченные значения (вероятности, входы нейросети с сигмоидой). Чувствителен к выбросам — один экстремальный объект «сожмёт» все остальные в узкий диапазон.
xₘᵢₙ и xₘₐₓ — минимум и максимум НА ОБУЧАЮЩЕЙ выборке
Логарифм (log transform) — спасение для скошенных распределений. Доход, цены, количество кликов — всё это имеет длинный правый хвост. log(x) «сжимает» хвост, делая распределение ближе к нормальному. Линейные модели работают с таким распределением намного лучше.
Binning (бинаризация) — превращает число в категорию. Возраст → «18-25», «26-35», «36-50», «50+». Зачем? Иногда зависимость нелинейная: доход растёт до 40 лет, потом стабилен. Binning позволяет линейной модели поймать такую зависимость без полиномиальных фичей.
Деревьям нормализация не нужна
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# Данные: доход (тыс. руб), возраст
X_train = np.array([[30, 25], [150, 40], [500, 55], [80, 32]])
# StandardScaler: среднее=0, std=1
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train)
# Результат: [[-0.96, -1.07], [0.02, 0.47], [2.08, 1.70], [-0.54, -0.29]]
# Log-transform для скошенного "дохода"
X_train_log = X_train.copy().astype(float)
X_train_log[:, 0] = np.log1p(X_train[:, 0]) # log(1+x), чтобы log(0) не упалКатегориальные признаки: от строк к числам
Город, профессия, тип устройства — модель не понимает строки. Нужно закодировать. Но выбор кодирования зависит от модели и числа уникальных значений.
One-Hot Encoding — создаём отдельный столбец для каждой категории. Город = «Москва» → [1, 0, 0], «Питер» → [0, 1, 0]. Плюс: нет ложного порядка. Минус: если категорий 10 000 (города России) — получим 10 000 столбцов. Разреженная матрица убивает линейные модели. Когда использовать: линейные модели + мало категорий (< 30-50).
Label Encoding — каждой категории присваиваем число: «Москва» = 0, «Питер» = 1, «Новосибирск» = 2. Просто и компактно, но линейная модель решит, что «Новосибирск (2) > Питер (1)» — а это бессмыслица. Когда использовать: деревья и бустинг (они смотрят на пороги, не на порядок).
Target Encoding — заменяем категорию средним значением целевой переменной для этой категории. Город «Москва» — средний доход 120K, «Томск» — 45K. Мощный метод: одна колонка вместо тысяч, и она несёт реальную информацию о связи с таргетом. Опасность: target leakage. Если считать среднее по всем данным (включая test), модель «подсматривает» ответы. Результат: метрики на валидации отличные, на проде — катастрофа.
Frequency Encoding — заменяем категорию частотой её появления. «Москва» встречается в 30% строк → 0.3. Нет утечки данных, работает как proxy для «популярности». Для городов, товаров, доменов — часто достаточно.
import pandas as pd
from category_encoders import TargetEncoder
# Target encoding — ТОЛЬКО на train!
encoder = TargetEncoder(smoothing=10) # сглаживание к глобальному среднему
X_train_enc = encoder.fit_transform(X_train[['city']], y_train)
X_val_enc = encoder.transform(X_val[['city']]) # БЕЗ fit!
# Frequency encoding — безопасный и простой
freq = X_train['city'].value_counts(normalize=True)
X_train['city_freq'] = X_train['city'].map(freq)
X_val['city_freq'] = X_val['city'].map(freq).fillna(0) # новые категории → 0⚠️ Утечка данных — смертный грех
Текстовые признаки: от слов к числам
Отзывы, описания товаров, тикеты поддержки — текст несёт огромную информацию, но модель работает с числами. Три основных подхода, от простого к сложному:
Bag of Words (мешок слов) — считаем, сколько раз каждое слово встречается в документе. «Кошка сидела на коврике» → {кошка: 1, сидела: 1, на: 1, коврике: 1}. Порядок слов теряется (поэтому «мешок»), но для классификации тональности часто достаточно. TF-IDF — улучшение BoW. Слово «и» встречается везде — оно неинформативно. TF-IDF умножает частоту слова в документе (TF) на «редкость» в корпусе (IDF). Редкие, но частые в конкретном тексте слова получают высокий вес. Embeddings (Word2Vec, BERT) — каждое слово или предложение → вектор в пространстве, где семантически похожие слова рядом. «Кот» и «кошка» — соседи, «автомобиль» — далеко. Для сложных задач (similarity, QA) — единственный вариант.
from sklearn.feature_extraction.text import TfidfVectorizer
# TF-IDF: важность слова = частота × редкость
tfidf = TfidfVectorizer(max_features=5000, ngram_range=(1, 2))
X_tfidf = tfidf.fit_transform(train_texts) # sparse matrix
# Биграммы (ngram_range=(1,2)) ловят "не нравится" — одно слово "не" бесполезноТема текстовых признаков огромна — подробнее в нодах Word Embeddings и BERT. Здесь запомни: для табличных задач с текстовым полем TF-IDF + бустинг — сильный бейзлайн.
Временные признаки: золотая жила из одной колонки
Из одного столбца «дата заказа» можно создать десяток полезных фичей. Временные данные делятся на три группы:
Datetime features — извлекаем компоненты даты: час, день недели, месяц, квартал, «выходной или нет», «рабочее время или нет». Паттерны реальны: люди покупают чаще по вечерам, отток клиентов выше в январе. Lag features — значение признака N периодов назад. Продажи вчера, неделю назад, месяц назад. Лаги ловят инерцию: если продажи росли 3 дня подряд — скорее всего вырастут и сегодня. Rolling statistics — скользящие агрегаты: среднее за 7 дней, максимум за 30 дней, стандартное отклонение за 14 дней. Ловят тренды и волатильность.
import pandas as pd
# Datetime features
df['dow'] = df['date'].dt.dayofweek # 0=Пн, 6=Вс
df['hour'] = df['date'].dt.hour
df['is_weekend'] = df['dow'].isin([5, 6]).astype(int)
df['month'] = df['date'].dt.month
df['quarter'] = df['date'].dt.quarter
# Lag features (осторожно с утечкой будущего!)
df['sales_lag_1'] = df.groupby('store_id')['sales'].shift(1)
df['sales_lag_7'] = df.groupby('store_id')['sales'].shift(7)
# Rolling statistics
df['sales_roll_7_mean'] = (
df.groupby('store_id')['sales']
.transform(lambda x: x.shift(1).rolling(7).mean()) # shift(1) — без текущего дня!
)
df['sales_roll_7_std'] = (
df.groupby('store_id')['sales']
.transform(lambda x: x.shift(1).rolling(7).std())
)⚠️ Утечка будущего
Отбор признаков: убрать шум, оставить сигнал
Больше фичей ≠ лучше. Лишние признаки — это шум, который мешает модели обобщать. 100 случайных признаков могут ухудшить бустинг, хотя он вроде бы «сам» отбирает важные. Три подхода к отбору:
Filter (фильтрация) — статистические тесты до обучения модели. Самый простой: считаем корреляцию каждого признака с целевой переменной и отбрасываем слабые. Также полезно удалить пары сильно коррелированных между собой признаков (корреляция > 0.95) — они дублируют информацию. Плюс: быстро. Минус: не учитывает взаимодействия между признаками.
Wrapper (обёртка) — обучаем модель, оцениваем подмножества признаков. RFE (Recursive Feature Elimination): обучаем модель на всех признаках, убираем наименее важный, повторяем. «Жадный» поиск — медленнее, но учитывает взаимодействия. Плюс: точнее filter. Минус: дорого — обучаем модель N раз.
Embedded (встроенный) — модель сама отбирает признаки в процессе обучения. L1-регуляризация (Lasso) зануляет веса ненужных признаков — буквально выбрасывает их из модели. Feature importance в бустинге показывает, какие признаки модель реально использует. Плюс: отбор как часть обучения. Минус: зависит от конкретной модели.
from sklearn.feature_selection import RFE
from sklearn.linear_model import Lasso
from sklearn.ensemble import GradientBoostingClassifier
# Filter: корреляция с таргетом
corr = X_train.corrwith(y_train).abs().sort_values(ascending=False)
top_features = corr[corr > 0.05].index.tolist()
# Wrapper: RFE — рекурсивное удаление признаков
rfe = RFE(estimator=GradientBoostingClassifier(), n_features_to_select=20)
rfe.fit(X_train, y_train)
selected = X_train.columns[rfe.support_]
# Embedded: L1-регуляризация зануляет ненужные веса
lasso = Lasso(alpha=0.01)
lasso.fit(X_train, y_train)
important = X_train.columns[lasso.coef_ != 0] # ненулевые — важныеFeature importance: какие фичи реально работают
Ты создал 200 признаков — какие из них модель реально использует? Feature importance отвечает на этот вопрос. Три подхода, от простого к мощному:
Встроенная importance — бустинг (XGBoost, LightGBM, CatBoost) считает, сколько раз каждый признак использовался в разбиениях деревьев и насколько он снизил ошибку. Быстро, бесплатно (уже посчитано при обучении), но зависит от модели. Permutation importance — перемешиваем значения одного признака случайным образом и смотрим, насколько упало качество модели. Если упало сильно — признак важен. Если не упало — можно выбросить. Не зависит от модели, работает с любым алгоритмом. SHAP (SHapley Additive exPlanations) — золотой стандарт. Для каждого конкретного предсказания SHAP показывает вклад каждого признака. Не просто «доход важен», а «для клиента #42 высокий доход увеличил вероятность одобрения на 0.15». Основан на значениях Шепли из теории игр — честное распределение «вклада» между игроками (признаками).
import shap
from sklearn.inspection import permutation_importance
# Встроенная importance (CatBoost/LightGBM/XGBoost)
importance = model.feature_importances_
top_10 = sorted(zip(feature_names, importance), key=lambda x: -x[1])[:10]
# Permutation importance — model-agnostic
perm = permutation_importance(model, X_val, y_val, n_repeats=10)
# perm.importances_mean — средняя потеря качества при перемешивании
# SHAP — объяснение каждого предсказания
explainer = shap.TreeExplainer(model) # для бустинга — быстро
shap_values = explainer.shap_values(X_val)
shap.summary_plot(shap_values, X_val) # beeswarm plot — красиво и информативноPermutation vs встроенная importance
Пропуски — не проблема, а информация
Пустое поле «доход» может означать: человек не хочет раскрывать зарплату (высокий доход?), безработный, или данные потеряли при миграции. Сам факт пропуска — это признак. Стратегия зависит от природы пропуска и модели.
Удаление строк — если пропусков < 5% и они случайны (MCAR — missing completely at random). Просто и безопасно, но теряем данные.
Заполнение константой — медиана для числовых (устойчива к выбросам), мода для категориальных. Самый частый подход. Для линейных моделей — почти обязательно, иначе NaN сломает вычисления.
Индикатор пропуска — создаём новый бинарный признак: income_is_null = 1/0. Часто индикатор сам по себе предсказывает таргет лучше, чем заполненное значение. Используй совместно с заполнением.
Нативная обработка — CatBoost, LightGBM, XGBoost умеют работать с NaN из коробки. Дерево само решает, в какую ветку отправить объекты с пропуском. Часто это лучше ручного заполнения.
from sklearn.impute import SimpleImputer
import pandas as pd
# 1. Индикатор пропуска (до заполнения!)
df['income_is_null'] = df['income'].isnull().astype(int)
# 2. Заполнение медианой
imputer = SimpleImputer(strategy='median')
df['income'] = imputer.fit_transform(df[['income']])
# 3. Для бустинга — можно оставить NaN
# CatBoost/LightGBM сами разберутся, и часто — лучше нас🎯 На собеседовании
Junior
Middle
Senior
Собираем всё вместе
Feature engineering — это мост между сырыми данными и моделью. Числовые признаки нормализуем (StandardScaler, log) — для линейных моделей. Категориальные кодируем (one-hot для малых, target/frequency encoding для больших). Текстовые превращаем в TF-IDF или эмбеддинги. Временные раскладываем на компоненты, лаги и rolling-статистики.
Отбор признаков — тоже часть feature engineering. Корреляция (filter), RFE (wrapper), L1 и feature importance (embedded) — три инструмента для удаления шума. SHAP — для объяснения, какие фичи работают и почему.
Если запомнить одну вещь: правильные фичи > правильная модель. CatBoost на хороших признаках победит нейросеть на сырых данных. Трать 80% времени на данные и фичи, 20% — на подбор модели.
Дальше на роадмапе: Классический ML расскажет, какие модели чем отличаются, а Model Selection — как правильно валидировать и не переобучиться.
Материалы
Интерактивный курс с соревновательными задачами. Практика на реальных датасетах.
Официальная документация: нормализация, кодирование, imputation.
Библиотека SHAP: объяснение предсказаний любой модели. TreeSHAP, force plots, summary plots.
Практический доклад по feature engineering от Open Data Science.
Практические приёмы генерации признаков для табличных данных.