LogoData Structures & Algorithms

k-NN регрессия – прогнозирование ответа

Цель: спрогнозировать, сколько буханок испечь. Признаки: температура, влажность, дождь, выходной (с весом ×2).
Идея k-NN: берём k ближайших дней по этим признакам (после нормализации), усредняем продажи с весами 1/(1+dist).
Веса признаков: позволяют задать важность – например, выходной день влияет на спрос сильнее, чем погода.
Расстояние: d = √(Σ wi·Δi2), где wi – вес признака, Δi – разность.

Датасет: погода → спрос на хлеб

#Temp, °CHumidity, %RainWeekendSold
11085rainweekday130
21280rainweekend165
31475rainweekend160
41570no rainweekday105
51668no rainweekend135
61865no rainweekday100
72255rainweekday110
82450no rainweekday90
92845no rainweekend130
Сложность:

Наивно: время – O(n log n) на сортировку; можно O(n + k log k) частичным отбором. Память – O(n).

knnRegression.ts
1type Sample = { temp: number; humidity: number; rain: 0 | 1; weekend: 0 | 1; sold: number };
2type Query = Omit<Sample, 'sold'>;
3type Weights = Readonly<{ temp: number; humidity: number; rain: number; weekend: number }>;
4const DEFAULT_WEIGHTS = { temp: 1, humidity: 1, rain: 1, weekend: 2 } as const satisfies Weights;
5
6function computeNorms(data: Sample[]) {
7 const tMin = Math.min(...data.map(d => d.temp));
8 const tMax = Math.max(...data.map(d => d.temp));
9 const hMin = Math.min(...data.map(d => d.humidity));
10 const hMax = Math.max(...data.map(d => d.humidity));
11 return {tMin, tMax, hMin, hMax};
12}
13
14/**
15 * Min–max нормализация: переводит x из [min,max] в [0–1],
16 * чтобы признаки были сопоставимы в метриках расстояния;
17 * при min===max возвращает 0.
18 */
19function norm01(x: number, min: number, max: number, clip = false) {
20 if (max === min) return 0;
21 const z = (x - min) / (max - min);
22 return clip ? Math.max(0, Math.min(1, z)) : z;
23}
24
25/**
26 * Взвешенное евклидово расстояние между двумя днями по 4 признакам.
27 * Формула: d = √(Σ wᵢ·Δᵢ²), где wᵢ – вес, Δᵢ – разность по признаку.
28 *
29 * Веса позволяют управлять важностью признаков:
30 * - вес > 1 делает признак более значимым (вклад в расстояние больше)
31 * - вес = 1 – стандартное влияние
32 * - вес < 1 делает признак менее значимым
33 *
34 * Например, weekend=2 означает, что различие по выходному дню
35 * вносит вклад в 2 раза больше, чем по температуре (при w_temp=1).
36 */
37function euclid4WeightedLinear(
38 a: Query,
39 b: Query,
40 norms: ReturnType<typeof computeNorms>,
41 w: Weights = DEFAULT_WEIGHTS
42): number {
43 const { tMin, tMax, hMin, hMax } = norms;
44
45 const aTemp = norm01(a.temp, tMin, tMax);
46 const bTemp = norm01(b.temp, tMin, tMax);
47 const aHum = norm01(a.humidity, hMin, hMax);
48 const bHum = norm01(b.humidity, hMin, hMax);
49
50 const dTemp = aTemp - bTemp;
51 const dHum = aHum - bHum;
52 const dRain = (+a.rain) - (+b.rain);
53 const dWend = (+a.weekend) - (+b.weekend);
54
55 const sum =
56 w.temp * (dTemp * dTemp) +
57 w.humidity * (dHum * dHum) +
58 w.rain * (dRain * dRain) +
59 w.weekend * (dWend * dWend);
60
61 return Math.sqrt(sum);
62}
63
64// k-NN регрессия: прогноз числа буханок по погоде (temp, humidity, rain, weekend).
65function knnPredictDemand(
66 data: Sample[],
67 q: Query,
68 k: number,
69 w: Weights = DEFAULT_WEIGHTS
70): number {
71 if (k <= 0) throw new Error('k must be >= 1');
72 if (data.length === 0) throw new Error('data cannot be empty');
73 const norms = computeNorms(data);
74 const withDist = data
75 .map(s => ({...s, dist: euclid4WeightedLinear(q, s, norms, w)}))
76 .sort((a, b) => a.dist - b.dist)
77 .slice(0, Math.min(Math.max(1, Math.trunc(k)), data.length));
78 let num = 0, den = 0;
79 for (const n of withDist) {
80 const sim = 1 / (1 + n.dist);
81 num += sim * n.sold;
82 den += sim;
83 }
84 return den ? Math.round(num / den) : NaN;
85}
86
87// Примеры
88knnPredictDemand(data, { temp: 13, humidity: 78, rain: 1, weekend: 1 }, 3); // -> 157
89knnPredictDemand(data, { temp: 21, humidity: 58, rain: 0, weekend: 0 }, 3); // -> 98
90knnPredictDemand(data, { temp: 29, humidity: 45, rain: 0, weekend: 1 }, 3); // -> 123
91knnPredictDemand(data, { temp: 31, humidity: 38, rain: 1, weekend: 0 }, 5); // -> 106
92knnPredictDemand(data, { temp: 20, humidity: 60, rain: 1, weekend: 0 }, 4, {"temp":1,"humidity":1,"rain":3,"weekend":2}); // -> 134