rust_igraph/algorithms/properties/
clustering_ratios.rs1#![allow(
11 clippy::cast_possible_truncation,
12 clippy::cast_precision_loss,
13 clippy::many_single_char_names,
14 clippy::needless_range_loop,
15 clippy::similar_names,
16 clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20
21pub fn clustering_degree_correlation(graph: &Graph) -> IgraphResult<f64> {
40 let n = graph.vcount() as usize;
41 if n < 3 {
42 return Ok(0.0);
43 }
44
45 let mut degrees = Vec::with_capacity(n);
46 let mut clusterings = Vec::with_capacity(n);
47
48 for v in 0..n {
49 let d = graph.degree(v as u32)?;
50 degrees.push(d);
51
52 if d < 2 {
53 clusterings.push(0.0_f64);
54 continue;
55 }
56
57 let neighbors = graph.neighbors(v as u32)?;
58 let mut triangles = 0_u64;
59 for i in 0..neighbors.len() {
60 for j in (i + 1)..neighbors.len() {
61 if graph.has_edge(neighbors[i], neighbors[j]) {
62 triangles += 1;
63 }
64 }
65 }
66
67 let possible = (d * (d - 1)) / 2;
68 clusterings.push(triangles as f64 / possible as f64);
69 }
70
71 let eligible: Vec<usize> = (0..n).filter(|&v| degrees[v] >= 2).collect();
72 if eligible.len() < 2 {
73 return Ok(0.0);
74 }
75
76 let mean_deg = eligible.iter().map(|&v| degrees[v] as f64).sum::<f64>() / eligible.len() as f64;
77 let mean_cc = eligible.iter().map(|&v| clusterings[v]).sum::<f64>() / eligible.len() as f64;
78
79 let mut cov = 0.0_f64;
80 let mut var_deg = 0.0_f64;
81 let mut var_cc = 0.0_f64;
82
83 for &v in &eligible {
84 let dd = degrees[v] as f64 - mean_deg;
85 let dc = clusterings[v] - mean_cc;
86 cov += dd * dc;
87 var_deg += dd * dd;
88 var_cc += dc * dc;
89 }
90
91 if var_deg < 1e-30 || var_cc < 1e-30 {
92 return Ok(0.0);
93 }
94
95 Ok(cov / (var_deg.sqrt() * var_cc.sqrt()))
96}
97
98pub fn transitivity_gap(graph: &Graph) -> IgraphResult<f64> {
117 let n = graph.vcount() as usize;
118 if n < 3 {
119 return Ok(0.0);
120 }
121
122 let mut total_triplets = 0_u64;
123 let mut closed_triplets = 0_u64;
124 let mut local_cc_sum = 0.0_f64;
125 let mut local_cc_count = 0_usize;
126
127 for v in 0..n {
128 let d = graph.degree(v as u32)?;
129 if d < 2 {
130 continue;
131 }
132
133 let neighbors = graph.neighbors(v as u32)?;
134 let mut triangles = 0_u64;
135 for i in 0..neighbors.len() {
136 for j in (i + 1)..neighbors.len() {
137 if graph.has_edge(neighbors[i], neighbors[j]) {
138 triangles += 1;
139 }
140 }
141 }
142
143 let possible = (d * (d - 1)) / 2;
144 total_triplets += possible as u64;
145 closed_triplets += triangles;
146
147 local_cc_sum += triangles as f64 / possible as f64;
148 local_cc_count += 1;
149 }
150
151 if total_triplets == 0 || local_cc_count == 0 {
152 return Ok(0.0);
153 }
154
155 let global_cc = closed_triplets as f64 / total_triplets as f64;
156 let avg_local_cc = local_cc_sum / local_cc_count as f64;
157
158 Ok(global_cc - avg_local_cc)
159}
160
161pub fn closed_triplet_ratio(graph: &Graph) -> IgraphResult<f64> {
178 let n = graph.vcount() as usize;
179 if n < 3 {
180 return Ok(0.0);
181 }
182
183 let mut total_triplets = 0_u64;
184 let mut closed_triplets = 0_u64;
185
186 for v in 0..n {
187 let d = graph.degree(v as u32)?;
188 if d < 2 {
189 continue;
190 }
191
192 let neighbors = graph.neighbors(v as u32)?;
193 let mut triangles = 0_u64;
194 for i in 0..neighbors.len() {
195 for j in (i + 1)..neighbors.len() {
196 if graph.has_edge(neighbors[i], neighbors[j]) {
197 triangles += 1;
198 }
199 }
200 }
201
202 let possible = (d * (d - 1)) / 2;
203 total_triplets += possible as u64;
204 closed_triplets += triangles;
205 }
206
207 if total_triplets == 0 {
208 return Ok(0.0);
209 }
210
211 Ok(closed_triplets as f64 / total_triplets as f64)
212}
213
214pub fn square_clustering_ratio(graph: &Graph) -> IgraphResult<f64> {
234 let n = graph.vcount() as usize;
235 if n < 3 {
236 return Ok(0.0);
237 }
238
239 let mut sum = 0.0_f64;
240 let mut count = 0_usize;
241
242 for v in 0..n {
243 let neighbors = graph.neighbors(v as u32)?;
244 let nn = neighbors.len();
245 if nn < 2 {
246 continue;
247 }
248
249 let mut closed = 0_u64;
250 let total = (nn * (nn - 1)) / 2;
251
252 for i in 0..nn {
253 let ni = neighbors[i];
254 let ni_neighbors = graph.neighbors(ni)?;
255 for j in (i + 1)..nn {
256 let nj = neighbors[j];
257 let has_common = ni_neighbors
258 .iter()
259 .any(|&w| w != v as u32 && graph.has_edge(w, nj));
260 if has_common {
261 closed += 1;
262 }
263 }
264 }
265
266 sum += closed as f64 / total as f64;
267 count += 1;
268 }
269
270 if count == 0 {
271 return Ok(0.0);
272 }
273
274 Ok(sum / count as f64)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn single_edge() -> Graph {
282 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
283 }
284
285 fn path3() -> Graph {
286 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
287 }
288
289 fn k3() -> Graph {
290 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
291 }
292
293 fn k4() -> Graph {
294 Graph::from_edges(
295 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
296 false,
297 Some(4),
298 )
299 .unwrap()
300 }
301
302 fn cycle4() -> Graph {
303 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
304 }
305
306 fn star5() -> Graph {
307 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
308 }
309
310 fn paw() -> Graph {
311 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
312 }
313
314 #[test]
317 fn cdc_empty() {
318 let g = Graph::with_vertices(0);
319 assert!(clustering_degree_correlation(&g).unwrap().abs() < 1e-10);
320 }
321
322 #[test]
323 fn cdc_single() {
324 let g = Graph::with_vertices(1);
325 assert!(clustering_degree_correlation(&g).unwrap().abs() < 1e-10);
326 }
327
328 #[test]
329 fn cdc_k3() {
330 assert!(clustering_degree_correlation(&k3()).unwrap().abs() < 1e-10);
332 }
333
334 #[test]
335 fn cdc_k4() {
336 assert!(clustering_degree_correlation(&k4()).unwrap().abs() < 1e-10);
337 }
338
339 #[test]
340 fn cdc_cycle4() {
341 assert!(clustering_degree_correlation(&cycle4()).unwrap().abs() < 1e-10);
343 }
344
345 #[test]
346 fn cdc_path3() {
347 assert!(clustering_degree_correlation(&path3()).unwrap().abs() < 1e-10);
349 }
350
351 #[test]
352 fn cdc_in_range() {
353 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
354 let r = clustering_degree_correlation(g).unwrap();
355 assert!(r >= -1.0 - 1e-10);
356 assert!(r <= 1.0 + 1e-10);
357 }
358 }
359
360 #[test]
363 fn tg_empty() {
364 let g = Graph::with_vertices(0);
365 assert!(transitivity_gap(&g).unwrap().abs() < 1e-10);
366 }
367
368 #[test]
369 fn tg_single() {
370 let g = Graph::with_vertices(1);
371 assert!(transitivity_gap(&g).unwrap().abs() < 1e-10);
372 }
373
374 #[test]
375 fn tg_k3() {
376 assert!(transitivity_gap(&k3()).unwrap().abs() < 1e-10);
378 }
379
380 #[test]
381 fn tg_k4() {
382 assert!(transitivity_gap(&k4()).unwrap().abs() < 1e-10);
384 }
385
386 #[test]
387 fn tg_cycle4() {
388 assert!(transitivity_gap(&cycle4()).unwrap().abs() < 1e-10);
390 }
391
392 #[test]
393 fn tg_star5() {
394 assert!(transitivity_gap(&star5()).unwrap().abs() < 1e-10);
396 }
397
398 #[test]
399 fn tg_paw() {
400 let r = transitivity_gap(&paw()).unwrap();
408 assert!(r < 0.0);
409 }
410
411 #[test]
414 fn ctr_empty() {
415 let g = Graph::with_vertices(0);
416 assert!(closed_triplet_ratio(&g).unwrap().abs() < 1e-10);
417 }
418
419 #[test]
420 fn ctr_single() {
421 let g = Graph::with_vertices(1);
422 assert!(closed_triplet_ratio(&g).unwrap().abs() < 1e-10);
423 }
424
425 #[test]
426 fn ctr_k3() {
427 assert!((closed_triplet_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
428 }
429
430 #[test]
431 fn ctr_k4() {
432 assert!((closed_triplet_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
433 }
434
435 #[test]
436 fn ctr_cycle4() {
437 assert!(closed_triplet_ratio(&cycle4()).unwrap().abs() < 1e-10);
438 }
439
440 #[test]
441 fn ctr_star5() {
442 assert!(closed_triplet_ratio(&star5()).unwrap().abs() < 1e-10);
443 }
444
445 #[test]
446 fn ctr_paw() {
447 assert!((closed_triplet_ratio(&paw()).unwrap() - 0.6).abs() < 1e-10);
449 }
450
451 #[test]
452 fn ctr_path3() {
453 assert!(closed_triplet_ratio(&path3()).unwrap().abs() < 1e-10);
456 }
457
458 #[test]
459 fn ctr_in_01() {
460 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
461 let r = closed_triplet_ratio(g).unwrap();
462 assert!(r >= -1e-10);
463 assert!(r <= 1.0 + 1e-10);
464 }
465 }
466
467 #[test]
470 fn scr_empty() {
471 let g = Graph::with_vertices(0);
472 assert!(square_clustering_ratio(&g).unwrap().abs() < 1e-10);
473 }
474
475 #[test]
476 fn scr_single() {
477 let g = Graph::with_vertices(1);
478 assert!(square_clustering_ratio(&g).unwrap().abs() < 1e-10);
479 }
480
481 #[test]
482 fn scr_k4() {
483 assert!((square_clustering_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
485 }
486
487 #[test]
488 fn scr_cycle4() {
489 assert!((square_clustering_ratio(&cycle4()).unwrap() - 1.0).abs() < 1e-10);
494 }
495
496 #[test]
497 fn scr_star5() {
498 assert!(square_clustering_ratio(&star5()).unwrap().abs() < 1e-10);
501 }
502
503 #[test]
504 fn scr_path3() {
505 assert!(square_clustering_ratio(&path3()).unwrap().abs() < 1e-10);
508 }
509
510 #[test]
511 fn scr_single_edge() {
512 assert!(square_clustering_ratio(&single_edge()).unwrap().abs() < 1e-10);
514 }
515
516 #[test]
517 fn scr_in_01() {
518 for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
519 let r = square_clustering_ratio(g).unwrap();
520 assert!(r >= -1e-10);
521 assert!(r <= 1.0 + 1e-10);
522 }
523 }
524
525 #[test]
528 fn complete_graphs_full_transitivity() {
529 assert!((closed_triplet_ratio(&k3()).unwrap() - 1.0).abs() < 1e-10);
530 assert!((closed_triplet_ratio(&k4()).unwrap() - 1.0).abs() < 1e-10);
531 }
532
533 #[test]
534 fn complete_graphs_zero_gap() {
535 assert!(transitivity_gap(&k3()).unwrap().abs() < 1e-10);
536 assert!(transitivity_gap(&k4()).unwrap().abs() < 1e-10);
537 }
538
539 #[test]
540 fn triangle_free_zero_ctr() {
541 assert!(closed_triplet_ratio(&cycle4()).unwrap().abs() < 1e-10);
542 assert!(closed_triplet_ratio(&star5()).unwrap().abs() < 1e-10);
543 assert!(closed_triplet_ratio(&path3()).unwrap().abs() < 1e-10);
544 }
545}