1mod diagnostics;
2mod metric;
3mod neighbor;
4mod predict;
5pub mod vptree;
7
8pub use diagnostics::{
9 FeatureDiagnostics, ModelDiagnostics, NeighborDetail, OutputStats, PredictionDiagnostics,
10};
11pub use metric::LearnedMetric;
12pub use neighbor::{Neighbor, Neighbors};
13pub use predict::ExtrapolatedPrediction;
14
15pub trait DataPoint {
30 fn feature_distances(&self, other: &Self) -> Vec<f64>;
32
33 fn feature_values(&self) -> Vec<f64>;
37}
38
39pub struct Renegade<P: DataPoint> {
53 points: Vec<P>,
56 values_flat: Vec<f64>,
59 outputs: Vec<f64>,
61 instance_weights: Vec<f64>,
63 num_features: usize,
65
66 optimal_k: Option<usize>,
68 learned_metric: Option<LearnedMetric>,
69 kernel_bandwidth: Option<f64>,
72 vp_index: Option<vptree::VpTree>,
74 computed_at: usize,
76 vp_built_at: usize,
78}
79
80const MIN_POINTS_FOR_METRIC: usize = 10;
82
83const VP_TREE_THRESHOLD: usize = 3;
85
86impl<P: DataPoint + Clone> Renegade<P> {
87 pub fn new() -> Self {
89 Renegade {
90 points: Vec::new(),
91 values_flat: Vec::new(),
92 outputs: Vec::new(),
93 instance_weights: Vec::new(),
94 num_features: 0,
95 optimal_k: None,
96 learned_metric: None,
97 kernel_bandwidth: None,
98 vp_index: None,
99 computed_at: 0,
100 vp_built_at: 0,
101 }
102 }
103
104 pub fn add(&mut self, point: P, output: f64) {
106 self.add_weighted(point, output, 1.0);
107 }
108
109 pub fn add_weighted(&mut self, point: P, output: f64, weight: f64) {
113 debug_assert!(weight > 0.0, "Instance weight must be positive");
114 let values = point.feature_values();
115 if self.num_features == 0 {
116 self.num_features = values.len();
117 debug_assert_eq!(
118 values.len(),
119 point.feature_distances(&point).len(),
120 "feature_values() and feature_distances() must return the same number of features"
121 );
122 } else {
123 debug_assert_eq!(
124 values.len(),
125 self.num_features,
126 "All data points must have the same number of features"
127 );
128 }
129 self.values_flat.extend_from_slice(&values);
130 self.outputs.push(output);
131 self.instance_weights.push(weight);
132 self.points.push(point);
133
134 if self.computed_at > 0 && self.len() >= self.computed_at + self.computed_at / 2 {
136 self.optimal_k = None;
137 self.learned_metric = None;
138 self.kernel_bandwidth = None;
139 self.vp_index = None;
140 self.vp_built_at = 0;
141 }
142
143 if self.vp_built_at > 0 {
145 let tail = self.len() - self.vp_built_at;
146 if tail > self.vp_built_at / 5 {
147 self.rebuild_vp_tree();
148 }
149 }
150 }
151
152 #[inline]
154 pub fn len(&self) -> usize {
155 self.outputs.len()
156 }
157
158 #[inline]
160 pub fn is_empty(&self) -> bool {
161 self.outputs.is_empty()
162 }
163
164 pub fn retain<F>(&mut self, mut f: F)
168 where
169 F: FnMut(&P, f64) -> bool,
170 {
171 let n = self.len();
172 let nf = self.num_features;
173 let mut write = 0;
174 for read in 0..n {
175 if f(&self.points[read], self.outputs[read]) {
176 if write != read {
177 self.points.swap(write, read);
178 self.outputs.swap(write, read);
179 self.instance_weights.swap(write, read);
180 self.values_flat
181 .copy_within(read * nf..(read + 1) * nf, write * nf);
182 }
183 write += 1;
184 }
185 }
186 self.points.truncate(write);
187 self.outputs.truncate(write);
188 self.instance_weights.truncate(write);
189 self.values_flat.truncate(write * nf);
190 self.invalidate();
191 }
192
193 pub fn force_retrain(&mut self) {
195 self.invalidate();
196 }
197
198 fn invalidate(&mut self) {
200 self.optimal_k = None;
201 self.learned_metric = None;
202 self.kernel_bandwidth = None;
203 self.vp_index = None;
204 self.vp_built_at = 0;
205 }
206
207 fn rebuild_vp_tree(&mut self) {
209 let n = self.len();
210 if n >= VP_TREE_THRESHOLD {
211 self.vp_index = Some(vptree::VpTree::build(n, &|a, b| {
212 self.distance_between(a, b)
213 }));
214 self.vp_built_at = n;
215 }
216 }
217
218 #[inline]
220 fn entry_values(&self, i: usize) -> &[f64] {
221 let nf = self.num_features;
222 &self.values_flat[i * nf..(i + 1) * nf]
223 }
224
225 #[inline]
227 fn distance_to_entry(&self, query_values: &[f64], query: &P, i: usize) -> f64 {
228 match &self.learned_metric {
229 Some(metric) => metric.distance(query_values, self.entry_values(i)),
230 None => {
231 let feat_dists = query.feature_distances(&self.points[i]);
232 if feat_dists.is_empty() {
233 return 0.0;
234 }
235 feat_dists.iter().sum::<f64>() / feat_dists.len() as f64
236 }
237 }
238 }
239
240 #[inline]
242 fn distance_between(&self, i: usize, j: usize) -> f64 {
243 match &self.learned_metric {
244 Some(metric) => metric.distance(self.entry_values(i), self.entry_values(j)),
245 None => {
246 let feat_dists = self.points[i].feature_distances(&self.points[j]);
247 if feat_dists.is_empty() {
248 return 0.0;
249 }
250 feat_dists.iter().sum::<f64>() / feat_dists.len() as f64
251 }
252 }
253 }
254
255 pub fn query_k(&self, query: &P, k: usize) -> Neighbors {
260 let query_values = query.feature_values();
261 let n = self.len();
262
263 let results = if let Some(ref vp) = self.vp_index {
264 let query_dist = |i: usize| self.distance_to_entry(&query_values, query, i);
265
266 let mut results = vp.query_nearest(k, &query_dist);
268
269 if self.vp_built_at < n {
271 for i in self.vp_built_at..n {
272 let dist = self.distance_to_entry(&query_values, query, i);
273 if results.len() < k {
274 results.push((i, dist));
275 results.sort_by(|a, b| {
276 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
277 });
278 } else if let Some(worst) = results.last() {
279 if dist < worst.1 {
280 results.pop();
281 results.push((i, dist));
282 results.sort_by(|a, b| {
283 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
284 });
285 }
286 }
287 }
288 }
289
290 results
291 } else {
292 let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n);
294 for i in 0..n {
295 let dist = self.distance_to_entry(&query_values, query, i);
296 distances.push((i, dist));
297 }
298 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
299 distances.truncate(k);
300 distances
301 };
302
303 let neighbors = results
304 .into_iter()
305 .map(|(i, dist)| Neighbor {
306 distance: dist,
307 output: self.outputs[i],
308 weight: self.instance_weights[i],
309 })
310 .collect();
311
312 Neighbors { neighbors }
313 }
314
315 pub fn query(&mut self, query: &P) -> Neighbors {
318 self.ensure_trained();
319 let k = self.optimal_k.unwrap();
320 self.query_k(query, k)
321 }
322
323 pub fn predict(&mut self, query: &P) -> f64 {
327 self.ensure_trained();
328 let k = self.optimal_k.unwrap();
329 if let Some(h) = self.kernel_bandwidth {
330 let max_k = (self.len() as f64).sqrt().ceil() as usize;
334 let neighbors = self.query_k(query, max_k);
335 neighbors.gaussian_weighted_mean(h)
336 } else {
337 let neighbors = self.query_k(query, k);
338 neighbors.weighted_mean()
339 }
340 }
341
342 pub fn predict_extrapolated(&mut self, query: &P) -> ExtrapolatedPrediction {
344 let neighbors = self.query(query);
345 neighbors.extrapolate()
346 }
347
348 pub fn predict_k(&self, query: &P, k: usize) -> f64 {
350 let neighbors = self.query_k(query, k);
351 neighbors.weighted_mean()
352 }
353
354 pub fn predict_k_extrapolated(&self, query: &P, k: usize) -> ExtrapolatedPrediction {
356 let neighbors = self.query_k(query, k);
357 neighbors.extrapolate()
358 }
359
360 fn ensure_trained(&mut self) {
366 if self.optimal_k.is_some() {
367 return;
368 }
369
370 if self.len() >= MIN_POINTS_FOR_METRIC {
371 self.learned_metric = None;
373 let (k_no_metric, error_no_metric, bw_no_metric) =
374 self.compute_optimal_k_and_bandwidth();
375
376 let candidate_metric = self.learn_metric();
378 self.learned_metric = Some(candidate_metric);
379 let (k_with_metric, error_with_metric, bw_with_metric) =
380 self.compute_optimal_k_and_bandwidth();
381
382 let best_no_metric = match bw_no_metric {
385 Some((_, bw_err)) if bw_err < error_no_metric => bw_err,
386 _ => error_no_metric,
387 };
388 let best_with_metric = match bw_with_metric {
389 Some((_, bw_err)) if bw_err < error_with_metric => bw_err,
390 _ => error_with_metric,
391 };
392
393 if best_with_metric < best_no_metric {
394 self.optimal_k = Some(k_with_metric);
396 if let Some((h, bw_err)) = bw_with_metric {
397 if bw_err < error_with_metric {
398 self.kernel_bandwidth = Some(h);
399 }
400 }
401 } else {
402 self.learned_metric = None;
404 self.optimal_k = Some(k_no_metric);
405 if let Some((h, bw_err)) = bw_no_metric {
406 if bw_err < error_no_metric {
407 self.kernel_bandwidth = Some(h);
408 }
409 }
410 }
411 } else {
412 self.learned_metric = None;
413 let k = self.compute_optimal_k();
414 self.optimal_k = Some(k);
415 }
416
417 self.rebuild_vp_tree();
419
420 self.computed_at = self.len();
421 }
422
423 pub fn get_optimal_k(&mut self) -> usize {
425 self.ensure_trained();
426 self.optimal_k.unwrap()
427 }
428
429 fn learn_metric(&self) -> LearnedMetric {
431 use metric::TrainingPoint;
432
433 let points: Vec<TrainingPoint> = (0..self.len())
434 .map(|i| TrainingPoint {
435 features: self.entry_values(i).to_vec(),
436 output: self.outputs[i],
437 })
438 .collect();
439
440 LearnedMetric::learn(&points)
441 }
442
443 fn compute_optimal_k(&self) -> usize {
450 self.compute_optimal_k_and_bandwidth().0
451 }
452
453 fn compute_optimal_k_and_bandwidth(&self) -> (usize, f64, Option<(f64, f64)>) {
456 let n = self.len();
457 if n <= 2 {
458 return (n.max(1), f64::MAX, None);
459 }
460
461 let max_k = (n as f64).sqrt().ceil() as usize;
462 let max_k = max_k.max(1).min(n - 1);
463
464 let is_classification = self.detect_classification();
465
466 let max_eval = 200.min(n);
467 let step = if n > max_eval { n / max_eval } else { 1 };
468
469 let eval_data: Vec<(usize, Vec<(usize, f64)>)> = (0..n)
471 .step_by(step)
472 .take(max_eval)
473 .map(|i| {
474 let mut distances: Vec<(usize, f64)> = (0..n)
475 .filter(|&j| j != i)
476 .map(|j| (j, self.distance_between(i, j)))
477 .collect();
478 distances
479 .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
480 distances.truncate(max_k);
481 (i, distances)
482 })
483 .collect();
484
485 let count = eval_data.len();
486 if count == 0 {
487 return (1, f64::MAX, None);
488 }
489
490 let mut errors_by_k = vec![0.0f64; max_k + 1];
492
493 for &(i, ref distances) in &eval_data {
494 if is_classification {
495 let mut votes: Vec<(f64, f64)> = Vec::new(); for k in 1..=max_k.min(distances.len()) {
498 let (j, dist) = distances[k - 1];
499 let val = self.outputs[j];
500 let w = if dist == 0.0 {
501 self.instance_weights[j] * 1e6
502 } else {
503 self.instance_weights[j] / dist
504 };
505 if let Some(entry) = votes.iter_mut().find(|(v, _)| (*v - val).abs() < 1e-10) {
506 entry.1 += w;
507 } else {
508 votes.push((val, w));
509 }
510 let predicted = votes
511 .iter()
512 .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
513 .unwrap()
514 .0;
515 if (predicted - self.outputs[i]).abs() > 0.5 {
516 errors_by_k[k] += 1.0;
517 }
518 }
519 } else {
520 let mut weight_sum = 0.0;
522 let mut value_sum = 0.0;
523 let mut exact_w = 0.0;
524 let mut exact_v = 0.0;
525 let mut has_exact = false;
526
527 for k in 1..=max_k.min(distances.len()) {
528 let (j, dist) = distances[k - 1];
529
530 if dist == 0.0 {
531 has_exact = true;
532 exact_w += self.instance_weights[j];
533 exact_v += self.instance_weights[j] * self.outputs[j];
534 } else if !has_exact {
535 let w = self.instance_weights[j] / dist;
536 weight_sum += w;
537 value_sum += w * self.outputs[j];
538 }
539
540 let predicted = if has_exact {
541 if exact_w > 0.0 {
542 exact_v / exact_w
543 } else {
544 self.outputs[j]
545 }
546 } else if weight_sum > 0.0 {
547 value_sum / weight_sum
548 } else {
549 continue;
550 };
551
552 let err = predicted - self.outputs[i];
553 errors_by_k[k] += err * err;
554 }
555 }
556 }
557
558 let mut best_k = 1;
559 let mut best_k_error = f64::MAX;
560 for (k, &err) in errors_by_k.iter().enumerate().skip(1) {
561 let error = err / count as f64;
562 if error < best_k_error {
563 best_k_error = error;
564 best_k = k;
565 }
566 }
567
568 let bandwidth_result = if !is_classification {
570 let mut all_dists: Vec<f64> = Vec::new();
572 for (_, distances) in &eval_data {
573 for &(_, d) in distances {
574 if d > 0.0 {
575 all_dists.push(d);
576 }
577 }
578 }
579
580 if all_dists.is_empty() {
581 None
582 } else {
583 all_dists.sort_by(|a, b| a.partial_cmp(b).unwrap());
584 let h_candidates: Vec<f64> = (1..=20)
585 .map(|t| {
586 let pct = t as f64 / 21.0;
587 let idx = (pct * all_dists.len() as f64) as usize;
588 all_dists[idx.min(all_dists.len() - 1)]
589 })
590 .collect();
591
592 let mut best_h = h_candidates[0];
593 let mut best_h_error = f64::MAX;
594
595 for &h in &h_candidates {
596 let h2 = 2.0 * h * h;
597 let mut total_error = 0.0;
598
599 for &(i, ref distances) in &eval_data {
600 let mut weight_sum = 0.0;
601 let mut value_sum = 0.0;
602 let mut exact_match = None;
603
604 for &(j, dist) in distances {
605 if dist == 0.0 {
606 exact_match = Some(self.outputs[j]);
607 break;
608 }
609 let w = (-dist * dist / h2).exp() * self.instance_weights[j];
610 if w < 1e-15 {
611 break;
612 }
613 weight_sum += w;
614 value_sum += w * self.outputs[j];
615 }
616
617 let predicted = if let Some(v) = exact_match {
618 v
619 } else if weight_sum > 0.0 {
620 value_sum / weight_sum
621 } else if let Some(&(j, _)) = distances.first() {
622 self.outputs[j]
623 } else {
624 continue;
625 };
626
627 let err = predicted - self.outputs[i];
628 total_error += err * err;
629 }
630
631 let avg_error = total_error / count as f64;
632 if avg_error < best_h_error {
633 best_h_error = avg_error;
634 best_h = h;
635 }
636 }
637
638 Some((best_h, best_h_error))
639 }
640 } else {
641 None
642 };
643
644 (best_k, best_k_error, bandwidth_result)
645 }
646
647 fn detect_classification(&self) -> bool {
653 if self.is_empty() {
654 return false;
655 }
656
657 let all_integer = self.outputs.iter().all(|&o| (o - o.round()).abs() < 1e-6);
658
659 if !all_integer {
660 return false;
661 }
662
663 let mut distinct: Vec<f64> = Vec::new();
664 for &o in &self.outputs {
665 let val = o.round();
666 if !distinct.iter().any(|&v| (v - val).abs() < 1e-10) {
667 distinct.push(val);
668 if distinct.len() > 20 {
669 return false;
670 }
671 }
672 }
673
674 let n = self.len();
675 let n_distinct = distinct.len();
676
677 if n < 10 {
680 return n_distinct <= 3;
681 }
682
683 let max_classes = (n as f64).sqrt().ceil() as usize;
687 n_distinct <= max_classes.min(20)
688 }
689}
690
691impl<P: DataPoint + Clone> Default for Renegade<P> {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697#[cfg(test)]
698mod tests;