Skip to main content

rust_igraph/algorithms/properties/
centrality_ratios.rs

1//! Centrality-based ratio indices (ALGO-TR-105).
2//!
3//! Ratios relating different centrality measures:
4//!
5//! - **Betweenness centralization** — max betweenness / star-graph max
6//! - **Closeness centralization** — max closeness / star-graph max
7//! - **Degree centralization** — max degree / star-graph max
8//! - **Centrality correlation** — Pearson r(betweenness, closeness)
9
10#![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
21/// Compute degree centralization.
22///
23/// `sum(max_degree - degree[v]) / ((n-1)*(n-2))` — the Freeman
24/// degree centralization. Measures how star-like the degree
25/// distribution is. Returns 0.0 for graphs with fewer than 3
26/// vertices.
27///
28/// # Examples
29///
30/// ```
31/// use rust_igraph::{Graph, degree_centralization};
32///
33/// // Star graph: maximum centralization = 1.0
34/// let g = Graph::from_edges(
35///     &[(0,1),(0,2),(0,3),(0,4)], false, Some(5)
36/// ).unwrap();
37/// assert!((degree_centralization(&g).unwrap() - 1.0).abs() < 1e-10);
38/// ```
39pub fn degree_centralization(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 max_deg = 0_usize;
46    let mut degrees = Vec::with_capacity(n);
47    for v in 0..n {
48        let d = graph.degree(v as u32)?;
49        degrees.push(d);
50        if d > max_deg {
51            max_deg = d;
52        }
53    }
54
55    let sum_diff: usize = degrees.iter().map(|&d| max_deg - d).sum();
56    let denom = (n - 1) * (n - 2);
57
58    if denom == 0 {
59        return Ok(0.0);
60    }
61
62    Ok(sum_diff as f64 / denom as f64)
63}
64
65/// Compute betweenness centralization.
66///
67/// `sum(max_bc - bc[v]) / ((n-1)*(n-2))` — Freeman betweenness
68/// centralization normalized by the theoretical maximum for a
69/// star graph. Uses unnormalized betweenness (divided by 2 for
70/// undirected graphs). Returns 0.0 for graphs with fewer than
71/// 3 vertices.
72///
73/// # Examples
74///
75/// ```
76/// use rust_igraph::{Graph, betweenness_centralization};
77///
78/// // K_3: all betweenness = 0 → centralization = 0
79/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
80/// assert!(betweenness_centralization(&g).unwrap().abs() < 1e-10);
81/// ```
82pub fn betweenness_centralization(graph: &Graph) -> IgraphResult<f64> {
83    let n = graph.vcount() as usize;
84    if n < 3 {
85        return Ok(0.0);
86    }
87
88    let bc = compute_betweenness(graph)?;
89
90    let max_bc = bc.iter().copied().fold(0.0_f64, f64::max);
91    let sum_diff: f64 = bc.iter().map(|&b| max_bc - b).sum();
92
93    let n_f = n as f64;
94    let denom = (n_f - 1.0) * (n_f - 2.0);
95
96    if denom < 1e-30 {
97        return Ok(0.0);
98    }
99
100    Ok(sum_diff / denom)
101}
102
103/// Compute closeness centralization.
104///
105/// `sum(max_cc - cc[v]) / ((n-2)*(n-1)/(2*n-3))` — Freeman
106/// closeness centralization. Returns 0.0 for disconnected or
107/// trivial graphs.
108///
109/// # Examples
110///
111/// ```
112/// use rust_igraph::{Graph, closeness_centralization};
113///
114/// // K_3: all closeness = 1.0 → centralization = 0
115/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
116/// assert!(closeness_centralization(&g).unwrap().abs() < 1e-10);
117/// ```
118pub fn closeness_centralization(graph: &Graph) -> IgraphResult<f64> {
119    let n = graph.vcount() as usize;
120    if n < 3 {
121        return Ok(0.0);
122    }
123
124    let dist = all_pairs_bfs(graph)?;
125
126    let mut closeness_vals = Vec::with_capacity(n);
127    for v in 0..n {
128        let mut sum_d = 0_u64;
129        let mut connected = true;
130        for u in 0..n {
131            if u == v {
132                continue;
133            }
134            let d = dist[v * n + u];
135            if d == u32::MAX {
136                connected = false;
137                break;
138            }
139            sum_d += u64::from(d);
140        }
141        if !connected || sum_d == 0 {
142            return Ok(0.0);
143        }
144        closeness_vals.push((n - 1) as f64 / sum_d as f64);
145    }
146
147    let max_cc = closeness_vals.iter().copied().fold(0.0_f64, f64::max);
148    let sum_diff: f64 = closeness_vals.iter().map(|&c| max_cc - c).sum();
149
150    let n_f = n as f64;
151    let denom = (n_f - 2.0) * (n_f - 1.0) / (2.0 * n_f - 3.0);
152
153    if denom < 1e-30 {
154        return Ok(0.0);
155    }
156
157    Ok(sum_diff / denom)
158}
159
160/// Compute the centrality correlation (betweenness vs closeness).
161///
162/// Pearson correlation coefficient between betweenness centrality
163/// and closeness centrality. Returns 0.0 for disconnected, trivial,
164/// or constant-centrality graphs.
165///
166/// # Examples
167///
168/// ```
169/// use rust_igraph::{Graph, centrality_correlation};
170///
171/// // K_3: constant betweenness and closeness → 0.0
172/// let g = Graph::from_edges(&[(0,1),(1,2),(0,2)], false, Some(3)).unwrap();
173/// assert!(centrality_correlation(&g).unwrap().abs() < 1e-10);
174/// ```
175pub fn centrality_correlation(graph: &Graph) -> IgraphResult<f64> {
176    let n = graph.vcount() as usize;
177    if n < 3 {
178        return Ok(0.0);
179    }
180
181    let bc = compute_betweenness(graph)?;
182    let dist = all_pairs_bfs(graph)?;
183
184    let mut closeness_vals = Vec::with_capacity(n);
185    for v in 0..n {
186        let mut sum_d = 0_u64;
187        for u in 0..n {
188            if u == v {
189                continue;
190            }
191            let d = dist[v * n + u];
192            if d == u32::MAX {
193                return Ok(0.0);
194            }
195            sum_d += u64::from(d);
196        }
197        if sum_d == 0 {
198            closeness_vals.push(0.0);
199        } else {
200            closeness_vals.push((n - 1) as f64 / sum_d as f64);
201        }
202    }
203
204    let mean_bc = bc.iter().sum::<f64>() / n as f64;
205    let mean_cc = closeness_vals.iter().sum::<f64>() / n as f64;
206
207    let mut cov = 0.0_f64;
208    let mut var_bc = 0.0_f64;
209    let mut var_cc = 0.0_f64;
210
211    for v in 0..n {
212        let db = bc[v] - mean_bc;
213        let dc = closeness_vals[v] - mean_cc;
214        cov += db * dc;
215        var_bc += db * db;
216        var_cc += dc * dc;
217    }
218
219    if var_bc < 1e-30 || var_cc < 1e-30 {
220        return Ok(0.0);
221    }
222
223    Ok(cov / (var_bc.sqrt() * var_cc.sqrt()))
224}
225
226fn all_pairs_bfs(graph: &Graph) -> IgraphResult<Vec<u32>> {
227    let n = graph.vcount() as usize;
228    let mut dist = vec![u32::MAX; n * n];
229
230    for s in 0..n {
231        dist[s * n + s] = 0;
232        let mut queue = std::collections::VecDeque::new();
233        queue.push_back(s);
234        while let Some(v) = queue.pop_front() {
235            let d = dist[s * n + v];
236            let neighbors = graph.neighbors(v as u32)?;
237            for &u in &neighbors {
238                let ui = u as usize;
239                if dist[s * n + ui] == u32::MAX {
240                    dist[s * n + ui] = d + 1;
241                    queue.push_back(ui);
242                }
243            }
244        }
245    }
246
247    Ok(dist)
248}
249
250fn compute_betweenness(graph: &Graph) -> IgraphResult<Vec<f64>> {
251    let n = graph.vcount() as usize;
252    let directed = graph.is_directed();
253    let mut bc = vec![0.0_f64; n];
254
255    for s in 0..n {
256        let mut stack = Vec::new();
257        let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
258        let mut sigma = vec![0.0_f64; n];
259        sigma[s] = 1.0;
260        let mut dist_bfs = vec![-1_i64; n];
261        dist_bfs[s] = 0;
262
263        let mut queue = std::collections::VecDeque::new();
264        queue.push_back(s);
265
266        while let Some(v) = queue.pop_front() {
267            stack.push(v);
268            let neighbors = graph.neighbors(v as u32)?;
269            for &w in &neighbors {
270                let wi = w as usize;
271                if dist_bfs[wi] < 0 {
272                    queue.push_back(wi);
273                    dist_bfs[wi] = dist_bfs[v] + 1;
274                }
275                if dist_bfs[wi] == dist_bfs[v] + 1 {
276                    sigma[wi] += sigma[v];
277                    pred[wi].push(v);
278                }
279            }
280        }
281
282        let mut delta = vec![0.0_f64; n];
283        while let Some(w) = stack.pop() {
284            for &v in &pred[w] {
285                delta[v] += (sigma[v] / sigma[w]) * (1.0 + delta[w]);
286            }
287            if w != s {
288                bc[w] += delta[w];
289            }
290        }
291    }
292
293    if !directed {
294        for b in &mut bc {
295            *b /= 2.0;
296        }
297    }
298
299    Ok(bc)
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    fn single_edge() -> Graph {
307        Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
308    }
309
310    fn path3() -> Graph {
311        Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
312    }
313
314    fn k3() -> Graph {
315        Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
316    }
317
318    fn k4() -> Graph {
319        Graph::from_edges(
320            &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
321            false,
322            Some(4),
323        )
324        .unwrap()
325    }
326
327    fn cycle4() -> Graph {
328        Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
329    }
330
331    fn star5() -> Graph {
332        Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
333    }
334
335    fn paw() -> Graph {
336        Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
337    }
338
339    // --- degree_centralization ---
340
341    #[test]
342    fn dc_empty() {
343        let g = Graph::with_vertices(0);
344        assert!(degree_centralization(&g).unwrap().abs() < 1e-10);
345    }
346
347    #[test]
348    fn dc_single() {
349        let g = Graph::with_vertices(1);
350        assert!(degree_centralization(&g).unwrap().abs() < 1e-10);
351    }
352
353    #[test]
354    fn dc_two() {
355        assert!(degree_centralization(&single_edge()).unwrap().abs() < 1e-10);
356    }
357
358    #[test]
359    fn dc_k3() {
360        // Regular → 0
361        assert!(degree_centralization(&k3()).unwrap().abs() < 1e-10);
362    }
363
364    #[test]
365    fn dc_k4() {
366        assert!(degree_centralization(&k4()).unwrap().abs() < 1e-10);
367    }
368
369    #[test]
370    fn dc_cycle4() {
371        assert!(degree_centralization(&cycle4()).unwrap().abs() < 1e-10);
372    }
373
374    #[test]
375    fn dc_star5() {
376        // Star: max_deg=4, others=1, sum_diff=4*3=12, denom=4*3=12 → 1.0
377        assert!((degree_centralization(&star5()).unwrap() - 1.0).abs() < 1e-10);
378    }
379
380    #[test]
381    fn dc_path3() {
382        // deg: [1,2,1], max=2, sum_diff=1+0+1=2, denom=2*1=2 → 1.0
383        assert!((degree_centralization(&path3()).unwrap() - 1.0).abs() < 1e-10);
384    }
385
386    #[test]
387    fn dc_paw() {
388        // deg: [2,2,3,1], max=3, sum_diff=1+1+0+2=4, denom=3*2=6 → 2/3
389        assert!((degree_centralization(&paw()).unwrap() - 2.0 / 3.0).abs() < 1e-10);
390    }
391
392    #[test]
393    fn dc_in_01() {
394        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
395            let r = degree_centralization(g).unwrap();
396            assert!(r >= -1e-10);
397            assert!(r <= 1.0 + 1e-10);
398        }
399    }
400
401    // --- betweenness_centralization ---
402
403    #[test]
404    fn bc_empty() {
405        let g = Graph::with_vertices(0);
406        assert!(betweenness_centralization(&g).unwrap().abs() < 1e-10);
407    }
408
409    #[test]
410    fn bc_single() {
411        let g = Graph::with_vertices(1);
412        assert!(betweenness_centralization(&g).unwrap().abs() < 1e-10);
413    }
414
415    #[test]
416    fn bc_two() {
417        assert!(betweenness_centralization(&single_edge()).unwrap().abs() < 1e-10);
418    }
419
420    #[test]
421    fn bc_k3() {
422        // All betweenness = 0 → centralization = 0
423        assert!(betweenness_centralization(&k3()).unwrap().abs() < 1e-10);
424    }
425
426    #[test]
427    fn bc_k4() {
428        assert!(betweenness_centralization(&k4()).unwrap().abs() < 1e-10);
429    }
430
431    #[test]
432    fn bc_cycle4() {
433        // All betweenness equal (regular) → centralization = 0
434        assert!(betweenness_centralization(&cycle4()).unwrap().abs() < 1e-10);
435    }
436
437    #[test]
438    fn bc_path3() {
439        // v0: bc=0, v1: bc=1, v2: bc=0
440        // max=1, sum_diff=1+0+1=2, denom=2*1=2 → 1.0
441        assert!((betweenness_centralization(&path3()).unwrap() - 1.0).abs() < 1e-10);
442    }
443
444    #[test]
445    fn bc_star5() {
446        // Center: bc = (4*3)/2 = 6, leaves: bc = 0
447        // sum_diff = 6*4 = 24, denom = 4*3 = 12 → 24/12 = 2.0
448        // Wait, that exceeds 1.0 — betweenness centralization uses a
449        // different denominator. Let me check.
450        // Standard Freeman BC centralization: sum(max-b_i) / ((n-1)*(n-2)/2)
451        // For star5: sum_diff=24, denom = 4*3/2 = 6 → 24/6 = 4.0
452        // Actually our formula uses (n-1)*(n-2) without the /2
453        // bc = unnormalized/2 for undirected. Center: pairs through center
454        // that must go through center: 4C2 = 6. bc_center = 6.
455        // sum_diff = 4*6 = 24, denom = 4*3 = 12 → 2.0
456        // This is > 1 which is fine for non-normalized betweenness centralization.
457        let r = betweenness_centralization(&star5()).unwrap();
458        assert!(r > 1.0);
459    }
460
461    #[test]
462    fn bc_nonneg() {
463        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
464            assert!(betweenness_centralization(g).unwrap() >= -1e-10);
465        }
466    }
467
468    // --- closeness_centralization ---
469
470    #[test]
471    fn cc_empty() {
472        let g = Graph::with_vertices(0);
473        assert!(closeness_centralization(&g).unwrap().abs() < 1e-10);
474    }
475
476    #[test]
477    fn cc_single() {
478        let g = Graph::with_vertices(1);
479        assert!(closeness_centralization(&g).unwrap().abs() < 1e-10);
480    }
481
482    #[test]
483    fn cc_two() {
484        assert!(closeness_centralization(&single_edge()).unwrap().abs() < 1e-10);
485    }
486
487    #[test]
488    fn cc_k3() {
489        // All closeness = 1 → centralization = 0
490        assert!(closeness_centralization(&k3()).unwrap().abs() < 1e-10);
491    }
492
493    #[test]
494    fn cc_k4() {
495        assert!(closeness_centralization(&k4()).unwrap().abs() < 1e-10);
496    }
497
498    #[test]
499    fn cc_cycle4() {
500        // Regular → all same closeness → 0
501        assert!(closeness_centralization(&cycle4()).unwrap().abs() < 1e-10);
502    }
503
504    #[test]
505    fn cc_star5() {
506        // Center: closeness=1.0 (dist 1 to all)
507        // Leaves: closeness=4/(1+2+2+2)=4/7
508        // max=1, sum_diff=4*(1-4/7)=4*3/7=12/7
509        // denom=(5-2)*(5-1)/(2*5-3) = 3*4/7 = 12/7
510        // centralization = (12/7)/(12/7) = 1.0
511        assert!((closeness_centralization(&star5()).unwrap() - 1.0).abs() < 1e-10);
512    }
513
514    #[test]
515    fn cc_path3() {
516        // v0: c=2/(1+2)=2/3, v1: c=2/(1+1)=1, v2: c=2/3
517        // max=1, sum_diff=(1/3)+(0)+(1/3)=2/3
518        // denom=(3-2)*(3-1)/(2*3-3) = 1*2/3 = 2/3
519        // centralization = (2/3)/(2/3) = 1.0
520        assert!((closeness_centralization(&path3()).unwrap() - 1.0).abs() < 1e-10);
521    }
522
523    #[test]
524    fn cc_disconnected() {
525        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
526        assert!(closeness_centralization(&g).unwrap().abs() < 1e-10);
527    }
528
529    #[test]
530    fn cc_nonneg() {
531        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
532            assert!(closeness_centralization(g).unwrap() >= -1e-10);
533        }
534    }
535
536    // --- centrality_correlation ---
537
538    #[test]
539    fn ccorr_empty() {
540        let g = Graph::with_vertices(0);
541        assert!(centrality_correlation(&g).unwrap().abs() < 1e-10);
542    }
543
544    #[test]
545    fn ccorr_single() {
546        let g = Graph::with_vertices(1);
547        assert!(centrality_correlation(&g).unwrap().abs() < 1e-10);
548    }
549
550    #[test]
551    fn ccorr_two() {
552        assert!(centrality_correlation(&single_edge()).unwrap().abs() < 1e-10);
553    }
554
555    #[test]
556    fn ccorr_k3() {
557        // Constant → 0
558        assert!(centrality_correlation(&k3()).unwrap().abs() < 1e-10);
559    }
560
561    #[test]
562    fn ccorr_k4() {
563        assert!(centrality_correlation(&k4()).unwrap().abs() < 1e-10);
564    }
565
566    #[test]
567    fn ccorr_cycle4() {
568        // Regular → constant betweenness and closeness → 0
569        assert!(centrality_correlation(&cycle4()).unwrap().abs() < 1e-10);
570    }
571
572    #[test]
573    fn ccorr_star5() {
574        // Center has highest betweenness AND highest closeness → positive
575        let r = centrality_correlation(&star5()).unwrap();
576        assert!(r > 0.5);
577    }
578
579    #[test]
580    fn ccorr_path3() {
581        // v1 has highest betweenness AND highest closeness → positive
582        let r = centrality_correlation(&path3()).unwrap();
583        assert!(r > 0.5);
584    }
585
586    #[test]
587    fn ccorr_disconnected() {
588        let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
589        assert!(centrality_correlation(&g).unwrap().abs() < 1e-10);
590    }
591
592    #[test]
593    fn ccorr_in_range() {
594        for g in &[single_edge(), path3(), k3(), k4(), cycle4(), star5(), paw()] {
595            let r = centrality_correlation(g).unwrap();
596            assert!(r >= -1.0 - 1e-10);
597            assert!(r <= 1.0 + 1e-10);
598        }
599    }
600
601    // --- cross-consistency ---
602
603    #[test]
604    fn regular_zero_dc() {
605        assert!(degree_centralization(&k3()).unwrap().abs() < 1e-10);
606        assert!(degree_centralization(&k4()).unwrap().abs() < 1e-10);
607        assert!(degree_centralization(&cycle4()).unwrap().abs() < 1e-10);
608    }
609
610    #[test]
611    fn complete_zero_bc() {
612        assert!(betweenness_centralization(&k3()).unwrap().abs() < 1e-10);
613        assert!(betweenness_centralization(&k4()).unwrap().abs() < 1e-10);
614    }
615
616    #[test]
617    fn complete_zero_cc() {
618        assert!(closeness_centralization(&k3()).unwrap().abs() < 1e-10);
619        assert!(closeness_centralization(&k4()).unwrap().abs() < 1e-10);
620    }
621}