impl DataScienceAnalyzer {
#[allow(clippy::cast_possible_truncation)]
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub fn detect_anomalies(&self, findings: &mut [Finding]) -> Vec<AnomalyPoint> {
if findings.len() < 5 {
return Vec::new();
}
let vectors: Vec<Vec<f32>> = findings
.iter()
.map(|f| self.finding_to_features(f))
.collect();
let n = vectors.len() as f32;
let num_features = vectors[0].len();
let mut means = vec![0.0f32; num_features];
let mut stds = vec![0.0f32; num_features];
for vec in &vectors {
for (i, &v) in vec.iter().enumerate() {
means[i] += v / n;
}
}
for vec in &vectors {
for (i, &v) in vec.iter().enumerate() {
stds[i] += (v - means[i]).powi(2) / n;
}
}
for std in &mut stds {
*std = std.sqrt().max(0.001); }
let mut anomalies = Vec::new();
for (finding, vec) in findings.iter_mut().zip(vectors.iter()) {
let max_z_score: f32 = vec
.iter()
.zip(means.iter())
.zip(stds.iter())
.map(|((&v, &mean), &std)| ((v - mean) / std).abs())
.fold(0.0f32, f32::max);
let anomaly_score = 1.0 / (1.0 + (-max_z_score + 2.0).exp());
finding.anomaly_score = Some(anomaly_score);
if anomaly_score as f64 >= self.anomaly_threshold {
anomalies.push(AnomalyPoint {
finding_id: finding.id.clone(),
score: anomaly_score as f64,
reason: self.explain_anomaly(finding, vec, &means, &stds),
action: self.suggest_anomaly_action(finding),
});
}
}
anomalies
}
fn explain_anomaly(
&self,
finding: &Finding,
features: &[f32],
means: &[f32],
stds: &[f32],
) -> String {
let mut reasons = Vec::new();
let feature_names = [
"severity",
"confidence",
"category",
"file_path",
"line_number",
"has_fix",
];
for (i, (&v, (&mean, &std))) in features
.iter()
.zip(means.iter().zip(stds.iter()))
.enumerate()
{
let z_score = (v - mean) / std;
if z_score.abs() > 2.0 && i < feature_names.len() {
reasons.push(format!("unusual {} (z={:.1})", feature_names[i], z_score));
}
}
if reasons.is_empty() {
format!("Unusual pattern in {}", finding.category)
} else {
reasons.join(", ")
}
}
fn suggest_anomaly_action(&self, finding: &Finding) -> String {
match finding.severity {
super::types::Severity::Critical => "Immediate review required".to_string(),
super::types::Severity::High => "Schedule for next sprint".to_string(),
super::types::Severity::Medium => "Add to backlog".to_string(),
super::types::Severity::Low => "Monitor for recurrence".to_string(),
}
}
}