rust_igraph/algorithms/properties/
link_prediction.rs1#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
15
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18pub fn link_pred_common_neighbors(
36 graph: &Graph,
37 pairs: &[(VertexId, VertexId)],
38) -> IgraphResult<Vec<f64>> {
39 validate_pairs(graph, pairs)?;
40 let mut scores = Vec::with_capacity(pairs.len());
41
42 for &(u, v) in pairs {
43 let nu = neighbor_set(graph, u)?;
44 let nv = neighbor_set(graph, v)?;
45 let cn = count_intersection(&nu, &nv);
46 scores.push(cn as f64);
47 }
48
49 Ok(scores)
50}
51
52pub fn link_pred_adamic_adar(
73 graph: &Graph,
74 pairs: &[(VertexId, VertexId)],
75) -> IgraphResult<Vec<f64>> {
76 validate_pairs(graph, pairs)?;
77 let nv = graph.vcount() as usize;
78
79 let mut degrees = Vec::with_capacity(nv);
80 for v in 0..nv {
81 degrees.push(graph.degree(v as VertexId)?);
82 }
83
84 let mut scores = Vec::with_capacity(pairs.len());
85
86 for &(u, v) in pairs {
87 let nu = neighbor_set(graph, u)?;
88 let nv_set = neighbor_set(graph, v)?;
89 let mut score = 0.0;
90 for &w in &nu {
91 if nv_set.contains(&w) {
92 let deg = degrees[w as usize];
93 if deg > 1 {
94 score += 1.0 / (deg as f64).ln();
95 }
96 }
97 }
98 scores.push(score);
99 }
100
101 Ok(scores)
102}
103
104pub fn link_pred_resource_allocation(
124 graph: &Graph,
125 pairs: &[(VertexId, VertexId)],
126) -> IgraphResult<Vec<f64>> {
127 validate_pairs(graph, pairs)?;
128 let nv = graph.vcount() as usize;
129
130 let mut degrees = Vec::with_capacity(nv);
131 for v in 0..nv {
132 degrees.push(graph.degree(v as VertexId)?);
133 }
134
135 let mut scores = Vec::with_capacity(pairs.len());
136
137 for &(u, v) in pairs {
138 let nu = neighbor_set(graph, u)?;
139 let nv_set = neighbor_set(graph, v)?;
140 let mut score = 0.0;
141 for &w in &nu {
142 if nv_set.contains(&w) {
143 let deg = degrees[w as usize];
144 if deg > 0 {
145 score += 1.0 / deg as f64;
146 }
147 }
148 }
149 scores.push(score);
150 }
151
152 Ok(scores)
153}
154
155pub fn link_pred_preferential_attachment(
174 graph: &Graph,
175 pairs: &[(VertexId, VertexId)],
176) -> IgraphResult<Vec<f64>> {
177 validate_pairs(graph, pairs)?;
178
179 let mut scores = Vec::with_capacity(pairs.len());
180
181 for &(u, v) in pairs {
182 let du = graph.degree(u)?;
183 let dv = graph.degree(v)?;
184 scores.push((du * dv) as f64);
185 }
186
187 Ok(scores)
188}
189
190pub fn link_pred_jaccard(graph: &Graph, pairs: &[(VertexId, VertexId)]) -> IgraphResult<Vec<f64>> {
209 validate_pairs(graph, pairs)?;
210 let mut scores = Vec::with_capacity(pairs.len());
211
212 for &(u, v) in pairs {
213 let nu = neighbor_set(graph, u)?;
214 let nv = neighbor_set(graph, v)?;
215 let intersection = count_intersection(&nu, &nv);
216 let union_size = nu.len() + nv.len() - intersection;
217 let score = if union_size == 0 {
218 0.0
219 } else {
220 intersection as f64 / union_size as f64
221 };
222 scores.push(score);
223 }
224
225 Ok(scores)
226}
227
228fn validate_pairs(graph: &Graph, pairs: &[(VertexId, VertexId)]) -> IgraphResult<()> {
231 let n = graph.vcount();
232 for &(u, v) in pairs {
233 if u >= n {
234 return Err(IgraphError::VertexOutOfRange { id: u, n });
235 }
236 if v >= n {
237 return Err(IgraphError::VertexOutOfRange { id: v, n });
238 }
239 }
240 Ok(())
241}
242
243fn neighbor_set(graph: &Graph, v: VertexId) -> IgraphResult<Vec<VertexId>> {
244 graph.neighbors(v)
245}
246
247fn count_intersection(a: &[VertexId], b: &[VertexId]) -> usize {
248 let mut count = 0;
250 for &x in a {
251 if b.contains(&x) {
252 count += 1;
253 }
254 }
255 count
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn diamond() -> Graph {
263 Graph::from_edges(&[(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], false, Some(4)).unwrap()
265 }
266
267 fn star5() -> Graph {
268 Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false, Some(5)).unwrap()
270 }
271
272 #[test]
275 fn cn_basic() {
276 let g = diamond();
277 let scores = link_pred_common_neighbors(&g, &[(0, 3)]).unwrap();
278 assert!((scores[0] - 2.0).abs() < 1e-10); }
280
281 #[test]
282 fn cn_adjacent_pair() {
283 let g = diamond();
284 let scores = link_pred_common_neighbors(&g, &[(0, 1)]).unwrap();
285 assert!((scores[0] - 1.0).abs() < 1e-10); }
287
288 #[test]
289 fn cn_no_common() {
290 let g = star5();
291 let scores = link_pred_common_neighbors(&g, &[(1, 2)]).unwrap();
292 assert!((scores[0] - 1.0).abs() < 1e-10); }
294
295 #[test]
296 fn cn_disconnected_pair() {
297 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
298 let scores = link_pred_common_neighbors(&g, &[(0, 2)]).unwrap();
299 assert!(scores[0].abs() < 1e-10);
300 }
301
302 #[test]
303 fn cn_multiple_pairs() {
304 let g = diamond();
305 let scores = link_pred_common_neighbors(&g, &[(0, 3), (0, 1), (1, 3)]).unwrap();
306 assert_eq!(scores.len(), 3);
307 }
308
309 #[test]
310 fn cn_empty_pairs() {
311 let g = diamond();
312 let scores = link_pred_common_neighbors(&g, &[]).unwrap();
313 assert!(scores.is_empty());
314 }
315
316 #[test]
317 fn cn_invalid_vertex() {
318 let g = diamond();
319 assert!(link_pred_common_neighbors(&g, &[(0, 10)]).is_err());
320 }
321
322 #[test]
325 fn aa_basic() {
326 let g = diamond();
327 let scores = link_pred_adamic_adar(&g, &[(0, 3)]).unwrap();
328 let expected = 2.0 / 3.0_f64.ln();
330 assert!((scores[0] - expected).abs() < 1e-10);
331 }
332
333 #[test]
334 fn aa_no_common() {
335 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
336 let scores = link_pred_adamic_adar(&g, &[(0, 2)]).unwrap();
337 assert!(scores[0].abs() < 1e-10);
338 }
339
340 #[test]
341 fn aa_degree_one_neighbor() {
342 let g = Graph::from_edges(&[(0, 2), (1, 2)], false, Some(3)).unwrap();
344 let scores = link_pred_adamic_adar(&g, &[(0, 1)]).unwrap();
345 let expected = 1.0 / 2.0_f64.ln();
347 assert!((scores[0] - expected).abs() < 1e-10);
348 }
349
350 #[test]
353 fn ra_basic() {
354 let g = diamond();
355 let scores = link_pred_resource_allocation(&g, &[(0, 3)]).unwrap();
356 let expected = 2.0 / 3.0;
358 assert!((scores[0] - expected).abs() < 1e-10);
359 }
360
361 #[test]
362 fn ra_no_common() {
363 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
364 let scores = link_pred_resource_allocation(&g, &[(0, 2)]).unwrap();
365 assert!(scores[0].abs() < 1e-10);
366 }
367
368 #[test]
371 fn pa_basic() {
372 let g = diamond();
373 let scores = link_pred_preferential_attachment(&g, &[(0, 3)]).unwrap();
374 assert!((scores[0] - 4.0).abs() < 1e-10);
376 }
377
378 #[test]
379 fn pa_star_center() {
380 let g = star5();
381 let scores = link_pred_preferential_attachment(&g, &[(1, 2)]).unwrap();
382 assert!((scores[0] - 1.0).abs() < 1e-10);
384 }
385
386 #[test]
387 fn pa_isolated() {
388 let g = Graph::with_vertices(3);
389 let scores = link_pred_preferential_attachment(&g, &[(0, 1)]).unwrap();
390 assert!(scores[0].abs() < 1e-10);
391 }
392
393 #[test]
396 fn jaccard_perfect_overlap() {
397 let g = diamond();
398 let scores = link_pred_jaccard(&g, &[(0, 3)]).unwrap();
399 assert!((scores[0] - 1.0).abs() < 1e-10);
401 }
402
403 #[test]
404 fn jaccard_partial_overlap() {
405 let g = diamond();
406 let scores = link_pred_jaccard(&g, &[(0, 1)]).unwrap();
407 assert!((scores[0] - 0.25).abs() < 1e-10);
410 }
411
412 #[test]
413 fn jaccard_no_overlap() {
414 let g = Graph::from_edges(&[(0, 1), (2, 3)], false, Some(4)).unwrap();
415 let scores = link_pred_jaccard(&g, &[(0, 2)]).unwrap();
416 assert!(scores[0].abs() < 1e-10);
417 }
418
419 #[test]
420 fn jaccard_both_isolated() {
421 let g = Graph::with_vertices(3);
422 let scores = link_pred_jaccard(&g, &[(0, 1)]).unwrap();
423 assert!(scores[0].abs() < 1e-10);
424 }
425}