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

Implementation of closeness_centrality #593

Merged
merged 11 commits into from
Mar 10, 2023
6 changes: 5 additions & 1 deletion docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ Shortest Paths
.. _centrality:

Centrality
--------------
----------

.. autosummary::
:toctree: apiref

rustworkx.betweenness_centrality
rustworkx.edge_betweenness_centrality
rustworkx.eigenvector_centrality
rustworkx.closeness_centrality

.. _traversal:

Expand Down Expand Up @@ -321,6 +323,7 @@ the functions from the explicitly typed based on the data type.
rustworkx.digraph_num_shortest_paths_unweighted
rustworkx.digraph_betweenness_centrality
rustworkx.digraph_edge_betweenness_centrality
rustworkx.digraph_closeness_centrality
rustworkx.digraph_eigenvector_centrality
rustworkx.digraph_unweighted_average_shortest_path_length
rustworkx.digraph_bfs_search
Expand Down Expand Up @@ -376,6 +379,7 @@ typed API based on the data type.
rustworkx.graph_num_shortest_paths_unweighted
rustworkx.graph_betweenness_centrality
rustworkx.graph_edge_betweenness_centrality
rustworkx.graph_closeness_centrality
rustworkx.graph_eigenvector_centrality
rustworkx.graph_unweighted_average_shortest_path_length
rustworkx.graph_bfs_search
Expand Down
42 changes: 42 additions & 0 deletions releasenotes/notes/closeness-centrality-459c5c7e35cb2e63.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
features:
- |
Added a new function, :func:`~.closeness_centrality` to compute the
closeness centrality of all nodes in a :class:`~.PyGraph` or
:class:`~.PyDiGraph` object.

The closeness centrality of a node :math:`u` is defined as the the
reciprocal of the average shortest path distance to :math:`u` over all
:math:`n-1` reachable nodes. In it's general form this can be expressed as:

.. math::

C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},

where :math:`d(v, u)` is the shortest-path distance between :math:`v` and
:math:`u`, and :math:`n` is the number of nodes that can reach :math:`u`.
For example, to visualize the closeness centrality of a graph:

.. jupyter-execute::

import matplotlib.pyplot as plt

import rustworkx as rx
from rustworkx.visualization import mpl_draw

graph = rx.generators.hexagonal_lattice_graph(4, 4)
centrality = rx.closeness_centrality(graph)
# Generate a color list
colors = []
for node in graph.node_indices():
colors.append(centrality[node])
# Generate a visualization with a colorbar
plt.rcParams['figure.figsize'] = [15, 10]
ax = plt.gca()
sm = plt.cm.ScalarMappable(norm=plt.Normalize(
vmin=min(centrality.values()),
vmax=max(centrality.values())
))
plt.colorbar(sm, ax=ax)
plt.title("Closeness Centrality of a 4 x 4 Hexagonal Lattice Graph")
mpl_draw(graph, node_color=colors, ax=ax)
100 changes: 100 additions & 0 deletions retworkx-core/tests/centrality.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

use petgraph::visit::Reversed;
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
use retworkx_core::centrality::closeness_centrality;
use retworkx_core::petgraph::graph::{DiGraph, UnGraph};

#[test]
fn test_simple() {
let g = UnGraph::<i32, ()>::from_edges(&[(1, 2), (2, 3), (3, 4), (1, 4)]);
let c = closeness_centrality(&g, true);
assert_eq!(
vec![
Some(0.0),
Some(0.5625),
Some(0.5625),
Some(0.5625),
Some(0.5625)
],
c
);
}

#[test]
fn test_wf_improved() {
let g = UnGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2), (2, 3), (4, 5), (5, 6)]);
let c = closeness_centrality(&g, true);
assert_eq!(
vec![
Some(1.0 / 4.0),
Some(3.0 / 8.0),
Some(3.0 / 8.0),
Some(1.0 / 4.0),
Some(2.0 / 9.0),
Some(1.0 / 3.0),
Some(2.0 / 9.0)
],
c
);
let cwf = closeness_centrality(&g, false);
assert_eq!(
vec![
Some(1.0 / 2.0),
Some(3.0 / 4.0),
Some(3.0 / 4.0),
Some(1.0 / 2.0),
Some(2.0 / 3.0),
Some(1.0),
Some(2.0 / 3.0)
],
cwf
);
}

#[test]
fn test_digraph() {
let g = DiGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let c = closeness_centrality(&g, true);
assert_eq!(vec![Some(0.), Some(1. / 2.), Some(2. / 3.)], c);

let cr = closeness_centrality(Reversed(&g), true);
assert_eq!(vec![Some(2. / 3.), Some(1. / 2.), Some(0.)], cr);
}

#[test]
fn test_k5() {
let g = UnGraph::<i32, ()>::from_edges(&[
(0, 1),
(0, 2),
(0, 3),
(0, 4),
(1, 2),
(1, 3),
(1, 4),
(2, 3),
(2, 4),
(3, 4),
]);
let c = closeness_centrality(&g, true);
assert_eq!(
vec![Some(1.0), Some(1.0), Some(1.0), Some(1.0), Some(1.0)],
c
);
}

#[test]
fn test_path() {
let g = UnGraph::<i32, ()>::from_edges(&[(0, 1), (1, 2)]);
let c = closeness_centrality(&g, true);
assert_eq!(vec![Some(2.0 / 3.0), Some(1.0), Some(2.0 / 3.0)], c);
}
82 changes: 81 additions & 1 deletion rustworkx-core/src/centrality.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@ use std::hash::Hash;
use std::sync::RwLock;

use hashbrown::HashMap;
use petgraph::algo::dijkstra;
use petgraph::visit::{
EdgeCount,
EdgeIndexable,
EdgeRef,
GraphBase,
GraphProp, // allows is_directed
IntoEdges,
IntoEdgesDirected,
IntoNeighbors,
IntoNeighborsDirected,
IntoNodeIdentifiers,
NodeCount,
NodeIndexable,
Reversed,
Visitable,
};
use rayon::prelude::*;
use rayon_cond::CondIterator;
Expand Down Expand Up @@ -190,7 +194,7 @@ where
/// Ulrik Brandes: On Variants of Shortest-Path Betweenness
/// Centrality and their Generic Computation.
/// Social Networks 30(2):136-145, 2008.
/// https://doi.org/10.1016/j.socnet.2007.11.001.
/// <https://doi.org/10.1016/j.socnet.2007.11.001>.
///
/// This function is multithreaded and will run in parallel if the number
/// of nodes in the graph is above the value of ``parallel_threshold``. If the
Expand Down Expand Up @@ -828,3 +832,79 @@ mod test_eigenvector_centrality {
}
}
}

/// Compute the closeness centrality of each node in the graph.
///
/// The closeness centrality of a node `u` is the reciprocal of the average
/// shortest path distance to `u` over all `n-1` reachable nodes.
///
/// In the case of a graphs with more than one connected component there is
/// an alternative improved formula that calculates the closeness centrality
/// as "a ratio of the fraction of actors in the group who are reachable, to
/// the average distance" [^WF]. You can enable this by setting `wf_improved` to `true`.
///
/// [^WF] Wasserman, S., & Faust, K. (1994). Social Network Analysis:
/// Methods and Applications (Structural Analysis in the Social Sciences).
/// Cambridge: Cambridge University Press. doi:10.1017/CBO9780511815478
///
/// Arguments:
///
/// * `graph` - The graph object to run the algorithm on
/// * `wf_improved` - If `true`, scale by the fraction of nodes reachable.
///
/// # Example
/// ```rust
/// use retworkx_core::petgraph;
/// use retworkx_core::centrality::closeness_centrality;
///
/// // Calculate the closeness centrality of Graph
/// let g = petgraph::graph::UnGraph::<i32, ()>::from_edges(&[
/// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4)
/// ]);
/// let output = closeness_centrality(&g, true);
/// assert_eq!(
/// vec![Some(1./2.), Some(2./3.), Some(4./7.), Some(2./3.), Some(4./5.)],
/// output
/// );
///
/// // Calculate the closeness centrality of DiGraph
/// let dg = petgraph::graph::DiGraph::<i32, ()>::from_edges(&[
/// (0, 4), (1, 2), (2, 3), (3, 4), (1, 4)
/// ]);
/// let output = closeness_centrality(&dg, true);
/// assert_eq!(
/// vec![Some(0.), Some(0.), Some(1./4.), Some(1./3.), Some(4./5.)],
/// output
/// );
/// ```
pub fn closeness_centrality<G>(graph: G, wf_improved: bool) -> Vec<Option<f64>>
where
G: NodeIndexable
+ IntoNodeIdentifiers
+ GraphBase
+ IntoEdges
+ Visitable
+ NodeCount
+ IntoEdgesDirected,
G::NodeId: std::hash::Hash + Eq,
{
let max_index = graph.node_bound();
let mut closeness: Vec<Option<f64>> = vec![None; max_index];
for node_s in graph.node_identifiers() {
let is = graph.to_index(node_s);
let map = dijkstra(Reversed(&graph), node_s, None, |_| 1);
let reachable_nodes_count = map.len();
let dists_sum: usize = map.into_values().sum();
if reachable_nodes_count == 1 {
closeness[is] = Some(0.0);
continue;
}
closeness[is] = Some((reachable_nodes_count - 1) as f64 / dists_sum as f64);
if wf_improved {
let node_count = graph.node_count();
closeness[is] = closeness[is]
.map(|c| c * (reachable_nodes_count - 1) as f64 / (node_count - 1) as f64);
}
}
closeness
}
59 changes: 58 additions & 1 deletion rustworkx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1600,9 +1600,65 @@ def _graph_betweenness_centrality(graph, normalized=True, endpoints=False, paral
)


@functools.singledispatch
def closeness_centrality(graph, wf_improved=True):
r"""Compute the closeness centrality of each node in a graph object.

The closeness centrality of a node :math:`u` is defined as the
reciprocal of the average shortest path distance to :math:`u` over all
:math:`n-1` reachable nodes in the graph. In it's general form this can
be expressed as:

.. math::

C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},

where:

* :math:`d(v, u)` - the shortest-path distance between :math:`v` and
:math:`u`
* :math:`n` - the number of nodes that can reach :math:`u`.

In the case of a graphs with more than one connected component there is
an alternative improved formula that calculates the closeness centrality
as "a ratio of the fraction of actors in the group who are reachable, to
the average distance" [WF]_. This can be expressed as

.. math::

C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},

where :math:`N` is the number of nodes in the graph. This alternative
formula can be used with the ``wf_improved`` argument.

:param graph: The input graph. Can either be a
:class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph`.
:param bool wf_improved: This is optional; the default is True. If True,
scale by the fraction of nodes reachable.

:returns: A dictionary mapping each node index to its closeness centrality.
:rtype: dict

.. [WF] Wasserman, S., & Faust, K. (1994). Social Network Analysis:
Methods and Applications (Structural Analysis in the Social Sciences).
Cambridge: Cambridge University Press. doi:10.1017/CBO9780511815478
"""
raise TypeError("Invalid input type %s for graph" % type(graph))


@closeness_centrality.register(PyDiGraph)
def _digraph_closeness_centrality(graph, wf_improved=True):
return digraph_closeness_centrality(graph, wf_improved=wf_improved)


@closeness_centrality.register(PyGraph)
def _graph_closeness_centrality(graph, wf_improved=True):
return graph_closeness_centrality(graph, wf_improved=wf_improved)


@functools.singledispatch
def edge_betweenness_centrality(graph, normalized=True, parallel_threshold=50):
"""Compute the edge betweenness centrality of all edges in a graph.
r"""Compute the edge betweenness centrality of all edges in a graph.

Edge betweenness centrality of an edge :math:`e` is the sum of the
fraction of all-pairs shortest paths that pass through :math`e`
Expand Down Expand Up @@ -1641,6 +1697,7 @@ def edge_betweenness_centrality(graph, normalized=True, parallel_threshold=50):
betweenness score for each node.
:rtype: EdgeCentralityMapping
"""
raise TypeError("Invalid input type %s for graph" % type(graph))


@edge_betweenness_centrality.register(PyDiGraph)
Expand Down
Loading