rust_igraph/algorithms/properties/
clustering_profile.rs1use crate::algorithms::properties::triangles::transitivity_local_undirected;
14use crate::core::{Graph, IgraphResult};
15
16pub fn clustering_variance(graph: &Graph) -> IgraphResult<f64> {
39 let ccs = defined_clustering_coefficients(graph)?;
40 if ccs.len() < 2 {
41 return Ok(0.0);
42 }
43 #[allow(clippy::cast_precision_loss)]
44 let n = ccs.len() as f64;
45 let mean = ccs.iter().sum::<f64>() / n;
46 let var = ccs.iter().map(|&c| (c - mean).powi(2)).sum::<f64>() / n;
47 Ok(var)
48}
49
50pub fn clustering_entropy(graph: &Graph) -> IgraphResult<f64> {
74 const NUM_BINS: usize = 10;
75
76 let ccs = defined_clustering_coefficients(graph)?;
77 if ccs.len() < 2 {
78 return Ok(0.0);
79 }
80
81 let mut bins = [0u32; NUM_BINS];
83 for &c in &ccs {
84 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
85 let idx = if c >= 1.0 {
86 NUM_BINS - 1
87 } else {
88 #[allow(clippy::cast_precision_loss)]
89 let scaled = c * (NUM_BINS as f64);
90 scaled as usize
91 };
92 bins[idx] += 1;
93 }
94
95 let non_empty: Vec<u32> = bins.iter().copied().filter(|&b| b > 0).collect();
96 let num_non_empty = non_empty.len();
97 if num_non_empty <= 1 {
98 return Ok(0.0);
99 }
100
101 #[allow(clippy::cast_precision_loss)]
102 let total = ccs.len() as f64;
103 let mut entropy = 0.0_f64;
104 for &count in &non_empty {
105 let p = f64::from(count) / total;
106 entropy -= p * p.ln();
107 }
108
109 #[allow(clippy::cast_precision_loss)]
110 let max_entropy = (num_non_empty as f64).ln();
111 Ok(entropy / max_entropy)
112}
113
114pub fn clustering_bimodality(graph: &Graph) -> IgraphResult<f64> {
137 let ccs = defined_clustering_coefficients(graph)?;
138 if ccs.len() < 4 {
139 return Ok(0.0);
140 }
141 #[allow(clippy::cast_precision_loss)]
142 let n = ccs.len() as f64;
143 let mean = ccs.iter().sum::<f64>() / n;
144 let m2 = ccs.iter().map(|&c| (c - mean).powi(2)).sum::<f64>() / n;
145 if m2 < 1e-15 {
146 return Ok(0.0);
147 }
148 let m3 = ccs.iter().map(|&c| (c - mean).powi(3)).sum::<f64>() / n;
149 let m4 = ccs.iter().map(|&c| (c - mean).powi(4)).sum::<f64>() / n;
150
151 let skewness = m3 / m2.powf(1.5);
152 let kurtosis = m4 / (m2 * m2); if kurtosis < 1e-15 {
155 return Ok(0.0);
156 }
157
158 Ok((skewness * skewness + 1.0) / kurtosis)
159}
160
161fn defined_clustering_coefficients(graph: &Graph) -> IgraphResult<Vec<f64>> {
163 let local_cc = transitivity_local_undirected(graph)?;
164 Ok(local_cc.into_iter().flatten().collect())
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
174 fn variance_empty() {
175 let g = Graph::with_vertices(0);
176 assert!(clustering_variance(&g).unwrap().abs() < 1e-12);
177 }
178
179 #[test]
180 fn variance_edgeless() {
181 let g = Graph::with_vertices(5);
182 assert!(clustering_variance(&g).unwrap().abs() < 1e-12);
183 }
184
185 #[test]
186 fn variance_complete() {
187 let g = Graph::from_edges(
189 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
190 false,
191 Some(4),
192 )
193 .unwrap();
194 assert!(clustering_variance(&g).unwrap().abs() < 1e-10);
195 }
196
197 #[test]
198 fn variance_mixed() {
199 let g = Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap();
204 let v = clustering_variance(&g).unwrap();
205 assert!(
206 v > 0.0,
207 "Mixed graph should have positive variance, got {v}"
208 );
209 }
210
211 #[test]
214 fn entropy_empty() {
215 let g = Graph::with_vertices(0);
216 assert!(clustering_entropy(&g).unwrap().abs() < 1e-12);
217 }
218
219 #[test]
220 fn entropy_complete() {
221 let g = Graph::from_edges(
223 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
224 false,
225 Some(4),
226 )
227 .unwrap();
228 assert!(clustering_entropy(&g).unwrap().abs() < 1e-10);
229 }
230
231 #[test]
232 fn entropy_mixed() {
233 let g =
235 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3), (3, 4)], false, Some(5)).unwrap();
236 let h = clustering_entropy(&g).unwrap();
237 assert!(h >= 0.0, "Entropy should be >= 0, got {h}");
238 assert!(h <= 1.0, "Entropy should be <= 1, got {h}");
239 }
240
241 #[test]
244 fn bimodality_empty() {
245 let g = Graph::with_vertices(0);
246 assert!(clustering_bimodality(&g).unwrap().abs() < 1e-12);
247 }
248
249 #[test]
250 fn bimodality_complete() {
251 let g = Graph::from_edges(
253 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
254 false,
255 Some(4),
256 )
257 .unwrap();
258 assert!(clustering_bimodality(&g).unwrap().abs() < 1e-10);
259 }
260
261 #[test]
262 fn bimodality_positive() {
263 let g = Graph::from_edges(
265 &[
266 (0, 1),
267 (1, 2),
268 (0, 2),
269 (2, 3),
270 (3, 4),
271 (4, 5),
272 (3, 5),
273 (5, 6),
274 (6, 7),
275 (5, 7),
276 ],
277 false,
278 Some(8),
279 )
280 .unwrap();
281 let b = clustering_bimodality(&g).unwrap();
282 assert!(b > 0.0, "Should have positive bimodality, got {b}");
283 }
284}