| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591 |
- """Betweenness centrality measures."""
- import math
- from collections import deque
- from heapq import heappop, heappush
- from itertools import count
- import networkx as nx
- from networkx.algorithms.shortest_paths.weighted import _weight_function
- from networkx.utils import py_random_state
- from networkx.utils.decorators import not_implemented_for
- __all__ = ["betweenness_centrality", "edge_betweenness_centrality"]
- @py_random_state("seed")
- @nx._dispatchable(edge_attrs="weight")
- def betweenness_centrality(
- G, k=None, normalized=True, weight=None, endpoints=False, seed=None
- ):
- r"""Compute the shortest-path betweenness centrality for nodes.
- Betweenness centrality of a node $v$ is the sum of the
- fraction of all-pairs shortest paths that pass through $v$.
- .. math::
- c_B(v) = \sum_{s, t \in V} \frac{\sigma(s, t | v)}{\sigma(s, t)}
- where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
- shortest $(s, t)$-paths, and $\sigma(s, t | v)$ is the number of
- those paths passing through some node $v$ other than $s$ and $t$.
- If $s = t$, $\sigma(s, t) = 1$, and if $v \in \{s, t\}$,
- $\sigma(s, t | v) = 0$ [2]_.
- The denominator $\sigma(s, t)$ is a normalization factor that can be
- turned off to get the raw path counts.
- Parameters
- ----------
- G : graph
- A NetworkX graph.
- k : int, optional (default=None)
- If `k` is not `None`, use `k` sampled nodes as sources for the considered paths.
- The resulting sampled counts are then inflated to approximate betweenness.
- Higher values of `k` give better approximation. Must have ``k <= len(G)``.
- normalized : bool, optional (default=True)
- If `True`, the betweenness values are rescaled by dividing by the number of
- possible $(s, t)$-pairs in the graph.
- weight : None or string, optional (default=None)
- If `None`, all edge weights are 1.
- Otherwise holds the name of the edge attribute used as weight.
- Weights are used to calculate weighted shortest paths, so they are
- interpreted as distances.
- endpoints : bool, optional (default=False)
- If `True`, include the endpoints $s$ and $t$ in the shortest path counts.
- This is taken into account when rescaling the values.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Note that this is only used if ``k is not None``.
- Returns
- -------
- nodes : dict
- Dictionary of nodes with betweenness centrality as the value.
- See Also
- --------
- betweenness_centrality_subset
- edge_betweenness_centrality
- load_centrality
- Notes
- -----
- The algorithm is from Ulrik Brandes [1]_.
- See [4]_ for the original first published version and [2]_ for details on
- algorithms for variations and related metrics.
- For approximate betweenness calculations, set `k` to the number of sampled
- nodes ("pivots") used as sources to estimate the betweenness values.
- The formula then sums over $s$ is in these pivots, instead of over all nodes.
- The resulting sum is then inflated to approximate the full sum.
- For a discussion of how to choose `k` for efficiency, see [3]_.
- For weighted graphs the edge weights must be greater than zero.
- Zero edge weights can produce an infinite number of equal length
- paths between pairs of nodes.
- Directed graphs and undirected graphs count paths differently.
- In directed graphs, each pair of source-target nodes is considered separately
- in each direction, as the shortest paths can differ by direction.
- However, in undirected graphs, each pair of nodes is considered only once,
- as the shortest paths are symmetric.
- This means the normalization factor to divide by is $N(N-1)$ for directed graphs
- and $N(N-1)/2$ for undirected graphs, where $N = n$ (the number of nodes)
- if endpoints are included and $N = n-1$ otherwise.
- This algorithm is not guaranteed to be correct if edge weights
- are floating point numbers. As a workaround you can use integer
- numbers by multiplying the relevant edge attributes by a convenient
- constant factor (e.g. 100) and converting to integers.
- References
- ----------
- .. [1] Ulrik Brandes:
- A Faster Algorithm for Betweenness Centrality.
- Journal of Mathematical Sociology 25(2):163--177, 2001.
- https://doi.org/10.1080/0022250X.2001.9990249
- .. [2] 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
- .. [3] Ulrik Brandes and Christian Pich:
- Centrality Estimation in Large Networks.
- International Journal of Bifurcation and Chaos 17(7):2303--2318, 2007.
- https://dx.doi.org/10.1142/S0218127407018403
- .. [4] Linton C. Freeman:
- A set of measures of centrality based on betweenness.
- Sociometry 40: 35--41, 1977
- https://doi.org/10.2307/3033543
- Examples
- --------
- Consider an undirected 3-path. Each pair of nodes has exactly one shortest
- path between them. Since the graph is undirected, only ordered pairs are counted.
- Of these (and when `endpoints` is `False`), none of the shortest paths pass
- through 0 and 2, and only the shortest path between 0 and 2 passes through 1.
- As such, the counts should be ``{0: 0, 1: 1, 2: 0}``.
- >>> G = nx.path_graph(3)
- >>> nx.betweenness_centrality(G, normalized=False, endpoints=False)
- {0: 0.0, 1: 1.0, 2: 0.0}
- If `endpoints` is `True`, we also need to count endpoints as being on the path:
- $\sigma(s, t | s) = \sigma(s, t | t) = \sigma(s, t)$.
- In our example, 0 is then part of two shortest paths (0 to 1 and 0 to 2);
- similarly, 2 is part of two shortest paths (0 to 2 and 1 to 2).
- 1 is part of all three shortest paths. This makes the new raw
- counts ``{0: 2, 1: 3, 2: 2}``.
- >>> nx.betweenness_centrality(G, normalized=False, endpoints=True)
- {0: 2.0, 1: 3.0, 2: 2.0}
- With normalization, the values are divided by the number of ordered $(s, t)$-pairs.
- If we are not counting endpoints, there are $n - 1$ possible choices for $s$
- (all except the node we are computing betweenness centrality for), which in turn
- leaves $n - 2$ possible choices for $t$ as $s \ne t$.
- The total number of ordered pairs when `endpoints` is `False` is $(n - 1)(n - 2)/2 = 1$.
- If `endpoints` is `True`, there are $n(n - 1)/2 = 3$ ordered $(s, t)$-pairs to divide by.
- >>> nx.betweenness_centrality(G, normalized=True, endpoints=False)
- {0: 0.0, 1: 1.0, 2: 0.0}
- >>> nx.betweenness_centrality(G, normalized=True, endpoints=True)
- {0: 0.6666666666666666, 1: 1.0, 2: 0.6666666666666666}
- If the graph is directed instead, we now need to consider $(s, t)$-pairs
- in both directions. Our example becomes a directed 3-path.
- Without counting endpoints, we only have one path through 1 (0 to 2).
- This means the raw counts are ``{0: 0, 1: 1, 2: 0}``.
- >>> DG = nx.path_graph(3, create_using=nx.DiGraph)
- >>> nx.betweenness_centrality(DG, normalized=False, endpoints=False)
- {0: 0.0, 1: 1.0, 2: 0.0}
- If we do include endpoints, the raw counts are ``{0: 2, 1: 3, 2: 2}``.
- >>> nx.betweenness_centrality(DG, normalized=False, endpoints=True)
- {0: 2.0, 1: 3.0, 2: 2.0}
- If we want to normalize directed betweenness centrality, the raw counts
- are normalized by the number of $(s, t)$-pairs. There are $n(n - 1)$
- possible paths with endpoints and $(n - 1)(n - 2)$ without endpoints.
- In our example, that's 6 with endpoints and 2 without endpoints.
- >>> nx.betweenness_centrality(DG, normalized=True, endpoints=True)
- {0: 0.3333333333333333, 1: 0.5, 2: 0.3333333333333333}
- >>> nx.betweenness_centrality(DG, normalized=True, endpoints=False)
- {0: 0.0, 1: 0.5, 2: 0.0}
- Computing the full betweenness centrality can be costly.
- This function can also be used to compute approximate betweenness centrality
- by setting `k`. This only determines the number of source nodes to sample;
- all nodes are targets.
- For simplicity, we only consider the case where endpoints are included in the counts.
- Since the partial sums only include `k` terms, instead of ``n``,
- we multiply them by ``n / k``, to approximate the full sum.
- As the sets of sources and targets are not the same anymore,
- paths have to be counted in a directed way. We thus count each as half a path.
- This ensures that the results approximate the standard betweenness for ``k == n``.
- For instance, in the undirected 3-path graph case, setting ``k = 2`` (with ``seed=42``)
- selects nodes 0 and 2 as sources.
- This means only shortest paths starting at these nodes are considered.
- The raw counts with endpoints are ``{0: 3, 1: 4, 2: 3}``. Accounting for the partial sum
- and applying the undirectedness half-path correction, we get
- >>> nx.betweenness_centrality(G, k=2, normalized=False, endpoints=True, seed=42)
- {0: 2.25, 1: 3.0, 2: 2.25}
- When normalizing, we instead want to divide by the total number of $(s, t)$-pairs.
- This is $k(n - 1)$ with endpoints.
- >>> nx.betweenness_centrality(G, k=2, normalized=True, endpoints=True, seed=42)
- {0: 0.75, 1: 1.0, 2: 0.75}
- """
- betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
- if k == len(G):
- # This is done for performance; the result is the same regardless.
- k = None
- if k is None:
- nodes = G
- else:
- nodes = seed.sample(list(G.nodes()), k)
- for s in nodes:
- # single source shortest paths
- if weight is None: # use BFS
- S, P, sigma, _ = _single_source_shortest_path_basic(G, s)
- else: # use Dijkstra's algorithm
- S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight)
- # accumulation
- if endpoints:
- betweenness, _ = _accumulate_endpoints(betweenness, S, P, sigma, s)
- else:
- betweenness, _ = _accumulate_basic(betweenness, S, P, sigma, s)
- # rescaling
- betweenness = _rescale(
- betweenness,
- len(G),
- normalized=normalized,
- directed=G.is_directed(),
- endpoints=endpoints,
- sampled_nodes=None if k is None else nodes,
- )
- return betweenness
- @py_random_state("seed")
- @nx._dispatchable(edge_attrs="weight")
- def edge_betweenness_centrality(G, k=None, normalized=True, weight=None, seed=None):
- r"""Compute betweenness centrality for edges.
- Betweenness centrality of an edge $e$ is the sum of the
- fraction of all-pairs shortest paths that pass through $e$.
- .. math::
- c_B(e) = \sum_{s, t \in V} \frac{\sigma(s, t | e)}{\sigma(s, t)}
- where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
- shortest $(s, t)$-paths, and $\sigma(s, t | e)$ is the number of
- those paths passing through edge $e$ [1]_.
- The denominator $\sigma(s, t)$ is a normalization factor that can be
- turned off to get the raw path counts.
- Parameters
- ----------
- G : graph
- A NetworkX graph.
- k : int, optional (default=None)
- If `k` is not `None`, use `k` sampled nodes as sources for the considered paths.
- The resulting sampled counts are then inflated to approximate betweenness.
- Higher values of `k` give better approximation. Must have ``k <= len(G)``.
- normalized : bool, optional (default=True)
- If `True`, the betweenness values are rescaled by dividing by the number of
- possible $(s, t)$-pairs in the graph.
- weight : None or string, optional (default=None)
- If `None`, all edge weights are 1.
- Otherwise holds the name of the edge attribute used as weight.
- Weights are used to calculate weighted shortest paths, so they are
- interpreted as distances.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Note that this is only used if ``k is not None``.
- Returns
- -------
- edges : dict
- Dictionary of edges with betweenness centrality as the value.
- See Also
- --------
- betweenness_centrality
- edge_betweenness_centrality_subset
- edge_load
- Notes
- -----
- The algorithm is from Ulrik Brandes [1]_.
- For weighted graphs the edge weights must be greater than zero.
- Zero edge weights can produce an infinite number of equal length
- paths between pairs of nodes.
- References
- ----------
- .. [1] 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
- Examples
- --------
- Consider an undirected 3-path. Each pair of nodes has exactly one shortest
- path between them. Since the graph is undirected, only ordered pairs are counted.
- Each edge has two shortest paths passing through it.
- As such, the raw counts should be ``{(0, 1): 2, (1, 2): 2}``.
- >>> G = nx.path_graph(3)
- >>> nx.edge_betweenness_centrality(G, normalized=False)
- {(0, 1): 2.0, (1, 2): 2.0}
- With normalization, the values are divided by the number of ordered $(s, t)$-pairs,
- which is $n(n-1)/2$. For the 3-path, this is $3(3-1)/2 = 3$.
- >>> nx.edge_betweenness_centrality(G, normalized=True)
- {(0, 1): 0.6666666666666666, (1, 2): 0.6666666666666666}
- For a directed graph, all $(s, t)$-pairs are considered. The normalization factor
- is $n(n-1)$ to reflect this.
- >>> DG = nx.path_graph(3, create_using=nx.DiGraph)
- >>> nx.edge_betweenness_centrality(DG, normalized=False)
- {(0, 1): 2.0, (1, 2): 2.0}
- >>> nx.edge_betweenness_centrality(DG, normalized=True)
- {(0, 1): 0.3333333333333333, (1, 2): 0.3333333333333333}
- Computing the full edge betweenness centrality can be costly.
- This function can also be used to compute approximate edge betweenness centrality
- by setting `k`. This determines the number of source nodes to sample.
- Since the partial sums only include `k` terms, instead of ``n``,
- we multiply them by ``n / k``, to approximate the full sum.
- As the sets of sources and targets are not the same anymore,
- paths have to be counted in a directed way. We thus count each as half a path.
- This ensures that the results approximate the standard betweenness for ``k == n``.
- For instance, in the undirected 3-path graph case, setting ``k = 2`` (with ``seed=42``)
- selects nodes 0 and 2 as sources.
- This means only shortest paths starting at these nodes are considered.
- The raw counts are ``{(0, 1): 3, (1, 2): 3}``. Accounting for the partial sum
- and applying the undirectedness half-path correction, we get
- >>> nx.edge_betweenness_centrality(G, k=2, normalized=False, seed=42)
- {(0, 1): 2.25, (1, 2): 2.25}
- When normalizing, we instead want to divide by the total number of $(s, t)$-pairs.
- This is $k(n-1)$, which is $4$ in our case.
- >>> nx.edge_betweenness_centrality(G, k=2, normalized=True, seed=42)
- {(0, 1): 0.75, (1, 2): 0.75}
- """
- betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
- # b[e]=0 for e in G.edges()
- betweenness.update(dict.fromkeys(G.edges(), 0.0))
- if k is None:
- nodes = G
- else:
- nodes = seed.sample(list(G.nodes()), k)
- for s in nodes:
- # single source shortest paths
- if weight is None: # use BFS
- S, P, sigma, _ = _single_source_shortest_path_basic(G, s)
- else: # use Dijkstra's algorithm
- S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight)
- # accumulation
- betweenness = _accumulate_edges(betweenness, S, P, sigma, s)
- # rescaling
- for n in G: # remove nodes to only return edges
- del betweenness[n]
- betweenness = _rescale(
- betweenness,
- len(G),
- normalized=normalized,
- directed=G.is_directed(),
- sampled_nodes=None if k is None else nodes,
- )
- if G.is_multigraph():
- betweenness = _add_edge_keys(G, betweenness, weight=weight)
- return betweenness
- # helpers for betweenness centrality
- def _single_source_shortest_path_basic(G, s):
- S = []
- P = {}
- for v in G:
- P[v] = []
- sigma = dict.fromkeys(G, 0.0) # sigma[v]=0 for v in G
- D = {}
- sigma[s] = 1.0
- D[s] = 0
- Q = deque([s])
- while Q: # use BFS to find shortest paths
- v = Q.popleft()
- S.append(v)
- Dv = D[v]
- sigmav = sigma[v]
- for w in G[v]:
- if w not in D:
- Q.append(w)
- D[w] = Dv + 1
- if D[w] == Dv + 1: # this is a shortest path, count paths
- sigma[w] += sigmav
- P[w].append(v) # predecessors
- return S, P, sigma, D
- def _single_source_dijkstra_path_basic(G, s, weight):
- weight = _weight_function(G, weight)
- # modified from Eppstein
- S = []
- P = {}
- for v in G:
- P[v] = []
- sigma = dict.fromkeys(G, 0.0) # sigma[v]=0 for v in G
- D = {}
- sigma[s] = 1.0
- seen = {s: 0}
- c = count()
- Q = [] # use Q as heap with (distance,node id) tuples
- heappush(Q, (0, next(c), s, s))
- while Q:
- (dist, _, pred, v) = heappop(Q)
- if v in D:
- continue # already searched this node.
- sigma[v] += sigma[pred] # count paths
- S.append(v)
- D[v] = dist
- for w, edgedata in G[v].items():
- vw_dist = dist + weight(v, w, edgedata)
- if w not in D and (w not in seen or vw_dist < seen[w]):
- seen[w] = vw_dist
- heappush(Q, (vw_dist, next(c), v, w))
- sigma[w] = 0.0
- P[w] = [v]
- elif vw_dist == seen[w]: # handle equal paths
- sigma[w] += sigma[v]
- P[w].append(v)
- return S, P, sigma, D
- def _accumulate_basic(betweenness, S, P, sigma, s):
- delta = dict.fromkeys(S, 0)
- while S:
- w = S.pop()
- coeff = (1 + delta[w]) / sigma[w]
- for v in P[w]:
- delta[v] += sigma[v] * coeff
- if w != s:
- betweenness[w] += delta[w]
- return betweenness, delta
- def _accumulate_endpoints(betweenness, S, P, sigma, s):
- betweenness[s] += len(S) - 1
- delta = dict.fromkeys(S, 0)
- while S:
- w = S.pop()
- coeff = (1 + delta[w]) / sigma[w]
- for v in P[w]:
- delta[v] += sigma[v] * coeff
- if w != s:
- betweenness[w] += delta[w] + 1
- return betweenness, delta
- def _accumulate_edges(betweenness, S, P, sigma, s):
- delta = dict.fromkeys(S, 0)
- while S:
- w = S.pop()
- coeff = (1 + delta[w]) / sigma[w]
- for v in P[w]:
- c = sigma[v] * coeff
- if (v, w) not in betweenness:
- betweenness[(w, v)] += c
- else:
- betweenness[(v, w)] += c
- delta[v] += c
- if w != s:
- betweenness[w] += delta[w]
- return betweenness
- def _rescale(
- betweenness, n, *, normalized, directed, endpoints=True, sampled_nodes=None
- ):
- # For edge betweenness, `endpoints` is always `True`.
- k = None if sampled_nodes is None else len(sampled_nodes)
- # N is used to count the number of valid (s, t) pairs where s != t that
- # could have a path pass through v. If endpoints is False, then v must
- # not be the target t, hence why we subtract by 1.
- N = n if endpoints else n - 1
- if N < 2:
- # No rescaling necessary: b=0 for all nodes
- return betweenness
- K_source = N if k is None else k
- if k is None or endpoints:
- # No sampling adjustment needed
- if normalized:
- # Divide by the number of valid (s, t) node pairs that could have
- # a path through v where s != t.
- scale = 1 / (K_source * (N - 1))
- else:
- # Scale to the full BC
- if not directed:
- # The non-normalized BC values are computed the same way for
- # directed and undirected graphs: shortest paths are computed and
- # counted for each *ordered* (s, t) pair. Undirected graphs should
- # only count valid *unordered* node pairs {s, t}; that is, (s, t)
- # and (t, s) should be counted only once. We correct for this here.
- correction = 2
- else:
- correction = 1
- scale = N / (K_source * correction)
- if scale != 1:
- for v in betweenness:
- betweenness[v] *= scale
- return betweenness
- # Sampling adjustment needed when excluding endpoints when using k. In this
- # case, we need to handle source nodes differently from non-source nodes,
- # because source nodes can't include themselves since endpoints are excluded.
- # Without this, k == n would be a special case that would violate the
- # assumption that node `v` is not one of the (s, t) node pairs.
- if normalized:
- # NaN for undefined 0/0; there is no data for source node when k=1
- scale_source = 1 / ((K_source - 1) * (N - 1)) if K_source > 1 else math.nan
- scale_nonsource = 1 / (K_source * (N - 1))
- else:
- correction = 1 if directed else 2
- scale_source = N / ((K_source - 1) * correction) if K_source > 1 else math.nan
- scale_nonsource = N / (K_source * correction)
- sampled_nodes = set(sampled_nodes)
- for v in betweenness:
- betweenness[v] *= scale_source if v in sampled_nodes else scale_nonsource
- return betweenness
- @not_implemented_for("graph")
- def _add_edge_keys(G, betweenness, weight=None):
- r"""Adds the corrected betweenness centrality (BC) values for multigraphs.
- Parameters
- ----------
- G : NetworkX graph.
- betweenness : dictionary
- Dictionary mapping adjacent node tuples to betweenness centrality values.
- weight : string or function
- See `_weight_function` for details. Defaults to `None`.
- Returns
- -------
- edges : dictionary
- The parameter `betweenness` including edges with keys and their
- betweenness centrality values.
- The BC value is divided among edges of equal weight.
- """
- _weight = _weight_function(G, weight)
- edge_bc = dict.fromkeys(G.edges, 0.0)
- for u, v in betweenness:
- d = G[u][v]
- wt = _weight(u, v, d)
- keys = [k for k in d if _weight(u, v, {k: d[k]}) == wt]
- bc = betweenness[(u, v)] / len(keys)
- for k in keys:
- edge_bc[(u, v, k)] = bc
- return edge_bc
|