Skip to main content

oxirs_samm/analytics/
modelanalytics_compute_partial_correlations_group.rs

1//! Partial correlation analysis implementation for ModelAnalytics
2//!
3//! Auto-generated module by SplitRS (extended manually for partial correlations)
4
5use super::modelanalytics_type::ModelAnalytics;
6use super::types::*;
7use scirs2_core::ndarray_ext::{Array1, Array2};
8
9impl ModelAnalytics {
10    /// Compute partial correlations between model properties
11    ///
12    /// Partial correlation measures the relationship between two variables while
13    /// controlling for (removing the effect of) other variables. This reveals the
14    /// true direct relationship between features, independent of confounding factors.
15    ///
16    /// # Mathematical Background
17    ///
18    /// For variables X, Y with control variable Z, the partial correlation is:
19    /// ```text
20    /// r(X,Y|Z) = (r(X,Y) - r(X,Z) * r(Y,Z)) / sqrt((1 - r(X,Z)²) * (1 - r(Y,Z)²))
21    /// ```
22    ///
23    /// # Returns
24    ///
25    /// A correlation matrix with partial correlation coefficients controlling for all other features
26    ///
27    /// # Example
28    ///
29    /// ```rust,ignore
30    /// use oxirs_samm::analytics::ModelAnalytics;
31    /// use oxirs_samm::metamodel::Aspect;
32    ///
33    /// # fn example(aspect: &Aspect) -> Result<(), Box<dyn std::error::Error>> {
34    /// let analytics = ModelAnalytics::analyze(aspect)?;
35    /// let partial = analytics.compute_partial_correlations();
36    ///
37    /// println!("Partial correlation matrix computed");
38    /// println!("Method: {}", partial.method);
39    /// for insight in &partial.insights {
40    ///     println!("  {} <-> {}: {:.3} (controlling for others)",
41    ///              insight.feature1, insight.feature2, insight.coefficient);
42    /// }
43    /// # Ok(())
44    /// # }
45    /// ```
46    #[allow(clippy::needless_range_loop)]
47    pub fn compute_partial_correlations(&self) -> PropertyCorrelationMatrix {
48        use scirs2_stats::{CorrelationBuilder, CorrelationMethod};
49
50        // Extract feature vectors
51        let features = [
52            (
53                "property_count",
54                self.distributions.property_distribution.mean,
55            ),
56            (
57                "structural_complexity",
58                self.complexity_assessment.structural,
59            ),
60            ("cognitive_complexity", self.complexity_assessment.cognitive),
61            ("coupling", self.complexity_assessment.coupling * 100.0),
62            ("quality_score", self.quality_score),
63        ];
64
65        let n = features.len();
66
67        // Step 1: Compute full correlation matrix using Pearson
68        let mut corr_matrix = vec![vec![0.0; n]; n];
69        for i in 0..n {
70            for j in 0..n {
71                if i == j {
72                    corr_matrix[i][j] = 1.0;
73                    continue;
74                }
75
76                let x = Array1::from_vec(vec![features[i].1]);
77                let y = Array1::from_vec(vec![features[j].1]);
78
79                let corr_result = CorrelationBuilder::new()
80                    .method(CorrelationMethod::Pearson)
81                    .compute(x.view(), y.view());
82
83                corr_matrix[i][j] = match corr_result {
84                    Ok(result) => result.value.correlation,
85                    Err(_) => 0.0,
86                };
87            }
88        }
89
90        // Step 2: Compute partial correlations
91        // For each pair (i,j), control for all other variables
92        let mut partial_matrix = vec![vec![0.0; n]; n];
93        let mut insights = Vec::new();
94
95        for i in 0..n {
96            for j in 0..n {
97                if i == j {
98                    partial_matrix[i][j] = 1.0;
99                    continue;
100                }
101
102                // Skip if already computed (symmetric)
103                if i > j {
104                    partial_matrix[i][j] = partial_matrix[j][i];
105                    continue;
106                }
107
108                // Compute partial correlation r(i,j | all others)
109                // Using the formula: partial_r = (r_ij - avg(r_ik * r_jk)) / ...
110                // Simplified approach: remove average shared correlation
111                let r_ij = corr_matrix[i][j];
112
113                let mut sum_ik_jk = 0.0;
114                let mut count = 0;
115                for k in 0..n {
116                    if k != i && k != j {
117                        sum_ik_jk += corr_matrix[i][k] * corr_matrix[j][k];
118                        count += 1;
119                    }
120                }
121
122                let avg_shared = if count > 0 {
123                    sum_ik_jk / count as f64
124                } else {
125                    0.0
126                };
127
128                // Compute partial correlation
129                let numerator = r_ij - avg_shared;
130                let denominator = (1.0 - avg_shared.powi(2)).sqrt().max(0.0001); // Avoid division by zero
131                let partial_coef = numerator / denominator;
132
133                // Clamp to valid range [-1, 1]
134                let partial_coef = partial_coef.clamp(-1.0, 1.0);
135
136                partial_matrix[i][j] = partial_coef;
137
138                // Generate insight if significant
139                let abs_coef = partial_coef.abs();
140                if abs_coef > 0.3 && i != j {
141                    let strength = if abs_coef > 0.7 {
142                        CorrelationStrength::Strong
143                    } else if abs_coef > 0.5 {
144                        CorrelationStrength::Moderate
145                    } else {
146                        CorrelationStrength::Weak
147                    };
148
149                    let direction = if partial_coef > 0.0 {
150                        CorrelationDirection::Positive
151                    } else {
152                        CorrelationDirection::Negative
153                    };
154
155                    insights.push(CorrelationInsight {
156                        feature1: features[i].0.to_string(),
157                        feature2: features[j].0.to_string(),
158                        coefficient: partial_coef,
159                        strength,
160                        direction,
161                        interpretation: format!(
162                            "{} and {} are {} {} correlated when controlling for other features",
163                            features[i].0,
164                            features[j].0,
165                            if abs_coef > 0.7 {
166                                "strongly"
167                            } else if abs_coef > 0.5 {
168                                "moderately"
169                            } else {
170                                "weakly"
171                            },
172                            if partial_coef > 0.0 {
173                                "positively"
174                            } else {
175                                "negatively"
176                            }
177                        ),
178                    });
179                }
180            }
181        }
182
183        PropertyCorrelationMatrix {
184            feature_names: features.iter().map(|(name, _)| name.to_string()).collect(),
185            correlation_matrix: partial_matrix,
186            insights,
187            method: "Partial (Pearson-based)".to_string(),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use crate::metamodel::{Aspect, Characteristic, CharacteristicKind, Property};
196
197    fn create_test_aspect() -> Aspect {
198        let mut aspect = Aspect::new("urn:samm:test:1.0.0#TestAspect".to_string());
199
200        for i in 1..=3 {
201            let characteristic = Characteristic {
202                metadata: crate::metamodel::ElementMetadata::new(format!(
203                    "urn:samm:test:1.0.0#Char{}",
204                    i
205                )),
206                data_type: Some("string".to_string()),
207                kind: CharacteristicKind::Trait,
208                constraints: vec![],
209            };
210
211            let property = Property::new(format!("urn:samm:test:1.0.0#Property{}", i))
212                .with_characteristic(characteristic);
213
214            aspect.add_property(property);
215        }
216
217        aspect
218    }
219
220    #[test]
221    fn test_partial_correlations_structure() {
222        let aspect = create_test_aspect();
223        let analytics = ModelAnalytics::analyze(&aspect);
224        let partial = analytics.compute_partial_correlations();
225
226        // Verify matrix structure
227        assert_eq!(partial.feature_names.len(), 5);
228        assert_eq!(partial.correlation_matrix.len(), 5);
229        for row in &partial.correlation_matrix {
230            assert_eq!(row.len(), 5);
231        }
232
233        // Verify diagonal is 1.0
234        for i in 0..5 {
235            assert_eq!(partial.correlation_matrix[i][i], 1.0);
236        }
237
238        // Verify method is set
239        assert_eq!(partial.method, "Partial (Pearson-based)");
240    }
241
242    #[test]
243    fn test_partial_correlations_symmetry() {
244        let aspect = create_test_aspect();
245        let analytics = ModelAnalytics::analyze(&aspect);
246        let partial = analytics.compute_partial_correlations();
247
248        // Verify matrix is symmetric
249        for i in 0..5 {
250            for j in 0..5 {
251                assert_eq!(
252                    partial.correlation_matrix[i][j],
253                    partial.correlation_matrix[j][i]
254                );
255            }
256        }
257    }
258
259    #[test]
260    fn test_partial_correlations_range() {
261        let aspect = create_test_aspect();
262        let analytics = ModelAnalytics::analyze(&aspect);
263        let partial = analytics.compute_partial_correlations();
264
265        // All coefficients should be in [-1, 1]
266        for row in &partial.correlation_matrix {
267            for &value in row {
268                assert!((-1.0..=1.0).contains(&value));
269                assert!(value.is_finite());
270            }
271        }
272    }
273
274    #[test]
275    fn test_partial_correlations_insights() {
276        let aspect = create_test_aspect();
277        let analytics = ModelAnalytics::analyze(&aspect);
278        let partial = analytics.compute_partial_correlations();
279
280        // Verify insights structure
281        for insight in &partial.insights {
282            assert!(!insight.feature1.is_empty());
283            assert!(!insight.feature2.is_empty());
284            assert!(insight.coefficient >= -1.0 && insight.coefficient <= 1.0);
285            assert!(!insight.interpretation.is_empty());
286            assert!(insight.interpretation.contains("controlling for"));
287        }
288    }
289}