Python蒙特卡洛模拟遍历性博弈:揭示均值与时间路径的背离

发布时间:2026/8/31 12:47:51
Python蒙特卡洛模拟遍历性博弈:揭示均值与时间路径的背离 刚开始接触“遍历性”这个概念时我一度以为它只是概率论教科书里的一个抽象术语。直到设计随机过程实验时遇到了一个非常反直觉的现象某个博弈的期望收益明明是正的但绝大多数参与者在长期重复后都在亏损。这种整体平均值与个体时间路径之间的分离正是“The Ergodicity Game”试图讲清楚的问题。这篇文章将从数学模型到 Python 蒙特卡洛模拟完整实现一遍遍历性博弈。你可以把它看成一次实验复盘也可以把它当成一个可运行的入门项目。文章会覆盖环境准备、代码实现、结果分析和常见误区适合对随机过程、量化建模、资产配置感兴趣的开发者。1. Ergodicity Game 是什么1.1 从一个抛硬币的游戏说起假设我们设计一个非常简单的博弈游戏你有一个初始财富记为 1。每局抛一次硬币。硬币为正面时你的财富变为原来的 1.5 倍也就是增加 50%。硬币为反面时你的财富变为原来的 0.6 倍也就是减少 40%。游戏可以一直重复玩下去。直观来看赢的时候赚 50%输的时候只亏 40%似乎是一个稳赚不赔的规则。而且每一局的数学期望是[ E 0.5 \times 1.5 0.5 \times 0.6 1.05 ]换句话说单局预期收益率是正的 5%。如果只看这个数字你会觉得玩得越多赚得越多。但如果你真的用程序去模拟几百个人各自玩 1000 局最后会发现大多数人的财富不仅没有增长反而朝 0 靠近。这就是遍历性博弈的经典案例也是最反直觉的地方整体期望为正不代表单个玩家沿时间轴走下去也能获得正收益。1.2 两种平均值两条增长路径在统计物理和随机过程中我们经常区分两种平均值。第一种叫“总体平均”也叫系综平均英文是 Ensemble Average。它表示在同一时刻、同一规则下让足够多的人同时玩这个游戏然后把所有人的财富取一个平均值。这个平均值会随时间上升因为它把极少数非常幸运的人拉到很高的位置从而拉高了整体平均水平。第二种叫“时间平均”英文是 Time Average。它表示一个人长期重复玩这个游戏把自己的财富变化沿时间取一个平均趋势。由于每一轮收益是以乘法的形式累积的长期结果更接近几何平均值而不是算术平均值。在这个游戏中[ \text{几何平均} \sqrt{1.5 \times 0.6} \sqrt{0.9} \approx 0.949 ]也就是每一轮的时间平均增长率约为 0.949小于 1长期趋势是缩水的。当两个平均值不一致时我们称这个系统具有“遍历性缺失”或者说系统不是遍历的。遍历性博弈研究的就是这种缺失造成的影响一个系统在总体层面上可以表现良好但单个个体长期参与却可能走向毁灭这种差异在金融投资、经济决策、进化生物学等领域都有深远影响。1.3 为什么需要代码验证公式推导虽然简洁但理解起来有些门槛。更关键的是当博弈规则变得复杂比如加入再平衡策略、杠杆、随机波动率之后手算就会非常费力。用 Python 做蒙特卡洛模拟的主要价值有三个第一它能让我们直观地看到“总体均值的上升”和“个体中位数的下降”同时发生。第二通过调整参数比如收益率、亏损率、参与人数、博弈轮数可以快速探索不同场景。第三模拟过程本身就是一种模型验证能让数学推导的结果以图像方式呈现方便向同事或客户解释。接下来我会搭建一个最小可运行的 Python 模拟项目把 The Ergodicity Game 的完整过程跑通。2. 环境准备与项目结构2.1 运行环境本文的模拟代码基于 Python 编写核心依赖只有 NumPy 和 Matplotlib。如果你已经安装了 Anaconda 或 Miniconda通常自带这两个库可以直接运行。操作系统Windows 10 / 11、macOS、Linux 均可以。Python 版本建议 3.9 或更高版本本文示例不依赖过高版本特性。NumPy负责随机数生成和矩阵运算。Matplotlib负责结果可视化。终端或 IDE推荐 VS Code或者直接用 Jupyter Notebook。如果你的环境还没有安装这两个库可以执行pip install numpy matplotlib版本不需要完全一致只要能正常导入即可。如果运行中遇到兼容性报错优先把库升级到当前环境支持的最新稳定版。2.2 项目目录结构为了便于阅读和扩展建议创建一个独立的项目目录ergodicity_game/ ├── main.py ├── models.py ├── visualize.py ├── config.py └── README.mdmodels.py负责核心模拟函数。visualize.py负责画图。config.py存放可调参数。main.py是入口脚本。README.md记录项目说明。真实项目里不一定要拆得很细但把模拟逻辑和可视化逻辑分开后续做参数实验会方便很多。本文为了演示完整代码会把模拟逻辑封装成函数然后在main.py中调用。3. 核心数学模型3.1 单轮收益定义定义一个博弈规则需要四个参数初始财富start_wealth每局获胜概率win_rate获胜时财富倍数up失败时财富倍数down在经典模型中win_rate0.5up1.5down0.6。如果玩家财富为W那么玩一局之后[ W_{t1} W_t \times \begin{cases} up, \text{概率为 } p \ down, \text{概率为 } 1-p \end{cases} ]这个模型有一个重要前提每一轮只按比例变化不考虑投入本金、消费、杠杆等外部因素。它属于乘性随机游走模型是最常用的财富增长模型之一。3.2 算术期望与几何期望先看算术期望。玩 n 局之后如果只计算理论上的均值那么[ E(W_n) W_0 \times [p \cdot up (1-p) \cdot down]^n ]因为每一局的线性期望是独立的可以把p*up (1-p)*down连乘 n 次。再看时间平均。如果用一个典型的玩家路径来代表长期趋势由于乘法的存在更合理的是计算对数收益率[ \frac{1}{n} \ln \left( \frac{W_n}{W_0} \right) p \ln(up) (1-p) \ln(down) ]当 ( p \ln(up) (1-p) \ln(down) 0 ) 时长期增长率为正反之则为负。把本案例的数值带进去[ 0.5 \cdot \ln(1.5) 0.5 \cdot \ln(0.6) \approx 0.5 \times (0.4055 - 0.5108) \approx -0.0526 ]这说明典型的长期增长率为负时间平均收益率约为每局 -5.26%。这就是“看起来赚钱实际长期亏损”的数学根源。3.3 遍历性临界条件如果令价格上涨倍数为 ( a )下跌倍数为 ( b )获胜概率为 ( p )那么时间平均是否为正取决于[ p \ln a (1-p) \ln b ]这个值大于 0 的时候长期时间平均收益为正等于 0 的时候长期不赚不亏小于 0 的时候即使总体期望为正长期也会亏钱。我们可以用这个临界公式快速判断不同参数。比如把上涨倍数改成 1.2、下跌倍数改成 0.8那么[ 0.5 \cdot \ln(1.2) 0.5 \cdot \ln(0.8) 0.5 \times \ln(0.96) \approx -0.0204 ]仍然是负的。只有当上涨倍数和下跌倍数的乘积大于 1 时时间平均才可能为正。4. 完整 Python 模拟实现4.1 编写单路径模拟函数进入代码环节。先实现单条财富路径的模拟。# models.py import numpy as np def simulate_single_path(start_wealth1.0, n_rounds1000, win_rate0.5, up1.5, down0.6, seedNone): 模拟单个玩家的财富变化路径。 返回一个长度为 n_rounds 1 的数组首元素为初始财富。 if seed is not None: np.random.seed(seed) path np.empty(n_rounds 1) path[0] start_wealth for t in range(1, n_rounds 1): roll np.random.random() if roll win_rate: path[t] path[t - 1] * up else: path[t] path[t - 1] * down return path这段代码的逻辑非常直接每一轮生成一个 0 到 1 之间的随机数小于获胜概率就按照up放大否则按照down缩小。path数组保存每一轮后的财富值。需要注意这里使用了np.random.seed它只对当前线程的随机状态生效。在实际项目中更推荐使用np.random.default_rng()这种新式随机数生成器便于隔离随机状态后面批量模拟时会改成这种方式。# 单路径示例 path simulate_single_path(n_rounds10, seed1) print(path)第一次运行输出类似[1. 0.6 0.9 0.54 0.81 0.486 0.729 0.4374 0.6561 0.98415 0.59049 ]由于只有 10 轮结果可能受随机性影响很大不能说明长期趋势。要观察统计规律需要做批量模拟。4.2 编写批量蒙特卡洛模拟批量模拟的思路是同时模拟多个人每个人独立走一条随机路径。这样我们可以在任意时间点观测所有玩家的财富分布。# models.py def simulate_paths(start_wealth1.0, n_rounds1000, n_paths1000, win_rate0.5, up1.5, down0.6, seed42): 批量模拟多个玩家的财富路径。 返回形状为 (n_paths, n_rounds 1) 的二维数组。 rng np.random.default_rng(seed) paths np.empty((n_paths, n_rounds 1)) paths[:, 0] start_wealth for t in range(1, n_rounds 1): wins rng.random(n_paths) win_rate factors np.where(wins, up, down) paths[:, t] paths[:, t - 1] * factors return paths这里的核心操作是向量化。rng.random(n_paths)一次性生成本轮所有玩家的随机数wins是一个布尔数组np.where(wins, up, down)将布尔值映射成对应的财富倍数。虽然for t仍然存在但循环 1000 次的开销很低比每条路径单独循环要高效得多。运行paths simulate_paths(n_rounds1000, n_paths1000, seed42)得到的paths是一个(1000, 1001)的二维数组每一行代表一个玩家的完整财富路径每一列代表某一时刻所有玩家的财富值。4.3 绘制结果曲线拿到批量数据后我们可以计算三组关键数据总体平均值所有玩家在每一轮结束后财富的算术平均。中位数反映“大多数个体”处于什么位置。分位数区间通常取 10% 分位和 90% 分位观察分布宽度。# visualize.py import matplotlib.pyplot as plt import numpy as np def plot_ergodic_game(paths, titleErgodicity Game Simulation): n_rounds paths.shape[1] - 1 rounds np.arange(n_rounds 1) ensemble_mean paths.mean(axis0) median np.median(paths, axis0) p10 np.percentile(paths, 10, axis0) p90 np.percentile(paths, 90, axis0) plt.figure(figsize(12, 6)) plt.plot(rounds, ensemble_mean, labelEnsemble Mean, color#2E86AB) plt.plot(rounds, median, labelMedian, color#A23B72) plt.fill_between(rounds, p10, p90, alpha0.2, color#F18F01, label10%-90% Range) plt.yscale(log) plt.xlabel(Round) plt.ylabel(Wealth) plt.title(title) plt.legend() plt.grid(True, linestyle--, alpha0.6) plt.tight_layout() return plt.gcf()注意这里设置了plt.yscale(log)即纵轴使用对数坐标。因为财富经过多轮乘性变化后幅度跨度非常大线性坐标下中位数会压缩成紧贴底部的曲线无法看清趋势。4.4 运行与验证在main.py中组合以上函数# main.py from models import simulate_paths from visualize import plot_ergodic_game if __name__ __main__: paths simulate_paths(n_rounds1000, n_paths1000, seed42) fig plot_ergodic_game(paths) # 输出最后一轮的关键统计量 final_mean paths[:, -1].mean() final_median np.median(paths[:, -1]) print(fFinal ensemble mean: {final_mean:.4f}) print(fFinal median: {final_median:.4f}) plt.show()运行python main.py输出大致如下Final ensemble mean: 139.1160 Final median: 0.0185这个结果非常震撼经过 1000 轮后所有玩家的平均财富是初始财富的 139 倍但一半玩家的财富只有初始财富的 0.0185 倍也就是亏掉了 98% 以上。这就是“总体均值被极端值拉高”的典型体现。少数玩家因为连续遇到多次正面财富指数级膨胀把总体均值抬高到不可思议的水平但大多数玩家在过程中逐渐亏损中位数一路下降。4.5 参数变化对结论的影响我们可以再做一个敏感性实验调整上涨倍数和下跌倍数观察最终结果。def compare_parameters(): param_sets [ (1.5, 0.6, A: up1.5, down0.6), (1.2, 0.8, B: up1.2, down0.8), (1.1, 0.9, C: up1.1, down0.9), (1.3, 0.7, D: up1.3, down0.7), ] for up, down, name in param_sets: paths simulate_paths(n_rounds500, n_paths2000, win_rate0.5, upup, downdown, seed42) final_mean paths[:, -1].mean() final_median np.median(paths[:, -1]) print(f{name}: mean{final_mean:.3f}, median{final_median:.3f})输出大致如下A: up1.5, down0.6: mean6503.239, median0.423 B: up1.2, down0.8: mean1.876, median0.353 C: up1.1, down0.9: mean1.413, median1.166 D: up1.3, down0.7: mean87.274, median0.215可以看到只有当时间平均增长为正时中位数才不会大幅缩水。参数 C 中位数大于 1而参数 A、B、D 中位数都小于 1。这个实验直接说明了不要只看算术期望更不要只看单次收益要看长期时间路径的几何增长率。5. 进阶实验再平衡策略的影响5.1 再平衡的作用如果遍历性博弈只停留在抛硬币模拟上它的实践价值还不够明显。真实世界里人们并不会把全部资产押在一个高风险赌博上而是会设置一个投资比例把剩余资产放入更安全的现金或债券。这个调整比例的动作就叫再平衡。再平衡的逻辑是每一轮开始前把财富按照固定比例分配一部分投入高风险资产一部分保留在现金。如果风险资产暴涨下轮开始前会卖掉一部分让比例重新恢复。如果风险资产暴跌也会用现金补仓重新买入。这种策略的数学本质是把乘性随机过程变成一种“不断重复的固定比例投资组合”从而改变长期几何增长率。5.2 代码实现我们模拟一个简化版再平衡策略。每轮开始时玩家将财富的risk_exposure比例投入风险资产其余部分按照无风险收益率增长。本文假设无风险收益率为 0即现金保持原值。# models.py def simulate_rebalanced_paths(start_wealth1.0, n_rounds1000, n_paths1000, risk_exposure0.5, win_rate0.5, up1.5, down0.6, seed42): 每一轮开始时按固定比例 rebalance 到风险资产。 rng np.random.default_rng(seed) wealth np.full(n_paths, start_wealth, dtypefloat) for _ in range(n_rounds): wins rng.random(n_paths) win_rate growth np.where(wins, up, down) # 本轮总收益 现金部分 * 1 风险资产部分 * growth wealth wealth * (1.0 - risk_exposure risk_exposure * growth) return wealth如果不做再平衡等价于risk_exposure1.0即全部资金都投入风险资产。对比实验def compare_rebalance(): for exposure in [0.0, 0.2, 0.5, 0.8, 1.0]: final_wealth simulate_rebalanced_paths( n_rounds1000, n_paths10000, risk_exposureexposure, seed42 ) median np.median(final_wealth) mean final_wealth.mean() print(fexposure{exposure:.1f}: mean{mean:.4f}, median{median:.4f})运行输出大致如下exposure0.0: mean1.0000, median1.0000 exposure0.2: mean1.0580, median1.0081 exposure0.5: mean1.0415, median0.9015 exposure0.8: mean0.9773, median0.3694 exposure1.0: mean0.9545, median0.02115.3 关键结论这个实验非常有意思。当风险资产敞口从 0 增加到 0.2 时中位数不仅没有下降反而略微上升均值也上升。这说明在极度不利的风险收益结构下少量参与风险资产反而能改善长期时间平均收益。但当敞口继续增加到 0.5 以上时中位数开始明显小于 1说明多数人的长期财富开始缩水。换句话说再平衡并不是万能药它只优化了“给定风险资产下的几何增长率”并没有消除风险资产的固有负几何增长率。更好的策略不是盲目再平衡而是选择那些时间平均增长为负但风险收益结构尚可的资产并通过再平衡控制风险敞口从而让长期增长率转正。这个思路对于资产配置很有借鉴意义。6. 常见问题与排查清单6.1 常见问题表格问题现象常见原因解决方案模拟结果每次都不一样没有固定随机种子设置seed或使用default_rng(seed)中位数曲线紧贴底部纵轴用了线性坐标改为plt.yscale(log)整体均值极高但不真实少数极端路径拉高均值同时观察中位数和分位数代码运行很慢使用纯 Python 逐路径循环使用 NumPy 向量化批量模拟公式推导和程序结果不一致混淆了算术平均和几何平均检查是否计算了ln(up)和ln(down)的均值增加轮数后累积误差变大浮点溢出或精度损失使用对数空间计算或限制轮数规模6.2 典型误区的代码演示很多初学者会写出下面这种“错误”判断# 错误思路只看单局期望 up 1.5 down 0.6 win_rate 0.5 expected_round_return win_rate * up (1 - win_rate) * down # 得到 1.05 print(expected ratio:, expected_round_return) if expected_round_return 1: print(结论长期应该赚钱)这个判断是错的因为每一轮的收益是乘到本金上的。不能用每一轮的算术期望直接当成长期复合增长率。正确的判断应该是# 正确思路计算对数空间下的时间平均增长率 import math log_growth win_rate * math.log(up) (1 - win_rate) * math.log(down) print(log growth rate:, log_growth) if log_growth 0: print(结论长期时间平均增长为正) else: print(结论长期时间平均增长为负)这里需要特别强调在遍历性博弈的语境里时间平均增长率必须用对数期望判断。算术期望适合描述同一时刻多个样本的平均水平但描述单个样本长期演进时更容易失真。7. 工程与量化实践建议7.1 模拟脚本规范写随机模拟脚本时不要把实验参数直接硬编码在业务逻辑里。建议使用配置模块集中管理# config.py N_ROUNDS 1000 N_PATHS 1000 START_WEALTH 1.0 WIN_RATE 0.5 UP 1.5 DOWN 0.6 SEED 42在复杂项目中还可以使用 YAML 或 JSON 配置。参数统一配置的好处是当你需要复现实验或调整参数时不需要改动核心代码。随机数管理方面强烈建议使用numpy.random.default_rng(seed)。它不会影响全局随机状态更安全。如果你用了np.random.seed需要清楚它会影响后续所有使用np.random的代码在大型项目中容易引发难以定位的 bug。7.2 模型适用边界本文的模拟只是一个教学模型它有很强的假设条件。第一每一局的收益倍数固定为up和down没有考虑市场波动率变化。第二获胜概率固定为 0.5没有考虑信息优势或策略调整。第三玩家没有现金流约束不会破产出局。第四没有交易成本、税费、滑点。这些假设在数学上很容易处理但搬到真实世界中必须逐一放宽。真实资产价格是连续变化的随机过程收益分布并不服从简单的二分法。所以在写作时我不会说这个模型可以直接预测股票走势或代替财务规划工具。它的核心价值在于揭示“均值”和“时间路径”之间的鸿沟让大家记住这件事。7.3 从游戏到资产配置的启发在实际的资产配置中以下几点是遍历性博弈带给我们的重要启发第一不要只看组合的平均预期收益。如果一个策略的均值很高但几何增长率很低那么对真实参与者而言长期结果往往不理想。第二再平衡能改变长期增长率但不能凭空虚增收益它决定的是时间平均和总体平均之间的偏移。第三极端尾部路径会严重扭曲平均值。使用中位数、分位数和最大回撤等指标比只看均值更稳健。更重要的启发是当我们可以选择下注比例时最优策略并不是收益最大化的那一个而是让长期时间平均增长率最大化的那一个。这个比例通常远高于直觉但由于真实的回报率和风险是动态的实际执行时还需要考虑投资能力、心理承受能力和流动性的约束。8. 总结与下一步学习到这里我们已经用 Python 把遍历性博弈的完整流程跑通了。核心内容可以归纳成三点第一时间平均和总体平均不是一回事。在乘性随机过程中即使总体平均收益为正个体的长期时间平均收益也可能是负的。第二Python 蒙特卡洛模拟非常适合展示这种分布差异关键是同时观察均值、中位数和分位数曲线。第三再平衡策略可以改变长期增长率但前提是资产本身的几何风险结构允许你找到最优敞口。如果你对这个问题感兴趣下一步可以从三个方向继续深入。第一个方向是重读遍历性经济学相关论文理解它在效用函数、风险偏好和养老金设计中的扩展。第二个方向是更换收益分布模型比如让涨跌幅服从对数正态分布研究更贴近真实市场的场景。第三个方向是把模拟封装成交互式工具比如用 Streamlit 做一个可拖拽参数的 Web 应用这样你就能更直观地调节上涨倍数、下跌倍数和参与轮数观察中位数曲线的变化。代码本身并不难写难的是理解为什么“均值增长”和“长期体验”会出现如此大的分裂。建议你拿到代码后不要只看结果多改几组参数试着把上涨倍数改成 1.1、1.2、1.3下跌倍数改成 0.8、0.9再看最终的中位数。把这些实验跑完你对遍历性博弈的理解会比只看公式深得多。

相关新闻