|
| 1 | +# bioneuralnet/clustering/spectral.py |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | +import numpy as np |
| 5 | +import networkx as nx |
| 6 | +from sklearn.cluster import SpectralClustering |
| 7 | +from bioneuralnet.utils.logger import get_logger |
| 8 | +import pandas as pd |
| 9 | +from typing import Sequence, Tuple |
| 10 | + |
| 11 | + |
| 12 | +def spectral_cluster_graph( |
| 13 | + G: nx.Graph, |
| 14 | + n_clusters: int, |
| 15 | + use_edge_weights: bool = True, |
| 16 | + random_state: int | None = 0, |
| 17 | +) -> Tuple[np.ndarray, Sequence]: |
| 18 | + """ |
| 19 | + Spectral clustering on a NetworkX graph using its adjacency |
| 20 | + as a precomputed affinity matrix. |
| 21 | +
|
| 22 | + Parameters |
| 23 | + G : networkx.Graph |
| 24 | + Input graph. Node labels can be anything hashable. |
| 25 | + n_clusters : int |
| 26 | + Number of clusters to find. |
| 27 | + use_edge_weights : bool, default=True |
| 28 | + If True, use the 'weight' attribute on edges (defaulting to 1.0 |
| 29 | + when missing). If False, treat the graph as unweighted. |
| 30 | + random_state : int or None, default=0 |
| 31 | + Random seed passed to scikit-learn's SpectralClustering. |
| 32 | +
|
| 33 | + Returns |
| 34 | + labels : np.ndarray, shape (n_nodes,) |
| 35 | + Cluster label for each node, in the order of `nodes_order`. |
| 36 | + nodes_order : list |
| 37 | + List of nodes corresponding to `labels`. |
| 38 | + """ |
| 39 | + logger = get_logger(__name__) |
| 40 | + |
| 41 | + # fixed node order |
| 42 | + nodes_order = list(G.nodes()) |
| 43 | + n = len(nodes_order) |
| 44 | + node_index = {node: i for i, node in enumerate(nodes_order)} |
| 45 | + |
| 46 | + # build dense affinity matrix |
| 47 | + A = np.zeros((n, n), dtype=float) |
| 48 | + |
| 49 | + if use_edge_weights: |
| 50 | + for u, v, data in G.edges(data=True): |
| 51 | + i = node_index[u] |
| 52 | + j = node_index[v] |
| 53 | + w = float(data.get("weight", 1.0)) |
| 54 | + A[i, j] = w |
| 55 | + A[j, i] = w # assume undirected |
| 56 | + else: |
| 57 | + for u, v in G.edges(): |
| 58 | + i = node_index[u] |
| 59 | + j = node_index[v] |
| 60 | + A[i, j] = 1.0 |
| 61 | + A[j, i] = 1.0 |
| 62 | + if not np.any(A): |
| 63 | + raise ValueError("Graph has no edges; spectral clustering is undefined.") |
| 64 | + |
| 65 | + logger.info( |
| 66 | + f"Running SpectralClustering on graph with {n} nodes, " |
| 67 | + f"{G.number_of_edges()} edges, n_clusters={n_clusters}, " |
| 68 | + f"use_edge_weights={use_edge_weights}." |
| 69 | + ) |
| 70 | + |
| 71 | + spec = SpectralClustering( |
| 72 | + n_clusters=n_clusters, |
| 73 | + affinity="precomputed", |
| 74 | + assign_labels="kmeans", |
| 75 | + random_state=random_state, |
| 76 | + n_init=10, |
| 77 | + ) |
| 78 | + labels = spec.fit_predict(A) |
| 79 | + |
| 80 | + return labels, nodes_order |
0 commit comments