Feature engineering — это работа, где из сырых таблиц orders, users, events я готовлю числовые признаки, на которых модель вообще способна учиться. И если коротко ответить на главный вопрос «что важнее — алгоритм или признаки», то признаки. Я много раз видел, как обычная логистическая регрессия на хорошо продуманных фичах обходит бустинг на сырых данных. Модель не понимает, что created_at = 2026-03-14 21:40 — это пятница вечер, а user_id = 8842 сделал последний заказ 63 дня назад. Пока я это не посчитаю и не положу в отдельные колонки, никакого сигнала для модели нет. Ниже — набор приёмов, которые я использую почти в каждой задаче, с кодом на SQL и pandas.
Что такое feature engineering и почему он решает больше, чем модель?
Признак (feature) — это одна колонка в финальной таблице, которую я отдаю в модель. Строка — один объект: пользователь, заказ, сессия, товар. Задача feature engineering в том, чтобы каждая колонка несла полезный сигнал про целевую переменную и при этом была честной — то есть содержала только ту информацию, которая реально доступна на момент прогноза.
Аналитику это близко: по сути я строю ту же витрину, что и для дашборда, только вместо графика на выходе — матрица чисел. Навыки те же — группировки, оконные функции, джойны. Если с группировками пока туго, стоит потренироваться на живых данных в SQL-тренажёре и разобрать оконные функции в SQL-справочнике — без них половина фич не соберётся.
Дальше я иду по типам признаков в том порядке, в каком обычно собираю их в реальном проекте.
Как собрать агрегаты по пользователю (RFM-подобные признаки)?
Первое, что я делаю — сворачиваю историю действий пользователя в несколько чисел. Классика — RFM: recency (как давно был последний заказ), frequency (сколько заказов всего), monetary (сколько денег принёс). Это база практически для любой задачи про клиента — от прогноза оттока до склонности к покупке.
SELECT
o.user_id,
COUNT(*) AS orders_cnt,
SUM(o.amount) AS revenue_total,
AVG(o.amount) AS avg_check,
MAX(o.created_at) AS last_order_at,
MIN(o.created_at) AS first_order_at,
COUNT(DISTINCT DATE(o.created_at)) AS active_days
FROM orders o
WHERE o.status = 'paid'
AND o.created_at < '2026-03-01' -- граница наблюдения, о ней ниже
GROUP BY o.user_id;
Обрати внимание на WHERE o.created_at < '2026-03-01'. Это дата отсечки (cutoff): всё, что я считаю в признаках, должно быть строго ДО момента, за который делаю прогноз. Без этой границы легко подтянуть будущие заказы в признаки прошлого — и получить утечку, к которой я ещё вернусь.
Из этих агрегатов рождается второй слой — соотношения. Средний чек к максимальному, доля оплаченных заказов от всех созданных, выручка на активный день. Часто именно отношения, а не абсолютные суммы, лучше всего разделяют пользователей: revenue_total сильно зависит от возраста аккаунта, а revenue_total / active_days — уже нет. Если хочется системно потренировать такие витрины на задачах уровня собеса, есть подборка SQL и Python задач ровно про агрегаты и джойны.
Какие временные признаки помогают модели?
Дата в сыром виде для модели бесполезна. Я всегда раскладываю её на компоненты, потому что поведение людей циклично: будни против выходных, утро против ночи, декабрь против февраля.
import pandas as pd
df['dt'] = pd.to_datetime(df['created_at'])
df['dow'] = df['dt'].dt.dayofweek # 0 = понедельник
df['hour'] = df['dt'].dt.hour
df['month'] = df['dt'].dt.month
df['is_weekend'] = df['dow'].isin([5, 6]).astype(int)
Отдельная история — лаги и скользящие средние. Лаг — это значение из прошлого: сколько пользователь потратил в предыдущем заказе. Скользящее среднее — сглаженный тренд за окно: средний чек за последние 7 заказов. Оба приёма дают модели контекст «а как было раньше».
df = df.sort_values(['user_id', 'dt'])
# лаг: сумма предыдущего заказа этого же пользователя
df['amount_lag_1'] = df.groupby('user_id')['amount'].shift(1)
# скользящее среднее за 7 последних заказов
df['amount_roll_7'] = (
df.groupby('user_id')['amount']
.rolling(7, min_periods=1)
.mean()
.reset_index(level=0, drop=True)
)
Здесь два критичных момента. Во-первых, обязательна сортировка по времени внутри пользователя — иначе shift(1) возьмёт случайную строку, а не прошлый заказ. Во-вторых, shift(1) намеренно сдвигает на одну строку назад: модель на момент текущего заказа не знает его собственную сумму, только предыдущие. Если забыть про сдвиг и посчитать rolling с текущей точкой — снова утечка. Потренировать pandas-конвейеры с группировками удобно в Python-тренажёре, а синтаксис rolling и shift подсмотреть в Python-справочнике.
Как кодировать категориальные признаки без утечки?
Модель ест числа, а city, device, payment_method — это строки. Простой способ — one-hot encoding: каждая категория превращается в отдельную колонку из нулей и единиц.
dummies = pd.get_dummies(df['device'], prefix='device')
df = pd.concat([df, dummies], axis=1)
# device_ios, device_android, device_web ...
One-hot отлично работает, пока категорий немного. Но если это city с тысячей значений или product_id с десятками тысяч — колонок становится столько, что модель захлёбывается. Тут в ход идёт target (mean) encoding: категорию заменяю на среднее значение целевой переменной по этой категории. Например, город кодирую средней конверсией в этом городе.
И вот здесь прячется самая коварная утечка во всём feature engineering. Если я посчитаю среднее по target на всех данных сразу, то в признак каждой строки попадёт её собственный таргет. Модель это «запомнит», на трейне покажет фантастику, а на проде развалится. Правильно считать mean encoding out-of-fold — среднее для строки берётся только по другим фолдам, где её нет.
from sklearn.model_selection import KFold
df['city_te'] = 0.0
global_mean = df['target'].mean()
kf = KFold(n_splits=5, shuffle=True, random_state=42)
for tr_idx, val_idx in kf.split(df):
means = df.iloc[tr_idx].groupby('city')['target'].mean()
df.loc[df.index[val_idx], 'city_te'] = (
df.iloc[val_idx]['city'].map(means).fillna(global_mean)
)
Для редких городов, где две-три строки, среднее нестабильно — его сглаживают в сторону глобального среднего (это называют smoothing). Но даже без сглаживания главное правило одно: таргет строки не должен участвовать в кодировании этой же строки.
Нужна ли нормализация признаков и когда?
Зависит от модели. Линейные модели, логистическая регрессия, метрические методы (kNN) и нейросети чувствительны к масштабу: если revenue_total измеряется в сотнях тысяч, а orders_cnt — в единицах, то первый признак задавит второй просто из-за размерности. Деревья и бустинг (случайный лес, градиентный бустинг) к масштабу равнодушны — они работают через пороги, а не расстояния.
Когда нормализация нужна, я привожу признаки к общему масштабу. Стандартизация — вычесть среднее и поделить на стандартное отклонение (получаем нулевое среднее и единичный разброс). Min-max — линейно сжать в диапазон от 0 до 1.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # учим параметры ТОЛЬКО на трейне
X_test_scaled = scaler.transform(X_test) # к тесту применяем те же параметры
Ключевая деталь в комментариях: fit только на обучающей выборке. Если посчитать среднее и стандартное отклонение на всех данных, включая тест, то статистика теста просочится в обучение. Это тоже утечка, просто менее заметная. Для сильно скошенных величин вроде выручки перед нормализацией полезно взять логарифм — длинный хвост богатых клиентов перестаёт растягивать всю шкалу.
Как правильно работать с пропусками в данных?
Пропуск (NaN) — это не всегда «нет данных», иногда это сам по себе сигнал. Если у пользователя пусто в поле income, возможно, он просто не заполнил профиль — а незаполненный профиль коррелирует с поведением. Поэтому я почти всегда делаю два действия: сначала флаг пропуска, потом заполнение.
# 1) флаг создаём ДО заполнения, иначе он весь занулится
df['income_was_null'] = df['income'].isna().astype(int)
# 2) числовое заполняем медианой (устойчивее среднего к выбросам)
df['income'] = df['income'].fillna(df['income'].median())
# 3) категориальное — отдельной категорией
df['city'] = df['city'].fillna('unknown')
Порядок важен: флаг income_was_null надо посчитать раньше, чем заполнить пропуски, иначе после fillna пропусков не останется и флаг будет бесполезен. И снова про честность: медиану для заполнения считаю по трейну, а к тесту применяю то же число. Заполнить тест его собственной медианой — значит подсмотреть распределение теста заранее.
Что такое утечка данных и почему это ошибка номер один?
Утечка данных (data leakage) — это когда в признаки попадает информация, которой на момент реального прогноза ещё не существует. Это самая дорогая ошибка в ML, потому что она не ломает пайплайн — наоборот, метрики на валидации взлетают, ты радуешься, катишь модель в прод, а там качество проседает вдвое. Утечка маскируется под успех.
Где я ловил её чаще всего:
- посчитал
recencyотносительно сегодняшнего дня, а не относительно даты отсечки — и в признак попало будущее; - сделал target encoding на всех данных без out-of-fold;
- нормализовал признаки на всём датасете до разбиения на train/test;
- взял признак, который физически появляется позже таргета. Классика — прогнозируем оплату заказа, а в фичах лежит
payment_confirmed_at, которого на момент прогноза ещё нет; - собрал скользящее среднее с текущей точкой без
shift, и модель увидела собственный таргет.
Общий рецепт защиты один — думать во времени. Для каждого признака спрашиваю себя: «Эта величина была известна СТРОГО до момента, за который я делаю прогноз?» Если хоть капля будущего просачивается — признак под нож. Отдельно помогает единая точка отсечки в SQL (тот самый WHERE created_at < cutoff) и правило «все fit — только на трейне». Понимание, как метрика ведёт себя во времени, приходит быстрее, когда разбираешь конкретные бизнес-показатели вроде DAU и retention и продуктовые кейсы с разбором — там ровно про то, что доступно в моменте, а что нет.
С чего начать feature engineering на практике?
Мой рабочий порядок такой: сначала фиксирую дату отсечки и целевую переменную, потом собираю пользовательские агрегаты в SQL, добавляю временные признаки и лаги, кодирую категории (осторожно с target encoding), нормализую под конкретную модель и в самом конце прохожусь чек-листом на утечки. Восемьдесят процентов результата дают простые агрегаты и честная работа со временем, а не экзотические фичи.
Дальше это лучше всего закрепляется руками. Если хочешь прокачать именно ту базу, на которой стоит feature engineering, начни с курса по SQL и группировок, добери оконные функции для лагов и скользящих в SQL-тренажёре, а логику признаков собери в Python-тренажёре. А если готовишься к собеседованию аналитика, где про фичи и утечку спрашивают почти всегда, посмотри разбор реальных вопросов с интервью — там эти темы разложены по полкам.
Полный доступ ко всем задачам, кейсам и AI-собесу открывается в Pro — если чувствуешь, что теории уже хватает, а не хватает практики на живых данных, это следующий логичный шаг.