
这次我们来看一个概念向的模拟项目The Ergodicity Game遍历性博弈。如果你关心“为什么期望值为正长期玩下去却可能破产”这类问题或者想用代码把一个大道理跑成可视化的结果这个项目非常值得拆解。先说结论The Ergodicity Game 用 Python 实现了一个经典赌局模拟器核心是演示**系综平均Ensemble Average和时间平均Time Average**的差异。它解决的不只是“赌局问题”而是统计物理、经济学和投资决策中一个很容易被忽略的数学陷阱。项目本身不需要高配硬件普通 CPU 就能跑代码结构简单可以快速跑出模拟曲线也方便改参数做扩展实验。本文会带你从理论背景、环境准备、源码实现、运行测试、结果分析到参数扩展完整跑一遍这个模拟器。如果你正在学概率论、蒙特卡洛模拟或者想理解“长期复利”背后的数学逻辑这篇文章可以直接收藏。1. 核心能力速览能力项说明项目类型概率论 / 蒙特卡洛模拟 / 遍历性经济学演示工具核心功能模拟多玩家重复下注的财富变化对比系综平均与时间平均理论背景Ole Peters 的遍历性经济学Ergodicity Economics硬件要求极低普通 CPU 即可运行显存占用不涉及 GPU 推理无显存需求支持平台Windows / macOS / Linux需要 Python 环境启动方式命令行运行 Python 脚本是否支持 API无内置 API但代码可直接封装成函数或服务是否支持批量任务支持通过调整玩家数量和轮次批量生成模拟数据输出内容控制台统计结果 Matplotlib 财富轨迹图适合场景教学演示、概率论实验、蒙特卡洛方法练习、投资逻辑科普单次赌局参数数值胜率50%赢时收益60%输时亏损-50%单次期望收益5%单次对数增长率约 -11.16%项目核心思路一句话单次期望是正数但长期复利下的时间平均是负数所以绝大多数玩家最终会破产。2. 遍历性博弈在演示什么这个概念不算新但很容易被忽略。先回顾一下遍历性Ergodicity的定义如果一个系统是遍历的那么对系统内所有个体做平均系综平均等价于对单个个体长时间做平均时间平均。经典统计物理中很多系统满足这个条件所以物理学家习惯用“期望值”描述大量粒子的行为。但经济决策和财富增长并不天然满足遍历性。Ole Peters 的遍历性经济学正是在这个点上做了展开当系统不满足遍历性时使用期望值作为决策依据会产生严重偏差。The Ergodicity Game 复现的就是这个逻辑。核心赌局是你有 1 万元初始资金每次下注全部资金或者固定比例。如果硬币正面资金乘以 1.6如果反面资金乘以 0.5。表面上看单次期望是E 0.5 × (60%) 0.5 × (-50%) 5%期望为正好像应该一直玩。但如果看长期复利增长结论完全不同。单次的对数增长率为E[ln(W_t1 / W_t)] 0.5 × ln(1.6) 0.5 × ln(0.5) ≈ -0.1116也就是说虽然算术期望是 5%但时间平均下的对数增长率是-11.16%。这意味着如果很多人同时开始玩这个游戏平均财富会一路暴涨但绝大多数独立玩家的财富会趋近于零。这就是系综平均与时间平均的背离。The Ergodicity Game 的价值就是把这个反直觉的结论用代码和图形完整呈现出来。3. 适用场景与使用边界适合谁正在学概率论、统计学、随机过程的学生需要一个能动手的实验来理解系综平均和时间平均。做量化交易、投资策略研究的技术人员想直观理解“复利”和“波动损耗”的数学关系。想写蒙特卡洛模拟练手的 Python 开发者这个项目代码量适中非常适合二次改造。做金融科普内容的人可以直接用这个模拟器的图来讲“长期投资中的风险”。不适合什么它不是一个量化交易回测框架不能直接拿来做实盘策略。它不是为了预测具体某只股票或某个市场走势。它不提供复杂的金融模型比如随机波动率、杠杆调整、流动性影响等。使用边界与合规提醒这是一个教学和理论演示项目输出结果是基于随机模拟的数学演示不构成任何投资建议。如果你要把它改造成金融策略演示工具请务必在输出中明确标注“模拟结果不代表真实市场表现”。如果后续在团队内部分享或对外发布建议保留理论出处避免产生误解。不要脱离遍历性经济学的讨论背景直接用它论证某一具体投资策略的优劣。4. 环境准备与前置条件这个项目对环境的要求非常简单理论上只要能跑 Python 和 Matplotlib 就能运行。推荐环境清单项目建议操作系统Windows 10/11、Ubuntu 20.04、macOS 12Python 版本Python 3.9 及以上包管理工具pip 或 condaNumPy1.21 及以上Matplotlib3.5 及以上不建议使用 Python 2.x部分语法和类型标注不兼容。检查本地环境的命令python --version pip --version如果还没有创建虚拟环境建议先建一个独立的目录避免污染全局 Python 环境。mkdir ergodicity_game cd ergodicity_game python -m venv venvWindows 激活虚拟环境venv\Scripts\activatemacOS / Linux 激活虚拟环境source venv/bin/activate5. 源码结构与核心实现建议按照下面的目录结构组织项目文件ergodicity_game/ ├── ergodicity_game.py # 主程序 ├── requirements.txt # 依赖清单 └── README.md # 项目说明requirements.txt 内容numpy1.21 matplotlib3.5下面给出核心实现的完整代码。代码分成三部分赌局模拟、统计分析、结果绘图。创建 ergodicity_game.py写入以下内容import argparse import numpy as np import matplotlib.pyplot as plt def run_single_game( n_players: int 1000, n_rounds: int 100, initial_wealth: float 1.0, win_prob: float 0.5, win_rate: float 0.6, lose_rate: float 0.5, seed: int | None None, ) - np.ndarray: 模拟多玩家重复下注。 返回形状为 (n_players, n_rounds 1) 的数组 第一列是初始财富后续每一列代表一轮结束后的财富。 rng np.random.default_rng(seed) results np.zeros((n_players, n_rounds 1)) results[:, 0] initial_wealth for _round in range(1, n_rounds 1): win_mask rng.random(n_players) win_prob results[:, _round] results[:, _round - 1] * np.where( win_mask, 1.0 win_rate, 1.0 - lose_rate ) return results def analyze_result(results: np.ndarray) - dict: 计算系综平均、系综中位数、破产人数占比等统计量。 ensemble_mean results.mean(axis0) ensemble_median np.median(results, axis0) final_wealth results[:, -1] broken_ratio (final_wealth 1e-6).mean() return { ensemble_mean: ensemble_mean, ensemble_median: ensemble_median, broken_ratio: broken_ratio, } def plot_result( results: np.ndarray, output_path: str ergodicity_game_result.png, sample_players: int 50, ) - None: 绘制玩家财富轨迹、系综平均和中位数曲线。 plt.figure(figsize(10, 6)) n_players results.shape[0] sample_count min(sample_players, n_players) for i in range(sample_count): plt.plot( results[i], colorgray, alpha0.3, linewidth0.8, labelindividual if i 0 else None, ) ensemble_mean results.mean(axis0) ensemble_median np.median(results, axis0) plt.plot( ensemble_mean, colortab:blue, linestyle--, linewidth2, labelensemble mean, ) plt.plot( ensemble_median, colortab:orange, linestyle-., linewidth2, labelensemble median, ) plt.axhline(1.0, colorblack, linewidth0.8) plt.yscale(log) plt.xlabel(round) plt.ylabel(wealth (log scale)) plt.title(The Ergodicity Game: Individual Trajectories vs Ensemble Mean) plt.legend() plt.grid(True, alpha0.3) plt.savefig(output_path, dpi150, bbox_inchestight) print(f[INFO] result plot saved to {output_path}) def parse_args(): parser argparse.ArgumentParser(descriptionThe Ergodicity Game simulator) parser.add_argument(--players, typeint, default1000, helpnumber of players) parser.add_argument(--rounds, typeint, default100, helpnumber of rounds) parser.add_argument(--initial, typefloat, default1.0, helpinitial wealth) parser.add_argument(--win-prob, typefloat, default0.5, helpwin probability) parser.add_argument(--win-rate, typefloat, default0.6, helpwin return rate) parser.add_argument(--lose-rate, typefloat, default0.5, helplose drawdown rate) parser.add_argument(--seed, typeint, defaultNone, helprandom seed) parser.add_argument(--output, typestr, defaultergodicity_game_result.png) return parser.parse_args() def main(): args parse_args() print( * 50) print(The Ergodicity Game) print(fplayers{args.players}, rounds{args.rounds}, initial{args.initial}) print(fwin_prob{args.win_prob}, win_rate{args.win_rate}, lose_rate{args.lose_rate}) print( * 50) results run_single_game( n_playersargs.players, n_roundsargs.rounds, initial_wealthargs.initial, win_probargs.win_prob, win_rateargs.win_rate, lose_rateargs.lose_rate, seedargs.seed, ) stats analyze_result(results) print(ffinal ensemble mean : {stats[ensemble_mean][-1]:.4f}) print(ffinal ensemble median: {stats[ensemble_median][-1]:.4f}) print(fplayers broken ratio : {stats[broken_ratio] * 100:.2f}%) plot_result(results, output_pathargs.output, sample_playersmin(50, args.players)) if __name__ __main__: main()如果不想用命令行参数也可以直接用函数方式调用from ergodicity_game import run_single_game, analyze_result # 1000 名玩家玩 100 轮 results run_single_game(n_players1000, n_rounds100, seed42) # 查看最后 5 轮的系综平均 stats analyze_result(results) print(stats[ensemble_mean][-5:])6. 安装依赖与启动运行安装依赖pip install -r requirements.txt运行默认模拟python ergodicity_game.py控制台会输出类似下面的内容 The Ergodicity Game players1000, rounds100, initial1.0 win_prob0.5, win_rate0.6, lose_rate0.5 final ensemble mean : 132.8341 final ensemble median: 0.0001 players broken ratio : 87.40%这个结果非常直观地展示了核心结论1000 名玩家同时玩 100 轮平均财富涨到了超过 130。但中位数财富几乎归零。超过 87% 的玩家在 100 轮后已经破产或接近破产。这就是“平均值被极少数暴富玩家拉高”的典型场景。如果你只看系综平均会误以为这是一个好游戏但如果站在普通玩家的时间维度看长期玩下去几乎必然亏损。7. 功能测试与效果验证建议按下面的顺序做一套完整的功能验证。7.1 基础赌局模拟测试测试目的验证模拟器是否能正确生成多玩家财富变化数据。运行命令python ergodicity_game.py --players 100 --rounds 50 --seed 1 --output base_test.png判断标准程序正常退出无报错。控制台输出最终系综平均、中位数和破产比例。生成 base_test.png 图表文件能看到大量灰色个体轨迹、蓝色系综平均线、橙色中位数线。7.2 随机种子可复现性测试测试目的确认相同随机种子下结果可复现。运行两次相同命令python ergodicity_game.py --players 500 --rounds 100 --seed 2024 --output seed_test.png判断标准两次运行的最终系综平均、中位数、破产比例完全一致。这一步对后续批量实验非常重要保证结果可以追溯。7.3 极端赔率测试测试目的调整参数后系综平均和时间平均的关系是否发生变化。运行一个“正常赌局”对比python ergodicity_game.py --players 2000 --rounds 100 --win-rate 0.1 --lose-rate 0.1 --seed 7 --output symmetric_test.png这里设置对称的 ±10% 收益。可以看到系综平均会下降中位数也会下降说明无正期望优势时长期玩下去会逐渐归零。7.4 破产比例观察测试目的观察破产比例随轮数增加的变化趋势。可以写一个简短的对比脚本from ergodicity_game import run_single_game for rounds in [10, 30, 50, 100]: results run_single_game(n_players5000, n_roundsrounds, seed42) broken (results[:, -1] 1e-6).mean() print(frounds{rounds}, broken_ratio{broken:.2%})预期会发现轮数越多破产比例越高逼近 100%。7.5 判断模拟是否成功的标准系综平均曲线稳步上升中位数曲线快速下降两条曲线的分叉收敛在图中能明显看到。最终玩家的财富分布极不均匀少量玩家占据绝大多数总财富。日志输出的破产比例与理论推导方向一致。7.6 常见失败原因现象可能原因排查方向控制台无输出脚本语法错误或依赖未安装检查 Python 版本、NumPy 是否正常导入报错 No module named numpy未安装依赖执行 pip install -r requirements.txt图表空白Matplotlib 渲染问题检查是否有 GUI 环境或使用 Agg 后端结果波动巨大玩家数太少增加 --players 到 5000 以上再观察8. 结果分析系综平均与时间平均模拟跑完后核心就是读图。下图是典型输出效果虽然具体画风取决于运行环境但整体形态是固定的横轴是轮次。纵轴是财富注意这里使用了 log 比例尺。灰色细线是单个玩家的财富轨迹。蓝色虚线是系综平均也就是每一轮所有玩家财富的算术平均。橙色点划线是系综中位数。从图上可以得到几个关键观察第一系综平均曲线以稳定的速度上升100 轮后到达一个非常大的数字。数学上系综平均每轮的期望增长率是 5%所以 100 轮后期望值约为 1.05^100 ≈ 131.5。这个数字与模拟输出高度一致说明模拟器实现正确。第二中位数曲线急剧下降。因为每期对数增长率的期望是负数中位数每轮大约乘以 e^{-0.1116} ≈ 0.894。100 轮后中位数约为 0.894^100 ≈ 1.4 × 10^-5。这个量级和模拟输出完全吻合。第三单条玩家轨迹波动极大。有的玩家在 100 轮后财富翻了上万倍但大部分玩家已经跌到接近零。对数坐标下这些轨迹并不是平滑下降而是呈现剧烈的锯齿状这是因为单局胜负带来的乘性变化波动很大。这就是遍历性博弈最核心的视觉结论平均值被极少数超级赢家主导但“普通玩家”的典型路径完全是另一回事。如果你只看期望值会得出“应该一直下注”的错误结论如果你看时间平均或中位数会发现“长期重复下注”是一个灾难。9. 数学推导与代码对照建议搞懂下面三组公式后续改参数就不会迷茫。单次期望收益E[R] p × W (1 - p) × (-L)代入默认参数E[R] 0.5 × 0.6 0.5 × (-0.5) 0.05单次对数增长率g p × ln(1 W) (1 - p) × ln(1 - L)代入默认参数g 0.5 × ln(1.6) 0.5 × ln(0.5) ≈ -0.1116100 轮后的期望财富和典型财富E[W_100] 1.0 × 1.05^100 ≈ 131.5 W_typical 1.0 × e^(g × 100) ≈ 1.4 × 10^-5第一行对应代码中的ensemble_mean第二行对应ensemble_median。如果模拟结果的量级能和这两个公式对应上说明参数实现和随机过程都是正确的。10. 参数扩展与进阶玩法这个模拟器的扩展空间非常大换一组参数就是另一个实验。10.1 经典“凯利公式”场景如果你把每次下注改成“只投入固定比例”而不是全部身家结果完全不同。假设每次下注比例为 f单次对数增长率为g(f) 0.5 × ln(1 0.6f) 0.5 × ln(1 - 0.5f)可以通过数值方式求出最优 f。直接改代码里的win_rate和lose_rate无法模拟部分投入需要新写一个函数。这是一个非常好的后续实验方向。10.2 多人不同起点测试我们可以让玩家初始财富不一致观察财富分布的变化。实现上只需要修改initial_wealth参数或直接传入不同初始值的数组模拟结果会展示“初始财富差距被放大”的过程。10.3 批量参数扫描需要批量实验时可以用脚本跑一组参数对比python ergodicity_game.py --players 3000 --rounds 200 --seed 11 --output scan_rounds_200.png python ergodicity_game.py --players 3000 --rounds 500 --seed 12 --output scan_rounds_500.png python ergodicity_game.py --players 3000 --rounds 1000 --seed 13 --output scan_rounds_1000.png对比不同轮数下的破产比例和财富分布能直观看到“时间越长风险越大”的趋势。10.4 引入手续费或税在每轮结算时额外减少一定比例的资金可以观察“摩擦成本”如何加速财富衰减。只需要在每轮财富更新后加一行results[:, _round] * 0.999 # 每轮扣除 0.1% 成本11. 资源占用与性能观察这个项目不需要 GPUCPU 模拟即可。但需要注意玩家数量、轮次和数据存储之间的关系。模拟数据是形状为(n_players, n_rounds 1)的数组每个 float64 占 8 字节。估算内存占用的公式很简单mem_bytes n_players × (n_rounds 1) × 8例如1000 玩家100 轮约 0.8 MB非常小。10000 玩家1000 轮约 80 MB普通电脑无压力。100000 玩家5000 轮约 4 GB开始吃内存。如果你只是关心最终统计量可以不保存完整的模拟矩阵只在计算时累加平均值和中位数。下面是一段内存友好的替代实现import numpy as np def run_single_game_streaming( n_players: int 10000, n_rounds: int 5000, win_prob: float 0.5, win_rate: float 0.6, lose_rate: float 0.5, seed: int | None None, ) - dict: rng np.random.default_rng(seed) wealth np.ones(n_players) final_wealth wealth.copy() mean_history [] median_history [] for _round in range(1, n_rounds 1): win_mask rng.random(n_players) win_prob wealth wealth * np.where(win_mask, 1.0 win_rate, 1.0 - lose_rate) mean_history.append(wealth.mean()) median_history.append(np.median(wealth)) final_wealth wealth broken_ratio (final_wealth 1e-6).mean() return { mean_history: mean_history, median_history: median_history, broken_ratio: broken_ratio, }用这个版本跑百万玩家、上千轮也更稳妥。性能观察建议从--players 1000 --rounds 100起步确认输出正确后再逐步扩大规模。如果发现运行很慢优先考虑是否保存了过多历史数据。多参数批量扫描时建议提前规划输出目录避免结果互相覆盖。在服务器上跑需要绘图时如果遇到no display的错误可以在代码开头加上import matplotlib matplotlib.use(Agg)12. 常见问题与排查方法问题现象可能原因排查方式解决方案安装依赖失败网络源慢或 Python 版本过低检查 pip 源和 Python 版本更换镜像源或升级 Python 到 3.9运行报错 ModuleNotFoundError未安装 NumPy 或 Matplotlib执行 pip list 查看已安装包执行 pip install -r requirements.txt图无法显示无 GUI 环境检查运行环境是否支持图形界面设置 Matplotlib Agg 后端保存到文件随机结果与其他教程不一致随机种子不同或玩家数不同确认 seed 参数是否一致固定相同的 seed 对比中位数直接变成 0玩家数少财富被下溢处理减少轮次或查看数值精度增加 --players使用更高精度或截断判断阈值模拟速度慢玩家数和轮次过大查看内存和 CPU 占用使用流式实现或分批模拟图表中文乱码Matplotlib 缺少中文字体查看日志警告改用英文标签或安装中文字体13. 最佳实践与使用建议几个工程化建议希望对你有帮助。第一第一次运行不要搞大参数。先跑--players 100 --rounds 20确认控制台输出和图表都正常再逐步扩大规模。这样能快速区分“代码问题”和“参数问题”。第二固定随机种子。任何涉及随机数的实验都应该给--seed传一个固定值。没有固定 seed批量实验之间的结果不可比也很难复现问题。第三把输入参数、输出图表和统计分析日志统一规划目录。建议结构experiments/ ├── run_001/ │ ├── args.json │ ├── result.png │ └── summary.txt └── run_002/ └── ...这样跑多组对比时不容易搞混结果。第四如果你要把这个模拟扩展为 API 服务或批量任务可以按下面的思路设计。先封装核心函数再提供 HTTP 接口from flask import Flask, request, jsonify from ergodicity_game import run_single_game, analyze_result app Flask(__name__) app.route(/api/simulate, methods[POST]) def simulate(): data request.get_json(forceTrue) n_players data.get(players, 1000) n_rounds data.get(rounds, 100) seed data.get(seed, None) results run_single_game(n_playersn_players, n_roundsn_rounds, seedseed) stats analyze_result(results) return jsonify( { final_ensemble_mean: float(stats[ensemble_mean][-1]), final_ensemble_median: float(stats[ensemble_median][-1]), broken_ratio: float(stats[broken_ratio]), } ) if __name__ __main__: app.run(host127.0.0.1, port8000)调用示例curl -X POST http://127.0.0.1:8000/api/simulate \ -H Content-Type: application/json \ -d {players: 1000, rounds: 100, seed: 42}第五涉及金融、投资、资产配置相关演示时输出材料中必须保留“模拟结果不构成投资建议”的说明。这是一个理论演示项目不是策略回测工具边界要分清楚。14. 总结与下一步The Ergodicity Game 最值得尝试的地方在于它用很少的代码把“期望值为正不代表适合长期下注”这个反直觉结论完整跑通。先验证 1000 玩家、100 轮下的系综平均和中位数分叉是最快的上手路径。最容易踩的坑是随机种子不一致和玩家数过少导致结果波动大。如果想要继续扩展建议优先做两件事一是把凯利公式的下注比例函数补上对比最优下注比例 vs 全押的极端情况二是做参数扫描观察不同盈利和亏损幅度下破产比例的变化曲线。这套模拟思路同样可以迁移到其他随机增长模型中去。