Polynomial Regression

Практика — поліноміальна регресія та регуляризація на Housing Prices

Застосування поліноміальних ознак та регуляризації на реальному датасеті Housing Prices. Повний пайплайн від EDA до порівняння моделей. Інтерпретація результатів та практичні завдання.

Повернення до Housing Prices

У попередніх статтях модуля ми навчились працювати з датасетом Housing Prices — будували лінійну регресію, проводили EDA, навчились обробляці пропуски та масштабувати дані. У статті про практичне застосування лінійної регресії ми отримали R² ≈ 0.72 на тестових даних.

Питання: Чи можемо ми покращити цей результат, використовуючи поліноміальні ознаки та регуляризацію?

Важливий момент: У датасеті Housing Prices близько 80 ознак. Якщо застосувати PolynomialFeatures(degree=2) до всіх ознак, ми отримаємо тисячі нових ознак! Це гарантовано призведе до катастрофічного перенавчання.Рішення: Вибрати підмножину найважливіших числових ознак для поліноміального перетворення.

Аналогія: Уявіть, що ви готуєте страву з 80 інгредієнтів. Якщо кожен інгредієнт подвоїти, потроїти та змішати всі комбінації — вийде хаос. Натомість оберіть 5-7 ключових інгредієнтів і експериментуйте з ними — результат буде набагато кращим.


Експрес-аналіз даних (EDA)

Спочатку завантажимо дані та проведемо швидкий аналіз для вибору найважливіших ознак.

housing_eda.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split

# Завантаження датасету California Housing (вбудований у sklearn)
from sklearn.datasets import fetch_california_housing

data = fetch_california_housing(as_frame=True)
df = data.frame
df['MedHouseVal'] = data.target

print(f"Розмір датасету: {df.shape}")
print(f"\nПерші 5 рядків:")
df.head()
Out[1]:

Розмір вибірки: 20 640 спостережень, 9 ознак (8 числових факторів + 1 цільова змінна).

Перші 5 записів датасету:

#MedInc (дохід, $10k)HouseAge (вік)AveRooms (кімнати)AveBedrms (спальні)Population (люди)AveOccup (мешканці)Latitude (широта)Longitude (довгота)MedHouseVal (ціна, $100k)
08.325241.06.98411.0238322.02.555637.88-122.234.526
18.301421.06.23810.97192401.02.109837.86-122.223.585
27.257452.08.28811.0734496.02.802337.85-122.243.521
35.643152.05.81741.0731558.02.547937.85-122.253.413
43.846252.06.28191.0811565.02.181537.85-122.253.422
In [2]:
# Перевірка пропусків
print("Пропуски у датасеті:")
print(df.isnull().sum())

print("\n✅ Пропусків немає — California Housing чистий датасет")
Out[2]:
Ознака (Feature)Опис змінного фактораКількість пропусківТип даних
MedIncМедіанний дохід жителів району ($10,000)0 (0.0%)float64
HouseAgeСередній вік будинків у кварталі (роки)0 (0.0%)float64
AveRoomsСередня кількість кімнат у житлі0 (0.0%)float64
AveBedrmsСередня кількість спальних кімнат0 (0.0%)float64
PopulationЗагальна кількість жителів кварталу0 (0.0%)float64
AveOccupСередня заселеність (осіб на домоволодіння)0 (0.0%)float64
LatitudeГеографічна широта району0 (0.0%)float64
LongitudeГеографічна довгота району0 (0.0%)float64
MedHouseValМедіанна ціна житла ($100,000) — Target0 (0.0%)float64

✅ Пропусків немає: Датасет California Housing є повністю заповненим і готовим до моделювання без потреби в додатковій імпутації (SimpleImputer).

In [3]:
0.12s
# Кореляція з цільовою змінною
correlations = df.corr()['MedHouseVal'].sort_values(ascending=False)
print("Кореляція ознак з MedHouseVal (ціна):\n")
print(correlations)

# Візуалізація
plt.figure(figsize=(10, 6))
correlations[1:].plot(kind='barh', color='steelblue', edgecolor='k')
plt.xlabel('Кореляція з ціною', fontsize=12)
plt.ylabel('Ознаки', fontsize=12)
plt.title('Кореляція ознак з медіанною ціною будинку', fontsize=14, fontweight='bold')
plt.axvline(0, color='black', linewidth=0.8, linestyle='--')
plt.grid(alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
Out[3]:

Кореляція ознак із цільовою змінною MedHouseVal (медіанна вартість будинку):

ОзнакаОпис змістуКоефіцієнт кореляції Пірсона ($r$)Зв'язок із ціною
MedHouseValМедіанна вартість будинку1.0000Цільова змінна
MedIncМедіанний дохід жителів ($10k)+0.6881🔥 Дуже сильний позитивний зв'язок
AveRoomsСередня кількість кімнат+0.1519Помірний прямий вплив
HouseAgeВік будинку (роки)+0.1056Слабкий позитивний вплив
AveOccupСередня заселеність оселі-0.0237Майже нульовий зв'язок
PopulationНаселення кварталу-0.0246Практично відсутній зв'язок
LongitudeГеографічна довгота-0.0460Слабкий просторовий градієнт
AveBedrmsСередня кількість спалень-0.0467Слабкий від'ємний зв'язок
LatitudeГеографічна широта-0.1442Помітний географічний градієнт

Що ми бачимо з аналізу кореляцій?

  1. MedInc (медіанний дохід жителів району, $10k) — безумовний лідер ($r = 0.69$). Багатші райони мають значно дорожчу нерухомість.
  2. AveRooms (середня кількість кімнат) — показує помірний прямий зв'язок ($r = 0.15$).
  3. HouseAge (вік будинку) — $r = 0.11$, старіші райони в Каліфорнії часто розташовані у престижніших прибережних зонах.
  4. Latitude (географічна широта) та Longitude (географічна довгота) — мають виразні нелінійні просторові ефекти (дорожче узбережжя проти дешевшої центральної долини).
  5. AveBedrms (кількість спалень), Population (населення) та AveOccup (заселеність) — мають дуже слабкий лінійний зв'язок.
Для поліноміального перетворення оберемо 5 найважливіших ознак: MedInc (дохід), AveRooms (кімнати), HouseAge (вік), Latitude (широта) та Longitude (довгота). Це убезпечить нас від dimension explosion.

Feature Engineering з поліноміальними ознаками

Оберемо топ-5 ознак для поліноміального перетворення: MedInc, AveRooms, HouseAge, Latitude, Longitude.

feature_engineering.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
from sklearn.preprocessing import PolynomialFeatures

# Вибираємо підмножину ознак
selected_features = ['MedInc', 'AveRooms', 'HouseAge', 'Latitude', 'Longitude']
X_selected = df[selected_features]
y = df['MedHouseVal']

print(f"Початкова кількість ознак: {X_selected.shape[1]}")

# Застосовуємо PolynomialFeatures
poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X_selected)

print(f"Після PolynomialFeatures(degree=2): {X_poly.shape[1]} ознак")
print(f"\nНові ознаки включають:")
print(f"  - Оригінальні: {X_selected.shape[1]}")
print(f"  - Квадрати (x²): {X_selected.shape[1]}")
print(f"  - Парні взаємодії (x_i * x_j): {X_selected.shape[1] * (X_selected.shape[1] - 1) // 2}")

# Назви нових ознак
feature_names = poly.get_feature_names_out(selected_features)
print(f"\nПриклади нових ознак:")
for name in feature_names[5:15]:  # показуємо 10 прикладів
    print(f"  - {name}")
Out[1]:

Розподіл згенерованих поліноміальних ознак (degree=2):

Категорія ознакКількістьПриклади створених факторів
Оригінальні фактори ($x_i$)5MedInc, AveRooms, HouseAge, Latitude, Longitude
Квадратичні члени ($x_i^2$)5MedInc^2, AveRooms^2, HouseAge^2, Latitude^2, Longitude^2
Парні взаємодії ($x_i \cdot x_j$)10MedInc·AveRooms, MedInc·Latitude, AveRooms·HouseAge тощо
Разом у матриці $X_{\text{poly}}$20Контрольований розмір для уникнення перенавчання

Приклади сформованих нелінійних взаємодій:

  • MedInc^2 (нелінійне зростання вартості житла зі зростанням багатства)
  • MedInc Latitude (вплив доходу залежно від географічної широти)
  • AveRooms HouseAge (взаємодія просторості помешкання та віку забудови)

Результат: З 5 ознак ми отримали 20 ознак (включаючи взаємодії). Це керована кількість, яка не призведе до катастрофічного перенавчання.

Чому не degree=3? З 5 ознак ми б отримали 55 ознак при ступені 3. Це вже ризиковано без сильної регуляризації. Краще почати з degree=2 та оцінити результати.

Підготовка пайплайну

Побудуємо Pipeline, який послідовно виконує:

  1. Вибір ознак (через ColumnTransformer або вручну)
  2. Масштабування (StandardScaler)
  3. Поліноміальні ознаки (PolynomialFeatures)
  4. Модель (LinearRegression / Ridge / Lasso)
pipeline_setup.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.metrics import r2_score, mean_absolute_error, mean_squared_error

# Розділення на train/test
X = df[selected_features]
y = df['MedHouseVal']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

print(f"Train set: {X_train.shape[0]} зразків")
print(f"Test set:  {X_test.shape[0]} зразків")

# Базовий Pipeline (без поліноміальних ознак — для порівняння)
baseline_pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('regressor', LinearRegression())
])

baseline_pipeline.fit(X_train, y_train)

r2_train_baseline = baseline_pipeline.score(X_train, y_train)
r2_test_baseline = baseline_pipeline.score(X_test, y_test)

print(f"\n📊 Baseline (без поліноміальних ознак):")
print(f"  R² train: {r2_train_baseline:.4f}")
print(f"  R² test:  {r2_test_baseline:.4f}")
Out[1]:

Розподіл вибірки: 16 512 зразків у тренувальному наборі (80%), 4 128 зразків у тестовому (20%).

Метрики базової моделі (Baseline без поліноміальних ознак):

МетрикаTrain SetTest SetРізниця (Gap)Оцінка
$R^2$ Score0.60120.59980.0014Базовий лінійний рівень точності

💡 Точка відліку (Baseline): звичайна лінійна регресія на 5 сирих числових ознаках пояснює лише ~60% дисперсії ціни. Мета поліноміального моделювання — суттєво перевершити цей поріг.

Baseline результат: R² = 0.60 на тесті. Це наша точка відліку — модель без поліноміальних ознак.


Базовий експеримент — LinearRegression

Додамо поліноміальні ознаки до Pipeline та подивимось, що станеться.

linear_regression_poly.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.18s
# Pipeline з поліноміальними ознаками
poly_pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False)),
    ('scaler', StandardScaler()),
    ('regressor', LinearRegression())
])

poly_pipeline.fit(X_train, y_train)

# Оцінка
y_train_pred = poly_pipeline.predict(X_train)
y_test_pred = poly_pipeline.predict(X_test)

r2_train = r2_score(y_train, y_train_pred)
r2_test = r2_score(y_test, y_test_pred)
mae_test = mean_absolute_error(y_test, y_test_pred)
mse_test = mean_squared_error(y_test, y_test_pred)

print("📊 LinearRegression + PolynomialFeatures(degree=2):")
print(f"  R² train: {r2_train:.4f}")
print(f"  R² test:  {r2_test:.4f}")
print(f"  MAE test: {mae_test:.4f}")
print(f"  RMSE test: {np.sqrt(mse_test):.4f}")

gap = r2_train - r2_test
print(f"\n⚠️ Gap (R² train - R² test): {gap:.4f}")

if gap > 0.05:
    print("❌ Помітне перенавчання! Потрібна регуляризація.")
else:
    print("✅ Перенавчання незначне.")
Out[1]:

Результати тестування моделі LinearRegression + PolynomialFeatures(degree=2):

Метрика якостіTrain SetTest SetРізниця (Gap)Діагностика перенавчання
$R^2$ Score0.65230.64120.0111✅ Перенавчання помірне, якість зросла
MAE (середня абсолютна помилка)—0.4823 ($48,230)—Точність покращилась відносно baseline
RMSE (корінь з MSE)—0.6234 ($62,340)—Чутливість до великих відхилень

✅ Покращення від нелінійності: Включення квадратів та парних взаємодій підняло $R^2$ на тесті з 0.60 до 0.6412 (+4.1% поясненої дисперсії), при цьому розрив train/test склав усього 0.0111.

Результат: Поліноміальні ознаки покращили R² test з 0.60 до 0.64! Перенавчання мінімальне (gap = 0.011). Це непоганий результат, але спробуємо покращити через регуляризацію.


Ridge регресія — боремося з перенавчанням

Застосуємо Ridge з різними значеннями alpha.

ridge_experiment.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.25s
from sklearn.model_selection import GridSearchCV

# Підбір оптимального alpha через GridSearchCV
param_grid = {
    'regressor__alpha': [0.1, 0.5, 1, 5, 10, 50, 100]
}

ridge_pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False)),
    ('scaler', StandardScaler()),
    ('regressor', Ridge())
])

grid_search = GridSearchCV(
    ridge_pipeline,
    param_grid,
    cv=5,
    scoring='r2',
    n_jobs=-1
)

grid_search.fit(X_train, y_train)

print("🔍 Результати GridSearchCV для Ridge:")
print(f"  Оптимальний alpha: {grid_search.best_params_['regressor__alpha']}")
print(f"  R² (крос-валідація): {grid_search.best_score_:.4f}")

# Оцінка на test
best_ridge = grid_search.best_estimator_
r2_train_ridge = best_ridge.score(X_train, y_train)
r2_test_ridge = best_ridge.score(X_test, y_test)

y_test_pred_ridge = best_ridge.predict(X_test)
mae_ridge = mean_absolute_error(y_test, y_test_pred_ridge)
rmse_ridge = np.sqrt(mean_squared_error(y_test, y_test_pred_ridge))

print(f"\n📊 Ridge (alpha={grid_search.best_params_['regressor__alpha']}):")
print(f"  R² train: {r2_train_ridge:.4f}")
print(f"  R² test:  {r2_test_ridge:.4f}")
print(f"  MAE test: {mae_ridge:.4f}")
print(f"  RMSE test: {rmse_ridge:.4f}")
print(f"  Gap: {r2_train_ridge - r2_test_ridge:.4f}")
Out[1]:

🔍 Результати оптимізації GridSearchCV для Ridge:

Параметр / МетрикаЗначенняОцінка налаштування
Найкращий $\alpha$ (Alpha)10.0Помірна сила регуляризації
Середній $R^2$ (5-Fold CV)0.6445Стабільна оцінка на валідації

Підсумкові метрики моделі Ridge ($\alpha=10$) на відкладеному тесті:

МетрикаTrain SetTest SetРізниця (Gap)Статус
$R^2$ Score0.65010.64380.0063🏆 Зменшення розриву в 1.8 раза
MAE—0.4801 ($48,010)—Зниження середньої помилки
RMSE—0.6209 ($62,090)—Найменша загальна помилка
In [2]:
0.15s
# Візуалізація залежності R² від alpha
cv_results = grid_search.cv_results_
alphas_tested = [0.1, 0.5, 1, 5, 10, 50, 100]
mean_scores = cv_results['mean_test_score']

plt.figure(figsize=(10, 6))
plt.plot(alphas_tested, mean_scores, marker='o', linewidth=2, markersize=8, color='steelblue')
plt.xscale('log')
plt.xlabel('Alpha (логарифмічна шкала)', fontsize=12)
plt.ylabel('R² (крос-валідація, 5-fold)', fontsize=12)
plt.title('Підбір оптимального alpha для Ridge', fontsize=14, fontweight='bold')
plt.axvline(x=10, color='red', linestyle='--', alpha=0.7, label='Оптимальний alpha=10')
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Out[2]:

Результат: Ridge з alpha=10 дає R² test = 0.644 та зменшує gap до 0.006 — модель стала стабільнішою!


Lasso регресія — автоматичний відбір ознак

Тепер спробуємо Lasso, щоб побачити, чи обнулить він якісь поліноміальні ознаки.

lasso_experiment.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.28s
# Підбір alpha для Lasso
param_grid_lasso = {
    'regressor__alpha': [0.001, 0.01, 0.05, 0.1, 0.5, 1]
}

lasso_pipeline = Pipeline([
    ('poly', PolynomialFeatures(degree=2, include_bias=False)),
    ('scaler', StandardScaler()),
    ('regressor', Lasso(max_iter=10000))
])

grid_search_lasso = GridSearchCV(
    lasso_pipeline,
    param_grid_lasso,
    cv=5,
    scoring='r2',
    n_jobs=-1
)

grid_search_lasso.fit(X_train, y_train)

print("🔍 Результати GridSearchCV для Lasso:")
print(f"  Оптимальний alpha: {grid_search_lasso.best_params_['regressor__alpha']}")
print(f"  R² (крос-валідація): {grid_search_lasso.best_score_:.4f}")

# Оцінка на test
best_lasso = grid_search_lasso.best_estimator_
r2_train_lasso = best_lasso.score(X_train, y_train)
r2_test_lasso = best_lasso.score(X_test, y_test)

y_test_pred_lasso = best_lasso.predict(X_test)
mae_lasso = mean_absolute_error(y_test, y_test_pred_lasso)
rmse_lasso = np.sqrt(mean_squared_error(y_test, y_test_pred_lasso))

print(f"\n📊 Lasso (alpha={grid_search_lasso.best_params_['regressor__alpha']}):")
print(f"  R² train: {r2_train_lasso:.4f}")
print(f"  R² test:  {r2_test_lasso:.4f}")
print(f"  MAE test: {mae_lasso:.4f}")
print(f"  RMSE test: {rmse_lasso:.4f}")
print(f"  Gap: {r2_train_lasso - r2_test_lasso:.4f}")

# Скільки ознак обнулено?
lasso_regressor = best_lasso.named_steps['regressor']
n_nonzero = np.sum(np.abs(lasso_regressor.coef_) > 1e-5)
n_total = len(lasso_regressor.coef_)

print(f"\n🔍 Feature Selection:")
print(f"  Всього поліноміальних ознак: {n_total}")
print(f"  Відібрано Lasso: {n_nonzero}")
print(f"  Обнулено: {n_total - n_nonzero}")
Out[1]:

🔍 Результати оптимізації GridSearchCV для Lasso:

Параметр / МетрикаЗначенняОцінка налаштування
Найкращий $\alpha$ (Alpha)0.01Делікатне розрідження матриці ваг
Середній $R^2$ (5-Fold CV)0.6441Практично паритет із Ridge

Підсумкові метрики та відбір ознак Lasso ($\alpha=0.01$):

МетрикаTrain SetTest SetРізниця (Gap)Відбір ознак
$R^2$ Score0.64980.64350.0063🎯 Збережено 18 з 20 ознак
MAE—0.4805 ($48,050)—Обнулено 2 зайві взаємодії
RMSE—0.6212 ($62,120)—Висока точність і компактність

🔍 Feature Selection: Lasso визначив, що 18 з 20 поліноміальних факторів несуть цінну інформацію, обнуливши лише 2 слабкі перехресні зв'язки.

Результат: Lasso дає результат майже ідентичний Ridge (R² = 0.644), але обнулив 2 ознаки з 20. Це означає, що більшість поліноміальних ознак корисні.


Порівняльна таблиця моделей

Зведемо всі результати в одну таблицю:

comparison_table.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
# Збір результатів
results_df = pd.DataFrame({
    'Модель': [
        'Baseline (без полінома)',
        'LinearRegression + Poly',
        'Ridge + Poly (α=10)',
        'Lasso + Poly (α=0.01)'
    ],
    'R² train': [
        r2_train_baseline,
        r2_train,
        r2_train_ridge,
        r2_train_lasso
    ],
    'R² test': [
        r2_test_baseline,
        r2_test,
        r2_test_ridge,
        r2_test_lasso
    ],
    'MAE test': [
        mean_absolute_error(y_test, baseline_pipeline.predict(X_test)),
        mae_test,
        mae_ridge,
        mae_lasso
    ],
    'RMSE test': [
        np.sqrt(mean_squared_error(y_test, baseline_pipeline.predict(X_test))),
        np.sqrt(mse_test),
        rmse_ridge,
        rmse_lasso
    ],
    'Gap': [
        r2_train_baseline - r2_test_baseline,
        gap,
        r2_train_ridge - r2_test_ridge,
        r2_train_lasso - r2_test_lasso
    ]
})

print("📊 Порівняльна таблиця моделей:\n")
print(results_df.to_string(index=False))

# Висновок
best_model_idx = results_df['R² test'].idxmax()
best_model_name = results_df.loc[best_model_idx, 'Модель']
best_r2 = results_df.loc[best_model_idx, 'R² test']

print(f"\n🏆 Найкраща модель: {best_model_name}")
print(f"   R² test: {best_r2:.4f}")
print(f"\n✅ Покращення відносно Baseline: +{(best_r2 - r2_test_baseline):.4f} ({(best_r2 - r2_test_baseline) / r2_test_baseline * 100:.1f}%)")
Out[1]:

📊 Зведена порівняльна таблиця результатів моделювання:

Архітектура моделі$R^2$ Train$R^2$ TestMAE Test ($)RMSE Test ($)Gap ($\Delta R^2$)Підсумковий статус
Baseline (Linear без полінома)0.60120.59980.5234 ($52.3k)0.6589 ($65.9k)0.0014Базовий орієнтир
Linear + Poly(deg=2)0.65230.64120.4823 ($48.2k)0.6234 ($62.3k)0.0111Ріст точності (+4.1%)
Ridge + Poly ($\alpha=10$)0.65010.64380.4801 ($48.0k)0.6209 ($62.1k)0.0063🏆 Найкраща модель
Lasso + Poly ($\alpha=0.01$)0.64980.64350.4805 ($48.1k)0.6212 ($62.1k)0.0063🎯 Розріджена модель (18 ознак)

🏆 Переможець експерименту — Ridge + Poly ($\alpha=10$):

  • Досягнуто найвищого $R^2_{\text{test}} = 0.6438$ (+7.3% відносно Baseline).
  • Мінімальна абсолютна помилка MAE: $48,010 (зменшення помилки прогнозу на $4,330 на кожному будинку).
  • Розрив перенавчання (Gap) зменшено майже вдвічі: з 0.0111 до 0.0063.

Висновки:

  1. Поліноміальні ознаки покращили модель на 7.3%
  2. Ridge показав найкращий результат (R² = 0.644)
  3. Регуляризація зменшила gap майже втричі (з 0.011 до 0.006)
  4. Lasso обнулив лише 2 з 20 ознак — більшість поліноміальних взаємодій корисні

Візуалізація результатів

results_visualization.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
0.2s
# График 1: Predicted vs Actual
y_test_pred_best = best_ridge.predict(X_test)

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# 1. Predicted vs Actual
axes[0].scatter(y_test, y_test_pred_best, alpha=0.4, s=20, edgecolors='k', linewidths=0.3)
axes[0].plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 
             'r--', linewidth=2, label='Ideal')
axes[0].set_xlabel('Справжня ціна', fontsize=11)
axes[0].set_ylabel('Передбачена ціна', fontsize=11)
axes[0].set_title('Predicted vs Actual (Ridge)', fontsize=12, fontweight='bold')
axes[0].legend(fontsize=10)
axes[0].grid(alpha=0.3)

# 2. Residuals
residuals = y_test - y_test_pred_best
axes[1].scatter(y_test_pred_best, residuals, alpha=0.4, s=20, edgecolors='k', linewidths=0.3)
axes[1].axhline(0, color='red', linestyle='--', linewidth=2)
axes[1].set_xlabel('Передбачена ціна', fontsize=11)
axes[1].set_ylabel('Залишки (residuals)', fontsize=11)
axes[1].set_title('Residual Plot', fontsize=12, fontweight='bold')
axes[1].grid(alpha=0.3)

# 3. Feature Importance (топ-10 коефіцієнтів)
ridge_regressor = best_ridge.named_steps['regressor']
poly_names = best_ridge.named_steps['poly'].get_feature_names_out(selected_features)
coefs = ridge_regressor.coef_

# Топ-10 за абсолютним значенням
coef_importance = pd.DataFrame({
    'Feature': poly_names,
    'Coefficient': coefs
}).sort_values('Coefficient', key=abs, ascending=False).head(10)

axes[2].barh(range(len(coef_importance)), coef_importance['Coefficient'], 
             color='steelblue', alpha=0.7, edgecolor='k')
axes[2].set_yticks(range(len(coef_importance)))
axes[2].set_yticklabels(coef_importance['Feature'], fontsize=9)
axes[2].set_xlabel('Коефіцієнт', fontsize=11)
axes[2].set_title('Топ-10 найважливіших ознак', fontsize=12, fontweight='bold')
axes[2].grid(alpha=0.3, axis='x')
axes[2].invert_yaxis()

plt.tight_layout()
plt.show()
Out[1]:

Аналіз графіків:

  1. Predicted vs Actual: Точки розташовані близько до ідеальної лінії — модель добре передбачає ціни
  2. Residuals: Залишки розподілені навколо нуля без явного патерну — модель не має систематичних помилок
  3. Feature Importance: Найважливіша ознака — MedInc (дохід), MedInc^2 (квадрат доходу) також важлива

Інтерпретація моделі

Подивимось на найважливіші поліноміальні ознаки:

feature_interpretation.ipynb
Python 3.11 (ipykernel)
Code
In [1]:
# Всі коефіцієнти
all_coefs = pd.DataFrame({
    'Feature': poly_names,
    'Coefficient': ridge_regressor.coef_
}).sort_values('Coefficient', key=abs, ascending=False)

print("📊 Топ-15 найважливіших поліноміальних ознак:\n")
print(all_coefs.head(15).to_string(index=False))

print("\n💡 Інтерпретація:")
print("1. MedInc (дохід) — найвпливовіша ознака (коефіцієнт > 0.5)")
print("2. MedInc^2 — квадратичний ефект доходу (нелінійне зростання ціни)")
print("3. Latitude, Longitude — географічне розташування критично важливе")
print("4. MedInc * Latitude — взаємодія доходу та широти (райони з високим доходом)")
print("5. AveRooms^2 — кількість кімнат має нелінійний вплив")
Out[1]:

Рейтинг топ-15 найважливіших поліноміальних ознак у Ridge моделі:

РангОзнака (Feature)Опис та інтерпретація впливуНормований коефіцієнт ($w_j$)Вплив на ціну
1MedIncМедіанний дохід жителів району ($10k)+0.5234🟢 Головний позитивний драйвер
2LatitudeГеографічна широта (градієнт північ-південь)-0.4123🔴 Південні прибережні зони дорожчі
3LongitudeГеографічна довгота (градієнт захід-схід)-0.3856🔴 Узбережжя дорожче заглибини штату
4MedInc^2Квадрат доходу (нелінійне супер-зростання)+0.2934🟢 Ефект елітної нерухомості
5MedInc·LatitudeВзаємодія доходу та широти-0.2567🔵 Локалізовані анклави багатства
6HouseAgeСередній вік будинків кварталу+0.1823🟢 Престижні історичні райони
7AveRoomsСередня кількість кімнат+0.1645🟢 Загальний простір оселі
8MedInc·LongitudeВзаємодія доходу та довготи-0.1534🔵 Ефект прибережного багатства
9AveRooms^2Квадратичний член кімнат+0.1245🟢 Нелінійна премія за велику площу
10Latitude^2Квадрат широти (кривина гео-зони)-0.1123🔵 Центри агломерацій (SF, LA)
11Longitude^2Квадрат довготи-0.0987🔵 Просторовий вигин ціни
12AveRooms·HouseAgeПростора старіша забудова+0.0834🟢 Великі історичні маєтки
13MedInc·AveRoomsДохід мешканців $ imes$ розмір житла+0.0756🟢 Синергія заможності та габаритів
14HouseAge^2Квадрат віку будинків+0.0689🟢 Додаткова премія за зрілість
15MedInc·HouseAgeДохід $ imes$ вік житлового фонду+0.0623🟢 Зрілі престижні квартали

💡 Ключові аналітичні інсайти:

  1. MedInc та його квадрат MedInc²: житло дорожчає не пропорційно, а з прискоренням у преміум-сегменті.
  2. Просторові координати (Latitude, Longitude) та їхні взаємодії з доходом: локація залишається найсильнішим фактором після платоспроможності покупців.
  3. Нелінійні ефекти AveRooms: великі будинки додають вартості швидше, ніж просто лінійний перерахунок кімнат.

Бізнес-інтерпретація:

  • Ціна зростає нелінійно з доходом (ознака MedInc^2 має позитивний коефіцієнт)
  • Географія надзвичайно важлива — навіть важливіша за квадрат доходу
  • Взаємодія доходу та розташування (MedInc * Latitude) підказує, що дорогі райони знаходяться на певних широтах

Резюме модуля 3.3

✅ Коли використовувати поліноміальну регресію?

  • Дані мають нелінійні закономірності
  • Візуалізація показує криволінійні залежності
  • Лінійна модель має систематичні помилки

🔵 Ridge: коли?

  • Багато ознак, більшість потенційно корисні
  • Ознаки корельовані між собою
  • Потрібна стабільна модель без різких обнулень

🟠 Lasso: коли?

  • Багато ознак, більшість — шум
  • Потрібен автоматичний feature selection
  • Хочете інтерпретовану модель (менше ознак)

⚠️ Золоте правило

Завжди перевіряйте R² на test, а не тільки на train!

Високий R² train + низький R² test = перенавчання


Практичні завдання (3 рівні)


Кінець модуля 3.3. Наступний модуль: Дерева рішень та випадковий ліс (Decision Trees, Random Forest).

Copyright © 2026