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.days

cumcount — порядковый номер в группе. Первый заказ пользователя = 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.codes

One-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.