updated to HEAD

This commit is contained in:
randogoth 2024-03-01 18:46:18 +02:00
parent 9c3220fd14
commit 4df5591c57
2 changed files with 82 additions and 69 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
*.json

View file

@ -5,44 +5,41 @@ use std::process;
use serde::Serialize;
use serde_json;
#[derive(Clone, Debug, Serialize)]
struct Point {
value: u32,
}
impl Point {
fn new(value: u32) -> Self {
Point { value }
}
}
#[derive(Debug, Clone, Serialize)]
struct ClusterGapInfo {
span_length: f32,
struct Anomaly {
elements: Vec<i32>,
start: i32,
end: i32,
span_length: i32,
num_elements: usize,
centroid: f32,
z_score: Option<f32>,
}
fn create_cluster_info(cluster: &[Point]) -> ClusterGapInfo {
let num_elements = cluster.len();
let span_length = (cluster.last().unwrap().value as f32) - (cluster.first().unwrap().value as f32);
let centroid = cluster.iter().map(|p| p.value as f32).sum::<f32>() / num_elements as f32;
fn anomaly_info(cluster: &[i32]) -> Anomaly {
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;
ClusterGapInfo {
Anomaly {
elements: cluster.to_vec(),
start,
end,
span_length,
num_elements,
centroid,
z_score: None,
z_score: None, // Placeholder for actual Z-score calculation
}
}
/// Calculates the densities (clusters) and significant gaps between points in a dataset.
///
/// This function iterates over a dataset of points, identifying clusters based on a distance threshold
/// (calculated from the mean distance between points and adjusted by a given factor) and identifying significant gaps
/// that exceed a certain threshold. Each cluster or significant gap identified is summarized in a `ClusterGapInfo` object.
/// that exceed a certain threshold. Each cluster or significant gap identified is summarized in a `Anomaly` object.
///
/// # Arguments
/// * `dataset`: A slice of `Point` objects representing the dataset to be analyzed.
@ -51,57 +48,60 @@ fn create_cluster_info(cluster: &[Point]) -> ClusterGapInfo {
/// * `min_cluster_size`: The minimum number of points required for a group of points to be considered a cluster.
///
/// # Returns
/// A vector of `ClusterGapInfo` objects, each representing either a cluster of points or a significant gap between points.
/// A vector of `Anomaly` objects, each representing either a cluster of points or a significant gap between points.
///
fn calculate_densities_and_gaps(dataset: &[Point], factor: f32, min_cluster_size: usize) -> Vec<ClusterGapInfo> {
fn scan_anomalies(dataset: &[i32], factor: f32, min_cluster_size: usize) -> Vec<Anomaly> {
// Return early if the dataset is too small to form any clusters or gaps.
if dataset.len() < 2 { return Vec::new(); }
// Calculate the mean distance between consecutive points in the dataset.
let mean_distance = dataset.windows(2)
.map(|w| w[1].value as f32 - w[0].value as f32)
.sum::<f32>() / (dataset.len() - 1) as f32;
let mean_distance: f32 = dataset.windows(2)
.map(|w| (w[1] - w[0]) as f32)
.sum::<f32>() / (dataset.len() - 1) as f32;
// Define thresholds for clustering and gap identification based on the mean distance and factor.
let cluster_threshold = mean_distance / factor;
let gap_threshold = factor * mean_distance * 2.0;
let cluster_threshold: f32 = mean_distance / factor;
let gap_threshold: f32 = factor * mean_distance;
let mut results: Vec<ClusterGapInfo> = Vec::new(); // Stores the resulting clusters and gaps.
let mut current_cluster: Vec<Point> = Vec::new(); // Temporary storage for points in the current cluster.
let mut results: Vec<Anomaly> = Vec::new(); // Stores the resulting clusters and gaps.
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 dataset.windows(2) {
let gap_distance = window[1].value as f32 - window[0].value as f32;
let gap_size: f32 = (window[1] - window[0]) as f32;
// If the distance between points is within the cluster threshold, add to current cluster.
if gap_distance <= cluster_threshold {
if gap_size <= cluster_threshold {
// Add points to the current cluster
if current_cluster.is_empty() {
current_cluster.push(window[0].clone()); // Start a new cluster with the first point.
current_cluster.push(window[0]); // Start a new cluster with the first point
}
current_cluster.push(window[1].clone()); // Add the second point to the cluster.
current_cluster.push(window[1]); // Add the second point to the cluster
} else {
// If the current cluster is large enough, finalize it and prepare for a new cluster.
// End the current cluster and start a new gap
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
results.push(create_cluster_info(&current_cluster));
results.push(anomaly_info(&current_cluster));
current_cluster.clear();
}
// If the gap between points is significant, record it as a gap.
if gap_distance > gap_threshold {
results.push(ClusterGapInfo {
span_length: gap_distance,
num_elements: 0, // Indicating this is a gap, not a cluster.
centroid: (window[0].value as f32 + window[1].value as f32) / 2.0,
z_score: None, // Z-score will be calculated later if necessary.
// Record the gap
if gap_size > gap_threshold {
results.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 it meets the size requirement.
// Finalize the last cluster if applicable
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
results.push(create_cluster_info(&current_cluster));
results.push(anomaly_info(&current_cluster));
}
results
@ -125,41 +125,54 @@ fn calculate_densities_and_gaps(dataset: &[Point], factor: f32, min_cluster_size
/// # Returns
/// Returns a `String` containing the JSON-serialized analysis results, including clusters and gaps with their z-scores.
///
fn lyagushka(dataset: Vec<Point>, factor: f32, min_cluster_size: usize) -> String {
fn lyagushka(mut dataset: Vec<i32>, factor: f32, min_cluster_size: usize) -> String {
// Sort the vector
dataset.sort_unstable();
// Calculate clusters and gaps from the dataset using predefined criteria.
let mut cluster_gap_infos = calculate_densities_and_gaps(&dataset, factor, min_cluster_size);
let mut anomalies: Vec<Anomaly> = scan_anomalies(&dataset, factor, min_cluster_size);
// Calculate the mean density of clusters in the dataset for comparison.
let mean_density: f32 = cluster_gap_infos.iter()
.filter(|info| info.num_elements > 0)
.map(|info| info.num_elements as f32 / info.span_length)
.sum::<f32>() / cluster_gap_infos.iter().filter(|info| info.num_elements > 0).count() as f32;
let mean_density: f32 = anomalies.iter()
.filter(|info: &&Anomaly| info.num_elements > 0)
.map(|info: &Anomaly| info.num_elements as f32 / info.span_length as f32)
.sum::<f32>() / 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 = cluster_gap_infos.iter()
.filter(|info| info.num_elements > 0)
.map(|info| info.num_elements as f32 / info.span_length)
.map(|density| (density - mean_density).powi(2))
.sum::<f32>() / cluster_gap_infos.iter().filter(|info| info.num_elements > 0).count() as f32;
let std_dev_density = variance_density.sqrt();
let variance_density: f32 = 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>() / anomalies.iter().filter(|info: &&Anomaly| info.num_elements > 0).count() as f32;
let std_dev_density: f32 = variance_density.sqrt();
// Calculate the average span of all clusters and gaps to assess gap significance.
let average_span: f32 = cluster_gap_infos.iter().map(|info| info.span_length).sum::<f32>() / cluster_gap_infos.len() as f32;
// Calculate mean span length
let mean_span_length: f32 = anomalies.iter()
.map(|info: &Anomaly| info.span_length as f32)
.sum::<f32>() / anomalies.len() as f32;
// Calculate variance
let variance: f32 = anomalies.iter()
.map(|info: &Anomaly| (info.span_length as f32 - mean_span_length).powi(2))
.sum::<f32>() / 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 &mut cluster_gap_infos {
for info in anomalies.iter_mut() {
if info.num_elements > 0 {
// Calculate and update Z-score for clusters based on density deviation.
let cluster_density = info.num_elements as f32 / info.span_length;
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 - average_span) / std_dev_density);
info.z_score = Some((info.span_length as f32 / std_dev_span_length) * -1.0);
}
}
serde_json::to_string_pretty(&cluster_gap_infos).unwrap_or_else(|_| "Failed to serialize data".to_string())
serde_json::to_string_pretty(&anomalies).unwrap_or_else(|_| "Failed to serialize data".to_string())
}
/// The entry point for the command-line tool that reads a dataset of integers from either a file or stdin,
@ -201,7 +214,7 @@ fn main() -> io::Result<()> {
let args: Vec<String> = env::args().collect();
// Input handling
let dataset: Vec<Point> = if atty::is(atty::Stream::Stdin) {
let dataset: Vec<i32> = if atty::is(atty::Stream::Stdin) {
if args.len() != 4 {
eprintln!("Usage: {} <filename> <factor> <min_cluster_size>", args[0]);
process::exit(1);
@ -209,16 +222,15 @@ fn main() -> io::Result<()> {
let filename = &args[1];
let file = File::open(filename)?;
BufReader::new(file).lines().filter_map(Result::ok)
.filter_map(|line| line.trim().parse::<u32>().ok())
.map(Point::new)
.filter_map(|line| line.trim().parse::<i32>().ok()) // Directly parse to i32
.collect()
} else {
stdin().lock().lines().filter_map(Result::ok)
.filter_map(|line| line.trim().parse::<u32>().ok())
.map(Point::new)
.filter_map(|line| line.trim().parse::<i32>().ok()) // Directly parse to i32
.collect()
};
let factor: f32 = args[args.len() - 2].parse().expect("Factor must be a float");
let min_cluster_size: usize = args[args.len() - 1].parse().expect("Min cluster size must be an integer");