d = √(Σ wi·Δi2), где wi – вес признака, Δi – разность.Датасет: погода → спрос на хлеб
| # | Temp, °C | Humidity, % | Rain | Weekend | Sold |
|---|---|---|---|---|---|
| 1 | 10 | 85 | rain | weekday | 130 |
| 2 | 12 | 80 | rain | weekend | 165 |
| 3 | 14 | 75 | rain | weekend | 160 |
| 4 | 15 | 70 | no rain | weekday | 105 |
| 5 | 16 | 68 | no rain | weekend | 135 |
| 6 | 18 | 65 | no rain | weekday | 100 |
| 7 | 22 | 55 | rain | weekday | 110 |
| 8 | 24 | 50 | no rain | weekday | 90 |
| 9 | 28 | 45 | no rain | weekend | 130 |
Наивно: время – O(n log n) на сортировку; можно O(n + k log k) частичным отбором. Память – O(n).
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;56function 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}1314/**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}2425/**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_WEIGHTS42): number {43 const { tMin, tMax, hMin, hMax } = norms;4445 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);4950 const dTemp = aTemp - bTemp;51 const dHum = aHum - bHum;52 const dRain = (+a.rain) - (+b.rain);53 const dWend = (+a.weekend) - (+b.weekend);5455 const sum =56 w.temp * (dTemp * dTemp) +57 w.humidity * (dHum * dHum) +58 w.rain * (dRain * dRain) +59 w.weekend * (dWend * dWend);6061 return Math.sqrt(sum);62}6364// k-NN регрессия: прогноз числа буханок по погоде (temp, humidity, rain, weekend).65function knnPredictDemand(66 data: Sample[],67 q: Query,68 k: number,69 w: Weights = DEFAULT_WEIGHTS70): 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 = data75 .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}8687// Примеры88knnPredictDemand(data, { temp: 13, humidity: 78, rain: 1, weekend: 1 }, 3); // -> 15789knnPredictDemand(data, { temp: 21, humidity: 58, rain: 0, weekend: 0 }, 3); // -> 9890knnPredictDemand(data, { temp: 29, humidity: 45, rain: 0, weekend: 1 }, 3); // -> 12391knnPredictDemand(data, { temp: 31, humidity: 38, rain: 1, weekend: 0 }, 5); // -> 10692knnPredictDemand(data, { temp: 20, humidity: 60, rain: 1, weekend: 0 }, 4, {"temp":1,"humidity":1,"rain":3,"weekend":2}); // -> 134