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

Merged
merged 10 commits into from
Nov 18, 2024
Merged
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
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