|
| 1 | +from easygraph.utils import * |
| 2 | +import numpy as np |
| 3 | +from easygraph.utils.decorators import * |
| 4 | + |
| 5 | +__all__ = ["katz_centrality"] |
| 6 | + |
| 7 | +@not_implemented_for("multigraph") |
| 8 | +@hybrid("cpp_katz_centrality") |
| 9 | +def katz_centrality(G, alpha=0.1, beta=1.0, max_iter=1000, tol=1e-6, normalized=True): |
| 10 | + r""" |
| 11 | + Compute the Katz centrality for nodes in a graph. |
| 12 | +
|
| 13 | + Katz centrality computes the influence of a node based on the total number |
| 14 | + of walks between nodes, attenuated by a factor of their length. It is |
| 15 | + defined as the solution to the linear system: |
| 16 | +
|
| 17 | + .. math:: |
| 18 | +
|
| 19 | + x = \alpha A x + \beta |
| 20 | +
|
| 21 | + where: |
| 22 | + - \( A \) is the adjacency matrix of the graph, |
| 23 | + - \( \alpha \) is a scalar attenuation factor, |
| 24 | + - \( \beta \) is the bias vector (typically all ones), |
| 25 | + - and \( x \) is the resulting centrality vector. |
| 26 | +
|
| 27 | + The algorithm runs an iterative fixed-point method until convergence. |
| 28 | +
|
| 29 | + Parameters |
| 30 | + ---------- |
| 31 | + G : easygraph.Graph |
| 32 | + An EasyGraph graph instance. Must be simple (non-multigraph). |
| 33 | +
|
| 34 | + alpha : float, optional (default=0.1) |
| 35 | + Attenuation factor, must be smaller than the reciprocal of the largest |
| 36 | + eigenvalue of the adjacency matrix to ensure convergence. |
| 37 | +
|
| 38 | + beta : float or dict, optional (default=1.0) |
| 39 | + Bias term. Can be a constant scalar applied to all nodes, or a dictionary |
| 40 | + mapping node IDs to values. |
| 41 | +
|
| 42 | + max_iter : int, optional (default=1000) |
| 43 | + Maximum number of iterations before the algorithm terminates. |
| 44 | +
|
| 45 | + tol : float, optional (default=1e-6) |
| 46 | + Convergence tolerance. Iteration stops when the L1 norm of the difference |
| 47 | + between successive iterations is below this threshold. |
| 48 | +
|
| 49 | + normalized : bool, optional (default=True) |
| 50 | + If True, the result vector will be normalized to unit norm (L2). |
| 51 | +
|
| 52 | + Returns |
| 53 | + ------- |
| 54 | + dict |
| 55 | + A dictionary mapping node IDs to Katz centrality scores. |
| 56 | +
|
| 57 | + Raises |
| 58 | + ------ |
| 59 | + RuntimeError |
| 60 | + If the algorithm fails to converge within `max_iter` iterations. |
| 61 | +
|
| 62 | + Examples |
| 63 | + -------- |
| 64 | + >>> import easygraph as eg |
| 65 | + >>> from easygraph import katz_centrality |
| 66 | + >>> G = eg.Graph() |
| 67 | + >>> G.add_edges_from([(0, 1), (1, 2), (2, 3)]) |
| 68 | + >>> katz_centrality(G, alpha=0.05) |
| 69 | + {0: 0.370..., 1: 0.447..., 2: 0.447..., 3: 0.370...} |
| 70 | + """ |
| 71 | + # Create node ordering |
| 72 | + nodes = list(G.nodes) |
| 73 | + n = len(nodes) |
| 74 | + node_to_index = {node: i for i, node in enumerate(nodes)} |
| 75 | + index_to_node = {i: node for i, node in enumerate(nodes)} |
| 76 | + |
| 77 | + # Build adjacency matrix |
| 78 | + A = np.zeros((n, n), dtype=np.float64) |
| 79 | + for u in G.nodes: |
| 80 | + for v in G.adj[u]: |
| 81 | + A[node_to_index[u], node_to_index[v]] = 1.0 |
| 82 | + |
| 83 | + # Initialize x and beta |
| 84 | + x = np.ones(n, dtype=np.float64) |
| 85 | + if isinstance(beta, dict): |
| 86 | + b = np.array([beta.get(index_to_node[i], 1.0) for i in range(n)]) |
| 87 | + else: |
| 88 | + b = np.ones(n, dtype=np.float64) * beta |
| 89 | + |
| 90 | + # Iterative update using vectorized ops |
| 91 | + for _ in range(max_iter): |
| 92 | + x_new = alpha * A @ x + b |
| 93 | + if np.linalg.norm(x_new - x, ord=1) < tol: |
| 94 | + break |
| 95 | + x = x_new |
| 96 | + else: |
| 97 | + raise RuntimeError(f"Katz centrality failed to converge in {max_iter} iterations") |
| 98 | + |
| 99 | + if normalized: |
| 100 | + norm = np.linalg.norm(x) |
| 101 | + if norm > 0: |
| 102 | + x /= norm |
| 103 | + |
| 104 | + result = {index_to_node[i]: float(x[i]) for i in range(n)} |
| 105 | + return result |
0 commit comments