使用Python、Pandas、Sklearn预测NBA比赛结果

Abstract: 作为课程作业,下面的内容是利用每场胜负数据、球员的生物数据等进行的NBA比赛预测。内容包括:1. 获得数据 2. 构造特征 3. 学习验证。最后能够提升16%的准确率。

获取数据集

获得如下数据集:

  1. 获得某一年(两年)赛季的队伍胜负,包括时间。比如:https://www.basketball-reference.com/leagues/NBA_2023_games.html,但是这个页面爬起来比较麻烦,代码参考https://aitechtogether.com/article/10860.html。我就直接手动复制粘贴了(也就几页)。
  2. 获得球员的信息:球员的生物数据(身高、体重),球员的球场数据(3分球命中率诸如此类)。前者在https://www.nba.com/stats/players/bio?PerMode=Totals&Season=2021-22&dir=D 可以得到。这个网站通过json的方式发来数据,在F12后选中XHR格式,其中有一个比较大的文件即此表格。球场数据在basketball-reference上有,各项代表的含义可以自行查询。
  3. 获得队伍的信息:关于缩写等等。参考wiki:https://en.wikipedia.org/wiki/Wikipedia:WikiProject_National_Basketball_Association/National_Basketball_Association_team_abbreviations

数据集预处理

使用VScode,将OT(加时赛)一列处理为:空则替换为0,OT则替换1,2OT则替换为2。并删去BoxScore内容。
Biodata的json格式转为csv则使用了Python的库json和json.loads()。

数据的处理和基础构造

将收集到的数据整理为.csv文件,并使用Pandas读入成其DataFrame格式。简要说明常用的操作:

  1. df[“Name”],包含Name列的一个dataframe
  2. for index, row in df.iterrows(),进行迭代,以按行修改,注意要使用df.iloc[index] = row保存修改
  3. 新建列:只需要df[“NewName”] = defaultValue
  4. 选出一些行:df.loc[condition] = value
import pandas as pd

path = '21-22.csv'
dataset = pd.read_csv(path, parse_dates=['Date'])
dataset.columns = [
    "Date","Start ","Visitor","VPTS","Home","HPTS","OT","Attend","Arena","Notes"
]
dataset.head()


# 统计客队得分小于主队的场次
dataset['HomeWin'] = dataset['VPTS'] < dataset['HPTS']
# 保留成label
y_true = dataset['HomeWin'].values
# y_true
dataset['HomeWin'].mean()

如果总是假定主场球队胜率,准确率能够达到55%。这是算法的baseline。构造predict即实际值Y:

total_data = dataset
total_data["Predict"] = 0
total_data.loc[total_data["HomeWin"] == True,"Predict"] = 1 

特征:历史胜负和上一场胜负

首先,由于我只用了一年的数据,不能将赛季总胜场作为特征。否则,就会出现大量的0:1预测1的现象,验证的结果非常高。于是,考虑使用在这场比赛之前的累计胜负作为特征,以及这场两支队伍的上一场比赛的胜负作为特征。但是,这里仍然面临着一个问题:
一种思路是,主场与客场分别统计,即如果要构造特征 “A胜场 | B胜场”,A主对B客与B主对A客不是简单的交换顺序,而是分别计算。或者说,如果要构造特征“上一场比赛的胜负”,A主B客对应的特征是上一场A主B客的胜负而非B主A客的胜负。由于我只用了一年的数据是比较少的,分别统计出现很多的0:0,于是选择不分主客。后面的代码也是相似的,不再展示了。

# 只构造累计胜负,now[2]是上一场胜负
win_cumulate_table = {}
dataset['CHomeWin'] = 0
dataset['CVisitorWin'] = 0
dataset['LastWin'] = 0
for i, row in dataset.iterrows():
    now = win_cumulate_table.get((row["Home"],row["Visitor"]), (0,0,0))
    row["CHomeWin"] = now[0]
    row["CVisitorWin"] = now[1]
    row["LastWin"] = now[2]
    if(row["HomeWin"] == True):
        now = now[0]+1,now[1],1
    else:
        now = now[0],now[1]+1,-1
    win_cumulate_table[row["Home"],row["Visitor"]] = now
    win_cumulate_table[row["Visitor"],row["Home"]] = now[1],now[0], -now[2]
    dataset.iloc[i] = row
dataset.tail()

特征1:在这一场前的赛季累计胜负
或者,也可以使用他们的差,比如:
作差
测试他们的相差不是很大。

开始测试

构造出一两个特征之后,就可以测试验证。使用slearn分别用不同的方法拟合,并使用交叉K折验证cross_val_score。分类方法例如:

  1. 决策树 from sklearn.tree import DecisionTreeClassifier
  2. 随机森林 from sklearn.ensemble import RandomForestClassifier
  3. 逻辑回归 from sklearn.linear_model import LogisticRegression
  4. 朴素贝叶斯 from sklearn.naive_bayes import GaussianNB
  5. 最近邻KNN from sklearn.neighbors import KNeighborsClassifier
    得到的结果在61.7% - 58.3%之间,已经提升了5%。
# 其他方法也类似,只需要改clf
from sklearn.naive_bayes import GaussianNB
from sklearn.model_selection import cross_val_score
clf = GaussianNB()
score = cross_val_score(clf, X, y, scoring='accuracy')
print('score is:{0:.1f}%'.format(np.mean(score) * 100))

构造关于加时赛特征

增加一项特征:累计OT,其大部分都是0,只有少量的1。随机森林验证得到了62.7%的精度,但是KNN在此时下降的比较厉害,只有50%,遂不再使用。
加入累计OT

构造“等级分”特征

文章https://aitechtogether.com/article/10860.html中使用了等级分这个概念。公式请看https://en.wikipedia.org/wiki/Elo_rating_system。
一种方法是,使用上个赛季的数据训练获得等级分,并预测这个赛季的数据,或者使用这个赛季的累计等级分。使用累计等级分(就是只根据本场以前赛况),最好的预测是到64.8%;使用整体等级分(即本赛季全部比赛的数据)能够达到68%,但是这种方法(原文的方法)属于信息泄露!

构造生物特征

在获得生物信息之后,得到了年龄、身高、体重、GP、PTS,并计算得到了BMI(公式:体重/身高^2)。注意原文是英制单位,需要换算。d = int(d[0])*30.48 + int(d[-1])*2.54(身高),以及d = float(d)*0.45359237(体重)。根据球员的Team信息得到每支队伍的平均身高体重如:

bio_prop = ["Age","Height","Weight","GP","PTS"]
tm_er = collections.defaultdict(list)
for index, row in players_data.iterrows():
    tm_er[team_dic[row['Tm']]].append(row["Player"])
tm_bio = collections.defaultdict(list)

tm_names = list(set(bio_data['Tm'].tolist())) # not abbr
for tname in tm_names:
    d = bio_data.loc[bio_data["Tm"] == tname]
    for p in bio_prop:
        if(p == "PTS"):
            a = d[d[p]>10][p]
        else : a = d[p]
        
        tm_bio[tname].append(a.mean())
tm_bio["Denver Nuggets"]
# output [27.2, 189.35699999999997, 96.9100098505, 47.4, 512.8421052631579]

特征:两支队伍比赛则作差。
球队平均生物特征
利用生物特征之差,得到最好的62.9%。但是GP和PTS算不上生物特征,只用身高、体重和年龄,朴素贝叶斯分类器得到更好的64.9%!也就说生物特征对NBA比赛的结果确实有影响而且影响很大。具体来说影响有多大呢?
相关系数矩阵

从矩阵来看(最后一列),年龄、身高和体重和结果是正相关的!影响最大的是年龄,而年龄是正相关即越大越有优势,其次是体重。这或许和表现差劲的队伍锐意革新、并派出许多新人上场有关。过滤掉新人(PTS<10)的人后,这个数据并没有太显著的变化。比较意外的是身高的差并不太影响比赛结果,可能是大家都很高,无论强队或弱队!

构造球员表现特征数据

球员数据中有一系列参数,比如: [
“G”,“GS”,“MP”,“FG”,“FGA”,“FG%”,“3P”,“3PA”,“3P%”,“2P”,“2PA”,“2P%”,“eFG%”,“FT”,“FTA”,“FT%”,“ORB”,“DRB”,“TRB”,“AST”,“STL”,“BLK”,“TOV”,“PF”,“PTS”],我们就全部将其投入到特征中。若如此做,逻辑回归就不太好用了。这种粗放特征使得结果达到68.1%的精度。从相关系数矩阵来看,每一项都是正相关(肯定是正相关),比较有用的特征是DRB、FG(fieldgoal?)等。相关系数矩阵

泛化性能,或更多数据

直接把上上个赛季(20-21)的比赛数据扔进来,并不改变生物信息和球员表现特征。这意味着一年中很多变化都没有考虑进去(比如换人)。即使在这种情况下,模型得到了63.2%的准确率,而基准是55.2%。如果加入上赛季数据的同时并不改变测试集的范围(仅是今年),即保留着关于比赛累计输赢的数据,能够达到71.2%的准确率!(只使用累计胜负特征,只有60.6%)这条曲线应当是随着数据增多先上升后下降的。

这一补充实验的另外一个意义是,球员数据中有PTS等等都是这个赛季现求的,是否会对预测造成干扰?首先PTS等与输赢的相关系数并不高。其次,如果相关程度很高就意味着泛化性不强,那么加入数据之后可能不增反降。因此这一定程度证明了他们是不止与本次比赛相关的,可能表征着球员本人的更深刻的信息。

总结

使用所有特征进行拟合,最后得到69.4%的结果,并在加入新的数据后得到71.2%的结果,比起原本55.8%的结果好了不少。总结一下:首先熟悉了DataFrame这个数据结构,然后练习了数据获取(本来打算使用爬虫),并分析了几个特征与结果的相关系数,最后拓展数据集补充了实验。

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值