Skip to content

Commit

Permalink
Degree centrality implementation (#1306)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
Gohlub and IvanIsCoding authored Nov 18, 2024
1 parent 44d9fb0 commit 2cf35c0
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ retworkx/*pyd
*.jpg
**/*.so
retworkx-core/Cargo.lock
**/.DS_Store
68 changes: 68 additions & 0 deletions rustworkx-core/src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,74 @@ fn accumulate_edges<G>(
}
}
}
/// Compute the degree centrality of all nodes in a graph.
///
/// For undirected graphs, this calculates the normalized degree for each node.
/// For directed graphs, this calculates the normalized out-degree for each node.
///
/// Arguments:
///
/// * `graph` - The graph object to calculate degree centrality for
///
/// # Example
/// ```rust
/// use rustworkx_core::petgraph::graph::{UnGraph, DiGraph};
/// use rustworkx_core::centrality::degree_centrality;
///
/// // Undirected graph example
/// let graph = UnGraph::<i32, ()>::from_edges(&[
/// (0, 1), (1, 2), (2, 3), (3, 0)
/// ]);
/// let centrality = degree_centrality(&graph, None);
///
/// // Directed graph example
/// let digraph = DiGraph::<i32, ()>::from_edges(&[
/// (0, 1), (1, 2), (2, 3), (3, 0), (0, 2), (1, 3)
/// ]);
/// let centrality = degree_centrality(&digraph, None);
/// ```
pub fn degree_centrality<G>(graph: G, direction: Option<petgraph::Direction>) -> Vec<f64>
where
G: NodeIndexable
+ IntoNodeIdentifiers
+ IntoNeighbors
+ IntoNeighborsDirected
+ NodeCount
+ GraphProp,
G::NodeId: Eq + Hash,
{
let node_count = graph.node_count() as f64;
let mut centrality = vec![0.0; graph.node_bound()];

for node in graph.node_identifiers() {
let (degree, normalization) = match (graph.is_directed(), direction) {
(true, None) => {
let out_degree = graph
.neighbors_directed(node, petgraph::Direction::Outgoing)
.count() as f64;
let in_degree = graph
.neighbors_directed(node, petgraph::Direction::Incoming)
.count() as f64;
let total = in_degree + out_degree;
// Use 2(n-1) normalization only if this is a complete graph
let norm = if total == 2.0 * (node_count - 1.0) {
2.0 * (node_count - 1.0)
} else {
node_count - 1.0
};
(total, norm)
}
(true, Some(dir)) => (
graph.neighbors_directed(node, dir).count() as f64,
node_count - 1.0,
),
(false, _) => (graph.neighbors(node).count() as f64, node_count - 1.0),
};
centrality[graph.to_index(node)] = degree / normalization;
}

centrality
}

struct ShortestPathData<G>
where
Expand Down
14 changes: 14 additions & 0 deletions rustworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,20 @@ def closeness_centrality(graph, wf_improved=True):
raise TypeError("Invalid input type %s for graph" % type(graph))


@_rustworkx_dispatch
def degree_centrality(graph):
r"""Compute the degree centrality of each node in a graph object.
:param graph: The input graph. Can either be a
:class:`~rustworkx.PyGraph` or :class:`~rustworkx.PyDiGraph`.
:returns: a read-only dict-like object whose keys are edges and values are the
degree centrality score for each node.
:rtype: CentralityMapping
"""
raise TypeError("Invalid input type %s for graph" % type(graph))


@_rustworkx_dispatch
def edge_betweenness_centrality(graph, normalized=True, parallel_threshold=50):
r"""Compute the edge betweenness centrality of all edges in a graph.
Expand Down
7 changes: 7 additions & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ from .rustworkx import digraph_closeness_centrality as digraph_closeness_central
from .rustworkx import graph_closeness_centrality as graph_closeness_centrality
from .rustworkx import digraph_katz_centrality as digraph_katz_centrality
from .rustworkx import graph_katz_centrality as graph_katz_centrality
from .rustworkx import digraph_degree_centrality as digraph_degree_centrality
from .rustworkx import graph_degree_centrality as graph_degree_centrality
from .rustworkx import in_degree_centrality as in_degree_centrality
from .rustworkx import out_degree_centrality as out_degree_centrality
from .rustworkx import graph_greedy_color as graph_greedy_color
from .rustworkx import graph_greedy_edge_color as graph_greedy_edge_color
from .rustworkx import graph_is_bipartite as graph_is_bipartite
Expand Down Expand Up @@ -484,6 +488,9 @@ def betweenness_centrality(
def closeness_centrality(
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T], wf_improved: bool = ...
) -> CentralityMapping: ...
def degree_centrality(
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T],
) -> CentralityMapping: ...
def edge_betweenness_centrality(
graph: PyGraph[_S, _T] | PyDiGraph[_S, _T],
normalized: bool = ...,
Expand Down
16 changes: 16 additions & 0 deletions rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,22 @@ def graph_closeness_centrality(
graph: PyGraph[_S, _T],
wf_improved: bool = ...,
) -> CentralityMapping: ...
def digraph_degree_centrality(
graph: PyDiGraph[_S, _T],
/,
) -> CentralityMapping: ...
def in_degree_centrality(
graph: PyDiGraph[_S, _T],
/,
) -> CentralityMapping: ...
def out_degree_centrality(
graph: PyDiGraph[_S, _T],
/,
) -> CentralityMapping: ...
def graph_degree_centrality(
graph: PyGraph[_S, _T],
/,
) -> CentralityMapping: ...
def digraph_katz_centrality(
graph: PyDiGraph[_S, _T],
/,
Expand Down
96 changes: 96 additions & 0 deletions src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,102 @@ pub fn digraph_betweenness_centrality(
}
}

/// Compute the degree centrality for nodes in a PyGraph.
///
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
///
/// :param PyGraph graph: The input graph
///
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
/// centrality score for each node.
/// :rtype: CentralityMapping
#[pyfunction(signature = (graph,))]
#[pyo3(text_signature = "(graph, /,)")]
pub fn graph_degree_centrality(graph: &graph::PyGraph) -> PyResult<CentralityMapping> {
let centrality = centrality::degree_centrality(&graph.graph, None);

Ok(CentralityMapping {
centralities: graph
.graph
.node_indices()
.map(|i| (i.index(), centrality[i.index()]))
.collect(),
})
}

/// Compute the degree centrality for nodes in a PyDiGraph.
///
/// Degree centrality assigns an importance score based simply on the number of edges held by each node.
/// This function computes the TOTAL (in + out) degree centrality.
///
/// :param PyDiGraph graph: The input graph
///
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
/// centrality score for each node.
/// :rtype: CentralityMapping
#[pyfunction(signature = (graph,))]
#[pyo3(text_signature = "(graph, /,)")]
pub fn digraph_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
let centrality = centrality::degree_centrality(&graph.graph, None);

Ok(CentralityMapping {
centralities: graph
.graph
.node_indices()
.map(|i| (i.index(), centrality[i.index()]))
.collect(),
})
}
/// Compute the in-degree centrality for nodes in a PyDiGraph.
///
/// In-degree centrality assigns an importance score based on the number of incoming edges
/// to each node.
///
/// :param PyDiGraph graph: The input graph
///
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
/// centrality score for each node.
/// :rtype: CentralityMapping
#[pyfunction(signature = (graph,))]
#[pyo3(text_signature = "(graph, /)")]
pub fn in_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
let centrality =
centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Incoming));

Ok(CentralityMapping {
centralities: graph
.graph
.node_indices()
.map(|i| (i.index(), centrality[i.index()]))
.collect(),
})
}

/// Compute the out-degree centrality for nodes in a PyDiGraph.
///
/// Out-degree centrality assigns an importance score based on the number of outgoing edges
/// from each node.
///
/// :param PyDiGraph graph: The input graph
///
/// :returns: a read-only dict-like object whose keys are the node indices and values are the
/// centrality score for each node.
/// :rtype: CentralityMapping
#[pyfunction(signature = (graph,))]
#[pyo3(text_signature = "(graph, /,)")]
pub fn out_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
let centrality =
centrality::degree_centrality(&graph.graph, Some(petgraph::Direction::Outgoing));

Ok(CentralityMapping {
centralities: graph
.graph
.node_indices()
.map(|i| (i.index(), centrality[i.index()]))
.collect(),
})
}

/// Compute the closeness centrality of each node in a :class:`~.PyGraph` object.
///
/// The closeness centrality of a node :math:`u` is defined as the
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,10 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(digraph_eigenvector_centrality))?;
m.add_wrapped(wrap_pyfunction!(graph_katz_centrality))?;
m.add_wrapped(wrap_pyfunction!(digraph_katz_centrality))?;
m.add_wrapped(wrap_pyfunction!(graph_degree_centrality))?;
m.add_wrapped(wrap_pyfunction!(digraph_degree_centrality))?;
m.add_wrapped(wrap_pyfunction!(in_degree_centrality))?;
m.add_wrapped(wrap_pyfunction!(out_degree_centrality))?;
m.add_wrapped(wrap_pyfunction!(graph_astar_shortest_path))?;
m.add_wrapped(wrap_pyfunction!(digraph_astar_shortest_path))?;
m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?;
Expand Down
95 changes: 95 additions & 0 deletions tests/digraph/test_centrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,98 @@ def test_path_graph_unnormalized(self):
expected = {0: 4.0, 1: 6.0, 2: 6.0, 3: 4.0}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])


class TestDiGraphDegreeCentrality(unittest.TestCase):
def setUp(self):
self.graph = rustworkx.PyDiGraph()
self.a = self.graph.add_node("A")
self.b = self.graph.add_node("B")
self.c = self.graph.add_node("C")
self.d = self.graph.add_node("D")
edge_list = [
(self.a, self.b, 1),
(self.b, self.c, 1),
(self.c, self.d, 1),
(self.a, self.c, 1), # Additional edge
]
self.graph.add_edges_from(edge_list)

def test_degree_centrality(self):
centrality = rustworkx.degree_centrality(self.graph)
expected = {
0: 2 / 3, # 2 total edges / 3
1: 2 / 3, # 2 total edges / 3
2: 1.0, # 3 total edges / 3
3: 1 / 3, # 1 total edge / 3
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_in_degree_centrality(self):
centrality = rustworkx.in_degree_centrality(self.graph)
expected = {
0: 0.0, # 0 incoming edges
1: 1 / 3, # 1 incoming edge
2: 2 / 3, # 2 incoming edges
3: 1 / 3, # 1 incoming edge
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_out_degree_centrality(self):
centrality = rustworkx.out_degree_centrality(self.graph)
expected = {
0: 2 / 3, # 2 outgoing edges
1: 1 / 3, # 1 outgoing edge
2: 1 / 3, # 1 outgoing edge
3: 0.0, # 0 outgoing edges
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_degree_centrality_complete_digraph(self):
graph = rustworkx.generators.directed_complete_graph(5)
centrality = rustworkx.degree_centrality(graph)
expected = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_degree_centrality_directed_path(self):
graph = rustworkx.generators.directed_path_graph(5)
centrality = rustworkx.degree_centrality(graph)
expected = {
0: 1 / 4, # 1 total edge (out only) / 4
1: 2 / 4, # 2 total edges (1 in + 1 out) / 4
2: 2 / 4, # 2 total edges (1 in + 1 out) / 4
3: 2 / 4, # 2 total edges (1 in + 1 out) / 4
4: 1 / 4, # 1 total edge (in only) / 4
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_in_degree_centrality_directed_path(self):
graph = rustworkx.generators.directed_path_graph(5)
centrality = rustworkx.in_degree_centrality(graph)
expected = {
0: 0.0, # 0 incoming edges
1: 1 / 4, # 1 incoming edge
2: 1 / 4, # 1 incoming edge
3: 1 / 4, # 1 incoming edge
4: 1 / 4, # 1 incoming edge
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])

def test_out_degree_centrality_directed_path(self):
graph = rustworkx.generators.directed_path_graph(5)
centrality = rustworkx.out_degree_centrality(graph)
expected = {
0: 1 / 4, # 1 outgoing edge
1: 1 / 4, # 1 outgoing edge
2: 1 / 4, # 1 outgoing edge
3: 1 / 4, # 1 outgoing edge
4: 0.0, # 0 outgoing edges
}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])
Loading

0 comments on commit 2cf35c0

Please sign in to comment.