採用 Python 機器學習預測足球比賽結果

Momodel發表於2019-05-08

採用 Python 機器學習預測足球比賽結果

足球是世界上最火爆的運動之一,世界盃期間也往往是球迷們最亢奮的時刻。比賽狂歡季除了炸出了熬夜看球的鐵桿粉絲,也讓足球競猜也成了大家茶餘飯後最熱衷的話題。甚至連原來不怎麼看足球的人,也是暗中努力惡補了很多足球相關知識,想通過賽事競猜先賺一個小目標。今天我們將介紹如何用機器學習來預測足球比賽結果!

本 Chat 採用 Python 程式語言,使用 人工智慧建模平臺 Mo 作為線上開發環境進行程式設計,通過獲取 2000 年到 2018 年共 19 年英超的比賽資料,然後基於監督學習中邏輯迴歸模型、支援向量機模型和 XGBoost 模型,對英超比賽結果進行預測。

下面我們一起來看看預測英超比賽結果的機器學習步驟:

主要流程步驟

  1. 獲取資料和讀取資料的資訊
  2. 資料清洗和預處理
  3. 特徵工程
  4. 建立機器學習模型並進行預測
  5. 總結與展望

1. 獲取資料和讀取資料的資訊

首先我們進入 Mo 工作臺,建立一個空白專案,點選 開始開發 進入內嵌 JupyterLab 的 Notebook 開發環境。

enter image description here

enter image description here

接著我們需要在專案中上傳資料集

英超每年舉辦一個賽季,在每年的 8 月到第二年的 5 月進行,共有 20 支球隊,實行主客場雙迴圈賽制,每個賽季共 38 輪比賽(其中 19 場主場比賽,19 場客場比賽),每輪比賽共計 10 場比賽,所以每個賽季,英超共有 380 場比賽。

如果您已經在 MO 平臺新建專案,可以在平臺直接匯入資料集,流程如下:

enter image description here

1.1 讀取 csv 資料介面解釋

  • 採用 Pandas 讀取、寫入資料 API 彙總網址
    讀取 csv 資料一般採用 pandas.read_csv():
    pandas.read_csv(filepath_or_buffer, sep =',' , delimiter = None)
    • filepath_or_buffer:檔案路徑
    • sep:指定分隔符,預設是逗號
    • delimiter:定界符,備選分隔符(如果指定改引數,則sep失效)
    • usecols: 指定讀取的列名,列表形式
# 匯入必須的包
import warnings
warnings.filterwarnings('ignore')  # 防止警告檔案的包
import pandas as pd  # 資料分析包
import os
import matplotlib.pyplot as plt # 視覺化包
import matplotlib
%matplotlib inline
import seaborn as sns  # 視覺化包
from time import time
from sklearn.preprocessing import scale  # 標準化操作
from sklearn.model_selection import train_test_split  # 將資料集分成測試集和訓練集
from sklearn.metrics import f1_score  # F1得分
import xgboost as xgb  # XGBoost模型
from sklearn.svm import SVC  ## 支援向量機分類模型
from sklearn.linear_model import LogisticRegression  # 邏輯迴歸模型
from sklearn.model_selection import GridSearchCV  # 超引數調參模組
from sklearn.metrics import make_scorer  # 模型評估
import joblib  # 模型的儲存與載入模組
複製程式碼

下面開始我們的表演:

# 獲取地址中的所有檔案
loc = './/football//' # 存放資料的路徑
res_name = []  # 存放資料名的列表
filecsv_list = []  # 獲取資料名後存放的列表
def file_name(file_name):
    # root:當前目錄路徑   dirs:當前目錄下所有子目錄   files:當前路徑下所有非目錄檔案
    for root,dirs,files in os.walk(file_name):
        files.sort() # 排序,讓列表裡面的元素有順序
        for i,file in enumerate(files):
            if os.path.splitext(file)[1] == '.csv':
                filecsv_list.append(file)
                res_name.append('raw_data_'+str(i+1))
    print(res_name)
    print(filecsv_list)
file_name(loc)
複製程式碼
['raw_data_1', 'raw_data_2', 'raw_data_3', 'raw_data_4', 'raw_data_5', 'raw_data_6', 'raw_data_7', 'raw_data_8', 'raw_data_9', 'raw_data_10', 'raw_data_11', 'raw_data_12', 'raw_data_13', 'raw_data_14', 'raw_data_15', 'raw_data_16', 'raw_data_17', 'raw_data_18', 'raw_data_19']

['2000-01.csv', '2001-02.csv', '2002-03.csv', '2003-04.csv', '2004-05.csv', '2005-06.csv', '2006-07.csv', '2007-08.csv', '2008-09.csv', '2009-10.csv', '2010-11.csv', '2011-12.csv', '2012-13.csv', '2013-14.csv', '2014-15.csv', '2015-16.csv', '2016-17.csv', '2017-18.csv', '2018-19.csv']
複製程式碼

1.2 時間列表

獲取每一年的資料後,將每一年的年份放入到 time_list 列表中:

time_list = [filecsv_list[i][0:4]  for i in range(len(filecsv_list))]
time_list
複製程式碼

['2000','2001','2002','2003','2004','2005','2006','2007','2008','2009','2010','2011','2012','2013','2014','2015','2016','2017','2018']

1.3 用 Pandas.read_csv()  介面讀取資料

讀取時將資料與 res_name 中的元素名一一對應。

for i in range(len(res_name)):
    res_name[i] = pd.read_csv(loc+filecsv_list[i],error_bad_lines=False)
    print('第%2s個檔案是%s,資料大小為%s'%(i+1,filecsv_list[i],res_name[i].shape))
複製程式碼
第 1個檔案是2000-01.csv,資料大小為(380, 45)
第 2個檔案是2001-02.csv,資料大小為(380, 48)
第 3個檔案是2002-03.csv,資料大小為(316, 48)
第 4個檔案是2003-04.csv,資料大小為(335, 57)
第 5個檔案是2004-05.csv,資料大小為(335, 57)
第 6個檔案是2005-06.csv,資料大小為(380, 68)
第 7個檔案是2006-07.csv,資料大小為(380, 68)
第 8個檔案是2007-08.csv,資料大小為(380, 71)
第 9個檔案是2008-09.csv,資料大小為(380, 71)
第10個檔案是2009-10.csv,資料大小為(380, 71)
第11個檔案是2010-11.csv,資料大小為(380, 71)
第12個檔案是2011-12.csv,資料大小為(380, 71)
第13個檔案是2012-13.csv,資料大小為(380, 74)
第14個檔案是2013-14.csv,資料大小為(380, 68)
第15個檔案是2014-15.csv,資料大小為(381, 68)
第16個檔案是2015-16.csv,資料大小為(380, 65)
第17個檔案是2016-17.csv,資料大小為(380, 65)
第18個檔案是2017-18.csv,資料大小為(380, 65)
第19個檔案是2018-19.csv,資料大小為(304, 62)
複製程式碼

1.4 刪除特定檔案的空值

經過檢視第 15 個檔案讀取的第 381 行為空值,故採取刪除行空值操作。

1.4.1 刪除空值的介面
  • Pandas.dropna(axis=0,how='any')
    • axis: 0 表示是行;1表示是列
    • how:'all'表示只去掉所有值均缺失的行、列;any表示只去掉有缺失值的行、列
1.4.2 介面運用
res_name[14] = res_name[14].dropna(axis=0,how='all')
res_name[14].tail()
複製程式碼
Div Date HomeTeam AwayTeam FTHG FTAG FTR HTHG HTAG HTR ... BbAv<2.5 BbAH BbAHh BbMxAHH BbAvAHH BbMxAHA BbAvAHA PSCH PSCD PSCA
375 E0 24/05/15 Hull Man United 0.0 0.0 D 0.0 0.0 D ... 1.99 25.0 0.50 1.76 1.71 2.27 2.19 3.20 3.76 2.27
376 E0 24/05/15 Leicester QPR 5.0 1.0 H 2.0 0.0 H ... 2.41 28.0 -1.00 1.98 1.93 1.98 1.93 1.53 4.94 6.13
377 E0 24/05/15 Man City Southampton 2.0 0.0 H 1.0 0.0 H ... 2.66 28.0 -1.00 2.00 1.94 2.03 1.93 1.60 4.35 6.00
378 E0 24/05/15 Newcastle West Ham 2.0 0.0 H 0.0 0.0 D ... 2.25 25.0 -0.50 1.82 1.78 2.20 2.10 1.76 4.01 4.98
379 E0 24/05/15 Stoke Liverpool 6.0 1.0 H 5.0 0.0 H ... 1.99 25.0 0.25 2.07 2.02 1.88 1.85 3.56 3.60 2.17

5 rows × 68 columns

1.5 刪除行數不是 380 的檔名

考慮到英超一般是 19 個球隊,每個球隊需要打 20 場球,故把行數不是 380 的資料刪除掉,並找到器原 CSV 檔案一一對應。

for i in range(len(res_name),0,-1): 
    # 採用從大到小的遍歷方式,然後進行刪除不滿足條件的。
    if res_name[i-1].shape[0] != 380:
        key = 'res_name[' + str(i) + ']'
        print('刪除的資料是:%s年的資料,檔名:%s大小是:%s'%(time_list[i-1],key,res_name[i-1].shape))
        res_name.pop(i-1)
        time_list.pop(i-1)
        continue
複製程式碼
刪除的資料是:2018年的資料,檔名:res_name[19]大小是:(304, 62)
刪除的資料是:2004年的資料,檔名:res_name[5]大小是:(335, 57)
刪除的資料是:2003年的資料,檔名:res_name[4]大小是:(335, 57)
刪除的資料是:2002年的資料,檔名:res_name[3]大小是:(316, 48)
複製程式碼

1.6 檢視某一個資料集前n行資料

  • 檔名.head(n)
    • n:預設是5,想獲取多少行資料就填寫數字值。

讀取資料前五行操作:

res_name[0].head()
複製程式碼
Div Date HomeTeam AwayTeam FTHG FTAG FTR HTHG HTAG HTR ... IWA LBH LBD LBA SBH SBD SBA WHH WHD WHA
0 E0 19/08/00 Charlton Man City 4 0 H 2 0 H ... 2.7 2.20 3.25 2.75 2.20 3.25 2.88 2.10 3.2 3.10
1 E0 19/08/00 Chelsea West Ham 4 2 H 1 0 H ... 4.2 1.50 3.40 6.00 1.50 3.60 6.00 1.44 3.6 6.50
2 E0 19/08/00 Coventry Middlesbrough 1 3 A 1 1 D ... 2.7 2.25 3.20 2.75 2.30 3.20 2.75 2.30 3.2 2.62
3 E0 19/08/00 Derby Southampton 2 2 D 1 2 A ... 3.5 2.20 3.25 2.75 2.05 3.20 3.20 2.00 3.2 3.20
4 E0 19/08/00 Leeds Everton 2 0 H 2 0 H ... 4.5 1.55 3.50 5.00 1.57 3.60 5.00 1.61 3.5 4.50

5 rows × 45 columns

讀取資料前10行:

res_name[0].head(10)
複製程式碼
Div Date HomeTeam AwayTeam FTHG FTAG FTR HTHG HTAG HTR ... IWA LBH LBD LBA SBH SBD SBA WHH WHD WHA
0 E0 19/08/00 Charlton Man City 4 0 H 2 0 H ... 2.7 2.20 3.25 2.75 2.20 3.25 2.88 2.10 3.20 3.10
1 E0 19/08/00 Chelsea West Ham 4 2 H 1 0 H ... 4.2 1.50 3.40 6.00 1.50 3.60 6.00 1.44 3.60 6.50
2 E0 19/08/00 Coventry Middlesbrough 1 3 A 1 1 D ... 2.7 2.25 3.20 2.75 2.30 3.20 2.75 2.30 3.20 2.62
3 E0 19/08/00 Derby Southampton 2 2 D 1 2 A ... 3.5 2.20 3.25 2.75 2.05 3.20 3.20 2.00 3.20 3.20
4 E0 19/08/00 Leeds Everton 2 0 H 2 0 H ... 4.5 1.55 3.50 5.00 1.57 3.60 5.00 1.61 3.50 4.50
5 E0 19/08/00 Leicester Aston Villa 0 0 D 0 0 D ... 2.5 2.35 3.20 2.60 2.25 3.25 2.75 2.40 3.25 2.50
6 E0 19/08/00 Liverpool Bradford 1 0 H 0 0 D ... 8.0 1.35 4.00 8.00 1.36 4.00 8.00 1.33 4.00 8.00
7 E0 19/08/00 Sunderland Arsenal 1 0 H 0 0 D ... 2.1 4.30 3.20 1.70 3.30 3.10 2.05 3.75 3.00 1.90
8 E0 19/08/00 Tottenham Ipswich 3 1 H 2 1 H ... 4.7 1.45 3.60 6.50 1.50 3.50 6.50 1.44 3.60 6.50
9 E0 20/08/00 Man United Newcastle 2 0 H 1 0 H ... 5.0 1.40 3.75 7.00 1.40 3.75 7.50 1.40 3.75 7.00

10 rows × 45 columns
讀取最後 5 行操作:

res_name[0].tail()
複製程式碼
Div Date HomeTeam AwayTeam FTHG FTAG FTR HTHG HTAG HTR ... IWA LBH LBD LBA SBH SBD SBA WHH WHD WHA
375 E0 19/05/01 Man City Chelsea 1 2 A 1 1 D ... 1.65 4.0 3.60 1.67 4.20 3.40 1.70 4.00 3.1 1.80
376 E0 19/05/01 Middlesbrough West Ham 2 1 H 2 1 H ... 3.20 1.8 3.25 3.75 1.90 3.20 3.50 1.83 3.4 3.50
377 E0 19/05/01 Newcastle Aston Villa 3 0 H 2 0 H ... 2.90 2.4 3.25 2.50 2.38 3.30 2.50 2.25 3.4 2.60
378 E0 19/05/01 Southampton Arsenal 3 2 H 0 1 A ... 2.35 2.5 3.25 2.37 2.63 3.25 2.30 2.62 3.5 2.20
379 E0 19/05/01 Tottenham Man United 3 1 H 1 1 D ... 2.10 2.6 3.20 2.37 2.60 3.25 2.35 2.62 3.3 2.25

5 rows × 45 columns

讀取最後 4 行操作:

res_name[0].tail(4)
複製程式碼
Div Date HomeTeam AwayTeam FTHG FTAG FTR HTHG HTAG HTR ... IWA LBH LBD LBA SBH SBD SBA WHH WHD WHA
376 E0 19/05/01 Middlesbrough West Ham 2 1 H 2 1 H ... 3.20 1.8 3.25 3.75 1.90 3.20 3.50 1.83 3.4 3.50
377 E0 19/05/01 Newcastle Aston Villa 3 0 H 2 0 H ... 2.90 2.4 3.25 2.50 2.38 3.30 2.50 2.25 3.4 2.60
378 E0 19/05/01 Southampton Arsenal 3 2 H 0 1 A ... 2.35 2.5 3.25 2.37 2.63 3.25 2.30 2.62 3.5 2.20
379 E0 19/05/01 Tottenham Man United 3 1 H 1 1 D ... 2.10 2.6 3.20 2.37 2.60 3.25 2.35 2.62 3.3 2.25

4 rows × 45 columns

1.8 獲取某一年主場隊伍的名稱

res_name[0]['HomeTeam'].unique()
複製程式碼
array(['Charlton', 'Chelsea', 'Coventry', 'Derby', 'Leeds', 'Leicester',
       'Liverpool', 'Sunderland', 'Tottenham', 'Man United', 'Arsenal',
       'Bradford', 'Ipswich', 'Middlesbrough', 'Everton', 'Man City',
       'Newcastle', 'Southampton', 'West Ham', 'Aston Villa'],
      dtype=object)
複製程式碼

1.9 解析資料集列表頭含義

資料集行數已經固定,一般都是 380 行,而列數可能每年統計指標有變化,不一定相等,而且我們也比較關心列數表表頭。由於比較小,可以直接看資料集列數,這樣比較快,也可以程式碼實現,找到最大的列數,然後獲取列數的表頭進行一般性介紹解釋。

# 獲取列表頭最大的列數,然後獲取器引數
shape_list = [res_name[i].shape[1] for i in range(len(res_name))]
for i in range(len(res_name)):
    if res_name[i].shape[1] == max(shape_list):
        print('%s年資料是有最大列數:%s,列元素表頭:\n %s'%(time_list[i],max(shape_list),res_name[i].columns))
複製程式碼
2012年資料是有最大列數:74,列元素表頭:
 Index(['Div', 'Date', 'HomeTeam', 'AwayTeam', 'FTHG', 'FTAG', 'FTR', 'HTHG',
       'HTAG', 'HTR', 'Referee', 'HS', 'AS', 'HST', 'AST', 'HF', 'AF', 'HC',
       'AC', 'HY', 'AY', 'HR', 'AR', 'B365H', 'B365D', 'B365A', 'BWH', 'BWD',
       'BWA', 'GBH', 'GBD', 'GBA', 'IWH', 'IWD', 'IWA', 'LBH', 'LBD', 'LBA',
       'PSH', 'PSD', 'PSA', 'WHH', 'WHD', 'WHA', 'SJH', 'SJD', 'SJA', 'VCH',
       'VCD', 'VCA', 'BSH', 'BSD', 'BSA', 'Bb1X2', 'BbMxH', 'BbAvH', 'BbMxD',
       'BbAvD', 'BbMxA', 'BbAvA', 'BbOU', 'BbMx>2.5', 'BbAv>2.5', 'BbMx<2.5',
       'BbAv<2.5', 'BbAH', 'BbAHh', 'BbMxAHH', 'BbAvAHH', 'BbMxAHA', 'BbAvAHA',
       'PSCH', 'PSCD', 'PSCA'],
      dtype='object')
複製程式碼

我們看到資料包括 **Date(比賽的時間),Hometeam(主場隊伍名),Awayteam(客場隊伍名),FTHG(主場球隊全場進球數),HTHG(主場球隊半場進球數),FTR(全場比賽結果)**等等,更多關於資料集中特徵資訊可以參考資料集特徵說明文件

2. 資料清洗和預處理

我們挑選 Hometeam,Awayteam,FTHG,FTAG,FTR 這五列資料,作為我們的原始的特徵資料,後面基於這些原始特徵,我們再構造一些新的特徵。

2.1  挑選資訊列

  • HomeTeam: 主場球隊名
  • AwayTeam: 客場球隊名
  • FTHG: 全場 主場球隊進球數
  • FTAG:  全場 客場球隊進球數
  • FTR:  比賽結果 ( H= 主場贏, D= 平局, A= 客場贏)
# 將挑選的資訊放在一個新的列表中
columns_req = ['HomeTeam','AwayTeam','FTHG','FTAG','FTR']
playing_statistics = []   # 創造處理後資料名存放處
playing_data = {}  # 鍵值對儲存資料
for i in range(len(res_name)):
    playing_statistics.append('playing_statistics_'+str(i+1))
    playing_statistics[i] = res_name[i][columns_req]
    print(time_list[i],'playing_statistics['+str(i)+']',playing_statistics[i].shape)
複製程式碼
2000 playing_statistics[0] (380, 5)
2001 playing_statistics[1] (380, 5)
2005 playing_statistics[2] (380, 5)
2006 playing_statistics[3] (380, 5)
2007 playing_statistics[4] (380, 5)
2008 playing_statistics[5] (380, 5)
2009 playing_statistics[6] (380, 5)
2010 playing_statistics[7] (380, 5)
2011 playing_statistics[8] (380, 5)
2012 playing_statistics[9] (380, 5)
2013 playing_statistics[10] (380, 5)
2014 playing_statistics[11] (380, 5)
2015 playing_statistics[12] (380, 5)
2016 playing_statistics[13] (380, 5)
2017 playing_statistics[14] (380, 5)
複製程式碼

2.2 分析原始資料

我們首先預測所有主場球隊全都勝利,然後預測所有的客場都會勝利,對結果進行對比分析:

2.2.1 統計所有主場球隊都會勝利的準確率
def predictions_0(data):
    """ 
    當我們統計所有主場球隊都贏,那麼我們預測的結果是什麼
    返回值是預測值和實際值
    """
    predictions = []
    for _, game in data.iterrows():
        
        if game['FTR']=='H':
            predictions.append(1)
        else:
            predictions.append(0)
    # 返回預測結果
    return pd.Series(predictions)

# 那我們對19年全部主場球隊都贏的結果進行預測,獲取預測的準確率。
avg_acc_sum = 0
for i in range(len(playing_statistics)):
    predictions = predictions_0(playing_statistics[i])
    acc=sum(predictions)/len(playing_statistics[i])
    avg_acc_sum += acc
    print("%s年資料主場全勝預測的準確率是%s"%(time_list[i],acc))
print('共%s年的平均準確率是:%s'%(len(playing_statistics),avg_acc_sum/len(playing_statistics)))
複製程式碼
2000年資料主場全勝預測的準確率是0.4842105263157895
2001年資料主場全勝預測的準確率是0.4342105263157895
2005年資料主場全勝預測的準確率是0.5052631578947369
2006年資料主場全勝預測的準確率是0.4789473684210526
2007年資料主場全勝預測的準確率是0.4631578947368421
2008年資料主場全勝預測的準確率是0.45526315789473687
2009年資料主場全勝預測的準確率是0.5078947368421053
2010年資料主場全勝預測的準確率是0.4710526315789474
2011年資料主場全勝預測的準確率是0.45
2012年資料主場全勝預測的準確率是0.4368421052631579
2013年資料主場全勝預測的準確率是0.4710526315789474
2014年資料主場全勝預測的準確率是0.45263157894736844
2015年資料主場全勝預測的準確率是0.4131578947368421
2016年資料主場全勝預測的準確率是0.4921052631578947
2017年資料主場全勝預測的準確率是0.45526315789473687
共15年的平均準確率是:0.46473684210526317
複製程式碼
2.2.2 統計所有客場球隊都會勝利的準確率
def predictions_1(data):
    """ 
    當我們統計所有客場球隊都贏,那麼我們預測的結果是什麼
    返回值是預測值和實際值
    """
    predictions = []
    for _, game in data.iterrows():
        
        if game['FTR']=='A':
            predictions.append(1)
        else:
            predictions.append(0)
    # 返回預測結果
    return pd.Series(predictions)

# 那我們對19年客場球隊都贏的結果進行預測,獲取預測的準確率。
for i in range(len(playing_statistics)):
    predictions = predictions_1(playing_statistics[i])
    acc=sum(predictions)/len(playing_statistics[i])
    print("%s年資料客場全勝預測的準確率是%s"%(time_list[i],acc))
複製程式碼
2000年資料客場全勝預測的準確率是0.25
2001年資料客場全勝預測的準確率是0.3
2005年資料客場全勝預測的準確率是0.29210526315789476
2006年資料客場全勝預測的準確率是0.2631578947368421
2007年資料客場全勝預測的準確率是0.2736842105263158
2008年資料客場全勝預測的準確率是0.2894736842105263
2009年資料客場全勝預測的準確率是0.2394736842105263
2010年資料客場全勝預測的準確率是0.23684210526315788
2011年資料客場全勝預測的準確率是0.30526315789473685
2012年資料客場全勝預測的準確率是0.2789473684210526
2013年資料客場全勝預測的準確率是0.3236842105263158
2014年資料客場全勝預測的準確率是0.3026315789473684
2015年資料客場全勝預測的準確率是0.30526315789473685
2016年資料客場全勝預測的準確率是0.2868421052631579
2017年資料客場全勝預測的準確率是0.28421052631578947
複製程式碼

綜上比較:我們可以看出主場勝利的概率相對於輸和平局來說,確實概率要大。

2.3 我們想知道 Arsenal 作為主場隊伍時,他們的表現,如何求出 2005-06 所有比賽累計進球數 ?

我們知道 2005-06 年資料在 playing_statistics[2] 中:

def score(data):
    """ Arsenal作為主場隊伍時,累計進球數 """
    scores=[]
    for _,game in data.iterrows():
        if game['HomeTeam']=='Arsenal':
            scores.append(game['FTHG'])
    return np.sum(scores)
Arsenal_score=score(playing_statistics[2])
print("Arsenal作為主場隊伍在2005年時,累計進球數:%s"%(Arsenal_score))
複製程式碼
Arsenal 作為主場隊伍在2005年時,累計進球數:48
複製程式碼

2.4 我們想知道各個球隊作為主場隊伍時,他們的表現如何 ?

先試試求 2005-06 所有比賽各個球隊累計進球數。

print(playing_statistics[5].groupby('HomeTeam').sum()['FTHG'])
複製程式碼
HomeTeam
Arsenal          31
Aston Villa      27
Blackburn        22
Bolton           21
Chelsea          33
Everton          31
Fulham           28
Hull             18
Liverpool        41
Man City         40
Man United       43
Middlesbrough    17
Newcastle        24
Portsmouth       26
Stoke            22
Sunderland       21
Tottenham        21
West Brom        26
West Ham         23
Wigan            17
Name: FTHG, dtype: int64
複製程式碼

3. 特徵工程

特徵工程指的是把原始資料轉變為模型的訓練資料的過程,它的目的就是獲取更好的訓練資料特徵,得到更好的訓練模型。特徵工程能使得模型的效能得到提升,有時甚至在簡單的模型上也能取得不錯的效果。特徵工程在機器學習中佔有非常重要的作用,一般認為括特徵構建、特徵提取、特徵選擇三大部分。

3.1 構造特徵

因為這個比賽是一年一個賽季,是有先後順序的,那我們就可以統計到截止到本場比賽之前,整個賽季內,主客場隊伍的淨勝球的數量。那麼對於每一個賽季的每一週,都統計出每個球隊到本週為止累計的進球數和丟球數之差,也就是淨勝球的數量。

3.1.1 計算每個隊周累計淨勝球數量

處理後的資料,我們可以通過看某一年的某幾條資料來體現,比如:05-06 年的後五條資料

def get_goals_diff(playing_stat):
    # 建立一個字典,每個 team 的 name 作為 key
    teams = {}
    for i in playing_stat.groupby('HomeTeam').mean().T.columns:
        teams[i] = []
    # 對於每一場比賽
    for i in range(len(playing_stat)):
        # 全場比賽,主場隊伍的進球數
        HTGS = playing_stat.iloc[i]['FTHG']
        # 全場比賽,客場隊伍的進球數
        ATGS = playing_stat.iloc[i]['FTAG']

        # 把主場隊伍的淨勝球數新增到 team 這個 字典中對應的主場隊伍下
        teams[playing_stat.iloc[i].HomeTeam].append(HTGS-ATGS)
        # 把客場隊伍的淨勝球數新增到 team 這個 字典中對應的客場隊伍下
        teams[playing_stat.iloc[i].AwayTeam].append(ATGS-HTGS)

    # 建立一個 GoalsDifference 的 dataframe
    # 行是 team 列是 matchweek,
    # 39解釋:19個球隊,每個球隊分主場客場2次,共38個賽次,但是range取不到最後一個值,故38+1=39
    GoalsDifference = pd.DataFrame(data=teams, index = [i for i in range(1,39)]).T
    GoalsDifference[0] = 0
    # 累加每個隊的周比賽的淨勝球數
    for i in range(2,39):
        GoalsDifference[i] = GoalsDifference[i] + GoalsDifference[i-1]
    return GoalsDifference

def get_gss(playing_stat):
    # 得到淨勝球數統計
    GD = get_goals_diff(playing_stat)
    j = 0
    #  主客場的淨勝球數
    HTGD = []
    ATGD = []
    # 全年一共380場比賽
    for i in range(380):
        ht = playing_stat.iloc[i].HomeTeam
        at = playing_stat.iloc[i].AwayTeam
        HTGD.append(GD.loc[ht][j])
        ATGD.append(GD.loc[at][j])
        if ((i + 1)% 10) == 0:
            j = j + 1
    # 把每個隊的 HTGD ATGD 資訊補充到 dataframe 中
    playing_stat.loc[:,'HTGD'] = HTGD
    playing_stat.loc[:,'ATGD'] = ATGD
    return playing_stat

for i in range(len(playing_statistics)):
    playing_statistics[i] = get_gss(playing_statistics[i])

####  檢視構造特徵後的05-06年的後五條資料
playing_statistics[2].tail()
複製程式碼
HomeTeam AwayTeam FTHG FTAG FTR HTGD ATGD
375 Fulham Middlesbrough 1 0 H -11 -9
376 Man United Charlton 4 0 H 34 -10
377 Newcastle Chelsea 1 0 H 4 51
378 Portsmouth Liverpool 1 3 A -23 30
379 West Ham Tottenham 2 1 H -4 16

通過以上資料:我們發現 376 行資料的特點, 截止到這一場比賽之前,本賽季主場曼聯隊的淨勝球數是 34 , 客場查爾頓隊的淨勝球數是 -10 。

3.1.2 統計主客場隊伍到當前比賽周的累計得分

統計整個賽季主客場隊伍截止到當前比賽周的累計得分。一場比賽勝利計 3 分, 平局計 1 分,輸了計 0 分。我們根據本賽季本週之前的比賽結果來統計這個值。我們繼續觀看  05-06 年的後五條資料:

# 把比賽結果轉換為得分,贏得三分,平局得一分,輸不得分
def get_points(result):
    if result == 'W':
        return 3
    elif result == 'D':
        return 1
    else:
        return 0
    
def get_cuml_points(matchres):
    matchres_points = matchres.applymap(get_points)
    for i in range(2,39):
        matchres_points[i] = matchres_points[i] + matchres_points[i-1]
    matchres_points.insert(column =0, loc = 0, value = [0*i for i in range(20)])
    return matchres_points

def get_matchres(playing_stat):
    # 建立一個字典,每個 team 的 name 作為 key
    teams = {}
    for i in playing_stat.groupby('HomeTeam').mean().T.columns:
        teams[i] = []
    # 把比賽結果分別記錄在主場隊伍和客場隊伍中
    # H:代表 主場 贏
    # A:代表 客場 贏
    # D:代表 平局
    for i in range(len(playing_stat)):
        if playing_stat.iloc[i].FTR == 'H':
            # 主場 贏,則主場記為贏,客場記為輸
            teams[playing_stat.iloc[i].HomeTeam].append('W')
            teams[playing_stat.iloc[i].AwayTeam].append('L')
        elif playing_stat.iloc[i].FTR == 'A':
            # 客場 贏,則主場記為輸,客場記為贏
            teams[playing_stat.iloc[i].AwayTeam].append('W')
            teams[playing_stat.iloc[i].HomeTeam].append('L')
        else:
            # 平局
            teams[playing_stat.iloc[i].AwayTeam].append('D')
            teams[playing_stat.iloc[i].HomeTeam].append('D')
    return pd.DataFrame(data=teams, index = [i for i in range(1,39)]).T

def get_agg_points(playing_stat):
    matchres = get_matchres(playing_stat)
    cum_pts = get_cuml_points(matchres)
    HTP = []
    ATP = []
    j = 0
    for i in range(380):
        ht = playing_stat.iloc[i].HomeTeam
        at = playing_stat.iloc[i].AwayTeam
        HTP.append(cum_pts.loc[ht][j])
        ATP.append(cum_pts.loc[at][j])

        if ((i + 1)% 10) == 0:
            j = j + 1
    # 主場累計得分
    playing_stat.loc[:,'HTP'] = HTP
    # 客場累計得分
    playing_stat.loc[:,'ATP'] = ATP
    return playing_stat

for i in range(len(playing_statistics)):
    playing_statistics[i] = get_agg_points(playing_statistics[i])
    
#檢視構造特徵後的05-06年的後五條資料
playing_statistics[2].tail()
複製程式碼
HomeTeam AwayTeam FTHG FTAG FTR HTGD ATGD HTP ATP
375 Fulham Middlesbrough 1 0 H -11 -9 45 45
376 Man United Charlton 4 0 H 34 -10 80 47
377 Newcastle Chelsea 1 0 H 4 51 55 91
378 Portsmouth Liverpool 1 3 A -23 30 38 79
379 West Ham Tottenham 2 1 H -4 16 52 65

我們處理得到 HTP (本賽季主場球隊截止到本週的累計得分), ATP (本賽季客場球隊截止到本週的累計得分)。
我們再看 376 行,截止到這一場比賽,本賽季,曼聯隊一共積了80分, 查爾頓隊積了 47 分。

3.1.3  統計某支隊伍最近三場比賽的表現

前面我們構造的特徵反映了一隻隊伍本賽季的歷史總表現,我們看看隊伍在最近三場比賽的表現。
我們用:

HM1 代表主場球隊上一次比賽的輸贏,

AM1 代表客場球隊上一次比賽是輸贏。

同理,HM2 AM2 就是上上次比賽的輸贏, HM3 AM3 就是上上上次比賽的輸贏。

我們繼續觀看處理後 05-06 年的後 5 五條資料:

def get_form(playing_stat,num):
    form = get_matchres(playing_stat)
    form_final = form.copy()
    for i in range(num,39):
        form_final[i] = ''
        j = 0
        while j < num:
            form_final[i] += form[i-j]
            j += 1
    return form_final

def add_form(playing_stat,num):
    form = get_form(playing_stat,num)
    # M 代表 unknown, 因為沒有那麼多歷史
    h = ['M' for i in range(num * 10)]
    a = ['M' for i in range(num * 10)]
    j = num
    for i in range((num*10),380):
        ht = playing_stat.iloc[i].HomeTeam
        at = playing_stat.iloc[i].AwayTeam

        past = form.loc[ht][j]
        h.append(past[num-1])

        past = form.loc[at][j]
        a.append(past[num-1])

        if ((i + 1)% 10) == 0:
            j = j + 1

    playing_stat['HM' + str(num)] = h
    playing_stat['AM' + str(num)] = a

    return playing_stat

def add_form_df(playing_statistics):
    playing_statistics = add_form(playing_statistics,1)
    playing_statistics = add_form(playing_statistics,2)
    playing_statistics = add_form(playing_statistics,3)
    return playing_statistics

for i in range(len(playing_statistics)):
    playing_statistics[i] = add_form_df(playing_statistics[i])

#檢視構造特徵後的05-06年的後5五條資料
playing_statistics[2].tail()
複製程式碼
HomeTeam AwayTeam FTHG FTAG FTR HTGD ATGD HTP ATP HM1 AM1 HM2 AM2 HM3 AM3
375 Fulham Middlesbrough 1 0 H -11 -9 45 45 L D W D W L
376 Man United Charlton 4 0 H 34 -10 80 47 D L L L W W
377 Newcastle Chelsea 1 0 H 4 51 55 91 D L W W W W
378 Portsmouth Liverpool 1 3 A -23 30 38 79 W W W W L W
379 West Ham Tottenham 2 1 H -4 16 52 65 W W L D L L
3.1.4 加入比賽周特徵(第幾個比賽周)

然後我們把比賽周的資訊也放在裡面,也就是這一場比賽發生在第幾個比賽周。
特徵構造後的結果,我們可以直接檢視 05-06 年的後 5 條資料:

def get_mw(playing_stat):
    j = 1
    MatchWeek = []
    for i in range(380):
        MatchWeek.append(j)
        if ((i + 1)% 10) == 0:
            j = j + 1
    playing_stat['MW'] = MatchWeek
    return playing_stat

for i in range(len(playing_statistics)):
    playing_statistics[i] = get_mw(playing_statistics[i])
    
#檢視構造特徵後的05-06年的後五條資料
playing_statistics[2].tail()
複製程式碼
HomeTeam AwayTeam FTHG FTAG FTR HTGD ATGD HTP ATP HM1 AM1 HM2 AM2 HM3 AM3 MW
375 Fulham Middlesbrough 1 0 H -11 -9 45 45 L D W D W L 38
376 Man United Charlton 4 0 H 34 -10 80 47 D L L L W W 38
377 Newcastle Chelsea 1 0 H 4 51 55 91 D L W W W W 38
378 Portsmouth Liverpool 1 3 A -23 30 38 79 W W W W L W 38
379 West Ham Tottenham 2 1 H -4 16 52 65 W W L D L L 38
3.1.5 合併比賽的資訊

我們打算把資料集比賽的資訊都合併到一個表裡面,然後我們把我們剛才計算得到的這些得分資料,淨勝球資料除以週數,就得到了周平均後的值。結果就可以通過檢視構造特徵後資料集的後 5 條資料。

# 將各個DataFrame表合併在一張表中
playing_stat = pd.concat(playing_statistics, ignore_index=True)

# HTGD, ATGD ,HTP, ATP的值 除以 week 數,得到平均分
cols = ['HTGD','ATGD','HTP','ATP']
playing_stat.MW = playing_stat.MW.astype(float)
for col in cols:
    playing_stat[col] = playing_stat[col] / playing_stat.MW
    
#檢視構造特徵後資料集的後5五條資料
playing_stat.tail()
複製程式碼
HomeTeam AwayTeam FTHG FTAG FTR HTGD ATGD HTP ATP HM1 AM1 HM2 AM2 HM3 AM3 MW
5695 Newcastle Chelsea 3.0 0.0 H -0.289474 0.710526 1.078947 1.842105 L D L W L W 38.0
5696 Southampton Man City 0.0 1.0 A -0.473684 2.052632 0.947368 2.552632 W W D D W W 38.0
5697 Swansea Stoke 1.0 2.0 A -0.710526 -0.894737 0.868421 0.789474 L L L D L D 38.0
5698 Tottenham Leicester 5.0 4.0 H 0.973684 -0.078947 1.947368 1.236842 W W L L W L 38.0
5699 West Ham Everton 3.0 1.0 H -0.578947 -0.315789 1.026316 1.289474 D D W W L W 38.0

我們看到資料集最後一行的行數是 5699 ,加上第一行為 0 行,則一共 5700 條資料;我們總共統計了 15 年的資料,每一年有 380 條資料,計算後發現我們統計後的資料集大小是準確的。

3.2 刪除某些資料

前面我們根據初始的特徵構造出了很多的特徵。這其中有一部分是中間的特徵,我們需要把這些中間特徵拋棄掉。因為前三週的比賽,每個隊的歷史勝負資訊不足,所以我們打算棄掉前三週的資料。

# 拋棄前三週的比賽
playing_stat = playing_stat[playing_stat.MW > 3]
playing_stat.drop(['HomeTeam', 'AwayTeam', 'FTHG', 'FTAG', 'MW'],1, inplace=True)

#我們檢視下此時的資料的特徵
playing_stat.keys()
複製程式碼
Index(['FTR', 'HTGD', 'ATGD', 'HTP', 'ATP', 'HM1', 'AM1', 'HM2', 'AM2', 'HM3','AM3'], dtype='object')
複製程式碼

3.3 分析我們構造的資料

在前面,我們計算了每一的年主客場的勝率,現在我們看看有效資料中,是主場勝利的多呢,還是客場勝利的多呢?

# 比賽總數
n_matches = playing_stat.shape[0]

# 特徵數
n_features = playing_stat.shape[1] - 1

# 主場獲勝的數目
n_homewins = len(playing_stat[playing_stat.FTR == 'H'])

# 主場獲勝的比例
win_rate = (float(n_homewins) / (n_matches)) * 100

# Print the results
print("比賽總數: {}".format(n_matches))
print("總特徵數: {}".format(n_features))
print("主場勝利數: {}".format(n_homewins))
print("主場勝率: {:.2f}%".format(win_rate))
複製程式碼
比賽總數: 5250
總特徵數: 10
主場勝利數: 2451
主場勝率: 46.69%
複製程式碼

通過統計結果看到:我們主場勝率 46.69% 與我們第 2.2.1 小節原始資料分析的結果是一致的,說明我們前面構造的特徵是有效的,比較貼近實際的。

3.4 解決樣本不均衡問題

通過構造特徵之後,發現主場獲勝的比例接近 50% ,所以對於這個三分類的問題,標籤比例是不均衡的。

我們把它簡化為二分類問題,也就是主場球隊會不會勝利,這也是一種解決標籤比例不均衡的問題的方法。

# 定義 target ,也就是否 主場贏
def only_hw(string):
    if string == 'H':
        return 'H'
    else:
        return 'NH'
playing_stat['FTR'] = playing_stat.FTR.apply(only_hw)
複製程式碼

3.5 將資料分為特徵值和標籤值

# 把資料分為特徵值和標籤值
X_all = playing_stat.drop(['FTR'],1)
y_all = playing_stat['FTR']
# 特徵值的長度
len(X_all)
複製程式碼
5250
複製程式碼

3.6 資料歸一化、標準化

我們對所有比賽的特徵 HTP 進行最大最小值歸一化。

def convert_1(data):
    max=data.max()
    min=data.min()
    return (data-min)/(max-min)
r_data=convert_1(X_all['HTGD'])
複製程式碼
# 資料標準化
from sklearn.preprocessing import scale
cols = [['HTGD','ATGD','HTP','ATP']]
for col in cols:
    X_all[col] = scale(X_all[col])
複製程式碼

3.7 轉換特徵資料型別

# 把這些特徵轉換成字串型別
X_all.HM1 = X_all.HM1.astype('str')
X_all.HM2 = X_all.HM2.astype('str')
X_all.HM3 = X_all.HM3.astype('str')
X_all.AM1 = X_all.AM1.astype('str')
X_all.AM2 = X_all.AM2.astype('str')
X_all.AM3 = X_all.AM3.astype('str')

def preprocess_features(X):
    '''把離散的型別特徵轉為啞編碼特徵 '''
    output = pd.DataFrame(index = X.index)
    for col, col_data in X.iteritems():
        if col_data.dtype == object:
            col_data = pd.get_dummies(col_data, prefix = col)
        output = output.join(col_data)
    return output

X_all = preprocess_features(X_all)
print("Processed feature columns ({} total features):\n{}".format(len(X_all.columns), list(X_all.columns)))
複製程式碼
Processed feature columns (22 total features):
['HTGD', 'ATGD', 'HTP', 'ATP', 'HM1_D', 'HM1_L', 'HM1_W', 'AM1_D', 'AM1_L', 'AM1_W', 'HM2_D', 'HM2_L', 'HM2_W', 'AM2_D', 'AM2_L', 'AM2_W', 'HM3_D', 'HM3_L', 'HM3_W', 'AM3_D', 'AM3_L', 'AM3_W']
複製程式碼
# 預覽處理好的資料
print("\nFeature values:")
display(X_all.head())
複製程式碼
Feature values:
複製程式碼
HTGD ATGD HTP ATP HM1_D HM1_L HM1_W AM1_D AM1_L AM1_W ... HM2_W AM2_D AM2_L AM2_W HM3_D HM3_L HM3_W AM3_D AM3_L AM3_W
30 0.724821 0.339985 -0.043566 -0.603098 1 0 0 1 0 0 ... 0 0 0 1 0 0 1 0 1 0
31 -0.702311 -1.088217 -1.097731 -2.192828 0 1 0 1 0 0 ... 0 0 1 0 0 0 1 0 1 0
32 0.011255 0.339985 -0.570649 -0.603098 0 1 0 1 0 0 ... 0 0 0 1 0 0 1 0 1 0
33 -0.345528 -0.374116 -1.097731 -1.662918 0 1 0 1 0 0 ... 0 0 1 0 0 0 1 1 0 0
34 0.011255 1.054086 -0.570649 0.456723 1 0 0 0 0 1 ... 0 0 0 1 0 0 1 0 1 0

5 rows × 22 columns

3.8 皮爾遜相關熱力圖

我們生成一些特徵的相關圖,以檢視特徵與特徵之間的相關性。 為此,我們將利用 Seaborn 繪圖軟體包,使我們能夠非常方便地繪製熱力圖,如下所示:

import matplotlib.pyplot as plt
import seaborn as sns
# 防止中文出現錯誤
plt.rcParams['font.sans-serif']=['SimHei'] 
plt.rcParams['axes.unicode_minus']=False
#製成皮爾森熱圖
#把標籤對映為0和1
y_all=y_all.map({'NH':0,'H':1})
#合併特徵集和標籤
train_data=pd.concat([X_all,y_all],axis=1)
colormap = plt.cm.RdBu
plt.figure(figsize=(21,18))
plt.title('Pearson Correlation of Features', y=1.05, size=15)
sns.heatmap(train_data.astype(float).corr(),linewidths=0.1,vmax=1.0,
            square=True, cmap=colormap, linecolor='white', annot=True)
複製程式碼
<matplotlib.axes._subplots.AxesSubplot at 0x211ffda5860>
複製程式碼

enter image description here

通過上圖我們可以看出特徵 HTP 特徵和 HTGD 特徵相關性很強,同樣 ATP 特徵和 ATGD 特徵相關性很強,可以表明多重共線性的情況。這個我們也很容易理解,主場周平均得分數越高,那麼主場周平均淨勝球數也同樣越高。如果我們考慮這些變數,我們可以得出結論,它們給出了幾乎相同的資訊,因此實際上發生了多重共線性,這裡我們會考慮刪除 HTP 和 'ATP' 這兩個特徵,保留 HTGD 和 ATGD 這兩個特徵。皮爾森熱圖非常適合檢測這種情況,並且在特徵工程中,它們是必不可少的工具。同時,我們也可以看出上上上次球隊的比賽結果對目前比賽的結果影響較小,這裡我們考慮保留這些特徵。

  • 考慮到樣本集特徵 HTP 和 HTGD,ATP 和 ATGD 的相關性都超過了 90% ,故我們刪除特徵 HTP , ATP :
X_all=X_all.drop(['HTP','ATP'],axis=1)
複製程式碼
  • 看看與FTR最相關的10個特徵
#FTR correlation matrix
plt.figure(figsize=(14,12))
k = 10 # number of variables for heatmap
cols = abs(train_data.astype(float).corr()).nlargest(k, 'FTR')['FTR'].index
cm = np.corrcoef(train_data[cols].values.T)
sns.set(font_scale=1.25)
hm = sns.heatmap(cm, cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10}, yticklabels=cols.values, xticklabels=cols.values)
plt.show()
複製程式碼

enter image description here

我們可以看出最相關的特徵是 HTGD ,表明一個球隊主場周平均淨勝球數越高,他們贏的概率也就越大。

4.建立機器學習模型並進行預測

4.1 切分資料

將資料集隨機分成為訓練集和測試集,並返回劃分好的訓練集測試集樣本和訓練集測試集標籤。我們直接採用 train_test_split 介面進行處理。

4.1.1  train_test_split API 介面介紹
  • X_train, X_test, y_train, y_test =cross_validation.train_test_split(train_data,train_target,test_size=0.3, random_state=0)
  • 引數解釋:
    • train_data:被劃分的樣本特徵集
    • train_target:被劃分的樣本標籤
    • test_size:如果是浮點數,在0-1之間,表示樣本佔比;如果是整數的話就是樣本的數量
    • random_state:是隨機數的種子。
  • 返回值解釋:
    • x_train:訓練集特徵值
    • x_test:測試集特徵值
    • y_train:訓練集目標值
    • y_test:測試集目標值

隨機數種子:其實就是該組隨機數的編號,在需要重複試驗的時候,保證得到一組一樣的隨機數。比如你每次都填1,其他引數一樣的情況下你得到的隨機陣列是一樣的。但填0或不填,每次都會不一樣。隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個規則:種子不同,產生不同的隨機數;種子相同,即使例項不同也產生相同的隨機數。

4.1.2 程式碼處理分割資料
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_all, y_all,test_size = 0.3,random_state = 2,stratify = y_all)
複製程式碼

4.2 相關模型及其介面介紹

下面我們分別使用邏輯迴歸、支援向量機和 XGBoost 這三種不同的模型,來看看他們的表現。我們先定義一些輔助函式,記錄模型的訓練時長和評估時長,計算模型的準確率和 f1 分數。我們首先介紹一下這三個模型聯絡與區別和相關的介面:

4.2.1 邏輯迴歸介紹

邏輯迴歸模型是:假設資料服從伯努利分佈,通過極大化似然函式的方法,運用梯度下降來求解引數,來達到將資料二分類的目的。該模型的主要優點是解釋性比較好;如果特徵工程做得好,模型效果也非常不錯;訓練速度也比較快;輸出結果也很容易調整。但是該模型的缺點也很突出,比如:準確率不是很高,比較難處理資料不均衡問題等。

4.2.2 邏輯迴歸模型介面介紹

API:sklearn.linear_model.LogisticRegression(penalty='l2', dual=False, tol=0.0001, C=1.0,fit_intercept=True, intercept_scaling=1, class_weight=None, random_state=None,solver='liblinear', max_iter=100, multi_class='ovr', verbose=0,warm_start=False, n_jobs=1)

  • 主要引數解析:
    • penalty:正則化引數,l1 or  l2, default: l2;
    • C:正則化係數λ的倒數,default: 1.0;
    • fit_intercept : 是否存在截距, default: True
    • solver:損失函式的優化方法,有以下四種可供選擇{newton-cg, lbfgs, liblinear,sag},  default: liblinear
    • multi_class:分類方式選擇,一般有{ovr, multinomial}, default:ovr;
    • class_weight:型別權重引數,預設為None
    • random_state:隨機數種子,預設為無
    • tol:迭代終止判據的誤差範圍
    • n_jobs:並行數,為-1時跟CPU核數一致,預設值為1。

以上是主要引數的簡單解析,如果大家想深入瞭解,可以參看官方網址

4.2.3 支援向量機介紹

SVM(Support Vector Machine) 是一種二類分類模型。它的基本模型是在特徵空間中尋找間隔最大化的分離超平面的線性分類器。

(1)當訓練樣本線性可分時,通過硬間隔最大化,學習一個線性分類器,即線性可分支援向量機;
(2)當訓練資料近似線性可分時,引入鬆弛變數,通過軟間隔最大化,學習一個線性分類器,即線性支援向量機;
(3)當訓練資料線性不可分時,通過使用核技巧及軟間隔最大化,學習非線性支援向量機。

4.2.4 支援向量機分類模型API

sklearn.svm.SVC(C=1.0,kernel='rbf',degree=3,gamma='auto',coef0=0.0,shrinking=True,probability=False,tol=0.001,cache_size=200,class_weight=None,verbose=False,max_iter=-1,decision_function_shape=None,random_state=None)

  • 主要引數解析:
    • C:C-SVC的懲罰引數C,預設值是1.0。C越大,相當於懲罰鬆弛變數,希望鬆弛變數接近0,即對誤分類的懲罰增大,趨向於對訓練集全分對的情況,這樣對訓練集測試時準確率很高,但泛化能力弱。C值小,對誤分類的懲罰減小,允許容錯,將他們當成噪聲點,泛化能力較強。
    • kernel :核函式,預設是rbf,可以是‘linear’, ‘poly’, ‘rbf’, ‘sigmoid’, ‘precomputed’
      • 0 – 線性:u'v
      • 1 – 多項式:(gamma_u'_v + coef0)^degree
      • 2 – RBF函式:exp(-gamma|u-v|^2)
      • 3 –sigmoid:tanh(gamma_u'_v + coef0)
    • degree :多項式poly函式的維度,預設是3,選擇其他核函式時會被忽略。
    • gamma :rbf,poly 和sigmoid的核函式引數。預設是auto,則會選擇1/n_features
    • coef0 :核函式的常數項。對於poly和 sigmoid有用。
    • max_iter :最大迭代次數。-1為無限制。
    • decision_function_shape :ovo, ovr or None, default=None。

主要調節的引數有:C、kernel、degree、gamma、coef0;引數詳解請參考官網

4.2.5 XGBoost 原理介紹

XGBoost 是 Boosting演算法的其中一種, Boosting 演算法的思想是許多弱分類器整合在一起,形成一個強分類器,基本原理是下一棵決策樹輸入樣本會與前面決策樹的訓練和預測相關。以為 XGBoost 是一種提升樹模型,所以他是將許多樹模型整合在一起,形成一個很強的分類器。而所用到的樹模型則是 CART 迴歸樹模型。

4.2.6 XGBoost 介面介紹

XGBoost.XGBRegressor(max_depth=3, learning_rate=0.1, n_estimators=100, silent=True, objective='reg:linear', booster='gbtree', n_jobs=1, nthread=None, gamma=0, min_child_weight=1, max_delta_step=0, subsample=1, colsample_bytree=1, colsample_bylevel=1, reg_alpha=0, reg_lambda=1, scale_pos_weight=1, base_score=0.5, random_state=0, seed=None, missing=None, **kwargs)

  • 主要引數解析:
    • booster:模型類別,主要有2種,gbtree 和 gbliner,預設是: gbtree ;
    • nthread:使用 CPU 個數,為 -1 時表示使用全部 CPU 進行並行運算(預設),等於 1 時表示使用1個 CPU 進行運算;
    • scale_pos_weight:正樣本的權重,在二分類任務中,當正負樣本比例失衡時,設定正樣本的權重,模型效果更好。例如,當正負樣本比例為 1:10 時,scale_pos_weight=10;
    • n_estimatores:總共迭代的次數,即決策樹的個數;
    • early_stopping_rounds:在驗證集上,當連續n次迭代,分數沒有提高後,提前終止訓練
    • max_depth:樹的深度,預設值為6,典型值3-10;
    • min_child_weight:值越大,越容易欠擬合;值越小,越容易過擬合(值較大時,避免模型學習到區域性的特殊樣本),預設為1;
    • learning_rate:學習率,控制每次迭代更新權重時的步長,預設0.3;
    • gamma:懲罰項係數,指定節點分裂所需的最小損失函式下降值;
    • alpha:L1 正則化係數,預設為 1 ;
    • lambda:L2 正則化係數,預設為 1 ;
    • seed:隨機種子。

如想詳細學習該 API ,可以參考官網網址

4.3 建立機器學習模型並評估

4.3.1 建立模型
from time import time
from sklearn.metrics import f1_score

def train_classifier(clf, X_train, y_train):
    ''' 訓練模型 '''
    # 記錄訓練時長
    start = time()
    clf.fit(X_train, y_train)
    end = time()
    print("訓練時間 {:.4f} 秒".format(end - start))
    
def predict_labels(clf, features, target):
    ''' 使用模型進行預測 '''
    # 記錄預測時長
    start = time()
    y_pred = clf.predict(features)
    end = time()
    print("預測時間 in {:.4f} 秒".format(end - start))
    return f1_score(target, y_pred, pos_label=1), sum(target == y_pred) / float(len(y_pred))

def train_predict(clf, X_train, y_train, X_test, y_test):
    ''' 訓練並評估模型 '''
    # Indicate the classifier and the training set size
    print("訓練 {} 模型,樣本數量 {}。".format(clf.__class__.__name__, len(X_train)))
    # 訓練模型
    train_classifier(clf, X_train, y_train)
    # 在測試集上評估模型
    f1, acc = predict_labels(clf, X_train, y_train)
    print("訓練集上的 F1 分數和準確率為: {:.4f} , {:.4f}。".format(f1 , acc))

    f1, acc = predict_labels(clf, X_test, y_test)
    print("測試集上的 F1 分數和準確率為: {:.4f} , {:.4f}。".format(f1 , acc))
複製程式碼
4.3.2 分別初始化,訓練和評估模型
import xgboost as xgb
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

# 分別建立三個模型
clf_A = LogisticRegression(random_state = 42)
clf_B = SVC(random_state = 42, kernel='rbf',gamma='auto')
clf_C = xgb.XGBClassifier(seed = 42)

train_predict(clf_A, X_train, y_train, X_test, y_test)
print('')
train_predict(clf_B, X_train, y_train, X_test, y_test)
print('')
train_predict(clf_C, X_train, y_train, X_test, y_test)
print('')
複製程式碼
訓練 LogisticRegression 模型,樣本數量 3675。
訓練時間 0.0050 秒
預測時間 in 0.0010 秒
訓練集上的 F1 分數和準確率為: 0.6232 , 0.6648。
預測時間 in 0.0010 秒
測試集上的 F1 分數和準確率為: 0.6120 , 0.6457。

訓練 SVC 模型,樣本數量 3675。
訓練時間 0.5755 秒
預測時間 in 0.3620 秒
訓練集上的 F1 分數和準確率為: 0.6152 , 0.6746。
預測時間 in 0.1486 秒
測試集上的 F1 分數和準確率為: 0.5858 , 0.6400.

訓練 XGBClassifier 模型,樣本數量 3675. . .
訓練時間 0.4079 秒
預測時間 in 0.0110 秒
訓練集上的 F1 分數和準確率為: 0.6652 , 0.7067.
預測時間 in 0.0060 秒
測試集上的 F1 分數和準確率為: 0.5844 , 0.6279。
複製程式碼

通過執行結果,我們發現:

  • 在訓練時間上,邏輯迴歸耗時最短,XGBoost 耗時最長,為 2 秒多。
  • 在預測時間上,邏輯迴歸耗時最短,支援向量機耗時最長。
  • 在訓練集上 F1 分數方面,**XGBoost **得分最高,支援向量機得分最低,但是差距不是很大。
  • 在訓練集上準確率方面分析,XGBoost得分最高,邏輯迴歸最低。
  • 在測試集上 F1 分數方面分析,邏輯迴歸的最好,其餘兩個模型基本相等,相對較低。
  • 在測試集上準確率方面分析,邏輯迴歸支援向量機** 2 個模型基本相等,稍微比 XBGoost 高一點。

4.4 超引數調整

我們使用 sklearn 的 GridSearch 來進行超引數調參。

from sklearn.model_selection import GridSearchCV
from sklearn.metrics import make_scorer
import xgboost as xgb

# 設定想要自動調參的引數
parameters = { 'n_estimators':[90,100,110],
               'max_depth': [5,6,7],
             }
# 初始化模型
clf = xgb.XGBClassifier(seed=42)
f1_scorer = make_scorer(f1_score,pos_label=1)
# 使用 grdi search 自動調參
grid_obj = GridSearchCV(clf,
                        scoring=f1_scorer,
                        param_grid=parameters,
                        cv=5)
grid_obj = grid_obj.fit(X_train,y_train)
# 得到最佳的模型
clf = grid_obj.best_estimator_
# print(clf)
# 檢視最終的模型效果
f1, acc = predict_labels(clf, X_train, y_train)
print("F1 score and accuracy score for training set: {:.4f} , {:.4f}。".format(f1 , acc))

f1, acc = predict_labels(clf, X_test, y_test)
print("F1 score and accuracy score for test set: {:.4f} , {:.4f}。".format(f1 , acc))
複製程式碼
預測時間 in 0.0368 秒
F1 score and accuracy score for training set: 0.7991 , 0.8201。
預測時間 in 0.0149 秒
F1 score and accuracy score for test set: 0.5702 , 0.6133。
複製程式碼

4.5 儲存模型和載入模型

然後我們可以把模型儲存下來,以供以後使用。

import joblib
#儲存模型
joblib.dump(clf, 'xgboost_model.model')
#讀取模型
xgb = joblib.load('xgboost_model.model')
複製程式碼
# 然後我們嘗試來進行一個預測
sample1 = X_test.sample(n=5, random_state=2)
y_test_1 = y_test.sample(n=5, random_state=2)
print(sample1)
# 進行預測
y_pred = xgb.predict(sample1)
print("實際值:%s \n預測值:%s"%(y_test_1.values,y_pred))
複製程式碼
HTGD      ATGD  HM1_D  HM1_L  HM1_W  AM1_D  AM1_L  AM1_W  HM2_D  \
70    0.189646 -1.088217      0      0      1      0      1      0      0   
5529 -0.668332 -0.901190      0      1      0      1      0      0      0   
4297 -0.702311 -0.136082      0      1      0      0      1      0      0   
5230 -0.654740 -1.302447      0      0      1      0      1      0      0   
1307  1.438387 -0.269101      1      0      0      0      0      1      0   

      HM2_L  HM2_W  AM2_D  AM2_L  AM2_W  HM3_D  HM3_L  HM3_W  AM3_D  AM3_L  \
70        0      1      0      1      0      1      0      0      1      0   
5529      0      1      0      1      0      1      0      0      0      1   
4297      1      0      0      1      0      1      0      0      1      0   
5230      1      0      1      0      0      0      1      0      0      1   
1307      0      1      0      0      1      1      0      0      0      0   

      AM3_W  
70        0  
5529      0  
4297      0  
5230      0  
1307      1  
實際值:[0 0 1 1 1]
預測值:[1 0 1 1 1]
複製程式碼

通過以上,我們從 test 資料集中隨機挑選5個,預測值跟實際值相同的有 4 個,考慮到我們準確率不高,能夠得到這個結果來說還是比較幸運的。

5. 總結與展望:

通過該文章,您應該初步熟悉資料探勘與分析和機器學習的流程,瞭解監督學習中邏輯迴歸模型,支援向量機模型和 XGBoost 模型的基本思想,熟悉機器學習庫 Pandas、Scikit-Learn、Searbon、XGBoost、joblib 的基本使用。需要注意的是:如果您未使用 MO 平臺,可能還需要安裝 XGBoost、SKlearn 等第三方庫,目前 Mo 平臺已安裝常用的機器學習相關的庫,可以省去您安裝開發平臺的時間;另外,資料集也已在平臺公開,可以直接匯入。目前對於主流的機器學習庫的相關資料,我們總結如下:

目前我們模型的準確率還不是很高,還可以進一步的改進我們的模型,這裡我們提供一下解決思路:

  • 1、獲取更多的資料或者使用更多的特徵;
  • 2、對資料集進行交叉驗證方式處理;
  • 3、可以對以上模型深入處理或者採用模型融合技術等;
  • 4、分析參賽球員的踢球技術資訊和健康資訊等;
  • 5、採用更全面的模型評估機制,目前我們僅僅考慮了準確率和 F1 分數,可以進一步考慮 ROCAUC** 曲線等。

我們已經將以上內容整理成機器學習實戰相關課程,您可以在網站 訓練營實戰教程 中選擇 監督學習-分析和預測足球比賽結果 進行實操學習。您在學習的過程中,發現我們的錯誤或者遇到難題,可以隨時聯絡我們。


Mo(網址:momodel.cn)是一個支援 Python 的人工智慧線上建模平臺,能幫助你快速開發、訓練並部署模型。


Mo 人工智慧俱樂部 是由網站的研發與產品設計團隊發起、致力於降低人工智慧開發與使用門檻的俱樂部。團隊具備大資料處理分析、視覺化與資料建模經驗,已承擔多領域智慧專案,具備從底層到前端的全線設計開發能力。主要研究方向為大資料管理分析與人工智慧技術,並以此來促進資料驅動的科學研究。

目前俱樂部每週六在杭州舉辦以機器學習為主題的線下技術沙龍活動,不定期進行論文分享與學術交流。希望能匯聚來自各行各業對人工智慧感興趣的朋友,不斷交流共同成長,推動人工智慧民主化、應用普及化。

enter image description here

相關文章