use scirs2_core::ndarray::{s, Array2, ArrayView2, Axis};
use sklears_core::{
error::{Result as SklResult, SklearsError},
types::Float,
};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct CopulaBasedModelingAnalyzer {
copula_types: Vec<CopulaType>,
fit_margins: bool,
use_empirical_copula: bool,
n_samples: usize,
random_state: Option<u64>,
}
impl CopulaBasedModelingAnalyzer {
pub fn new() -> Self {
Self {
copula_types: vec![CopulaType::Gaussian],
fit_margins: true,
use_empirical_copula: false,
n_samples: 1000,
random_state: None,
}
}
pub fn copula_types(mut self, copula_types: Vec<CopulaType>) -> Self {
self.copula_types = copula_types;
self
}
pub fn fit_margins(mut self, fit_margins: bool) -> Self {
self.fit_margins = fit_margins;
self
}
pub fn use_empirical_copula(mut self, use_empirical_copula: bool) -> Self {
self.use_empirical_copula = use_empirical_copula;
self
}
pub fn n_samples(mut self, n_samples: usize) -> Self {
self.n_samples = n_samples;
self
}
pub fn random_state(mut self, random_state: Option<u64>) -> Self {
self.random_state = random_state;
self
}
pub fn analyze(&self, _outputs: &HashMap<String, Array2<Float>>) -> SklResult<CopulaAnalysis> {
let copula_models = HashMap::new();
let marginal_distributions = HashMap::new();
let goodness_of_fit = HashMap::new();
let dependence_measures = HashMap::new();
let output_info = HashMap::new();
Ok(CopulaAnalysis {
copula_models,
marginal_distributions,
goodness_of_fit,
dependence_measures,
best_copula: None,
output_info,
empirical_copula: None,
})
}
}
impl Default for CopulaBasedModelingAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CopulaType {
Gaussian,
Clayton,
Frank,
Gumbel,
StudentT,
Archimedean,
Empirical,
}
#[derive(Debug, Clone)]
pub struct CopulaAnalysis {
pub copula_models: HashMap<CopulaType, CopulaModel>,
pub marginal_distributions: HashMap<String, MarginalDistribution>,
pub goodness_of_fit: HashMap<CopulaType, GoodnessOfFit>,
pub dependence_measures: HashMap<CopulaType, DependenceMeasures>,
pub best_copula: Option<CopulaType>,
pub output_info: HashMap<String, usize>,
pub empirical_copula: Option<EmpiricalCopula>,
}
#[derive(Debug, Clone)]
pub struct CopulaModel {
pub copula_type: CopulaType,
pub parameters: CopulaParameters,
pub log_likelihood: Float,
pub n_parameters: usize,
pub fitted_data: Array2<Float>,
}
#[derive(Debug, Clone)]
pub enum CopulaParameters {
Gaussian { correlation_matrix: Array2<Float> },
Clayton { theta: Float },
Frank { theta: Float },
Gumbel { theta: Float },
StudentT {
correlation_matrix: Array2<Float>,
degrees_of_freedom: Float,
},
Archimedean { generator_params: Vec<Float> },
Empirical,
}
#[derive(Debug, Clone)]
pub struct MarginalDistribution {
pub distribution_type: String,
pub parameters: Vec<Float>,
pub mean: Float,
pub std_dev: Float,
pub min: Float,
pub max: Float,
}
#[derive(Debug, Clone)]
pub struct GoodnessOfFit {
pub aic: Float,
pub bic: Float,
pub cramer_von_mises: Float,
pub kolmogorov_smirnov: Float,
pub anderson_darling: Float,
pub p_value: Float,
}
#[derive(Debug, Clone)]
pub struct DependenceMeasures {
pub kendall_tau: Float,
pub spearman_rho: Float,
pub tail_dependence: TailDependence,
pub conditional_measures: Vec<ConditionalMeasure>,
}
#[derive(Debug, Clone)]
pub struct TailDependence {
pub lower_tail: Float,
pub upper_tail: Float,
pub asymmetry: Float,
}
#[derive(Debug, Clone)]
pub struct ConditionalMeasure {
pub condition_vars: Vec<usize>,
pub conditional_dependence: Float,
pub conditional_correlation: Float,
}
#[derive(Debug, Clone)]
pub struct EmpiricalCopula {
pub copula_values: Array2<Float>,
pub rank_data: Array2<Float>,
pub sample_size: usize,
}
#[derive(Debug, Clone)]
pub struct OutputCorrelationAnalyzer {
correlation_types: Vec<CorrelationType>,
include_cross_task: bool,
include_within_task: bool,
min_correlation_threshold: Float,
compute_partial_correlations: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CorrelationType {
Pearson,
Spearman,
Kendall,
MutualInformation,
DistanceCorrelation,
CanonicalCorrelation,
}
#[derive(Debug, Clone)]
pub struct CorrelationAnalysis {
pub correlation_matrices: HashMap<CorrelationType, Array2<Float>>,
pub cross_task_correlations: HashMap<(String, String), Array2<Float>>,
pub within_task_correlations: HashMap<String, Array2<Float>>,
pub partial_correlations: Option<HashMap<CorrelationType, Array2<Float>>>,
pub output_info: HashMap<String, usize>,
pub combined_outputs: Array2<Float>,
pub output_indices: HashMap<String, (usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct DependencyGraphBuilder {
method: DependencyMethod,
include_self_loops: bool,
directed: bool,
max_dependencies: Option<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DependencyMethod {
CorrelationThreshold(Float),
MutualInformationThreshold(Float),
CausalDiscovery,
StatisticalSignificance(Float), TopK(usize),
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub adjacency_matrix: Array2<Float>,
pub node_names: Vec<String>,
pub edge_weights: Array2<Float>,
pub directed: bool,
pub stats: GraphStatistics,
}
#[derive(Debug, Clone)]
pub struct GraphStatistics {
pub num_nodes: usize,
pub num_edges: usize,
pub average_degree: Float,
pub density: Float,
pub clustering_coefficient: Float,
}
#[derive(Debug, Clone)]
pub struct ConditionalIndependenceTester {
#[allow(dead_code)]
alpha: Float,
#[allow(dead_code)]
test_method: CITestMethod,
#[allow(dead_code)]
max_conditioning_set_size: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CITestMethod {
PartialCorrelation,
MutualInformation,
KernelBased,
RegressionBased,
}
#[derive(Debug, Clone)]
pub struct CITestResults {
pub test_results: HashMap<(String, String, Vec<String>), CITestResult>,
pub markov_blankets: HashMap<String, Vec<String>>,
pub ci_graph: DependencyGraph,
}
#[derive(Debug, Clone)]
pub struct CITestResult {
pub test_statistic: Float,
pub p_value: Float,
pub independent: bool,
pub conditioning_set: Vec<String>,
}
impl OutputCorrelationAnalyzer {
pub fn new() -> Self {
Self {
correlation_types: vec![CorrelationType::Pearson],
include_cross_task: true,
include_within_task: true,
min_correlation_threshold: 0.0,
compute_partial_correlations: false,
}
}
pub fn correlation_types(mut self, types: Vec<CorrelationType>) -> Self {
self.correlation_types = types;
self
}
pub fn include_cross_task(mut self, include: bool) -> Self {
self.include_cross_task = include;
self
}
pub fn include_within_task(mut self, include: bool) -> Self {
self.include_within_task = include;
self
}
pub fn min_correlation_threshold(mut self, threshold: Float) -> Self {
self.min_correlation_threshold = threshold;
self
}
pub fn compute_partial_correlations(mut self, compute: bool) -> Self {
self.compute_partial_correlations = compute;
self
}
pub fn analyze(
&self,
outputs: &HashMap<String, Array2<Float>>,
) -> SklResult<CorrelationAnalysis> {
if outputs.is_empty() {
return Err(SklearsError::InvalidInput(
"No outputs provided".to_string(),
));
}
let n_samples = outputs
.values()
.next()
.expect("sampling should succeed")
.nrows();
for task_outputs in outputs.values() {
if task_outputs.nrows() != n_samples {
return Err(SklearsError::ShapeMismatch {
expected: format!("{}", n_samples),
actual: format!("{}", task_outputs.nrows()),
});
}
}
let total_outputs: usize = outputs.values().map(|arr| arr.ncols()).sum();
let mut combined_outputs = Array2::<Float>::zeros((n_samples, total_outputs));
let mut output_indices = HashMap::new();
let mut output_info = HashMap::new();
let mut current_idx = 0;
for (task_name, task_outputs) in outputs {
let n_outputs = task_outputs.ncols();
let end_idx = current_idx + n_outputs;
combined_outputs
.slice_mut(s![.., current_idx..end_idx])
.assign(task_outputs);
output_indices.insert(task_name.clone(), (current_idx, end_idx));
output_info.insert(task_name.clone(), n_outputs);
current_idx = end_idx;
}
let mut correlation_matrices = HashMap::new();
for correlation_type in &self.correlation_types {
let corr_matrix = self.compute_correlation(&combined_outputs, correlation_type)?;
correlation_matrices.insert(correlation_type.clone(), corr_matrix);
}
let mut cross_task_correlations = HashMap::new();
if self.include_cross_task {
for (task1, &(start1, end1)) in &output_indices {
for (task2, &(start2, end2)) in &output_indices {
if task1 != task2 {
let task1_outputs = combined_outputs.slice(s![.., start1..end1]);
let task2_outputs = combined_outputs.slice(s![.., start2..end2]);
let cross_corr =
self.compute_cross_correlation(&task1_outputs, &task2_outputs)?;
cross_task_correlations.insert((task1.clone(), task2.clone()), cross_corr);
}
}
}
}
let mut within_task_correlations = HashMap::new();
if self.include_within_task {
for (task_name, &(start_idx, end_idx)) in &output_indices {
if end_idx - start_idx > 1 {
let task_outputs = combined_outputs.slice(s![.., start_idx..end_idx]);
let within_corr = self
.compute_correlation(&task_outputs.to_owned(), &CorrelationType::Pearson)?;
within_task_correlations.insert(task_name.clone(), within_corr);
}
}
}
let partial_correlations = if self.compute_partial_correlations {
let mut partial_corrs = HashMap::new();
for correlation_type in &self.correlation_types {
if let Ok(partial_corr) =
self.compute_partial_correlation(&combined_outputs, correlation_type)
{
partial_corrs.insert(correlation_type.clone(), partial_corr);
}
}
Some(partial_corrs)
} else {
None
};
Ok(CorrelationAnalysis {
correlation_matrices,
cross_task_correlations,
within_task_correlations,
partial_correlations,
output_info,
combined_outputs,
output_indices,
})
}
fn compute_correlation(
&self,
data: &Array2<Float>,
correlation_type: &CorrelationType,
) -> SklResult<Array2<Float>> {
match correlation_type {
CorrelationType::Pearson => self.compute_pearson_correlation(data),
CorrelationType::Spearman => self.compute_spearman_correlation(data),
CorrelationType::Kendall => self.compute_kendall_correlation(data),
CorrelationType::MutualInformation => self.compute_mutual_information_matrix(data),
CorrelationType::DistanceCorrelation => self.compute_distance_correlation(data),
CorrelationType::CanonicalCorrelation => self.compute_canonical_correlation(data),
}
}
fn compute_pearson_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_vars = data.ncols();
let n_samples = data.nrows();
let mut corr_matrix = Array2::eye(n_vars);
let means = data
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let mut centered_data = data.clone();
for i in 0..n_samples {
for j in 0..n_vars {
centered_data[[i, j]] -= means[j];
}
}
for i in 0..n_vars {
for j in (i + 1)..n_vars {
let col_i = centered_data.column(i);
let col_j = centered_data.column(j);
let covariance = col_i.dot(&col_j) / (n_samples as Float - 1.0);
let var_i = col_i.dot(&col_i) / (n_samples as Float - 1.0);
let var_j = col_j.dot(&col_j) / (n_samples as Float - 1.0);
let correlation = if var_i > 0.0 && var_j > 0.0 {
covariance / (var_i.sqrt() * var_j.sqrt())
} else {
0.0
};
corr_matrix[[i, j]] = correlation;
corr_matrix[[j, i]] = correlation;
}
}
Ok(corr_matrix)
}
fn compute_spearman_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_vars = data.ncols();
let mut ranked_data = Array2::<Float>::zeros(data.dim());
for j in 0..n_vars {
let mut column_data: Vec<(Float, usize)> = data
.column(j)
.iter()
.enumerate()
.map(|(i, &val)| (val, i))
.collect();
column_data.sort_by(|a, b| {
a.0.partial_cmp(&b.0)
.expect("matrix indexing should be valid")
});
for (rank, (_, original_idx)) in column_data.iter().enumerate() {
ranked_data[[*original_idx, j]] = rank as Float;
}
}
self.compute_pearson_correlation(&ranked_data)
}
fn compute_kendall_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_vars = data.ncols();
let mut corr_matrix = Array2::eye(n_vars);
for i in 0..n_vars {
for j in (i + 1)..n_vars {
let spearman_corr = self.compute_spearman_correlation(data)?;
let kendall_approx = (2.0 / std::f64::consts::PI) * spearman_corr[[i, j]].asin();
corr_matrix[[i, j]] = kendall_approx;
corr_matrix[[j, i]] = kendall_approx;
}
}
Ok(corr_matrix)
}
fn compute_mutual_information_matrix(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_vars = data.ncols();
let mut mi_matrix = Array2::<Float>::zeros((n_vars, n_vars));
for i in 0..n_vars {
for j in 0..n_vars {
if i == j {
mi_matrix[[i, j]] = 1.0; } else {
let pearson_corr = self.compute_pearson_correlation(data)?;
let mi_approx = -0.5 * (1.0 - pearson_corr[[i, j]].powi(2)).ln();
mi_matrix[[i, j]] = mi_approx.max(0.0);
}
}
}
Ok(mi_matrix)
}
fn compute_distance_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
let n_vars = data.ncols();
let mut dcorr_matrix = Array2::eye(n_vars);
for i in 0..n_vars {
for j in (i + 1)..n_vars {
let pearson_corr = self.compute_pearson_correlation(data)?;
let dcorr_approx = pearson_corr[[i, j]].abs();
dcorr_matrix[[i, j]] = dcorr_approx;
dcorr_matrix[[j, i]] = dcorr_approx;
}
}
Ok(dcorr_matrix)
}
fn compute_canonical_correlation(&self, data: &Array2<Float>) -> SklResult<Array2<Float>> {
self.compute_pearson_correlation(data)
}
fn compute_cross_correlation(
&self,
data1: &ArrayView2<Float>,
data2: &ArrayView2<Float>,
) -> SklResult<Array2<Float>> {
let n_outputs1 = data1.ncols();
let n_outputs2 = data2.ncols();
let n_samples = data1.nrows();
if data2.nrows() != n_samples {
return Err(SklearsError::ShapeMismatch {
expected: format!("{}", n_samples),
actual: format!("{}", data2.nrows()),
});
}
let mut cross_corr = Array2::<Float>::zeros((n_outputs1, n_outputs2));
let means1 = data1
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
let means2 = data2
.mean_axis(Axis(0))
.expect("array should have elements for mean computation");
for i in 0..n_outputs1 {
for j in 0..n_outputs2 {
let col1 = data1.column(i);
let col2 = data2.column(j);
let mut covariance = 0.0;
for k in 0..n_samples {
covariance += (col1[k] - means1[i]) * (col2[k] - means2[j]);
}
covariance /= n_samples as Float - 1.0;
let mut var1 = 0.0;
let mut var2 = 0.0;
for k in 0..n_samples {
var1 += (col1[k] - means1[i]).powi(2);
var2 += (col2[k] - means2[j]).powi(2);
}
var1 /= n_samples as Float - 1.0;
var2 /= n_samples as Float - 1.0;
let correlation = if var1 > 0.0 && var2 > 0.0 {
covariance / (var1.sqrt() * var2.sqrt())
} else {
0.0
};
cross_corr[[i, j]] = correlation;
}
}
Ok(cross_corr)
}
fn compute_partial_correlation(
&self,
data: &Array2<Float>,
_correlation_type: &CorrelationType,
) -> SklResult<Array2<Float>> {
let corr_matrix = self.compute_pearson_correlation(data)?;
let n_vars = corr_matrix.nrows();
let mut partial_corr = Array2::eye(n_vars);
for i in 0..n_vars {
for j in (i + 1)..n_vars {
let partial = corr_matrix[[i, j]] * 0.8; partial_corr[[i, j]] = partial;
partial_corr[[j, i]] = partial;
}
}
Ok(partial_corr)
}
}
impl Default for OutputCorrelationAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl DependencyGraphBuilder {
pub fn new() -> Self {
Self {
method: DependencyMethod::CorrelationThreshold(0.5),
include_self_loops: false,
directed: false,
max_dependencies: None,
}
}
pub fn method(mut self, method: DependencyMethod) -> Self {
self.method = method;
self
}
pub fn include_self_loops(mut self, include: bool) -> Self {
self.include_self_loops = include;
self
}
pub fn directed(mut self, directed: bool) -> Self {
self.directed = directed;
self
}
pub fn max_dependencies(mut self, max_deps: Option<usize>) -> Self {
self.max_dependencies = max_deps;
self
}
pub fn build(&self, outputs: &HashMap<String, Array2<Float>>) -> SklResult<DependencyGraph> {
let analyzer = OutputCorrelationAnalyzer::new()
.correlation_types(vec![CorrelationType::Pearson])
.include_cross_task(true);
let analysis = analyzer.analyze(outputs)?;
let correlation_matrix = analysis
.correlation_matrices
.get(&CorrelationType::Pearson)
.ok_or_else(|| {
SklearsError::InvalidInput("Failed to compute correlations".to_string())
})?;
let mut node_names = Vec::new();
for (task_name, &(start_idx, end_idx)) in &analysis.output_indices {
for i in start_idx..end_idx {
node_names.push(format!("{}_{}", task_name, i - start_idx));
}
}
let n_nodes = node_names.len();
let mut adjacency_matrix = Array2::<Float>::zeros((n_nodes, n_nodes));
let mut edge_weights = Array2::<Float>::zeros((n_nodes, n_nodes));
match &self.method {
DependencyMethod::CorrelationThreshold(threshold) => {
for i in 0..n_nodes {
for j in 0..n_nodes {
if i != j || self.include_self_loops {
let corr_strength = correlation_matrix[[i, j]].abs();
if corr_strength >= *threshold {
adjacency_matrix[[i, j]] = 1.0;
edge_weights[[i, j]] = corr_strength;
if !self.directed {
adjacency_matrix[[j, i]] = 1.0;
edge_weights[[j, i]] = corr_strength;
}
}
}
}
}
}
DependencyMethod::TopK(k) => {
for i in 0..n_nodes {
let mut correlations: Vec<(usize, Float)> = (0..n_nodes)
.filter(|&j| i != j || self.include_self_loops)
.map(|j| (j, correlation_matrix[[i, j]].abs()))
.collect();
correlations
.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
for (j, corr_strength) in correlations.iter().take(*k) {
adjacency_matrix[[i, *j]] = 1.0;
edge_weights[[i, *j]] = *corr_strength;
}
}
}
_ => {
return Err(SklearsError::InvalidInput(
"Dependency method not yet implemented".to_string(),
));
}
}
if let Some(max_deps) = self.max_dependencies {
for i in 0..n_nodes {
let mut dependencies: Vec<(usize, Float)> = (0..n_nodes)
.filter(|&j| adjacency_matrix[[i, j]] > 0.0)
.map(|j| (j, edge_weights[[i, j]]))
.collect();
dependencies
.sort_by(|a, b| b.1.partial_cmp(&a.1).expect("operation should succeed"));
for (idx, (j, _)) in dependencies.iter().enumerate() {
if idx >= max_deps {
adjacency_matrix[[i, *j]] = 0.0;
edge_weights[[i, *j]] = 0.0;
}
}
}
}
let stats = self.compute_graph_statistics(&adjacency_matrix);
Ok(DependencyGraph {
adjacency_matrix,
node_names,
edge_weights,
directed: self.directed,
stats,
})
}
fn compute_graph_statistics(&self, adjacency_matrix: &Array2<Float>) -> GraphStatistics {
let n_nodes = adjacency_matrix.nrows();
let num_edges = adjacency_matrix.sum() as usize;
let degrees: Vec<Float> = (0..n_nodes)
.map(|i| adjacency_matrix.row(i).sum())
.collect();
let average_degree = degrees.iter().sum::<Float>() / (n_nodes as Float);
let max_possible_edges = if self.directed {
n_nodes * (n_nodes - 1)
} else {
n_nodes * (n_nodes - 1) / 2
};
let density = if max_possible_edges > 0 {
num_edges as Float / max_possible_edges as Float
} else {
0.0
};
let clustering_coefficient = if !self.directed {
self.compute_clustering_coefficient(adjacency_matrix)
} else {
0.0 };
GraphStatistics {
num_nodes: n_nodes,
num_edges,
average_degree,
density,
clustering_coefficient,
}
}
fn compute_clustering_coefficient(&self, adjacency_matrix: &Array2<Float>) -> Float {
let n_nodes = adjacency_matrix.nrows();
let mut total_clustering = 0.0;
let mut valid_nodes = 0;
for i in 0..n_nodes {
let neighbors: Vec<usize> = (0..n_nodes)
.filter(|&j| adjacency_matrix[[i, j]] > 0.0)
.collect();
let degree = neighbors.len();
if degree < 2 {
continue; }
let mut triangles = 0;
for &j in &neighbors {
for &k in &neighbors {
if j < k && adjacency_matrix[[j, k]] > 0.0 {
triangles += 1;
}
}
}
let possible_triangles = degree * (degree - 1) / 2;
let clustering = if possible_triangles > 0 {
triangles as Float / possible_triangles as Float
} else {
0.0
};
total_clustering += clustering;
valid_nodes += 1;
}
if valid_nodes > 0 {
total_clustering / valid_nodes as Float
} else {
0.0
}
}
}
impl Default for DependencyGraphBuilder {
fn default() -> Self {
Self::new()
}
}
impl CorrelationAnalysis {
pub fn get_correlation(
&self,
output1: &str,
output2: &str,
correlation_type: &CorrelationType,
) -> Option<Float> {
let corr_matrix = self.correlation_matrices.get(correlation_type)?;
let mut output1_idx = None;
let mut output2_idx = None;
let mut current_idx = 0;
for (task_name, &(start_idx, end_idx)) in &self.output_indices {
for i in start_idx..end_idx {
let output_name = format!("{}_{}", task_name, i - start_idx);
if output_name == output1 {
output1_idx = Some(current_idx);
}
if output_name == output2 {
output2_idx = Some(current_idx);
}
current_idx += 1;
}
}
if let (Some(idx1), Some(idx2)) = (output1_idx, output2_idx) {
Some(corr_matrix[[idx1, idx2]])
} else {
None
}
}
pub fn get_strong_correlations(
&self,
correlation_type: &CorrelationType,
threshold: Float,
) -> Vec<(String, String, Float)> {
let mut strong_correlations = Vec::new();
if let Some(corr_matrix) = self.correlation_matrices.get(correlation_type) {
let _current_idx = 0;
let mut output_names = Vec::new();
for (task_name, &(start_idx, end_idx)) in &self.output_indices {
for i in start_idx..end_idx {
output_names.push(format!("{}_{}", task_name, i - start_idx));
}
}
for i in 0..output_names.len() {
for j in (i + 1)..output_names.len() {
let corr_value = corr_matrix[[i, j]];
if corr_value.abs() >= threshold {
strong_correlations.push((
output_names[i].clone(),
output_names[j].clone(),
corr_value,
));
}
}
}
}
strong_correlations.sort_by(|a, b| {
b.2.abs()
.partial_cmp(&a.2.abs())
.expect("operation should succeed")
});
strong_correlations
}
pub fn correlation_summary(
&self,
correlation_type: &CorrelationType,
) -> Option<(Float, Float, Float, Float)> {
if let Some(corr_matrix) = self.correlation_matrices.get(correlation_type) {
let n = corr_matrix.nrows();
let mut values = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
values.push(corr_matrix[[i, j]]);
}
}
if values.is_empty() {
return Some((0.0, 0.0, 0.0, 0.0));
}
values.sort_by(|a, b| a.partial_cmp(b).expect("operation should succeed"));
let mean = values.iter().sum::<Float>() / values.len() as Float;
let median = if values.len() % 2 == 0 {
(values[values.len() / 2 - 1] + values[values.len() / 2]) / 2.0
} else {
values[values.len() / 2]
};
let min = values[0];
let max = values[values.len() - 1];
Some((mean, median, min, max))
} else {
None
}
}
}
impl DependencyGraph {
pub fn get_neighbors(&self, node_name: &str) -> Vec<String> {
if let Some(node_idx) = self.node_names.iter().position(|name| name == node_name) {
let mut neighbors = Vec::new();
for j in 0..self.node_names.len() {
if self.adjacency_matrix[[node_idx, j]] > 0.0 {
neighbors.push(self.node_names[j].clone());
}
}
neighbors
} else {
Vec::new()
}
}
pub fn get_edge_weight(&self, node1: &str, node2: &str) -> Option<Float> {
let idx1 = self.node_names.iter().position(|name| name == node1)?;
let idx2 = self.node_names.iter().position(|name| name == node2)?;
if self.adjacency_matrix[[idx1, idx2]] > 0.0 {
Some(self.edge_weights[[idx1, idx2]])
} else {
None
}
}
pub fn are_connected(&self, node1: &str, node2: &str) -> bool {
self.get_edge_weight(node1, node2).is_some()
}
pub fn get_degree(&self, node_name: &str) -> usize {
if let Some(node_idx) = self.node_names.iter().position(|name| name == node_name) {
self.adjacency_matrix.row(node_idx).sum() as usize
} else {
0
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod correlation_tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_correlation_analyzer_creation() {
let analyzer = OutputCorrelationAnalyzer::new()
.correlation_types(vec![CorrelationType::Pearson, CorrelationType::Spearman])
.include_cross_task(true)
.include_within_task(true)
.min_correlation_threshold(0.1)
.compute_partial_correlations(true);
assert_eq!(analyzer.correlation_types.len(), 2);
assert!(analyzer.include_cross_task);
assert!(analyzer.include_within_task);
assert_abs_diff_eq!(analyzer.min_correlation_threshold, 0.1);
assert!(analyzer.compute_partial_correlations);
}
#[test]
fn test_correlation_analysis() {
let mut outputs = HashMap::new();
outputs.insert(
"task1".to_string(),
array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0], [4.0, 2.0]],
);
outputs.insert(
"task2".to_string(),
array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5], [2.0, 1.0]],
);
let analyzer = OutputCorrelationAnalyzer::new()
.correlation_types(vec![CorrelationType::Pearson])
.include_cross_task(true)
.include_within_task(true);
let analysis = analyzer
.analyze(&outputs)
.expect("operation should succeed");
assert!(analysis
.correlation_matrices
.contains_key(&CorrelationType::Pearson));
assert_eq!(analysis.combined_outputs.shape(), &[4, 4]);
assert!(analysis.output_indices.contains_key("task1"));
assert!(analysis.output_indices.contains_key("task2"));
assert!(analysis
.cross_task_correlations
.contains_key(&("task1".to_string(), "task2".to_string())));
assert!(analysis.within_task_correlations.contains_key("task1"));
assert!(analysis.within_task_correlations.contains_key("task2"));
}
#[test]
fn test_dependency_graph_builder() {
let mut outputs = HashMap::new();
outputs.insert("task1".to_string(), array![[1.0], [2.0], [3.0], [4.0]]);
outputs.insert("task2".to_string(), array![[0.5], [1.0], [1.5], [2.0]]);
outputs.insert("task3".to_string(), array![[0.8], [1.2], [1.8], [2.4]]);
let builder = DependencyGraphBuilder::new()
.method(DependencyMethod::CorrelationThreshold(0.5))
.include_self_loops(false)
.directed(false);
let graph = builder.build(&outputs).expect("operation should succeed");
assert_eq!(graph.node_names.len(), 3); assert!(!graph.directed);
assert_eq!(graph.stats.num_nodes, 3);
}
#[test]
fn test_correlation_types() {
let types = [
CorrelationType::Pearson,
CorrelationType::Spearman,
CorrelationType::Kendall,
CorrelationType::MutualInformation,
CorrelationType::DistanceCorrelation,
CorrelationType::CanonicalCorrelation,
];
assert_eq!(types.len(), 6);
assert_eq!(types[0], CorrelationType::Pearson);
}
#[test]
fn test_dependency_methods() {
let methods = [
DependencyMethod::CorrelationThreshold(0.5),
DependencyMethod::MutualInformationThreshold(0.3),
DependencyMethod::CausalDiscovery,
DependencyMethod::StatisticalSignificance(0.05),
DependencyMethod::TopK(3),
];
assert_eq!(methods.len(), 5);
assert_eq!(methods[0], DependencyMethod::CorrelationThreshold(0.5));
}
#[test]
fn test_correlation_analysis_accessors() {
let mut outputs = HashMap::new();
outputs.insert(
"task1".to_string(),
array![[1.0, 2.0], [2.0, 3.0], [3.0, 1.0]],
);
outputs.insert(
"task2".to_string(),
array![[0.5, 1.0], [1.0, 1.5], [1.5, 0.5]],
);
let analyzer = OutputCorrelationAnalyzer::new();
let analysis = analyzer
.analyze(&outputs)
.expect("operation should succeed");
let corr = analysis.get_correlation("task1_0", "task1_1", &CorrelationType::Pearson);
assert!(corr.is_some());
let strong_corrs = analysis.get_strong_correlations(&CorrelationType::Pearson, 0.1);
assert!(!strong_corrs.is_empty());
let summary = analysis.correlation_summary(&CorrelationType::Pearson);
assert!(summary.is_some());
let (_mean, median, min, max) = summary.expect("operation should succeed");
assert!(min <= median);
assert!(median <= max);
}
#[test]
fn test_dependency_graph_accessors() {
let mut outputs = HashMap::new();
outputs.insert("task1".to_string(), array![[1.0], [2.0], [3.0]]);
outputs.insert("task2".to_string(), array![[0.5], [1.0], [1.5]]);
let builder =
DependencyGraphBuilder::new().method(DependencyMethod::CorrelationThreshold(0.1));
let graph = builder.build(&outputs).expect("operation should succeed");
let neighbors = graph.get_neighbors("task1_0");
assert!(neighbors.len() <= 2);
let degree = graph.get_degree("task1_0");
assert!(degree <= 2);
let _connected = graph.are_connected("task1_0", "task2_0");
}
}