当前位置: 首页 > news >正文

遍历性游戏Python模拟:期望正收益为何长期亏损?

第一次看到 Ergodicity Game(遍历性游戏)时,我的第一反应是:这不就是个概率题吗?一个抛硬币游戏,正面赚 50%,反面亏 40%,单轮期望明明是正的,长期玩下去怎么可能会亏?直到自己把蒙特卡洛模拟跑出来,看到那条中位数财富曲线一路跌到几乎为零,才真正意识到这个结论有多反直觉。

本文会用 Python 从零实现这个经典实验,对比“集合平均”和“时间平均”的本质差异,并结合 Kelly 公式分析下注比例。代码全部给出,环境只需要 NumPy 和 Matplotlib。想理解 Ergodicity Economics、想加深对复利和风险的理解,或者单纯想复现这个有趣模拟的同学,都可以跟着本文完整跑一遍。

1. “遍历性游戏”是什么:一个反直觉的抛硬币实验

1.1 游戏规则

“Ergodicity Game”最早由物理学家 Ole Peters 在遍历性经济学的研究中广泛使用,也被很多概率论和金融课程拿来当课堂演示。游戏规则非常简单:

  • 初始资金为 1 个单位,计为 (W_0)。
  • 每一轮抛一枚公平硬币。
  • 如果正面朝上,资金乘以 1.5,也就是赚 50%。
  • 如果反面朝上,资金乘以 0.6,也就是亏 40%。
  • 游戏可以连续进行任意轮,你可以选择全仓参与,也可以只投入一部分资金。

这里需要注意:反面并不是“损失下注金额的 40%”,而是在原有资金基础上直接打六折。也就是说,如果你有 100 元,反面结束后就变成 60 元。如果下一轮继续反面,就变成 36 元。这种“连乘”结构是后续所有反直觉结论的关键。

1.2 从数学期望看:这游戏应该稳赚

先算单轮数学期望。每一轮赚 50% 和亏 40% 的概率都是 0.5,那么单轮期望收益率为:

[ E[R] = 0.5 \times 50% + 0.5 \times (-40%) = 5% ]

也就是说,如果只玩一轮,平均收益是 5%。如果连续玩 (T) 轮,从期望值角度出发,财富的期望可以写成:

[ E[W_T] = W_0 \times 1.05^T ]

把这个公式代入具体数字,结果非常惊人。初始 100 元,玩 100 轮后,平均财富大约是:

[ 100 \times 1.05^{100} \approx 13150 ]

从“平均”的角度看,这个游戏简直是一个印钞机,玩 100 轮资金能翻 130 倍。这就是绝大多数人第一眼看到这个游戏时的判断:期望为正,应该值得玩。

1.3 从时间平均看:典型路径几乎必亏

问题在于,“数学期望”描述的是大量平行世界的人在同一时刻的平均财富,而不是某一个人连续玩 100 轮之后最可能出现的财富。

如果一个人长期玩下去,每一轮赚 50% 和亏 40% 都会作用在同一个本金上。两轮一组的典型结果是:先赚后亏,或者先亏后赚。无论哪种顺序,财富都会变成:

[ 1 \times 1.5 \times 0.6 = 0.9 ]

也就是说,连续玩两轮,最典型的结果不是赚 10.25%(也就是 (1.05^2)),而是变成原来的 0.9 倍,亏掉 10%。因此,每一轮“时间平均”意义上的增长因子是:

[ \sqrt{1.5 \times 0.6} = \sqrt{0.9} \approx 0.9487 ]

如果初始资金是 100 元,玩 100 轮后,一个典型个体最可能的财富是:

[ 100 \times 0.9487^{100} \approx 0.52 ]

这意味着,虽然所有玩家的平均财富能涨到一万多元,但一个普通玩家最可能的结果是从 100 元亏到只剩 0.52 元。这就是“期望为正,长期必亏”的核心原因。

1.4 遍历性:集合平均不等于时间平均

Ergodicity Game 背后真正想说明的概念是“遍历性”。

在概率论中,一个随机过程是否具有遍历性,可以粗略理解为:用大量个体在同一时刻取平均值,和用同一个个体在很长时间段上取平均值,结果是否一致。

  • 集合平均:比如 10000 个玩家同时玩第 100 轮,取这 10000 个人财富的平均值。
  • 时间平均:一个人连续玩 10000 轮,看这条财富路径的长期平均增长率。

在 Ergodicity Game 中,集合平均和时间平均完全不同。10000 个玩家的平均财富会按 (1.05^t) 指数上涨,但任何一个典型个体的财富路径都按 (0.9487^t) 指数下跌。两者方向完全相反。

下面用一个表格直观展示遍历系统和非遍历系统的差别:

维度遍历系统非遍历系统(本游戏)
时间平均与集合平均相等不相等
典型个体体验与总体平均基本一致与总体平均差异巨大
能否用期望值直接决策可以不可以
典型例子大量独立重复抛硬币的次数统计财富反复乘以随机因子

理解这一点非常重要。很多金融决策模型默认使用“期望收益”作为目标函数,但真实世界中的个体只能经历一条路径,而这条路径由“时间平均”决定,而不是“集合平均”。

2. 环境准备与实验设计

在开始写代码之前,先确认实验环境。

2.1 运行环境

本文示例基于以下环境:

  • 操作系统:Windows 10 / macOS / Linux 均可
  • Python:3.8 及以上
  • NumPy:1.21 及以上
  • Matplotlib:3.5 及以上
  • 编辑器:VS Code、PyCharm、Jupyter Notebook 都可以

如果你的环境里还没有安装依赖库,可以执行:

pip install numpy matplotlib

如果是在国内使用 pip 下载较慢,可以使用镜像源:

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

2.2 项目结构

建议创建一个独立目录,代码分为三个文件,方便分别运行:

ergodicity_game/ ├── main_compare.py # 主实验:集合平均 vs 中位数 vs 时间平均 ├── kelly_scan.py # 进阶实验:扫描 Kelly 下注比例 ├── withdrawal.py # 扩展实验:利润取现 └── output/ # 保存图片

2.3 随机种子

蒙特卡洛模拟依赖随机数生成。为了让实验结果可复现,我会使用numpy.random.default_rng(seed)这样的随机数生成器,而不是全局的np.random.seed

default_rng是 NumPy 1.17 之后推荐的随机数接口,它比旧的np.random.seed方式更独立、更稳定,不会影响其他模块的随机状态。文中的代码固定seed=42,所以你自己运行得到的结果会和博文基本一致。

3. 为什么期望为正,长期却是亏损?

这一节从数学上拆解上一节的反直觉结论。

3.1 对数增长率才是时间平均的关键

设一轮后的财富乘子为随机变量 (X),它有两个取值:

[ X = \begin{cases} 1.5, & \text{概率 } 0.5 \ 0.6, & \text{概率 } 0.5 \end{cases} ]

如果一个人玩了 (T) 轮,那么最终财富可以写成:

[ W_T = W_0 \times X_1 \times X_2 \times \cdots \times X_T ]

两边取对数,把连乘变成连加:

[ \ln W_T = \ln W_0 + \ln X_1 + \ln X_2 + \cdots + \ln X_T ]

根据大数定律,当 (T) 足够大时,样本均值会收敛到期望值。所以长期来看,每轮的对数增长率收敛到:

[ E[\ln X] = 0.5 \times \ln 1.5 + 0.5 \times \ln 0.6 ]

代入数值:

[ E[\ln X] = 0.5 \times 0.405465 + 0.5 \times (-0.510826) \approx -0.05268 ]

也就是说,财富对数每轮平均下降 0.05268,对应每轮典型增长因子:

[ e^{-0.05268} \approx 0.9487 ]

这就是前面提到的 0.9487 的由来。结论是:虽然线性空间下单轮期望收益率是正的 5%,但对数空间下的期望增长率是负的 5.27%。

3.2 风险倍增:从不平等关系到波动率拖累

另一个理解角度来自连续时间金融中的“波动率拖累”公式。

一个资产的单期算术平均收益率为 (\mu),波动率为 (\sigma),在连续复利近似下,长期对数增长率 (g) 近似为:

[ g \approx \mu - \frac{\sigma^2}{2} ]

在 Ergodicity Game 中,单期收益率只有两个值:+50% 和 -40%,于是:

[ \mu = 0.5 \times 0.5 + 0.5 \times (-0.4) = 0.05 ]

[ \sigma^2 = 0.5 \times (0.5 - 0.05)^2 + 0.5 \times (-0.4 - 0.05)^2 = 0.2025 ]

代入公式:

[ g \approx 0.05 - \frac{0.2025}{2} = -0.05125 ]

也就是大约每年亏损 5.125%。这个结果和精确的 (0.5 \ln 0.9 \approx -0.05268) 非常接近。公式里的 (\sigma^2 / 2) 就是“波动率拖累”,它说明了为什么波动本身会消耗复利收益。

3.3 中位数和均值为什么会分道扬镳

由于财富是乘性增长,经过很多轮后,财富分布会变成强烈的右偏分布。绝大多数玩家的财富会不断向 0 靠拢,而极少数连续多次赚钱的幸运儿财富会指数级增长,把整体均值拉得很高。

因此,均值描述的是“平行世界中所有人的平均结果”,而中位数更能代表“一个典型个体玩 (T) 轮后最可能出现的结果”。在 Ergodicity Game 中:

  • 第 100 轮均值财富约等于 (1.05^{100} \approx 131.5)
  • 第 100 轮中位财富约等于 (0.9487^{100} \approx 0.0052)

两者相差超过 25000 倍。如果不区分这两个指标,很容易得出完全错误的结论。

3.4 验证公式的极简代码

先把上面的计算用代码验证一下:

import numpy as np up = 1.5 down = 0.6 p = 0.5 log_g = p * np.log(up) + (1 - p) * np.log(down) typical_factor = np.exp(log_g) theoretical_mean = 1.05 ** 100 typical_wealth_100 = np.exp(log_g * 100) print("每轮对数增长率:", log_g) print("每轮典型增长因子:", typical_factor) print("100轮后典型财富(初始=1):", typical_wealth_100) print("100轮后理论均值财富:", theoretical_mean)

运行后可以看到类似输出:

每轮对数增长率: -0.052680... 每轮典型增长因子: 0.948683... 100轮后典型财富(初始=1): 0.005186... 100轮后理论均值财富: 131.501...

4. 蒙特卡洛模拟实战:用代码验证遍历性陷阱

理解了数学原理后,下面通过蒙特卡洛模拟直观地看到“集合平均”和“时间平均”的分裂。

4.1 创建主实验文件

在项目目录下创建main_compare.py,内容如下:

# main_compare.py import numpy as np import matplotlib.pyplot as plt # 参数配置 N_PLAYERS = 1000 # 玩家数量:模拟1000个平行玩家 TRIALS = 100 # 游戏轮数 START_WEALTH = 1.0 # 初始资金 UP = 1.5 # 正面:资金乘1.5 DOWN = 0.6 # 反面:资金乘0.6 SEED = 42 # 随机种子,保证可复现 def run_ensemble(): """向量化生成所有玩家每一轮的财富。 返回的数组形状为 (N_PLAYERS, TRIALS + 1), 每一行代表一个玩家的完整财富路径。 """ rng = np.random.default_rng(SEED) # 生成 (N_PLAYERS, TRIALS) 的硬币结果 coin = rng.random((N_PLAYERS, TRIALS)) < 0.5 # 将布尔值映射为财富乘子 factors = np.where(coin, UP, DOWN) # 对每一行做累计乘法,得到每一轮的累计乘子 cumulative = np.cumprod(factors, axis=1) # 拼接第0轮:所有人初始财富相同 wealth = np.hstack([np.ones((N_PLAYERS, 1)), cumulative]) * START_WEALTH return wealth # 开始模拟 wealth = run_ensemble() # 集合平均:每一轮所有玩家财富的平均值 ensemble_mean = wealth.mean(axis=0) # 集合中位数:每一轮所有玩家财富的中位数 ensemble_median = np.median(wealth, axis=0) # 单一个体的路径:我们取第一个玩家 single_path = wealth[0] # 理论曲线 log_g = 0.5 * np.log(UP) + 0.5 * np.log(DOWN) theory_time = START_WEALTH * np.exp(log_g * np.arange(TRIALS + 1)) theory_ensemble = START_WEALTH * (1.05 ** np.arange(TRIALS + 1)) # 打印第100轮的结果 print(f"第 {TRIALS} 轮 群体平均财富: {ensemble_mean[-1]:.4f}") print(f"第 {TRIALS} 轮 群体中位财富: {ensemble_median[-1]:.4f}") print(f"第 {TRIALS} 轮 理论均值(1.05^{TRIALS}): {theory_ensemble[-1]:.4f}") print(f"第 {TRIALS} 轮 理论时间平均: {theory_time[-1]:.4f}") # 画图:注意使用对数坐标 plt.figure(figsize=(10, 6)) plt.plot(ensemble_mean, label="ensemble mean", linewidth=2) plt.plot(ensemble_median, label="ensemble median", linewidth=2) plt.plot(single_path, label="single player path", alpha=0.6) plt.plot(theory_time, label="theory time average", linestyle="--", linewidth=2) plt.plot(theory_ensemble, label="theory ensemble average", linestyle="--", linewidth=2) plt.yscale("log") plt.xlabel("round") plt.ylabel("wealth (log scale)") plt.title(f"Ergodicity Game: {N_PLAYERS} players x {TRIALS} rounds") plt.legend() plt.grid(True, which="both", alpha=0.3) plt.tight_layout() plt.savefig("output/ergodicity_game.png", dpi=150) plt.show()

运行命令:

python main_compare.py

4.2 预期输出

运行后控制台会打印类似下面的结果:

第 100 轮 群体平均财富: 128.4231 第 100 轮 群体中位财富: 0.0051 第 100 轮 理论均值(1.05^100): 131.5013 第 100 轮 理论时间平均: 0.0052

由于固定了随机种子,不同机器上的结果会有微小差异,但数量级应该一致。群体平均财富在一百二十左右,而中位财富只剩 0.005 左右。这就是遍历性缺失最直观的体现。

4.3 图像说明

生成的output/ergodicity_game.png图片中,可以看到五条线:

  • 实线蓝色线是群体平均财富,一路向上,最终到达大约 130。
  • 实线橙色线是群体中位数财富,一路向下,最终接近 0.005。
  • 绿色半透明线是第一个玩家的单一路径,上下震荡,但整体趋势也是向下的。
  • 蓝色虚线是理论集合平均曲线 (1.05^t),和群体平均基本重合。
  • 红色虚线是理论时间平均曲线 (e^{-0.05268t}),和中位数以及单一路径方向一致。

这里必须使用plt.yscale("log")。因为均值 128 和中位数 0.005 相差超过 25000 倍,如果使用线性坐标,中位数曲线会完全贴死在横轴上,图像就失去了对比意义。

4.4 每次运行结果为什么不同

如果你把SEED = 42删掉,或者改用别的随机种子,最终数值会略有变化。这是蒙特卡洛模拟的正常现象:每次生成的硬币序列不同,路径自然不同。

但从统计规律上看,无论换多少随机种子,下面两个结论都稳定成立:

  • 群体平均财富指数级上升,接近理论均值。
  • 中位数财富指数级下降,接近理论时间平均。

这正好说明,单次结果具有随机性,但集合平均和时间平均的方向是确定的。为了科学对比,建议始终固定随机种子。

4.5 更专业的替代写法:逐轮循环

上面的写法使用cumprod一次性生成了所有玩家所有轮次的财富,优点是代码简洁,缺点是当玩家数和轮数非常大时,内存占用会很高。

例如,10 万玩家 × 1000 轮,生成的矩阵为 10 万 × 1000,按浮点型计算大约占用 800 MB 内存。这种情况下可以改用逐轮循环:

def run_ensemble_loop(n_players=10000, trials=1000, seed=1): rng = np.random.default_rng(seed) wealth = np.full(n_players, START_WEALTH, dtype=float) history = np.empty((n_players, trials + 1), dtype=float) history[:, 0] = wealth for t in range(1, trials + 1): coin = rng.random(n_players) < 0.5 wealth = wealth * np.where(coin, UP, DOWN) history[:, t] = wealth return history

两种写法结果一致,如果内存足够,优先使用向量化写法;如果规模超大,就使用循环写法。

5. 进阶实验:Kelly 下注比例与利润取现

上面模拟的规则有一个隐藏前提:玩家每一轮都把全部财富暴露在风险下。如果只拿一部分资金投入游戏,剩余资金保留为现金,长期结果会发生很大变化。这就引出了 Kelly 公式。

5.1 引入下注比例 f

假设每轮把财富的 (f) 比例投入游戏,比例 (1-f) 保留为现金。

  • 正面:参与部分的资金乘以 1.5,于是总资产乘子为:

[ (1-f) + f \times 1.5 = 1 + 0.5f ]

  • 反面:参与部分的资金乘以 0.6,于是总资产乘子为:

[ (1-f) + f \times 0.6 = 1 - 0.4f ]

那么长期对数增长率为:

[ g(f) = 0.5 \times \ln(1 + 0.5f) + 0.5 \times \ln(1 - 0.4f) ]

对 (f) 求导,令导数为 0:

[ \frac{0.25}{1 + 0.5f} - \frac{0.2}{1 - 0.4f} = 0 ]

解得:

[ f = 0.25 ]

也就是说,在这个游戏里,最优下注比例是 25%。超过 25% 反而会降低长期增长率;如果达到 100%,长期增长率就是上一节算出的负值。

5.2 扫描 Kelly 比例的代码

在项目目录下创建kelly_scan.py

# kelly_scan.py import numpy as np import matplotlib.pyplot as plt def growth_rate(f): """给定下注比例 f,返回长期对数增长率。""" if f < 0 or f >= 1: return -np.inf # 正面:1 + 0.5f;反面:1 - 0.4f return 0.5 * np.log(1 + 0.5 * f) + 0.5 * np.log(1 - 0.4 * f) # 在 [0, 1] 范围内均匀取 500 个点 f_values = np.linspace(0, 1, 500) g_values = np.array([growth_rate(f) for f in f_values]) # 找最大增长率对应的 f optimal_idx = np.argmax(g_values) optimal_f = f_values[optimal_idx] optimal_g = g_values[optimal_idx] print(f"最优下注比例: {optimal_f:.3f}") print(f"最优长期对数增长率: {optimal_g:.4f}") print(f"全仓下注(g(1))长期对数增长率: {growth_rate(1):.4f}") # 画图 plt
http://www.cnnetsun.cn/news/4304373.html

相关文章:

  • Spring AI 2.0实战:从多模型到Agent的一周学习路线
  • 单片机电源管理:12V转5V转3.3V两级降压方案设计与调试
  • 分布式服务的自动巡检设计
  • 爱奇艺iOS校招笔试全复盘:核心考点、解题思路与备战策略
  • you-get -I 批量下载:一个文本文件搞定100条URL
  • 前端两年经验跳槽面经:从简历准备到高频面试题拆解
  • Magisk Root 完全掌握:从原理到定制的完整指南
  • Pandas入门指南:2小时掌握DataFrame数据清洗与分组聚合
  • 科学计算日常巡检的有效方法
  • 从老源码到现代API:接口设计、安全与实战全解析
  • Typst 快速上手:5 分钟从零编译出第一份 PDF 文档
  • VS2019+OSG+osgEarth+GDAL+Qt全链路编译指南:三维GIS开发环境搭建
  • 快速上手 whisper.cpp:把语音转文字搬回自己设备的完整指南
  • Starship 提示符提速:3 档方案把 500ms 压进 50ms
  • MRIcroGL完全上手:从DICOM转换到出版级脑图渲染
  • 用 Remotion 一个下午做出三语视频:React 视频国际化的完整实战
  • AI音乐应用落地避坑指南:从提示词到音频交付的完整链路实践
  • 旧设备新生:reMarkable 2 的固件升级、SSH与自动化维护
  • whisper.cpp 配置 CUDA 加速,长音频语音转写跑到实时以上
  • 60G高清纹理Mod详解:黑暗之魂2画质升级与龙祭坛跑图指南
  • 无订阅AI生图与AI生视频:从模型原理到工程落地
  • ESP32上实现LLM思考过程可视化:用Brainscope调试微型语言模型
  • LlamaIndex 安装与快速上手指南:3条路线让私有文档接入大模型问答
  • 可持久化并查集:一片森林为何只需一个根?
  • trackerslist:BT 下载没节点?每天自动更新的公共 Tracker 列表配置指南
  • 网易Java实习生笔试题深度拆解:HashMap、并发与JVM实战
  • LangChain自定义工具失灵?拆解ReAct循环定位问题
  • 用Common Lisp实现LLM推理引擎:AVX2加速与多线程实战
  • 网络延迟与资源消耗的取舍
  • 软件定义汽车核心:车规级存储架构与设计实践