oxirs_samm/analytics/
modelanalytics_compute_partial_correlations_group.rs1use super::modelanalytics_type::ModelAnalytics;
6use super::types::*;
7use scirs2_core::ndarray_ext::{Array1, Array2};
8
9impl ModelAnalytics {
10 #[allow(clippy::needless_range_loop)]
47 pub fn compute_partial_correlations(&self) -> PropertyCorrelationMatrix {
48 use scirs2_stats::{CorrelationBuilder, CorrelationMethod};
49
50 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 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 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 if i > j {
104 partial_matrix[i][j] = partial_matrix[j][i];
105 continue;
106 }
107
108 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 let numerator = r_ij - avg_shared;
130 let denominator = (1.0 - avg_shared.powi(2)).sqrt().max(0.0001); let partial_coef = numerator / denominator;
132
133 let partial_coef = partial_coef.clamp(-1.0, 1.0);
135
136 partial_matrix[i][j] = partial_coef;
137
138 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 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 for i in 0..5 {
235 assert_eq!(partial.correlation_matrix[i][i], 1.0);
236 }
237
238 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 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 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 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}