Практика 10 · k найближчих сусідів> 📖 Теорія: відкрий lecture.html у цій же теці.> 📝 Домашнє: homework.md · 🧪 Тест: quiz.htmlУ лекції kNN показано ззовні: межа рішень на площині, голосування сусідів, повзунок k.Тут ми розберемо його зсередини — так, щоб не лишилось жодного місця, де «бібліотекащось порахувала сама».Що зробимо:1. Зберемо таблицю оголошень про вживані телефони й розділимо її на навчальну й тестову частини2. Порахуємо евклідову відстань руками на NumPy й знайдемо k найближчих сусідів3. Напишемо власний kNN-класифікатор і зіставимо його прогнози з KNeighborsClassifier — до останнього обʼєкта4. Проженемо k від 1 до 50 і побудуємо криву точності5. Увімкнемо StandardScaler і побачимо стрибок6. Наприкінці оцінимо ціну телефона через KNeighborsRegressor¶
1. Дані: дошка оголошень про вживані телефониТой самий приклад, що в лекції. Кожен рядок — одне оголошення:| колонка | що це ||---|---|| модель, рік, памʼять_гб, стан, ємність_батареї | що саме продають || типова_ціна | скільки такий телефон коштує на ринку || ціна | скільки просить продавець || відносна_ціна | ціна / типова_ціна — головна ознака || вік_акаунта | скільки днів акаунту продавця || ціна_продажу | за скільки телефон реально пішов (знадобиться в кінці) || шахрайство | 1, якщо оголошення виявилось приманкою |Шахрайство тут двох ґатунків: дешеві приманки зі свіжих акаунтів і вужчий згусток«преміум»-приманок — майже ринкова ціна, зате акаунту менш як два місяці. Плюс 6% мітокперевернуто: у житті розмітка теж буває помилковою, і саме через цей шум вибір kвзагалі має значення.¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score, mean_absolute_error
генератор = np.random.default_rng(42)
КІЛЬКІСТЬ = 800
# скільки коштує модель у базовій комплектації
базова_ціна_моделі = {"iPhone 12": 12000, "Samsung S21": 9000, "Xiaomi Note 11": 5000}
надбавка_за_памʼять = {64: 0.90, 128: 1.00, 256: 1.15}
надбавка_за_стан = {3: 0.85, 4: 1.00, 5: 1.10}
модель = генератор.choice(list(базова_ціна_моделі), КІЛЬКІСТЬ)
рік = генератор.integers(2019, 2024, КІЛЬКІСТЬ)
памʼять_гб = генератор.choice([64, 128, 256], КІЛЬКІСТЬ)
стан = генератор.choice([3, 4, 5], КІЛЬКІСТЬ)
ємність_батареї = генератор.uniform(70, 100, КІЛЬКІСТЬ).round(1)
# типова ринкова ціна: модель × памʼять × стан × свіжість року × стан батареї
типова_ціна = np.array([базова_ціна_моделі[m] for m in модель], dtype=float)
типова_ціна *= np.array([надбавка_за_памʼять[p] for p in памʼять_гб])
типова_ціна *= np.array([надбавка_за_стан[s] for s in стан])
типова_ціна *= 1 + 0.08 * (рік - 2019)
типова_ціна *= 0.85 + 0.01 * (ємність_батареї - 70)
# дві ознаки, за якими ловитимемо шахрайство
відносна_ціна = генератор.uniform(0.30, 1.45, КІЛЬКІСТЬ)
вік_акаунта = генератор.integers(0, 366, КІЛЬКІСТЬ)
дешева_приманка = (відносна_ціна < 0.85) & (вік_акаунта < 150)
преміум_приманка = (відносна_ціна >= 0.95) & (відносна_ціна <= 1.28) & (вік_акаунта < 48)
шахрайство = (дешева_приманка | преміум_приманка).astype(int)
# 6% міток перевертаємо: без шуму k = 1 був би ідеальним і крива точності нічого б не показала
помилкова_розмітка = генератор.random(КІЛЬКІСТЬ) < 0.06
шахрайство[помилкова_розмітка] = 1 - шахрайство[помилкова_розмітка]
таблиця = pd.DataFrame({
"модель": модель,
"рік": рік,
"памʼять_гб": памʼять_гб,
"стан": стан,
"ємність_батареї": ємність_батареї,
"типова_ціна": типова_ціна.round().astype(int),
"ціна": (типова_ціна * відносна_ціна).round().astype(int),
"відносна_ціна": відносна_ціна,
"вік_акаунта": вік_акаунта,
"ціна_продажу": (типова_ціна * генератор.normal(1.0, 0.07, КІЛЬКІСТЬ)).round().astype(int),
"шахрайство": шахрайство,
})
print(таблиця.head(6).round(3).to_string(index=False))
print(f"\nусього оголошень: {len(таблиця)}")
print(f"шахрайських: {таблиця['шахрайство'].sum()} ({таблиця['шахрайство'].mean():.1%})")
модель рік памʼять_гб стан ємність_батареї типова_ціна ціна відносна_ціна вік_акаунта ціна_продажу шахрайство
iPhone 12 2019 64 3 97.4 10318 10173 0.986 196 9842 0
Xiaomi Note 11 2020 256 4 74.2 5539 6709 1.211 278 6190 0
Samsung S21 2021 256 5 90.3 13907 16216 1.166 113 13723 0
Samsung S21 2023 64 4 94.4 11697 15515 1.326 337 11334 0
Samsung S21 2022 64 4 77.1 9251 11347 1.227 297 9960 0
Xiaomi Note 11 2022 256 5 78.3 7318 4276 0.584 335 6948 1
усього оголошень: 800
шахрайських: 218 (27.3%)
2. Навчальна й тестова частиниЦе той самий розподіл ролей, що в теміTrain / Validation / Test. Для kNN навчальначастина — це буквально і є модель: серед цих рядків він шукатиме сусідів. Тестову чіпаємотільки для оцінки.stratify тримає однакову частку шахрайських оголошень в обох частинах — інакше оцінкана рідшому класі стрибала б без причини.¶
ознаки_класифікації = ["відносна_ціна", "вік_акаунта"]
X = таблиця[ознаки_класифікації].to_numpy(dtype=float)
y = таблиця["шахрайство"].to_numpy()
X_навч, X_тест, y_навч, y_тест = train_test_split(
X, y, test_size=0.35, random_state=42, stratify=y)
базова_точність = 1 - y_тест.mean()
print(f"навчальна вибірка: {len(X_навч)} оголошень, шахрайських {y_навч.mean():.1%}")
print(f"тестова вибірка: {len(X_тест)} оголошень, шахрайських {y_тест.mean():.1%}")
print(f"\nбазова лінія «усі оголошення чесні»: точність {базова_точність:.4f}")
print("нижче цього числа модель не має права опускатись — інакше вона гірша за бездіяльність")
навчальна вибірка: 520 оголошень, шахрайських 27.3% тестова вибірка: 280 оголошень, шахрайських 27.1% базова лінія «усі оголошення чесні»: точність 0.7286 нижче цього числа модель не має права опускатись — інакше вона гірша за бездіяльність
3. Відстань рукамиЕвклідова відстань між двома оголошеннями — корінь із суми квадратів різниць по кожній ознаці:$$d(a, b) = \sqrt{\sum_j (a_j - b_j)^2}$$NumPy рахує відстань від одного оголошення до всіх одразу: віднімання рядка від матрицірозтягує цей рядок на всі рядки (це називають broadcasting), далі підносимо до квадрата,додаємо по осі ознак і беремо корінь.¶
def відстані_до_всіх(оголошення, таблиця_оголошень):
"""Евклідові відстані від одного оголошення до кожного рядка таблиці.
Повертає одновимірний масив довжиною в кількість рядків таблиці."""
різниці = таблиця_оголошень - оголошення # broadcasting: рядок мінус уся матриця
return np.sqrt(np.sum(різниці ** 2, axis=1))
# беремо одне тестове оголошення — на ньому далі буде добре видно роль k
НОМЕР_ЗАПИТУ = 23
запит = X_тест[НОМЕР_ЗАПИТУ]
відстані = відстані_до_всіх(запит, X_навч)
print(f"запит: відносна ціна {запит[0]:.3f}, вік акаунта {запит[1]:.0f} днів")
print(f"справжня мітка: {'шахрайство' if y_тест[НОМЕР_ЗАПИТУ] == 1 else 'чесне'}\n")
# argsort повертає номери рядків у порядку зростання відстані
порядок = np.argsort(відстані)
найближчі = pd.DataFrame({
"відносна_ціна": X_навч[порядок[:8], 0].round(3),
"вік_акаунта": X_навч[порядок[:8], 1].astype(int),
"відстань": відстані[порядок[:8]].round(3),
"мітка": y_навч[порядок[:8]],
}, index=range(1, 9))
print(найближчі.to_string())
запит: відносна ціна 0.329, вік акаунта 52 днів справжня мітка: шахрайство відносна_ціна вік_акаунта відстань мітка 1 0.599 52 0.270 1 2 0.373 51 1.001 1 3 0.707 51 1.069 1 4 0.886 51 1.145 0 5 1.171 53 1.307 0 6 0.469 54 2.005 1 7 0.681 54 2.031 1 8 0.927 50 2.088 0
Придивись до цієї таблиціВідстань майже дорівнює різниці у віці акаунта, а відносна ціна на порядок сусідів майжене впливає: серед «найближчих» є і 0.5, і 1.3. Це рівно та пастка, про яку йшлося в лекції —вік живе в діапазоні 0…365, відносна ціна у 0.30…1.45, і дні розчавлюють частки.Полагодимо це в розділі 6. Спершу розберемось із самим механізмом.¶
4. Голосування: пишемо kNN саміПрогноз для одного оголошення — три дії: порахувати відстані, взяти k найменших,подивитись, яких міток серед них більше.¶
def передбачити_одне(оголошення, X_навч, y_навч, k):
"""Голос більшості серед k найближчих сусідів."""
відстані = відстані_до_всіх(оголошення, X_навч)
номери_найближчих = np.argsort(відстані)[:k]
мітки_сусідів = y_навч[номери_найближчих]
# сусідів рівно k, тож «більшість» — це строго більше половини
return int(мітки_сусідів.sum() * 2 > k)
мітка_словами = "шахрайство" if y_тест[НОМЕР_ЗАПИТУ] else "чесне"
print(f"справжня мітка запиту: {мітка_словами}\n")
for k in [1, 3, 5, 9, 15, 25]:
номери = np.argsort(відстані)[:k]
голосів_за_шахрайство = int(y_навч[номери].sum())
вирок = передбачити_одне(запит, X_навч, y_навч, k)
print(f"k = {k:2d}: за шахрайство {голосів_за_шахрайство:2d} з {k:2d}"
f" -> {'шахрайство' if вирок else 'чесне'}")
справжня мітка запиту: шахрайство k = 1: за шахрайство 1 з 1 -> шахрайство k = 3: за шахрайство 3 з 3 -> шахрайство k = 5: за шахрайство 3 з 5 -> шахрайство k = 9: за шахрайство 5 з 9 -> шахрайство k = 15: за шахрайство 6 з 15 -> чесне k = 25: за шахрайство 12 з 25 -> чесне
Одне й те саме оголошення, різні k — різні відповіді. Саме тому k називають головнимналаштуванням методу.Тепер прогноз для всієї тестової вибірки. Ніякого «навчання» перед цим не було: kNN простотримає навчальну таблицю під рукою.
def передбачити_багато(X_запитів, X_навч, y_навч, k):
"""Прогноз для кожного рядка X_запитів."""
прогнози = np.zeros(len(X_запитів), dtype=int)
for номер, оголошення in enumerate(X_запитів):
прогнози[номер] = передбачити_одне(оголошення, X_навч, y_навч, k)
return прогнози
наші_прогнози = передбачити_багато(X_тест, X_навч, y_навч, k=9)
наша_точність = accuracy_score(y_тест, наші_прогнози)
print(f"власний kNN, k = 9: точність {наша_точність:.4f}")
print(f"базова лінія «усі чесні»: {базова_точність:.4f}")
print("\nрозрив крихітний — і це не вада алгоритму, а наслідок несиметричних масштабів")
власний kNN, k = 9: точність 0.7750 базова лінія «усі чесні»: 0.7286 розрив крихітний — і це не вада алгоритму, а наслідок несиметричних масштабів
5. Звірка з бібліотекоюНайцінніша клітинка практики: переконатись, що всередині scikit-learn немає магії.fit тут просто запамʼятовує таблицю — тому й виконується миттєво.¶
бібліотечний_knn = KNeighborsClassifier(n_neighbors=9)
бібліотечний_knn.fit(X_навч, y_навч) # усе «навчання» — копіювання таблиці
прогнози_sklearn = бібліотечний_knn.predict(X_тест)
assert np.array_equal(наші_прогнози, прогнози_sklearn), "прогнози розійшлися!"
print("✅ збігається: наші прогнози й прогнози KNeighborsClassifier однакові")
print(f" перевірено обʼєктів: {len(X_тест)}")
print(f" точність обох: {accuracy_score(y_тест, прогнози_sklearn):.4f}")
✅ збігається: наші прогнози й прогнози KNeighborsClassifier однакові перевірено обʼєктів: 280 точність обох: 0.7750
6. Скільки брати сусідівПроженемо k від 1 до 50 і подивимось на дві криві: точність на навчальній вибірці йна тестовій. Найцікавіше — ліва межа графіка.¶
значення_k = list(range(1, 51))
точність_на_навчанні = []
точність_на_тесті = []
for k in значення_k:
модель_knn = KNeighborsClassifier(n_neighbors=k)
модель_knn.fit(X_навч, y_навч)
точність_на_навчанні.append(accuracy_score(y_навч, модель_knn.predict(X_навч)))
точність_на_тесті.append(accuracy_score(y_тест, модель_knn.predict(X_тест)))
найкраще_k = int(np.argmax(точність_на_тесті)) + 1
print(f"k = 1: навчання {точність_на_навчанні[0]:.4f}, тест {точність_на_тесті[0]:.4f}")
print(f"k = {найкраще_k:2d}: навчання {точність_на_навчанні[найкраще_k - 1]:.4f}, "
f"тест {точність_на_тесті[найкраще_k - 1]:.4f} <- найкраще")
print(f"k = 50: навчання {точність_на_навчанні[-1]:.4f}, тест {точність_на_тесті[-1]:.4f}")
print(f"\nточність на навчанні при k = 1 дорівнює {точність_на_навчанні[0]:.4f}:")
print("найближчий сусід навчального оголошення — завжди він сам, тож помилитись ніде")
k = 1: навчання 1.0000, тест 0.7821 k = 25: навчання 0.8058, тест 0.8393 <- найкраще k = 50: навчання 0.8192, тест 0.7893 точність на навчанні при k = 1 дорівнює 1.0000: найближчий сусід навчального оголошення — завжди він сам, тож помилитись ніде
plt.figure(figsize=(8, 4.2))
plt.plot(значення_k, точність_на_навчанні, label="навчальна вибірка")
plt.plot(значення_k, точність_на_тесті, label="тестова вибірка")
plt.axhline(базова_точність, linestyle=":", color="gray", label="«усі чесні»")
plt.axvline(найкраще_k, linestyle="--", color="gray", label=f"найкраще k = {найкраще_k}")
plt.xlabel("k — скільки сусідів питаємо")
plt.ylabel("точність")
plt.title("Криві точності на сирих, незрівняних ознаках")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
print(f"максимум тестової точності: {max(точність_на_тесті):.4f} при k = {найкраще_k}")
print(f"це лише на {max(точність_на_тесті) - базова_точність:.4f} краще за бездіяльність")
максимум тестової точності: 0.8393 при k = 25 це лише на 0.1107 краще за бездіяльність
7. Масштаб ознак: головна пастка темиЗапамʼятай число вище — а тепер полагодимо відстань. Спершу подивимось, наскільки взагалінесиметричні наші дві ознаки.¶
розмах_ціни = X[:, 0].max() - X[:, 0].min()
розмах_віку = X[:, 1].max() - X[:, 1].min()
print(f"відносна ціна: {X[:, 0].min():.2f} … {X[:, 0].max():.2f} розмах {розмах_ціни:.2f}")
print(f"вік акаунта: {X[:, 1].min():.0f} … {X[:, 1].max():.0f} розмах {розмах_віку:.0f}")
print(f"\nвік більший за ціну у {розмах_віку / розмах_ціни:.0f} разів,")
print(f"а у квадраті — у {(розмах_віку / розмах_ціни) ** 2:.0f} разів.")
print("саме у квадраті ознака й входить у відстань, тож ціна не важить майже нічого")
відносна ціна: 0.30 … 1.45 розмах 1.15 вік акаунта: 0 … 365 розмах 365 вік більший за ціну у 318 разів, а у квадраті — у 101027 разів. саме у квадраті ознака й входить у відстань, тож ціна не важить майже нічого
StandardScaler віднімає середнє й ділить на стандартне відхилення. Обидва числа рахуємолише на навчальній частині — інакше в модель просочилась би інформація про тестову(це називають витоком даних).Подивимось на того самого запиту з розділу 3: чи змінився склад його сусідів.
масштабувальник = StandardScaler()
масштабувальник.fit(X_навч) # середнє й відхилення — тільки з навчальної
X_навч_масштаб = масштабувальник.transform(X_навч)
X_тест_масштаб = масштабувальник.transform(X_тест)
запит_масштаб = X_тест_масштаб[НОМЕР_ЗАПИТУ]
відстані_масштаб = відстані_до_всіх(запит_масштаб, X_навч_масштаб)
порядок_масштаб = np.argsort(відстані_масштаб)
найближчі_після = pd.DataFrame({
"відносна_ціна": X_навч[порядок_масштаб[:8], 0].round(3),
"вік_акаунта": X_навч[порядок_масштаб[:8], 1].astype(int),
"відстань": відстані_масштаб[порядок_масштаб[:8]].round(3),
"мітка": y_навч[порядок_масштаб[:8]],
}, index=range(1, 9))
print(f"запит: відносна ціна {запит[0]:.3f}, вік акаунта {запит[1]:.0f} днів\n")
print(найближчі_після.to_string())
print("\nтепер сусіди схожі на запит за обома ознаками, а не лише за віком акаунта")
запит: відносна ціна 0.329, вік акаунта 52 днів відносна_ціна вік_акаунта відстань мітка 1 0.373 51 0.134 1 2 0.336 38 0.134 1 3 0.347 37 0.153 1 4 0.319 69 0.164 1 5 0.342 27 0.240 1 6 0.411 37 0.286 1 7 0.427 48 0.299 1 8 0.415 34 0.310 1 тепер сусіди схожі на запит за обома ознаками, а не лише за віком акаунта
точність_масштаб = []
for k in значення_k:
модель_knn = KNeighborsClassifier(n_neighbors=k)
модель_knn.fit(X_навч_масштаб, y_навч)
точність_масштаб.append(accuracy_score(y_тест, модель_knn.predict(X_тест_масштаб)))
найкраще_k_масштаб = int(np.argmax(точність_масштаб)) + 1
print(" без масштабування зі StandardScaler")
print(f"k = 9 {точність_на_тесті[8]:.4f} {точність_масштаб[8]:.4f}")
print(f"найкраще k {найкраще_k:2d} {найкраще_k_масштаб:2d}")
print(f"точність при ньому {max(точність_на_тесті):.4f} {max(точність_масштаб):.4f}")
print(f"\nстрибок при k = 9: {точність_масштаб[8] - точність_на_тесті[8]:+.4f}")
без масштабування зі StandardScaler k = 9 0.7750 0.9214 найкраще k 25 17 точність при ньому 0.8393 0.9286 стрибок при k = 9: +0.1464
plt.figure(figsize=(8, 4.2))
plt.plot(значення_k, точність_на_тесті, label="сирі ознаки")
plt.plot(значення_k, точність_масштаб, label="після StandardScaler")
plt.axhline(базова_точність, linestyle=":", color="gray", label="«усі чесні»")
plt.xlabel("k — скільки сусідів питаємо")
plt.ylabel("точність на тестовій вибірці")
plt.title("Одна модель, одні дані — різниця лише в одиницях відстані")
plt.legend()
plt.grid(alpha=0.3)
plt.show()
print(f"розрив між кривими у найкращих точках: "
f"{max(точність_масштаб) - max(точність_на_тесті):+.4f}")
розрив між кривими у найкращих точках: +0.0893
Тепер знайдемо конкретні оголошення, вирок для яких перевернувся.
прогноз_без = KNeighborsClassifier(n_neighbors=9).fit(X_навч, y_навч).predict(X_тест)
прогноз_зі = KNeighborsClassifier(n_neighbors=9).fit(X_навч_масштаб, y_навч).predict(X_тест_масштаб)
виправлені = np.where((прогноз_без != y_тест) & (прогноз_зі == y_тест))[0]
зіпсовані = np.where((прогноз_без == y_тест) & (прогноз_зі != y_тест))[0]
print(f"масштабування виправило {len(виправлені)} оголошень, зіпсувало {len(зіпсовані)}")
номер = виправлені[0]
print(f"\nприклад — оголошення №{номер} у тестовій вибірці:")
print(f" відносна ціна {X_тест[номер, 0]:.3f}")
print(f" вік акаунта {X_тест[номер, 1]:.0f} днів")
print(f" справді {'шахрайство' if y_тест[номер] else 'чесне'}")
print(f" без масштабу {'шахрайство' if прогноз_без[номер] else 'чесне'} <- промах")
print(f" з масштабом {'шахрайство' if прогноз_зі[номер] else 'чесне'} <- влучив")
масштабування виправило 46 оголошень, зіпсувало 5 приклад — оголошення №3 у тестовій вибірці: відносна ціна 0.511 вік акаунта 117 днів справді шахрайство без масштабу чесне <- промах з масштабом шахрайство <- влучив
Як це роблять у продакшені. Замість двох окремих обʼєктів беруть конвеєр:>
make_pipeline(StandardScaler(), KNeighborsClassifier(k)). Тоді всередині> крос-валідації масштабування перераховується> для кожної частини окремо, і забути про витік даних просто ніде.
8. Той самий метод для регресіїЗадача змінюється: тепер потрібна не мітка, а число — скільки коштує телефон, схожийна цей. Змінюється рівно одна дія: замість голосування сусідів беремо середнє їхніх цін.Візьмемо колонку ціна_продажу — за скільки телефон реально пішов, — і обмежимось однієюмоделлю, щоб не заводити окремих ознак під назву моделі.> Якби моделей було кілька, колонку модель довелось би перетворити на числа —> наприклад, one-hot: кожна модель стає окремою ознакою 0/1.¶
одна_модель = таблиця[таблиця["модель"] == "iPhone 12"].copy()
ознаки_регресії = ["рік", "памʼять_гб", "стан", "ємність_батареї"]
X_рег = одна_модель[ознаки_регресії].to_numpy(dtype=float)
y_рег = одна_модель["ціна_продажу"].to_numpy(dtype=float)
X_рег_навч, X_рег_тест, y_рег_навч, y_рег_тест = train_test_split(
X_рег, y_рег, test_size=0.3, random_state=42)
print(f"завершених угод по цій моделі: {len(одна_модель)}")
print(f"навчальна {len(X_рег_навч)}, тестова {len(X_рег_тест)}")
print(f"ціни продажу: {y_рег.min():.0f} … {y_рег.max():.0f} ₴, середня {y_рег.mean():.0f} ₴")
завершених угод по цій моделі: 264 навчальна 184, тестова 80 ціни продажу: 8030 … 22621 ₴, середня 13840 ₴
# рік вимірюється тисячами, стан — одиницями: без масштабування рік зʼїв би все,
# тож одразу беремо конвеєр
регресор = make_pipeline(StandardScaler(), KNeighborsRegressor(n_neighbors=5))
регресор.fit(X_рег_навч, y_рег_навч)
прогноз_ціни = регресор.predict(X_рег_тест)
# з чим порівнювати: найтупіший прогноз — завжди середня ціна навчальної вибірки
тупий_прогноз = np.full(len(y_рег_тест), y_рег_навч.mean())
print(f"kNN, 5 сусідів: середня помилка {mean_absolute_error(y_рег_тест, прогноз_ціни):.0f} ₴")
print(f"«завжди середня»: середня помилка {mean_absolute_error(y_рег_тест, тупий_прогноз):.0f} ₴")
print(f"середня ціна в тесті: {y_рег_тест.mean():.0f} ₴\n")
порівняння = pd.DataFrame({
"рік": X_рег_тест[:6, 0].astype(int),
"памʼять": X_рег_тест[:6, 1].astype(int),
"стан": X_рег_тест[:6, 2].astype(int),
"батарея": X_рег_тест[:6, 3],
"справжня_ціна": y_рег_тест[:6].round().astype(int),
"прогноз": прогноз_ціни[:6].round().astype(int),
})
print(порівняння.to_string(index=False))
kNN, 5 сусідів: середня помилка 934 ₴ «завжди середня»: середня помилка 2429 ₴ середня ціна в тесті: 14273 ₴ рік памʼять стан батарея справжня_ціна прогноз 2022 128 5 83.5 17738 16197 2019 256 4 78.1 13447 12798 2022 64 5 79.1 14036 14736 2021 64 5 72.7 13247 12365 2022 256 4 77.4 14484 16481 2023 64 4 87.2 13606 14834
І остання перевірка: переконаймось, що KNeighborsRegressor справді бере просте середнєцін сусідів. Порахуємо його руками для першого тестового обʼєкта.
# повторюємо те, що робить конвеєр: масштабуємо тими самими числами, що й на навчанні
масштабувальник_рег = StandardScaler().fit(X_рег_навч)
X_рег_навч_масштаб = масштабувальник_рег.transform(X_рег_навч)
X_рег_тест_масштаб = масштабувальник_рег.transform(X_рег_тест)
запит_рег = X_рег_тест_масштаб[0]
відстані_рег = відстані_до_всіх(запит_рег, X_рег_навч_масштаб)
пʼять_найближчих = np.argsort(відстані_рег)[:5]
ціни_сусідів = y_рег_навч[пʼять_найближчих]
наше_середнє = ціни_сусідів.mean()
прогноз_бібліотеки = регресор.predict(X_рег_тест[:1])[0]
print("ціни пʼятьох найближчих сусідів:", ціни_сусідів.round().astype(int).tolist())
print(f"їхнє середнє: {наше_середнє:.2f} ₴")
print(f"прогноз KNeighborsRegressor: {прогноз_бібліотеки:.2f} ₴")
assert np.isclose(наше_середнє, прогноз_бібліотеки), "розрахунок розійшовся!"
print("\n✅ збігається: kNN-регресія — це буквально середнє цін сусідів")
ціни пʼятьох найближчих сусідів: [14524, 17034, 18038, 16806, 14583] їхнє середнє: 16197.00 ₴ прогноз KNeighborsRegressor: 16197.00 ₴ ✅ збігається: kNN-регресія — це буквально середнє цін сусідів