2024-02-29 21:04:50 +02:00
|
|
|
use pyo3::prelude::*;
|
|
|
|
|
use pyo3::types::PyList;
|
2024-03-01 12:26:05 +02:00
|
|
|
use pyo3::wrap_pyfunction;
|
2024-02-10 19:58:33 +02:00
|
|
|
use serde::Serialize;
|
2024-03-01 15:45:26 +02:00
|
|
|
use serde_json::to_string_pretty;
|
2024-02-10 14:05:25 +02:00
|
|
|
|
2024-02-10 19:58:33 +02:00
|
|
|
#[derive(Clone, Debug, Serialize)]
|
2024-02-10 14:05:25 +02:00
|
|
|
struct Point {
|
|
|
|
|
value: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Point {
|
|
|
|
|
fn new(value: u32) -> Self {
|
2024-02-10 17:28:59 +02:00
|
|
|
Point { value }
|
2024-02-10 14:05:25 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-10 19:58:33 +02:00
|
|
|
#[derive(Debug, Clone, Serialize)]
|
2024-02-10 17:28:59 +02:00
|
|
|
struct ClusterGapInfo {
|
2024-03-01 12:26:05 +02:00
|
|
|
span_length: f32,
|
|
|
|
|
num_elements: usize,
|
|
|
|
|
centroid: f32,
|
|
|
|
|
z_score: Option<f32>,
|
2024-02-10 17:28:59 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-01 12:46:34 +02:00
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
ClusterGapInfo {
|
|
|
|
|
span_length,
|
|
|
|
|
num_elements,
|
|
|
|
|
centroid,
|
|
|
|
|
z_score: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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.
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
/// * `dataset`: A slice of `Point` objects representing the dataset to be analyzed.
|
|
|
|
|
/// * `factor`: A multiplier used to define the thresholds for clustering and gap identification.
|
|
|
|
|
/// A lower factor tightens the cluster threshold and widens the gap threshold, and vice versa.
|
|
|
|
|
/// * `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.
|
|
|
|
|
///
|
2024-02-10 17:28:59 +02:00
|
|
|
fn calculate_densities_and_gaps(dataset: &[Point], factor: f32, min_cluster_size: usize) -> Vec<ClusterGapInfo> {
|
2024-03-01 12:46:34 +02:00
|
|
|
|
|
|
|
|
// Return early if the dataset is too small to form any clusters or gaps.
|
2024-03-01 12:26:05 +02:00
|
|
|
if dataset.len() < 2 { return Vec::new(); }
|
2024-02-10 15:07:21 +02:00
|
|
|
|
2024-03-01 12:46:34 +02:00
|
|
|
// Calculate the mean distance between consecutive points in the dataset.
|
2024-02-10 17:28:59 +02:00
|
|
|
let mean_distance = dataset.windows(2)
|
2024-03-01 12:46:34 +02:00
|
|
|
.map(|w| w[1].value as f32 - w[0].value as f32)
|
2024-02-10 17:28:59 +02:00
|
|
|
.sum::<f32>() / (dataset.len() - 1) as f32;
|
2024-03-01 12:46:34 +02:00
|
|
|
|
|
|
|
|
// Define thresholds for clustering and gap identification based on the mean distance and factor.
|
2024-03-01 12:26:05 +02:00
|
|
|
let cluster_threshold = mean_distance / factor;
|
2024-02-10 17:28:59 +02:00
|
|
|
let gap_threshold = factor * mean_distance * 2.0;
|
|
|
|
|
|
2024-03-01 12:46:34 +02:00
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// If the distance between points is within the cluster threshold, add to current cluster.
|
|
|
|
|
if gap_distance <= cluster_threshold {
|
|
|
|
|
if current_cluster.is_empty() {
|
|
|
|
|
current_cluster.push(window[0].clone()); // Start a new cluster with the first point.
|
|
|
|
|
}
|
|
|
|
|
current_cluster.push(window[1].clone()); // Add the second point to the cluster.
|
|
|
|
|
} else {
|
|
|
|
|
// If the current cluster is large enough, finalize it and prepare for a new cluster.
|
|
|
|
|
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
|
|
|
|
|
results.push(create_cluster_info(¤t_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.
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-02-10 14:05:25 +02:00
|
|
|
}
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finalize the last cluster if it meets the size requirement.
|
|
|
|
|
if !current_cluster.is_empty() && current_cluster.len() >= min_cluster_size {
|
|
|
|
|
results.push(create_cluster_info(¤t_cluster));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results
|
2024-02-29 21:04:50 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-01 15:45:26 +02:00
|
|
|
/// Analyzes a dataset of integers to identify clusters and gaps, then calculates Z-scores
|
|
|
|
|
/// for each based on their deviation from mean metrics. The analysis aims to highlight
|
|
|
|
|
/// significant clusters of closely grouped points and notable gaps between them, providing
|
|
|
|
|
/// a statistical measure of their significance through Z-scores. The results, including
|
|
|
|
|
/// clusters, gaps, and their Z-scores, are serialized into a JSON string.
|
2024-03-01 12:46:34 +02:00
|
|
|
///
|
|
|
|
|
/// # Arguments
|
2024-03-01 15:45:26 +02:00
|
|
|
/// * `_py` - The Python interpreter instance, used for Python-Rust interoperability.
|
|
|
|
|
/// This argument is necessary for functions exposed to Python via PyO3 but is not
|
|
|
|
|
/// directly used within the function.
|
|
|
|
|
/// * `int_list` - A Python list of integers representing the dataset to be analyzed.
|
|
|
|
|
/// This list is converted into a Vec<Point> for internal processing.
|
|
|
|
|
/// * `factor` - A floating-point value used as a threshold factor to adjust the sensitivity
|
|
|
|
|
/// of cluster and gap detection. This factor influences the identification of clusters
|
|
|
|
|
/// by defining the minimum density or separation required.
|
|
|
|
|
/// * `min_cluster_size` - An integer specifying the minimum number of contiguous points
|
|
|
|
|
/// required for a group of points to be considered a cluster. This parameter helps
|
|
|
|
|
/// filter out noise by defining a threshold for the minimum cluster size.
|
2024-03-01 12:46:34 +02:00
|
|
|
///
|
|
|
|
|
/// # Returns
|
2024-03-01 15:45:26 +02:00
|
|
|
/// Returns a `PyResult<String>` containing a JSON-formatted string of the analysis results.
|
|
|
|
|
/// The JSON string includes detailed information about each identified cluster and gap,
|
|
|
|
|
/// such as their span length, number of elements (if applicable), centroid, and calculated
|
|
|
|
|
/// Z-score. In case of an error during processing or serialization, a Python exception is
|
|
|
|
|
/// returned.
|
2024-03-01 12:46:34 +02:00
|
|
|
///
|
2024-02-29 21:04:50 +02:00
|
|
|
#[pyfunction]
|
2024-03-01 12:26:05 +02:00
|
|
|
fn lyagushka(_py: Python, int_list: &PyList, factor: f32, min_cluster_size: usize) -> PyResult<String> {
|
2024-03-01 15:45:26 +02:00
|
|
|
// Extract integers from a Python list and create a vector of Point structs.
|
|
|
|
|
let dataset: Vec<Point> = int_list.extract::<Vec<u32>>()?
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(Point::new)
|
|
|
|
|
.collect();
|
2024-03-01 12:26:05 +02:00
|
|
|
|
2024-03-01 15:45:26 +02:00
|
|
|
// Calculate clusters and gaps from the dataset using predefined criteria.
|
2024-03-01 12:46:34 +02:00
|
|
|
let mut cluster_gap_infos = calculate_densities_and_gaps(&dataset, factor, min_cluster_size);
|
|
|
|
|
|
2024-03-01 15:45:26 +02:00
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
// Update Z-scores for both clusters and gaps based on their deviation from mean metrics.
|
|
|
|
|
for info in &mut cluster_gap_infos {
|
|
|
|
|
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;
|
|
|
|
|
info.z_score = Some((cluster_density - mean_density) / std_dev_density);
|
2024-03-01 12:46:34 +02:00
|
|
|
} else {
|
2024-03-01 15:45:26 +02:00
|
|
|
// Calculate and update Z-score for gaps based on span length deviation.
|
|
|
|
|
info.z_score = Some((info.span_length - average_span) / std_dev_density);
|
|
|
|
|
}
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-01 15:45:26 +02:00
|
|
|
// Serialize the updated cluster and gap information, including Z-scores, to a JSON string.
|
|
|
|
|
to_string_pretty(&cluster_gap_infos)
|
2024-03-01 12:26:05 +02:00
|
|
|
.map_err(|e| PyErr::new::<pyo3::exceptions::PyException, _>(format!("JSON Serialization Error: {}", e)))
|
2024-02-29 21:04:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[pymodule]
|
2024-03-01 13:51:42 +02:00
|
|
|
fn pyagushka(_py: Python, m: &PyModule) -> PyResult<()> {
|
2024-03-01 12:26:05 +02:00
|
|
|
m.add_function(wrap_pyfunction!(lyagushka, m)?)?;
|
2024-02-10 14:05:25 +02:00
|
|
|
Ok(())
|
2024-03-01 12:46:34 +02:00
|
|
|
}
|