大乐透4.97亿元大奖 (大乐透4.39亿大奖得主)

中午吃饭的时候,跟同事聊起天,就说起了肇庆这4.4亿大乐透的大奖。大家聊着聊着,吃完饭就跑去公司附近的彩票站各买了一注。

后来聊起中这大奖的人是怎么想到买这个号码的,还追加46倍投,有同事说,八成是有问题,正常人谁追投46倍啊。这朴素的道理,听着也对。不过世间万物无奇不有,没准人家就做了个预知梦,内心又十分自信,于是买了呢?

说不好。但是我就不好直接反驳啦,毕竟,这种民间猜测遍地都是,讲道理是讲不清楚的。于是我说,其实也有算法可以预测,至于预测得有多准,那就不知道了。没试过。

晚上回家的两小时,我琢磨了一下,贝叶斯公式或许可以预测。当然了,概率论下的贝叶斯公式也不是多复杂的计算方法,只是把随机问题概率化而已。但凡是概率,那总是有对有错的。

不过我也尝试用Python写了个演算过程。闲来无事,发出来看看,明天看看基于简化的贝叶斯公式的概率预测有多准,或,多不准。

贝叶斯公式虽然简单,但基础概率是不容忽视的,而贝叶斯的AB定义,其实也有个标准问题。我纯属瞎凑热闹,看到的朋友就别太当真了。权当看看Python代码的应用吧。

以下是解法。

第一步:获得原始数据

原始数据当然是大乐透的往期结果了。我大概摘录了从22023期到6月27日的结果,一共多少期没算。

从大数据的角度来说,这点数据量对概率模型来说大概是不够的,可能要扒到2009年的第一期才算够。鉴于本来就是随便写写,所以我也就不去搞那么多数据了。

存数据的方法我偷了个懒,用一长串字符串的形式存储这些期的结果数据,如下:

# 先用最简便的方法获取彩票历史记录 22023期 - 当前最后一期
s_CP_Result = "04051820251112010211303501120407222429010711142223270810030611313205120409161735010707081014171012121415213001090714162028040805091831320105081516193303040210242728060709111323310508181921263406070109172028061007091112260912010811223105111019252933081205152526330611081011293202060216172430091201021617200307020307122001050607122227030507101112330311162228293208090610202835011002102124300810011617193305060113182935040909171920250109051621233407100102121523040602111517310709061218213507100304242635070804151832340409020610112003090609192230010605091112310711020322263106100219222535050705101322280104022425263403120414153233101103061015230308020323283502090107172335071109151925290809"
l_CP_Result = []

s_前缀是我的变量命名习惯,表示string类型。

所以“l_”自然就是表示列表(此处用的是Python,故按标准不称数组)了。

# 拆分 s_CP_Result,拆分逻辑:每14个字符为一组。

i = len(s_CP_Result)
while i > 0:
    s_temp = s_CP_Result[0:14]
    l_CP_Result.append(s_temp)
    s_CP_Result = s_CP_Result.removeprefix(s_CP_Result[0:14])    
    i -= 14

对长字符串进行拆分。大乐透一期是7个数字,前区5个数字,后区2个。实际上每组数字都是字符的形式,比如号码“1”,实际上是用“01”来表示的,所以我们把它看作是字符串,而不是用整数类型。

# 可以打印一下变量看看结果
print(l_CP_Result)

以上代码的打印结果是这样的:

['04051820251112', '01021130350112', '04072224290107', '11142223270810', '03061131320512', '04091617350107', '07081014171012', '12141521300109', '07141620280408', '05091831320105', '08151619330304', '02102427280607', '09111323310508', '18192126340607', '01091720280610', '07091112260912', '01081122310511', '10192529330812', '05152526330611', '08101129320206', '02161724300912', '01021617200307', '02030712200105', '06071222270305', '07101112330311', '16222829320809', '06102028350110', '02102124300810', '01161719330506', '01131829350409', '09171920250109', '05162123340710', '01021215230406', '02111517310709', '06121821350710', '03042426350708', '04151832340409', '02061011200309', '06091922300106', '05091112310711', '02032226310610', '02192225350507', '05101322280104', '02242526340312', '04141532331011', '03061015230308', '02032328350209', '01071723350711', '09151925290809']

这个结果是把每期分割成列表内的对象了。但这样的对象字符串还不是我们需要的。所以需要进一步拆分。

# l_CP_Result结果里的每一项,都是一期大乐透的数据。从[0]开始,逐期递增。
# 对 l_CP_Result 的每一期数据进行二次拆分

l_CP_Result_splited = []
l_temp = []
for item in l_CP_Result:
    i = len(item)
    item_copy = item
    while i > 0:        
        s_temp = item_copy[0:2]
        l_temp.append(s_temp)
        item_copy = item_copy.removeprefix(item_copy[0:2])
        i -= 2
    l_CP_Result_splited.append(l_temp)
    l_temp = []

print(f"分割后的每期结果:{l_CP_Result_splited}")

这实际上是把第一次生成的列表内的每个对象进行二次拆分,按位序拆分成了每组7段。这段代码结果打印后是这样的:

分割后的每期结果:[['04', '05', '18', '20', '25', '11', '12'], ['01', '02', '11', '30', '35', '01', '12'], ['04', '07', '22', '24', '29', '01', '07'], ['11', '14', '22', '23', '27', '08', '10'], ['03', '06', '11', '31', '32', '05', '12'], ['04', '09', '16', '17', '35', '01', '07'], ['07', '08', '10', '14', '17', '10', '12'], ['12', '14', '15', '21', '30', '01', '09'], ['07', '14', '16', '20', '28', '04', '08'], ['05', '09', '18', '31', '32', '01', '05'], ['08', '15', '16', '19', '33', '03', '04'], ['02', '10', '24', '27', '28', '06', '07'], ['09', '11', '13', '23', '31', '05', '08'], ['18', '19', '21', '26', '34', '06', '07'], ['01', '09', '17', '20', '28', '06', '10'], ['07', '09', '11', '12', '26', '09', '12'], ['01', '08', '11', '22', '31', '05', '11'], ['10', '19', '25', '29', '33', '08', '12'], ['05', '15', '25', '26', '33', '06', '11'], ['08', '10', '11', '29', '32', '02', '06'], ['02', '16', '17', '24', '30', '09', '12'], ['01', '02', '16', '17', '20', '03', '07'], ['02', '03', '07', '12', '20', '01', '05'], ['06', '07', '12', '22', '27', '03', '05'], ['07', '10', '11', '12', '33', '03', '11'], ['16', '22', '28', '29', '32', '08', '09'], ['06', '10', '20', '28', '35', '01', '10'], ['02', '10', '21', '24', '30', '08', '10'], ['01', '16', '17', '19', '33', '05', '06'], ['01', '13', '18', '29', '35', '04', '09'], ['09', '17', '19', '20', '25', '01', '09'], ['05', '16', '21', '23', '34', '07', '10'], ['01', '02', '12', '15', '23', '04', '06'], ['02', '11', '15', '17', '31', '07', '09'], ['06', '12', '18', '21', '35', '07', '10'], ['03', '04', '24', '26', '35', '07', '08'], ['04', '15', '18', '32', '34', '04', '09'], ['02', '06', '10', '11', '20', '03', '09'], ['06', '09', '19', '22', '30', '01', '06'], ['05', '09', '11', '12', '31', '07', '11'], ['02', '03', '22', '26', '31', '06', '10'], ['02', '19', '22', '25', '35', '05', '07'], ['05', '10', '13', '22', '28', '01', '04'], ['02', '24', '25', '26', '34', '03', '12'], ['04', '14', '15', '32', '33', '10', '11'], ['03', '06', '10', '15', '23', '03', '08'], ['02', '03', '23', '28', '35', '02', '09'], ['01', '07', '17', '23', '35', '07', '11'], ['09', '15', '19', '25', '29', '08', '09']]

这就使原始数据变成了高维列表。每一维是一期结果。

从开始到现在的整个过程,我都是本着偷懒的方式写的。以连续字符串的方式输入,我照着往期结果敲小键盘就好了,也不用再去写个抓数据的脚本了。

第二步:数据准备

贝叶斯定理,实际上是对2组事件(一般称AB)之间关系的概率描述。分为先验概率和后验概率,AB可以互作为为先验概率,先验概率实际上是独立概率,后验概率则是AB之间的关联。

在大乐透的出奖中,我们首先可以定义A事件为某个号码在往期所有数据中出现的概率,这是个独立概率。然后,同理,B事件事实上也是个独立事件,我们可以表述为另一个号码在所有数据中出现的概率(A、B对应的号码可以相同)。

那么相应的后验概率如何表述呢?实际上就是:当某一期b出结果后,它的参照期数对应的结果,所包含的每个球,在往期所有包含b这期号码的结果中,所占比例。

这话说得有点拗口,我举个例子,比如我们的“当期b”为最后一期,而我们的参照期数,是往前倒推10期。那么相应的后验概率则为:b这期号码,与b-10这期号码,逐一组成的配对关系,在以往所有出现了b这期号码(中的任意一个)时,其(-10)的那期参照结果中,包含了与b-10这期号码中的任意一个的实际概率。

可能还是不太容易理解。更直白一些、不包含任何逻辑的表述就是:当我们看到“特朗普”的新闻时,新闻内容在说“没有人比我更懂”的概率有多大。

所以,我们要对已经整理完的往期结果进行号码关联组合。

# l_CP_Result_splited[0][0] 表示的就是该数据段中的 首期的第一个红号
# print(l_CP_Result_splited[0][0])

i = 0
j = 0
k = 0
m = len(l_CP_Result_splited)
l_temp = []

# 红区号码,在相邻两期的号码对关系。
l_Red_Pair = []
while i < m-1:
    while j < 5:
        while k < 5:
            l_temp.append(l_CP_Result_splited[i][j])
            l_temp.append(l_CP_Result_splited[i+1][k])
            l_Red_Pair.append(l_temp)
            l_temp = []
            k += 1        
        k = 0
        l_temp = []
        j += 1    
    j = 0
    i += 1

# print(f"红球相邻期数的对关系:{l_Red_Pair}")

# l_Red_Pair_Unique = []
# for item in l_Red_Pair:
#     if item not in l_Red_Pair_Unique:
#         l_Red_Pair_Unique.append(item)

# print(l_Red_Pair_Unique)
# print(len(l_Red_Pair))
# print(len(l_Red_Pair_Unique))


i = 0
j = 5
k = 5
m = len(l_CP_Result_splited)
l_temp = []

# 蓝区号码,在相邻两期的号码对关系。
l_Blue_Pair = []
while i < m-1:
    while j < 7:
        while k < 7:
            l_temp.append(l_CP_Result_splited[i][j])
            l_temp.append(l_CP_Result_splited[i+1][k])
            l_Blue_Pair.append(l_temp)
            l_temp = []
            k += 1        
        k = 5
        l_temp = []
        j += 1    
    j = 5
    i += 1

# l_Blue_Pair_Unique = []
# for item in l_Blue_Pair:
#     if item not in l_Blue_Pair_Unique:
#         l_Blue_Pair_Unique.append(item)

# print(f"蓝球相邻期数的对关系:{l_Blue_Pair}")
# print(l_Blue_Pair_Unique)
# print(len(l_Blue_Pair))
# print(len(l_Blue_Pair_Unique))

可以忽略掉print的代码,那是我为了随时看结果写的,目前没什么实际意义。

以上代码,是将往期结果分成了红区和蓝区,也就是大乐透的前区和后区,分别进行号码关联。毕竟红区和蓝区覆盖的号码数字不同呀,所以要区分对待。

仔细看,其实代码逻辑是一样的。

还要进行一项数据准备,是有关P(A)和P(B)的概率计算。有人可能认为这里有问题,比如,每个号码出现的独立概率,难道不就是1/35(前区)吗?为什么还要按往期计算。1/35当然也可以作为一个标准,但大乐透的开奖虽然是随机事件,但它的期数毕竟是有确定值的。1/35的概率,对应的数据范围实际上是无穷,故此我这里不用1/35。

有关P(A)和P(B)的概率计算如下:

# 贝叶斯公式P(A|B) = P(B|A) * P(A) / P(B)
# 我们要根据某一期 b 的所有结果,推算出当前的下一期出01的概率,则要计算,出现了01的所有期数的所有结果中,b 中每一个结果的概率,对应为P(B|A)。
# A、B事件概率都是对应的球在总期数中出现的概率

# 计算P(A) 或 P(B)
Red_List = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31', '32', '33', '34', '35']
Blue_List = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']

x = 0
Red_List_Report = []
for item in Red_List:
    for each in l_CP_Result_splited:
        if item in each[0:5]:
            x += 1
    Red_List_Report.append(x)
    x = 0

# print(f"红区各球在过去期数中出现的次数:{Red_List_Report}")

此外,我们还要计算一个总数:

# 往期期数的所有红球数
Total = len(l_CP_Result_splited) * 5
Red_Rate_List = []
for item in Red_List_Report:
    Red_Rate_List.append(round(item/Total,2))

# print(f"各球在往期中出现的概率:{Red_Rate_List}")

有了上述的数据准备,我们就可以按照我们既定的参照系来推算P(B|A)了。

如何计算P(B|A)

P(B|A)的计算,从逻辑上,实际不复杂,写成代码其实也不复杂,不过其中的思辨过程确实有点弯弯绕绕,尤其是因为数据量大,涉及到嵌套循环计算时,嵌套的逻辑要一定要想清楚。我脑子慢,所以想了好一会儿。

代码如下:

# 计算P(B|A),因我们的A、B关系,定义为只观察邻期的关系,所以直接在上面计算完的 l_Red_Pair和 l_Blue_Pair中查询即可。
# 当然,为提高概率的准确性,也可以继续探究 间隔为1的两期之间的关系、间隔为2的...通过循环也可以分别计算出来,最后求平均值。

# print(l_CP_Result_splited[-1])

i_counter_all = 0
i_counter_b = 0
s_counter_txt = ""
ls_Counter_List = []
li_Counter_List = []
# 对于最近一期的5个红球中的每一个
for each in l_CP_Result_splited[-1][0:5]:
    for index in Red_List:
        # 对于已计算的历史期数红球对里的每一对(这是个List)
        for item in l_Red_Pair:
            # 如果 index 在这个对中的第二位,
                if index == item[1]:
                    # 那么有效总数 +1
                    i_counter_all += 1
                    # 如果 each 在这个对中的第一位,那么对应球号的有效总数 +1
                    if each == item[0]:
                        i_counter_b += 1
        if i_counter_all > 0:
            s_counter_txt = "当期为" + index + "时,上期为" + each + "的P(B|A)概率值为" + str(round(i_counter_b / i_counter_all,2))
            li_Counter_List.append(round(i_counter_b / i_counter_all,2))
            ls_Counter_List.append(s_counter_txt)
        s_counter_txt = ""
        i_counter_all = 0
        i_counter_b = 0

# print(ls_Counter_List)

于是,我们将每个号码对应参照系球号的后验概率,都存在了列表"ls_Counter_List"里。

仔细看代码,参照系实际上是可以改的哦,有兴趣的可以仔细看看,如果不明白,可以问我。这里我选的参照系是相邻的一期,这个参照系也是没动脑子研究的,纯偷懒。正如我代码注释中所说的,如果你真的较劲想要算出一个更有意义的概率值的话,那你应该用轮循的方式,把每一组相对期数的概率全都算出来。那会耗费极大的计算量和时间。

我还是那句话,玩耍的事不要太认真了哈。

以上数据都有了,我们只需进行最后的贝叶斯计算即可

贝叶斯公式其实就是:

P(A|B) = P(B|A) * P(A) / P(B)

这个公式其实很好记的,把等号右边的 “|” 看作是分子分母线,它就会像是个消除法等式,很容易背下来。

所以,以上数据都有了之后,我们直接对每一组概率进行计算即可。

Index_Rate = 0
Total_Rate_List = []
Index_Rate_List = []
for each in l_CP_Result_splited[-1][0:5]:
    for index in Red_List:
        # 对应index球号的 P(A|B) 值为: each的P(B|A)值 * index的P(A) / each的P(B)
        Index_Rate = round(li_Counter_List[Red_List.index(index)] * Red_Rate_List[Red_List.index(index)] / Red_Rate_List[Red_List.index(each)],2)
        Index_Rate_List.append(Index_Rate)
        Index_Rate = 0
    Total_Rate_List.append(Index_Rate_List)
    Index_Rate_List = []

AVG_Rate_list = []
i = 0
while i < 35:
    AVG_Rate_list.append(0)
    i += 1

for each in Total_Rate_List:
    i = 0
    while i < 35:
        AVG_Rate_list[i] += each[0]
        each.remove(each[0])
        i += 1

for each in AVG_Rate_list:
    each = round(each/5,2)

print(f"红区各球概率依次为:{AVG_Rate_list}")

以我前文所采录的数据,最终的结果打印如下:

红区各球概率依次为:[0.14, 0.16999999999999998, 0.09, 0.0, 0.26, 0.0, 0.25, 0.49, 0.26, 0.14, 0.34, 0.25, 0.0, 0.14, 0.14, 0.25, 0.14, 0.14, 0.25, 0.0, 0.26, 0.26, 0.14, 0.0, 0.0, 0.34, 0.0, 0.0, 0.0, 0.0, 0.45, 0.0, 0.09, 0.34, 0.0]

由于本人很懒,业余时间也不多,所以蓝区的对应计算就不写了。

最后一个代码块里的概率,是6月29日晚8点半开奖结果的概率预测,到时候,我们一起看看吧。