rust_igraph/algorithms/properties/
degree_deviation.rs1#![allow(
12 clippy::cast_possible_truncation,
13 clippy::cast_precision_loss,
14 clippy::many_single_char_names,
15 clippy::needless_range_loop,
16 clippy::too_many_lines
17)]
18
19use crate::core::{Graph, IgraphResult};
20use std::collections::HashMap;
21
22pub fn degree_mad(graph: &Graph) -> IgraphResult<f64> {
39 let n = graph.vcount() as usize;
40 if n == 0 {
41 return Ok(0.0);
42 }
43
44 let degrees = collect_degrees(graph)?;
45 let mean = degrees.iter().sum::<f64>() / n as f64;
46
47 let sum_abs: f64 = degrees.iter().map(|&d| (d - mean).abs()).sum();
48 Ok(sum_abs / n as f64)
49}
50
51pub fn degree_median_ad(graph: &Graph) -> IgraphResult<f64> {
68 let n = graph.vcount() as usize;
69 if n == 0 {
70 return Ok(0.0);
71 }
72
73 let degrees = collect_degrees(graph)?;
74 let med = median_of(°rees);
75
76 let mut abs_devs: Vec<f64> = degrees.iter().map(|&d| (d - med).abs()).collect();
77 abs_devs.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
78
79 Ok(median_sorted(&abs_devs))
80}
81
82pub fn degree_entropy_ln(graph: &Graph) -> IgraphResult<f64> {
101 let n = graph.vcount() as usize;
102 if n == 0 {
103 return Ok(0.0);
104 }
105
106 let mut counts: HashMap<usize, usize> = HashMap::new();
107 for v in 0..n {
108 let d = graph.degree(v as u32)?;
109 *counts.entry(d).or_insert(0) += 1;
110 }
111
112 let nf = n as f64;
113 let mut entropy = 0.0_f64;
114 for &count in counts.values() {
115 if count > 0 {
116 let p = count as f64 / nf;
117 entropy -= p * p.ln();
118 }
119 }
120
121 Ok(entropy)
122}
123
124pub fn degree_entropy_normalized(graph: &Graph) -> IgraphResult<f64> {
144 let n = graph.vcount() as usize;
145 if n < 2 {
146 return Ok(0.0);
147 }
148
149 let h = degree_entropy_ln(graph)?;
150 let h_max = (n as f64).ln();
151
152 if h_max <= 0.0 {
153 return Ok(0.0);
154 }
155
156 Ok(h / h_max)
157}
158
159fn collect_degrees(graph: &Graph) -> IgraphResult<Vec<f64>> {
160 let n = graph.vcount() as usize;
161 let mut degrees = Vec::with_capacity(n);
162 for v in 0..n {
163 degrees.push(graph.degree(v as u32)? as f64);
164 }
165 Ok(degrees)
166}
167
168fn median_of(vals: &[f64]) -> f64 {
169 let mut sorted = vals.to_vec();
170 sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
171 median_sorted(&sorted)
172}
173
174fn median_sorted(sorted: &[f64]) -> f64 {
175 let n = sorted.len();
176 if n == 0 {
177 return 0.0;
178 }
179 if n % 2 == 1 {
180 sorted[n / 2]
181 } else {
182 f64::midpoint(sorted[n / 2 - 1], sorted[n / 2])
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn single_edge() -> Graph {
191 Graph::from_edges(&[(0, 1)], false, Some(2)).unwrap()
192 }
193
194 fn path3() -> Graph {
195 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
196 }
197
198 fn k3() -> Graph {
199 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
200 }
201
202 fn k4() -> Graph {
203 Graph::from_edges(
204 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
205 false,
206 Some(4),
207 )
208 .unwrap()
209 }
210
211 fn cycle4() -> Graph {
212 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
213 }
214
215 fn star5() -> Graph {
216 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
217 }
218
219 fn paw() -> Graph {
220 Graph::from_edges(&[(0, 1), (1, 2), (0, 2), (2, 3)], false, Some(4)).unwrap()
221 }
222
223 #[test]
226 fn mad_empty() {
227 let g = Graph::with_vertices(0);
228 assert!(degree_mad(&g).unwrap().abs() < 1e-10);
229 }
230
231 #[test]
232 fn mad_isolated() {
233 let g = Graph::with_vertices(5);
234 assert!(degree_mad(&g).unwrap().abs() < 1e-10);
235 }
236
237 #[test]
238 fn mad_regular_zero() {
239 assert!(degree_mad(&k3()).unwrap().abs() < 1e-10);
240 assert!(degree_mad(&k4()).unwrap().abs() < 1e-10);
241 assert!(degree_mad(&cycle4()).unwrap().abs() < 1e-10);
242 }
243
244 #[test]
245 fn mad_single_edge() {
246 assert!(degree_mad(&single_edge()).unwrap().abs() < 1e-10);
247 }
248
249 #[test]
250 fn mad_star5() {
251 assert!((degree_mad(&star5()).unwrap() - 0.96).abs() < 1e-10);
255 }
256
257 #[test]
258 fn mad_path3() {
259 let expected = 4.0 / 9.0;
263 assert!((degree_mad(&path3()).unwrap() - expected).abs() < 1e-10);
264 }
265
266 #[test]
267 fn mad_paw() {
268 assert!((degree_mad(&paw()).unwrap() - 0.5).abs() < 1e-10);
272 }
273
274 #[test]
275 fn mad_nonneg() {
276 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
277 assert!(degree_mad(g).unwrap() >= -1e-10);
278 }
279 }
280
281 #[test]
284 fn medad_empty() {
285 let g = Graph::with_vertices(0);
286 assert!(degree_median_ad(&g).unwrap().abs() < 1e-10);
287 }
288
289 #[test]
290 fn medad_isolated() {
291 let g = Graph::with_vertices(5);
292 assert!(degree_median_ad(&g).unwrap().abs() < 1e-10);
293 }
294
295 #[test]
296 fn medad_regular_zero() {
297 assert!(degree_median_ad(&k3()).unwrap().abs() < 1e-10);
298 assert!(degree_median_ad(&k4()).unwrap().abs() < 1e-10);
299 assert!(degree_median_ad(&cycle4()).unwrap().abs() < 1e-10);
300 }
301
302 #[test]
303 fn medad_single_edge() {
304 assert!(degree_median_ad(&single_edge()).unwrap().abs() < 1e-10);
305 }
306
307 #[test]
308 fn medad_star5() {
309 assert!(degree_median_ad(&star5()).unwrap().abs() < 1e-10);
313 }
314
315 #[test]
316 fn medad_path3() {
317 assert!(degree_median_ad(&path3()).unwrap().abs() < 1e-10);
321 }
322
323 #[test]
324 fn medad_paw() {
325 assert!((degree_median_ad(&paw()).unwrap() - 0.5).abs() < 1e-10);
329 }
330
331 #[test]
332 fn medad_nonneg() {
333 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
334 assert!(degree_median_ad(g).unwrap() >= -1e-10);
335 }
336 }
337
338 #[test]
341 fn ent_empty() {
342 let g = Graph::with_vertices(0);
343 assert!(degree_entropy_ln(&g).unwrap().abs() < 1e-10);
344 }
345
346 #[test]
347 fn ent_isolated() {
348 let g = Graph::with_vertices(5);
350 assert!(degree_entropy_ln(&g).unwrap().abs() < 1e-10);
351 }
352
353 #[test]
354 fn ent_regular_zero() {
355 assert!(degree_entropy_ln(&k3()).unwrap().abs() < 1e-10);
356 assert!(degree_entropy_ln(&k4()).unwrap().abs() < 1e-10);
357 assert!(degree_entropy_ln(&cycle4()).unwrap().abs() < 1e-10);
358 }
359
360 #[test]
361 fn ent_single_edge() {
362 assert!(degree_entropy_ln(&single_edge()).unwrap().abs() < 1e-10);
364 }
365
366 #[test]
367 fn ent_star5() {
368 let expected = -(0.2_f64 * 0.2_f64.ln() + 0.8 * 0.8_f64.ln());
371 assert!((degree_entropy_ln(&star5()).unwrap() - expected).abs() < 1e-10);
372 }
373
374 #[test]
375 fn ent_path3() {
376 let p1: f64 = 2.0 / 3.0;
379 let p2: f64 = 1.0 / 3.0;
380 let expected = -(p1 * p1.ln() + p2 * p2.ln());
381 assert!((degree_entropy_ln(&path3()).unwrap() - expected).abs() < 1e-10);
382 }
383
384 #[test]
385 fn ent_paw() {
386 let expected = -(0.5_f64 * 0.5_f64.ln() + 2.0 * 0.25 * 0.25_f64.ln());
389 assert!((degree_entropy_ln(&paw()).unwrap() - expected).abs() < 1e-10);
390 }
391
392 #[test]
393 fn ent_nonneg() {
394 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
395 assert!(degree_entropy_ln(g).unwrap() >= -1e-10);
396 }
397 }
398
399 #[test]
402 fn entnorm_empty() {
403 let g = Graph::with_vertices(0);
404 assert!(degree_entropy_normalized(&g).unwrap().abs() < 1e-10);
405 }
406
407 #[test]
408 fn entnorm_single() {
409 let g = Graph::with_vertices(1);
410 assert!(degree_entropy_normalized(&g).unwrap().abs() < 1e-10);
411 }
412
413 #[test]
414 fn entnorm_regular_zero() {
415 assert!(degree_entropy_normalized(&k3()).unwrap().abs() < 1e-10);
416 assert!(degree_entropy_normalized(&k4()).unwrap().abs() < 1e-10);
417 assert!(degree_entropy_normalized(&cycle4()).unwrap().abs() < 1e-10);
418 }
419
420 #[test]
421 fn entnorm_star5() {
422 let h = degree_entropy_ln(&star5()).unwrap();
423 let h_max = 5.0_f64.ln();
424 let expected = h / h_max;
425 assert!((degree_entropy_normalized(&star5()).unwrap() - expected).abs() < 1e-10);
426 }
427
428 #[test]
429 fn entnorm_paw() {
430 let h = degree_entropy_ln(&paw()).unwrap();
431 let h_max = 4.0_f64.ln();
432 let expected = h / h_max;
433 assert!((degree_entropy_normalized(&paw()).unwrap() - expected).abs() < 1e-10);
434 }
435
436 #[test]
437 fn entnorm_in_01() {
438 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
439 let val = degree_entropy_normalized(g).unwrap();
440 assert!(val >= -1e-10);
441 assert!(val <= 1.0 + 1e-10);
442 }
443 }
444
445 #[test]
448 fn mad_le_maxdev() {
449 for g in &[single_edge(), path3(), k3(), k4(), star5(), paw()] {
451 let mad_val = degree_mad(g).unwrap();
452 let n = g.vcount() as usize;
453 let mut degrees = Vec::with_capacity(n);
454 for v in 0..n {
455 degrees.push(g.degree(v as u32).unwrap() as f64);
456 }
457 let mean = degrees.iter().sum::<f64>() / n as f64;
458 let max_dev: f64 = degrees
459 .iter()
460 .map(|&d| (d - mean).abs())
461 .fold(0.0, f64::max);
462 assert!(mad_val <= max_dev + 1e-10);
463 }
464 }
465
466 #[test]
467 fn medad_le_mad() {
468 for g in &[k3(), k4(), cycle4()] {
470 assert!(degree_median_ad(g).unwrap() <= degree_mad(g).unwrap() + 1e-10);
471 }
472 }
473}