我总觉得,要是把数学的各个分支拟人化,那么“概率论”就是绝对的新闻女王,先天纯血八卦体质。理由很简单:媒体上最抓人眼球的内容,就是各种各样的小概率事件,就比如前几天登上热搜的这条新闻:
我对彩票一窍不通,所以只引不评。不过看到网友们对这种连号组合到底有多大概率、是否属于“不可能事件”的争论,我又想起了15年前同样引发热议的“购房抽签出现14连号”的新闻。
因为这是一个非常合适的随机模拟(蒙特卡洛分析)案例,所以每次给经管学院相关专业开课时,我都会把它布置为重要的阶段性作业。因此今天就以此为题,跟大家聊一聊怎样使用VBA(或Python)实现简单漂亮的“模拟”或“仿真”任务。
1.事件回顾
首先回顾一下这个“14连号”事件:2009年7月,某市安排1138名申请人摇号抽签,抽取514套经济适用房的购买资格。但是抽签结果甫一公开,就有市民向媒体举报,怀疑其中存在舞弊,理由是:在编号为1到1138的所有申请人中,从第996号到1009号,竟有连续14位全部中签。
“14连号”,听起来的确罕见。特别是一个月之前,也就是2009年6月,另一城市刚刚发生著名的 “六连号”事件 —— 当时有5141位市民抽签申请124套经适房,其中00811到00816号申请人连续中签。记者找到数学专家测算,结论是发生六连号的概率为千万亿分之一。于是各级政府,马上展开调查,两周后就确认该事件中存在舞弊行为,通报并严肃处理了5名工作人员。
既然如此,这次看起来更夸张的“14连号”自然引发了更大的争议。记得那天我正在学校做暑期值班,上午打开办公室电脑,就看到大部分网友们都在愤怒声讨。不过有趣的是,中午时媒体又发布了一条跟进报道 —— 记者请来时任华中科技大学概率统计系副主任的王湘君教授做演算,计算结果却是 “本次抽签发生14连号的概率为1%左右”。显然,这个概率虽然不高,但真算不上“不可能”。
于是网友们迅速分化为两派:一派痛斥专家唬人 ——“6连号都千万亿分之一了,14连号才百分之一?良心呢?立场呢?糊弄鬼呢?”
而另一派则坚定尊重科学 —— “虽然不明白怎么算的,但我还是觉得数学好厉害!”。
此外还有一小撮狂热的数学原教旨主义极端分子,已经开始扎堆争论计算过程与1%这个结果的准确性。有人算出是1.49%,有人认为不超过0.8% …… 一时间众家大佬纷纷祭出看家法器,各种丧心病狂的公式不要钱一样的砸向对方,坛子上属实热闹了好几天。
那么“14连号”到底有多大概率?为什么专家推算的结果竟然比“6连号”高出这么多?当我看到记者采访王教授的这篇新闻时,也不禁有些好奇。于是打算在交班回家之前,利用最后这几个小时的值班时间找出答案。
2. “蒙特卡洛分析”
不过我也是概率论的门外汉,自忖没那个本事用数学手段快速求出精确解。好在我也不需要精确结论,只要能够直观地看到连号概率是否接近1%就行。而对于这种需求,程序员早有现成对策 —— “蒙特卡洛分析”。
听起来有点高大上,其实翻译成人话不过就是四个字:随机模拟。比如对于经典的概率问题“掷骰子掷出6的概率有多大?”,按照蒙特卡洛分析,我们只需写个一万次的循环结构,每次循环生成一个1-6之间的随机数,然后看看一共生成了多少个“6”就可以(循环次数越多,结果越可信):
所以回到以这次的14连号案例,我们一样可以用随机数模拟抽签,然后反复试验100次甚至1000次,看看会出现几次14连号。
下面就是我那一天编写的VBA效果。之所以使用VBA,主要是看中它能够轻松操作Excel单元格的背景色,从而直观漂亮的展示出模拟结果。如下图所示,工作表中的每一行代表一名申请者,编号从1到1138;每一列代表一次“模拟抽签”试验,抽中显示绿色、未中显示灰色。最后,一旦某次抽签中出现14甚至更高连号,就把连续抽中者显示为红色。
3. 主要技巧
对于学习过《全民一起VBA》的同学来说,做一个这样的随机模拟程序其实并不复杂,只要领会几个关键技巧就可以:
(1)基本操作:生成指定范围的随机数
VBA中的随机数很简单,直接调用Rnd()函数就能得到一个0到1之间的随机小数。然后按照《全民一起VBA提高篇》第20回中讲解的技巧,通过一个简单的公式,就能把这个随机小数转换为任意范围内的整数或小数:
(2)随机“洗牌”
在“14连号”这个案例中,最关键的环节就是“怎样模拟一次抽签”,也就是怎样模拟出“从1134名申请人中,随机选中514个幸运者”。(非常不好意思,写完后才发现我做示例代码时,把1138人误记成1134人。考虑到截图修改不易,也不影响讲解,下面的代码将错就错,就按1134人算吧)
对于玩Python的同学来说,这种从M个人中随机选取N个人的任务根本不需要动脑筋,因为Python标准库random中已经提供了sample() 方法,可以直接从列表中不重复地随机抽样(详见《全民一起玩Python 提高篇》第12回)。不过VBA没有类似函数,需要我们手动编写代码。具体实现算法很多,这里杨老师介绍一个常用的“随机洗牌”方法:
首先,我们创建一个最大下标为1134的数组,数组中每个元素代表一个申请人。
然后我们把最前面的514个元素,也就是下标为1到514的部分全都设置为1,代表他们都“默认中签”;而后面的申请人也就是下标为515到1134的元素,全都设置为0,代表他们都“默认落选”。于是数组如下所示:
接下来随机生成两个“人员编号”i和j,也就是1到1134之间的随机数。这样,person(i)和person(j)就相当于在数组1134个申请人中随机指定了两个人;我们把这两个人的“运气数值”(也就是1或者0)交换一下,就会随机地“重排”一次数组:
当然,这样一次随机交换并不一定总是把1变成0、把0变成1。比如我们生成的两个随机数分别是1和2,也就是交换1号申请人和2号申请人的运气,那么交换后两个人仍然都是1,与交换前并无不同。
不过没关系,统计学向来信奉 “大力出奇迹” —— 只要不断重复上面的随机交换操作、只要交换的次数足够多,总会得到让人满意的随机效果。这跟我们平时洗牌时,反复切牌换位是一个道理。
于是咱们写一个1万次的循环,每次循环都执行一遍“生成两个随机编号、然后交换二者数值”的操作,最终就会把数组中的514个“1”,随机安置到各个位置上。而这些位置所对应的申请人,就是最终中签的幸运者。
要想直观地把这个结果显示在屏幕上,我们只需要扫描这个数组:如果1号元素是0(落选),就把第1行单元格的背景色设为灰色;如果是1(中签),就设为绿色,如此类推 ……
涂色部分的具体代码如下:
这样,我们就完成了一次“模拟抽签”试验。如果把这段代码做成一个子过程或函数,然后重复调用100次,我们就可以一口气模拟100次抽签。
(3)查找连续范围
在这个案例中,我们的最终目标不仅是模拟抽签,而且要在每一次模拟的结果中,检查是否存在“连续14人中签”的情况。如果存在,还要用可视化的方式把它标记出来,比如把这些单元格的背景色改为红色。
而关于查找连续区域的通用方法,我们已经在之前公众号文章 《办公自动化》常见问题与套路 —— 查找连续范围 中详细讲过,大家可以直接点击阅读。因此这里不再赘述,我们直接上代码:
事实上,这段代码完全可以与步骤2中最后一段代码(根据数组在表格中绘制灰色与绿色)合并。这里为了让读者聚焦于“发现连号”本身,所以单独列示。
4. 完整代码与试验结论
搞定这几个关键技巧,剩下的就是把它们放在一起做成子过程,把总人数(1134人)、“连号门槛”(14连号以上则记录)等做成灵活的参数,然后在主程序中循环调用。下面是参考代码:
至此,我们把这个“14连号”案例做成了一个可视化的蒙特卡洛仿真程序。那天下午写好代码,杨老师马上就运行了几次程序,每次执行100组试验。最终统计结果是,14连号发生概率在1%左右,与专家计算非常接近。于是综合各方面分析,可以认为,单纯从统计角度看,在这个案例中,出现14连号并非“极不可能”。
而这种随机模拟算法中蕴含的暴力美学,正是我从小痴迷计算机的原因之一。