生物医学网络科学Biomedical Network Science
Example11 / 11

比较两张网络——已知节点对应的距离、谱距离、WL 图核,以及基于距离的置换检验

所属专题
网络构建与统计验证
下载
Notebook · .py

生成 4 类网络(N=100、边数相同):ER、Watts–Strogatz、Barabási–Albert、随机块模型(SBM),每类 6 个实例。 距离:(a) Jaccard 边距离(需要节点对应);(b) 归一化拉普拉斯谱距离(不需要对应); (c) 度分布的 Jensen–Shannon 散度;(d) Weisfeiler–Lehman(WL)子树特征的余弦距离(一种图核)。 用“类内平均距离 / 类间平均距离”衡量每种距离区分网络类型的能力(越小越好)。 最后对两组“被试网络”做基于距离的置换检验:统计量 = 组间平均距离 − 组内平均距离。 解读:节点无对应时,Jaccard 距离只在 WS 这类“共享格点骨架”的网络之间较小,对同类随机网络接近 1,不适用;谱距离、度分布与 WL 核关注的结构层次不同, 应按研究问题选择,并像任何检验统计量一样配合置换或零模型使用。

Output输出图

比较两张网络——已知节点对应的距离、谱距离、WL 图核,以及基于距离的置换检验:输出图 1

Output文字输出

距离        类内均值  类间均值  类内/类间
Jaccard       0.787     0.958      0.82
拉普拉斯谱         0.176     0.737      0.24
度分布JS         0.183     0.404      0.45
WL核           0.097     0.379      0.26
拉普拉斯谱距离的类间矩阵(行、列为 ER/WS/BA/SBM):
[[0.16 1.24 0.23 0.25]
 [1.24 0.11 1.22 1.17]
 [0.23 1.22 0.12 0.32]
 [0.25 1.17 0.32 0.2 ]]
组间−组内平均汉明距离=3.7,置换 p=0.0025

Code代码

# 生成 4 类网络(N=100、边数相同):ER、Watts–Strogatz、Barabási–Albert、随机块模型(SBM),每类 6 个实例。
# 距离:(a) Jaccard 边距离(需要节点对应);(b) 归一化拉普拉斯谱距离(不需要对应);
#       (c) 度分布的 Jensen–Shannon 散度;(d) Weisfeiler–Lehman(WL)子树特征的余弦距离(一种图核)。
# 用“类内平均距离 / 类间平均距离”衡量每种距离区分网络类型的能力(越小越好)。
# 最后对两组“被试网络”做基于距离的置换检验:统计量 = 组间平均距离 − 组内平均距离。
# 解读:节点无对应时,Jaccard 距离只在 WS 这类“共享格点骨架”的网络之间较小,对同类随机网络接近 1,不适用;谱距离、度分布与 WL 核关注的结构层次不同,
# 应按研究问题选择,并像任何检验统计量一样配合置换或零模型使用。
import numpy as np
import networkx as nx
from collections import Counter
from scipy.spatial.distance import jensenshannon

N, k = 100, 6
m = N * k // 2

def make(kind, s):
    if kind == "ER":
        return nx.gnm_random_graph(N, m, seed=s)
    if kind == "WS":
        return nx.watts_strogatz_graph(N, k, 0.1, seed=s)
    if kind == "BA":
        return nx.barabasi_albert_graph(N, k // 2, seed=s)
    sizes = [25] * 4                                    # SBM:4 个模块,块内连接更密
    pin = 0.17; pout = (2 * m / N - 24 * pin) / 75
    return nx.stochastic_block_model(sizes, [[pin if i == j else pout for j in range(4)] for i in range(4)], seed=s)

def jaccard(G, H):
    a, b = set(map(frozenset, G.edges())), set(map(frozenset, H.edges()))
    return 1 - len(a & b) / len(a | b)

def lap_spec(G):
    return np.sort(np.linalg.eigvalsh(nx.normalized_laplacian_matrix(G).toarray()))

def degree_js(G, H, kmax=60):
    def pk(G):
        c = np.bincount([d for _, d in G.degree()], minlength=kmax + 1)[:kmax + 1]
        return c / c.sum()
    return jensenshannon(pk(G), pk(H))

def wl_features(G, it=2):
    """WL 子树特征:所有节点初始标签相同,第 1 轮标签等价于度,第 2 轮编码“邻居度的多重集”。
    迭代轮数越多特征越细,随机网络之间几乎不再共享特征,距离趋近 1。"""
    G = G.copy()
    nx.set_node_attributes(G, {v: "0" for v in G}, "lab")
    h = nx.weisfeiler_lehman_subgraph_hashes(G, node_attr="lab", iterations=it)
    return Counter(x for hs in h.values() for x in hs)

def wl_dist(fa, fb):
    keys = set(fa) | set(fb)
    a = np.array([fa.get(x, 0) for x in keys], float); b = np.array([fb.get(x, 0) for x in keys], float)
    return 1 - a @ b / np.linalg.norm(a) / np.linalg.norm(b)

kinds = ["ER", "WS", "BA", "SBM"]
graphs = [(kd, make(kd, 10 * i + j)) for i, kd in enumerate(kinds) for j in range(6)]
specs = [lap_spec(G) for _, G in graphs]
wls = [wl_features(G) for _, G in graphs]
n = len(graphs)
D = {name: np.zeros((n, n)) for name in ("Jaccard", "拉普拉斯谱", "度分布JS", "WL核")}
for a in range(n):
    for b in range(a + 1, n):
        Ga, Gb = graphs[a][1], graphs[b][1]
        vals = {"Jaccard": jaccard(Ga, Gb), "拉普拉斯谱": np.linalg.norm(specs[a] - specs[b]),
                "度分布JS": degree_js(Ga, Gb), "WL核": wl_dist(wls[a], wls[b])}
        for name, v in vals.items():
            D[name][a, b] = D[name][b, a] = v

lab = np.array([kd for kd, _ in graphs])
same = (lab[:, None] == lab[None, :]) & ~np.eye(n, dtype=bool)
diff = lab[:, None] != lab[None, :]
print("距离        类内均值  类间均值  类内/类间")
for name, M in D.items():
    print(f"{name:<10} {M[same].mean():8.3f}  {M[diff].mean():8.3f}  {M[same].mean() / M[diff].mean():8.2f}")
print("拉普拉斯谱距离的类间矩阵(行、列为 ER/WS/BA/SBM):")
blk = np.array([[D['拉普拉斯谱'][np.ix_(lab == x, lab == y)].mean() for y in kinds] for x in kinds])
print(np.round(blk, 2))

# 基于距离的置换检验:两组各 12 个“被试网络”,节点对应已知;B 组模块内连接略强
rng = np.random.default_rng(51)
def subject(pin):
    pout = (2 * m / N - 24 * pin) / 75
    return nx.stochastic_block_model([25] * 4, [[pin if i == j else pout for j in range(4)] for i in range(4)],
                                     seed=int(rng.integers(1e9)))
subs = [subject(0.15) for _ in range(12)] + [subject(0.18) for _ in range(12)]
g = np.r_[np.zeros(12), np.ones(12)].astype(int)
A = np.array([nx.to_numpy_array(G, nodelist=range(N))[np.triu_indices(N, 1)] for G in subs])
Dm = np.array([[np.abs(x - y).sum() for y in A] for x in A])          # 汉明距离(边集合的对称差大小)
def stat(g):
    w = g[:, None] == g[None, :]
    off = ~np.eye(len(g), dtype=bool)
    return Dm[~w].mean() - Dm[w & off].mean()
s0 = stat(g)
null = np.array([stat(rng.permutation(g)) for _ in range(2000)])
print(f"组间−组内平均汉明距离={s0:.1f},置换 p={(1 + np.sum(null >= s0)) / 2001:.4f}")

# 作图:24 个网络两两之间的拉普拉斯谱距离矩阵;基于距离的置换检验零分布
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
im = ax[0].imshow(D["拉普拉斯谱"], cmap="viridis")
ax[0].set_xticks(np.arange(3, n, 6)); ax[0].set_xticklabels(kinds)
ax[0].set_yticks(np.arange(3, n, 6)); ax[0].set_yticklabels(kinds)
fig.colorbar(im, ax=ax[0], label="Laplacian spectral distance")
ax[0].set_title("Spectral distance separates network classes", fontsize=10)
ax[1].hist(null, bins=30, color="0.7", edgecolor="white", label="permutation null")
ax[1].axvline(s0, color="C3", lw=2, label="observed")
ax[1].set_xlabel("between - within mean Hamming distance"); ax[1].set_ylabel("count"); ax[1].legend(fontsize=8)
ax[1].set_title("Distance-based permutation test (SBM groups)", fontsize=10)
plt.tight_layout(); plt.show()

Run在浏览器中运行

首次加载 Python 环境约需 20–40 秒;可以修改参数后重新运行。

比较两张网络——已知节点对应的距离、谱距离、WL 图核,以及基于距离的置换检验