5D Library › 复杂系统相变、临界慢化与生态韧性的非线性动力学 · English
第八章 实践与预注册试验
注
本章的核查表是一个思考工具,不是工程规范、医疗方案或投资方法。 示例是一个虚构的小镇供水系统,所有名称与数字都是编造的。任何真实的水库、电网、医院或企业的风险评估,都必须由相应领域的专业人员依照当地规范进行。本章不提供医疗建议,也不作任何投资或经营建议。
第一部分 四步韧性与早期预警核查表
前七章的模型与证据可以整理成四个问题。它们帮助一个团队看清自己的系统离门槛有多远、失效会怎样传播、翻转之后要付出什么代价,而不是预报某个日期。
- 恢复时间与滚动自相关监测。 关键指标受扰动后恢复得是否越来越慢?用滚动窗口计算一阶自相关,换算成恢复时间 (第二章),同时看方差,并检查外界扰动本身是否变大。上升只是「值得查明原因」的提示,不是警报;判定规则事先写下,并在没有转变的时段上估计误报率(第三章)。
- 冗余容量 审计。 每个关键部件的容量比日常负荷多多少()?更重要的是:任何一个部件失效后,它的负荷能否被其余部件吸收(「N−1」检查,第五章)?最关键的冗余平时最像浪费(第四章)。
- 紧耦合与单点故障的隔舱化。 有没有一个部件一旦失效就切断一大块系统?哪些部分是紧耦合的、没有余量与延迟(第六章)?能否增加旁路、分隔模块,同时检查新增的切换装置是否带来新的复杂交互?
- 不可逆与滞后成本评估。 如果系统翻转了,要把压力退到多低才能恢复,需要多久(第一章)?现在离门槛还有多少余地?预防的代价通常是放弃一点日常的产量或效率;恢复的代价可能大得多,甚至不可能。
推导
把核查表用在虚构的「清溪镇」供水系统上:一座水库、三座水厂(P1—P3)、一个枢纽 M 与五个片区。第 1 步用合成的两年逐日叶绿素异常数据(恢复速率在第二年从每天 0.40 降到 0.15)计算每季度的一阶自相关与恢复时间;第 2 步对三座水厂做 N−1 检查(一座水厂停运时,负荷按其余水厂的余量比例转移,过载的水厂随之停运);第 3 步找出管网中的单点故障;第 4 步用第一章的湖泊模型()估计水库的安全余地与翻转后的恢复要求。
推导
# The four-step checklist on a FICTITIOUS small-town water system ("Clearbrook"): a reservoir,
# three treatment plants (P1-P3), a hub M and five districts. All names and numbers invented.
import math, random
rng = random.Random(15)
# Step 1. Rolling AC(1) of the reservoir's daily chlorophyll anomaly (synthetic: the recovery
# rate falls from 0.40 to 0.15 per day during year 2), and recovery time -1/ln AC(1).
x, days = 0.0, []
for d in range(730):
lam = 0.40 if d < 365 else 0.40 - 0.25 * (d - 365) / 365
x += -lam * x + 0.5 * rng.gauss(0, 1); days.append(x)
def ac1(w):
m = sum(w) / len(w); v = sum((u - m) ** 2 for u in w)
return sum((w[i] - m) * (w[i + 1] - m) for i in range(len(w) - 1)) / v
q_ac = [ac1(days[q * 91 - 90:q * 91]) for q in range(1, 9)]
print("Step 1 quarter " + " ".join(f" Q{q} " for q in range(1, 9)))
print(" AC(1), 90 days " + " ".join(f"{a:.2f}" for a in q_ac))
print(" recovery (days) " + " ".join(f"{-1 / math.log(a):4.1f}" for a in q_ac))
s = sum((q_ac[j] > q_ac[i]) - (q_ac[j] < q_ac[i]) for i in range(8) for j in range(i + 1, 8))
ratio = -1 / math.log(q_ac[7]) / (sum(-1 / math.log(a) for a in q_ac[:4]) / 4)
print(f" Kendall tau over 8 quarters {s / 28:+.2f}; recovery time Q8 / mean(Q1-Q4)"
f" = {ratio:.1f}\n -> REVIEW (look for a cause; not an alarm)")
# Step 2. N-1 check of spare capacity alpha = C/L - 1: a failed plant's load moves to the
# others in proportion to their spare capacity; a plant pushed above capacity fails in turn.
def n_minus_1(load, cap):
out = {}
for first in load:
down = {first}
while True:
up = [v for v in load if v not in down]
moved = sum(load[v] for v in down); spare = sum(cap[v] - load[v] for v in up)
over = [v for v in up if spare <= 0 or moved * (cap[v] - load[v]) / spare
> cap[v] - load[v] + 1e-9]
down |= set(over)
if not over or len(over) == len(up): break
out[first] = len(down)
return out
load, cap = {"P1": 60, "P2": 40, "P3": 30}, {"P1": 70, "P2": 70, "P3": 45}
print("Step 2 alpha: " + ", ".join(f"{v} {cap[v] / load[v] - 1:.2f}" for v in load)
+ f"; total spare {sum(cap.values()) - sum(load.values())}, largest load 60")
print(" plants down if one fails: " + str(n_minus_1(load, cap)))
cap["P2"] += 20; cap["P3"] += 15
print(" after P2 +20, P3 +15: " + str(n_minus_1(load, cap)))
# Step 3. Single points of failure: which one node, if lost, cuts a district off all plants?
links = [("P1", "M"), ("P2", "M"), ("P2", "E"), ("P3", "W"), ("M", "N"), ("M", "E"),
("M", "S"), ("M", "W"), ("S", "T"), ("W", "T")]
def cut_off(links, lost):
nb = {}
for a, b in links:
if lost not in (a, b): nb.setdefault(a, set()).add(b); nb.setdefault(b, set()).add(a)
seen = {p for p in ("P1", "P2", "P3") if p != lost}; st = list(seen)
while st:
for w in nb.get(st.pop(), ()):
if w not in seen: seen.add(w); st.append(w)
return [d for d in "NESWT" if d != lost and d not in seen]
nodes = sorted({v for l in links for v in l})
print("Step 3 single points of failure: " + str({v: cut_off(links, v) for v in nodes
if cut_off(links, v)}))
links.append(("E", "N"))
print(" after adding link E-N: " + str({v: cut_off(links, v) for v in nodes
if cut_off(links, v)}))
# Step 4. Hysteresis cost of the reservoir (lake model of Chapter 1, loss rate b = 0.8).
a_now, a_on, a_off = 0.42, 0.510, 0.144
print(f"Step 4 loading {a_now} vs tipping point {a_on}: safety margin {1 - a_now / a_on:.0%}")
print(f" once turbid, loading must fall to {a_off} ({1 - a_off / a_now:.0%} below today)")
# Step 1 quarter Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8
# AC(1), 90 days 0.40 0.52 0.63 0.60 0.71 0.63 0.84 0.73
# recovery (days) 1.1 1.5 2.2 2.0 2.9 2.1 5.9 3.1
# Kendall tau over 8 quarters +0.71; recovery time Q8 / mean(Q1-Q4) = 1.8
# -> REVIEW (look for a cause; not an alarm)
# Step 2 alpha: P1 0.17, P2 0.75, P3 0.50; total spare 55, largest load 60
# plants down if one fails: {'P1': 3, 'P2': 3, 'P3': 1}
# after P2 +20, P3 +15: {'P1': 1, 'P2': 1, 'P3': 1}
# Step 3 single points of failure: {'M': ['N']}
# after adding link E-N: {}
# Step 4 loading 0.42 vs tipping point 0.51: safety margin 18%
# once turbid, loading must fall to 0.144 (66% below today)第 1 步:八个季度的一阶自相关从 0.40 升到 0.73(第七季度为 0.84),肯德尔 ,最近一季的恢复时间是第一年平均值的 1.8 倍。结论是「复查」:查明原因(营养输入是否增加、监测方法是否改变),而不是宣布水库即将翻转;只有八个季度的数据,第三章的模拟提示,这样的趋势在什么都没变的系统中也不罕见。第 2 步:三座水厂的 分别为 0.17、0.75 与 0.50,总容量比总负荷多 42%,但总余量 55 小于最大水厂的负荷 60:P1 或 P2 停运时,三座全部停运。给 P2 增加 20、P3 增加 15 的容量之后,任何一座停运都只影响它自己。第 3 步:枢纽 M 是单点故障,它失效时 N 片区与所有水厂断开;增加 E—N 一条连接后,不再有单点故障。第 4 步:当前负荷 0.42,离翻转门槛 0.51 还有 18% 的余地;一旦翻转,负荷要降到 0.144 才能恢复,比现在低 66%。这个不对称就是滞后的成本。数字都是编造的,只用来说明计算方法。
第二部分 预注册随机对照试验方案
问题
在一个管理者不知道系统结构、其中存在一个隐藏折叠点的资源管理任务中,除了当前存量与当期收益之外,再给管理者看一个「滚动恢复时间与方差」的早期预警仪表盘,能否减少资源的不可逆崩溃?要付出多少日常收益的代价?
已有的实验提示这个问题值得问。莫克斯内斯让 83 位主要来自挪威渔业部门的参与者各自独立管理一个模拟鱼群:每人拥有独占的产权,排除了「公地悲剧」,他们仍普遍过度投资(捕捞能力比需要的多约 60%),把鱼群压到最优水平以下约 15%;他归因于对动态系统中反馈的错误感知 (Moxnes 1998)。巴雷特与丹嫩贝格在气候谈判实验中发现,灾难性门槛的位置确知时,集体行动几乎总能避免越过它;位置不确定时,合作就会瓦解 (Barrett and Dannenberg 2012)。问题是,第二章的指标能否部分替代「知道门槛在哪里」。
设计
| 要素 | 规定 |
|---|---|
| 设计 | 两组平行、以个人为单位随机化的对照试验,按实施地点分层,1:1 分配 |
| 参与者 | 年满 18 岁的成年人,通过大学与研究机构的公开招募参加,按实验经济学惯例获得与任务收益挂钩的报酬;不从作者的社群、学员或客户中招募 |
| 排除 | 受过临界转变或早期预警专门训练者;曾参加本研究预试验者 |
| 任务 | 80 轮的计算机资源管理任务。存量按隐藏模型变化(参与者不知道模型与它的折叠点):;参与者每 4 轮设定一次努力 ;每轮收益 = 收获 。规则写明:存量一旦低于 2.0,渔场永久关闭 |
| 两组共同的界面 | 当前存量、当期收益与累计收益;任务说明相同 |
| A 组(仪表盘) | 另加仪表盘:用最近 20 轮存量计算的滚动恢复时间(由一阶自相关换算)与滚动方差,以折线显示;附两分钟的中性说明(「恢复变慢、波动变大可能表示资源对扰动更敏感」),不提示门槛位置或建议的努力水平 |
| B 组(对照) | 只有共同界面 |
| 盲法 | 参与者知道自己的界面,但不知道研究假设与另一组的界面;主要结局由程序自动计算;分析人员在分析计划锁定前不知道组别 |
| 主要结局 | 不可逆崩溃:80 轮内存量是否低于 2.0(按规则即永久关闭),每人一个二元结局 |
| 次要结局 | 累计收益;崩溃的轮次;最后 20 轮的平均努力;仪表盘上升时的努力调整;任务后对系统的理解(由不知组别的评分者编码) |
| 分析 | 逻辑斯谛回归:崩溃 ~ 组别 + 实施地点分层,报告风险差及 95% 置信区间;双侧 |
主要结局只设一项。收益作为最重要的次要结局报告,以便看清预警的日常代价。
样本量
推导
# Chapter 8 trial. Part 1: the simulated resource game, played by two simple rule-following
# bots (a MODEL of the hypothesis, not data about people). Part 2: sample size.
# Hidden stock dynamics per round (participants are not told the model or its fold):
# x <- x + 0.5 [x (1 - x/10) - E x^2/(x^2 + 1)] + 0.1 N(0,1); fold at E = 2.604.
# Profit per round = catch - 0.2 E. Collapse (primary outcome): x < 2.0 in any round; by the
# game's rules the fishery is then closed for good. 80 rounds; effort E reviewed every 4 rounds.
import math, random
from statistics import NormalDist
rng = random.Random(16)
def play(dashboard, rounds=80, heed=0.3):
"""A bot that nudges effort up or down by its own step, reversing when the block's profit
falls, and stops exploring once profit per round reaches its own aspiration. The dashboard
bot also looks at lag-1 autocorrelation of the last 20 stock values (with probability heed
at each review) and cuts effort by 0.3 if it exceeds 0.6."""
x, E, step = 8.9, rng.uniform(0.5, 2.0), rng.uniform(0.05, 0.3)
asp, stopped, last, hold = rng.uniform(0.6, 0.9), False, None, 0
xs, block, total = [], 0.0, 0.0
for t in range(1, rounds + 1):
catch = 0.5 * E * x * x / (x * x + 1)
x += 0.5 * (x * (1 - x / 10)) - catch + 0.1 * rng.gauss(0, 1)
xs.append(x); block += catch - 0.2 * E; total += catch - 0.2 * E
if x < 2.0: return True, total
if t % 4: continue
if not stopped and block / 4 >= asp: stopped = True
if last is not None and block < last: step = -step # profit fell: reverse
last, block = block, 0.0
if dashboard and len(xs) >= 20 and rng.random() < heed:
w = xs[-20:]; m = sum(w) / 20; v = sum((u - m) ** 2 for u in w)
a = sum((w[i] - m) * (w[i + 1] - m) for i in range(19)) / v
if a > 0.6: E, step, hold = max(0.0, E - 0.3), -abs(step), 3
if hold: hold -= 1; continue
if not stopped: E = max(0.0, E + step)
return False, total
for dash, lab in ((False, "stock and profit only"), (True, "plus recovery dashboard")):
res = [play(dash) for _ in range(2000)]
print(f"bots, {lab:24s}: collapse in {sum(c for c, _ in res) / 2000:.0%} of games;"
f" mean profit {sum(p for _, p in res) / 2000:.1f}")
def n_per_arm(p1, p2, alpha=0.05, power=0.90):
z = NormalDist().inv_cdf; pb = (p1 + p2) / 2
num = (z(1 - alpha / 2) * math.sqrt(2 * pb * (1 - pb))
+ z(power) * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)))
return math.ceil(num ** 2 / (p1 - p2) ** 2)
print("assumed collapse share: control -> dashboard analysable per arm (90 % power)")
for p1, p2 in ((0.40, 0.25), (0.40, 0.28), (0.50, 0.35), (0.30, 0.18)):
print(f" {p1:.2f} -> {p2:.2f} {n_per_arm(p1, p2)}")
n = n_per_arm(0.40, 0.25); rec = math.ceil(n / 0.9)
print(f"chosen 0.40 -> 0.25: {n} per arm; allowing 10 % incomplete sessions,")
print(f" recruit {rec} per arm, {2 * rec} in all")
hits = 0
for _ in range(4000):
a = sum(rng.random() < 0.40 for _ in range(n)); b = sum(rng.random() < 0.25 for _ in range(n))
pa, pb = a / n, b / n; pp = (a + b) / (2 * n)
hits += abs(pa - pb) / math.sqrt(pp * (1 - pp) * 2 / n) > 1.96
print(f"simulation check of power with {n} per arm: {hits / 4000:.2f}")
# bots, stock and profit only : collapse in 40% of games; mean profit 42.3
# bots, plus recovery dashboard : collapse in 7% of games; mean profit 35.3
# assumed collapse share: control -> dashboard analysable per arm (90 % power)
# 0.40 -> 0.25 203
# 0.40 -> 0.28 326
# 0.50 -> 0.35 227
# 0.30 -> 0.18 265
# chosen 0.40 -> 0.25: 203 per arm; allowing 10 % incomplete sessions,
# recruit 226 per arm, 452 in all
# simulation check of power with 203 per arm: 0.90程序第一部分不是数据,而是对假说的一个模型:两种按简单规则行事的「机器人」玩家。只看存量与收益的机器人在 40% 的游戏中使资源崩溃;同样的机器人若在每次复查时以 30% 的概率参考仪表盘(自相关超过 0.6 就把努力降低 0.3),崩溃降到 7%,累计收益则从 42.3 降到 35.3。这个差距是一个上限:机器人对仪表盘的反应是设计好的,隐藏模型中也确实有折叠点。真实的人可能误读、忽视或过度反应。
所以样本量按保守得多的假设计算:对照组崩溃 40%,仪表盘组 25%,双侧 ,把握度 90%:每组需要 203 名可分析的参与者;考虑约 10% 的会话不完整,每组招募 226 人,共 452 人;模拟检验的把握度为 0.90。若真实差距只有 12 个百分点(40% 对 28%),每组需要 326 人。这些参数都是假设值,正式实施前应当用预试验的数据核对。
判定规则
推导
# Decision rule for the Chapter 8 trial, fixed before any data are collected.
# diff = collapse share in the control arm (stock and profit only) minus the dashboard arm
# (plus rolling recovery time and variance); POSITIVE diff = fewer collapses with the dashboard.
# 95 % CI from the pre-specified model (logistic regression, arm + site stratum, reported as a
# risk difference). Smallest difference of interest: 0.10 (10 percentage points).
SESOI = 0.10
def verdict(diff, lo, hi, shown, profit_change):
if shown < 0.90:
return (f"diff {diff:+.2f}: NOT INTERPRETABLE - dashboard shown as specified"
f" in only {shown:.0%} of sessions")
if lo >= SESOI: v = "dashboard cuts collapses by >= 10 points"
elif lo > 0: v = "dashboard cuts collapses; >= 10 points not shown"
elif hi < 0: v = "dashboard INCREASES collapses"
elif lo > -SESOI and hi < SESOI: v = "no difference worth acting on"
else: v = "unclear: the interval is too wide to decide"
return f"diff {diff:+.2f} ({lo:+.2f} to {hi:+.2f}): {v}; profit {profit_change:+.0%}"
for case in ((0.19, 0.11, 0.27, 0.96, -0.08), (0.08, 0.01, 0.15, 0.95, -0.03),
(0.02, -0.05, 0.09, 0.94, 0.00), (-0.09, -0.17, -0.01, 0.95, 0.05),
(0.21, 0.13, 0.29, 0.80, -0.10), (0.05, -0.08, 0.18, 0.93, -0.02)):
print(verdict(*case))
print("profit and time to collapse are secondary: reported with intervals, never used to")
print(" override the primary verdict")
# diff +0.19 (+0.11 to +0.27): dashboard cuts collapses by >= 10 points; profit -8%
# diff +0.08 (+0.01 to +0.15): dashboard cuts collapses; >= 10 points not shown; profit -3%
# diff +0.02 (-0.05 to +0.09): no difference worth acting on; profit +0%
# diff -0.09 (-0.17 to -0.01): dashboard INCREASES collapses; profit +5%
# diff +0.21: NOT INTERPRETABLE - dashboard shown as specified in only 80% of sessions
# diff +0.05 (-0.08 to +0.18): unclear: the interval is too wide to decide; profit -2%
# profit and time to collapse are secondary: reported with intervals, never used to
# override the primary verdict示例结果是编造的。仪表盘没有按规定显示的会话超过 10% 时,无论结果如何,报告都写「不可解释」。只有当崩溃比例之差的 95% 置信区间下限不低于 10 个百分点时,报告才写「仪表盘使崩溃至少减少了最小关注的幅度」。收益与崩溃轮次只报告估计与区间,不用来推翻主要结论。
这项试验不能说明什么
第一,这是 80 轮的计算机任务,真实的资源管理以年和几十年计,涉及多方利益、政治与制度;结论不能直接外推到真实的渔业、湖泊或任何企业。第二,隐藏模型中确实有折叠点、噪声恒定,仪表盘的信号在这里是有效的;第三章说明真实系统中同样的指标常常误报或漏报。阳性结果只说明「在信号有效时人们能否利用它」。一个自然的后续试验是加入一个没有折叠点的隐藏模型,检验仪表盘在那里是否会误导人们放弃收益。第三,「永久关闭」是游戏规则造成的不可逆;在模型本身中,压力降到下方折叠点以下时资源可以恢复。第四,A 组同时得到额外的信息与一段说明,阳性结果不能区分是仪表盘的内容起了作用,还是「有人提醒我注意风险」起了作用。第五,参与者多数不是专业的资源管理者。第六,只用了一组模型参数。
伦理与利益冲突
这是一项低风险研究:任务是计算机上的模拟游戏,不涉及任何健康干预,也不收集健康信息,可预见的不适只是资源「崩溃」时的挫败感与报酬较少。即便如此,试验仍须经独立伦理委员会审查批准,每位参与者签署知情同意,并在第一位参与者入组之前登记方案与分析计划 (Nosek et al. 2018)。任务说明如实告诉参与者这是一个简化的模拟、规则中包括永久关闭;不告诉参与者隐藏模型的细节,这属于信息的保留而不是欺骗,任务结束后向所有参与者说明研究目的与模型。报酬规则事先公布;任何参与者可以随时退出而不需说明理由,并获得已完成部分的报酬。数据匿名保存。无论结果正负,都将发表。
利益披露
作者以 5D Library 的著作(包括本书)作为其社群课程、升维牌卡(5D Ascension Cards,Dimensional Shift Card Decks)与 Yafu AI 知识库的方法论基础,因此与本书主题有商业利益关系。本书不推销任何课程、牌卡或服务;第八章的试验设计为由独立于作者企业的研究人员实施和分析,不从作者的社群、学员或客户中招募;书中所有代码与研究方案均公开,供任何人检验。为限制利益的影响:任务程序与分析代码在入组前公开;无论结果如何,结果、数据和代码都将公开。
参考文献
Fan, Y. (2026). Phase Transitions, Critical Slowing Down and Resilience in Complex Dynamical Systems: From Saddle-Node Bifurcations and Early-Warning Signals to Cascading Failures, Read Beside Classical Chinese Thought on Incipient Change [复杂系统相变、临界慢化与生态韧性的非线性动力学] (Version 1.0.0). 5D Library. https://doi.org/10.5281/zenodo.23770082 · Licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0).