Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Degree centrality implementation #1306

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
65 changes: 65 additions & 0 deletions rustworkx-core/src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use petgraph::visit::{
Reversed,
Visitable,
};
use petgraph::Direction;
use rayon_cond::CondIterator;

/// Compute the betweenness centrality of all nodes in a graph.
Expand Down Expand Up @@ -335,6 +336,70 @@ fn accumulate_edges<G>(
}
}
}
/// Compute the degree centrality of all nodes in a graph.
///
/// The algorithm used in this function is based on:
///
/// Degree Centrality, Betweenness Centrality, and Closeness Centrality in Social Network
/// Atlantis Press - Junlong Zhang, Yu Luo
/// https://doi.org/10.2991/msam-17.2017.68
///
/// Example usage of `degree_centrality` function
/// ```
/// use rustworkx_core::petgraph::graph::UnGraph;
/// use rustworkx_core::centrality::degree_centrality;
///
/// // Create an undirected graph with 4 nodes and 4 edges
/// let graph = UnGraph::<i32, ()>::from_edges(&[
/// (0, 1), (1, 2), (2, 3), (3, 0)
/// ]);
///
/// // Calculate degree centrality
/// let centrality = degree_centrality(&graph);
///
/// // Each node has 2 connections out of 3 possible (n-1 where n=4)
/// for value in centrality {
/// assert!((value - 2.0/3.0).abs() < 1e-10);
/// }
/// ```

pub fn graph_degree_centrality<G>(graph: G) -> Vec<f64>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For rustworkx-core, use a generic method that takes both kind of graphs.

pub fn eigenvector_centrality<G, F, E>(
is a good example

where
G: IntoNodeIdentifiers + NodeCount + IntoNeighbors + NodeIndexable,
{
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 = graph.neighbors(node).count() as f64;
IvanIsCoding marked this conversation as resolved.
Show resolved Hide resolved
centrality[graph.to_index(node)] = degree / (node_count - 1.0);
}

centrality
}

pub fn digraph_degree_centrality<G>(graph: G, direction: Option<Direction>) -> Vec<f64>
where
G: IntoNodeIdentifiers + NodeCount + IntoNeighborsDirected + NodeIndexable,
{
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 = match direction {
Some(Direction::Incoming) => {
graph.neighbors_directed(node, Direction::Incoming).count() as f64
}
Some(Direction::Outgoing) => {
graph.neighbors_directed(node, Direction::Outgoing).count() as f64
}
_ => graph.neighbors(node).count() as f64,
};
centrality[graph.to_index(node)] = degree / (node_count - 1.0);
}

centrality
}

struct ShortestPathData<G>
where
Expand Down
2 changes: 2 additions & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ 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 graph_degree_centrality as graph_degree_centrality
from .rustworkx import digraph_degree_centrality as digraph_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
37 changes: 37 additions & 0 deletions src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,43 @@ 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]
#[pyo3(text_signature = "(graph, /)")]
pub fn graph_degree_centrality(graph: &graph::PyGraph) -> PyResult<CentralityMapping> {
let centrality = centrality::graph_degree_centrality(&graph.graph);

Ok(CentralityMapping {
centralities: centrality
.into_iter()
.enumerate()
.map(|(i, v)| (i, v))
.collect(),
})
}

#[pyfunction]
#[pyo3(text_signature = "(graph, /)")]
pub fn digraph_degree_centrality(graph: &digraph::PyDiGraph) -> PyResult<CentralityMapping> {
let centrality = centrality::digraph_degree_centrality(&graph.graph, None);

Ok(CentralityMapping {
centralities: centrality
.into_iter()
.enumerate()
.map(|(i, v)| (i, v))
.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
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,8 @@ 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!(graph_astar_shortest_path))?;
m.add_wrapped(wrap_pyfunction!(digraph_astar_shortest_path))?;
m.add_wrapped(wrap_pyfunction!(graph_greedy_color))?;
Expand Down
87 changes: 87 additions & 0 deletions tests/graph/test_centrality.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,90 @@ def test_custom_graph_unnormalized(self):
expected = {0: 9, 1: 9, 2: 12, 3: 15, 4: 11, 5: 14, 6: 10, 7: 13, 8: 9, 9: 9}
for k, v in centrality.items():
self.assertAlmostEqual(v, expected[k])


class TestGraphDegreeCentrality(unittest.TestCase):
def setUp(self):
self.graph = rustworkx.PyGraph()
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.graph.add_edges_from(edge_list)

def test_degree_centrality(self):
centrality = rustworkx.graph_degree_centrality(self.graph)
expected = {
0: 1 / 3, # Node A has 1 edge, normalized by (n-1) = 3
1: 2 / 3, # Node B has 2 edges
2: 2 / 3, # Node C has 2 edges
3: 1 / 3, # Node D has 1 edge
}
self.assertEqual(expected, centrality)

def test_degree_centrality_complete_graph(self):
graph = rustworkx.generators.complete_graph(5)
centrality = rustworkx.graph_degree_centrality(graph)
expected = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
self.assertEqual(expected, centrality)

def test_degree_centrality_star_graph(self):
graph = rustworkx.generators.star_graph(5)
centrality = rustworkx.graph_degree_centrality(graph)
expected = {0: 1.0, 1: 0.25, 2: 0.25, 3: 0.25, 4: 0.25}
self.assertEqual(expected, centrality)

def test_degree_centrality_empty_graph(self):
graph = rustworkx.PyGraph()
centrality = rustworkx.graph_degree_centrality(graph)
expected = {}
self.assertEqual(expected, centrality)

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

def test_digraph_degree_centrality(self):
centrality = rustworkx.digraph_degree_centrality(self.digraph)
expected = {
0: 1 / 3, # Node A has 1 outgoing edge
1: 2 / 3, # Node B has 1 incoming and 1 outgoing edge
2: 2 / 3, # Node C has 1 incoming and 1 outgoing edge
3: 1 / 3, # Node D has 1 incoming edge
}
self.assertEqual(expected, centrality)

def test_digraph_degree_centrality_with_direction(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor organization detail. This should put the digraph tests in tests/digraph/test_centrality.py with the other directed graph tests

centrality_incoming = rustworkx.digraph_degree_centrality(self.digraph, direction=rustworkx.Direction.Incoming)
expected_incoming = {
0: 0.0, # Node A has 0 incoming edges
1: 1 / 3, # Node B has 1 incoming edge
2: 1 / 3, # Node C has 1 incoming edge
3: 1 / 3, # Node D has 1 incoming edge
}
self.assertEqual(expected_incoming, centrality_incoming)

centrality_outgoing = rustworkx.digraph_degree_centrality(self.digraph, direction=rustworkx.Direction.Outgoing)
expected_outgoing = {
0: 1 / 3, # Node A has 1 outgoing edge
1: 1 / 3, # Node B has 1 outgoing edge
2: 1 / 3, # Node C has 1 outgoing edge
3: 0.0, # Node D has 0 outgoing edges
}
self.assertEqual(expected_outgoing, centrality_outgoing)

Loading