rust_igraph/algorithms/properties/
pagerank.rs1use crate::core::{Graph, IgraphResult};
22
23const DEFAULT_DAMPING: f64 = 0.85;
24const DEFAULT_EPS: f64 = 1e-10;
25const DEFAULT_MAX_ITER: usize = 1000;
26
27pub fn pagerank(graph: &Graph) -> IgraphResult<Vec<f64>> {
53 let n = graph.vcount();
54 let n_us = n as usize;
55 if n == 0 {
56 return Ok(Vec::new());
57 }
58 if n == 1 {
59 return Ok(vec![1.0]);
60 }
61
62 let directed = graph.is_directed();
63
64 let n_f = f64::from(n);
69 let mut out_deg = vec![0u64; n_us];
70 if directed {
71 for v in 0..n {
72 let v_us = v as usize;
73 let nbrs = graph.neighbors(v)?;
76 out_deg[v_us] = nbrs.len() as u64;
77 }
78 } else {
79 for v in 0..n {
80 let v_us = v as usize;
81 let nbrs = graph.neighbors(v)?;
82 out_deg[v_us] = nbrs.len() as u64;
86 }
87 }
88
89 let mut in_adj: Vec<Vec<u32>> = vec![Vec::new(); n_us];
92 if directed {
93 let m = u32::try_from(graph.ecount())
95 .map_err(|_| crate::core::IgraphError::Internal("ecount overflows u32"))?;
96 for e in 0..m {
97 let (u, v) = graph.edge(e)?;
98 in_adj[v as usize].push(u);
99 }
100 } else {
101 let m = u32::try_from(graph.ecount())
103 .map_err(|_| crate::core::IgraphError::Internal("ecount overflows u32"))?;
104 for e in 0..m {
105 let (u, v) = graph.edge(e)?;
106 if u == v {
109 in_adj[v as usize].push(u);
110 in_adj[v as usize].push(u);
111 } else {
112 in_adj[u as usize].push(v);
113 in_adj[v as usize].push(u);
114 }
115 }
116 }
117
118 let mut pr = vec![1.0 / n_f; n_us];
120 let mut pr_new = vec![0.0_f64; n_us];
121
122 for _ in 0..DEFAULT_MAX_ITER {
123 let mut dangling_sum: f64 = 0.0;
125 for v in 0..n_us {
126 if out_deg[v] == 0 {
127 dangling_sum += pr[v];
128 }
129 }
130
131 let teleport = (1.0 - DEFAULT_DAMPING) / n_f;
132 let dangling_share = DEFAULT_DAMPING * dangling_sum / n_f;
133
134 for v in 0..n_us {
135 let mut incoming: f64 = 0.0;
136 for &u in &in_adj[v] {
137 #[allow(clippy::cast_precision_loss)]
138 let denom = out_deg[u as usize] as f64;
139 if denom > 0.0 {
140 incoming += pr[u as usize] / denom;
141 }
142 }
143 pr_new[v] = teleport + dangling_share + DEFAULT_DAMPING * incoming;
144 }
145
146 let mut diff: f64 = 0.0;
148 for v in 0..n_us {
149 diff += (pr_new[v] - pr[v]).abs();
150 }
151 std::mem::swap(&mut pr, &mut pr_new);
152 if diff < DEFAULT_EPS {
153 break;
154 }
155 }
156
157 Ok(pr)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn close(actual: &[f64], expected: &[f64], tol: f64) {
165 assert_eq!(actual.len(), expected.len(), "length mismatch");
166 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
167 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
168 }
169 }
170
171 #[test]
172 fn empty_graph_yields_empty() {
173 let g = Graph::with_vertices(0);
174 assert!(pagerank(&g).unwrap().is_empty());
175 }
176
177 #[test]
178 fn singleton_yields_one() {
179 let g = Graph::with_vertices(1);
180 assert_eq!(pagerank(&g).unwrap(), vec![1.0]);
181 }
182
183 #[test]
184 fn isolated_vertices_uniform() {
185 let g = Graph::with_vertices(4);
188 let pr = pagerank(&g).unwrap();
189 close(&pr, &[0.25, 0.25, 0.25, 0.25], 1e-9);
190 }
191
192 #[test]
193 fn triangle_uniform_one_third() {
194 let mut g = Graph::with_vertices(3);
195 g.add_edge(0, 1).unwrap();
196 g.add_edge(1, 2).unwrap();
197 g.add_edge(2, 0).unwrap();
198 let pr = pagerank(&g).unwrap();
199 close(&pr, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 1e-9);
200 }
201
202 #[test]
203 fn directed_4cycle_uniform_quarter() {
204 let mut g = Graph::new(4, true).unwrap();
205 for i in 0..4u32 {
206 g.add_edge(i, (i + 1) % 4).unwrap();
207 }
208 let pr = pagerank(&g).unwrap();
209 close(&pr, &[0.25, 0.25, 0.25, 0.25], 1e-9);
210 }
211
212 #[test]
213 fn pagerank_sums_to_one() {
214 let mut g = Graph::with_vertices(4);
216 g.add_edge(0, 1).unwrap();
217 g.add_edge(0, 2).unwrap();
218 g.add_edge(1, 2).unwrap();
219 g.add_edge(1, 3).unwrap();
220 g.add_edge(2, 3).unwrap();
221 let pr = pagerank(&g).unwrap();
222 let total: f64 = pr.iter().sum();
223 assert!((total - 1.0).abs() < 1e-9, "sum {total} != 1.0");
224 }
225
226 #[test]
227 fn star_centre_has_higher_pagerank_than_leaves() {
228 let mut g = Graph::with_vertices(4);
229 for v in 1..4 {
230 g.add_edge(0, v).unwrap();
231 }
232 let pr = pagerank(&g).unwrap();
233 for &leaf in &pr[1..4] {
235 assert!(pr[0] > leaf, "centre {} not > leaf {}", pr[0], leaf);
236 }
237 close(&pr[1..4], &[pr[1], pr[1], pr[1]], 1e-9);
239 }
240
241 #[test]
242 fn pagerank_dangling_node_distributes() {
243 let mut g = Graph::new(2, true).unwrap();
246 g.add_edge(0, 1).unwrap();
247 let pr = pagerank(&g).unwrap();
248 let total: f64 = pr.iter().sum();
249 assert!((total - 1.0).abs() < 1e-9);
250 assert!(pr[1] > pr[0]);
254 }
255}