rust_igraph/algorithms/properties/
assortativity_nominal.rs1use crate::core::{Graph, IgraphError, IgraphResult};
11
12pub fn assortativity_nominal(
50 graph: &Graph,
51 types: &[u32],
52 directed: bool,
53 normalized: bool,
54) -> IgraphResult<f64> {
55 let n = graph.vcount();
56 let ecount = graph.ecount();
57
58 #[allow(clippy::cast_possible_truncation)]
59 if types.len() != n as usize {
60 return Err(IgraphError::InvalidArgument(format!(
61 "assortativity_nominal: types length ({}) does not match vertex count ({n})",
62 types.len()
63 )));
64 }
65
66 if n == 0 || ecount == 0 {
67 return Ok(f64::NAN);
68 }
69
70 let directed = directed && graph.is_directed();
71
72 let no_of_types = types
73 .iter()
74 .copied()
75 .max()
76 .unwrap_or(0)
77 .checked_add(1)
78 .ok_or_else(|| {
79 IgraphError::InvalidArgument("assortativity_nominal: type value overflow".to_string())
80 })? as usize;
81
82 let mut ai: Vec<u64> = vec![0; no_of_types];
83 let mut bi: Vec<u64> = vec![0; no_of_types];
84 let mut eii: Vec<u64> = vec![0; no_of_types];
85
86 for eid in 0..ecount {
87 #[allow(clippy::cast_possible_truncation)]
88 let (from, to) = graph.edge(eid as u32)?;
89 let from_type = types[from as usize] as usize;
90 let to_type = types[to as usize] as usize;
91
92 ai[from_type] += 1;
93 bi[to_type] += 1;
94 if from_type == to_type {
95 eii[from_type] += 1;
96 }
97 if !directed {
98 if from_type == to_type {
99 eii[from_type] += 1;
100 }
101 ai[to_type] += 1;
102 bi[from_type] += 1;
103 }
104 }
105
106 #[allow(clippy::cast_precision_loss)]
107 let m = ecount as f64;
108
109 let mut sumaibi = 0.0_f64;
110 let mut sumeii = 0.0_f64;
111
112 for i in 0..no_of_types {
113 #[allow(clippy::cast_precision_loss)]
114 let ai_f = ai[i] as f64;
115 #[allow(clippy::cast_precision_loss)]
116 let bi_f = bi[i] as f64;
117 #[allow(clippy::cast_precision_loss)]
118 let eii_f = eii[i] as f64;
119
120 sumaibi += (ai_f / m) * (bi_f / m);
121 sumeii += eii_f / m;
122 }
123
124 if !directed {
125 sumaibi /= 4.0;
126 sumeii /= 2.0;
127 }
128
129 if normalized {
130 let denom = 1.0 - sumaibi;
131 if denom.abs() < f64::EPSILON {
132 return Ok(f64::NAN);
133 }
134 Ok((sumeii - sumaibi) / denom)
135 } else {
136 Ok(sumeii - sumaibi)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn approx_eq(a: f64, b: f64) -> bool {
145 (a - b).abs() < 1e-10
146 }
147
148 #[test]
149 fn empty_graph() {
150 let g = Graph::with_vertices(0);
151 let r = assortativity_nominal(&g, &[], false, true).unwrap();
152 assert!(r.is_nan());
153 }
154
155 #[test]
156 fn no_edges() {
157 let g = Graph::with_vertices(3);
158 let types = vec![0, 1, 2];
159 let r = assortativity_nominal(&g, &types, false, true).unwrap();
160 assert!(r.is_nan());
161 }
162
163 #[test]
164 fn perfect_assortative_undirected() {
165 let mut g = Graph::with_vertices(4);
167 g.add_edge(0, 1).unwrap();
168 g.add_edge(2, 3).unwrap();
169 let types = vec![0, 0, 1, 1];
170 let r = assortativity_nominal(&g, &types, false, true).unwrap();
171 assert!(approx_eq(r, 1.0));
172 }
173
174 #[test]
175 fn perfect_disassortative_undirected() {
176 let mut g = Graph::with_vertices(4);
178 g.add_edge(0, 2).unwrap();
179 g.add_edge(0, 3).unwrap();
180 g.add_edge(1, 2).unwrap();
181 g.add_edge(1, 3).unwrap();
182 let types = vec![0, 0, 1, 1];
183 let r = assortativity_nominal(&g, &types, false, true).unwrap();
184 assert!(approx_eq(r, -1.0));
185 }
186
187 #[test]
188 fn mixed_undirected() {
189 let mut g = Graph::with_vertices(3);
190 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); g.add_edge(0, 2).unwrap(); let types = vec![0, 0, 1];
194 let r = assortativity_nominal(&g, &types, false, true).unwrap();
195 assert!(approx_eq(r, -0.5));
196 }
197
198 #[test]
199 fn directed_graph() {
200 let mut g = Graph::new(3, true).unwrap();
201 g.add_edge(0, 1).unwrap(); g.add_edge(1, 2).unwrap(); let types = vec![0, 0, 1];
204 let r = assortativity_nominal(&g, &types, true, true).unwrap();
205 assert!(approx_eq(r, 0.0));
207 }
208
209 #[test]
210 fn directed_as_undirected() {
211 let mut g = Graph::new(4, true).unwrap();
212 g.add_edge(0, 1).unwrap();
213 g.add_edge(2, 3).unwrap();
214 let types = vec![0, 0, 1, 1];
215 let r = assortativity_nominal(&g, &types, false, true).unwrap();
217 assert!(approx_eq(r, 1.0));
218 }
219
220 #[test]
221 fn unnormalized_equals_modularity() {
222 let mut g = Graph::with_vertices(4);
223 g.add_edge(0, 1).unwrap();
224 g.add_edge(2, 3).unwrap();
225 g.add_edge(1, 2).unwrap();
226 let types = vec![0, 0, 1, 1];
227 let r = assortativity_nominal(&g, &types, false, false).unwrap();
228 assert!(approx_eq(r, 1.0 / 6.0));
230 }
231
232 #[test]
233 fn types_length_mismatch() {
234 let g = Graph::with_vertices(3);
235 let types = vec![0, 1]; assert!(assortativity_nominal(&g, &types, false, true).is_err());
237 }
238
239 #[test]
240 fn single_type_all_same() {
241 let mut g = Graph::with_vertices(3);
242 g.add_edge(0, 1).unwrap();
243 g.add_edge(1, 2).unwrap();
244 let types = vec![0, 0, 0];
245 let r = assortativity_nominal(&g, &types, false, true).unwrap();
246 assert!(r.is_nan());
249 }
250
251 #[test]
252 fn self_loop_undirected() {
253 let mut g = Graph::with_vertices(2);
254 g.add_edge(0, 0).unwrap(); g.add_edge(0, 1).unwrap();
256 let types = vec![0, 1];
257 let r = assortativity_nominal(&g, &types, false, true).unwrap();
258 assert!(approx_eq(r, -1.0 / 3.0));
260 }
261
262 #[test]
263 fn many_types() {
264 let mut g = Graph::with_vertices(6);
265 g.add_edge(0, 1).unwrap(); g.add_edge(2, 3).unwrap(); g.add_edge(4, 5).unwrap(); let types = vec![0, 1, 2, 0, 1, 2];
269 let r = assortativity_nominal(&g, &types, false, true).unwrap();
270 assert!(r < 0.0);
271 }
272}