use crate::metric::LearnedMetric;
use crate::{DataPoint, Renegade};
#[derive(Debug, Clone)]
pub struct ModelDiagnostics {
pub num_entries: usize,
pub optimal_k: Option<usize>,
pub metric_active: bool,
pub kernel_bandwidth: Option<f64>,
pub trained_at: usize,
pub entries_since_training: usize,
pub is_classification: bool,
pub feature_metrics: Option<Vec<FeatureDiagnostics>>,
pub output_stats: OutputStats,
}
#[derive(Debug, Clone)]
pub struct FeatureDiagnostics {
pub index: usize,
pub weight: f64,
pub effect_curve: Vec<(f64, f64)>,
}
#[derive(Debug, Clone)]
pub struct OutputStats {
pub min: f64,
pub max: f64,
pub mean: f64,
pub num_distinct: usize,
}
#[derive(Debug, Clone)]
pub struct PredictionDiagnostics {
pub prediction: f64,
pub k: usize,
pub neighbors: Vec<NeighborDetail>,
}
#[derive(Debug, Clone)]
pub struct NeighborDetail {
pub distance: f64,
pub output: f64,
pub feature_distances: Option<Vec<f64>>,
}
impl<P: DataPoint + Clone> Renegade<P> {
pub fn diagnostics(&self) -> ModelDiagnostics {
let output_stats = self.compute_output_stats();
let is_classification = self.detect_classification();
let feature_metrics = self
.learned_metric
.as_ref()
.map(|metric| metric.feature_diagnostics());
ModelDiagnostics {
num_entries: self.len(),
optimal_k: self.optimal_k,
metric_active: self.learned_metric.is_some(),
kernel_bandwidth: self.kernel_bandwidth,
trained_at: self.computed_at,
entries_since_training: self.len().saturating_sub(self.computed_at),
is_classification,
feature_metrics,
output_stats,
}
}
pub fn predict_with_diagnostics(&self, query: &P, k: usize) -> PredictionDiagnostics {
let query_values = query.feature_values();
let n = self.len();
let mut distances: Vec<(usize, f64)> = Vec::with_capacity(n);
for i in 0..n {
let dist = self.distance_to_entry(&query_values, query, i);
distances.push((i, dist));
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
distances.truncate(k);
let neighbors: Vec<NeighborDetail> = distances
.iter()
.map(|&(i, dist)| {
let feature_distances = if self.learned_metric.is_none() {
Some(query.feature_distances(&self.points[i]))
} else {
None
};
NeighborDetail {
distance: dist,
output: self.outputs[i],
feature_distances,
}
})
.collect();
let prediction = if neighbors.is_empty() {
f64::NAN
} else {
let mut exact = None;
let mut ws = 0.0;
let mut vs = 0.0;
for n in &neighbors {
if n.distance == 0.0 {
exact = Some(n.output);
break;
}
let w = 1.0 / n.distance;
ws += w;
vs += w * n.output;
}
exact.unwrap_or_else(|| if ws > 0.0 { vs / ws } else { f64::NAN })
};
PredictionDiagnostics {
prediction,
k,
neighbors,
}
}
fn compute_output_stats(&self) -> OutputStats {
if self.is_empty() {
return OutputStats {
min: f64::NAN,
max: f64::NAN,
mean: f64::NAN,
num_distinct: 0,
};
}
let mut min = f64::MAX;
let mut max = f64::MIN;
let mut sum = 0.0;
let mut distinct: Vec<f64> = Vec::new();
for &o in &self.outputs {
min = min.min(o);
max = max.max(o);
sum += o;
if !distinct.iter().any(|&v| (v - o).abs() < 1e-10) {
distinct.push(o);
}
}
OutputStats {
min,
max,
mean: sum / self.outputs.len() as f64,
num_distinct: distinct.len(),
}
}
}
impl LearnedMetric {
pub fn feature_diagnostics(&self) -> Vec<FeatureDiagnostics> {
self.effect_regressions
.iter()
.zip(self.weights.iter())
.enumerate()
.map(|(i, (reg, &weight))| {
let points = reg.get_points_sorted();
let effect_curve: Vec<(f64, f64)> =
points.iter().map(|p| (*p.x(), *p.y())).collect();
FeatureDiagnostics {
index: i,
weight,
effect_curve,
}
})
.collect()
}
}