第 16 章 蒙特卡罗模拟
16 MONTE CARLO SIMULATION
曲政 / 2018-05-09
什么是蒙特卡洛模拟。
蒙特卡罗模拟用于求事件的近似概率,它多次执行同一模拟,然后将结果进行平均。
概率和赌运气有什么联系?
乌拉姆不是第一个想使用概率工具来理解赌运气游戏 a game of chance 的数学家。
概率的历史与赌博的历史紧密相连。不确定性的存在使赌博成为可能,赌博的存在又促进了用来解释不确定性的数学理论的发展。
为概率论的奠基做出重要贡献的有卡尔达诺 Cardano、帕斯卡 Pascal、费马 Fermat、伯努利 Bernoulli、棣莫弗 de Moivre 和拉普拉斯 Laplace,他们的目的都是为了更好地理解(也可能是赢得)赌运气游戏。
16.1 Pascal’s Problem
帕斯卡和费马怎么回答他朋友的问题?
帕斯卡对概率论这个领域产生兴趣是因为他的朋友问了他一个问题,即 “连续掷一对骰子 24 次得到两个 6” 这个赌注是否有利可图。这在 17 世纪中叶是非常困难的一个问题。
- 第一次投掷时,每个骰子掷出 6 的概率是 1/6,所以两个骰子都掷出 6 的概率是 1/36;
- 因此,第一次投掷时没有掷出两个 6 的概率是 1 - 1/36 = 35/36;
- 因此,连续 24 次投掷都没有掷出两个 6 的概率是 (35/36) 24,差不多是 0.51;
- 所以掷出两个 6 的概率是 1 - (35/36) 24,大约是 0.49。长期来看,在 24 次投掷中掷出两个 6 这个赌注是无利可图的。
关键思想: 正反转换 独立相乘 长期
不懂概率分析,可以用代码来模拟出结果:
import random
def rollDie ():
return random.choice ([1,2,3,4,5,6])
def checkPascal (numTrials):
""" 假设 numTrials 是正整数
输出获胜概率的估值 """
numWins = 0
for i in range (numTrials):
for j in range (24):
d1 = rollDie ()
d2 = rollDie ()
if d1 == 6 and d2 == 6:
numWins += 1
break
print ('Probability of winning =', numWins/numTrials)
checkPascal (1000000)
等两分钟,terminal 输出: Probability of winning = 0.492051
在 Python shell 中输入 1-(35/36)**24 会计算出 0.49140387613090342。
16.2 Pass or Don’t Pass
过线、不过线游戏怎么描述?
有些赌运气游戏的问题是很难找到答案的。在双骰儿赌博中,掷手 shooter(即掷骰子的人)可以选择在 “过线 pass line” 或 “不过线 don’t pass line” 之间投注。
过线:如果初掷是 “自然点 natural”(7 或 11),那么掷手获胜;如果初掷是 “垃圾点 craps”(2、3 或 12),那么掷手失败。如果掷出其他数字,这个数字就成为 “点数 point”,掷手继续掷骰子。如果掷手在掷出 7 之前掷出这个点数,那么掷手获胜,否则掷手失败。
- 7 就是线?
不过线:如果初掷是 7 或 11,那么掷手失败;如果初掷是 2 或 3,那么掷手获胜;如果初掷是 12,则是平局(赌博的行话称为 push)。如果掷出其他数字,那么这个数字成为 “点数”,掷手继续掷骰子。如果掷手在掷出这个点数之前掷出 7,那么掷手获胜,否则掷手失败。
是否有一种赌注比另一种更好呢?还是说二者都一样?通过分析推导可以回答这些问题,但(至少对我们来说)编写一个程序的方式会更容易。
投资回报率怎么写?在掷骰子游戏中为什么可以那么写?
投资回报率 Return on Investment 由以下公式定义:
(更精确地说,这个公式定义的是 “简单 ROI”,它不考虑投资开始时和取得回报时的时间价值方面的差异。当进行投资和看到财务上的收益之间的时间非常长的时候(例如,在大学教育上的投资),应该考虑时间价值。对于双骰儿游戏来说,这不是什么问题。)
ROI = 投资纯收益 / 投资成本 = (投资收益 - 投资成本) / 投资成本
在骰子游戏中,下 100 次注,每次 1 元钱,赢了拿走 2 元,输了什么也拿不回来来。所以总的投资成本就是下注次数。
投资纯收益 = 拿回的钱 - 给出的钱 = 赢的次数 * 2 - 下注次数 * 1 = 赢的次数 - 输的次数,
所以:
举例来说,如果你对过线投注 100 次,并且获胜 50 次,那么你的 ROI 就是:
\[\frac {50-50}{100}=0\]
如果你对不过线投注 100 次,并且获胜 25 次,平局 5 次,那么 ROI 就应该是:
\[\frac {25-70}{100}=\frac {-45}{100}=-4.5\]
双骰子游戏对过线和不过线的模拟结果,怎么理解?
# crapsSim (20, 10)
# Pass: Mean ROI = 2.0% Std. Dev. = 14.6969%
# Don't pass: Mean ROI = -6.0% Std Dev = 14.9666%
# crapsSim (20, 10)
# Pass: Mean ROI = -0.0% Std. Dev. = 26.8328%
# Don't pass: Mean ROI = -3.5% Std Dev = 25.7924%
# crapsSim (20, 10)
# Pass: Mean ROI = -14.0% Std. Dev. = 19.5959%
# Don't pass: Mean ROI = 11.0% Std Dev = 18.8149%
# crapsSim (100000, 10)
# Pass: Mean ROI = -1.463% Std. Dev. = 0.3025%
# Don't pass: Mean ROI = -1.3021% Std Dev = 0.2844%
# crapsSim (20, 100000)
# Pass: Mean ROI = -1.3093% Std. Dev. = 22.3475%
# Don't pass: Mean ROI = -1.4659% Std Dev = 22.0177%
# def rollDie ():
# return random.choice ([1,1,2,3,3,4,4,5,5,5,6,6])
# crapsSim (1000000, 10) 会得到以下结果:
Pass: Mean ROI = 6.7385% Std. Dev. = 0.13%
Don't pass: Mean ROI = -9.5186% Std Dev = 0.1226%
- 每手投注 20 把,做 10 次游戏,结果很不稳定。标准差非常大。
- 每手投注十万把,做 10 次游戏,标准差变小,ROI 收敛,接近于概率分析值:过线 ROI 为 - 1.414%,不过线 ROI 为 - 1.364%。
- 看上去似乎不过线的投注稍好一点,但我们完全不能指望靠它来挣钱。如果过线投注和不过线投注的 95% 置信区间没有重合,就可以认为这两个均值之间的差异在统计上是显著的。7 但是它们重合了,所以我们不能得出任何确定的结论。
- 假设不增加每次游戏的手数,而是增加游戏次数,每手投注 20 把,做十万次模拟,标准差还是很高。
- 骰子的微小改变,会导致获胜几率的巨大倾斜。
为什么不增加每次游戏的手数,而是增加游戏次数,每手投注 20 把,做十万次模拟,标准差还是很高?
按照我过去的理解,多做实验,在更多的数据中取得平均值和标准差,标准差会更小。这里或许有特殊。
def crapsSim (handsPerGame, numGames):
""" 假设 handsPerGame 和 numGames 是正整数
玩 numGames 次游戏,每次 handsPerGame 手;输出结果。"""
games = []
#玩 numGames 次游戏,为了取平均值。
for t in range (numGames):
c = CrapsGame ()
for i in range (handsPerGame):
c.playHand_with_dict ()
games.append (c)
#为每次游戏生成统计量
pROIPerGame, dpROIPerGame = [], []
for g in games:
# Return on Investment
wins, losses = g.passResults ()
pROIPerGame.append ((wins - losses)/float (handsPerGame))
wins, losses, pushes = g.dpResults ()
dpROIPerGame.append ((wins - losses)/float (handsPerGame))
#生成并输出摘要统计量
# 先乘 100,再做 round,不然会出尾数。
meanROI = str (round ((100*sum (pROIPerGame)/numGames), 4)) + '%'
sigma = str (round (100*stdDev (pROIPerGame), 4)) + '%'
print ('Pass:', 'Mean ROI =', meanROI, 'Std. Dev. =', sigma)
meanROI = str (round ((100*sum (dpROIPerGame)/numGames), 4)) +'%'
sigma = str (round (100*stdDev (dpROIPerGame), 4)) + '%'
print ('Don\'t pass:','Mean ROI =', meanROI, 'Std Dev =', sigma)
handsPerGame 影响的是 wins 和 losses 的值。它们不能太小,因为要做差后再做除法。
numGames 影响的是 games 列表的长度,用来算取平均数和标准差。
经过几轮实验,确实后者对标准差的影响很小。
这一观察更正了我对数组长度对标准差影响的认识。
双骰子游戏,过线和不过线投注的模拟,代码怎么写?
import random, pylab
# 引用 stdDev 就够了。stdDev 引用的 variance 不用单独提出来。
# 注意引用 module 中的可执行命令都会得到执行,最好先行注释掉。
from em_15_3_flip import stdDev
def rollDie ():
"""
正常的单骰子,随机得到 1~6 之间的一个正整数。
:return: 正整数,在 1~6 之间
"""
return random.choice ([1,2,3,4,5,6])
# return random.choice ([1,1,2,3,3,4,4,5,5,5,6,6])
class CrapsGame (object):
"""
给双骰子赌博建抽象类,特殊目的是比较 pass 和 dp 的投资回报率。
因此,没有区分 game 是 pass line 还是 don't pass line.
而是分别建立 instance attibutes。
"""
def __init__(self):
self.passWins, self.passLosses = 0, 0
self.dpWins, self.dpLosses, self.dpPushes = 0, 0, 0
def playHand (self):
# 记录双骰子结果
throw = rollDie () + rollDie ()
if throw == 7 or throw == 11:
self.passWins += 1
self.dpLosses += 1
elif throw == 2 or throw == 3 or throw == 12:
self.passLosses += 1
if throw == 12:
self.dpPushes += 1
else: # throw = 2 或 3
self.dpWins += 1
else: # throw = 4 5 6 8 9 10
# 上次 throw 存为 point 点数
point = throw
while True:
# 再扔一把
throw = rollDie () + rollDie ()
if throw == point: # 先达到了点数
self.passWins += 1
self.dpLosses += 1
break
elif throw == 7: # 先达到了 7 线
self.passLosses += 1
self.dpWins += 1
break
def playHand_with_dict (self):
"""playHand 函数的另外一种更快的实现方式"""
pointsDict = {4: 1 / 3, 5: 2 / 5, 6: 5 / 11, 8: 5 / 11, 9: 2 / 5, 10: 1 / 3}
throw = rollDie () + rollDie ()
if throw == 7 or throw == 11:
self.passWins += 1
self.dpLosses += 1
elif throw == 2 or throw == 3 or throw == 12:
self.passLosses += 1
if throw == 12:
self.dpPushes += 1
else:
self.dpWins += 1
else:
# 下面比的是出现的相对概率,道理如下:
# 比如 8 点,两个骰子组合出 8 点有 5 种情况,组合出 7 点有 6 种情况。掷出 11 种情况中的任何一种,游戏结束。
# 只看这 11 种可以让游戏结束的组合,那么 8 点相对于 7 点,出现的相对概率是 5/11。
# 上面那句也可以理解成:把 [0, 1) 区间均匀分成了 11 份。生成一个 [0,1) 间的随机数,如果它落在左边 5 份,则认为是 8 点,如果它落在了右边 6 份,则认为是 7 点。
if random.random () <= pointsDict [throw]: # 在掷出 7 之前掷出点数
self.passWins += 1
self.dpLosses += 1
else: # 在掷出点数之前掷出 7
self.passLosses += 1
self.dpWins += 1
# 把 pass line 的信息打包输出。
def passResults (self):
return (self.passWins, self.passLosses)
# 把 don't pass line 的信息打包输出。
def dpResults (self):
return (self.dpWins, self.dpLosses, self.dpPushes)
def crapsSim (handsPerGame, numGames):
""" 假设 handsPerGame 和 numGames 是正整数
玩 numGames 次游戏,每次 handsPerGame 手;输出结果。"""
games = []
#玩 numGames 次游戏,为了取平均值。
for t in range (numGames):
c = CrapsGame ()
for i in range (handsPerGame):
c.playHand_with_dict ()
games.append (c)
#为每次游戏生成统计量
pROIPerGame, dpROIPerGame = [], []
for g in games:
# Return on Investment
wins, losses = g.passResults ()
pROIPerGame.append ((wins - losses)/float (handsPerGame))
wins, losses, pushes = g.dpResults ()
dpROIPerGame.append ((wins - losses)/float (handsPerGame))
#生成并输出摘要统计量
# 先乘 100,再做 round,不然会出尾数。
meanROI = str (round ((100*sum (pROIPerGame)/numGames), 4)) + '%'
sigma = str (round (100*stdDev (pROIPerGame), 4)) + '%'
print ('Pass:', 'Mean ROI =', meanROI, 'Std. Dev. =', sigma)
meanROI = str (round ((100*sum (dpROIPerGame)/numGames), 4)) +'%'
sigma = str (round (100*stdDev (dpROIPerGame), 4)) + '%'
print ('Don\'t pass:','Mean ROI =', meanROI, 'Std Dev =', sigma)
# crapsSim (20, 10)
# Pass: Mean ROI = 2.0% Std. Dev. = 14.6969%
# Don't pass: Mean ROI = -6.0% Std Dev = 14.9666%
# crapsSim (20, 10)
# Pass: Mean ROI = -0.0% Std. Dev. = 26.8328%
# Don't pass: Mean ROI = -3.5% Std Dev = 25.7924%
# crapsSim (20, 10)
# Pass: Mean ROI = -14.0% Std. Dev. = 19.5959%
# Don't pass: Mean ROI = 11.0% Std Dev = 18.8149%
# crapsSim (100000, 10)
# Pass: Mean ROI = -1.463% Std. Dev. = 0.3025%
# Don't pass: Mean ROI = -1.3021% Std Dev = 0.2844%
# crapsSim (20, 100000)
# Pass: Mean ROI = -1.3093% Std. Dev. = 22.3475%
# Don't pass: Mean ROI = -1.4659% Std Dev = 22.0177%
怎样模拟灌了铅的骰子?
def rollDie ():
"""
正常的单骰子,随机得到 1~6 之间的一个正整数。
:return: 正整数,在 1~6 之间
"""
return random.choice ([1,2,3,4,5,6])
return random.choice ([1,1,2,3,3,4,4,5,5,5,6,6])
为什么可以用查表法提高 playhand 的性能?
关键发现:
playHand 的运行时间依赖于其中循环执行的次数。理论上,这个循环可以执行无限次,因为掷出 7 或者点数的时间是没有限制的。
playHand 的结果与循环执行的次数没有关系,只与跳出循环的条件有关。
对于每个可能的点数,我们可以很容易地计算出掷出 7 之前掷出这个点数的概率。
我的理解:
这里比的是出现的相对概率,道理如下:
比如 8 点,两个骰子组合出 8 点有 5 种情况,组合出 7 点有 6 种情况。掷出 11 种情况中的任何一种,游戏结束。
只看这 11 种可以让游戏结束的组合,那么 8 点相对于 7 点,出现的相对概率是 5/11。
上面那句也可以理解成:把 [0, 1) 区间均匀分成了 11 份。生成一个 [0,1) 间的随机数,如果它落在左边 5 份,则认为是 8 点,如果它落在了右边 6 份,则认为是 7 点。
if random.random () <= pointsDict [throw]: # 在掷出 7 之前掷出点数
16.4 Finding pi
随机的过程可以用来估算确定的事情?
Buffon 和 Laplace 用随机扔针来估算 pi 值。
import random
def variance (X):
""" 求得数值型列表的方差。
假设 X 是一个数值型列表。
返回 X 的方差。"""
mean = sum (X)/len (X)
tot = 0.0
for x in X:
tot += (x - mean)**2
return tot/len (X)
def stdDev (X):
""" 求 X 数值型列表的标准差。
假设 X 是一个数值型列表。
返回 X 的标准差。"""
return variance (X)**0.5
def throwNeedles (numNeedles):
"""
随机向 [0, 1) 见方范围内扔针,根据落入圆内的针数比例,估计 pi 值。
:param numNeedles: int,针的数量
:return: float,落入圆内针数与整体针的比例
"""
inCircle = 0
for Needles in range (1, numNeedles + 1):
x = random.random ()
y = random.random ()
if (x*x + y*y)**0.5 <= 1:
inCircle += 1
#数出落入圆中的针数比例
return inCircle/numNeedles
def getEst (numNeedles, numTrials):
"""
多次实验,求估计 pi 的平均值和标准差。
:param numNeedles: 每次扔多少针。
:param numTrials: 做多少次实验。
:return: (估计 pi 的平均值,标准差)
"""
estimates = []
for t in range (numTrials):
piGuess = 4*throwNeedles (numNeedles) #。正方形面积是 4,所以要乘以 4。。
estimates.append (piGuess) #加入列表中。
sDev = stdDev (estimates) #算列表的标准差
curEst = sum (estimates)/len (estimates) #算列表的平均值
print ('Est. =', str (round (curEst, 5)) + ',',
'Std. dev. =', str (round (sDev, 5)) + ',',
'Needles =', numNeedles)
return (curEst, sDev)
def estPi (precision, numTrials):
"""
倍增提高扔针的数量 numNeedles,缩小标准差,直到 95% 的数据都落入 precision 范围内。
:param precision: float,估算 pi 与真实 pi 之间的精度
:param numTrials: int,做多少次实验,对标准差无影响。
:return: float,估算出来的 pi 值
"""
numNeedles = 1000
sDev = precision
while sDev > precision/1.96:
curEst, sDev = getEst (numNeedles, numTrials)
numNeedles *= 2
return curEst
estPi (0.01, 100)
从估算 pi 值的输出中,吸取什么重要教训?
Est. = 3.14844, Std. dev. = 0.04789, Needles = 1000
Est. = 3.13918, Std. dev. = 0.0355, Needles = 2000
Est. = 3.14108, Std. dev. = 0.02713, Needles = 4000
Est. = 3.14143, Std. dev. = 0.0168, Needles = 8000
Est. = 3.14135, Std. dev. = 0.0137, Needles = 16000
Est. = 3.14131, Std. dev. = 0.00848, Needles = 32000
Est. = 3.14117, Std. dev. = 0.00703, Needles = 64000
Est. = 3.14159, Std. dev. = 0.00403, Needles = 128000
- 估算出来的 pi 值可能跑得更差,那没关系,考虑标注差,真是值都在一个标准差距离内,那就是很好的估计。
- 给出一个好的答案是不够的,还必须有足够的理由来确信它真是个好的答案。
- 随着扔针数量翻倍,标准差在稳步缩小。
- 标准差缩小到一定程度,我们就有信心说:这个结论在统计上是有效的。
- 统计上有效的结论!= 正确的结论
- for having confidence in the validity of the result, a necessary condition, not a sufficient condition.
Est. = 1.57422, Std. dev. = 0.02394, Needles = 1000
Est. = 1.56959, Std. dev. = 0.01775, Needles = 2000
Est. = 1.57054, Std. dev. = 0.01356, Needles = 4000
Est. = 1.57072, Std. dev. = 0.0084, Needles = 8000
Est. = 1.57068, Std. dev. = 0.00685, Needles = 16000
Est. = 1.57066, Std. dev. = 0.00424, Needles = 32000
- 上面的数据在统计上有效,但是不正确。
- 模型正确 model is correct + 正确实现 correctly implemented + 真实性验证 validate against reality.
16.5 Some Closing Remarks About Simulation Models
分析模型在科学上的地位?
在科学发展的大部分时间中,理论学家使用数学技术构建纯分析模型,根据一组参数和初始条件来预测系统的行为,并由此发展出了很多重要的数学工具,从微积分到概率论。这些工具帮助科学家对微观物质世界有了更加精确的认识。
For most of the history of science, theorists used mathematical techniques to construct purely analytical models that could be used to predict the behavior of a system from a set of parameters and initial conditions.
This led to the development of important mathematical tools ranging from calculus to probability theory. These tools helped scientists develop a reasonably accurate understanding of the macroscopic physical world.
模拟模型相对于分析模型有什么优势?
- 人们对社会科学(如经济学)的兴趣不断增加,需要对不容易使用数学处理的系统进行很好地建模;
- 要建模的系统越来越复杂,相对于构建精确的分析模型,逐渐优化一系列模拟模型要更容易一些;
- 相对于分析模型,更容易从模拟模型中提取出有用的中间结果 intermediate results,例如进行 “如果…… 那么……”what if 的实验;
计算机的出现使运行大规模模拟可行。模拟的作用一直受到手工计算时间的限制,直到 20 世纪中期现代计算机的出现。
- 数学描述不清的。
- 复杂系统。
- 改变条件看现象。
计算能力不受限了。
模拟模型的局限是什么?
模拟模型是描述性而非规定性的。它可以描述出系统如何在给定的条件下运行,但不能告诉我们如何安排条件才能使系统运行得最好。模拟模型只会进行描述,不会进行优化。但这并不是说模拟不能作为优化过程的一部分,例如,寻找参数设定的最优集合时,经常使用模拟作为搜索过程的一部分。
descriptive not prescriptive
how a system works under given conditions not how to arrange the conditions to make the system work best
A simulation does not optimize, it merely describes.
模拟模型可以按照哪三个维度分类?
确定性与随机性 静态与动态 离散与连续
确定性模拟和随机性模拟各有什么特点?
确定性模拟的行为完全由模型定义,重新运行模拟不会改变结果。当要建模的系统过于复杂而不能使用分析模型时,通常使用确定性模拟模型,例如处理器芯片的性能。
- 这个没有见过例子。
随机性模拟在模型中引入了随机性,多次运行同一个模型会得到不同的结果。这种随机因素要求我们生成多个结果以找出结果的可能范围。需要生成 10 个、1000 个还是 100 000 个结果是个统计问题,正如我们之前讨论过的那样。
- 这个在本书中见过例子。
静态模型和动态模型各有什么特点?
在静态模型中,时间的作用不大。本章中估计 π 值的扔针模拟就是一个静态模型。
在动态模型中,时间(或类似的项目 or some analog)是个基本要素 plays an essential role。在第 14 章的一系列随机游走模拟中,步数就是时间的代理项 surrogate for time。
离散模型与连续模型各有什么特点,在特定场景中如何选择?
在离散模型中,相关变量的值是可数的,例如所有值都是整数。
在连续模型中,相关变量的值位于一个不可数集合中,例如实数集合。
假设我们要分析高速公路上的车流量,可以选择对每辆车进行建模,这样就会得到一个离散模型。或者,也可以将交通情况看作一个流,流中的变化可以使用微分方程描述,这就是一个连续模型。 在本例中,离散模型更接近实际情况(没有人能驾驶半辆汽车,尽管有些车的体积只是其他车的一半),但在计算上要比连续模型更复杂。
实际上,模型一般既包括离散的部分,也包括连续的部分。 举例来说,如果要对人体中的血流建模,既可以使用描述血液的离散模型(即对血液中的细胞建模),也可以使用描述血压的连续模型。
以上,2018-05-09 18:38:04