Skip to content

Commit

Permalink
Implementation of closeness_centrality (#593)
Browse files Browse the repository at this point in the history
* Initial implementation of closeness_centrality

* fix typo

* add tests

* apply the suggestions.

* add a releasenote and improve Doc

* apply the suggestions

* supports rust version 1.48.0

* improve variable name

* Fix up docstrings and release note

* Fix rustworkx-core tests

---------

Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
derbuihan and mtreinish authored Mar 10, 2023
1 parent baffac5 commit 9dd7a8a
Show file tree
Hide file tree
Showing 12 changed files with 431 additions and 3 deletions.
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)
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 rustworkx_core::petgraph;
/// use rustworkx_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
}
100 changes: 100 additions & 0 deletions rustworkx-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;
use rustworkx_core::centrality::closeness_centrality;
use rustworkx_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);
}
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

0 comments on commit 9dd7a8a

Please sign in to comment.