2024-02-29 21:04:50 +02:00
|
|
|
use pyo3::prelude::*;
|
2024-02-10 19:58:33 +02:00
|
|
|
use serde::Serialize;
|
2024-02-10 14:05:25 +02:00
|
|
|
|
2024-02-10 19:58:33 +02:00
|
|
|
#[derive(Debug, Clone, Serialize)]
|
2024-03-01 18:35:31 +02:00
|
|
|
struct Anomaly {
|
|
|
|
|
elements: Vec<i32>,
|
|
|
|
|
start: i32,
|
|
|
|
|
end: i32,
|
|
|
|
|
span_length: i32,
|
2024-03-01 12:26:05 +02:00
|
|
|
num_elements: usize,
|
|
|
|
|
centroid: f32,
|
|
|
|
|
z_score: Option<f32>,
|
2024-02-10 17:28:59 +02:00
|
|
|
}
|
|
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
impl Anomaly {
|
|
|
|
|
|
|
|
|
|
pub fn new(cluster: &[i32]) -> Self {
|
|
|
|
|
let num_elements: usize = cluster.len();
|
|
|
|
|
let start: i32 = *cluster.first().expect("Cluster has no start");
|
|
|
|
|
let end: i32 = *cluster.last().expect("Cluster has no end");
|
|
|
|
|
let span_length: i32 = end - start;
|
|
|
|
|
let centroid: f32 = start as f32 + span_length as f32 / 2.0;
|
|
|
|
|
|
|
|
|
|
Anomaly {
|
|
|
|
|
elements: cluster.to_vec(),
|
|
|
|
|
start,
|
|
|
|
|
end,
|
|
|
|
|
span_length,
|
|
|
|
|
num_elements,
|
|
|
|
|
centroid,
|
|
|
|
|
z_score: None,
|
|
|
|
|
}
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
#[pyclass]
|
2024-04-09 08:35:31 +03:00
|
|
|
pub struct Lyagushka {
|
2024-04-09 07:47:42 +03:00
|
|
|
dataset: Vec<i32>,
|
|
|
|
|
anomalies: Vec<Anomaly>,
|
|
|
|
|
}
|
2024-03-01 18:35:31 +02:00
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
#[pymethods]
|
|
|
|
|
impl Lyagushka {
|
|
|
|
|
|
|
|
|
|
#[new]
|
|
|
|
|
pub fn new(dataset: Vec<i32>) -> Self {
|
|
|
|
|
Lyagushka {
|
|
|
|
|
dataset,
|
|
|
|
|
anomalies: vec![]
|
2024-02-10 14:05:25 +02:00
|
|
|
}
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
|
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
fn scan_anomalies(&mut self, factor: f32, min_cluster_size: usize) {
|
|
|
|
|
|
|
|
|
|
// Calculate the mean distance between consecutive points in the dataset.
|
|
|
|
|
let mean_distance: f32 = self.dataset.windows(2)
|
|
|
|
|
.map(|w| (w[1] - w[0]) as f32)
|
|
|
|
|
.sum::<f32>() / (self.dataset.len() - 1) as f32;
|
|
|
|
|
|
|
|
|
|
// Define thresholds for clustering and gap identification based on the mean distance and factor.
|
|
|
|
|
let cluster_threshold: f32 = mean_distance / factor;
|
|
|
|
|
let gap_threshold: f32 = factor * mean_distance;
|
|
|
|
|
|
|
|
|
|
let mut current_cluster: Vec<i32> = Vec::new(); // Temporary storage for points in the current cluster.
|
|
|
|
|
|
|
|
|
|
// Iterate through pairs of consecutive points to find clusters and significant gaps.
|
|
|
|
|
for window in self.dataset.windows(2) {
|
|
|
|
|
let gap_size: f32 = (window[1] - window[0]) as f32;
|
|
|
|
|
|
|
|
|
|
if gap_size <= cluster_threshold {
|
|
|
|
|
// Add points to the current cluster
|
|
|
|
|
if current_cluster.is_empty() {
|
|
|
|
|
current_cluster.push(window[0]); // Start a new cluster with the first point
|
|
|
|
|
}
|
|
|
|
|
current_cluster.push(window[1]); // Add the second point to the cluster
|
|
|
|
|
} else {
|
|
|
|
|
// End the current cluster and start a new gap
|
|
|
|
|
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
|
|
|
|
|
self.anomalies.push(Anomaly::new(¤t_cluster));
|
|
|
|
|
current_cluster.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Record the gap
|
|
|
|
|
if gap_size > gap_threshold {
|
|
|
|
|
self.anomalies.push(Anomaly {
|
|
|
|
|
elements: Vec::new(), // No elements in a gap
|
|
|
|
|
start: window[0],
|
|
|
|
|
end: window[1],
|
|
|
|
|
span_length: gap_size as i32,
|
|
|
|
|
num_elements: 0,
|
|
|
|
|
centroid: (window[0] as f32 + window[1] as f32) / 2.0,
|
|
|
|
|
z_score: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finalize the last cluster if applicable
|
|
|
|
|
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
|
|
|
|
|
self.anomalies.push(Anomaly::new(¤t_cluster));
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
|
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
pub fn search(&mut self, factor: f32, min_cluster_size: usize) -> String {
|
2024-03-01 15:45:26 +02:00
|
|
|
|
2024-04-09 07:47:42 +03:00
|
|
|
// Sort the vector
|
|
|
|
|
self.dataset.sort_unstable();
|
|
|
|
|
|
|
|
|
|
// Calculate clusters and gaps from the dataset using predefined criteria.
|
|
|
|
|
self.scan_anomalies(factor, min_cluster_size);
|
|
|
|
|
|
|
|
|
|
// Calculate the mean density of clusters in the dataset for comparison.
|
|
|
|
|
let mean_density: f32 = self.anomalies.iter()
|
|
|
|
|
.filter(|info: &&Anomaly| info.num_elements > 0)
|
|
|
|
|
.map(|info: &Anomaly| info.num_elements as f32 / info.span_length as f32)
|
|
|
|
|
.sum::<f32>() / self.anomalies.iter().filter(|info: &&Anomaly| info.num_elements > 0).count() as f32;
|
|
|
|
|
|
|
|
|
|
// Calculate the standard deviation of cluster densities to evaluate variation.
|
|
|
|
|
let variance_density: f32 = self.anomalies.iter()
|
|
|
|
|
.filter(|info: &&Anomaly| info.num_elements > 0)
|
|
|
|
|
.map(|info: &Anomaly| info.num_elements as f32 / info.span_length as f32)
|
|
|
|
|
.map(|density: f32| (density - mean_density).powi(2))
|
|
|
|
|
.sum::<f32>() / self.anomalies.iter().filter(|info: &&Anomaly| info.num_elements > 0).count() as f32;
|
|
|
|
|
let std_dev_density: f32 = variance_density.sqrt();
|
|
|
|
|
|
|
|
|
|
// Calculate mean span length
|
|
|
|
|
let mean_span_length: f32 = self.anomalies.iter()
|
|
|
|
|
.map(|info: &Anomaly| info.span_length as f32)
|
|
|
|
|
.sum::<f32>() / self.anomalies.len() as f32;
|
|
|
|
|
|
|
|
|
|
// Calculate variance
|
|
|
|
|
let variance: f32 = self.anomalies.iter()
|
|
|
|
|
.map(|info: &Anomaly| (info.span_length as f32 - mean_span_length).powi(2))
|
|
|
|
|
.sum::<f32>() / self.anomalies.len() as f32;
|
|
|
|
|
|
|
|
|
|
// Standard deviation is the square root of variance
|
|
|
|
|
let std_dev_span_length: f32 = variance.sqrt();
|
|
|
|
|
|
|
|
|
|
// Update Z-scores for both clusters and gaps based on their deviation from mean metrics.
|
|
|
|
|
for info in self.anomalies.iter_mut() {
|
|
|
|
|
if info.num_elements > 0 {
|
|
|
|
|
// Calculate and update Z-score for clusters based on density deviation.
|
|
|
|
|
let cluster_density: f32 = info.num_elements as f32 / info.span_length as f32;
|
|
|
|
|
info.z_score = Some((cluster_density - mean_density) / std_dev_density);
|
|
|
|
|
} else {
|
|
|
|
|
// Calculate and update Z-score for gaps based on span length deviation.
|
|
|
|
|
info.z_score = Some((info.span_length as f32 / std_dev_span_length) * -1.0);
|
|
|
|
|
}
|
2024-03-01 15:45:26 +02:00
|
|
|
}
|
2024-04-09 07:47:42 +03:00
|
|
|
|
|
|
|
|
serde_json::to_string_pretty(&self.anomalies).unwrap_or_else(|_| "Failed to serialize data".to_string())
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
2024-02-29 21:04:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[pymodule]
|
2024-04-09 08:35:31 +03:00
|
|
|
fn lyagushka(_py: Python, m: &PyModule) -> PyResult<()> {
|
2024-04-09 07:47:42 +03:00
|
|
|
m.add_class::<Lyagushka>()?;
|
2024-02-10 14:05:25 +02:00
|
|
|
Ok(())
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|