機器學習與賽馬

前言

香港影視劇裏面比較有意思的元素是賽馬, 忽然弄懂了一部分賽馬的投注規則,再者學些機器學習ing,所以試試能否通過機器學習來預測賽馬結果。

注: 這裏當然任何沒有鼓勵大家×××的意思,純粹練手。

爲了避免直接被用於線下實戰中,所以這裏使用的是新加坡賽馬的數據.

源代碼: 機器學習與賽馬

文章結構

  • 數據獲取
  • 數據分析
  • 數據可視化
  • 機器學習
  • 總結

數據獲取

由於新加坡的馬會網站不能直接訪問需要×××,由於沒有比較好的×××,所以採用單線程訪問,怕被屏蔽IP,有興趣的可以在有比較好的牆外代理的話,可以加上多線程或者多協程. 而scrapy之類的框架用的不多,所以手寫了。

雖然很簡單,但是代碼也有300多行,這裏只講邏輯,源代碼可以直接查看源代碼。

找出鏈接

經過觀察,官網的每日的賽馬結果在209/01/01之後會有一個遞增的ID值,2009/01/01的ID值是1,下一賽馬日是2,以此類推,截止至2019/03/29日,ID值爲8375.

而歷史結果通過下面的形式訪問:

http://cn.turfclub.com.sg/Racing/Pages/ViewRaceResult/<ID>/All

如:
http://cn.turfclub.com.sg/Racing/Pages/ViewRaceResult/1/All

因此遍歷所有ID範圍就能獲取所有歷史結果。
代碼示例如下:

base_url = "http://cn.turfclub.com.sg/Racing/Pages/ViewRaceResult/%s/All"
for num in range(start_num, 8376):
    url = base_url % num
    soup = download(url)

找出數據

經過觀察,主觀覺得需要以下字段。

機器學習與賽馬

存儲數據

爲了減少對數據庫的依賴這裏通過sqlite3存儲數據.
關於sqlite3的數據格式存儲結果及csv的結果可以在源代碼倉庫查看。

Sqlite3數據結構如下:

CREATE TABLE IF NOT EXISTS DATA
   (ID CHAR(30) PRIMARY KEY     NOT NULL,
   DATE         CHAR(20)    NOT NULL,
   LOCATION     CHAR(30) NOT NULL,
   LENGTH       INT     NOT NULL,
   TRACK        CHAR(30) NOT NULL,
   TRACK_TYPE   CHAR(30),
   TRACK_STATUS CHAR(30)     NOT NULL,
   RACE_NUM     INT     NOT NULL,
   H_NO         INT     NOT NULL,
   HORSE_NAME   CHAR(30)     NOT NULL,
   GEAR         CHAR(30),
   HORSE_RATING INT     NOT NULL,
   H_WT         REAL     NOT NULL,
   HCP_WT       REAL     NOT NULL,
   C_WT         REAL     NOT NULL,
   BAR          INT     NOT NULL,
   JOCKEY       CHAR(30)     NOT NULL,
   TRAINER      CHAR(30)     NOT NULL,
   RUNING_POSITION   CHAR(10)     NOT NULL,
   PI           INT     NOT NULL,
   TOTAL_SECONDS     INT,
   LBW          REAL     NOT NULL
   );

小結

很有意思的是,在這些結果裏面除了新加坡的結果還有其他國家的結果,我也不知道爲啥,這裏這裏只取新加坡的數據結果。

還有就是有的ID值沒有結果,我也不知道爲啥。

注: 在獲取過程中,有些部分處理的並不是非常好,所以一些字符串類型的數據,摘取的並不是非常準確, 所以GitHub倉庫裏面提供的數據文件,有一小部分會有問題,希望你知悉。

數據分析

上一節我們將數據爬了下來,這一節就是將數據進行一些統計分析,試圖發現一些其中的規律。

準備工作

首先導入python數據分析三劍客

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

將保存的數據通過pandas加載。

import sqlite3
conn = sqlite3.connect('data.db')
df = pd.read_sql("SELECT * FROM DATA", conn, index_col="ID", parse_dates=["DATE"])

常規操作

查看數據
df.head()

DATE    LOCATION    LENGTH  TRACK   TRACK_TYPE  TRACK_STATUS    RACE_NUM    H_NO    HORSE_NAME  GEAR    ... H_WT    HCP_WT  C_WT    BAR JOCKEY  TRAINER RUNING_POSITION PI  TOTAL_SECONDS   LBW
ID                                                                                  
2009-01-01-1-3-1    2009-01-01  新加坡 1700    unkonw  (POLYTRACK) 良好  1   10  WINNIEDUN       ... 440.0   52.0    52.0    3   M AU    HK TAN  1-1-1   1   0.0 0.0
2009-01-01-1-9-2    2009-01-01  新加坡 1700    unkonw  (POLYTRACK) 良好  1   2   REGAL KNIGHT        ... 457.0   59.0    57.0    9   E ASLAM L TRELOAR   9-5-2   2   0.0 0.5
2009-01-01-1-10-3   2009-01-01  新加坡 1700    unkonw  (POLYTRACK) 良好  1   3   JOYLUCK     ... 477.0   58.5    58.5    10  N CALLOW    RB MARSH    11-10-3 3   0.0 1.3
2009-01-01-1-1-4    2009-01-01  新加坡 1700    unkonw  (POLYTRACK) 良好  1   1   MAJESTIC KNIGHT     ... 521.0   59.0    55.0    1   T. AFFANDI  L TRELOAR   4-3-4   4   0.0 1.8
2009-01-01-1-8-5    2009-01-01  新加坡 1700    unkonw  (POLYTRACK) 良好  1   7   ELEVENTH AVENUE     ... 477.0   54.5    54.5    8   R FRADD M FREEDMAN  8-8-5   5   0.0 2.6

常規統計
df.descibe()


LENGTH  RACE_NUM    H_NO    HORSE_RATING    H_WT    HCP_WT  C_WT    BAR PI  TOTAL_SECONDS   LBW
count   88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000    88110.000000
mean    1331.634321 5.464317    6.324413    47.422812   495.280524  54.975701   54.326813   6.137975    6.339610    79.863567   5.974457
std 256.724820  2.977882    3.494310    20.767828   31.383475   2.532528    2.667349    3.383717    5.787673    20.725862   5.966324
min 1000.000000 1.000000    1.000000    0.000000    372.000000  45.500000   0.000000    0.000000    1.000000    0.000000    0.000000
25% 1200.000000 3.000000    3.000000    40.000000   474.000000  53.000000   52.500000   3.000000    3.000000    70.052500   2.100000
50% 1200.000000 5.000000    6.000000    49.000000   494.000000  55.500000   55.000000   6.000000    6.000000    73.650000   4.900000
75% 1600.000000 8.000000    9.000000    59.000000   516.000000  57.000000   56.500000   9.000000    9.000000    94.980000   8.200000
max 2400.000000 12.000000   18.000000   127.000000  647.000000  59.500000   59.500000   16.000000   99.000000   640.980000  99.800000

有的放矢

查看自己感興趣的數據

查看每匹馬每場比賽的速度

首先去掉total_seconds爲零的場次

其實我應該講時間字段的原始數據保存下來的,這裏原始數據其實是0:00.00,

但是由於爲了怕時間單位一樣,但是名次不一樣,所以在時間單位上還加了一個離頭馬的距離除以100,假設離頭馬的距離是1,那麼1/100 等於10毫秒

所以這裏應該去掉小於1的數據, 但是數據其實還是有些異常,所以去掉小於50的結果


df2 = df.query("TOTAL_SECONDS > 50")
df2["SPEED"] = df["LENGTH"] / df["TOTAL_SECONDS"]
df2["SPEED"].head()

ID
2009-04-03-1-8-1     16.605385
2009-04-03-1-5-2     16.568047
2009-04-03-1-7-3     16.560208
2009-04-03-1-10-4    16.554334
2009-04-03-1-3-5     16.495817
Name: SPEED, dtype: float64

查看每個騎師每場比賽的速度

df2[["SPEED", "JOCKEY"]].groupby("JOCKEY").mean().sort_values(by="SPEED").tail()

            SPEED
JOCKEY  
麥維凱 16.899374
郗福年 16.952015
薛凱華 16.969355
杜美爾 16.973066
杜利萊 17.204301

df2[["SPEED", "JOCKEY"]].groupby("JOCKEY").mean().mean()
SPEED    16.400328
dtype: float64

數據可視化

可視化自己感興趣的特徵值

可視化速度分佈

df2[["SPEED", "HORSE_NAME"]].groupby("HORSE_NAME").mean().hist(bins=20)

機器學習與賽馬

可視化馬匹速度的歷史規律

# 首先找一匹歷史數據中賽馬次數最多的馬匹

speed_and_horse = df2[["SPEED", "HORSE_NAME"]]
pd.value_counts(speed_and_horse["HORSE_NAME"]).head()

亂亂來       89
甜蜜舞曲      77
PACINO    71
太陽帝國      69
白咖啡       68
Name: HORSE_NAME, dtype: int64

speed_and_horse.query('HORSE_NAME == "亂亂來"').plot(figsize=(16,9))

機器學習與賽馬

可以發現這匹馬在18年以前基本上還是在16.25左右上下徘徊, 不好不壞, 但是18年以後就不行了,可能老了吧

但是很有可能這是特例,所以我們將出場次數前20的馬匹都繪製出來

順便繪製一下賽馬次數的前二十匹馬的歷史數據。

#  首先看看前二十的馬匹
pd.value_counts(speed_and_horse["HORSE_NAME"]).head(20)

亂亂來                89
甜蜜舞曲               77
PACINO             71
太陽帝國               69
白咖啡                68
LUCKY SUN          67
NINTH AVENUE       66
六合興旺               66
東道主                66
SHINKANSEN         65
DAAD'S THE WAY     65
經典烏龍               64
新邦                 64
INCREDIBLE HULK    64
藍寶石                64
聖冠                 64
快好                 64
銀河快車               64
巨大火球               64
CONQUEST           63
Name: HORSE_NAME, dtype: int64

plot_df = pd.DataFrame()
plt.rcParams['font.sans-serif'] = ['simhei']
for horse_name in pd.value_counts(speed_and_horse["HORSE_NAME"]).head(20).index:
    df_horse = speed_and_horse.query(f'HORSE_NAME == "{horse_name}"')["SPEED"].iloc[:63]

    plot_df[horse_name] = df_horse.reset_index()["SPEED"]

plot_df.plot(figsize=(16,9))

機器學習與賽馬

機器學習

不知大家是否知道的賽馬的規則,賽馬的名次排名是誰的速度最快,誰就獲勝,所以想要預測賽馬的名次應該是比較誰的速度快,而不是看其獲得名次的概率,換言之,這是一個迴歸的問題。

這裏就嘗試線性迴歸吧。

爲啥不用神經網絡深度學習什麼的?不會呀

特徵選擇

首先選擇特徵跟預測值,預測值很好確定,就是前面的速度。

首先靠直覺選幾個特徵可視化以下。這裏選擇的特徵是C_WT(配磅),以及HORSE_RATING(馬匹評分)。

配磅是指,讓馬匹配備以下重量的物件,用於儘可能讓本輪比賽的每匹馬的狀態差不多,鋤強扶弱。

馬匹評分由馬會根據制定的規則打分。

可視化一下C_WT與SPEED之間的關係

# 這裏還是選擇查看  "亂亂來"這匹馬

df2.query('HORSE_NAME == "亂亂來"')[["C_WT", "SPEED"]].reset_index().plot.scatter(x="C_WT", y="SPEED", figsize=(16,9))

機器學習與賽馬

上圖看不出啥,但是大概知道兩者沒啥相關性,可借用seaborn更直觀的可視化一下

import seaborn as sns
j = sns.jointplot("C_WT", "SPEED", data=df2.query('HORSE_NAME == "亂亂來"')[["C_WT", "SPEED"]].reset_index(), kind="reg")
# j = sns.jointplot('Num of A', ' Ratio B', data = data_df, kind='reg', height=8)
j.annotate(stats.pearsonr)

機器學習與賽馬

可視化一下HORS_RATING與SPEED之間的關係

import seaborn as sns
import scipy.stats as stats

j = sns.jointplot("HORSE_RATING", "SPEED", data=df2.query('HORSE_NAME == "亂亂來"')[["HORSE_RATING", "SPEED"]].reset_index(), kind="reg")
j.annotate(stats.pearsonr)

機器學習與賽馬

很明顯還是沒啥關係。

其實相關性太差,但是不適合做特徵,但是又不是真的爲了真的做一個可以實戰的模型,這裏就將這兩個做作爲特徵吧。

爲了機器學習而學習吧, 強行擬合!!!

擬合模型

選一個模型,這裏選擇LinearRegression。

from sklearn.linear_model import LinearRegression
df_lll = df2.query("HORSE_NAME == '亂亂來'")[["SPEED", "C_WT", "HORSE_RATING"]]
split_index = int(len(df_lll) * 0.6)
X_Train = df_lll[:split_index][["C_WT", "HORSE_RATING"]]
Y_Train = df_lll[:split_index][["SPEED"]]
X_Test = df_lll[split_index:][["C_WT", "HORSE_RATING"]]
Y_Test = df_lll[split_index:][["SPEED"]]

model = LinearRegression()
model.fit(X_Train.values, Y_Train.values)

將結果擬合的模型可視化一下

from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = Axes3D(fig)
ax.set_xlabel("C_WT")
ax.set_xlabel("HORSE_RATING")
ax.set_xlabel("SPEED")
ax.scatter(X_Train["C_WT"].values, X_Train["HORSE_RATING"].values, Y_Train.values)
ax.plot_surface(X_Train["C_WT"].values, X_Train["HORSE_RATING"].values, model.predict(X_Train.values))

機器學習與賽馬

總結

其實在機器學習這一節應該將數據正則一下,或者說清洗一下。但是沒關係,本來就知道沒啥好擬合的。

關於本文的notebook也在源代碼裏面。有空在寫。

後記

其實這裏不應該基於特徵做模型,因爲賽果的成績精度非常高,再者會有很多噪音會干擾的結果,所以無論迴歸還是分類的結果都不能精確的命中最終結果(不過也有可能是我的水平有限)。

既然不行爲啥還寫篇文章?論證一下咯。

但是就個人而言,我覺得還是可以提升一定的命中概率的,沒必要那麼精確,那麼提升概率的關鍵在於均值迴歸,即每匹馬的速度回圍繞着一個均值來回震動,如果大家有興趣,我們來做一個更細緻的分析吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章