Skip to content

Commit 2cf35c0

Browse files
GohlubIvanIsCoding
andauthored
Degree centrality implementation (#1306)
* Add degree centrality Co-authored-by: Gohlub [email protected] Co-authored-by: onsali [email protected] * Rename method in rustworkx-core * Add universal functions * Fix typo * Fix method names in tests * Add type stubs * Remove unnecessary diff * Handle graphs with removed edges * Fix rustdoc tests * Fix type stubs --------- Co-authored-by: Ivan Carvalho <[email protected]>
1 parent 44d9fb0 commit 2cf35c0

File tree

9 files changed

+364
-0
lines changed

9 files changed

+364
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ retworkx/*pyd
2222
*.jpg
2323
**/*.so
2424
retworkx-core/Cargo.lock
25+
**/.DS_Store

rustworkx-core/src/centrality.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,74 @@ fn accumulate_edges<G>(
335335
}
336336
}
337337
}
338+
/// Compute the degree centrality of all nodes in a graph.
339+
///
340+
/// For undirected graphs, this calculates the normalized degree for each node.
341+
/// For directed graphs, this calculates the normalized out-degree for each node.
342+
///
343+
/// Arguments:
344+
///
345+
/// * `graph` - The graph object to calculate degree centrality for
346+
///
347+
/// # Example
348+
/// ```rust
349+
/// use rustworkx_core::petgraph::graph::{UnGraph, DiGraph};
350+
/// use rustworkx_core::centrality::degree_centrality;
351+
///
352+
/// // Undirected graph example
353+
/// let graph = UnGraph::<i32, ()>::from_edges(&[
354+
/// (0, 1), (1, 2), (2, 3), (3, 0)
355+
/// ]);
356+
/// let centrality = degree_centrality(&graph, None);
357+
///
358+
/// // Directed graph example
359+
/// let digraph = DiGraph::<i32, ()>::from_edges(&[
360+
/// (0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)
361+
/// ]);
362+
/// let centrality = degree_centrality(&digraph, None);
363+
/// ```
364+
pub fn degree_centrality<G>(graph: G, direction: Option<petgraph::Direction>) -> Vec<f64>
365+
where
366+
G: NodeIndexable
367+
+ IntoNodeIdentifiers
368+
+ IntoNeighbors
369+
+ IntoNeighborsDirected
370+
+ NodeCount
371+
+ GraphProp,
372+
G::NodeId: Eq + Hash,
373+
{
374+
let node_count = graph.node_count() as f64;
375+
let mut centrality = vec![0.0; graph.node_bound()];
376+
377+
for node in graph.node_identifiers() {
378+
let (degree, normalization) = match (graph.is_directed(), direction) {
379+
(true, None) => {
380+
let out_degree = graph
381+
.neighbors_directed(node, petgraph::Direction::Outgoing)
382+
.count() as f64;
383+
let in_degree = graph
384+
.neighbors_directed(node, petgraph::Direction::Incoming)
385+
.count() as f64;
386+
let total = in_degree + out_degree;
387+
// Use 2(n-1) normalization only if this is a complete graph
388+
let norm = if total == 2.0 * (node_count - 1.0) {
389+
2.0 * (node_count - 1.0)
390+
} else {
391+
node_count - 1.0
392+
};
393+
(total, norm)
394+
}
395+
(true, Some(dir)) => (
396+
graph.neighbors_directed(node, dir).count() as f64,
397+
node_count - 1.0,
398+
),
399+
(false, _) => (graph.neighbors(node).count() as f64, node_count - 1.0),
400+
};
401+
centrality[graph.to_index(node)] = degree / normalization;
402+
}
403+
404+
centrality
405+
}
338406

339407
struct ShortestPathData<G>
340408
where

rustworkx/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,6 +1184,20 @@ def closeness_centrality(graph, wf_improved=True):
11841184
raise TypeError("Invalid input type %s for graph" % type(graph))
11851185

11861186

1187+
@_rustworkx_dispatch
1188+
def degree_centrality(graph):
1189+
r"""Compute the degree centrality of each node in a graph object.
1190+
1191+
:param graph: The input graph. Can either be a
1192+
:class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
1193+
1194+
:returns: a read-only dict-like object whose keys are edges and values are the
1195+
degree centrality score for each node.
1196+
:rtype: CentralityMapping
1197+
"""
1198+
raise TypeError("Invalid input type %s for graph" % type(graph))
1199+
1200+
11871201
@_rustworkx_dispatch
11881202
def edge_betweenness_centrality(graph, normalized=True, parallel_threshold=50):
11891203
r"""Compute the edge betweenness centrality of all edges in a graph.

rustworkx/__init__.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ from .rustworkx import digraph_closeness_centrality as digraph_closeness_central
5050
from .rustworkx import graph_closeness_centrality as graph_closeness_centrality
5151
from .rustworkx import digraph_katz_centrality as digraph_katz_centrality
5252
from .rustworkx import graph_katz_centrality as graph_katz_centrality
53+
from .rustworkx import digraph_degree_centrality as digraph_degree_centrality
54+
from .rustworkx import graph_degree_centrality as graph_degree_centrality
55+
from .rustworkx import in_degree_centrality as in_degree_centrality
56+
from .rustworkx import out_degree_centrality as out_degree_centrality
5357
from .rustworkx import graph_greedy_color as graph_greedy_color
5458
from .rustworkx import graph_greedy_edge_color as graph_greedy_edge_color
5559
from .rustworkx import graph_is_bipartite as graph_is_bipartite
@@ -484,6 +488,9 @@ def betweenness_centrality(
484488
def closeness_centrality(
485489
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], wf_improved: bool = ...
486490
) -> CentralityMapping: ...
491+
def degree_centrality(
492+
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T],
493+
) -> CentralityMapping: ...
487494
def edge_betweenness_centrality(
488495
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T],
489496
normalized: bool = ...,

rustworkx/rustworkx.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,22 @@ def graph_closeness_centrality(
124124
graph: PyGraph[_S, _T],
125125
wf_improved: bool = ...,
126126
) -> CentralityMapping: ...
127+
def digraph_degree_centrality(
128+
graph: PyDiGraph[_S, _T],
129+
/,
130+
) -> CentralityMapping: ...
131+
def in_degree_centrality(
132+
graph: PyDiGraph[_S, _T],
133+
/,
134+
) -> CentralityMapping: ...
135+
def out_degree_centrality(
136+
graph: PyDiGraph[_S, _T],
137+
/,
138+
) -> CentralityMapping: ...
139+
def graph_degree_centrality(
140+
graph: PyGraph[_S, _T],
141+
/,
142+
) -> CentralityMapping: ...
127143
def digraph_katz_centrality(
128144
graph: PyDiGraph[_S, _T],
129145
/,

src/centrality.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,102 @@ pub fn digraph_betweenness_centrality(
165165
}
166166
}
167167

168+
/// Compute the degree centrality for nodes in a PyGraph.
169+
///
170+
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
171+
///
172+
/// :param PyGraph graph: The input graph
173+
///
174+
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
175+
/// centrality score for each node.
176+
/// :rtype: CentralityMapping
177+
#[pyfunction(signature = (graph,))]
178+
#[pyo3(text_signature = "(graph, /,)")]
179+
pub fn graph_degree_centrality(graph: &graph::PyGraph) -> PyResult<CentralityMapping> {
180+
let centrality = centrality::degree_centrality(&graph.graph, None);
181+
182+
Ok(CentralityMapping {
183+
centralities: graph
184+
.graph
185+
.node_indices()
186+
.map(|i| (i.index(), centrality[i.index()]))
187+
.collect(),
188+
})
189+
}
190+
191+
/// Compute the degree centrality for nodes in a PyDiGraph.
192+
///
193+
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
194+
/// This function computes the TOTAL (in + out) degree centrality.
195+
///
196+
/// :param PyDiGraph graph: The input graph
197+
///
198+
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
199+
/// centrality score for each node.
200+
/// :rtype: CentralityMapping
201+
#[pyfunction(signature = (graph,))]
202+
#[pyo3(text_signature = "(graph, /,)")]
203+
pub fn digraph_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
204+
let centrality = centrality::degree_centrality(&graph.graph, None);
205+
206+
Ok(CentralityMapping {
207+
centralities: graph
208+
.graph
209+
.node_indices()
210+
.map(|i| (i.index(), centrality[i.index()]))
211+
.collect(),
212+
})
213+
}
214+
/// Compute the in-degree centrality for nodes in a PyDiGraph.
215+
///
216+
/// In-degree centrality assigns an importance score based on the number of incoming edges
217+
/// to each node.
218+
///
219+
/// :param PyDiGraph graph: The input graph
220+
///
221+
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
222+
/// centrality score for each node.
223+
/// :rtype: CentralityMapping
224+
#[pyfunction(signature = (graph,))]
225+
#[pyo3(text_signature = "(graph, /)")]
226+
pub fn in_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
227+
let centrality =
228+
centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Incoming));
229+
230+
Ok(CentralityMapping {
231+
centralities: graph
232+
.graph
233+
.node_indices()
234+
.map(|i| (i.index(), centrality[i.index()]))
235+
.collect(),
236+
})
237+
}
238+
239+
/// Compute the out-degree centrality for nodes in a PyDiGraph.
240+
///
241+
/// Out-degree centrality assigns an importance score based on the number of outgoing edges
242+
/// from each node.
243+
///
244+
/// :param PyDiGraph graph: The input graph
245+
///
246+
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
247+
/// centrality score for each node.
248+
/// :rtype: CentralityMapping
249+
#[pyfunction(signature = (graph,))]
250+
#[pyo3(text_signature = "(graph, /,)")]
251+
pub fn out_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
252+
let centrality =
253+
centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Outgoing));
254+
255+
Ok(CentralityMapping {
256+
centralities: graph
257+
.graph
258+
.node_indices()
259+
.map(|i| (i.index(), centrality[i.index()]))
260+
.collect(),
261+
})
262+
}
263+
168264
/// Compute the closeness centrality of each node in a :class:`~.PyGraph` object.
169265
///
170266
/// The closeness centrality of a node :math:`u` is defined as the

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,10 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
533533
m.add_wrapped(wrap_pyfunction!(digraph_eigenvector_centrality))?;
534534
m.add_wrapped(wrap_pyfunction!(graph_katz_centrality))?;
535535
m.add_wrapped(wrap_pyfunction!(digraph_katz_centrality))?;
536+
m.add_wrapped(wrap_pyfunction!(graph_degree_centrality))?;
537+
m.add_wrapped(wrap_pyfunction!(digraph_degree_centrality))?;
538+
m.add_wrapped(wrap_pyfunction!(in_degree_centrality))?;
539+
m.add_wrapped(wrap_pyfunction!(out_degree_centrality))?;
536540
m.add_wrapped(wrap_pyfunction!(graph_astar_shortest_path))?;
537541
m.add_wrapped(wrap_pyfunction!(digraph_astar_shortest_path))?;
538542
m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?;

tests/digraph/test_centrality.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,3 +241,98 @@ def test_path_graph_unnormalized(self):
241241
expected = {0: 4.0, 1: 6.0, 2: 6.0, 3: 4.0}
242242
for k, v in centrality.items():
243243
self.assertAlmostEqual(v, expected[k])
244+
245+
246+
class TestDiGraphDegreeCentrality(unittest.TestCase):
247+
def setUp(self):
248+
self.graph = rustworkx.PyDiGraph()
249+
self.a = self.graph.add_node("A")
250+
self.b = self.graph.add_node("B")
251+
self.c = self.graph.add_node("C")
252+
self.d = self.graph.add_node("D")
253+
edge_list = [
254+
(self.a, self.b, 1),
255+
(self.b, self.c, 1),
256+
(self.c, self.d, 1),
257+
(self.a, self.c, 1), # Additional edge
258+
]
259+
self.graph.add_edges_from(edge_list)
260+
261+
def test_degree_centrality(self):
262+
centrality = rustworkx.degree_centrality(self.graph)
263+
expected = {
264+
0: 2 / 3, # 2 total edges / 3
265+
1: 2 / 3, # 2 total edges / 3
266+
2: 1.0, # 3 total edges / 3
267+
3: 1 / 3, # 1 total edge / 3
268+
}
269+
for k, v in centrality.items():
270+
self.assertAlmostEqual(v, expected[k])
271+
272+
def test_in_degree_centrality(self):
273+
centrality = rustworkx.in_degree_centrality(self.graph)
274+
expected = {
275+
0: 0.0, # 0 incoming edges
276+
1: 1 / 3, # 1 incoming edge
277+
2: 2 / 3, # 2 incoming edges
278+
3: 1 / 3, # 1 incoming edge
279+
}
280+
for k, v in centrality.items():
281+
self.assertAlmostEqual(v, expected[k])
282+
283+
def test_out_degree_centrality(self):
284+
centrality = rustworkx.out_degree_centrality(self.graph)
285+
expected = {
286+
0: 2 / 3, # 2 outgoing edges
287+
1: 1 / 3, # 1 outgoing edge
288+
2: 1 / 3, # 1 outgoing edge
289+
3: 0.0, # 0 outgoing edges
290+
}
291+
for k, v in centrality.items():
292+
self.assertAlmostEqual(v, expected[k])
293+
294+
def test_degree_centrality_complete_digraph(self):
295+
graph = rustworkx.generators.directed_complete_graph(5)
296+
centrality = rustworkx.degree_centrality(graph)
297+
expected = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
298+
for k, v in centrality.items():
299+
self.assertAlmostEqual(v, expected[k])
300+
301+
def test_degree_centrality_directed_path(self):
302+
graph = rustworkx.generators.directed_path_graph(5)
303+
centrality = rustworkx.degree_centrality(graph)
304+
expected = {
305+
0: 1 / 4, # 1 total edge (out only) / 4
306+
1: 2 / 4, # 2 total edges (1 in + 1 out) / 4
307+
2: 2 / 4, # 2 total edges (1 in + 1 out) / 4
308+
3: 2 / 4, # 2 total edges (1 in + 1 out) / 4
309+
4: 1 / 4, # 1 total edge (in only) / 4
310+
}
311+
for k, v in centrality.items():
312+
self.assertAlmostEqual(v, expected[k])
313+
314+
def test_in_degree_centrality_directed_path(self):
315+
graph = rustworkx.generators.directed_path_graph(5)
316+
centrality = rustworkx.in_degree_centrality(graph)
317+
expected = {
318+
0: 0.0, # 0 incoming edges
319+
1: 1 / 4, # 1 incoming edge
320+
2: 1 / 4, # 1 incoming edge
321+
3: 1 / 4, # 1 incoming edge
322+
4: 1 / 4, # 1 incoming edge
323+
}
324+
for k, v in centrality.items():
325+
self.assertAlmostEqual(v, expected[k])
326+
327+
def test_out_degree_centrality_directed_path(self):
328+
graph = rustworkx.generators.directed_path_graph(5)
329+
centrality = rustworkx.out_degree_centrality(graph)
330+
expected = {
331+
0: 1 / 4, # 1 outgoing edge
332+
1: 1 / 4, # 1 outgoing edge
333+
2: 1 / 4, # 1 outgoing edge
334+
3: 1 / 4, # 1 outgoing edge
335+
4: 0.0, # 0 outgoing edges
336+
}
337+
for k, v in centrality.items():
338+
self.assertAlmostEqual(v, expected[k])

0 commit comments

Comments
 (0)