模拟最终位置

Justin Worrall演示如何将一组赛季积分的价格扩展成为联赛中每个球队的最终位置概率的矩阵

Categories: 全国足球联赛, 工具, 执行和进展, 技术, 数据, 篮球, 统计模型, 职业级, 足球

Related Jobs

从前,不久前,你可以走进一家博彩店,给任何你想要的球队下注,只要你想支持那个球队去赢得比赛就行。但你不可能赌某球队输,不,先生,不行,那将是不道德的,这种想法可能会给你的房子带来瘟疫,包括青蛙,蝗虫和其他各种不愉快的事物。

至少Hills和Ladbrokes两个公司这么说。

值得庆幸的是,最高法院认为两匹马的赛马比赛中,下注A队等同于支持B队,大庄家都夹着尾巴避开这样的投注,虽然他们十年来一直不断地回归,所以你不能怪他们不坚持。与此同时,必发公司成立,迅速发展壮大,使玩家随意支持、投注他们心仪的球队,直到流动性枯竭,必发公司雇了一个庄家为首席执行官,并以体育博彩作为发展方向。

但后来某天这又成了一个可悲的故事。到现在为止,这个投注精灵已经恢复,被从魔瓶中释放出来,而且没法再把它放回去。虽然必发公司使这一概念得以推广,但人们却时常忘记在Andrew Black隐约酝酿交易所之前,该主张就已经被差额投注公司提出并尝试了。

是的,当时所有的庄家都想成为差额投注人。立博指数,威廉希尔指数,城市指数,IG差额投注,spreadfair公司……,所有这些伟大的公司(啊哈),现在都成了必发的手下败将。该交易所的天才之处在于它允许对固定赔率的产品进行投注,而不是创造需要信用账户的新产品,这对彩民来说更好理解。

所以现在的差额投注博彩业很惨淡,只剩下两家公司了(Sporting Index和Spreadex)。然而,至少从建模的角度来看,他们依然做一些有趣的市场。其他公司是没有这些业务的,正如我们看到的,多是信息类内容。

比如赛季积分,英超联赛的赛季积分都在这里。关于赛季积分有趣的是,每一个球队的积分都对应着一个有意义的价格。曼城队每个积分100磅,有风险但是却是聪明的选择,与用100磅赌利物浦旗鼓相当。你还可以在哪里进行这样的投注呢?不在赢家市场。在2 / 1盘口投注100磅赌曼城赢,这是个有风险但聪明的选择,但与在10/1盘口投注100磅赌水晶宫队赢不同,但愿你在对水晶宫的投注中获得一个有意义的价格。

鉴于赛季积分市场是“完整的”(即每个球队有与积分对等的价格),我们可以用少量的代码和几个分布假设做一些有趣的分析。

让我们从Sporting Index公司获取一些数据,该公司通过JSON数据交换格式(当然是没有获得认证的)提供这些数据;然而你需要从网页上单独抓取标识映射才能理解它。

import lxml.html, json, re, urllib

 

LivePricingUrl=”http://livepricing.sportingindex.com/LivePricing.svc/jsonp/GetLivePricesByMeeting?meetingKey=”

 

def get_market_quotes(url):

doc=lxml.html.fromstring(urllib.urlopen(url).read())

ids=dict([(li.attrib[“key”], re.sub(” Points$”, “”, li.xpath(“span[@class=’markets’]”)[0].text))

for li in doc.xpath(“//ul[@class=’prices’]/li”)

if “key” in li.attrib])

quotes=json.loads(urllib.urlopen(LivePricingUrl+url.split(“/”)[-2]).read())

return [{“name”: ids[quote[“Key”]],

“so_far”: tuple([int(tok) for tok in quote[“SoFar”].split(“/”)]),

“bid”: quote[“Sell”],

“offer”: quote[“Buy”]}

for quote in quotes[“Markets”]]

来看看数据是啥样的:

MarketQuotes=get_market_quotes(“http://www.sportingindex.com/spread-betting/football-domestic/premier-league/mm4.uk.meeting.4191659/premier-league-points-2013-2014”)

 

print pd.DataFrame(sorted(MarketQuotes, key=lambda row: -(row[“bid”]+row[“offer”])/2.0), columns=[“name”, “bid”, “offer”, “so_far”])

 

name   bid offer   so_far

0         Man City 78.0   79.5 (25, 13)

1         Arsenal 77.5   79.0 (31, 13)

2         Chelsea 76.5   78.0 (27, 13)

3         Man Utd 72.0   73.5 (22, 13)

4       Liverpool 67.5   69.0 (24, 13)

5       Tottenham 64.5   66.0 (21, 13)

6         Everton 61.0   62.5 (24, 13)

7     Southampton 55.5   57.0 (22, 13)

8       Newcastle 52.0   53.5 (23, 13)

9         Swansea 46.0   47.5 (15, 13)

10     Aston Villa 45.0   46.5 (16, 13)

11       West Brom 44.5   46.0 (15, 13)

12           Stoke 39.5   41.0 (13, 13)

13           Hull 39.5   41.0 (17, 13)

14       West Ham 39.0   40.5 (13, 14)

15         Norwich 38.0   39.5 (14, 13)

16         Cardiff 37.5   39.0 (13, 13)

17         Fulham 34.0   35.5 (10, 13)

18     Sunderland 33.0   34.5   (8, 13)

19 Crystal Palace 27.5   29.0 (10, 14)

好吧,看起来很切合实际。现在我们要做的是给赛季积分建立一个模拟过程。上面表格中的中期市场价格代表了每个球队可能获得的积分的期望值。当然,这只是一个期望值,每个球队都有可能比报价获得更多或更少的积分,围绕着每个球队的平均期望值都有一个积分的分布。

但是积分的分布是怎样的呢?

嗯,他们肯定是有界的,一个队的积分不可能低于零分,也不可能超过114分(在英超联赛中,3分乘以38场比赛)。事实上,积分的分布比这更窄些,假如赛季过半,一个球队接下来不可能得到少于他们目前的分数,也不可能正好获得114分,因为每个球队现在由于输球或平局至少丢了一分。

我们不想假设赛季积分呈正态分布,最好是建立一个函数来模拟分布,然后再看结果所呈的图形。

def simulate_points(quotes, paths, draw_prob=0.3):

import random

simpoints=dict([(quote[“name”],

[quote[“so_far”][0]

for i in range(paths)])

for quote in quotes])

ngames=2*(len(quotes)-1)

for quote in quotes:

midprice=(quote[“bid”]+quote[“offer”])/float(2)

currentpoints, played = quote[“so_far”]

toplay=ngames-played

expectedpoints=(midprice-currentpoints)/float(toplay)

winprob=(expectedpoints-draw_prob)/float(3)

for i in range(paths):

for j in range(toplay):

q=random.random()

if q < winprob:

simpoints[quote[“name”]][i]+=3

elif q < winprob+draw_prob:

simpoints[quote[“name”]][i]+=1

return [{“name”: key,

“simulated_points”: value}

for key, value in simpoints.items()]

现在对于这个函数有很多争论。具体而言,该函数不考虑剩余的固定值,盲目地假设所有接下来的比赛都是对战同等水平的球队。给大家留的家庭作业是思考如何补救上述缺陷。同时,它作为一个有用的工具可以用来探索积分的分布。让我们来模拟一下,并看看初次和二次的结果:

SimulatedPoints=simulate_points(MarketQuotes, paths=50000)

 

MidPrices=dict([(quote[“name”], (quote[“bid”]+quote[“offer”])/float(2)) for quote in MarketQuotes])

 

for row in SimulatedPoints:

row[“mid”]=MidPrices

]

row[“mean”]=np.mean(row[“simulated_points”])

row[“stdev”]=np.std(row[“simulated_points”])

row[“error”]=row[“mid”]-row[“mean”]

 

print pd.DataFrame(sorted(SimulatedPoints, key=lambda row: -row[“mid”]), columns=[“name”, “mid”, “mean”, “stdev”, “error”])

name   mid     mean     stdev   error

0         Man City 78.75 78.71340 5.542557 0.03660

1         Arsenal 78.25 78.31092 6.133936 -0.06092

2         Chelsea 77.25 77.19956 5.903095 0.05044

3         Man Utd 72.75 72.77466 5.822711 -0.02466

4       Liverpool 68.25 68.25186 6.274254 -0.00186

5       Tottenham 65.25 65.20336 6.292056 0.04664

6         Everton 61.75 61.76346 6.434456 -0.01346

7     Southampton 56.25 56.25740 6.401914 -0.00740

8       Newcastle 52.75 52.78540 6.233348 -0.03540

9         Swansea 46.75 46.82266 6.358935 -0.07266

10     Aston Villa 45.75 45.76336 6.241635 -0.01336

11       West Brom 45.25 45.28530 6.292197 -0.03530

12           Hull 40.25 40.25510 5.770000 -0.00510

13           Stoke 40.25 40.29252 6.104398 -0.04252

14       West Ham 39.75 39.69928 5.977452 0.05072

15         Norwich 38.75 38.78156 5.877997 -0.03156

16         Cardiff 38.25 38.21546 5.909287 0.03454

17         Fulham 34.75 34.70772 5.890475 0.04228

18     Sunderland 33.75 33.76118 5.974816 -0.01118

19 Crystal Palace 28.25 28.22622 5.119481 0.02378

误差列是中间市场报价和模拟分布中的均值之间的差异。你会注意到所有的错误都是相当小的,并且可以通过增加路径的数目来改善。重要的是,模拟的均值与市场报价的均值相似,这意味着(不是一语双关),我们的模拟与市场价格相符。

我们也生成了每个分布的标准差。有趣的是,每个球队的数字大致相似(约5、6分),但中间阶段的数字明显稍高些。如果考虑一下赢球/平局/失球点是怎样分布的,这还是有道理的。像水晶宫这样的弱旅通常会得0和1分;像斯旺西这样排名居中的球队将取得0、1和3分;而像曼城一样强大的球队,通常只会拿3和1分。

在这三类得分中,最小方差是水晶宫(0,1),最大的是斯旺西(0,1,3);曼城则居中(1,3)。

积分分布很有趣,但不是真正的奖项。没有几个俱乐部在签合同时直接以赛季积分为基础,但事实上我认为应该以赛季积分作为签合同的基础。相反,很多俱乐部签合同时考查球队的最终位置——冠军、晋级、六强、降级等,而最终位置实际上是每个球队赛季积分的直接作用。

因此,我们需要一个函数来把赛季积分分布转换成最终位置概率:

def calc_position_probabilities(simpoints):

paths=len(simpoints[0][“simulated_points”])

positionprob=dict([(team[“name”],

[0 for i in range(len(simpoints))])

for team in simpoints])

for i in range(paths):

sortedpoints=sorted([(team[“name”], team[“simulated_points”][i])

for team in simpoints],

key=lambda x: -x[-1])

for j in range(len(simpoints)):

name=sortedpoints[j][0]

positionprob[name][j]+=1/float(paths)

return [{“name”: key,

“position_probabilities”: value}

for key, value in positionprob.items()]

这个函数在每个模拟路径上循环,根据球队所得积分排名,然后为每个球队创建一个柱状图的排名,这个柱状图是相当于一个最终位置概率的向量。

最后,我们需要一个热图生成功能。

# http://stackoverflow.com/questions/14391959/heatmap-in-matplotlib-with-pcolor

 

def generate_heatmap(data, size, colourmap, alpha=1.0):

sorted_data=sorted(data, key=lambda row: np.inner(np.arange(len(data)), row[“position_probabilities”]))

df=pd.DataFrame(

for row in sorted_data],

index=

for row in sorted_data],

columns=np.arange(1, len(sorted_data)+1))

fig, ax = plt.subplots()

heatmap=ax.pcolor(df, cmap=colourmap, alpha=alpha)

fig=plt.gcf()

fig.set_size_inches(*size)

ax.set_frame_on(False)

ax.set_yticks(np.arange(df.shape[0])+0.5, minor=False)

ax.set_xticks(np.arange(df.shape[1])+0.5, minor=False)

ax.invert_yaxis()

ax.xaxis.tick_top()

ax.set_xticklabels(df.columns, minor=False)

ax.set_yticklabels(df.index, minor=False)

# plt.xticks(rotation=90)

ax.grid(False)

ax=plt.gca()

for t in ax.xaxis.get_major_ticks():

t.tick1On=False

t.tick2On=False

for t in ax.yaxis.get_major_ticks():

t.tick1On=False

t.tick2On=False

现在一切就绪:

PositionProbabilities=calc_position_probabilities(SimulatedPoints)

 

generate_heatmap(PositionProbabilities, size=(6, 6), colourmap=plt.cm.Reds)

瞧,英超联赛最终位置概率的热图出炉了。

有几点问题凸显出来:为什么水晶宫队垫底?为什么六强分裂成一个前三强组(曼城,阿森纳,切尔西)加上其他队,为什么图表下部的球队与上部球队相比在最终位置上有更多的不确定性(稍浅的颜色),图表上部的每个球队似乎都可以与邻近的位置交换。

你可以做进一步的分析。显然可以根据最终位置概率函数对冠军、六强、降级进行投注。这个分析就留作家庭作业吧。我只是想要大家明白,市场的价格比表面上看起来蕴含着更多的信息,我们通常可以应用少量的代码、一些分布假设、和一点想象力来提取这些信息。

 

 

About Justin Worrall

Justin Worrall works at Sportsrisq Capital, focusing on modelling sports- related insurance risks. In a former life he was a derivatives structurer for a US bank.
No Thoughts on 模拟最终位置

Leave A Comment