Алгоритм k-ближайших соседей (k-Nearest Neighbors).
Цель: предсказать, какие фильмы (товары/треки) понравятся пользователю.
Идея k-NN: находим k ближайших по вкусу пользователей (по общим оценкам), затем для неоценённых фильмов усредняем рейтинги соседей с весами по схожести.
Наивно: время – O(U log U + k·I) (сортируем пользователей и агрегируем по фильмам), память – O(U+I).
1type Ratings = Record<string, Record<string, number>>;23function euclidOnOverlap(a: Record<string, number>, b: Record<string, number>): number {4 const common = Object.keys(a).filter(k => k in b);5 // common – это массив общих ключей двух объектов a и b, то есть пересечение их ключей.6 if (common.length === 0) return Infinity;7 let sumSq = 0; // sumSq – это накопитель суммы квадратов разностей оценок на пересечении ключей.8 for (const k of common) sumSq += (a[k] - b[k]) ** 2;9 return Math.sqrt(sumSq);10}1112/**13 * Рекомендательная система k-NN.14 * 1) Ищем k ближайших пользователей (схожесть = 1 / (1 + distance)).15 * 2) Для фильмов, которых у user нет, считаем взвешенное среднее рейтингов соседей.16 */17function knnRecommend(data: Ratings, user: string, k: number): {18 neighbors: string[];19 recommendations: Array<{ item: string; score: number }>;20} {21 const dists: Array<{ name: string; dist: number; sim: number }> = [];22 for (const [name, r] of Object.entries(data)) {23 if (name === user) continue;24 const d = euclidOnOverlap(data[user], r); // d – евклидова дистанция25 if (!Number.isFinite(d)) continue;26 dists.push({ name, dist: d, sim: 1 / (1 + d) });27 }28 dists.sort((a, b) => a.dist - b.dist);29 const neighbors = dists.slice(0, k);3031 const seen = new Set(Object.keys(data[user]));32 const score: Record<string, number> = {};33 const weight: Record<string, number> = {};3435 for (const n of neighbors) {36 for (const [item, rating] of Object.entries(data[n.name])) {37 if (seen.has(item)) continue;38 score[item] = (score[item] ?? 0) + n.sim * rating;39 weight[item] = (weight[item] ?? 0) + n.sim;40 }41 }4243 const recommendations = Object.keys(score)44 .map(item => ({ item, score: +(score[item] / weight[item]).toFixed(2) }))45 .sort((a, b) => b.score - a.score);4647 return { neighbors: neighbors.map(n => n.name), recommendations };48}4950// Данные51const ratings: Ratings = {52 you: { Inception: 5, Matrix: 4 },53 alice: { Inception: 5, Matrix: 5, Titanic: 1 },54 bob: { Inception: 4, Matrix: 4, Avatar: 3, Titanic: 1 },55 carol: { Matrix: 5, Interstellar: 5, Avatar: 2 },56 dave: { Inception: 2, Titanic: 5, Avatar: 4 },57 eve: { Matrix: 4, Interstellar: 4, Inception: 5 },58};5960// Примеры61knnRecommend(ratings, 'you', 2);62 // neighbors -> ["eve","alice"];63 // top -> [["Interstellar",4],["Titanic",1]]64knnRecommend(ratings, 'you', 3);65 // neighbors -> ["eve","alice","bob"];66 // top -> [["Interstellar",4],["Avatar",3],["Titanic",1]]67knnRecommend(ratings, 'you', 4);68 // neighbors -> ["eve","alice","bob","carol"];69 // top -> [["Interstellar",4.33],["Avatar",2.5],["Titanic",1]]