Как сервисы угадывают, что вам понравится? Один из самых простых и понятных способов — k ближайших соседей
(k-nearest neighbors, kNN): найти k самых похожих на вас людей и посмотреть, что нравится им. «Похожие» — это
близкие по расстоянию, если каждого человека записать строкой чисел. Метод хорошо разобран, например, в книге
«Грокаем алгоритмы» (глава 10). Здесь соберём его сами из того, что уже знаем: разности с транслированием,
np.linalg.norm, np.argsort.
Шаг 1. Похожие гости
Восемь постоянных гостей кофейни оценили шесть напитков по шкале от 1 до 5. Столбцы: эспрессо, капучино, латте, раф, чай, какао. Новая гостья Оля оценила первые пять, а какао не пробовала. Понравится ли оно ей?
import numpy as np
drinks = np.array(["эспрессо", "капучино", "латте", "раф", "чай", "какао"])
ratings = np.array([
[5, 4, 2, 1, 2, 1],
[2, 3, 5, 5, 3, 5],
[4, 5, 3, 2, 1, 2],
[1, 2, 4, 5, 4, 4],
[5, 5, 3, 2, 2, 1],
[3, 3, 3, 3, 5, 3],
[2, 2, 5, 4, 3, 5],
[4, 3, 2, 2, 4, 2],
])
olya = np.array([2, 3, 5, 4, 3]) # без какао
print(ratings.shape, olya.shape)
print(drinks[:5])(8, 6) (5,) ['эспрессо' 'капучино' 'латте' 'раф' 'чай']
Сохраните в dists расстояние от оценок Оли olya до оценок каждого гостя — только по первым пяти напиткам,
которые Оля пробовала.
ratings[:, :5], таблица (8, 5); olya — (5,), разность транслируется.np.linalg.norm(ratings[:, :5] - olya, axis=1).Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
dists = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
dists = np.linalg.norm(ratings[:, :5] - olya, axis=1)Шаг 2. Три ближайших
np.argsort возвращает номера в порядке возрастания — первые k из них и есть ближайшие соседи.
Сохраните в neighbors номера трёх гостей, ближайших к Оле.
np.argsort(dists) — от ближнего к дальнему; первые три — срез [:3].Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
neighbors = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
neighbors = np.argsort(dists)[:3]Шаг 3. Предсказание
Прогноз для Оли — средняя оценка какао у её соседей. Массив номеров выбирает строки таблицы так же, как
элементы одномерного массива в уроке о сортировке: ratings[neighbors] — таблица из трёх строк.
Сохраните в cocoa_pred среднюю оценку какао (столбец 5) у трёх соседей Оли.
ratings[neighbors]; столбец какао — [:, 5].ratings[neighbors][:, 5].mean().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
cocoa_pred = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
cocoa_pred = ratings[neighbors][:, 5].mean()Прогноз 4.7 из 5 — какао Оле стоит предложить. Для сравнения, средняя оценка какао у всех гостей — около 2.9: соседи знают об Оле больше, чем «средний гость».
Ниже первая строка — средние оценки трёх соседей Оли по всем шести напиткам, вторая — средние всех гостей:
print(np.round(ratings[neighbors].mean(axis=0), 1))
print(np.round(ratings.mean(axis=0), 1))[1.7 2.3 4.7 4.7 3.3 4.7] [3.2 3.4 3.4 3. 3. 2.9]
Шаг 4. Прогноз продаж
Тот же метод предсказывает числа. Кофейня записывала для каждого дня три признака — температуру, выходной ли это (1 или 0) и была ли акция (1 или 0) — и сколько чашек продали. Завтра будний день, +17 °C, без акции. Сколько варить?
days = np.array([
# температура, выходной, акция
[5, 0, 0],
[12, 0, 1],
[18, 1, 0],
[22, 1, 1],
[8, 1, 0],
[25, 0, 0],
[15, 0, 1],
[19, 0, 0],
[3, 1, 1],
[16, 1, 0],
[28, 1, 0],
[10, 0, 0],
])
cups = np.array([140, 190, 260, 330, 250, 150, 200, 155, 280, 265, 240, 145])
tomorrow = np.array([17, 0, 0])
print(days.shape, cups.shape)(12, 3) (12,)
Сохраните в naive_dists расстояние от завтрашнего дня tomorrow до каждого прошлого дня по признакам days,
в naive_nb — номера четырёх ближайших дней, а в naive_pred — среднее число чашек cups в эти дни.
np.linalg.norm(days - tomorrow, axis=1), np.argsort(…)[:4].cups[naive_nb].mean().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
naive_dists = ...
naive_nb = ...
naive_pred = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
naive_dists = np.linalg.norm(days - tomorrow, axis=1)
naive_nb = np.argsort(naive_dists)[:4]
naive_pred = cups[naive_nb].mean()print(days[naive_nb])
print(cups[naive_nb])[[18 1 0] [16 1 0] [19 0 0] [15 0 1]] [260 265 155 200]
Среди «похожих» дней оказались выходные с продажами 260 и 265 чашек — хотя завтра будний день. Почему? Температура меняется от 3 до 28 — разница в 5 градусов даёт в расстоянии 5, а разница «выходной или будний» — всего 1. Признак с большими числами заглушает остальные.
Шаг 5. Нормализация
Решение — привести каждый признак к одной шкале 0–1, как в уроке о нормализации: (x - min) / (max - min) по
каждому столбцу. Минимум и максимум берутся по прошлым дням, и завтрашний день приводится теми же числами.
Приведите признаки к 0–1: lo и hi — минимум и максимум каждого столбца days, days_n — нормализованная
таблица, tomorrow_n — нормализованный завтрашний день. Затем сохраните в scaled_nb номера четырёх ближайших
дней по нормализованным признакам, а в scaled_pred — среднее число чашек в эти дни.
lo = days.min(axis=0), hi = days.max(axis=0); days_n = (days - lo) / (hi - lo) — транслирование по строкам.tomorrow_n = (tomorrow - lo) / (hi - lo); дальше — как в шаге 4.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
lo = ...
hi = ...
days_n = ...
tomorrow_n = ...
scaled_nb = ...
scaled_pred = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
lo = days.min(axis=0)
hi = days.max(axis=0)
days_n = (days - lo) / (hi - lo)
tomorrow_n = (tomorrow - lo) / (hi - lo)
scaled_nb = np.argsort(np.linalg.norm(days_n - tomorrow_n, axis=1))[:4]
scaled_pred = cups[scaled_nb].mean()print("соседи без нормализации:", days[naive_nb][:, 1].sum(), "выходных из 4 →", naive_pred, "чашек")
print("соседи после нормализации:", days[scaled_nb][:, 1].sum(), "выходных из 4 →", scaled_pred, "чашек")соседи без нормализации: 2 выходных из 4 → 220.0 чашек соседи после нормализации: 0 выходных из 4 → 147.5 чашек
Выводы
После нормализации соседями стали будние дни без акции — те, что действительно похожи на завтрашний, — и прогноз
упал с 220 до 148 чашек: сырые признаки завысили бы заказ почти в полтора раза. Метод ближайших соседей — это
всего три операции NumPy: разность с транслированием, норма по строкам и argsort. А главный урок проекта —
перед тем как измерять расстояния, признаки приводят к одной шкале.