摘要:多层网络为「同一批节点在多个关系层面上的叠加」建图,时序网络为「边的生灭与时间秩序」建图。两层新口径各带来一次结论翻转:层间依赖把渐进退化改写成突然崩塌(一阶灾变);忽略时间秩序会让传播「提前发生」、可达性被高估。本节在体系中的位置:新翼第一件展品,把第 1 章的建图规程升级到多层与时间维。
离开主馆前清点一下隐含假设,你会发现前七章的口径欠了两笔账。第一笔:单层假设。社交平台的人既是好友图上的节点(关注层),又是通信图上的节点(消息层),还是资金图上的节点(转账层)——把三层压成一层,等于宣布「这三种关系可以互相顶替」。可它们分明不同:消息能到的地方好友未必到,资金能走的路径消息未必走。第二笔:静态假设。任何快照里的「不存在边」都只是「此刻不存在」——上周聊过天的两人这周没有边,但他们上周的信息交换已经发生了。把快照摞起来当永久结构,时间就欠账了。
新翼的第一件展品就是来还账的。多层网络给同一节点集配多个「层」:层内有各自的关系边,层间有依赖边(同一实体在不同层的身份连线,或跨层的功能依赖——电网层给通信层供电,通信层给电网层传调度指令)。时序网络把每条边打上时间戳,网络变成边的事件序列而非静态集合。两套口径解决「压层」与「压时间」各自的失真。
多层口径带来的第一个大发现令人不安。单看一层网络,故障通常是「渐坏」:节点逐步失联,巨连通分量平滑萎缩(第 5 章渗流的二阶相变)。把两层网络用依赖边焊在一起,灾变性质突变:一层里的局部故障沿依赖边传到另一层,另一层的失效又反过来加剧第一层——往返放大的结果是一阶相变:系统在临界点前几乎完好,跨过临界点瞬间全面崩塌,没有任何渐进预警。相互依赖网络的经典模型给出两个刺眼的读数:临界占据率比单层大幅提前(更脆弱),崩塌形式是跳变而非滑坡(更突然)。
真实现场对号入座:区域大停电的经典剧本常是「电网—通信/调度网」的相互依赖雪崩——电网局部故障让通信基站失电,调度信息中断又让电网无法正确切除故障段,往返数轮后整个系统跳崖。第 6.2 节的级联是「层内转嫁」,相互依赖是「层间转嫁」——两台引擎可以叠加,脆弱性按乘法结算。
import networkx as nx import random def interdependent_cascade(A, B, keep, seed=0, dep_mode="random"): """两层各保留比例为keep的节点,依赖边失败连坐,返回两层巨分量占比""" rng = random.Random(seed) dep = {v: rng.choice(list(B)) for v in A} # A层每个节点依赖B层一个节点 aliveA = set(rng.sample(list(A), int(A.number_of_nodes() * keep))) aliveB = set(B) - {dep[v] for v in A if v not in aliveA} # 依赖失效连坐 while True: # 层内渗流:各自取最大连通分量 mainA = aliveA and set(max(nx.connected_components(A.subgraph(aliveA)), key=len)) mainB = aliveB and set(max(nx.connected_components(B.subgraph(aliveB)), key=len)) if not mainA and not mainB: return 0.0, 0.0 # 层间连坐:不在对侧主分量里的依赖者死亡 newA = {v for v in mainA if dep[v] in (mainB or set())} newB = {dep[v] for v in newA} if newA == aliveA and newB == aliveB: break aliveA, aliveB = newA, newB nA, nB = A.number_of_nodes(), B.number_of_nodes() return len(aliveA) / nA, len(aliveB) / nB A = nx.gnp_random_graph(300, 6 / 299, seed=1) B = nx.gnp_random_graph(300, 6 / 299, seed=2) print("占据率 | A层巨分量 | B层巨分量(相互依赖双网)") for keep in [0.95, 0.9, 0.85, 0.8]: fa, fb = interdependent_cascade(A, B, keep, seed=3) print(f"{keep:6.2f} | {fa:9.2f} | {fb:9.2f}")
读数呈台阶状:占据率尚高时双层几乎完好,跌破某个水平直接跳崖归零——与单层渗流的光滑萎缩判然两途。层间依赖是系统性风险的放大器,监管语言里的「跨业态风险传染」说的就是它。
时序口径的要点是:边是事件,不是状态。「A 与 B 在某时刻有边」只在那段时间里能传信息;跨越更长的窗口,需要「时间尊重路径」——一条依次发生、时间严格递增的边序列。这个约束直接改写动力学的起点时间。经典读数:静态聚合图上的「平均最短路径」系统性地过短——它默认任何边随时可用,等于让信息搭乘时间机器;按时间尊重路径重算,真实可达距离更长、可达集合更小。聚合三失真登记在案:其一,传播提前(不存在的边被提前启用);其二,可达高估(时间上「回不去」的路被当作通路);其三,突发性丢失——人际与通信事件的时间分布呈重尾(长静默夹短爆发),聚合后变成均匀背景,动力学的触发节奏整个变形。此外,交互与关系的二分也在此显形:认识十年(关系边)与上周聊了一次(交互事件)在静态图里是同一条边,时序口径把它们拆开——这正是第 1 章二分投影问题的时间维版本。
import networkx as nx from collections import defaultdict def temporal_vs_static(events, src, horizon): """事件表 [(t, u, v)]:静态可达 vs 时间尊重可达""" G = nx.Graph() G.add_nodes_from({u for _, u, _ in events} | {v for _, _, v in events}) G.add_edges_from({(u, v) for _, u, v in events}) static_reach = nx.descendants(G, src) if G.has_node(src) else set() adj = defaultdict(list) for t, u, v in events: adj[u].append((t, v)) adj[v].append((t, u)) seen, frontier = {src}, [(0, src)] temporal_reach = set() while frontier: t0, node = frontier.pop() for t, nxt in adj[node]: if t >= t0 and nxt not in seen: seen.add(nxt); temporal_reach.add(nxt) frontier.append((t, nxt)) return len(static_reach), len(temporal_reach) events = [(1, "A", "B"), (2, "B", "C"), (3, "C", "D"), # 时序顺行链 (9, "D", "A"), # 时刻9才回连A (2, "C", "B"), (5, "A", "C")] static_n, temporal_n = temporal_vs_static(events, "A", horizon=10) print(f"静态聚合可达: {static_n} 个节点 | 时间尊重可达: {temporal_n} 个节点")
小例子里静态与时间尊重的可达集合已经分岔:聚合图声称 A 能立刻到的一切,时序上要等对应事件发生。规模上去后两类读数的差距会拉开数倍——「世界真小」的时间版本要重算。

两套口径各带一族新仪表,登记两件最常用的。多层侧:层间耦合强度扫描(依赖边逐条打开,观察巨分量与灾变阶数的变化)、层的角色分析(哪层是「骨架层」哪层是「服务层」)。时序侧:突发性系数(事件间隔分布的前后矩之比,衡量「时间上的肥尾」)、时间尊重距离与快照序列的动力学重放(把第 6 章的传播引擎直接架在事件流上而非聚合图上)。建模框架侧,活动驱动模型(节点按活动率随机唤醒、临时建边)给了时序网络一个可解析的最小模型,地位相当于静态世界的随机图——又一个「先造对照」的老传统。
⚠️ 常见坑:把多层网络当成「几个网络的列表」并联分析。层间依赖才是多层理论的全部价值——只在层内各算各的,等于没还「压层」这笔账。同理,时序分析若只在快照上做静态指标再取平均,也还不了「时间秩序」的账。
下一件展品换一个问题:既然结构写剧本,那牵住哪些节点就能改剧本——网络控制。