🔴 Сложный ⏱️ 55 минут

Feature engineering

📋 Содержание урока

Feature engineering 🛠️

Английское словосочетание Feature engineering переводится на русский как «конструирование признаков», и дальше в этом уроке будет использоваться именно русский вариант. За скромным названием скрывается, вероятно, самый недооценённый навык во всём машинном обучении. Начинающие обычно фокусируются на выборе модели: линейная регрессия или градиентный бустинг, kNN или случайный лес. Опытные практики почти единогласно говорят другое — качество признаков, которые ты подаёшь на вход модели, чаще определяет итоговое качество прогноза, чем то, какой именно алгоритм ты выбрал или насколько тщательно подобрал его гиперпараметры.

Формулируется эта идея старой инженерной поговоркой из мира вычислительной техники: garbage in, garbage out — «мусор на входе, мусор на выходе». Если признаки не несут полезного сигнала о целевой переменной, никакая модель, даже самая мощная из тех, что разобраны в этом курсе, не сможет предсказывать хорошо: градиентный бустинг (урок 318) с идеальными гиперпараметрами на плохих признаках проиграет простой линейной регрессии (урок 310) на хорошо сконструированных признаках. И наоборот — удачно придуманный признак иногда даёт больший прирост качества, чем смена алгоритма целиком.

Конструирование признаков — это процесс превращения сырых, часто неудобных для модели данных в представление, которое эту закономерность обнажает. Модель линейной регрессии не умеет работать со строкой "2024-03-08 09:15:00" напрямую, а модель kNN (урок 314) измеряет расстояния между точками — и если один признак измеряется в тысячах, а другой в единицах, то геометрия расстояний окажется искажена ещё до того, как модель успеет чему-то научиться. Устранение подобных препятствий, а не подбор самого «умного» алгоритма, — и есть основное ежедневное занятие специалиста по данным. Стандартный набор инструментов для этой работы — библиотеки pandas (для работы с таблицами и датами) и sklearn.preprocessing (для кодирования, масштабирования и других преобразований) — они встретятся почти в каждом примере этого урока.

Урок разберёт четыре большие группы техник: обработку категориальных признаков (унитарное, порядковое и целевое кодирование), обработку числовых признаков (масштабирование и биннинг), создание новых признаков из существующих (полиномиальные признаки, взаимодействия, признаки из даты и времени, агрегации по временным рядам) и работу с пропущенными значениями. Этим уроком завершается блок «Основы Machine Learning» (уроки 301–325) — дальше курс переходит к нейронным сетям, но всё, что здесь будет разобрано про признаки, продолжит работать и там: даже самая глубокая нейросеть учится быстрее и надёжнее на осмысленно подготовленных данных.

История

Термин garbage in, garbage out («мусор на входе, мусор на выходе») появился задолго до машинного обучения — ещё на заре программируемых компьютеров, в 1950-х годах, когда инженеры объясняли клиентам, что программа не способна выдать разумный результат, если ей подать на вход бессмысленные данные: компьютер не «исправляет» ошибки во входной информации, он честно превращает их в такие же ошибочные вычисления. С появлением статистического и машинного обучения эта же идея получила новое, куда более тонкое прочтение: даже технически корректные, без единой ошибки данные могут быть «мусором» для конкретной задачи, если они не содержат признаков, которые действительно связаны с тем, что модель пытается предсказать.

Ключевую роль конструирования признаков для практического машинного обучения ясно сформулировал исследователь Педро Домингос в статье 2012 года «A Few Useful Things to Know about Machine Learning» (в переводе — «Несколько полезных вещей, которые стоит знать о машинном обучении»), опубликованной в журнале Communications of the ACM. Один из главных тезисов этой статьи — то, что успех или провал конкретного ML-проекта решает не выбор алгоритма, а то, насколько удачно подобраны и сконструированы признаки: по наблюдению автора, специалисты, потратившие основное время на осмысленное конструирование признаков, а не на перебор моделей, систематически получают более сильные результаты.

Соревновательное машинное обучение 2010-х годов на платформе Kaggle стало массовой практической иллюстрацией этого тезиса. В решениях призёров снова и снова повторялся один и тот же паттерн: победители не изобретали новый алгоритм, а находили нетривиальный признак — например, «сколько дней прошло с последней покупки клиента» или «отношение текущей цены товара к средней цене за последний месяц», — который резко улучшал метрику качества на конкретной задаче. Именно из этой практики выросло устойчивое правило: прежде чем тратить время на перебор моделей и подбор гиперпараметров, стоит потратить время на вопрос, какие признаки вообще есть в данных и какие из них ещё можно создать.

Категориальные признаки: как объяснить машине, что такое «цвет»

Интуиция

Практически любая модель машинного обучения — от линейной регрессии до градиентного бустинга — внутри себя оперирует числами: складывает их, умножает на веса, сравнивает пороги. Но огромная часть реальных данных приходит в виде категорий: цвет товара, город клиента, уровень образования, способ оплаты. Модель не понимает строку "красный" — ей нужно число или набор чисел, которые эту категорию однозначно и полезно описывают. Задача кодирования категориальных признаков — придумать такое числовое представление, чтобы модель могла им воспользоваться, не внося при этом ложных предположений о данных, которых на самом деле нет.

Здесь принципиально важно первое различие, с которого начинается выбор техники: есть ли у категорий естественный порядок. Цвета «красный», «синий», «зелёный» — категории номинальные: между ними нет отношения «больше» или «меньше», сравнивать их бессмысленно. А уровни образования «школьное», «бакалавриат», «магистратура», «аспирантура» — категории порядковые (ordinal): между ними есть естественная упорядоченность, и это отношение порядка несёт полезную информацию, которую стоит сохранить в числовом представлении.

Техника

Три способа кодирования категорий. Унитарное кодирование (one-hot encoding) превращает категориальный признак с $K$ уникальными значениями в $K$ (или $K-1$, если один столбец опускают как базовый) бинарных столбцов — по одному на категорию, где $1$ стоит в столбце своей категории и $0$ во всех остальных; подходит для номинальных признаков без порядка. Порядковое кодирование (ordinal encoding) заменяет каждую категорию целым числом $0, 1, 2, \dots$ в соответствии с осмысленным порядком категорий; подходит только когда порядок действительно существует и задан явно, а не по алфавиту. Кодирование через целевую переменную (target encoding) заменяет категорию средним значением целевой переменной среди объектов этой категории: $\mathrm{TE}(c) = \frac{1}{n_c}\sum_{i:\,x_i = c} y_i$, где $n_c$ — число объектов категории $c$; компактно работает с высокой кардинальностью (сотнями и тысячами категорий), но требует осторожности из-за риска утечки данных.

Разбор примеров

Пример 1 (унитарное кодирование признака «цвет_товара»). В интернет-магазине одежды признак «цвет» принимает значения без естественного порядка:

id цвет
1 красный
2 синий
3 зелёный
4 красный

После унитарного кодирования:

id цвет_красный цвет_синий цвет_зелёный
1 1 0 0
2 0 1 0
3 0 0 1
4 1 0 0
import pandas as pd

df = pd.DataFrame({"id": [1, 2, 3, 4],
                    "цвет": ["красный", "синий", "зелёный", "красный"]})
df_encoded = pd.get_dummies(df, columns=["цвет"], prefix="цвет")

Если закодировать «красный»=1, «синий»=2, «зелёный»=3 одним числовым столбцом (как это делает порядковое кодирование), модель линейной регрессии решит, что «зелёный» в полтора раза «больше» «синего» — утверждение, лишённое смысла для цвета. Унитарное кодирование этого ложного порядка не создаёт.

Пример 2 (порядковое кодирование признака «уровень_образования»). Здесь порядок между категориями реален и важен:

id уровень_образования
1 школьное
2 магистратура
3 бакалавриат
4 аспирантура

После порядкового кодирования с явно заданным порядком:

id уровень_образования_код
1 0
2 2
3 1
4 3
from sklearn.preprocessing import OrdinalEncoder

order = [["школьное", "бакалавриат", "магистратура", "аспирантура"]]
encoder = OrdinalEncoder(categories=order)
df["уровень_образования_код"] = encoder.fit_transform(df[["уровень_образования"]])

Здесь важно явно передать порядок категорий (categories=order) — если этого не сделать, OrdinalEncoder по умолчанию упорядочит категории по алфавиту, и «аспирантура» окажется меньше «бакалавриата», что разрушит смысл кодирования.

Пример 3 (кодирование через целевую переменную для признака «город», с предупреждением про утечку). Пусть в датасете есть признак «город» с сотнями уникальных значений и бинарная целевая переменная «совершил_покупку»:

город покупки клиентов (0/1) среднее (target encoding)
Москва 1, 1, 0, 1 0,75
Санкт-Петербург 0, 1 0,50
Казань 1 1,00
target_means = df_train.groupby("город")["покупка"].mean()
df_train["город_te"] = df_train["город"].map(target_means)
df_test["город_te"] = df_test["город"].map(target_means)  # та же карта, посчитанная на train

Значение для Казани, равное $1{,}00$, посчитано всего по одному наблюдению — это не реальная закономерность, а шум, но модель воспримет его как абсолютно надёжный сигнал. Ровно так же опасно применять groupby().mean() ко всему датасету до разбиения на обучающую и тестовую выборки (урок 305): в этом случае среднее по каждому городу в обучающей выборке будет частично посчитано по объектам, которые физически принадлежат тестовой выборке, — целевая переменная теста незаметно «утекает» в признак, который видит модель на обучении, и оценка качества станет завышенной. Корректный порядок обратный: сначала разбиение на train/test (или фолды кросс-валидации, урок 306), затем вычисление средних значений только по train, и лишь потом — применение этой уже готовой карты соответствий к test.

Почему это важно

Выбор техники кодирования — не формальность, а решение с прямыми последствиями для качества модели. Унитарное кодирование признака с тысячей уникальных категорий создаёт тысячу новых разреженных столбцов — для линейных моделей и особенно для kNN (урок 314), где важна геометрия расстояний между точками, это резко увеличивает размерность пространства и «разбавляет» вклад остальных признаков. Порядковое кодирование, применённое к признаку без реального порядка, навязывает модели ложную структуру, которую она будет наивно использовать. А кодирование через целевую переменную, при всей своей компактности, — единственная из трёх техник, способная напрямую привести к утечке данных, если не соблюдать правильный порядок вычислений относительно разбиения на выборки.

Числовые признаки: масштабирование и биннинг

Интуиция

Числовой признак — не значит «готовый к использованию» признак. Площадь квартиры измеряется в десятках и сотнях квадратных метров, а число комнат — единицами; годовой доход — в десятках и сотнях тысяч, а возраст — двузначным числом лет. Модели, которые явно опираются на расстояния или дисперсию в пространстве признаков, чувствительны к этой разнице единиц измерения гораздо сильнее, чем кажется на первый взгляд: для kNN (урок 314) евклидово расстояние между точками фактически определяется тем признаком, у которого самый большой числовой размах, а для PCA (урок 323) первая главная компонента стремится совпасть с направлением наибольшей необработанной дисперсии — и если один признак измеряется в тысячах единиц, а другой в единицах, PCA невольно опишет в первую очередь колебания именно первого признака, даже если реальная, содержательная структура данных совсем другая.

Масштабирование решает именно эту проблему — приводит все числовые признаки к сопоставимому диапазону значений, не искажая при этом относительный порядок и форму распределения внутри каждого признака. Биннинг (дискретизация) решает смежную, но отдельную задачу: превращает непрерывный числовой признак в набор дискретных интервалов, когда важна не точная величина, а принадлежность к диапазону.

Техника

Стандартизация, нормализация и биннинг. Стандартизация (StandardScaler) переводит признак в шкалу со средним $0$ и стандартным отклонением $1$: $z = \dfrac{x - \mu}{\sigma}$, где $\mu$ и $\sigma$ вычисляются по обучающей выборке. Нормализация по диапазону (MinMaxScaler) линейно переводит значения признака в фиксированный интервал, обычно $[0, 1]$: $x' = \dfrac{x - \min}{\max - \min}$. Биннинг (дискретизация) заменяет непрерывное значение признака на номер интервала (бина), в который оно попадает, согласно заранее заданным или вычисленным границам.

Разбор примеров

Пример 1 (kNN: как немасштабированный признак «съедает» расстояние). Пусть датасет описывает квартиры признаками «площадь» (кв. м) и «число_комнат», а по всей выборке среднее и стандартное отклонение равны $\mu_{\text{площадь}}=70$, $\sigma_{\text{площадь}}=30$, $\mu_{\text{комнаты}}=3$, $\sigma_{\text{комнаты}}=1$. Возьмём две квартиры:

площадь комнаты
A 100 2
B 40 4

Без масштабирования: $d(A,B)=\sqrt{(100-40)^2+(2-4)^2}=\sqrt{3600+4}=\sqrt{3604}\approx 60{,}03$ — различие в числе комнат ($2$ комнаты) практически не влияет на итоговое расстояние на фоне разницы в площади ($60$ кв. м).

После стандартизации: $z_A^{\text{площадь}}=\frac{100-70}{30}=1{,}0$, $z_A^{\text{комнаты}}=\frac{2-3}{1}=-1{,}0$; $z_B^{\text{площадь}}=\frac{40-70}{30}=-1{,}0$, $z_B^{\text{комнаты}}=\frac{4-3}{1}=1{,}0$. Теперь $d(A,B)=\sqrt{(1{,}0-(-1{,}0))^2+((-1{,}0)-1{,}0)^2}=\sqrt{4+4}=\sqrt{8}\approx 2{,}83$, и оба признака вносят в это расстояние одинаковый по масштабу вклад — ровно то поведение, которое ожидается от алгоритма ближайших соседей.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train[["площадь", "комнаты"]])
X_test_scaled = scaler.transform(X_test[["площадь", "комнаты"]])  # transform, не fit_transform!

Пример 2 (PCA: как масштаб признаков искажает главные компоненты). Пусть датасет описывает людей признаками «рост_см» (диапазон примерно $150$–$200$) и «доход_тыс_руб» (диапазон примерно $30$–$300$). Раскид значений дохода в необработанных единицах в разы больше раскида роста, поэтому необработанная дисперсия признака «доход» намного превышает дисперсию признака «рост» — просто из-за выбора единиц измерения, а не из-за реальной значимости признака. Без масштабирования первая главная компонента PCA (урок 323) почти полностью совпадёт с осью «доход», как бы объявляя её «главным направлением изменчивости данных», хотя содержательная причина может быть совсем не в этом. После стандартизации оба признака получают одинаковую дисперсию $1$, и PCA находит направления, основанные на реальной корреляционной структуре данных, а не на случайности выбора единиц измерения (метры и рубли вместо, скажем, сантиметров и тысяч рублей).

from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

X_scaled = StandardScaler().fit_transform(X[["рост_см", "доход_тыс_руб"]])
pca = PCA(n_components=2).fit(X_scaled)

Пример 3 (биннинг возраста в интерпретируемые группы). Пусть признак «возраст» непрерывен, а его связь с целевой переменной нелинейна и не монотонна — например, вероятность отклика на маркетинговую рассылку выше у молодёжи и у пенсионеров, чем у людей среднего возраста:

id возраст
1 17
2 42
3 65
4 28

После биннинга по границам $[0, 18, 35, 60, 100]$:

id возраст возрастная_группа
1 17 подросток
2 42 средний
3 65 старший
4 28 молодой
bins = [0, 18, 35, 60, 100]
labels = ["подросток", "молодой", "средний", "старший"]
df["возрастная_группа"] = pd.cut(df["возраст"], bins=bins, labels=labels)

Биннинг полезен в первую очередь линейным моделям (урок 310, урок 313): линейная регрессия описывает только монотонную зависимость от непрерывного возраста, а после разбиения на группы каждая группа получает собственный, независимый вес — и немонотонная закономерность становится доступной модели без перехода к полиномиальным признакам.

Почему это важно

Масштабирование — не эстетическая деталь, а необходимое условие корректной работы целого класса алгоритмов: kNN (урок 314), метод опорных векторов (урок 319), K-means (урок 320), PCA и t-SNE (уроки 323–324), а также любая модель, обучаемая градиентным спуском (урок 285), — все они чувствительны к масштабу входных признаков, и результат без масштабирования может радикально отличаться от результата с ним. Древовидные модели (уроки 316–318) устроены иначе: решающее дерево ищет пороги отдельно по каждому признаку и не сравнивает признаки друг с другом напрямую, поэтому масштабирование для них не обязательно — но привычка масштабировать числовые признаки по умолчанию редко вредит и часто спасает от неожиданных сюрпризов при смене модели.

Создание новых признаков: превращаем сырые данные в сигнал

Интуиция

Иногда самый ценный признак — тот, которого изначально не было в данных вовсе, а появился только после того, как кто-то подумал о задаче содержательно. Модель не обязана сама догадываться, что важно не абсолютное число комнат и не абсолютная площадь квартиры, а их отношение — площадь на одну комнату; не обязана сама вычислять день недели из строки с датой; не обязана сама понимать, что резкий скачок продаж за последнюю неделю значит больше, чем средние продажи за весь год. Всё это — конструирование новых признаков вручную, на основе понимания предметной области, и именно здесь чаще всего проявляется разница между посредственной и отличной моделью.

Техника

Четыре способа получить новые признаки. Полиномиальные признаки добавляют степени исходного признака ($x^2$, $x^3$, …), позволяя линейной по параметрам модели описывать нелинейные зависимости (урок 311). Признаки взаимодействия — это произведения или отношения двух и более исходных признаков ($x_1 \cdot x_2$ или $x_1 / x_2$), которые фиксируют эффект, не сводимый к сумме отдельных признаков. Агрегации по времени сворачивают историю наблюдений в одно число: скользящее среднее, скользящее стандартное отклонение, значение с задержкой (лаг). Извлечение признаков из даты и времени превращает единый временной штамп в отдельные, содержательно осмысленные столбцы: день недели, месяц, флаг выходного дня, флаг праздника.

Разбор примеров

Пример 1 (полиномиальные признаки, связь с уроком 311). В уроке 311 уже разбирался приём: для признака $x$ — «стаж работы в годах» — модель линейной регрессии не способна уловить эффект плато в зарплате при большом стаже, но после добавления $x^2$ прямая линия превращается в параболу, и модель ловит замедление роста:

стаж (x) $x^2$
2 4
5 25
10 100
from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X[["стаж"]])  # столбцы: [стаж, стаж^2]

Пример 2 (взаимодействие признаков: «площадь на комнату»). Пусть есть два признака квартиры — «площадь» и «число_комнат». По отдельности они не показывают, насколько просторна каждая комната, а именно эта величина сильно влияет на цену:

id площадь число_комнат площадь_на_комнату
1 60 4 15,0
2 60 2 30,0
df["площадь_на_комнату"] = df["площадь"] / df["число_комнат"]

Обе квартиры из таблицы имеют одинаковую общую площадь, но вторая, скорее всего, будет стоить дороже — в ней просторнее каждая комната. Признак «площадь_на_комнату» делает этот эффект явным для модели, которая иначе увидела бы лишь две отдельные и слабо информативные цифры.

Пример 3 (агрегации по времени для временного ряда продаж). Для предсказания продаж магазина на конкретный день часто полезнее не абсолютное значение за один день, а сглаженная динамика за последние дни:

дата продажи скользящее_среднее_3дня
2024-03-01 100
2024-03-02 120
2024-03-03 110 110,0
2024-03-04 130 120,0
2024-03-05 150 130,0
df["скользящее_среднее_3дня"] = df["продажи"].rolling(window=3).mean()
df["продажи_вчера"] = df["продажи"].shift(1)  # лаг-признак

Пример 4 (извлечение признаков из даты и времени). Единый временной штамп разбирается на несколько содержательных признаков:

timestamp день_недели месяц выходной праздник
2024-03-08 09:15:00 пятница 3 0 1

8 марта 2024 года выпадает на пятницу и одновременно является государственным праздником (Международный женский день) в России — модель, у которой есть отдельные признаки «день недели» и «праздник», способна учесть оба этих эффекта на активность пользователей, тогда как исходная строка с датой сама по себе для неё бессмысленна.

df["timestamp"] = pd.to_datetime(df["timestamp"])
df["день_недели"] = df["timestamp"].dt.day_name()
df["месяц"] = df["timestamp"].dt.month
df["выходной"] = df["timestamp"].dt.dayofweek.isin([5, 6]).astype(int)
df["праздник"] = df["timestamp"].dt.strftime("%m-%d").isin(["01-01", "03-08", "05-09"]).astype(int)

Почему это важно

Ни одна из этих техник не требует новых данных — все они извлекают дополнительный сигнал из уже собранной информации, просто представляя её в форме, которую модели легче использовать. Разница между «сырой строкой с датой» и явными признаками «день недели», «месяц», «праздник» — это разница между информацией, физически присутствующей в данных, и информацией, которую модель реально способна увидеть и использовать при обучении.

Пропущенные значения: чего не хватает и что с этим делать

Интуиция

Реальные данные почти всегда содержат пропуски: клиент не указал доход в анкете, сенсор не записал показание из-за сбоя связи, пользователь пропустил необязательное поле формы. Большинство моделей машинного обучения не умеют работать с пропусками напрямую и либо выдадут ошибку, либо (в случае некоторых древовидных реализаций) обработают их особым внутренним образом. Самое простое решение — удалить строки или столбцы с пропусками — часто оказывается и самым расточительным: если пропуски раскиданы по разным столбцам, удаление всех неполных строк способно уничтожить большую часть датасета.

Техника

Заполнение пропусков. Импутация — это замена пропущенного значения на вычисленную оценку: для числового признака обычно берут среднее или медиану по обучающей выборке (медиана устойчивее к выбросам), для категориального — моду (самое частое значение). Дополнительно к самому заполненному значению часто добавляют отдельный бинарный признак-индикатор пропуска, который сохраняет информацию о том, что значение изначально отсутствовало, даже после того как пропуск был чем-то заполнен.

Разбор примеров

Пример 1 (медиана против среднего при выбросе в доходе). Пусть в столбце «доход_тыс» пять наблюдений, одно пропущено, а среди известных есть выброс:

id доход_тыс
1 48
2 50
3 52
4 NaN
5 1000

Среднее по известным значениям: $\frac{48+50+52+1000}{4}=287{,}5$. Медиана по тем же значениям (отсортированы: $48, 50, 52, 1000$): $\frac{50+52}{2}=51$. Заполнение медианой ($51$) даёт правдоподобную типичную величину, тогда как заполнение средним ($287{,}5$) целиком определяется единственным выбросом и никак не отражает типичного клиента.

median_income = df_train["доход_тыс"].median()
df_train["доход_тыс"] = df_train["доход_тыс"].fillna(median_income)
df_test["доход_тыс"] = df_test["доход_тыс"].fillna(median_income)  # то же значение, посчитанное на train

Пример 2 (мода для категориального признака «способ_оплаты»).

id способ_оплаты
1 карта
2 карта
3 NaN
4 наличные
5 карта

Мода — «карта» (встречается чаще остальных), поэтому пропуск в строке 3 заполняется значением «карта».

mode_payment = df_train["способ_оплаты"].mode()[0]
df_train["способ_оплаты"] = df_train["способ_оплаты"].fillna(mode_payment)

Пример 3 (индикатор пропуска, когда сам факт пропуска несёт информацию). Пусть в анкете пользователя доход указан не всегда, а нежелание указывать доход чаще встречается именно среди пользователей с очень высоким доходом (они просто не хотят его раскрывать) — то есть пропуск не случаен, он статистически связан с самой скрытой величиной:

id доход_тыс (исходно) доход_тыс (после импутации медианой) доход_пропущен
1 55 55 0
2 NaN 51 (медиана) 1
3 48 48 0
df["доход_пропущен"] = df["доход_тыс"].isna().astype(int)
df["доход_тыс"] = df["доход_тыс"].fillna(df_train["доход_тыс"].median())

Если просто заполнить пропуск медианой и не добавить индикатор, модель потеряет полезный сигнал: сам факт того, что человек скрыл доход, может быть связан с целевой переменной сильнее, чем любое правдоподобное числовое заполнение.

Почему это важно

Наивное удаление всех строк с пропусками особенно опасно, когда пропуски распределены не случайно, — например, если пользователи, не указавшие доход, в среднем реже совершают покупки: удаление таких строк не просто уменьшает выборку, а систематически смещает её, выбрасывая целый содержательный сегмент пользователей. Комбинация «импутация плюс индикатор пропуска» — практический компромисс, который сохраняет и размер выборки, и информацию о самом факте отсутствия данных.

Кратко про автоматизированный feature engineering

Часть работы по конструированию признаков поддаётся автоматизации. Библиотеки вроде featuretools реализуют так называемый Deep Feature Synthesis — автоматический перебор агрегаций (сумма, среднее, количество, максимум) между связанными таблицами: например, для клиента автоматически считаются признаки «сумма всех его заказов», «среднее время между заказами», «количество различных категорий товаров, которые он покупал». Сами древовидные модели — решающие деревья, случайный лес, градиентный бустинг (уроки 316–318) — тоже частично автоматизируют часть этой работы, неявно находя полезные пороги и взаимодействия признаков в процессе обучения, без явного конструирования вручную.

Автоматизация ускоряет перебор множества комбинаций и полезна как источник идей, но не отменяет необходимость понимать предметную область: автоматически сгенерированные признаки нужно проверять на утечку данных (особенно связанную с временными границами — не используются ли в агрегации данные из будущего), а самые сильные, «прорывные» признаки в реальных проектах и соревнованиях чаще всего по-прежнему придумывает человек, разбирающийся в задаче.

Практика: 30 заданий

Базовые задания (1–10)

Задание 1: Признак «способ_доставки» имеет 4 уникальных значения: курьер, почта, самовывоз, постамат. Сколько новых бинарных столбцов создаст стандартное унитарное кодирование без удаления базового столбца?


Задание 2: Категории уровня подготовки: «начальный», «средний», «продвинутый», «эксперт» — имеют естественный порядок. Присвой им порядковые коды от 0 до 3.


Задание 3: По выборке студентов среднее по баллу $\mu=75$, стандартное отклонение $\sigma=5$. Найди стандартизированное значение (z-score) для студента с баллом $x=82$.


Задание 4: Признак «оценка» принимает значения от $\min=60$ до $\max=100$. Найди значение после MinMax-нормализации для $x=82$.


Задание 5: Заданы границы бинов возраста: $[0, 18, 35, 60, 100]$ с метками «подросток», «молодой», «средний», «старший». В какой бин попадёт возраст $42$?


Задание 6: Столбец «доход_тыс»: $[50,\ 52,\ \text{NaN},\ 48,\ 1000]$. Найди значение, которым медианная импутация заполнит пропуск.


Задание 7: Столбец «способ_оплаты»: ['visa', 'mastercard', 'visa', NaN, 'visa', 'mir']. Каким значением заполнит пропуск импутация модой?


Задание 8: Построй полиномиальные признаки степени 2 (со свободным членом) для $x=3$.


Задание 9: Квартира: число_комнат $=4$, площадь $=60$ кв. м. Вычисли признак взаимодействия «площадь_на_комнату».


Задание 10: Столбец «возраст»: $[25,\ \text{NaN},\ 31,\ \text{NaN},\ 40]$. Построй индикатор пропуска (1 — пропуск, 0 — значение есть).


Продвинутые задания (11–20)

Задание 11: По городам известна целевая переменная «покупка»: Москва — $[1,1,0,1]$, Санкт-Петербург — $[0,1]$, Казань — $[1]$. Посчитай target encoding (среднее target) для каждого города.


Задание 12: Специалист вычислил target encoding для признака «город» на всём датасете, а затем разбил данные на train и test. AUC на кросс-валидации получился $0{,}95$, но в продакшене модель показала заметно худший результат. Объясни причину.


Задание 13: Столбец: $[10, 12, 14, 16, 18]$. Найди среднее, стандартное отклонение и стандартизированное значение для $x=18$.


Задание 14: Столбец с выбросом: $[5, 6, 7, 8, 100]$. Примени MinMax-нормализацию. Что происходит с четырьмя «нормальными» значениями?


Задание 15: Точки A(площадь=100, комнаты=2) и B(площадь=40, комнаты=4). Известно: $\mu_{площадь}=70$, $\sigma_{площадь}=30$, $\mu_{комнаты}=3$, $\sigma_{комнаты}=1$. Сравни евклидово расстояние между A и B до и после стандартизации.


Задание 16: Признаки «рост_см» ($150$–$200$) и «доход_тыс_руб» ($30$–$300$) подаются в PCA без масштабирования. Какой признак, скорее всего, будет доминировать в первой главной компоненте и почему?


Задание 17: Дата и время: 2024-03-08 09:15:00. Определи день недели, месяц, флаг выходного и флаг праздника (для России).


Задание 18: Продажи по дням: $[100, 120, 110, 130, 150]$. Найди скользящее среднее с окном 3 для последнего (пятого) дня.


Задание 19: Признак «браузер» с частотами: Chrome — 8000, Safari — 1500, Firefox — 400, Edge — 80, Opera — 15, Internet Explorer — 5 (всего 10000 наблюдений). Порог для группировки редких категорий — 1% от общего числа (100 наблюдений). Какие категории нужно объединить в «Other»?


Задание 20: Признак «зарплата_тыс»: $[45, 50, 48, 52, 500]$. Какую статистику для импутации выбрать — среднее или медиану — и почему?


Задания-челленджи (21–30)

Задание 21: Дан сырой датасет с полями: id, дата_регистрации, город, доход (с пропусками), количество_покупок, статус_подписки (целевая переменная). Опиши правильный порядок шагов конструирования признаков, избегая утечки данных.


Задание 22: Target encoding для «город» посчитан один раз на всей обучающей выборке перед запуском 5-фолдовой кросс-валидации (урок 306). Тестовая выборка при этом не тронута. Есть ли здесь утечка данных, и если да — в чём именно?


Задание 23: Для категории «Казань» с $n_c=1$ наблюдением и средним $\overline{y}_c=1{,}0$, при глобальном среднем target $\overline{y}=0{,}5$ и параметре сглаживания $m=10$, вычисли сглаженное target encoding.


Задание 24: Признак «почтовый_индекс» имеет 1000 уникальных значений. Сравни унитарное кодирование и target encoding для этого признака по компромиссам, которые они создают.


Задание 25: Для двух признаков $x_1, x_2$ построй полиномиальные признаки (со свободным членом) степени 2, включая взаимодействие. Сколько всего получится столбцов?


Задание 26: В анкете признак «доход» пропущен чаще именно у людей с высоким доходом (они не хотят его раскрывать) — пропуск не случаен. Почему простая импутация медианой может ухудшить модель, и что стоит сделать дополнительно?


Задание 27: Возраста: $[18, 19, 20, 22, 25, 30, 45, 60, 65, 90]$. Сравни равноширинный биннинг (3 равных по ширине интервала) и равночастотный / квантильный биннинг (3 бина с примерно равным числом наблюдений) по результату.


Задание 28: Объясни, почему StandardScaler нужно обучать (fit) только на train, а к test применять уже обученный scaler (transform), а не обучать его заново на test или на всём датасете сразу.


Задание 29: Нужно предсказать продажи магазина на завтрашний день по временному ряду. Опиши, какие лаговые и агрегированные признаки допустимо использовать, а какие приведут к заглядыванию в будущее (lookahead bias).


Задание 30: Инструмент автоматизированного конструирования признаков (например, featuretools) для связанных таблиц «клиенты» и «заказы» автоматически строит признаки вроде COUNT(заказы), SUM(заказы.сумма), MEAN(заказы.сумма) для каждого клиента. Какая задача при этом всё равно остаётся за человеком?

Частые ошибки

  • Считают target encoding по всему датасету до разбиения на train/test. Как показано в задании 12, это создаёт прямую утечку целевой переменной test в признаки, на которых обучается модель, — метрика на кросс-валидации получается завышенной, а поведение в продакшене заметно хуже (см. урок 305 про принципы честного разбиения выборок).

  • Обучают StandardScaler или MinMaxScaler на всём датасете, а не только на train. Тот же самый принцип утечки данных, разобранный в задании 28: параметры масштабирования должны определяться исключительно обучающей выборкой, иначе test перестаёт быть независимой оценкой качества.

  • Применяют унитарное кодирование к признаку с сотнями или тысячами уникальных категорий не глядя. Как показано в задании 24, это резко увеличивает размерность и особенно вредит моделям, чувствительным к геометрии признакового пространства (kNN, урок 314); разумная альтернатива — группировка редких категорий (задание 19) или аккуратно посчитанное target encoding.

  • Путают порядковое кодирование с унитарным для номинальных признаков без реального порядка. Присвоение чисел 0, 1, 2, … категориям вроде цвета или города навязывает модели ложное отношение «больше — меньше» там, где сравнивать категории физически бессмысленно.

  • Строят признаки для временных рядов, заглядывая в будущее. Скользящие агрегаты с center=True или статистики, посчитанные по всему ряду сразу, включают информацию, которая на момент реального прогноза ещё не существовала (задание 29) — на исторических данных такая модель покажет прекрасное качество, а в реальном последовательном применении откажет.

  • Заполняют пропуски одним и тем же способом для всех признаков без разбора причины пропуска. Если отсутствие значения статистически связано с самой целевой переменной (задание 26), простое заполнение медианой стирает этот сигнал — нужен дополнительный индикатор пропуска.

Главное запомнить

  • Качество признаков на практике часто важнее выбора алгоритма или тюнинга гиперпараметров — принцип garbage in, garbage out («мусор на входе, мусор на выходе») работает и для самых мощных моделей курса.

  • Для номинальных категорий без порядка используется унитарное кодирование (one-hot encoding), для категорий с естественным порядком — порядковое кодирование (ordinal encoding) с явно заданным порядком категорий.

  • Кодирование через целевую переменную (target encoding) компактно работает с высокой кардинальностью, но требует сглаживания и вычисления строго по train (или по фолдам кросс-валидации, урок 306) — иначе возникает утечка данных.

  • Масштабирование (StandardScaler, MinMaxScaler) критично для kNN (урок 314), PCA (урок 323), t-SNE (урок 324) и любых моделей, чувствительных к геометрии признакового пространства или обучаемых градиентным спуском.

  • Биннинг превращает непрерывный признак в дискретные интервалы, что особенно полезно линейным моделям при немонотонной зависимости от исходного признака.

  • Полиномиальные признаки (урок 311), взаимодействия и агрегации по времени превращают сырые столбцы в сигнал, который модель не способна извлечь самостоятельно из исходного представления данных.

  • Извлечение признаков из даты и времени (день недели, месяц, флаг выходного, флаг праздника) регулярно оказывается одним из самых недорогих и результативных приёмов конструирования признаков.

  • Пропущенные значения заполняются медианой (для числовых, особенно при выбросах) или модой (для категориальных); индикатор пропуска сохраняет сигнал, когда сам факт отсутствия значения неслучаен.

  • Все статистики для кодирования, масштабирования и импутации вычисляются только на train и затем применяются к test без пересчёта — это тот же принцип, что лежит в основе честного train/validation/test split (урок 305).

  • Автоматизированное конструирование признаков (featuretools и аналоги) ускоряет перебор комбинаций, но не заменяет проверку на утечку данных и понимание предметной области.

Связь с темами курса

Этим уроком завершается большой блок «Основы Machine Learning» (уроки 301–325) — и стоит оглянуться на пройденный путь целиком. Блок начался с базовых понятий: что такое машинное обучение и какие бывают типы задач (уроки 301–303), переобучение и недообучение (урок 304) и честная методология оценки — train/validation/test split и кросс-валидация (уроки 305–306), метрики качества, ROC-кривая и confusion matrix (уроки 307–309). Дальше курс прошёл через классические модели: линейную и полиномиальную регрессию, регуляризацию, логистическую регрессию, метод ближайших соседей, наивный байесовский классификатор, решающие деревья, случайный лес и градиентный бустинг (уроки 310–318), метод опорных векторов (урок 319), алгоритмы кластеризации — K-means, иерархическую кластеризацию, DBSCAN (уроки 320–322), и методы снижения размерности — PCA и t-SNE (уроки 323–324). Сегодняшний урок про конструирование признаков закрывает этот блок не случайно: все перечисленные модели получают на вход именно те признаки, которые ты научился готовить в этом уроке, — методология оценки моделей (уроки 304–309) без адекватных признаков теряет смысл, а хорошо сконструированные признаки способны заметно улучшить результат работы почти любой модели из уроков 310–324.

Материал этого урока напрямую опирается на кодирование категорий из примера с признаком «город» в уроке 305 (принципы честного разбиения выборок и предотвращения утечки данных), на понимание чувствительности kNN к масштабу признаков из урока 314 и на зависимость PCA от масштаба переменных из урока 323, а полиномиальные признаки прямо продолжают тему урока 311. Дальше курс переходит к принципиально новому классу моделей — нейронным сетям (урок 326 и далее). Важно понимать: конструирование признаков не теряет значения и там. Глубокие нейросети действительно умеют автоматически извлекать сложные представления из сырых данных — особенно из изображений, текста и звука, — но для табличных данных, с которыми ты работал на протяжении всего этого блока, аккуратно подготовленные признаки по-прежнему остаются одним из самых надёжных способов улучшить качество модели, и даже входной слой нейросети получает на вход именно признаки — масштабированные, закодированные и обогащённые ровно теми же приёмами, которые разобраны в этом уроке.

Интересные факты

  • В отчётах о победах на соревнованиях Kaggle команды нередко указывают, что тратили от 60 до 90 процентов всего рабочего времени именно на конструирование и отбор признаков, а не на подбор или настройку самой модели.

  • Фраза garbage in, garbage out («мусор на входе, мусор на выходе») старше машинного обучения примерно на семь десятилетий — она использовалась ещё применительно к первым программируемым компьютерам 1950-х годов, задолго до того, как появился сам термин «машинное обучение».

  • Один хорошо продуманный признак взаимодействия иногда заменяет собой десятки слоёв сложной модели: линейная регрессия с удачно подобранным признаком-произведением способна обойти по качеству нелинейную модель без него на той же задаче.

  • Библиотеки автоматизированного конструирования признаков, такие как featuretools, способны сгенерировать сотни кандидатов-признаков за считанные секунды — но отбор из них действительно полезных, не содержащих утечки данных признаков по-прежнему требует ручной проверки специалистом.

Лайфхаки

  • Перед выбором способа кодирования категориального признака сначала задай себе один вопрос: есть ли у категорий естественный порядок. Ответ сразу отсекает половину вариантов и экономит время на эксперименты.

  • Заведи привычку писать пайплайн sklearn (Pipeline с шагами импутации, кодирования, масштабирования) вместо разрозненных вызовов fit_transform — это автоматически защищает от случайного обучения статистик на test и делает код воспроизводимым.

  • Если категориальный признак имеет больше сотни уникальных значений, начинай с группировки редких категорий по порогу частоты (задание 19) — это часто улучшает качество ещё до выбора между унитарным и target encoding.

  • При любых сомнениях, не приведёт ли новый признак к утечке данных, задай контрольный вопрос: мог бы этот признак быть точно вычислен в момент реального предсказания, до того как стал известен фактический результат? Если нет — признак нужно переработать.

  • Не пытайся угадать самый мощный признак с первой попытки — заведи привычку быстро перебирать десяток гипотез (отношения признаков, агрегации, признаки из даты) и проверять их влияние на валидационную метрику, вместо того чтобы долго обдумывать один «идеальный» признак заранее.

Большой практический блок курса — «Основы Machine Learning», уроки 301–325, — на этом завершён. Ты прошёл путь от определения задачи и честной методологии оценки через десяток классических моделей до методов кластеризации, снижения размерности и, наконец, до инструментов подготовки признаков, которые делают все эти модели по-настоящему рабочими на реальных, неидеальных данных. Это солидный фундамент: понимание, зачем нужна валидационная выборка, как избежать утечки данных, когда масштабировать признаки и как из сырой строки с датой добыть содержательный сигнал, — навыки, которые не устаревают и продолжают работать в любой более сложной архитектуре. Дальше курс открывает новую большую тему — искусственные нейронные сети, где ты увидишь, как идеи, знакомые по линейной и логистической регрессии (уроки 310, 313) и по градиентному спуску (урок 285), складываются в модели, способные учиться на изображениях, тексте и звуке практически без ручного конструирования признаков. Но фундамент, который ты только что закрепил, — про честную оценку, про то, что подаёшь на вход модели, и про утечку данных (data leakage), которую нужно ловить на каждом шаге, — останется с тобой и там.

Понял тему? Закрепи в боте! 🚀

Попрактикуйся на задачах и получи персональные рекомендации от AI

💪 Начать тренировку
💬 Есть вопрос? Спроси бота!