Source code for modelly.space.networks.collectors
"""Cheap network statistics for data collection: degrees, clustering, components.
Every statistic takes a graph in any of the forms a model holds: a ``MultilayerNetwork``
or ``TemporalNetwork`` (its current combined adjacency), a ``Network`` cell space, a
NetworkX graph, an igraph graph or a scipy sparse matrix. The structure is read as a
simple undirected graph (weights, edge directions and self-loops are ignored), and the
work is a few sparse-matrix products, so a reporter costs milliseconds on large graphs.
On simple undirected graphs the results equal NetworkX's ``degree_histogram``,
``clustering``, ``average_clustering`` and connected component sizes.
``network_reporter`` wraps a statistic as a ``DataCollector`` model reporter that can be
checkpointed (no lambda)::
DataCollector({"giant": network_reporter("contacts", largest_component)})
"""
from __future__ import annotations
from collections.abc import Callable
from functools import partial
from typing import TYPE_CHECKING, Any
import numpy as np
if TYPE_CHECKING:
from scipy.sparse import csr_matrix
[docs]
def as_adjacency(graph: Any) -> csr_matrix:
"""The binary, symmetric, loop-free adjacency of ``graph`` as a CSR matrix."""
from scipy import sparse # noqa: PLC0415 - scipy is heavy
if sparse.issparse(graph):
matrix = sparse.csr_matrix(graph)
elif hasattr(graph, "adjacency") and hasattr(graph, "_row"):
matrix = graph.adjacency()
elif hasattr(graph, "get_adjacency_sparse"): # igraph
matrix = sparse.csr_matrix(graph.get_adjacency_sparse())
else:
import networkx as nx # noqa: PLC0415
nx_graph = graph.G if hasattr(graph, "G") else graph
matrix = nx.to_scipy_sparse_array(nx_graph, weight=None, format="csr")
matrix = (matrix != 0).astype(np.int64)
matrix = ((matrix + matrix.T) > 0).astype(np.int64).tolil()
matrix.setdiag(0)
return sparse.csr_matrix(matrix)
[docs]
def degrees(graph: Any) -> np.ndarray:
"""The degree of every node, in node order."""
return np.asarray(as_adjacency(graph).sum(axis=1)).ravel()
[docs]
def degree_distribution(graph: Any) -> np.ndarray:
"""``counts[k]`` = number of nodes of degree ``k``, as ``networkx.degree_histogram``."""
return np.bincount(degrees(graph))
[docs]
def clustering(graph: Any) -> np.ndarray:
"""The local clustering coefficient of every node, as ``networkx.clustering``."""
adjacency = as_adjacency(graph)
triangles = np.asarray(
(adjacency @ adjacency).multiply(adjacency).sum(axis=1)
).ravel()
k = np.asarray(adjacency.sum(axis=1)).ravel().astype(float)
pairs = k * (k - 1)
return np.divide(triangles, pairs, out=np.zeros_like(pairs), where=pairs > 0)
[docs]
def average_clustering(graph: Any) -> float:
"""The mean local clustering over all nodes, as ``networkx.average_clustering``."""
values = clustering(graph)
return float(values.mean()) if values.size else 0.0
[docs]
def component_sizes(graph: Any) -> np.ndarray:
"""The sizes of the connected components, largest first."""
from scipy.sparse.csgraph import connected_components # noqa: PLC0415
adjacency = as_adjacency(graph)
if adjacency.shape[0] == 0:
return np.zeros(0, dtype=np.int64)
_, labels = connected_components(adjacency, directed=False)
return np.sort(np.bincount(labels))[::-1]
[docs]
def largest_component(graph: Any) -> int:
"""The number of nodes in the largest connected component."""
sizes = component_sizes(graph)
return int(sizes[0]) if sizes.size else 0
[docs]
def mean_degree(graph: Any) -> float:
"""The mean degree."""
values = degrees(graph)
return float(values.mean()) if values.size else 0.0
[docs]
def network_reporter(attribute: str, statistic: Callable[[Any], Any]) -> partial:
"""A model reporter computing ``statistic(getattr(model, attribute))``.
Unlike a lambda it can be pickled, so models that use it can be checkpointed.
Args:
attribute: the model attribute holding the network.
statistic: a module-level function of the network, such as ``mean_degree``.
"""
return partial(_report, attribute=attribute, statistic=statistic)
def _report(model: Any, attribute: str, statistic: Callable[[Any], Any]) -> Any:
return statistic(getattr(model, attribute))