ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

遍历性游戏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 均可Python3.8 及以上NumPy1.21 及以上Matplotlib3.5 及以上编辑器VS Code、PyCharm、Jupyter Notebook 都可以如果你的环境里还没有安装依赖库可以执行pip install numpy matplotlib如果是在国内使用 pip 下载较慢可以使用镜像源pip install numpy matplotlib -i https://pypi.tuna.tsinghua.edu.cn/simple2.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方式更独立、更稳定不会影响其他模块的随机状态。文中的代码固定seed42所以你自己运行得到的结果会和博文基本一致。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, axis1) # 拼接第0轮所有人初始财富相同 wealth np.hstack([np.ones((N_PLAYERS, 1)), cumulative]) * START_WEALTH return wealth # 开始模拟 wealth run_ensemble() # 集合平均每一轮所有玩家财富的平均值 ensemble_mean wealth.mean(axis0) # 集合中位数每一轮所有玩家财富的中位数 ensemble_median np.median(wealth, axis0) # 单一个体的路径我们取第一个玩家 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, labelensemble mean, linewidth2) plt.plot(ensemble_median, labelensemble median, linewidth2) plt.plot(single_path, labelsingle player path, alpha0.6) plt.plot(theory_time, labeltheory time average, linestyle--, linewidth2) plt.plot(theory_ensemble, labeltheory ensemble average, linestyle--, linewidth2) plt.yscale(log) plt.xlabel(round) plt.ylabel(wealth (log scale)) plt.title(fErgodicity Game: {N_PLAYERS} players x {TRIALS} rounds) plt.legend() plt.grid(True, whichboth, alpha0.3) plt.tight_layout() plt.savefig(output/ergodicity_game.png, dpi150) plt.show()运行命令python main_compare.py4.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_players10000, trials1000, seed1): rng np.random.default_rng(seed) wealth np.full(n_players, START_WEALTH, dtypefloat) history np.empty((n_players, trials 1), dtypefloat) 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
返回列表