2018俄罗斯世界杯小组抽签出炉,几家欢喜几家愁。世界杯从来就不乏看点,东道主俄罗斯能走多远、德国能否卫冕、西班牙是否有望东山再起、两位球王谁更接近大力神杯...距世界杯开幕还有半年时间,一切都是未知数,不过整个赛程已定,我完全按照赛程模拟了所有64场比赛比分1000次,得出了A~H组各自的出线形势、每支队伍进四强的概率、以及最终的夺冠概率。一切结果,先卖个关子。
做这件事分四个步骤:爬数据
计算球队进球、失球均值,构建泊松模型
模拟1000次世界杯赛事
统计出线概率、夺冠概率、四强概率
爬数据
上一篇文章用Python分析本赛季英超争四形势提到从OPTA抓取数据,由于接口权限不对外公开,现在我改用公开的免费数据,方便大家自行抓取。这次所有比赛数据、赛程数据是我从球探网上抓的。利用selenium库,我将每只参赛国家队最近一年的比赛数据都抓取下来,保存成Pandas库的数据框。举个例子,这是葡萄牙国家队的页面,以及下面一张截图是抓下来存储的干净数据框。葡萄牙国家队葡萄牙国家队的Pandas数据框
计算球队进球、失球均值,构建泊松模型
泊松模型是模拟比赛的核心算法,理论在用Python分析本赛季英超争四形势文章中介绍过。针对国家队,我做了以下修改:若进球数
,强制
。这是因为热身赛双方实力差距过大,德国8:0马来西亚,这种差距在世界杯决赛圈几乎不存在。
亚洲球队与欧洲球队水平存在一个差异值,需要整体乘以一个系数。韩国场均进2球,相比德国场均1.5球,韩国的对手亚洲球队居多,德国打过欧洲杯对手实力不俗,韩国的场均2球必须打折扣。
得到计算结果,按进攻实力排序,欧洲豪强与南美双雄占据前列。(尾部的球队没有列出来)
模拟1000次世界杯赛事
先解决如何模拟一场比赛。淘汰赛与小组赛不同,如果打成平局必须进行点球大战,决出胜负。点球大战就设定各自50%概率晋级,下面这个simulate_match函数传入knockout参数为True时,就会激发这个机制,返回晋级的球队名。如果不是knockout,就是小组赛,就是输出模拟的比分。
import scipy as sp
import pandas as pd
# 读取球队进球率、失球率参数
team_strength = pd.read_csv('球队攻防参数.csv')
# 每一场球生成几次泊松随机数,次数越多随机因素越小
n_sim = 5
def simulate_match(team_A, team_B, knockout=False):
"""模拟一场比赛,返回主队进球数、客队进球数"""
# 获取比赛双方进球率、失球率
home_scoring_strength = (team_strength.loc[team_A, 'alpha'] + \
team_strength.loc[team_B, 'beta']) / 2
away_scoring_strength = (team_strength.loc[team_A, 'beta'] + \
team_strength.loc[team_B, 'alpha']) / 2
# 模拟n次比赛进球数取众数
fs_A = sp.stats.mode(poisson.rvs(home_scoring_strength, size=n_sim))[0][0]
fs_B = sp.stats.mode(poisson.rvs(away_scoring_strength, size=n_sim))[0][0]
print(team_A, fs_A, team_B, fs_B)
# 进入淘汰赛,若平局,点球大战晋级概率50%:50%
if knockout:
if fs_A == fs_B:
return [team_A, team_B][sp.random.randint(0, 2)]
elif fs_A > fs_B:
return team_A
else:
return team_B
return fs_A, fs_B
# 例如:
simulate_match('阿根廷', '尼日利亚', knockout=True)
>> 阿根廷
接下来是赛程,小组赛有6场每个组,8组共48场。按照赛程我手动写入列表里,比如A组的比赛按顺序,对战双方分别是这样:
# 小组每场比赛对阵双方:[主队, 客队]
fixture_A = \
[['俄罗斯', '沙特阿拉伯'],
['埃及', '乌拉圭'],
['俄罗斯', '埃及'],
['乌拉圭', '沙特阿拉伯'],
['沙特阿拉伯', '埃及'],
['俄罗斯', '乌拉圭']
]
然后建了一个类,每个组分别各自初始化自己的类,传入参数fixture就是上面创建的赛程,只需调用play函数就可以模拟该小组6场比赛比分。self.table是小组积分榜,保存下来每次模拟的小组头两名球队名,后面统计每支队在1000次模拟里出线的次数,即出线概率。
class Group:
"""模拟小组赛阶段,直接调用.play方法。"""
def __init__(self, group_teams, group_name, fixture):
self.group_teams = group_teams
self.group_name = group_name
self.table = pd.DataFrame(0, columns=['场次', '积分', '进球', '失球', '净胜球'], index=self.group_teams)
self.fixture = fixture
self.result = None
def play(self):
result = []
for [team_A, team_B] in self.fixture:
fs_A, fs_B = simulate_match(team_A, team_B)
self.table.loc[team_A, '场次'] += 1
self.table.loc[team_B, '场次'] += 1
self.table.loc[team_A, '进球'] += fs_A
self.table.loc[team_B, '进球'] += fs_B
self.table.loc[team_A, '失球'] += fs_B
self.table.loc[team_B, '失球'] += fs_A
if fs_A > fs_B:
self.table.loc[team_A, '积分'] += 3
elif fs_A == fs_B:
self.table.loc[team_A, '积分'] += 1
self.table.loc[team_B, '积分'] += 1
elif fs_A < fs_B:
self.table.loc[team_B, '积分'] += 1
else:
raise ValueError('比赛比分模拟有误!')
result.append([team_A, team_B, fs_A, fs_B])
self.result = pd.DataFrame(result, columns=['主队', '客队', '主队进球', '客队进球'])
self.table['净胜球'] = self.table['进球'] - self.table['失球']
self.table.sort_values(by=['积分', '净胜球', '进球'],
ascending=[False, False, False], inplace=True)
随后淘汰赛,16进8、8进4、半决赛和决赛。赛程球探网给出了,包括进入16强的对阵形势,每场由哪组第一对阵哪组第二都写清楚了,只要继续用上面模拟比赛的方式继续按照赛程模拟就行。
至此,我可以完整模拟一届世界杯的所有64场比赛的比分。最重要的,我记录下每组的出线球队、以及冠亚军、季军、殿军分别是哪个国家。接下来就可以轻松循环1000次,并进行统计。
统计出线概率、夺冠概率、四强概率
A~H组各自的出线概率我已经统计完成,东道主俄罗斯的FIFA世界排名已跌至65位,不过俄罗斯抽签抽到上上签,有望小组出线进入下一轮。(由于32球队太多,图片拆分4波展示。)A组B组出线形势C组D组出线形势E组F组出线形势G组H组出线形势
以下是夺冠概率、及打进四强的概率。列出了所有夺冠热门球队。
最后,韩国队在1000次模拟中11次进入四强,并有1次夺冠。这种小概率事件不禁让我想起2015/16赛季英超,以赛季前1赔5000逆天夺冠的莱斯特城。所以,足球是圆的,任何事情都有它的可能性存在,中国国足什么时候再进世界杯呢?