NBA比赛通常是难分胜负,有些时候会在最后一刻才会决出胜负,因此,预测哪支球队最后获胜会非常困难。通常你看好的球队恰恰在这场比赛中就会输给比它弱的球队。
许多预测比赛胜负的研究往往会有准确率上限,根据不同的比赛,准确率一般会在70%~80%之间,体育赛事的预测一般使用数据挖掘和统计学习方法。
在此,我们将用到决策树和随机森林来预测谁是某场NBA比赛的获胜队,决策树有两个主要的优势:
(1)决策过程易于机器和人类理解
(2)能处理不同类型的特征
1. 数据集
这里用到的数据集是NBA 2015-2016赛季所有场次的历史数据,数据集可以从http://www.basketball-reference.com/leagues/NBA_2016_games.html上下载,我已经将整理好的数据集放到网盘:https://pan.baidu.com/s/1Zq6BroS8y0A2LtIAxFz7lA 密码:qso1
加载数据集
import pandas as pd
import numpy as np
data_filename = "D:/data_folder/nba/basketball.csv"
dataset = pd.read_csv(data_filename)
#查看前5行数据
dataset.head()
输出:
清洗数据集
通过上面的输出我们发现一些问题:
(1)Date属性不是Date对象而是String对象
(2)第一行标题列不完整或是部分列对应的属性名不正确
我们可以通过pd.read_csv函数来解决上述问题。
dataset = pd.read_csv(data_filename, parse_dates=["Date"])
#修改每列名称
dataset.columns = ["Date", "Start (ET)", "Visitor Team", "VisitorPts", "Home Team", "HomePts", "OT?", "Score Type", "Attend.", "Notes"]
dataset.head()
可以通过下面方法查看数据集每列属性的类型:
print(dataset.dtypes)Date datetime64[ns]
Start (ET) object
Visitor Team object
VisitorPts int64
Home Team object
HomePts int64
OT? object
Score Type object
Attend. int64
Notes object
dtype: object
数据集前期的处理基本完成,我们可以用通过计算数据集的基线(baseline)来获得给定问题的的准确度,任何数据挖掘算法都应达到该基线。
对于我们的数据集,每场比赛有两支队伍:主队和客队。一个很简单的基线准确率是50%,如果我们随意预测某支球队获胜,准确率就是50%(因为每场比赛只有两支队伍),这就没什么意义。我们可以设置更大的基线值,并利用相关的领域知识进行预测,当预测结果大于该基线值时,可以判断某支队伍获胜。
抽取新特征
可以通过组合或比较现有数据集的相关特性来构建新的特征。首先,我们需要指定每条记录的类别值。在测试阶段,那算法得到的分类结果和它对比就能知道预测结果是否正确。类别可以有很多种表示方式,我们可以指定类别值为1表示主队胜利,0表示主队失败。因此,我们可以构造出一个新的属性值“HomeWin”:
dataset["HomeWin"] = dataset["VisitorPts"] < dataset["HomePts"]
dataset.head()
由于Pandas和scikit-learn没有完美整合,而Numpy和scikit-learn能很好地协同工作,因此,可以先将Pandas中的值转化为Numpy,然后再将Numpy配合scikit-learn工作。这里我们抽取属性“HomeWin”列为类别特征列y_true,这样就能转化为scikit-learn能识别的形式。
y_true = dataset["HomeWin"].values
此外,体育赛事的预测基线一个更好的选择是预测在每场比赛中主队获胜情况,众所周知,主队几乎在所有比赛中都会有一定的优势。那么我们的数据集中主队有多大的优势呢,我们可以通过查询主队获胜的平均概率获得:
dataset["HomeWin"].mean()
结果是:0.5942249240121581
还不错,至少比我们之前随机预测某支队伍获胜50%的概率要强。
这里值得注意的是: 当我们预测某场比赛的胜负时,不能使用这场比赛的数据作为特征值(即使这些值确实存在于我们的数据集中),原因是我们无法知道在我们预测这场比赛之前这场比赛的最终结果。
接下来,有两个新的属性需要去构造以便帮助我们更好地预测哪支球队获胜:
(1)要预测的两支球队在它们各自的上一场比赛中谁赢了(这可以大概看出哪支球队现在表现得更好一些)
我们通过按顺序遍历数据集中的每行记录并记录哪支球队获胜来计算出该属性。当遍历来到新的一行,会查询上一场比赛这两支球队的胜负情况。
#We first create a (default) dictionary to store the team's last result
from collections import defaultdict
won_last = defaultdict(int)
#We then create a new feature on our dataset to store the results of our new features
#gives a false value to all teams (including the previous year's champion!) when
#they are first seen
dataset["HomeLastWin"] = 0
dataset["VisitorLastWin"] = 0
#The key of this dictionary will be the team and the value will be whether they won
#their previous game. We can then iterate over all the rows and update the current
#row with the team's last result
for index, row in dataset.iterrows():
home_team = row['Home Team']
visitor_team = row['Visitor Team']
row["HomeLastWin"] = won_last[home_team]
dataset.set_value(index, "HomeLastWin", won_last[home_team])
dataset.set_value(index, "VisitorLastWin", won_last[visitor_team])
won_last[home_team] = int(row["HomeWin"])
won_last[visitor_team] = 1 - int(row["HomeWin"])
#查看前6行数据
dataset.head(6)
输出:
当然,也可以通过Pandas的索引来查看其它行的数据:
dataset.ix[1000:1005]
注意,前面的代码基于我们数据集中的每条记录都是按照时间顺序进行排序的,如果你使用的数据集没有排序,你可以使用dataset.sort(“Date”).iterrows()代替dataset.iterrows()
2.使用决策树进行预测
在scikit-learn包中已经实现了分类回归树(Classification and Regression Trees )CART算法作为决策树的默认算法,它支持类别型( categorical )和连续型(continuous)特征。
决策树中的参数
决策树中的一个非常重要的参数就是停止标准(stopping criterion)。在构建决策树过程准备要结束时,最后几步决策仅依赖少量样本而且随机性很大,如果应用最后这几个少量的样本训练出的决策树模型会过拟合训练数据(overfit training data)。取而代之的是,使用停止标准会防止决策树对训练数据精度过高而带来的过拟合。
除了使用停止标准外,我们也可以根据已有样本将一颗树完整地构建出来,然后再通过剪枝(pruning)来获得一个通用模型,剪枝的过程就是将一些对整个决策树构建过程提供微不足道的信息的一些节点给去除掉。
scikit-learn中实现的决策树提供了以下两个选项来作为停止树构建的标准:
(1)min_samples_split:指定了在决策树中新建一个节点需要样本的数量。
(2)min_samples_leaf:指定为了保留节点,每个节点至少应该包含的样本数。
第一个参数控制决策树节点的创建,第二个参数决定节点是否会被保留。
决策树的另一个参数就是创建决策的标准,主要用到的就是基尼不纯度(Gini impurity) 和 信息增益(information gain)
(1)Gini impurity:用于衡量决策节点错误预测新样本类别的比例。
(2)information gain:用于信息论中的熵来表示决策节点提供多少新信息。
上面提到的这些参数值完成的功能大致相同(即决定使用什么样的准则或值去将节点拆分(split)为子节点)。值本身就是用来确定拆分的度量标准,因此值得选择会对最终的模型带来重要影响。
使用决策树
通过导入scikit-learn包中的DecisionTreeClassifier类来创建决策树。同时DecisionTreeClassifier类参数中有一个random_state的参数,这里指定为14。我建议你使用和我相同的参数值,这也是为了使整个实验得意重复。当然,你也可以使用别的值,来看看别的值带来算法性能有什么改变。此外,我们抽取数据集中的部分特征来训练决策树,这里用到刚刚新建的两个特征“HomeLastWin”和“VisitorLastWin”。在scikit-learn中,决策树就是一个估计器(estimator),分类算法会用到scikit-laern中的估计器,而估计器会有fit和predict两个方法。也可以使用交叉验证方法( cross_val_score)来获得平均正确率:
from sklearn.tree import DecisionTreeClassifier
from sklearn.cross_validation import cross_val_score
import numpy as np
clf = DecisionTreeClassifier(random_state=14)
X_previouswins = dataset[["HomeLastWin", "VisitorLastWin"]].values
scores = cross_val_score(clf, X_previouswins, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))
输出:
Accuracy: 59.4%
准确率已经达到59.4%,这比我们随机预测的50%的准确率高出不少。但我们不应该只选择主队的一些特征值作为基线,同时选择别的一些特征或许会得到更好的结果。特征工程( Feature engineering)在数据挖掘中相当重要而且也有一定难度,往往占据整个过程的时间也是最多的。对于一个机器学习问题,数据和特征往往决定了结果的上限,而模型、算法的选择及优化则是在逐步接近这个上限。
3.NBA比赛结果预测
我们可以尝试添加其他特征以获得更好的预测结果。有很多可能的特征可以利用进来,因此有如下问题:
(1)通常来说哪支队伍被认为更厉害?
(2)两支球队上次相遇,谁是赢家?
此外,我们还会将算法应用于新的球队,以检验算法是否能得到一个用于判断不同球队比赛情况的模型。
首先对于第(1)个问题,我们可以新建一个特征来告诉我们主队是否通常比客队要厉害些。因此可以将上个赛季的积分榜(standings)作为依据:一个球队如果排名(积分)比另一个球队高,则可以认为积分高的球队更强。
查看积分榜:
standings_filename = 'D:/data_folder/nba/standings.csv'
standings = pd.read_csv(standings_filename, skiprows=0)
standings.head()
输出:
这样一来,我们就可以创建新的特征值“HomeTeamRanksHigher”,并将该特征和之前的两个特征一起提取出来用于训练模型:
dataset["HomeTeamRanksHigher"] = 0
for index, row in dataset.iterrows():
home_team = row["Home Team"]
visitor_team = row["Visitor Team"]
home_rank = standings[standings["Team"] == home_team]["Rk"].values[0]
visitor_rank = standings[standings["Team"] == visitor_team]["Rk"].values[0]
dataset.set_value(index, "HomeTeamRanksHigher", int(home_rank < visitor_rank))
X_homehigher = dataset[["HomeTeamRanksHigher", "HomeLastWin", "VisitorLastWin",]].values
clf = DecisionTreeClassifier(random_state=14)
scores = cross_val_score(clf, X_homehigher, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))
输出:
Accuracy: 60.9%
非常好!现在的准确率又比上一次提高了。
接下来,我们需要知道我们即将预测的两支球队他们上一场比赛谁是胜者。因为我们不仅要看这两支球队在上赛季的排名,有时候也要看这两支球队之前比赛的情况。由于弱队某些战术得当,或者球员在碰到某些强球队打法时更加适应,使得这些在积分榜上排名靠后的球队会在某些时候能战胜排名靠前的球队,这个特征也必须考虑进来,这恰恰是上面我们的第(2)个问题。
# a dictionary to store the winner of the past game
last_match_winner = defaultdict(int)
dataset["HomeTeamWonLast"] = 0
for index, row in dataset.iterrows():
home_team = row["Home Team"]
visitor_team = row["Visitor Team"]
teams = tuple(sorted([home_team, visitor_team]))
home_team_won_last = 1 if last_match_winner[teams] == row["Home Team"] else 0
dataset.set_value(index, "HomeTeamWonLast", home_team_won_last)
#作为下一次预测的两支球队根据
winner = row["Home Team"] if row["HomeWin"] else row["Visitor Team"]
last_match_winner[teams] = winner
X_lastwinner = dataset[["HomeTeamWonLast", "HomeTeamRanksHigher", "HomeLastWin", "VisitorLastWin",]].values
clf = DecisionTreeClassifier(random_state=14, criterion='entropy')
scores = cross_val_score(clf, X_lastwinner, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))
输出:
Accuracy: 62.2%
结果不错,预测准确率又上升了。这里注意的地方是,我们将两支球队按照字典顺序排序后并组成元组(tuple)类型的键是为了不要区分谁是主队谁是客队,这或许会对预测精准率有提升。
最后,我们将检验当将数据量很大的情况下,决策树能否得到有效的分类模型。由此,我们打算将球队名称作为决策树的训练特征来检验算法能否整合新增的信息。
虽然决策树可以处理类别特征,但是在scikit-learn中的实现需要将类别特征转化为数值型特征。我们可以使用 LabelEncoder转换器来讲字符串类型的球队名称转化为整型。
from sklearn.preprocessing import LabelEncoder
encoding = LabelEncoder()
encoding.fit(dataset["Home Team"].values)
home_teams = encoding.transform(dataset["Home Team"].values)
visitor_teams = encoding.transform(dataset["Visitor Team"].values)
X_teams = np.vstack([home_teams, visitor_teams]).T
这些球队名转化后的整数值将会被决策树用到,但这仍然会被 DecisionTreeClassifier解释为连续的特征。比如我们这里有32支球队,0~31会对应各支球队。但算法会认为数值1和2类似,而4和10就非常不同。这显然没什么意义,因为对于两支球队,他们要么是同一支球队,要么不是,没有中间状态。
为了消除这种和实际情况不一致的现象,我们可以使用 OneHotEncoder转换器来将这些整数转化成二值类型的特征。比如如果芝加哥公牛队Chicago Bulls 被 LabelEncoder转换为数字7,当使用OneHotEncoder进行再次转换时,如果某支球队是Chicago Bulls,那就用1表示,否则用0表示。每个可能的特征值都这样处理,会导致数据集变得很大。
from sklearn.preprocessing import OneHotEncoder
onehot = OneHotEncoder()
X_teams = onehot.fit_transform(X_teams).todense()
clf = DecisionTreeClassifier(random_state=14)
scores = cross_val_score(clf, X_teams, y_true, scoring='accuracy')
print("Accuracy: {0:.1f}%".format(np.mean(scores) * 100))
输出:
Accuracy: 62.8%
尽管给出的信息只是球队的比赛,但得分仍然更好。 决策树可能无法正确处理大量特征。鉴于此,我们尝试改变算法来看会不会起作用。数据挖掘有时是不断尝试新算法、使用新特征的过程。