Feature engineering в pandas: практика для ML и аналитики
Почему feature engineering важен
Говорят, что в ML 80% успеха — это правильные фичи. Алгоритмы выучат что угодно, если у вас есть информативные признаки. И провалят задачу, если вход — мусор.
Для аналитика fe (feature engineering) — критическая часть работы с моделями. Даже не создавая сложные ML, вы постоянно трансформируете сырые данные в метрики и сегменты, которые используются в анализе. Навык один и тот же.
В этой статье — практические приёмы для создания фичей в pandas, которые работают для продуктовой аналитики: churn prediction, scoring, клиентская сегментация.
Фичи из дат
Дата — богатый источник сигналов. Из одного datetime можно достать десятки признаков.
import pandas as pd
df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek # 0=понедельник
df['day_of_month'] = df['date'].dt.day
df['hour'] = df['date'].dt.hour
df['is_weekend'] = df['day_of_week'].isin([5, 6])
df['is_month_start'] = df['date'].dt.is_month_start
df['is_month_end'] = df['date'].dt.is_month_end
df['quarter'] = df['date'].dt.quarterДля сезонности добавляют cyclic encoding:
import numpy as np
df['day_sin'] = np.sin(2 * np.pi * df['day_of_week'] / 7)
df['day_cos'] = np.cos(2 * np.pi * df['day_of_week'] / 7)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)Cyclic encoding — чтобы модель понимала, что воскресенье (6) и понедельник (0) — «близкие» дни, а не «далёкие». Линейная модель без этого думает, что различие между ними 6, что неверно.
Age/tenure-фичи
Время между датами — одна из самых ценных фичей:
df['days_since_signup'] = (pd.Timestamp.now() - df['signup_date']).dt.days
df['days_since_last_purchase'] = (pd.Timestamp.now() - df['last_purchase']).dt.days
df['tenure_months'] = ((pd.Timestamp.now() - df['signup_date']).dt.days / 30).astype(int)Для churn prediction days_since_last_event — обычно топ-1 по важности. Пользователь, который зашёл вчера — не уйдёт. Кто не заходил месяц — кандидат.
Aggregate features по группам
Частый паттерн — обогатить строку характеристиками группы. Например, для каждого заказа добавить среднюю сумму заказов пользователя:
df['user_avg_order'] = df.groupby('user_id')['amount'].transform('mean')
df['user_total_orders'] = df.groupby('user_id')['order_id'].transform('count')
df['user_max_order'] = df.groupby('user_id')['amount'].transform('max')
# Отклонение текущего заказа от среднего
df['amount_deviation'] = df['amount'] - df['user_avg_order']
df['amount_zscore'] = (df['amount'] - df['user_avg_order']) / df.groupby('user_id')['amount'].transform('std')transform возвращает Series той же длины, что и DataFrame. Значение для каждой строки — агрегат по её группе. Идеально для обогащения.
Cumulative features
Сколько раз пользователь покупал ДО текущей покупки:
df = df.sort_values(['user_id', 'created_at'])
df['order_num'] = df.groupby('user_id').cumcount() + 1
df['prev_total_spent'] = df.groupby('user_id')['amount'].cumsum() - df['amount']
df['days_since_prev_order'] = df.groupby('user_id')['created_at'].diff().dt.dayscumcount — порядковый номер в группе. Первый заказ пользователя = 1, второй = 2.
cumsum() - current — сумма всех предыдущих, исключая текущий. Полезно для churn: модель видит, сколько клиент потратил до этой точки.
diff() на created_at — время с предыдущего события. Для retention/churn модели — один из самых важных сигналов.
Rolling features
Скользящие окна — сигналы тренда:
df = df.sort_values(['user_id', 'created_at'])
df['rolling_7d_orders'] = (
df.groupby('user_id')['amount']
.rolling(7, min_periods=1).count()
.reset_index(0, drop=True)
)
df['rolling_30d_revenue'] = (
df.groupby('user_id')['amount']
.rolling(30, min_periods=1).sum()
.reset_index(0, drop=True)
)Активность пользователя за последние 7 и 30 дней. Убывающий rolling 7d — сигнал приближающегося churn.
Работа с фичами — классика middle-уровня. В тренажёре Карьерник есть задачи на pandas и analytics, включая построение моделей churn и scoring.
Категориальные фичи
Для моделей категории нужно закодировать.
Label encoding — каждой категории уникальный integer. Просто, но плохо для линейных моделей (они думают, что 3 > 2 > 1 в «порядковом» смысле).
df['category_encoded'] = df['category'].astype('category').cat.codesOne-hot encoding — один столбец на каждую категорию:
df = pd.get_dummies(df, columns=['category'], prefix='cat')
# Результат: cat_A, cat_B, cat_C — по 0/1Проблема — для колонок с высокой кардинальностью (тысячи значений) создаётся тысячи столбцов. Неэффективно.
Target encoding — замена категории средним значением target:
# Для train
target_map = df.groupby('category')['target'].mean().to_dict()
df['category_target'] = df['category'].map(target_map)Мощная техника для категорий с высокой кардинальностью. Но осторожно с data leakage — target_map должен считаться только на train, не на test.
Frequency encoding — замена на частоту:
freq_map = df['category'].value_counts(normalize=True).to_dict()
df['category_freq'] = df['category'].map(freq_map)Частые категории получают большие значения. Редкие — маленькие.
Binning числовых
Иногда полезно превратить числовой признак в категориальный:
df['age_group'] = pd.cut(df['age'], bins=[0, 18, 35, 55, 100], labels=['child', 'young', 'middle', 'old'])
df['price_quartile'] = pd.qcut(df['price'], q=4, labels=['cheap', 'medium', 'expensive', 'premium'])cut — по явным границам. qcut — по квантилям (равные по размеру группы).
Помогает нелинейным моделям (trees) или если бизнес-логика имеет разные пороги.
Строковые фичи
Из текстовых полей можно извлекать структурированные признаки:
df['email_domain'] = df['email'].str.split('@').str[1]
df['email_is_gmail'] = df['email_domain'] == 'gmail.com'
df['name_length'] = df['name'].str.len()
df['has_numbers_in_name'] = df['name'].str.contains(r'\d')Для product analytics из url или page_title часто достаются полезные сигналы.
Interaction features
Комбинирование двух признаков даёт новую информацию:
df['price_per_item'] = df['total_amount'] / df['items_count']
df['days_per_order'] = df['tenure_days'] / df['total_orders']
df['revenue_per_day'] = df['lifetime_revenue'] / df['tenure_days']Такие фичи часто важнее сырых. Например, revenue_per_day лучше предсказывает churn, чем отдельные revenue и tenure.
Lag features
Для time series — значение N шагов назад:
df = df.sort_values(['user_id', 'date'])
df['prev_day_revenue'] = df.groupby('user_id')['revenue'].shift(1)
df['revenue_7d_ago'] = df.groupby('user_id')['revenue'].shift(7)
df['change_vs_yesterday'] = df['revenue'] - df['prev_day_revenue']
df['change_vs_week_ago'] = df['revenue'] - df['revenue_7d_ago']Модель видит тренд — растёт или падает активность пользователя.
Missing indicators
NULL — тоже сигнал. Создаём флаг «было NULL»:
df['email_was_missing'] = df['email'].isna().astype(int)
df['email'] = df['email'].fillna('unknown')Два столбца: original (с заполненными NULL) и флаг. Модель может учиться, что отсутствие email — сигнал (например, fraud).
Ratio features
Отношения — всегда полезно:
df['conversion_rate'] = df['purchases'] / df['visits']
df['refund_rate'] = df['refunds'] / df['purchases']
df['engagement_score'] = df['sessions'] / df['days_active']Часто A/B информативнее, чем A и B по отдельности.
Domain-specific фичи
Лучшие фичи — из знания продукта. Для e-commerce:
df['avg_items_per_session'] = df['total_items'] / df['total_sessions']
df['discount_usage_rate'] = df['discounted_orders'] / df['total_orders']
df['weekend_shopping_ratio'] = df['weekend_orders'] / df['total_orders']Такие фичи требуют понимания бизнеса. Без этого данные говорят о случайных корреляциях, а не о реальных факторах.
Feature selection
Много фичей — не всегда лучше. После создания проверьте:
# Корреляция с target
correlations = df.corr()['target'].sort_values(ascending=False)
print(correlations.head(20))
# Корреляции между features (убрать дубликаты)
feature_corr = df.drop('target', axis=1).corr()
# Найти пары с |r| > 0.9 — убрать одну из нихДля ML-моделей потом — feature importance через Random Forest или SHAP.
Pipeline
Для production:
def make_features(df):
df = df.copy()
# Дата
df['days_since_signup'] = (pd.Timestamp.now() - df['signup_date']).dt.days
# Agregates
df['user_avg_order'] = df.groupby('user_id')['amount'].transform('mean')
# Ratios
df['revenue_per_day'] = df['lifetime_revenue'] / df['days_since_signup'].replace(0, 1)
return df
df_features = make_features(df)Инкапсулировать в функцию. Переиспользовать для train и inference. Документировать, что каждая фича означает.
Читайте также
FAQ
Сколько фичей оптимально?
Для линейных моделей — 10-30 после feature selection. Для деревьев/бустингов — до сотен. Больше не всегда лучше.
Data leakage — как избежать?
Считать target-encoded и aggregates только на train. На test/prod — применять сохранённые mapping. Не использовать будущие данные в создании фич.
Категории с высокой кардинальностью?
Target encoding или frequency encoding. One-hot не работает для тысяч значений.
Auto feature engineering — годится?
Featuretools и подобные инструменты автоматизируют создание. Полезны как стартовая точка, но сгенерированные фичи обычно хуже domain-specific.