news 2026/9/8 19:37:54

遍历性游戏:为什么期望值为正却长期必亏?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遍历性游戏:为什么期望值为正却长期必亏?

这次我们来看一个概率论里的经典反直觉模型:The Ergodicity Game(遍历性游戏)。它不依赖 GPU,不需要安装几十 GB 的模型,也不需要什么高端显卡。它只用一个简单的抛硬币游戏,就能演示一个在经济学、量化和风险决策里很容易被忽略的问题:为什么一个“每局数学期望为正”的游戏,玩家长期玩下去却几乎必然亏钱。

如果你之前接触过“期望值”和“长期复利”这两个概念,但是没想清楚它们什么时候等价、什么时候会分道扬镳,那么这篇文章就是为你准备的。下面我会从数学原理讲起,然后用 Python 完整复现这个游戏,对比“系综平均”和“时间平均”,再做批量蒙特卡洛模拟、参数化测试,最后验证凯利公式给出的最优下注比例。整个过程只需要 numpy 和 matplotlib,普通笔记本就能跑完。

1. 核心概念速览

项目类型概率论教学游戏 / 遍历性经济学演示模型
核心思想用简单随机过程说明时间平均不等于系综平均
经典玩法抛硬币:50% 概率财富乘以 1.5,50% 概率财富乘以 0.6
运行环境Windows / macOS / Linux,任意支持 Python 的系统
硬件要求纯 CPU 计算,无 GPU 需求
核心依赖Python 3.8+、numpy、matplotlib
是否支持批量支持,本质是多智能体向量化蒙特卡洛模拟
是否支持 API无固定 API,但可以封装为函数或本地服务
主要输出系综平均曲线、单轨迹时间曲线、不同下注比例的增长率对比
适合场景课堂教学、量化入门、经济学讨论、风险教育、凯利公式验证

需要说明的是,“The Ergodicity Game”在网络上存在多个教学实现版本,参数细节可能略有不同。本文采用的+50% / -40%抛硬币模型,是遍历性经济学研究中最常见、最经典的一组示例参数,适合作为通用复现基准。

2. 这个游戏到底在讲什么:数学背景与适用边界

2.1 什么是遍历性

一个随机过程如果具有遍历性,那么“对大量样本同时取平均”的结果,等于“让单个样本跑足够长时间取平均”的结果。换句话说,系综平均等于时间平均。

现实中很多物理过程近似具备这个性质,所以用“期望值”去预测长期结果是合理的。但我们的财富增长过程通常不具备这个性质。财富增长是乘性的:上一轮的收益会成为下一轮的本金,一个负收益造成的损失不会在下一轮自动补回来。这就导致了时间平均和系综平均的系统性偏离。

2.2 经典抛硬币游戏

设初始财富为 1 个单位,每一轮抛一枚公平硬币:

  • 正面:财富乘以 1.5,即收益 +50%。
  • 反面:财富乘以 0.6,即收益 -40%。

先算单轮期望:

E[X] = 0.5 × 1.5 + 0.5 × 0.6 = 1.05

所以单轮期望收益是 +5%,看起来是一个值得长期玩下去的游戏。

但注意,财富是乘法累加。如果玩 n 轮,单个玩家的财富路径几乎必然收敛到:

(1.5)^(n/2) × (0.6)^(n/2) = (1.5 × 0.6)^(n/2) = (0.9)^(n/2)

这里1.5 × 0.6 = 0.9,开根号得到sqrt(0.9) ≈ 0.9487。也就是说,每一轮的时间平均增长率大约只有 0.9487,小于 1。一个玩家一直玩下去,财富的中位数和众数会越来越小,最终趋近于 0。

这就出现了一个悖论:

  • 所有人资产的算术平均:以每轮 5% 的速度指数增长。
  • 单个玩家长期财富:以每轮约 -5.13% 的速度指数衰减。

为什么会这样?核心原因是少数极端幸运者把资产拉到了天文数字,导致“平均”被严重抬高。绝大多数玩家的财富却在中位数附近持续缩水。

2.3 适用场景与使用边界

这个游戏最适合三类人:

第一类是概率论和统计学初学者。它提供了一个非常具体的例子,说明“期望值为正”不等于“值得长期参与”,帮助建立对重尾分布、几何平均和随机乘性过程的直觉。

第二类是量化交易和资产配置相关从业者。复利环境下真正重要不是单期期望收益,而是对数收益的期望。这个游戏可以很直观地演示“波动拖累”问题。

第三类是金融教育和投资者教育内容创作者。用它来讲风险、长期投资和仓位管理,比直接抛理论公式更有说服力。

同时需要明确边界:这是一个教学模型,不是真实市场模型。真实的资产收益不是独立同分布,存在自相关、波动率聚集、极端事件和交易成本。因此,不要把这个游戏直接当成投资建议。文章中出现的任何公式和模拟结果都只用于说明数学性质,不构成任何形式的投资决策依据。

3. 环境准备与前置条件

整个复现过程不需要 GPU,也不需要深度学习框架。你需要准备的是:

  • Python 3.8 或更高版本。
  • numpy,用于向量化随机模拟。
  • matplotlib,用于绘制曲线。
  • 可选 Jupyter Notebook 或 VS Code,用于交互式运行。

安装依赖的命令如下:

pip install numpy matplotlib

如果是在国内网络环境,可以指定国内镜像源加速:

pip install numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple

环境检查:

python -c "import numpy; import matplotlib; print(numpy.__version__, matplotlib.__version__)"

能正常输出版本号就说明环境没问题。如果提示模块不存在,回到上一步重新安装。

4. 用 Python 复现遍历性游戏

4.1 单玩家轨迹模拟

先写一个最基础的单玩家模拟函数,观察一条财富路径的发展趋势:

import numpy as np import matplotlib.pyplot as plt def simulate_single_path(steps=1000, bet_fraction=1.0, seed=42): rng = np.random.default_rng(seed) wealth = 1.0 history = [wealth] for _ in range(steps): if rng.random() < 0.5: wealth *= (1 + 0.5 * bet_fraction) else: wealth *= (1 - 0.4 * bet_fraction) history.append(wealth) return np.array(history) path = simulate_single_path(steps=1000, seed=42) print("最终财富:", path[-1]) print("时间平均增长率:", (path[-1]) ** (1 / 1000))

运行后你会发现,单条路径的最终财富通常小得惊人,比如1e-22这种量级。时间平均增长率在 0.94 到 0.95 附近波动。这就是时间平均小于 1 的直接体现。

4.2 多玩家系综模拟

再看系综平均。模拟大量玩家同时玩 1000 轮,每一轮都使用向量化操作更新所有玩家的财富:

def simulate_ensemble(n_agents=10000, steps=1000, seed=42): rng = np.random.default_rng(seed) wealth = np.ones(n_agents) ensemble_mean = np.zeros(steps + 1) ensemble_median = np.zeros(steps + 1) ensemble_mean[0] = 1.0 ensemble_median[0] = 1.0 for step in range(1, steps + 1): wins = rng.random(n_agents) < 0.5 wealth[wins] *= 1.5 wealth[~wins] *= 0.6 ensemble_mean[step] = wealth.mean() ensemble_median[step] = np.median(wealth) return ensemble_mean, ensemble_median mean_path, median_path = simulate_ensemble(n_agents=10000, steps=1000, seed=42) print("第1000轮系综平均:", mean_path[-1]) print("第1000轮系综中位数:", median_path[-1])

这一步是整个游戏最关键的地方。你会在输出中看到两个完全不同的数字:

  • 系综平均非常大,理论上是1.05^1000 ≈ 5.4e21,实际模拟在指数爆炸后可能更大。
  • 系综中位数非常小,接近0.9487^1000 ≈ 1e-23

4.3 画图对比

把两条曲线画到一张图里,方便直观对比:

plt.figure(figsize=(10, 6)) steps_range = np.arange(len(mean_path)) plt.plot(steps_range, mean_path, label="系综平均 (10000人)", linewidth=1.5) plt.plot(steps_range, median_path, label="系综中位数 (10000人)", linewidth=1.5) plt.yscale("log") plt.xlabel("轮次") plt.ylabel("财富 (对数坐标)") plt.title("遍历性游戏:系综平均 vs 系综中位数") plt.legend() plt.grid(True, which="both", linestyle="--", alpha=0.6) plt.show()

注意这里我用了对数坐标,否则均值曲线会直接冲出画布,而中位数曲线会贴在地板上,什么都看不清。从图上可以清楚看到两条线像剪刀一样张开,这就是“期望值为正但个体长期必输”的可视化证据。

5. 功能测试与效果验证

下面把验证拆成几个明确的测试用例。每个用例都给出输入、操作、预期结果和判断标准,方便你照着跑。

5.1 测试一:单轨迹长跑

  • 测试目的:验证单个玩家长期玩这个游戏,财富是否会收敛到 0。
  • 输入参数:steps=10000bet_fraction=1.0seed=42
  • 操作步骤:运行simulate_single_path,打印最终财富。
  • 预期结果:最终财富在1e-200甚至更小的量级,时间平均增长率约0.9487
  • 判断标准:最终财富远小于 1,且时间平均增长率小于 1,说明单玩家长期必亏。
  • 常见失败原因:如果使用了float32精度,在指数衰减到极小时可能出现下溢为 0,建议使用默认的float64

5.2 测试二:系综平均与时间平均分叉

  • 测试目的:验证系综平均和时间平均不再相等。
  • 输入参数:n_agents=10000steps=1000
  • 操作步骤:同时运行simulate_ensemble,比较均值和中位数。
  • 预期结果:均值随轮次单调爆炸式增长,中位数持续衰减。
  • 判断标准:两条曲线在对数坐标下呈对称张开趋势,说明过程不具备遍历性。

5.3 测试三:不同下注比例的影响

  • 测试目的:验证“把全部资产押上去”并不是唯一玩法,降低下注比例可以改变时间平均增长率。
  • 输入参数:bet_fraction从 0 到 1 依次取 0.1、0.25、0.5、1.0。
  • 操作步骤:对每个比例跑多路径模拟,计算对数增长率的理论值:
fractions = np.linspace(0, 1, 101) growth_rates = 0.5 * np.log1p(0.5 * fractions) + 0.5 * np.log1p(-0.4 * fractions) for f, g in zip([0.1, 0.25, 0.5, 1.0], [0.5 * np.log1p(0.5 * x) + 0.5 * np.log1p(-0.4 * x) for x in [0.1, 0.25, 0.5, 1.0]]): print(f"下注比例 {f:.2f}: 单轮对数增长率 {g:.6f}, 等效乘数 {np.exp(g):.4f}")
  • 预期结果:下注比例在 0.25 附近时对数增长率由负转正。
  • 判断标准:增长率先升后降,存在一个最大值点,这个点就是凯利比例。

5.4 测试四:玩家数量对系综平均的影响

  • 测试目的:验证增加玩家数量不会消除非遍历性,只会让系综平均更稳定地爆炸。
  • 输入参数:n_agents分别取 100、1000、10000。
  • 操作步骤:分别运行simulate_ensemble,打印最终均值。
  • 预期结果:三人均值都在1e21以上量级,且玩家数量越大,均值越接近理论值1.05^1000
  • 判断标准:均值不随玩家数量下降,中位数始终远小于均值,说明这不是“样本量不够”造成的偏差。

5.5 测试五:改成不利赔率

  • 测试目的:验证游戏对赔率参数高度敏感。
  • 输入参数:把正面收益从 +50% 改成 +30%,反面损失仍为 -40%。
  • 操作步骤:修改乘法因子为1.30.6后重新跑系综模拟。
  • 预期结果:单轮期望变为0.95,系综平均也开始衰减。
  • 判断标准:均值和中位数同时下降,原因明确,逻辑自洽。

6. 批量蒙特卡洛模拟与参数化测试

遍历性游戏最有价值的地方在于,它可以作为“批量随机模拟”的教学载体。你可以一次性扫描上百组参数,观察增长率曲面。

6.1 参数扫描模板

def compute_growth_rate(win_prob=0.5, win_gain=0.5, lose_loss=0.4, fractions=None): if fractions is None: fractions = np.linspace(0, 1, 101) with np.errstate(divide='ignore', invalid='ignore'): growth = (win_prob * np.log1p(win_gain * fractions) + (1 - win_prob) * np.log1p(-lose_loss * fractions)) return fractions, growth fractions, growth = compute_growth_rate() best_idx = np.nanargmax(growth) print(f"最大增长率出现在下注比例 {fractions[best_idx]:.2f}") print(f"最大对数增长率 {growth[best_idx]:.6f}")

这段代码直接用向量化方式扫描 101 个下注比例,不需要循环,秒出结果。理论输出是 0.25 附近达到最大增长率,对应凯利公式的解。

6.2 批量任务设计

如果你要跑数千组参数,建议把任务设计成可重复、可断点续跑的结构:

import json import time def batch_experiment(params_list, save_path="results.json"): results = [] for params in params_list: start = time.time() _, growth = compute_growth_rate( win_prob=params["win_prob"], win_gain=params["win_gain"], lose_loss=params["lose_loss"] ) best_idx = int(np.nanargmax(growth)) results.append({ "win_prob": params["win_prob"], "win_gain": params["win_gain"], "lose_loss": params["lose_loss"], "best_fraction": float(fractions[best_idx]), "max_log_growth": float(growth[best_idx]), "elapsed_sec": round(time.time() - start, 4) }) with open(save_path, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2) return results

批量实验有几个工程化要点:

  • 参数列表存放在 JSON 或 YAML 配置文件中,不要硬编码在脚本里。
  • 每个实验记录随机种子、参数组合、结果和耗时,方便复现。
  • 大任务建议分批执行,把结果追加写盘,避免跑了一半程序崩溃导致所有结果丢失。
  • 遇到异常参数(比如负收益导致log报错),使用np.errstate捕获,不要让单个实验中断整体任务。

6.3 接口化封装

虽然原项目没有固定 API,但你可以把游戏核心封装成函数,再包一层 HTTP 服务,供本地工具调用:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class GameRequest(BaseModel): win_prob: float = 0.5 win_gain: float = 0.5 lose_loss: float = 0.4 n_agents: int = 10000 steps: int = 1000 seed: int = 42 class GameResponse(BaseModel): ensemble_mean_final: float ensemble_median_final: float @app.post("/simulate", response_model=GameResponse) def simulate(req: GameRequest): mean_path, median_path = simulate_ensemble( n_agents=req.n_agents, steps=req.steps, seed=req.seed ) return GameResponse( ensemble_mean_final=float(mean_path[-1]), ensemble_median_final=float(median_path[-1]) )

启动命令:

uvicorn main:app --host 127.0.0.1 --port 8000

调用方式:

import requests resp = requests.post("http://127.0.0.1:8000/simulate", json={ "win_prob": 0.5, "win_gain": 0.5, "lose_loss": 0.4, "n_agents": 10000, "steps": 1000, "seed": 42 }, timeout=30) print(resp.json())

如果你只是学习概念,不必引入 FastAPI;但如果你想把游戏集成到教学平台或内部工具里,这种封装方式很实用。

7. 资源占用与性能观察

这个模型是纯 CPU 计算,资源占用很低,但仍然有一些值得注意的性能点。

7.1 显存与内存

游戏不涉及 GPU,所以不存在显存问题。内存方面,如果你像 4.2 节那样用循环更新所有玩家的财富,内存占用只取决于n_agents这个一维数组的大小。10000 个float64大约只需要 80KB,可以忽略不计。

真正吃内存的是“保存所有人每个时间步的财富路径”。如果在循环里不断把中间结果 append 到列表,10000 人乘 1000 步,就是10000 × 1000个浮点数,约 80MB。如果扩大到 100 万人乘 10000 步,就是 8GB 以上,普通机器很容易卡死。

建议做法是:只在需要画图时保存均值/中位数这类聚合指标,不保存完整路径;如果需要完整路径用于分析分位数,可以周期性抽样保存,或者改用np.memmap写盘。

7.2 计算耗时

对 10000 个玩家跑 1000 轮,使用向量化操作,在普通 CPU 上通常只需要几十毫秒到几百毫秒。瓶颈主要发生在:

  • Python 循环次数:如果写成逐玩家、逐轮的双层循环,速度会慢几个数量级。
  • 随机数生成:rng.random(n_agents)是向量化操作,效率很高;不要每次循环里再生成单个随机数。
  • np.median在每轮调用会有排序开销,玩家数量大时可以改为每 10 轮或 50 轮记录一次中位数。

7.3 如何降低内存与加速

# 推荐:流式计算,不保存完整路径 def simulate_ensemble_streaming(n_agents=100000, steps=10000, seed=42): rng = np.random.default_rng(seed) wealth = np.ones(n_agents) sample_points = np.linspace(0, steps, 20, dtype=int) snapshot = {} for step in range(1, steps + 1): wins = rng.random(n_agents) < 0.5 wealth[wins] *= 1.5 wealth[~wins] *= 0.6 if step in sample_points: snapshot[int(step)] = { "mean": float(wealth.mean()), "median": float(np.median(wealth)), "p10": float(np.percentile(wealth, 10)), "p90": float(np.percentile(wealth, 90)) } return snapshot

这种流式写法在 10 万玩家、10000 轮规模下依然稳定。

8. 常见问题与排查方法

问题现象可能原因排查方式解决方案
每次运行结果都不一样未固定随机种子检查代码里是否调用np.random.default_rng(seed)显式传入固定 seed,并记录到结果文件
均值巨大而中位数极小非遍历性,属正常现象检查是否打印了中位数不要只报告均值,要同时输出中位数和分位数
loglog1p报 NaN下注比例导致1 - 0.4 * f出现负数检查fractions是否超过 2.5限制下注比例在[0, 1]区间,或使用np.clip
内存占用过高保存了完整财富路径查看代码中是否把所有历史 append 到列表改为只保存聚合指标或抽样快照
中文标签显示为方框matplotlib 字体缺少中文字体查看系统字体列表设置中文字体后再绘图
图中均值曲线消失未使用对数坐标,均值数量级远超纵轴范围查看plt.yscale设置改为plt.yscale("log")
批量任务中途崩溃,结果丢失没有断点续跑和结果写盘检查批量脚本是否批量写 JSON每完成一个实验立即追加写盘,记录进度
模拟结果和理论值偏差过大随机种子或样本量不足对比理论公式E[X] = 1.05^nsqrt(1.5*0.6)^n增大n_agents到 100000 以上再跑一次

9. 最佳实践与使用建议

9.1 教学场景建议

如果你准备用这个游戏给学生或读者讲概率论,建议按“先猜后跑”的顺序设计活动:

  • 先让观众口头回答:这个游戏每轮期望是正还是负?你愿意把全部身家押进去吗?
  • 再让观众估计:玩 100 轮后,10000 个玩家中大约多少人盈利?
  • 最后运行模拟,比较直觉和结果。

这种方式比直接给公式更能冲击认知。大约会有相当比例的观众凭“期望为正”认为值得玩,但模拟结果会告诉他们,绝大多数玩家在 100 轮后已经所剩无几。

9.2 工程化建议

  • 所有实验脚本必须固定随机种子,并在结果文件中记录 seed、参数、numpy 版本,保证可复现。
  • 模型文件、输入参数、模拟结果分目录管理,比如configs/results/figures/
  • 批量参数扫描使用向量化写法,避免 Python 双层循环。
  • 如果封装成接口服务,必须限制访问范围,只绑定127.0.0.1或内网地址,避免公网裸奔。
  • 对输出结果做可视化检查时,优先使用对数坐标,否则均值和中位数会互相掩盖。

9.3 合规与边界提醒

这个游戏用数学方式揭示了一个重要事实:在乘性随机过程中,单期期望收益不能直接等同于长期增长。这个概念在投资教育中有一定价值,但它不等于真实市场模型,更不能作为买卖依据。

如果要把游戏用于投资教育或金融相关内容,需要注意:

  • 明确标注这是简化教学模型,不包含交易成本、税费、流动性风险和黑天鹅事件。
  • 不要使用“稳赚”“必然”“最佳策略”等绝对化表述。
  • 涉及具体金融产品、资产配置或个人投资建议时,应避免给出确定性结论。
  • 引用他人的教学代码、图表或论文内容时,注明出处并遵守版权规定。

10. 总结与下一步

这个项目最值得尝试的一点,是它用最简单的方式击穿了“期望值为正就值得做”的常识。你不需要昂贵的计算设备,不需要安装乱七八糟的依赖,只需要一个 Python 环境,就能亲眼看到系综平均和时间平均的剪刀差。

我建议第一次跑通时,先做两件事:第一,跑 4.2 节的系综模拟,同时打印均值和分位数;第二,跑 6.1 节的参数扫描,找到最优下注比例。这两步加起来不超过五分钟,但你对“长期复利”“波动拖累”“期望值陷阱”这三个概念的理解会明显不一样。

接下来可以扩展的方向包括:

  • 加入凯利公式的严格推导,比较理论最优值和数值扫描结果。
  • 把正反面概率改得不均匀,观察概率偏差对最优仓位的影响。
  • 引入每次只下注固定金额而不是固定比例,对比“加法下注”和“乘法下注”的区别。
  • 把模型扩展到多资产组合,模拟再平衡对长期增长率的影响。
  • 如果面对的是普通读者,可以把模拟结果导出成图表,做成互动式教学页面。

如果你也想在自己的电脑上完整复现一遍,直接复制第 4 节的代码,安装好 numpy 和 matplotlib 就能开始。建议收藏备用,后续研究凯利公式和遍历性经济学时,这份模拟代码可以直接当测试底座。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:40:55

前端构建工具链拆解:从Webpack到Vite+tsup+Rolldown的迁移实践

Webpack 统治前端工程化的时间太久&#xff0c;久到很多团队已经把“项目复杂就必须上 Webpack”当成默认选项。但项目一旦过了某个规模&#xff0c;Webpack 的配置膨胀、构建变慢、调试链路拖泥带水&#xff0c;就会成为开发效率最直接的阻碍。用 TS tsup Vite Rolldown 这…

作者头像 李华
网站建设 2026/9/4 15:37:10

LVGL手表UI开发实战:内存、刷新、功耗与页面管理的平衡术

做嵌入式 GUI 有一个特别容易踩的坑&#xff1a;以为用 LVGL 做手表 UI&#xff0c;重点是把界面画得好看。真到上手时会发现&#xff0c;在 MCU 上做手表界面&#xff0c;难点从来不在某个控件怎么用&#xff0c;而在内存、刷新、事件和功耗这四件事怎么平衡。特别是当你决定做…

作者头像 李华
网站建设 2026/9/5 14:38:27

信号与系统必考点:单位冲激函数性质与解题套路

信号与系统这门课&#xff0c;网上讨论度最高的两个考点&#xff0c;一个是卷积&#xff0c;另一个就是单位冲激函数。很多新手看到教材里写“δ(t) 在零点等于无穷大”就直接懵掉&#xff0c;觉得这是一个数学家拿来吓人的概念。实际上&#xff0c;在“做题”这个层面&#xf…

作者头像 李华
网站建设 2026/9/5 18:07:19

网约车抽成比例下调:规则引擎如何支撑计费系统灵活调整

最近不少城市都在讨论网约车平台下调抽成比例的事情。作为开发者&#xff0c;我们看到的可能不只是一个“比例数字变化”&#xff0c;而是一整套计费系统、规则配置、结算链路和数据对账逻辑需要跟着调整。业务侧一句话&#xff0c;技术侧往往要动好几个服务。这篇文章想从工程…

作者头像 李华
网站建设 2026/9/6 0:40:12

上帝视角不是一张图:从无人机航拍到实景三维的空间数据工作流

先从一个真实画面说起。一台多旋翼无人机起飞后&#xff0c;按照规划好的航线采集了几百张影像&#xff1b;另一边&#xff0c;十几个固定在塔吊和围挡上的摄像头正在回传现场画面&#xff1b;调度室里&#xff0c;项目经理盯着大屏&#xff0c;不再需要反复切单路画面&#xf…

作者头像 李华