使用 Deep Neural Network 做「粗糙」的明星賽預測,專注在練習處理資料和深度學習理論的部分,算是一個滿入門的專案。

NBA 每個賽季會在季中的時候進行全明星賽,會由觀眾、記者投票選出兩個分區的明星球員(共 24 位),得票數反應了該球員的「人氣」。

我們的猜想是 該球員的數據觀眾投票的意向 呈某種程度的正相關,場上成績較好的球員可能會比較容易被選為全明星。所以我們的目標是蒐集球員在明星賽前的成績,使用 DNN 來預測該球員是否可以進入明星賽,屬於一個二元分類的問題。

蒐集數據

首先因為沒有現成的明星賽前 Dataset 只能設法從網路上爬蟲,大多數網站的資料都是整個年度的球員數據,只有在官方網站的球員資料庫才能找到相關的資料。 NBA 官網是動態載入的網頁,所以就用了 selenium 來自動抓取資料。

Image

蒐集了 01~18 年,327筆明星賽球員,3200筆非明星賽球員,共 3527 筆球員資料,分別有傳統、進階、雜項等等五個向度的數據,共 83 種。

1
2
# 蒐集的資料共有底下幾種欄位
columns = ['allstar', 'name', 'pid', 'Overall', 'GP', 'MIN', 'PTS', 'FGM', 'FGA', 'FG%', '3PM', '3PA', '3P%', 'FTM', 'FTA', 'FT%', 'OREB', 'DREB', 'REB', 'AST', 'TOV', 'STL', 'BLK', 'PF', 'FP', 'DD2', 'TD3', '+/-', 'OffRtg', 'DefRtg', 'NetRtg', 'AST%', 'AST/TO', 'AST Ratio', 'OREB%', 'DREB%', 'REB%', 'TO Ratio', 'eFG%', 'TS%', 'USG%', 'PACE', 'PIE', 'PTS\xa0OFF\xa0TO', '2nd\xa0PTS', 'FBPs', 'PITP', 'OppPTS\xa0OFF\xa0TO', 'Opp2nd\xa0PTS', 'OppFBPs', 'OppPITP', 'BLKA', 'PFD', '%FGA2PT', '%FGA3PT', '%PTS2PT', '%PTS2PT\xa0MR', '%PTS3PT', '%PTSFBPs', '%PTSFT', '%PTSOffTO', '%PTSPITP', '2FGM%AST', '2FGM%UAST', '3FGM%AST', '3FGM%UAST', 'FGM%AST', 'FGM%UAST', '%FGM', '%FGA', '%3PM', '%3PA', '%FTM', '%FTA', '%OREB', '%DREB', '%REB', '%AST', '%TOV', '%STL', '%BLK', '%BLKA', '%PF', '%PFD', '%PTS', 'FGM %AST', 'FGM %UAST']

Image

Download

整理完的 01~18 年球員資料,數據屬於 NBA 官網,僅供學術使用。

DownLoad data

分析一下

在這 83 項數據中,某些項目的數據較優秀可能會影響觀眾的投票意向,所以將資料視覺化看看大概長怎麼樣。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Run in jupyter notebook
%matplotlib inline
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

data = pd.read_excel('player.xlsx')

def scatter_plot(x,y):
    ax = data.query('allstar==1').plot.scatter(x=x,y=y,color='#ef5350')
    data.query('allstar==0')sample(1000).plot.scatter(x=x,y=y,color='#26C6DA',ax=ax,figsize=(13,7))

備註

紅色為進入明星賽的球員,藍色為非明星賽的球員

scatter_plot('PTS','MIN') 場均得分&場均時間圖,可以看出上場時間高、得分高的球員比較有可能進入明星賽,但沒辦法從肉眼看出明顯的分界。 Image

scatter_plot('REB','MIN') 場均籃板&場均時間圖 Image

1
2
3
fig ,ax = plt.subplots(figsize=(13,7))
def box_plot(x):
    sns.boxplot(data=data.sample(1000),x=x,y='allstar',width=0.1,palette='Set3',ax=ax,orient='h')

box_plot('PTS') 得分的箱型圖 Image

box_plot('AST/TO') AST/TO 助攻失誤比 Image

DNN

主要我們是想要利用球員的成績,來預測他是否可以進明星賽,這個簡單的 task 用傳統 Machine Learning 的方法也可以得到不錯的 performance。不過現在 Deep learning 那麼潮,我們就 call 套件疊好幾層 hidden layer 直接做爆。

備註

01~12 年當作 Training set

13-17 年為 Testing set

Data

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import pandas as pd
import numpy as np

# Read data from excel
train_data0107 = pd.read_excel(DATA_PATH+"01-07Player.xlsx")
train_data0812 = pd.read_excel(DATA_PATH+"08-12Player.xlsx")
train_data1317 = pd.read_excel(DATA_PATH+"13-17Player.xlsx")
train_df = pd.concat([train_data0107,train_data0812])
test_df = train_data1317

# Numpy array
train_x = train_df.drop(['allstar','name','pid','Overall'],axis=1).values
test_x = test_df.drop(['allstar','name','pid','Overall'],axis=1).values
train_y = train_df['allstar'].values
test_y = test_df['allstar'].values

print(train_x.shape) # (2200, 83)
print(test_x.shape) # (1327, 83)

Training

1
2
3
4
5
6
7
8
from keras import callbacks
from keras.models import Sequential
from keras.layers.core import Dense, Activation
from keras.optimizers import Adam
from sklearn.utils import shuffle, class_weight

# Shuffle data
train_x,train_y = shuffle(train_x,train_y,random_state=87)

Model structure

input_dim 是輸入的維度,我們有 83 維的資料。

接著加上幾層 100 維的 fully connected hidden layer。因為我們的資料和目標都滿簡單的,所以基本上只需要幾層 hidden layer ,在參數量不會很多的時候就可以 fit 我們的目標,使用 relu 作為 activation function。最後為 1 維的 output layer 用 sigmoid

binary_crossentropy 為二元分類的 loss function,以 Adam 作為 optimizer。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Sequential model
model = Sequential()
model.add(Dense(10,input_dim=83,activation='relu'))
model.add(Dense(64,activation='relu'))
model.add(Dense(64,activation='relu'))
model.add(Dense(1,activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer=Adam(lr=0.0001),
              metrics=['accuracy'])

Fit

設定一下 callback function 讓 val_loss 最小的 model 被保存起來,作為最終較好的 model ,也可以使用 earlystopping 等方法。

因為資料不太平均,在 model.fit() 傳入 class_weight 讓 model 在 training 的時候可以根據資料的比例調整 loss function。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
epochs = 1000 # 訓練次數
batch_size = 512 # gradient decent 的 batch 大小

# Class weight
class_weight = class_weight.compute_class_weight('balanced',np.unique(train_y),train_y)

# Callback function
callback = callbacks.ModelCheckpoint('NBA_weights.{epoch:02d}-{val_loss:.2f}.hdf5',
                                            monitor='val_loss',
                                            verbose=1,
                                            save_best_only=True,
                                            mode='auto',
                                            period=10)
# Fit
history = model.fit(train_x,
                    train_y,
                    epochs=epochs,
                    batch_size=batch_size,
                    validation_split=0.3,
                    class_weight = class_weight,
                    callbacks=[callback])

Predict

訓練好 model 之後,我們使用 13~17 年的球員成績作為測試資料,加上混淆矩陣看一下最終的成果。

在平均分佈的資料裡,以 0.5 做為 sigmoid 的分界。現在處理的是不平衡的資料,在訓練時我們加入了 class weight ,在預測時也要根據比例調整預測的門檻值。

tip

在處理不平衡的資料時,如果只看 accuracy 很容易陷入成功率很高的錯覺。因為真實 Positive 的狀況太少。在一個分佈是 9:1 的資料集裡面,即便模型全部預測為「進不了明星賽」也可以有 90%的 accuracy。

在分析結果的時候,使用混淆矩陣比較容易看出真實模型學到的成果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
confution_matrix = {
    'TP':0,
    'TN':0,
    'FP':0,
    'FN':0
}

positive_count = 0
negative_count = 0

# Threshold
# 根據 class weight 調整門檻
TH = class_weight[0]/(class_weight[0]+class_weight[1])

for idx,p in enumerate(model.predict(test_x)):
    real = test_df.iloc[idx]['allstar']  # real data
    p = p>= TH
    positive_count += real
    negative_count += not real
    # confusion matrix
    if real and p:
        confution_matrix['TP']+=1
    elif not real and not p:
        confution_matrix['TN']+=1
    elif real and not p:
        confution_matrix['FP']+=1
    elif not real and p:
        confution_matrix['FN']+=1
CM Positive Negetive
True 84 1130
False 18 95

Image

結論

  • 投票意向和球員場上成績有關
  • 有些無法從球員成績看出的因素,使得準確度不容易達到非常高
    • (在模型中很難看出這些猜測是否正確,這只是可能的假設)
    • 球員的名氣(老將可能成績較差但仍受觀眾喜愛)
    • 球員私人行為(發表歧視言論等等)
    • 作弊手段得到數據
    • 球員所在的隊伍

我們蒐集的資料是「明星賽前的成績」,如此一來就無法在結果真正揭曉前「預知」結果,雖然這個專案最終沒有得到特別有意義成果,但從資料蒐集整理、建立模型架構、訓練、分析,完整訓練了一個簡單的 DNN。

future work

未來可以經過一些調整之後得到更有意義的結果。

  • 分析此模型的結果,看看是哪些球員過譽了?
  • 蒐集比較完整的資料
  • 做比較深入的特徵工程
  • 使用其他技巧,像是用 NLP 分析球壇傳聞等等
  • 針對其他目標做訓練,如:預測東西區 24 人名單,總冠軍預測 ⋯⋯

有一些程式碼、過程和實驗的細節沒有寫出來在文章上(如調整參數、模型等等),如有不清楚的地方請留言 :)