renegade_ml/
diagnostics.rs1use crate::metric::LearnedMetric;
2use crate::{DataPoint, Renegade};
3
4#[derive(Debug, Clone)]
6pub struct ModelDiagnostics {
7 pub num_entries: usize,
9 pub optimal_k: Option<usize>,
11 pub metric_active: bool,
13 pub kernel_bandwidth: Option<f64>,
15 pub trained_at: usize,
17 pub entries_since_training: usize,
19 pub is_classification: bool,
21 pub feature_metrics: Option<Vec<FeatureDiagnostics>>,
23 pub output_stats: OutputStats,
25}
26
27#[derive(Debug, Clone)]
29pub struct FeatureDiagnostics {
30 pub index: usize,
32 pub weight: f64,
34 pub effect_curve: Vec<(f64, f64)>,
37}
38
39#[derive(Debug, Clone)]
41pub struct OutputStats {
42 pub min: f64,
43 pub max: f64,
44 pub mean: f64,
45 pub num_distinct: usize,
47}
48
49#[derive(Debug, Clone)]
51pub struct PredictionDiagnostics {
52 pub prediction: f64,
54 pub k: usize,
56 pub neighbors: Vec<NeighborDetail>,
58}
59
60#[derive(Debug, Clone)]
62pub struct NeighborDetail {
63 pub distance: f64,
65 pub output: f64,
67 pub feature_distances: Option<Vec<f64>>,
69}
70
71impl<P: DataPoint + Clone> Renegade<P> {
72 pub fn diagnostics(&self) -> ModelDiagnostics {
74 let output_stats = self.compute_output_stats();
75 let is_classification = self.detect_classification();
76
77 let feature_metrics = self
78 .learned_metric
79 .as_ref()
80 .map(|metric| metric.feature_diagnostics());
81
82 ModelDiagnostics {
83 num_entries: self.len(),
84 optimal_k: self.optimal_k,
85 metric_active: self.learned_metric.is_some(),
86 kernel_bandwidth: self.kernel_bandwidth,
87 trained_at: self.computed_at,
88 entries_since_training: self.len().saturating_sub(self.computed_at),
89 is_classification,
90 feature_metrics,
91 output_stats,
92 }
93 }
94
95 pub fn predict_with_diagnostics(&self, query: &P, k: usize) -> PredictionDiagnostics {
97 let query_values = query.feature_values();
98 let n = self.len();
99 let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n);
100 for i in 0..n {
101 let dist = self.distance_to_entry(&query_values, query, i);
102 distances.push((i, dist));
103 }
104
105 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
106 distances.truncate(k);
107
108 let neighbors: Vec<NeighborDetail> = distances
109 .iter()
110 .map(|&(i, dist)| {
111 let feature_distances = if self.learned_metric.is_none() {
112 Some(query.feature_distances(&self.points[i]))
113 } else {
114 None
115 };
116 NeighborDetail {
117 distance: dist,
118 output: self.outputs[i],
119 feature_distances,
120 }
121 })
122 .collect();
123
124 let prediction = if neighbors.is_empty() {
126 f64::NAN
127 } else if let Some(h) = self.kernel_bandwidth {
128 let h2 = 2.0 * h * h;
130 let exact: Vec<&NeighborDetail> =
131 neighbors.iter().filter(|n| n.distance == 0.0).collect();
132 if !exact.is_empty() {
133 exact[0].output
134 } else {
135 let mut ws = 0.0;
136 let mut vs = 0.0;
137 for n in &neighbors {
138 let w = (-n.distance * n.distance / h2).exp();
139 if w < 1e-15 {
140 break;
141 }
142 ws += w;
143 vs += w * n.output;
144 }
145 if ws > 0.0 {
146 vs / ws
147 } else {
148 neighbors[0].output
149 }
150 }
151 } else {
152 let mut exact = None;
154 let mut ws = 0.0;
155 let mut vs = 0.0;
156 for n in &neighbors {
157 if n.distance == 0.0 {
158 exact = Some(n.output);
159 break;
160 }
161 let w = 1.0 / n.distance;
162 ws += w;
163 vs += w * n.output;
164 }
165 exact.unwrap_or_else(|| if ws > 0.0 { vs / ws } else { f64::NAN })
166 };
167
168 PredictionDiagnostics {
169 prediction,
170 k,
171 neighbors,
172 }
173 }
174
175 fn compute_output_stats(&self) -> OutputStats {
176 if self.is_empty() {
177 return OutputStats {
178 min: f64::NAN,
179 max: f64::NAN,
180 mean: f64::NAN,
181 num_distinct: 0,
182 };
183 }
184
185 let mut min = f64::MAX;
186 let mut max = f64::MIN;
187 let mut sum = 0.0;
188 let mut distinct: Vec<f64> = Vec::new();
189
190 for &o in &self.outputs {
191 min = min.min(o);
192 max = max.max(o);
193 sum += o;
194 if !distinct.iter().any(|&v| (v - o).abs() < 1e-10) {
195 distinct.push(o);
196 }
197 }
198
199 OutputStats {
200 min,
201 max,
202 mean: sum / self.outputs.len() as f64,
203 num_distinct: distinct.len(),
204 }
205 }
206}
207
208impl LearnedMetric {
209 pub fn feature_diagnostics(&self) -> Vec<FeatureDiagnostics> {
211 self.effect_regressions
212 .iter()
213 .zip(self.weights.iter())
214 .enumerate()
215 .map(|(i, (reg, &weight))| {
216 let points = reg.get_points_sorted();
217 let effect_curve: Vec<(f64, f64)> =
218 points.iter().map(|p| (*p.x(), *p.y())).collect();
219
220 FeatureDiagnostics {
221 index: i,
222 weight,
223 effect_curve,
224 }
225 })
226 .collect()
227 }
228}