用python预测NBA赛果:加入Elo评分系统

Python publisher01 36℃ 0评论


6月13日,猛龙首夺NBA总冠军。新华社 图

高中同学群最近突然讨论起了NBA,作为姚明退役后就再也没看过球的伪球迷,感觉有点说不上话。
为了改善下赛季的群聊体验,决定研究下一直很感兴趣的比赛预测问题。

1. 简介

本文通过basketball reference提供的18-19赛季统计数据,构造包含球队Elo评分的样本,利用logistic回归模型进行建模,并验证准确性。最后,由于18-19赛季刚刚结束,19-20赛季日程尚未公布,利用18-19赛季的日程对赛果进行模拟预测,验证该方法的可用性。

2. 数据与方法来源

3. 主要步骤

3.1 数据获取与整理:

数据获取的具体方法请见这里
这里需要用到基于18-19赛季的4张表格:

  • Team Per Game Stats(简称T表):记录了场均技术统计数据


    T表

  • Opponent Per Game Stats(简称O表):记录了对手的场均技术统计数据,结构与T表相同

  • Miscellaneous Stats(简称M表):其他统计数据

数据项 数据含义
Rk (Rank) 联盟排名
Age 球员平均年龄
W (Wins) 胜场数
L (Losses) 败场数
PW (Pythagorean wins) 基于毕达哥拉斯理论计算的胜场数
PL (Pythagorean losses) 基于毕达哥拉斯理论计算的败场数
MOV (Margin of Victory) 场均分差
SOS (Strength of Schedule) 赛程难度(跟赛区有关,不同赛区间相遇场次不同)
SRS (Simple Rating System) 综合考虑MOV和SOS后得出的评分,具体公式未知
ORtg (Offensive Rating) 每100回合的进攻得分
DRtg (Defensive Rating) 每100的回合中防守得分
Pace (Pace Factor) 场均回合数
FTr (Free Throw Attempt Rate) 罚球占投篮次数的比例
3PAr (3-Point Attempt Rate) 三分球占投篮次数的比例
TS% (True Shooting Percentage) 二分球、三分球和罚球的总共命中率
eFG% (Effective Field Goal Percentage) 有效的投射百分比(考虑三分球权重)
TOV% (Turnover Percentage) 失误率
ORB% (Offensive Rebound Percentage) 进攻篮板率
FT/FGA 每次进攻导致的罚球数
eFG% (Opponent Effective Field Goal Percentage) 对手命中率
TOV% (Opponent Turnover Percentage) 对手失误率
DRB% (Defensive Rebound Percentage) 对手防守篮板率
FT/FGA (Opponent Free Throws Per Field Goal Attempt) 对手的罚球次数占投射次数的比例

其中比较有趣的是毕达哥拉斯胜场数,一个在体育菠菜中的重要参考指标:

美國棒球統計專家比爾˙詹姆斯在80年代初整理美國職業網球聯盟球隊的過去成績時,發現可以用一支球隊的總得分和總失分算出勝率。然後用直角三角型斜線長的平方,等於其他兩邊乘和的“畢達哥拉斯定理”算出了一個公式。就是“勝率=總得分的平方÷(總得分的平方+總失分的平方)”,即“畢達哥拉斯乘率”。
来源:計算“畢達哥拉斯勝率”SK,最有成績的棒球

将T、M、O三表读入并去除部分列,拼接在一起:

def initialize_data(Mstat, Ostat, Tstat):  # csv文件初始化
    
    new_Mstat = Mstat.drop(['Rk', 'Arena'], axis=1)
    new_Ostat = Ostat.drop(['Rk', 'G', 'MP'], axis=1)
    new_Tstat = Tstat.drop(['Rk', 'G', 'MP'], axis=1)
    team_stats1 = pd.merge(new_Mstat, new_Ostat, how='left', on='Team')
    team_stats1 = pd.merge(team_stats1, new_Tstat, how='left', on='Team')
    
    return team_stats1.set_index('Team', inplace=False, drop=True)
  • 包含主客场信息的赛果表(wlloc.csv)
    这张表需要从basketball reference获取相关数据后手工处理一下,将结构改成如下形式:

    包含主客场信息的赛果表(wlloc.csv)
3.2 根据每场比赛的结果构建样本(包含Elo Score):

当最初没有elo时,给每个队伍最初赋base_elo:

def get_elo(team):
    try:
        return team_elos[team]
    except:
        # 当最初没有elo时,给每个队伍最初赋base_elo
        team_elos[team] = base_elo
        return team_elos[team]

通过wlloc.csv计算每个球队的elo值:

def calc_elo(win_team, lose_team):
    
    winner_rank = get_elo(win_team)
    loser_rank = get_elo(lose_team)
    rank_diff = winner_rank - loser_rank
    exp = (rank_diff*-1) / 400
    odds = 1 / (1 + math.pow(10, exp))
    
    # 根据rank级别修改K值
    if winner_rank < 2100:
        k = 32
    elif 2100 <= winner_rank < 2400:
        k = 24
    else:
        k = 16
    
    new_winner_rank = round(winner_rank + (k * (1 - odds)))
    new_rank_diff = new_winner_rank - winner_rank
    new_loser_rank = loser_rank - new_rank_diff
    return new_winner_rank, new_loser_rank

之后将Elo score和其他统计数据通过 build_dataSet()拼接在一起(详见文末),构成样本X,其具体结构为:
[Elo score(A), T(A), O(A), M(A), Elo score(B), T(B), O(B), M(B)]
用向量y记录胜负(0-1)。
下面是Elo rating system的简介:

ELO等级分制度(英语:Elo rating system)是指由匈牙利裔美国物理学家Arpad Elo创建的一个衡量各类对弈活动水平的评价方法,是当今对弈水平评估的公认的权威方法。被广泛用于国际象棋、围棋、足球、篮球等运动。网络游戏英雄联盟、魔兽世界、王者荣耀内的竞技对战系统也采用此分级制度。

假设棋手A和B的当前等级分分别为RARB,则按Logistic distribution A对B的胜率期望值当为:


A的胜率期望值

假如棋手A在比赛中的真实得分SA(胜=1分,和=0.5分,负=0分)和他的胜率期望值EA不同,需要根据以下公式进行调整:

分数调整公式

按照国际象棋的习惯,K的取值方法为:

  • 评分<2100: K = 32;
  • 2100 < 评分 < 2400: k = 24;
  • 评分> 2400: K = 16.(大师级棋手)
3.3 通过logistic回归模型进行建模和预测:

用(X, y)训练模型,做十折交叉验证,并进行预测。
运行结果实例:


运行结果


模拟预测结果

4. 结尾

关注NBA的同学可以看看,模拟预测结果是否符合18-19赛季各队的实力情况。
Elo score不是预测绝对胜负,而是相对胜率的一种评分方法,因此本方法也不是预测绝对胜负,只是基于实力预测胜率。虽然目前看预测准确率比较高(65%左右),但19年夏天各种转会地震层出不穷,相信对下赛季的预测准确性是个挑战,拭目以待中~
方法总体上不是很复杂,但也有很多小细节需要注意,具体请见全部代码中的注释吧~
全部代码如下:

import pandas as pd
import math
import csv
import random
import numpy as np
from sklearn import linear_model
from sklearn.model_selection import cross_val_score
# 初始化
base_elo = 1600
team_elos = {}
team_stats = {}
folder = 'H:\\quant\\NBA\\'  # 存放数据的目录
def initialize_data(Mstat, Ostat, Tstat):  # csv文件初始化
    
    new_Mstat = Mstat.drop(['Rk', 'Arena'], axis=1)
    new_Ostat = Ostat.drop(['Rk', 'G', 'MP'], axis=1)
    new_Tstat = Tstat.drop(['Rk', 'G', 'MP'], axis=1)
    team_stats1 = pd.merge(new_Mstat, new_Ostat, how='left', on='Team')
    team_stats1 = pd.merge(team_stats1, new_Tstat, how='left', on='Team')
    
    return team_stats1.set_index('Team', inplace=False, drop=True)
def get_elo(team):
    try:
        return team_elos[team]
    except:
        # 当最初没有elo时,给每个队伍最初赋base_elo
        team_elos[team] = base_elo
        return team_elos[team]
# 计算每个球队的elo值
def calc_elo(win_team, lose_team):
    
    winner_rank = get_elo(win_team)
    loser_rank = get_elo(lose_team)
    rank_diff = winner_rank - loser_rank
    exp = (rank_diff*-1) / 400
    odds = 1 / (1 + math.pow(10, exp))
    
    # 根据rank级别修改K值
    if winner_rank < 2100:
        k = 32
    elif 2100 <= winner_rank < 2400:
        k = 24
    else:
        k = 16
    
    new_winner_rank = round(winner_rank + (k * (1 - odds)))
    new_rank_diff = new_winner_rank - winner_rank
    new_loser_rank = loser_rank - new_rank_diff
    return new_winner_rank, new_loser_rank
def  build_dataSet(all_data):
    print("Building data set..")
    X = []
    y = []
    
    for index, row in all_data.iterrows():
        Wteam = row['WTeam']
        Lteam = row['LTeam']
        
        # 获取最初的elo或是每个队伍最初的elo值
        team1_elo = get_elo(Wteam)
        team2_elo = get_elo(Lteam)
        # 体现主场优势:给主场比赛的队伍加上100的elo值
        if row['WLoc'] == 'H':
            team1_elo += 100
        else:
            team2_elo += 100
        # 把elo作为评价每个队伍的第一个特征值
        team1_features = [team1_elo]
        team2_features = [team2_elo]
        # 添加我们从basketball reference.com获得的每个队伍的统计信息
        for key, value in team_stats.loc[Wteam].iteritems():
            team1_features.append(value)
        for key, value in team_stats.loc[Lteam].iteritems():
            team2_features.append(value)
        # 将两支队伍的特征值随机的分配在每场比赛数据的左右两侧
        # 并将对应的0/1赋给y值
        if random.random() > 0.5:
            X.append(team1_features + team2_features)
            y.append(0)
        else:
            X.append(team2_features + team1_features)
            y.append(1)
        # 根据这场比赛的数据更新队伍的elo值
        new_winner_rank, new_loser_rank = calc_elo(Wteam, Lteam)
        team_elos[Wteam] = new_winner_rank
        team_elos[Lteam] = new_loser_rank
    return np.nan_to_num(X), np.array(y)
def predict_winner(team_1, team_2, model):
    features = []
    # team 1,客场队伍
    features.append(get_elo(team_1))
    for key, value in team_stats.loc[team_1].iteritems():
        features.append(value)
    # team 2,主场队伍
    features.append(get_elo(team_2) + 100)
    for key, value in team_stats.loc[team_2].iteritems():
        features.append(value)
    features = np.nan_to_num(features)
    return model.predict_proba([features])
if __name__ == '__main__':
    Mstat = pd.read_csv(folder + '/M.csv')
    Ostat = pd.read_csv(folder + '/O.csv')
    Tstat = pd.read_csv(folder + '/T.csv')
    team_stats = initialize_data(Mstat, Ostat, Tstat)
    
    result_data = pd.read_csv(folder + '/wlloc.csv')
    
    X, y = build_dataSet(result_data)
    # 训练网络模型
    print("Fitting on %d game samples.." % len(X))
    model = linear_model.LogisticRegression()
    model.fit(X, y)
    # 利用10折交叉验证计算训练正确率
    print("Doing cross-validation..")
    print(cross_val_score(model, X, y, cv = 10, scoring='accuracy', n_jobs=-1).mean())
    print('Predicting on new schedule..')
    schedule1819 = pd.read_csv(folder + '18-19Schedule.csv')
    result = []
    
    for index, row in schedule1819.iterrows():
        team1 = row['Vteam']
        team2 = row['Hteam']
        pred = predict_winner(team1, team2, model)
        prob = pred[0][0]
        
        if prob > 0.5:
            winner = team1
            loser = team2
            result.append([winner, loser, prob])
        else:
            winner = team2
            loser = team1
            result.append([winner, loser, 1 - prob])
    with open('18-19Result.csv', 'w') as f:
        writer = csv.writer(f)
        writer.writerow(['win', 'lose', 'probability'])
        writer.writerows(result)
    
    print('Ω')

你或许想:《去原作者写文章的地方

转载请注明:Python量化投资 » 用python预测NBA赛果:加入Elo评分系统

喜欢 (0)or分享 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址