MLfeature engineeringпризнакиPython

Feature engineering для аналитика: примеры признаков, которые работают

2026-07-11 11 мин

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, потому что она не ломает пайплайн — наоборот, метрики на валидации взлетают, ты радуешься, катишь модель в прод, а там качество проседает вдвое. Утечка маскируется под успех.

Где я ловил её чаще всего:

Общий рецепт защиты один — думать во времени. Для каждого признака спрашиваю себя: «Эта величина была известна СТРОГО до момента, за который я делаю прогноз?» Если хоть капля будущего просачивается — признак под нож. Отдельно помогает единая точка отсечки в SQL (тот самый WHERE created_at < cutoff) и правило «все fit — только на трейне». Понимание, как метрика ведёт себя во времени, приходит быстрее, когда разбираешь конкретные бизнес-показатели вроде DAU и retention и продуктовые кейсы с разбором — там ровно про то, что доступно в моменте, а что нет.

С чего начать feature engineering на практике?

Мой рабочий порядок такой: сначала фиксирую дату отсечки и целевую переменную, потом собираю пользовательские агрегаты в SQL, добавляю временные признаки и лаги, кодирую категории (осторожно с target encoding), нормализую под конкретную модель и в самом конце прохожусь чек-листом на утечки. Восемьдесят процентов результата дают простые агрегаты и честная работа со временем, а не экзотические фичи.

Дальше это лучше всего закрепляется руками. Если хочешь прокачать именно ту базу, на которой стоит feature engineering, начни с курса по SQL и группировок, добери оконные функции для лагов и скользящих в SQL-тренажёре, а логику признаков собери в Python-тренажёре. А если готовишься к собеседованию аналитика, где про фичи и утечку спрашивают почти всегда, посмотри разбор реальных вопросов с интервью — там эти темы разложены по полкам.

Полный доступ ко всем задачам, кейсам и AI-собесу открывается в Pro — если чувствуешь, что теории уже хватает, а не хватает практики на живых данных, это следующий логичный шаг.

Закрепи Python на задачах
402 Python-задачи через pandas/numpy/scipy — попробуй бесплатно.
Python-тренажёр →