大纲
什么是随机
生活中我们经常遇到碰运气这个说法,这是因为某些事件具有随机性。所谓随机性是指某个事件的结果可能是这样也可能是那样,我们在事前并不能预测到。例如我们往桌上扔一个硬币,硬币可能正面朝上,也可能反面朝上,在扔这个硬币之前我们是没办法预测到具体的结果的。这种具有随机性的事件我们叫做随机事件。
虽然随机事件的单次结果我们无法预测,但是当这种随机事件发生的次数非常多,有一些基本规律还是可以得到的。还是以扔硬币为例,单次扔硬币的结果无法预测,但是如果说硬币是正常的,扔很多次下来,应该正面和反面的出现次数各占一半左右,这种规律我们是可以得到的。类似扔硬币这种事件,出现正反面结果所占的比率叫做概率,有专门的概率论来研究各种概率,不过我们这次并不会深入学习概率,我们只是以程序模拟的角度来初步尝试一下。
骰子的模拟
计算机可以很好的来模拟一些随机事件,例如掷骰子。在前一节我们学习了python中的numpy模块,它有一个子模块,叫做random,可以用来做各种随机的模拟和计算。我们先将模块加载进来
Copy import numpy as np
import numpy.random as rd
random模块中有一个很有趣的函数就是randint,它可以用来随机生成一些整数,使用的时候,需要指定产生随机整数的两边的边界,如果我们要模拟一次掷骰子的结果,假设骰子只有6个面,各自数字是从1到6,那么我们需要在函数中定义好,最小值是1,最大值7,因为最大值并不会出现在生成结果中,所以真实生成的数字就是1到6.
另一种生成1到6的随机数字的方法是使用choice函数,它可以从一个列表或数组中随机选择一下元素。
Copy number = [1,2,3,4,5,6]
rd.choice(number)
如果是想模拟生成多个随机数字,可以在函数后面加一个输入参数,表示生成的个数,例如我们想扔骰子连续扔10次,就得到下面的结果,可以看到模拟的结果是一个数组
Copy dies = rd.randint(1,7,10)
dies
Copy array([4, 6, 3, 3, 3, 1, 1, 6, 5, 5])
也可以用choice来模拟连续扔骰子
Copy number = [1,2,3,4,5,6]
dies = rd.choice(number,10)
dies
Copy array([2, 1, 5, 3, 5, 5, 2, 4, 6, 5])
计算机是不知疲惫的,我们可以扔10次,干嘛不扔一万次呢,我们可以扔一万次,将结果存到dies变量中
Copy dies = rd.randint(1,7,10000)
我们想看一下扔一万次骰子的结果到底是怎么样的,可以数一下各自点数出现的次数,也就是说1点出现多少次,2点出现多少次,等等。我们可以写一个函数来计算
Copy def func_count(x):
result = dict()
number = np.unique(x)
for n in number:
result[n] = x[x == n].size
return result
Copy {1: 1696, 2: 1679, 3: 1635, 4: 1687, 5: 1649, 6: 1654}
在上面这个函数中,用一个字典来存放最终的结果,先用unique来计算这些生成数组中的去重值,也就是1到6这六个点数,然后用一个循环去遍历观察,每个点数出现的次数,放入字典中。从结果上来看,各种点数出现的次数不完全一样,但是也差不多,说明点数的出现是平均的。
一个骰子的点数是平均出现的,那么2个骰子的点数之和也是平均的吗?我们可以模拟多个骰子,例如下面我们来试试2个骰子
Copy die_1 = rd.randint(1,7,10000)
die_2 = rd.randint(1,7,10000)
die_sum = die_1 + die_2
Copy {2: 262,
3: 578,
4: 799,
5: 1121,
6: 1398,
7: 1618,
8: 1467,
9: 1082,
10: 844,
11: 563,
12: 268}
可以看到这些点数存在一些规律,在点数较小和点数较大的区域,如2点,12点,只有2百多次,7点出现的最多,有1618次,这是为什么呢?因为2点出现的条件是两个骰子都是等于1,这种事件出现的机会比较小,而7点出现的条件就很多了,2+5也可以是7,3+4也可以是7,所以7点出现的机会比较大。我们可以把上面的函数稍微改一下,计算出各点数出现的次数,相对于总次数的占比,这个占比就是概率,它衡量了某个点数出现的机会。
Copy def func_prob(x):
result = dict()
number = np.unique(x)
for n in number:
result[n] = x[x == n].size/len(x)
return result
Copy {2: 0.0262,
3: 0.0578,
4: 0.0799,
5: 0.1121,
6: 0.1398,
7: 0.1618,
8: 0.1467,
9: 0.1082,
10: 0.0844,
11: 0.0563,
12: 0.0268}
研究了骰子之和,我们再换个问题,扔两个骰子,扔出两个5都朝上的概率是多少?
Copy test_num = 10000
die_1 = rd.randint(1,7,test_num)
die_2 = rd.randint(1,7,test_num)
result = (die_1 == 5) & (die_2 == 5)
sim_prob = result.sum()/test_num
print("模拟概率为{x:.4f}".format(x=sim_prob))
让我们换个思路,如果不用计算机程序来计算,而是用数学的思路来思考,某个骰子点数等于5出现的概率应该是1/6,那另一个骰子点数等于5出现的概率为是1/6,所以直接相乘,这就得到了真实的概率。
Copy real_prob = (1/6)*(1/6)
print("真实概率为{x:.4f}".format(x=real_prob))
小结:可以使用numpy.random模块中的一些函数来模拟一些随机事件,从而计算出模拟的概率值,从扔骰子的例子看出,当数据量较大的时候,模拟的概率值和真实的概率会非常接近。
硬币的模拟
玩完了骰子,我们来玩一下硬币,先提出一个问题,如果抛三枚硬币,三枚硬币都是正面或反面的概率是多少?
模拟硬币的代码和骰子是类似的,我们用1来表示硬币的反面,1来表示硬币的正面,可以写出类似的程序,得到三个硬币各扔一万次的结果。
Copy test_num = 10000
coin_1 = rd.randint(0,2,test_num)
coin_2 = rd.randint(0,2,test_num)
coin_3 = rd.randint(0,2,test_num)
这些模拟的结果都是数组的形式,计算数组是否满足某个条件,直接使用==比较操作符,再用&操作符,表示“且”的关系,这样计算出三个硬币都是正面或都是反面的模拟结果,
Copy result_1 = (coin_1 == 1) & (coin_2 == 1) & (coin_3 == 1)
result_2 = (coin_1 == 0) & (coin_2 == 0) & (coin_3 == 0)
(result_1.sum()+result_2.sum())/test_num
我们再来看一个稍微复杂一点的问题: 假设有人找你玩一个游戏,每人一个硬币,每次两人一起给出正反面
如果: 1. 出现两个正面,你赢 3 块钱 2. 出现两个反面,你赢 1 块钱 3. 出现一正一反,你输 2 块钱
问题是你要不要和他去玩这个游戏?
如果玩这个游戏可以不亏钱,应该就可以玩,先假设两个人给出正反面都是随机给出的,可以模拟一下看看。
Copy test_num = 10000
coin_1 = rd.randint(0,2,test_num)
coin_2 = rd.randint(0,2,test_num)
result_1 = (coin_1 == 1) & (coin_2 == 1)
result_2 = (coin_1 == 0) & (coin_2 == 0)
result_3 = ((coin_1 == 0) & (coin_2 == 1)) | ((coin_1 == 1) & (coin_2 == 0))
可以看到这三种可能的结果,对应的概率大约是1/4,1/4,1/2
Copy print(result_1.sum())
print(result_2.sum())
print(result_3.sum())
然后我们把各结果对应的收益考虑进去,就是各收益直接乘上对应的结果,再求和。
Copy amt_result = 3*result_1+1*result_2 +(-2)*result_3
看起来没有亏,可以玩起来,但是要注意一点,我们的假设是双方的硬币是随机出正反,有可能对方的硬币并不是随机出现正反的,如果对方一直出现反面会怎么样?
Copy test_num = 10000
coin_1 = rd.randint(0,2,test_num)
coin_2 = np.zeros(test_num)
result_1 = (coin_1 == 1) & (coin_2 == 1)
result_2 = (coin_1 == 0) & (coin_2 == 0)
result_3 = ((coin_1 == 0) & (coin_2 == 1)) | ((coin_1 == 1) & (coin_2 == 0))
amt_result = 3*result_1+1*result_2 +(-2)*result_3
amt_result.sum()
看起来我们就大亏了,你可以想一想,如果对手使用了一直出反面的策略,我们应该怎么做?
练习
基于前述的知识,利用python来写一个抽扑克牌的程序,也就是尝试从一副牌中随机抽取两张牌,计算抽得一对A的概率是多少。然后和真实的概率值进行对比。
下面我们用数组来建立一副扑克,数组中每个元素实际上是一个字典,s表示了花色,n表示了点数。
Copy suit = ['红心','黑桃','方片','草花']
number = ['2','3','4','5','6','7','8','9','10','J','Q','K','A']
cards = np.array([{'s':s,'n':n} for s in suit for n in number])
Copy array([{'s': '红心', 'n': '2'}, {'s': '红心', 'n': '3'},
{'s': '红心', 'n': '4'}, {'s': '红心', 'n': '5'}], dtype=object)
可以使用choice来从数组中模拟随机抽牌。
Copy my_card = rd.choice(cards, 2,replace=False)
Copy array([{'s': '黑桃', 'n': '10'}, {'s': '红心', 'n': 'J'}], dtype=object)
下面来写一个函数,计算抽得一对A的概率是多少
Copy def cards_func():
my_card = rd.choice(cards, 2,replace=False)
is_AA = my_card[0]['n'] == 'A' and my_card[1]['n'] == 'A'
return is_AA
Copy test_num = 10000
result = np.array([cards_func() for x in range(test_num)])
Copy sim_prob = result.sum()/test_num
print("模拟概率为{x:.4f}".format(x=sim_prob))
抽得一对A的理论概率可以这样来思考,52张牌中只有4张A,那么要抽一对A的过程可以分为两步,第一步先从52中抽到4张A的一张,这一步的概率是4/52,第二步是从剩下的51张牌中抽3张A中的一张,这一步概率是3/51。
Copy real_prob = (4/52)*(3/51)
print("真实概率为{x:.4f}".format(x=real_prob))