Інженерія ознак (Feature Engineering) у Python: практичний посібник з pandas, scikit-learn та feature-engine

Повний практичний посібник з інженерії ознак у Python: числові трансформації, кодування категорій, обробка пропусків, автоматизація з feature-engine та Featuretools, побудова пайплайнів scikit-learn 1.8.

Що таке інженерія ознак і чому вона визначає долю вашої моделі

Інженерія ознак (feature engineering) — це процес створення, перетворення та відбору змінних із сирих даних, щоб моделі машинного навчання працювали краще. Звучить просто, правда? Але на практиці саме цей етап з'їдає більшу частину часу в будь-якому ML-проєкті — і саме він визначає до 80% кінцевої точності моделі. Вибір алгоритму? Це лише решта 20%.

У 2026 році інструментарій Python для роботи з ознаками став справді потужним. Scikit-learn дійшов до версії 1.8, feature-engine — до 1.9, і разом вони покривають практично будь-який сценарій, який можна собі уявити.

У цьому посібнику ми пройдемо повний цикл інженерії ознак: від числових трансформацій і кодування категорій до автоматизованого генерування ознак через Featuretools. Усі приклади працюють із pandas 3.x, scikit-learn 1.8 та feature-engine 1.9 — можете копіювати й запускати.

Підготовка середовища

Перш за все, встановіть потрібні бібліотеки:

pip install pandas scikit-learn feature-engine featuretools category_encoders

А тепер імпортуємо базові модулі, які знадобляться далі:

import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import (
    StandardScaler, MinMaxScaler, OneHotEncoder,
    OrdinalEncoder, PolynomialFeatures, FunctionTransformer
)
from feature_engine import encoding, imputation, transformation, creation

Для демонстрації створимо синтетичний датасет про нерухомість. Чому саме нерухомість? Бо тут є й числові, і категоріальні ознаки, і пропуски — ідеальний полігон для feature engineering:

np.random.seed(42)
n = 1000

df = pd.DataFrame({
    "area_m2": np.random.uniform(25, 200, n),
    "rooms": np.random.choice([1, 2, 3, 4, 5], n),
    "floor": np.random.randint(1, 26, n),
    "total_floors": np.random.randint(5, 30, n),
    "district": np.random.choice(
        ["Центр", "Поділ", "Оболонь", "Печерськ", "Троєщина"], n
    ),
    "condition": np.random.choice(
        ["без ремонту", "косметичний", "євроремонт", "дизайнерський"], n
    ),
    "build_year": np.random.randint(1960, 2025, n),
    "price_usd": np.random.uniform(20000, 300000, n),
})

# Додамо пропущені значення
df.loc[df.sample(frac=0.05).index, "area_m2"] = np.nan
df.loc[df.sample(frac=0.08).index, "condition"] = np.nan

Числові трансформації

Масштабування (Scaling)

Більшість алгоритмів ML — зокрема SVM, KNN, логістична регресія — чутливі до масштабу ознак. Якщо одна ознака вимірюється в тисячах, а інша — від 0 до 1, модель буде "бачити" переважно першу. Два найпоширеніші підходи:

  • StandardScaler — приводить ознаку до середнього 0 та стандартного відхилення 1 (так званий Z-score).
  • MinMaxScaler — масштабує значення до діапазону [0, 1].
from sklearn.preprocessing import StandardScaler, MinMaxScaler

scaler_z = StandardScaler()
df["area_z"] = scaler_z.fit_transform(df[["area_m2"]].fillna(df["area_m2"].median()))

scaler_mm = MinMaxScaler()
df["floor_norm"] = scaler_mm.fit_transform(df[["floor"]])

До речі, для деревоподібних моделей (Random Forest, XGBoost) масштабування не потрібне — ці алгоритми приймають рішення на основі порогів, а не відстаней. Тож не витрачайте на це час, якщо працюєте тільки з деревами.

Логарифмічна та степенева трансформація

Коли розподіл ознаки сильно скошений (а ціни та доходи скошені практично завжди), логарифмічна трансформація допомагає наблизити його до нормального. Це особливо важливо для лінійних моделей.

from feature_engine.transformation import LogTransformer, YeoJohnsonTransformer

# Логарифмічна трансформація
log_tf = LogTransformer(variables=["price_usd"])
df_log = log_tf.fit_transform(df.dropna(subset=["price_usd"]))

# Yeo-Johnson — працює і з від'ємними значеннями
yj_tf = YeoJohnsonTransformer(variables=["price_usd"])
df_yj = yj_tf.fit_transform(df.dropna(subset=["price_usd"]))

Feature-engine 1.9 також підтримує BoxCoxTransformer, PowerTransformer та ArcsinTransformer — вибирайте залежно від типу скошеності ваших даних.

Бінування (Binning / Discretisation)

Іноді корисно перетворити неперервну змінну на категоріальні інтервали. Це допомагає виявити нелінійні залежності, які модель інакше може пропустити.

from feature_engine.discretisation import (
    EqualFrequencyDiscretiser,
    EqualWidthDiscretiser,
    DecisionTreeDiscretiser,
)

# Рівночастотне бінування — кожний бін містить приблизно однакову кількість спостережень
eq_freq = EqualFrequencyDiscretiser(variables=["area_m2"], q=5)
df_binned = eq_freq.fit_transform(df.dropna(subset=["area_m2"]))

# Бінування на основі дерева рішень — оптимальні межі для цільової змінної
dt_disc = DecisionTreeDiscretiser(
    variables=["area_m2"],
    regression=True,
    scoring="neg_mean_squared_error"
)
df_dt = dt_disc.fit_transform(
    df.dropna(subset=["area_m2", "price_usd"]),
    y=df.dropna(subset=["area_m2", "price_usd"])["price_usd"]
)

Чесно кажучи, DecisionTreeDiscretiser — мій улюблений підхід до бінування. Він знаходить межі, які максимально корисні для прогнозування цільової змінної, а не просто ділить дані на рівні частини.

Кодування категоріальних ознак

One-Hot Encoding

Класика жанру — створюємо бінарний стовпець для кожної категорії. Добре підходить для номінальних змінних з невеликою кількістю унікальних значень (до 10–15):

# scikit-learn
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
encoded = ohe.fit_transform(df[["district"]])
ohe_df = pd.DataFrame(encoded, columns=ohe.get_feature_names_out())

# feature-engine — повертає DataFrame і не потребує окремого кроку
from feature_engine.encoding import OneHotEncoder as FEOneHotEncoder
fe_ohe = FEOneHotEncoder(variables=["district"], drop_last=True)
df_ohe = fe_ohe.fit_transform(df)

Ordinal Encoding

Для ознак із природним порядком (як-от стан ремонту: "без ремонту" < "косметичний" < "євроремонт" < "дизайнерський") використовуйте порядкове кодування:

from feature_engine.encoding import OrdinalEncoder as FEOrdinalEncoder

condition_order = ["без ремонту", "косметичний", "євроремонт", "дизайнерський"]

ord_enc = FEOrdinalEncoder(
    encoding_method="ordered",
    variables=["condition"]
)
df_ord = ord_enc.fit_transform(
    df.dropna(subset=["condition"]),
    y=df.dropna(subset=["condition"])["price_usd"]
)

Target Encoding (Mean Encoding)

Цей метод замінює категорію середнім значенням цільової змінної. Потужна штука, але є підступний нюанс — без регуляризації легко перенавчити модель:

from feature_engine.encoding import MeanEncoder

mean_enc = MeanEncoder(variables=["district"], smoothing=10)
df_mean = mean_enc.fit_transform(
    df.dropna(subset=["price_usd"]),
    y=df.dropna(subset=["price_usd"])["price_usd"]
)

У scikit-learn 1.8 з'явився TargetEncoder із вбудованою крос-валідаційною регуляризацією — він значно безпечніший у використанні:

from sklearn.preprocessing import TargetEncoder

target_enc = TargetEncoder(smooth="auto")
df["district_target"] = target_enc.fit_transform(
    df[["district"]],
    df["price_usd"]
)

Частотне кодування (Count / Frequency Encoding)

Простий і дієвий метод — замінюємо категорію частотою її появи в датасеті. Особливо добре працює з деревоподібними моделями:

from feature_engine.encoding import CountFrequencyEncoder

freq_enc = CountFrequencyEncoder(
    encoding_method="frequency",
    variables=["district"]
)
df_freq = freq_enc.fit_transform(df)

Обробка пропущених значень

Пропущені дані — це не просто технічна проблема. Часто сам факт відсутності значення несе інформацію (наприклад, продавці можуть не вказувати площу для маленьких квартир). Feature-engine надає гнучкий набір імп'ютерів для різних ситуацій:

from feature_engine.imputation import (
    MeanMedianImputer,
    CategoricalImputer,
    ArbitraryNumberImputer,
    AddMissingIndicator,
)

# Числові ознаки — заповнення медіаною
median_imp = MeanMedianImputer(
    imputation_method="median",
    variables=["area_m2"]
)

# Категоріальні ознаки — заповнення модою або спеціальною міткою
cat_imp = CategoricalImputer(
    imputation_method="missing",
    variables=["condition"],
    fill_value="невідомо"
)

# Додавання індикатора пропуску — окремий бінарний стовпець
indicator = AddMissingIndicator(variables=["area_m2", "condition"])
df_ind = indicator.fit_transform(df)

AddMissingIndicator — це той трюк, який варто запам'ятати. Він створює окремий бінарний стовпець, що позначає, чи було значення пропущеним. Модель зможе використати цю інформацію, якщо пропуски дійсно корелюють із цільовою змінною.

Створення нових ознак

Арифметичні та комбінаторні ознаки

Ось де починається справжня магія feature engineering. Домейнне знання — найпотужніший інструмент, який у вас є. Ніякий алгоритм не замінить розуміння предметної області.

Створімо ознаки, що мають реальний бізнес-сенс:

# Вік будинку
df["building_age"] = 2026 - df["build_year"]

# Ціна за квадратний метр
df["price_per_m2"] = df["price_usd"] / df["area_m2"]

# Відносний поверх
df["relative_floor"] = df["floor"] / df["total_floors"]

# Чи це останній поверх?
df["is_top_floor"] = (df["floor"] == df["total_floors"]).astype(int)

# Чи перший поверх?
df["is_ground_floor"] = (df["floor"] == 1).astype(int)

Зверніть увагу на relative_floor — 5-й поверх із 25 і 5-й із 5 це зовсім різні речі. Така ознака фіксує цю різницю, яку модель могла б і не вловити з сирих даних.

Feature-engine дозволяє автоматизувати створення математичних комбінацій:

from feature_engine.creation import MathFeatures

math_feat = MathFeatures(
    variables=["area_m2", "rooms"],
    func=["sum", "mean", "std", "min", "max"],
    missing_values="ignore"
)
df_math = math_feat.fit_transform(df.dropna(subset=["area_m2"]))

Поліноміальні та інтеракційні ознаки

Scikit-learn пропонує PolynomialFeatures для автоматичного створення поліноміальних і перехресних ознак:

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
num_cols = ["area_m2", "rooms", "floor"]
df_clean = df[num_cols].dropna()

poly_features = poly.fit_transform(df_clean)
poly_df = pd.DataFrame(
    poly_features,
    columns=poly.get_feature_names_out(num_cols)
)
print(poly_df.head())

Параметр interaction_only=True — важливий момент. Він генерує тільки перехресні добутки (area × rooms, area × floor тощо), без квадратів окремих ознак. Це суттєво зменшує розмірність і рідко шкодить якості.

Ознаки на основі дати та часу

Якщо у ваших даних є дати — не ігноруйте їх. Витягніть корисні компоненти:

from feature_engine.datetime import DatetimeFeatures

df["listing_date"] = pd.to_datetime("2026-01-01") + pd.to_timedelta(
    np.random.randint(0, 365, len(df)), unit="D"
)

dt_feat = DatetimeFeatures(
    variables=["listing_date"],
    features_to_extract=["month", "day_of_week", "quarter", "weekend"],
    drop_original=True
)
df_dt = dt_feat.fit_transform(df)

Місяць, день тижня, квартал, вихідний — кожен з цих компонентів може виявитися інформативним. Наприклад, ціни на нерухомість часто залежать від сезону.

Автоматизована інженерія ознак з Featuretools

Для складних реляційних даних є Featuretools 1.31 — бібліотека, що автоматично генерує десятки й сотні ознак за допомогою алгоритму Deep Feature Synthesis (DFS). Отже, давайте подивимося, як це працює:

import featuretools as ft

# Створюємо EntitySet
es = ft.EntitySet(id="real_estate")

df_ft = df.copy().reset_index().rename(columns={"index": "id"})
df_ft["id"] = df_ft["id"].astype(int)

es = es.add_dataframe(
    dataframe_name="listings",
    dataframe=df_ft,
    index="id",
    logical_types={
        "district": "Categorical",
        "condition": "Categorical",
    }
)

# Deep Feature Synthesis
feature_matrix, feature_defs = ft.dfs(
    entityset=es,
    target_dataframe_name="listings",
    max_depth=1,
    trans_primitives=["multiply_numeric", "divide_numeric", "add_numeric"],
    verbose=True
)

print(f"Згенеровано ознак: {feature_matrix.shape[1]}")
print(feature_matrix.head())

DFS рекурсивно застосовує примітиви трансформації та агрегації. Параметр max_depth контролює глибину: 1 — це прості трансформації, 2 — комбінації трансформацій (щось на кшталт SUM(LOG(price))). Будьте обережні з глибиною 3+ — кількість ознак зростає експоненційно, і більшість із них виявляться шумом.

Відбір ознак (Feature Selection)

Згенерували купу ознак? Тепер треба відібрати найінформативніші. Без цього кроку ви ризикуєте перенавчити модель і сповільнити навчання.

from feature_engine.selection import (
    DropCorrelatedFeatures,
    SmartCorrelatedSelection,
    SelectByShuffling,
)

# Видалення високо корельованих ознак
drop_corr = DropCorrelatedFeatures(
    variables=None,
    method="pearson",
    threshold=0.9
)

# Розумний відбір — зберігає ознаку з найвищою кореляцією до цілі
smart_sel = SmartCorrelatedSelection(
    variables=None,
    method="pearson",
    threshold=0.9,
    selection_method="variance"
)

Scikit-learn 1.8 теж має свої класичні методи відбору:

from sklearn.feature_selection import (
    SelectKBest, f_regression, mutual_info_regression
)

# Відбір 10 найкращих ознак за взаємною інформацією
selector = SelectKBest(score_func=mutual_info_regression, k=10)
# selector.fit_transform(X, y)

Мій підхід зазвичай такий: спочатку прибираю корельовані ознаки (поріг 0.9), потім використовую SelectByShuffling або важливість ознак з дерева — це дає непоганий баланс між простотою й якістю.

Повний пайплайн: збираємо все разом

А тепер найважливіше — інкапсулюємо весь процес у єдиний пайплайн scikit-learn. Це гарантує, що трансформації на тренувальних і тестових даних будуть ідентичними, і жодного витоку даних не станеться:

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import GradientBoostingRegressor
from feature_engine.imputation import MeanMedianImputer, CategoricalImputer
from feature_engine.encoding import OrdinalEncoder as FEOrdinalEncoder

# Визначаємо стовпці
num_cols = ["area_m2", "rooms", "floor", "total_floors", "build_year"]
cat_cols = ["district", "condition"]

# Числовий пайплайн
num_pipeline = Pipeline([
    ("imputer", MeanMedianImputer(imputation_method="median")),
    ("scaler", StandardScaler()),
])

# Категоріальний пайплайн
cat_pipeline = Pipeline([
    ("imputer", CategoricalImputer(imputation_method="missing")),
    ("encoder", FEOrdinalEncoder(encoding_method="ordered")),
])

# Об'єднання
preprocessor = ColumnTransformer([
    ("num", num_pipeline, num_cols),
    ("cat", cat_pipeline, cat_cols),
])

# Повний пайплайн з моделлю
full_pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("model", GradientBoostingRegressor(n_estimators=200, random_state=42)),
])

# Навчання
X = df[num_cols + cat_cols]
y = df["price_usd"]
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

full_pipeline.fit(X_train, y_train)
score = full_pipeline.score(X_test, y_test)
print(f"R² на тестовій вибірці: {score:.4f}")

Що це дає на практиці:

  • Відтворюваність — параметри всіх трансформацій зберігаються всередині пайплайна.
  • Відсутність витоку данихfit виконується лише на тренувальних даних, тестові бачать тільки transform.
  • Простоту деплою — весь пайплайн серіалізується через joblib або pickle одним рядком коду.

Найкращі практики інженерії ознак у 2026 році

  1. Починайте з EDA — завжди вивчайте розподіли, кореляції та пропуски перед створенням ознак. Без цього ви працюватимете наосліп.
  2. Використовуйте домейнне знання — найкращі ознаки створює не код, а розуміння предметної області. Поговоріть із доменним експертом — це окупиться.
  3. Уникайте витоку даних — ніколи не використовуйте інформацію з тестової вибірки для навчання трансформацій. Це класична помилка, яка дає завищені метрики.
  4. Автоматизуйте рутину — feature-engine та Featuretools чудово справляються зі стандартними операціями, звільняючи вас для творчої роботи.
  5. Валідуйте кожну ознаку — перевіряйте через крос-валідацію, чи нова ознака дійсно покращує метрику. Не все, що виглядає логічно, працює на практиці.
  6. Документуйте логіку — записуйте, чому ви створили ту чи іншу ознаку. Через місяць ви самі не згадаєте.
  7. Контролюйте розмірність — після генерації ознак обов'язково проведіть відбір. Прокляття розмірності — цілком реальна проблема, особливо на невеликих датасетах.

FAQ

Чим інженерія ознак відрізняється від відбору ознак?

Інженерія ознак — це створення нових змінних із сирих даних (кодування, трансформації, комбінації). Відбір ознак — наступний етап, де з усього набору обирають найінформативніші. Спочатку генеруємо якомога більше потенційно корисних ознак, потім відсіюємо зайві. Ці два процеси доповнюють один одного.

Яке кодування категоріальних змінних обрати?

Залежить від ситуації. Для номінальних змінних із малою кардинальністю (до 10–15 категорій) — One-Hot Encoding. Для високої кардинальності (сотні чи тисячі категорій) — Target Encoding або Frequency Encoding. Для порядкових змінних (рівень освіти, стан ремонту) — Ordinal Encoding з правильним порядком значень.

Чи потрібне масштабування для всіх моделей?

Ні, не для всіх. Деревоподібні моделі (Random Forest, XGBoost, LightGBM, CatBoost) нечутливі до масштабу. А ось для моделей на основі відстаней (KNN, SVM), лінійних моделей та нейронних мереж масштабування критичне.

Як автоматизувати feature engineering у Python?

Використовуйте Featuretools для автоматичної генерації ознак із реляційних даних через Deep Feature Synthesis. Для стандартних трансформацій (кодування, імп'ютація, бінування) — feature-engine, що повністю сумісна з пайплайнами scikit-learn. Для часових рядів варто подивитися на TSFresh.

Скільки ознак потрібно для гарної моделі?

Універсальної відповіді немає — все залежить від обсягу даних і складності задачі. Є емпіричне правило: кількість тренувальних прикладів має перевищувати кількість ознак хоча б у 5–10 разів. Тобто якщо у вас 1000 рядків і 200 ознак — це явно забагато. Використовуйте методи відбору для зменшення розмірності.

Про Автора Editorial Team

Our team of expert writers and editors.