Source code for xgi.stats.nodestats

"""Node statistics.

This module is part of the stats package, and it defines node-level statistics.  That
is, each function defined in this module is assumed to define a node-quantity mapping.
Each callable defined here is accessible via a `Network` object, or a
:class:`~xgi.core.views.NodeView` object.  For more details, see the `tutorial
<https://xgi.readthedocs.io/en/stable/api/tutorials/focus_6.html>`_.

Examples
--------

>>> import xgi
>>> H = xgi.Hypergraph([[1, 2, 3], [2, 3, 4, 5], [3, 4, 5]])
>>> H.degree()
{1: 1, 2: 2, 3: 3, 4: 2, 5: 2}
>>> H.nodes.degree.asdict()
{1: 1, 2: 2, 3: 3, 4: 2, 5: 2}

"""

import numpy as np

import xgi

__all__ = [
    "attrs",
    "degree",
    "average_neighbor_degree",
    "local_clustering_coefficient",
    "clustering_coefficient",
    "two_node_clustering_coefficient",
    "clique_eigenvector_centrality",
    "h_eigenvector_centrality",
    "z_eigenvector_centrality",
    "node_edge_centrality",
    "katz_centrality",
]


[docs]def attrs(net, bunch, attr=None, missing=None): """Access node attributes. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. attr : str | None (default) If None, return all attributes. Otherwise, return a single attribute with name `attr`. missing : Any Value to impute in case a node does not have an attribute with name `attr`. Default is None. Returns ------- dict If attr is None, return a nested dict of the form `{node: {"attr": val}}`. Otherwise, return a simple dict of the form `{node: val}`. Notes ----- When requesting all attributes (i.e. when `attr` is None), no value is imputed. Examples -------- >>> import xgi >>> H = xgi.Hypergraph([[1, 2, 3], [2, 3, 4, 5], [3, 4, 5]]) >>> H.add_nodes_from([ ... (1, {"color": "red", "name": "horse"}), ... (2, {"color": "blue", "name": "pony"}), ... (3, {"color": "yellow", "name": "zebra"}), ... (4, {"color": "red", "name": "orangutan", "age": 20}), ... (5, {"color": "blue", "name": "fish", "age": 2}), ... ]) Access all attributes as different types. >>> H.nodes.attrs.asdict() # doctest: +NORMALIZE_WHITESPACE {1: {'color': 'red', 'name': 'horse'}, 2: {'color': 'blue', 'name': 'pony'}, 3: {'color': 'yellow', 'name': 'zebra'}, 4: {'color': 'red', 'name': 'orangutan', 'age': 20}, 5: {'color': 'blue', 'name': 'fish', 'age': 2}} >>> H.nodes.attrs.asnumpy() # doctest: +NORMALIZE_WHITESPACE array([{'color': 'red', 'name': 'horse'}, {'color': 'blue', 'name': 'pony'}, {'color': 'yellow', 'name': 'zebra'}, {'color': 'red', 'name': 'orangutan', 'age': 20}, {'color': 'blue', 'name': 'fish', 'age': 2}], dtype=object) Access a single attribute as different types. >>> H.nodes.attrs('color').asdict() {1: 'red', 2: 'blue', 3: 'yellow', 4: 'red', 5: 'blue'} >>> H.nodes.attrs('color').aslist() ['red', 'blue', 'yellow', 'red', 'blue'] By default, None is imputed when a node does not have the requested attribute. >>> H.nodes.attrs('age').asdict() {1: None, 2: None, 3: None, 4: 20, 5: 2} Use `missing` to change the imputed value. >>> H.nodes.attrs('age', missing=100).asdict() {1: 100, 2: 100, 3: 100, 4: 20, 5: 2} """ if isinstance(attr, str): return {n: net._node_attr[n].get(attr, missing) for n in bunch} elif attr is None: return {n: net._node_attr[n] for n in bunch} else: raise ValueError('"attr" must be str or None')
[docs]def degree(net, bunch, order=None, weight=None): """Node degree. The degree of a node is the number of edges it belongs to. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. order : int | None If not None (default), only count the edges of the given order. weight : str | None If not None, specifies the name of the edge attribute that determines the weight of each edge. Returns ------- dict """ if order is None and weight is None: return {n: len(net._node[n]) for n in bunch} if order is None and weight: return { n: sum(net._edge_attr[e].get(weight, 1) for e in net._node[n]) for n in bunch } if order is not None and weight is None: return { n: len([e for e in net._node[n] if len(net._edge[e]) == order + 1]) for n in bunch } if order is not None and weight: return { n: sum( net._edge_attr[e].get(weight, 1) for e in net._node[n] if len(net._edge[e]) == order + 1 ) for n in bunch }
[docs]def average_neighbor_degree(net, bunch): """Average neighbor degree. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. Returns ------- dict Examples -------- >>> import xgi, numpy as np >>> H = xgi.Hypergraph([[1, 2, 3], [2, 3, 4, 5], [3, 4, 5]]) >>> np.round(H.nodes.average_neighbor_degree.asnumpy(), 3) array([2.5 , 2. , 1.75 , 2.333, 2.333]) """ result = {} for n in bunch: neighbors = net.nodes.neighbors(n) result[n] = sum(len(net._node[nbr]) for nbr in neighbors) result[n] = result[n] / len(neighbors) if neighbors else 0 return result
[docs]def clustering_coefficient(net, bunch): """Clustering coefficient based on the pairwise projection of the hypergraph. See :func:`xgi.algorithms.clustering.clustering_coefficient` for the definition, formula, and references. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. Returns ------- dict See Also -------- ~xgi.algorithms.clustering.clustering_coefficient local_clustering_coefficient two_node_clustering_coefficient """ cc = xgi.clustering_coefficient(net) return {n: cc[n] for n in cc if n in bunch}
[docs]def local_clustering_coefficient(net, bunch): """Local clustering coefficient based on edge overlap. See :func:`xgi.algorithms.clustering.local_clustering_coefficient` for the definition and references. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. Returns ------- dict keys are node IDs and values are the clustering coefficients. See Also -------- ~xgi.algorithms.clustering.local_clustering_coefficient clustering_coefficient two_node_clustering_coefficient """ cc = xgi.local_clustering_coefficient(net) return {n: cc[n] for n in cc if n in bunch}
[docs]def two_node_clustering_coefficient(net, bunch, kind="union"): """Average over all two-node clustering coefficients involving each node. See :func:`xgi.algorithms.clustering.two_node_clustering_coefficient` for the definition and references. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. kind : str The type of two-node clustering coefficient: "union", "min", or "max". By default, "union". Returns ------- dict nodes are keys, clustering coefficients are values. See Also -------- ~xgi.algorithms.clustering.two_node_clustering_coefficient clustering_coefficient local_clustering_coefficient """ cc = xgi.two_node_clustering_coefficient(net, kind=kind) return {n: cc[n] for n in cc if n in bunch}
[docs]def clique_eigenvector_centrality(net, bunch, tol=1e-6): """Clique motif eigenvector centrality of a hypergraph. See :func:`xgi.algorithms.centrality.clique_eigenvector_centrality` for the definition and references. Parameters ---------- net : xgi.Hypergraph The hypergraph of interest. bunch : Iterable Nodes in `net`. tol : float > 0, default: 1e-6 The desired L2 error in the centrality vector. Returns ------- dict Centrality, where keys are node IDs and values are centralities. See Also -------- ~xgi.algorithms.centrality.clique_eigenvector_centrality """ c = xgi.clique_eigenvector_centrality(net, tol) return {n: c[n] for n in c if n in bunch}
[docs]def h_eigenvector_centrality(net, bunch, max_iter=10, tol=1e-6): """H-eigenvector centrality of a hypergraph. See :func:`xgi.algorithms.centrality.h_eigenvector_centrality` for the definition and references. Parameters ---------- net : xgi.Hypergraph The hypergraph of interest. bunch : Iterable Nodes in `net`. max_iter : int, default: 10 The maximum number of iterations before the algorithm terminates. tol : float > 0, default: 1e-6 The desired L2 error in the centrality vector. Returns ------- dict Centrality, where keys are node IDs and values are centralities. See Also -------- ~xgi.algorithms.centrality.h_eigenvector_centrality """ c = xgi.h_eigenvector_centrality(net, max_iter, tol) return {n: c[n] for n in c if n in bunch}
[docs]def z_eigenvector_centrality(net, bunch, max_iter=10, tol=1e-6): """Z-eigenvector centrality of a hypergraph. See :func:`xgi.algorithms.centrality.z_eigenvector_centrality` for the definition and references. Parameters ---------- net : xgi.Hypergraph The hypergraph of interest. bunch : Iterable Nodes in `net`. max_iter : int, default: 10 The maximum number of iterations before the algorithm terminates. tol : float > 0, default: 1e-6 The desired L2 error in the centrality vector. Returns ------- dict Centrality, where keys are node IDs and values are centralities. See Also -------- ~xgi.algorithms.centrality.z_eigenvector_centrality """ c = xgi.z_eigenvector_centrality(net, max_iter, tol) return {n: c[n] for n in c if n in bunch}
[docs]def node_edge_centrality( net, bunch, f=lambda x: np.power(x, 2), g=lambda x: np.power(x, 0.5), phi=lambda x: np.power(x, 2), psi=lambda x: np.power(x, 0.5), max_iter=100, tol=1e-6, ): """Node component of the nonlinear node-edge centrality. See :func:`xgi.algorithms.centrality.node_edge_centrality` for the definition, parameters, and references. Parameters ---------- net : Hypergraph The hypergraph of interest. bunch : Iterable Nodes in `net`. Returns ------- dict Node centralities. See Also -------- ~xgi.algorithms.centrality.node_edge_centrality """ c, _ = xgi.node_edge_centrality(net, f, g, phi, psi, max_iter, tol) return {n: c[n] for n in c if n in bunch}
[docs]def katz_centrality(net, bunch, cutoff=100): """Katz centrality of a hypergraph. See :func:`xgi.algorithms.centrality.katz_centrality` for the definition, formula, and references. Parameters ---------- net : xgi.Hypergraph The hypergraph of interest. bunch : Iterable Nodes in `net`. cutoff : int Power at which to truncate the underlying series. Default 100. Returns ------- dict Node IDs are keys and centrality values are values (1-normalized). Raises ------ XGIError If the hypergraph is empty. See Also -------- ~xgi.algorithms.centrality.katz_centrality """ c = xgi.katz_centrality(net, cutoff=cutoff) return {n: c[n] for n in c if n in bunch}
[docs]def local_simplicial_fraction(net, bunch, min_size=2, exclude_min_size=True): """The local simplicial fraction. For each node, computes :func:`xgi.algorithms.simpliciality.simplicial_fraction` on the subhypergraph induced by the node and its neighbors. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. min_size: int, default: 2 The minimum hyperedge size to include when calculating whether a hyperedge is a simplex by counting subfaces. exclude_min_size : bool, optional Whether to include minimal simplices when counting simplices, by default True Returns ------- dict See Also -------- ~xgi.algorithms.simpliciality.simplicial_fraction References ---------- "The simpliciality of higher-order order networks" by Nicholas Landry, Jean-Gabriel Young, and Nicole Eikmeier, *EPJ Data Science* **13**, 17 (2024). """ s = dict() for n in bunch: nbrs = net.nodes.neighbors(n) if len(nbrs) == 0: s[n] = np.nan else: nbrs.add(n) sh = xgi.subhypergraph(net, nodes=nbrs) s[n] = xgi.simplicial_fraction(sh, min_size, exclude_min_size) return s
[docs]def local_edit_simpliciality(net, bunch, min_size=2, exclude_min_size=True): """The local edit simpliciality. For each node, computes :func:`xgi.algorithms.simpliciality.edit_simpliciality` on the subhypergraph induced by the node and its neighbors. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. min_size: int, default: 2 The minimum hyperedge size to include when calculating whether a hyperedge is a simplex by counting subfaces. exclude_min_size : bool, optional Whether to include minimal simplices when counting simplices, by default True Returns ------- dict See Also -------- ~xgi.algorithms.simpliciality.edit_simpliciality References ---------- "The simpliciality of higher-order order networks" by Nicholas Landry, Jean-Gabriel Young, and Nicole Eikmeier, *EPJ Data Science* **13**, 17 (2024). """ s = dict() for n in bunch: nbrs = net.nodes.neighbors(n) if len(nbrs) == 0: s[n] = np.nan else: nbrs.add(n) sh = xgi.subhypergraph(net, nodes=nbrs) s[n] = xgi.edit_simpliciality(sh, min_size, exclude_min_size) return s
[docs]def local_face_edit_simpliciality(net, bunch, min_size=2, exclude_min_size=True): """The local face edit simpliciality. For each node, computes :func:`xgi.algorithms.simpliciality.face_edit_simpliciality` on the subhypergraph induced by the node and its neighbors. Parameters ---------- net : xgi.Hypergraph The network. bunch : Iterable Nodes in `net`. min_size: int, default: 2 The minimum hyperedge size to include when calculating whether a hyperedge is a simplex by counting subfaces. exclude_min_size : bool, optional Whether to include minimal simplices when counting simplices, by default True Returns ------- dict See Also -------- ~xgi.algorithms.simpliciality.face_edit_simpliciality References ---------- "The simpliciality of higher-order order networks" by Nicholas Landry, Jean-Gabriel Young, and Nicole Eikmeier, *EPJ Data Science* **13**, 17 (2024). """ s = dict() for n in bunch: nbrs = net.nodes.neighbors(n) if len(nbrs) == 0: s[n] = np.nan else: nbrs.add(n) sh = xgi.subhypergraph(net, nodes=nbrs) s[n] = xgi.face_edit_simpliciality(sh, min_size, exclude_min_size) return s