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

比例阈值与密度匹配——整体连接强度差异如何“制造”拓扑差异

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

两组模拟被试拥有完全相同的模块结构,只是 B 组的系统内信号整体更弱(例如头动更大或信噪比更低)。 比较三种做法下的组间差异:(1) 绝对阈值;(2) 比例阈值(各被试保留最强的前 10% 边); (3) 在 10%–30% 密度范围上积分(曲线下面积),并把聚集系数除以同度序列随机网络的值做归一化。 依次模拟两种情形:B 组信号略弱、B 组信噪比明显更低。 解读:绝对阈值下的“差异”主要来自密度不同;比例阈值消除了密度差。信号略弱时比例阈值下差异基本消失; 信噪比明显更低时,B 组最强的 10% 边中混入更多接近噪声的边,网络向随机网络靠拢,差异重新出现 (本例中在归一化聚集系数的 AUC 上最明显)—— 这时的差异来源是信噪比,而不是“拓扑组织”本身不同。最后演示 MST 骨架 + 比例补边保证连通。

Output输出图

比例阈值与密度匹配——整体连接强度差异如何“制造”拓扑差异:输出图 1

Output文字输出

B 组系统内信号载荷=0.6;组平均连接强度 {'A': 0.089, 'B': 0.062}
  绝对阈值 r>0.2:密度              A=0.233  B=0.199  t=+10.24  p=5.66e-11
  绝对阈值 r>0.2:聚集系数            A=0.984  B=0.848  t=+9.71  p=1.82e-10
  比例阈值 10%:聚集系数              A=0.517  B=0.498  t=+0.87  p=0.391
  比例阈值 10%:已知划分模块度           A=0.714  B=0.720  t=-0.82  p=0.42
  密度10%–30% 归一化C的AUC         A=0.897  B=0.875  t=+0.93  p=0.361
B 组系统内信号载荷=0.45;组平均连接强度 {'A': 0.089, 'B': 0.041}
  绝对阈值 r>0.2:密度              A=0.232  B=0.076  t=+33.71  p=3.27e-24
  绝对阈值 r>0.2:聚集系数            A=0.981  B=0.343  t=+22.17  p=2.67e-19
  比例阈值 10%:聚集系数              A=0.478  B=0.436  t=+1.70  p=0.101
  比例阈值 10%:已知划分模块度           A=0.698  B=0.687  t=+1.07  p=0.296
  密度10%–30% 归一化C的AUC         A=0.897  B=0.655  t=+9.14  p=6.72e-10
B 组被试 1:比例阈值 10% 连通=False,MST+比例阈值 连通=True,边数 78 -> 81

Code代码

# 两组模拟被试拥有完全相同的模块结构,只是 B 组的系统内信号整体更弱(例如头动更大或信噪比更低)。
# 比较三种做法下的组间差异:(1) 绝对阈值;(2) 比例阈值(各被试保留最强的前 10% 边);
# (3) 在 10%–30% 密度范围上积分(曲线下面积),并把聚集系数除以同度序列随机网络的值做归一化。
# 依次模拟两种情形:B 组信号略弱、B 组信噪比明显更低。
# 解读:绝对阈值下的“差异”主要来自密度不同;比例阈值消除了密度差。信号略弱时比例阈值下差异基本消失;
# 信噪比明显更低时,B 组最强的 10% 边中混入更多接近噪声的边,网络向随机网络靠拢,差异重新出现
# (本例中在归一化聚集系数的 AUC 上最明显)——
# 这时的差异来源是信噪比,而不是“拓扑组织”本身不同。最后演示 MST 骨架 + 比例补边保证连通。
import numpy as np
import networkx as nx
from scipy import stats

rng = np.random.default_rng(5)
n_mod, per, T, n_sub = 4, 10, 200, 15
N = n_mod * per
labels = np.repeat(np.arange(n_mod), per)
iu = np.triu_indices(N, 1)

def subject_R(strength):
    sys_sig = rng.standard_normal((n_mod, T))
    X = strength * sys_sig[labels] + rng.standard_normal((N, T))
    R = np.corrcoef(X)
    np.fill_diagonal(R, 0)
    return R

def threshold_abs(R, tau):
    return nx.from_numpy_array((R > tau).astype(int))

def threshold_prop(R, density):
    k = int(round(density * len(iu[0])))
    w = R[iu]
    keep = np.argsort(-w)[:k]
    G = nx.Graph()
    G.add_nodes_from(range(R.shape[0]))
    G.add_edges_from(zip(iu[0][keep], iu[1][keep]))
    return G

def report(name, fA, fB):
    a, b = np.array(fA), np.array(fB)
    t, pv = stats.ttest_ind(a, b)
    print(f"  {name:<26} A={a.mean():.3f}  B={b.mean():.3f}  t={t:+.2f}  p={pv:.3g}")

def norm_clustering(G, n_null=3, seed=0):
    """聚集系数除以同度序列随机网络(networkx 内置双边交换)的平均聚集系数。"""
    m = G.number_of_edges()
    c_rand = []
    for s in range(n_null):
        H = G.copy()
        nx.double_edge_swap(H, nswap=5 * m, max_tries=100 * m, seed=seed + s)
        c_rand.append(nx.average_clustering(H))
    return nx.average_clustering(G) / np.mean(c_rand)

grid = np.arange(0.10, 0.31, 0.05)
def auc_norm_c(R, seed):
    vals = np.array([norm_clustering(threshold_prop(R, d), seed=seed) for d in grid])
    return float(np.sum((vals[1:] + vals[:-1]) / 2 * np.diff(grid)))   # 梯形法积分

partition = [set(np.where(labels == k)[0]) for k in range(n_mod)]
for strength_B in (0.6, 0.45):            # 情形一:B 组信号略弱;情形二:B 组信噪比明显更低
    groups = {"A": [subject_R(0.8) for _ in range(n_sub)],
              "B": [subject_R(strength_B) for _ in range(n_sub)]}
    mean_fc = {g: round(float(np.mean([R[iu].mean() for R in Rs])), 3) for g, Rs in groups.items()}
    print(f"B 组系统内信号载荷={strength_B};组平均连接强度 {mean_fc}")
    report("绝对阈值 r>0.2:密度", [nx.density(threshold_abs(R, 0.2)) for R in groups["A"]],
           [nx.density(threshold_abs(R, 0.2)) for R in groups["B"]])
    report("绝对阈值 r>0.2:聚集系数", [nx.average_clustering(threshold_abs(R, 0.2)) for R in groups["A"]],
           [nx.average_clustering(threshold_abs(R, 0.2)) for R in groups["B"]])
    report("比例阈值 10%:聚集系数", [nx.average_clustering(threshold_prop(R, 0.10)) for R in groups["A"]],
           [nx.average_clustering(threshold_prop(R, 0.10)) for R in groups["B"]])
    report("比例阈值 10%:已知划分模块度",
           [nx.community.modularity(threshold_prop(R, 0.10), partition) for R in groups["A"]],
           [nx.community.modularity(threshold_prop(R, 0.10), partition) for R in groups["B"]])
    report("密度10%–30% 归一化C的AUC", [auc_norm_c(R, 10 * s) for s, R in enumerate(groups["A"])],
           [auc_norm_c(R, 500 + 10 * s) for s, R in enumerate(groups["B"])])

# 最小生成树(MST)骨架 + 比例补边:保证每个被试的网络连通
R = groups["B"][0]
T_mst = nx.maximum_spanning_tree(nx.from_numpy_array(np.clip(R, 0, None)))
G_prop = threshold_prop(R, 0.10)
G_union = nx.compose(T_mst, G_prop)
print(f"B 组被试 1:比例阈值 10% 连通={nx.is_connected(G_prop)},"
      f"MST+比例阈值 连通={nx.is_connected(G_union)},边数 {G_prop.number_of_edges()} -> {G_union.number_of_edges()}")

# 作图(情形二的被试):绝对阈值下聚集系数几乎完全跟着密度走;比例阈值消除了密度差
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
for g, mk in (("A", "o"), ("B", "s")):
    Gs = [threshold_abs(R, 0.2) for R in groups[g]]
    ax[0].plot([nx.density(H) for H in Gs], [nx.average_clustering(H) for H in Gs], mk, label=f"group {g}")
ax[0].set_xlabel("density (absolute threshold r > 0.2)"); ax[0].set_ylabel("average clustering")
ax[0].set_title("Absolute threshold: clustering tracks density", fontsize=10); ax[0].legend()
cA = [nx.average_clustering(threshold_prop(R, 0.10)) for R in groups["A"]]
cB = [nx.average_clustering(threshold_prop(R, 0.10)) for R in groups["B"]]
ax[1].boxplot([cA, cB]); ax[1].set_xticklabels(["group A", "group B (low SNR)"])
ax[1].set_ylabel("average clustering (top 10% edges)")
ax[1].set_title("Proportional threshold: equal density, residual SNR effect", fontsize=10)
plt.tight_layout(); plt.show()

Run在浏览器中运行

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

比例阈值与密度匹配——整体连接强度差异如何“制造”拓扑差异