トップ 差分 一覧 ソース 検索 ヘルプ PDF RSS ログイン

Rと競馬データで学ぶ統計学 第3回 基本統計関数を適用する

キーワード

この記事はRと競馬データで学ぶ統計学シリーズの一部です。

はじめに

Rと競馬データで学ぶ統計学シリーズ第3回は、第2回で紹介した方法で読み込んだ競馬データに対し、さまざまな統計関数を適用し、データの特性や傾向を見ていきます。

この記事では、はじめに使用する競馬データについて紹介し、読み込みます。そして、データに対してさまざまな処理を適用していきます。

なお、以降の説明はWindows環境をベースに進めます。Mac OS XやUNIX / Linux環境をお使いの方は適宜読み替えてください。

使用する競馬データについて

この記事では、第1回で紹介した、中央競馬(JRA)または地方競馬(NAR)のデータを使います。以下のGitHubレポジトリをクローンまたはダウンロードしてください。


なお、上記のレポジトリにあるCSVファイルは、文字コードがUTF-8になっています。自分で変換できる方は適当に対応していただければよいですが、Windowsユーザー用にShift-JISに変換したデータをまとめてZIP形式でアップロードしています。


いずれか、適当なものをダウンロードしてください。

データの確認とRへの読み込み

 データの確認

ダウンロードしたデータを(ZIPファイルは展開して)、適当なフォルダに配置します。GitHubレポジトリをクローンすると、以下のようなフォルダ構成になっているはずです。

2016/12/09  17:07          39391001 jra_race_result.csv
2016/12/09  17:22              3790 keiba_data_small_sample.csv
2016/12/06  01:42          72658197 nar_race_result.csv
2016/12/09  17:22                74 README.md
2016/12/09  17:24          23839711 Win_R_Keiba_data.zip
    5 個のファイル         135892773 バイト

ZIPファイルをダウンロードして展開した場合は、以下のような構成になります。

2016/11/30  02:54          32514070 jra_race_result.csv
2016/12/09  17:10            620662 jra_race_result_subset.csv
2016/12/06  01:40          61675929 nar_race_result.csv
    3 個のファイル         94810661 バイト

jra_race_result_subset.csv は、コンピュータのスペックなどの関係で大きなファイルが読み込めない場合にかわりに使用するデータです。JRAの2016年11月のレース結果のみが含まれます。

このデータを、Rを起動し、読み込みます。Windowsの場合は、スタートメニューやデスクトップのショートカットからR(この記事ではMicrosoft R Openを推奨しています)を起動します。

 データを配置したフォルダへの移動

Rを起動してすぐの状態では、Rは「マイドキュメント」[1]作業フォルダ(ようは、今開いているフォルダのこと)としています。そのため、競馬データを配置したフォルダに移動する必要があります。Windowsでは、Rの作業フォルダを変更する方法は2つあります。

GUIメニューからの移動

RのGUIでは、上部にメニューバーがあります。その中の「ファイル」メニューをクリックすると、「ディレクトリの変更」という項目があります。これをクリックすると、Windowsの標準的な方法でフォルダの移動ができます。データを配置したフォルダを選択し、「OK」を押します。


setwd 関数による移動

Windows以外のOSでも使える一般的な方法として、Rの setwd 関数を使うやり方があります。Rのプロンプトに以下のように入力します。

setwd("移動先フォルダ名")
(例)
setwd("C:/User/XXXXX/Desktop/Win_R_Keiba_data) # 絶対パス
setwd("../Desktop/Win_R_Keiba_data") # 相対パス

setwd 関数の引数に、移動先のフォルダパスを絶対パスまたは相対パスで指定します。なお、Rではフォルダ区切りに / を使う必要があります。[2]Windowsの標準は \ ですが、書き換える必要があります。

GUIあるいは setwd 関数、どちらか使いやすいほうで、競馬データを配置したフォルダ(Win_R_Keiba_data など)に移動します。移動できたかどうかを確認するには、getwd 関数を使います。

setwd("Win_R_Keiba_data/")
getwd()
[1] "C:/Users/XXXXX/Desktop/Win_R_Keiba_data"

また、フォルダ内のファイルを一覧するには(エクスプローラで見ればいいんですが)、dir 関数を使います。

dir()
[1] "jra_race_result.csv"  "jra_race_result_subset.csv"
[3] "nar_race_result.csv"

 データの読み込み(read.csv

データを配置したフォルダに移動したら、データを読み込みましょう。データはCSV形式[3]なので、read.csv 関数で読み込みます。その他のファイル形式の読み込みについては、第2回で紹介しています。

# 中央競馬データの読み込み
jra_data <- read.csv("jra_race_result.csv", header=TRUE)
# 地方競馬データの読み込み
nar_data <- read.csv("nar_race_result.csv", header=TRUE)
# 上記ファイルが大きすぎて開けない環境では
jra_data <- read.csv("jra_race_result_subset.csv", header=TRUE)

上記は、すべて実行する必要はありません。中央競馬(そのサブセット)、地方競馬いずれかをお好みで読み込んでください。コンピュータのスペックに余裕がある方は、両方とも読み込んでみてください。

(ようやく)競馬データを扱う準備が整いました。これから、中央競馬あるいは地方競馬のデータについて、さまざまな関数を適用していきます。

データの一覧とデータ型

競馬データに限らず、なんらかのデータファイルをRに読み込んだら、まずはその構造、列ごとの型を確認しましょう。

 データの一覧と構造

Rに現在存在するオブジェクトの一覧を表示するには、ls 関数を使います。[4]

ls()
[1] "jra_data" "nar_data"

先ほど読み込んだデータが2つのオブジェクトとして格納されています。(CSVファイルを読み込んだデータフレームだとわかっているのですが)手順として、オブジェクトの型を確認してみましょう。オブジェクトの型は2章でも紹介しましたがclass 関数で確認できます。

class(jra_data)
[1] "data.frame"
class(nar_data)
[1] "data.frame"

ちなみに、オブジェクトを削除するには rm 関数を使います。

rm(オブジェクト名) # ダブルクォーテーションで囲まない

たくさんオブジェクトを作成して、メモリが不足した場合などに適宜実行します。詳細は、0093 Weblog: Rでメモリが足りなくて大変だよ!助けてドラえもん!などを参照してください。

データフレームに格納されたデータの構造を確認するには、str 関数を使います。

# 中央競馬データの構造
str(jra_data)
'data.frame':   193825 obs. of  30 variables:
 $ 開催日      : Factor w/ 426 levels "2013-01-05",..: 1 1 1...
 $ 競馬場      : Factor w/ 10 levels "京都","阪神",..: 7 7 7...
 $ レース番号  : int  1 1 1 1 1 1 1 1 1 1 ...
 $ レース名    : Factor w/ 1130 levels "1932-1950sダービーメモリーズトキノミノルカップ",..: 152 152 152...
 $ コース      : Factor w/ 3 levels "ダート","芝",..: 1 1 1...
 $ 周回        : Factor w/ 8 levels "右","右2周","左",..: 1 1 1...
 $ 距離        : int  1200 1200 1200 1200 1200 1200...
 $ 馬場状態    : Factor w/ 4 levels "重","不良","良",..: 3 3 3...
 $ 賞金        : num  500 200 125 75 50 0 0 0 0 0 ...
 $ 頭数        : int  16 16 16 16 16 16 16 16 16 16 ...
 $ 着順        : int  1 2 3 4 5 6 7 8 9 10 ...
 $ 枠番        : int  6 2 7 1 5 5 3 2 4 3 ...
 $ 馬番        : int  12 4 13 1 10 9 6 3 7 5 ...
 $ 馬名        : Factor w/ 24373 levels "アーカイブ",...: 22934 350...
 $ 性別        : Factor w/ 3 levels "セ","牡","牝": 2 3 3 2...
 $ 年齢        : int  3 3 3 3 3 3 3 3 3 3 ...
 $ 騎手        : Factor w/ 257 levels "A.アッゼニ",..: 75 168 74...
 $ タイム      : num  73.6 73.6 73.7 74 74.3 74.3 74.3...
 $ 着差        : Factor w/ 22 levels "","1 1/2馬身",..: 1 20 20...
 $ 通過順      : Factor w/ 8698 levels "","01-01",..: 2 233 2279...
 $ 上り3F      : num  39.4 39.3 39.1 39.5 38.9 39.4...
 $ 斤量        : num  56 54 54 56 56 56 53 56 54 54 ...
 $ 馬体重      : int  484 454 396 484 462 474 506 448 448 476 ...
 $ 増減        : int  2 -2 0 6 2 8 -4 8 -4 -2 ...
 $ 人気        : int  1 7 5 2 3 6 10 14 11 4 ...
 $ オッズ      : num  1.9 44.1 11.8 5.3 6.1 ...
 $ ブリンカー  : Factor w/ 2 levels "","B": 1 1 1 1 1 1 1 1 1 1 ...
 $ 調教師      : Factor w/ 321 levels "C.チャン",...: 120 204...
 $ 調教コメント: Factor w/ 181 levels "いま一息",..: 46 102...
 $ 調教評価    : Factor w/ 5 levels "A","B","C","D",..: 2 3 2...
 
# 地方競馬データの構造
str(nar_data)
'data.frame':   552645 obs. of  22 variables:
 $ 開催日    : Factor w/ 1429 levels "2013-01-01",..: 1 1 1 ...
 $ 競馬場    : Factor w/ 15 levels "浦和","園田",..: 11 11 11 ...
 $ レース番号: int  1 1 1 1 1 1 1 1 2 2 ...
 $ コース    : Factor w/ 2 levels "ダ","芝": 1 1 1 ...
 $ 距離      : int  200 200 200 200 200 200 200 200 200 200 ...
 $ 馬場状態  : Factor w/ 63 levels "0.4%","0.5%",..: 46 46 46 ...
 $ 賞金      : int  70000 70000 70000 ...
 $ 着順      : int  1 2 3 4 5 6 7 8 1 2 ...
 $ 馬番      : int  6 7 2 1 8 4 5 3 1 3 ...
 $ 馬名      : Factor w/ 26695 levels "アアイマデショー",..: 12235 19324 ...
 $ 性別      : Factor w/ 3 levels "セ","牡","牝": 3 2 2 3 2 3 2 2 2 2 ...
 $ 年齢      : int  4 4 4 4 4 4 4 4 4 4 ...
 $ 毛色      : Factor w/ 12 levels "芦毛","栗駁毛",..: 10 3 10 ...
 $ 斤量      : num  610 630 630 610 630 610 630 630 620 630 ...
 $ 騎手      : Factor w/ 478 levels "Cデム","Iチョ",..: 392 457 233 ...
 $ 馬体重    : int  920 942 929 984 970 1021 980 895 969 982 ...
 $ 増減      : int  -8 -6 -2 2 2 3 -8 -4 7 -9 ...
 $ タイム    : num  106 116 122 129 131 ...
 $ 上り3F    : num  NA NA NA NA NA NA NA NA NA NA ...
 $ 調教師    : Factor w/ 766 levels "Bチェ","C・ウ",..: 87 352 345 ...
 $ 人気      : int  1 3 2 4 7 5 6 8 4 1 ...
 $ オッズ    : num  1.5 5.5 5.4 8 29.3 16.2 19.9 37.3 7.1 1.6 ...

筆者が収集しているデータでは、中央競馬と地方競馬で、データの構造が異なります。予測に必要と思われる基本的な要素は共通していますが、中央競馬データに含まれる通過順や調教評価、コメントなどは地方競馬データには含まれません。また、地方競馬にはばんえい競馬も含まれるため、馬場状態(ばんえいでは含水率表示)、毛色(ばんえいはそもそもサラブレッドではない重種なので、「栗駁毛」(くりぶちげ)などが存在する[5])などで中央競馬とは大きく異なる部分があります。まずはデータの構造を吟味して、意図した型でデータが格納されているかを確認しましょう。

ばんえい競馬の「ばん馬」(2012年5月、帯広競馬場にて)


たとえば、馬体重やその増減など、数値以外入らないことが明らかなデータが factor 型などで格納されている場合、元のファイル中に異常値が含まれている可能性が高いため、ファイルを精査する必要があります。実際に、筆者が取っている方法でデータを取得する場合、新馬戦[6]における馬体重の前走との増減は存在しないため、“ - ”(スペース、ハイフン、スペース)として記録されます。[7]このようなデータが混じると、「増減」列が factor 型になってしまいます。筆者が提供しているデータに関しては、そのあたりの前処理は行ったうえで公開していますが、ご自身でデータを収集する場合には、異常値、欠損値への対応を検討する必要があります。

 データ型の変換

上述のように str 関数などでデータの構造、型を確認し、意図通りであればよいのですが、中には期待する型ではない型でデータが読み込まれる場合があります(大抵の場合、データの不備が原因ですが)。そのような時に、データ型を変換する関数を適用し、データを修正する必要があります。ここでは、いくつかの代表的な関数を紹介します。

数値型への変換: as.numeric 関数

as.numeric 関数は、引数に指定したデータ(ベクトル)を数値型に変換します。表計算ソフトで保存したデータファイルなどで、数値であるべき行・列の値がダブルクォーテーションで囲まれるなどの結果、文字列型などで読み込まれた場合に適用します。

as.numeric(ベクトル)
(例)
a <- c("1", "2", "3", "4") # あえて文字列ベクトルを作る

a[1] + a[2] # 計算はできない
a[1] + a[2] でエラー:  二項演算子の引数が数値ではありません

a <- as.numeric(a) # 数値型に変換して自分自身に代入
a[1] + a[2] # 計算ができる
[1] 3

ただし、変換対象のデータの型が factor 型である場合、“見た目”の値とRの内部で割り当てる値が異なる場合があるため、変換時にもうひと手間が必要になることがあります。

# あえてfactor型の変なデータを作る
a <- factor(c("1", "2", "3", "4"), levels=c("4", "3", "2", "1"))
b <- as.numeric(a) # とりあえず数値型に変換するものの
b[1] + b[2]
[1] 7 # なんでそーなる!?

b <- as.numeric(as.character(a)) # “見た目”の値のまま数値型に変換する
b[1] + b[2]
[1] 3 # 意図した結果になった

特殊なケースなので、あまり詳しくは触れませんが、ようは factor 型は“見た目”の値(ラベル)と内部の値(水準)が異なる場合があることに注意する必要があります。

factor 型への変換: as.factor 関数、factor 関数

上で出てきた factor 型ですが、日本語では「水準型」と訳されることもあります。factor 型は一般に、データをグループ(水準)分けし、水準ごとに集計したり、比較をする際の“キー”に使われます。例えば、東京競馬場、中山競馬場...あるいは芝、ダート、障害と競馬場 / コースごとにデータを集計したい場合、「競馬場」列や「コース」列の値が factor 型になっていると、Rは処理をしやすくなります。

factor 型の変数を作る場合、2つの方法があります。

as.factor 関数
順序のない(あるいはデフォルト順でよい)水準を作ります。デフォルトとは、アルファベットあるいは文字コード順に水準が割り当てられるという意味です。

# デフォルト順で困らない場合
types <- c("a", "b", "c", "d")
class(types) # この時点では文字列型
[1] "character"

types <- as.factor(types) # factor型に変換する。順番はa, b, c, dの順でよい
types # オブジェクトの内容を確認する。4つの水準からなるfactor型になっている
[1] a b c d
Levels: a b c d
factor 関数
順序を任意に指定して、水準を作成できます。文字コード順の割り当てでは不都合がある場合に使います。
# デフォルト順だと困る場合
sex <- c("male", "female")
sex.factor <- as.factor(sex) # とりあえず変換してみる
sex.factor # オブジェクトの内容を確認する。水準としてはfemaleが先に来る
[1] male   female
Levels: female male

# factor関数で水準の順番を指定する
sex.factor <- factor(sex, levels=c("male", "female"))
sex.factor # オブジェクトの内容を確認する
[1] male   female
Levels: male female

sex <- c("男性", "女性") # 日本語でも同様の問題が起きる
sex.factor <- as.factor(sex) # とりあえず変換してみる
sex.factor # オブジェクトの内容を確認する。水準としては女性が先に来る
sex.factor <- factor(sex, levels=c("男性", "女性"))
sex.factor # オブジェクトの内容を確認する
[1] 男性 女性
Levels: 男性 女性

factor 型の水準の順番は、グラフなどで可視化した際に、凡例の並び順などに影響します。順番を指定する必要がある場合は、factor 関数の levels オプションで適切に指定します。

read.* 関数の stringsAsFactors オプションについて
read.* 関数では stringsAsFactors オプションに FALSE を指定することで、文字列を character 型で読み込むようになります。

ファイルからデータを読み込みデータフレームを作成する read.* 関数は、デフォルトの動作として文字列は factor 型として格納します。ただし、factor 型である必要がないデータ(たとえばアンケートの自由回答文字列など)や、水準の順番を明示的に指定しなければならない場合は、まずは文字列型(character 型)として読み込んだほうが手間がかかりません。

read.csv(ファイル名, stringsAsFactors = FALSE)

文字列型への変換: as.character 関数

数値型や factor 型のデータを文字列(character)型に変換するには、as.character 関数を使います。

a <- c(1, 2, 3, 4) # 数値型のベクトルを作成する
class(a)
[1] "numeric"
a <- as.character(a) # 文字列型に変換する
class(a)
[1] "character"
a # オブジェクトの内容を確認する
[1] "1" "2" "3" "4"

時刻データの取り扱い: as.POSIXct 関数、as.POSIXlt 関数、as.Date 関数

開催日など、競馬データの中には時刻(日時)をあらわすデータも含まれます。例えば、筆者が提供しているデータでは、開催日は 2016-12-18 という形式で記録しています。Rは、データを読み込んだ時点ではそのような文字列を factor 型または character 型として保存します。しかし、そのままでは「1週間前」「3か月前」のデータを参照する、といった時に不便です。

そこで、時刻(のように見える)データを時刻型に変換します。Rにおける時刻型には大きく3種類、POSIXct 型、POSIXlt 型、Date 型があります。

POSIXct 型:UNIX timeを格納
POSIXct 型は、内部では1970-01-01 00:00:00 からの経過秒数、いわゆるUNIX timeを格納しています。

POSIXct 型のオブジェクトを作成するには、as.POSIXct 関数を使います。なお、デフォルトで受け付ける書式は YYYY-mm-DD HH:MM:SS です。

a <- as.POSIXct("2016-12-18 17:30:00")
[1] "2016-12-18 17:30:00 JST"

str(unclass(a)) # 内部の値を確認する
atomic [1:1] 1.48e+09 # 基準時から1480000000秒経過したことを示す

実際には、UNIX timeを意識する必要はほとんどありませんが、データを POSIXct 型に変換することで、「翌日」「昨日」「1時間前」などの操作が簡単にできるようになります。

a + 86400 # 1日=86400秒を足す
[1] "2016-12-19 17:30:00 JST"

a - 3600 # 1時間=3600秒を引く
[1] "2016-12-18 16:30:00 JST"
POSIXlt 型:時刻情報をリストで格納
POSIXlt 型は、年月日、時刻をリストの各要素として保持し、個別に取り出すことができます。

外見(プロンプトからオブジェクトを呼び出した結果)は POSIXct 型と変わりません。なお、デフォルトで受け付ける書式は YYYY-mm-DD HH:MM:SS です。

a <- as.POSIXlt("2016-12-18 17:30:00")
a
[1] "2016-12-18 17:30:00 JST"

オブジェクトの構造を見ると、POSIXct 型よりも複雑になっています。なお、基準年は1900年です。

str(unclass(a))
List of 11
 $ sec   : num 0
 $ min   : int 30
 $ hour  : int 17
 $ mday  : int 18
 $ mon   : int 11
 $ year  : int 116
 $ wday  : int 0
 $ yday  : int 352
 $ isdst : int 0
 $ zone  : chr "JST"
 $ gmtoff: int NA

a$year
[1] 116
a$year + 1900
[1] 2016

POSIXct 型と POSIXlt 型のオブジェクトは、ベクトルなどオブジェクト単体で完結する場合は、どちらでも好みの(+処理のしやすい)ものを使えばよいですが、データフレームの1つの列など、より大きなデータ構造の中に組み込んで使用する場合は、構造がシンプルな POSIXct 型を使うとよいでしょう。Rの時刻型データに関するマニュアルにも、以下のように記載されています。

"POSIXct" is more convenient for including in data frames,

and "POSIXlt" is closer to human-readable forms.

"POSIXct" 型は、データフレームに格納するのに適しています。

"POSIXlt" 型は、人間にとってわかりやすい構造になっています。(訳: 筆者)

POSIXlt 型をデータフレームに格納すると、日付ごとにリスト構造が格納されるため、オブジェクトのサイズが大きくなります。実際に確認してみましょう。

jra_data<-read.csv("jra_race_result.csv")
jra_data$開催日<-as.POSIXlt(jra_data$開催日, format="%Y-%m-%d")
print(object.size(jra_data), units = "auto")
37.1 Mb

jra_data<-read.csv("jra_race_result.csv")
jra_data$開催日<-as.POSIXct(jra_data$開催日, format="%Y-%m-%d")
print(object.size(jra_data), units = "auto")
29 Mb

jra_data<-read.csv("jra_race_result.csv")
jra_data$開催日<-as.Date(jra_data$開催日)
print(object.size(jra_data), units = "auto")
29 Mb

POSIXlt 型で時刻を格納すると、他の型よりも27%ほどオブジェクトのサイズが大きくなりました。

Date 型:日付レベルの情報を保持する
Date 型は、名前の通り年・月・日までの情報を保持します。

競馬データの中の「開催日」については、年月日しか保持していないため、Date 型で充分です。なお、デフォルトで受け付ける書式は YYYY-mm-DD です。

jra_data$開催日 <- as.Date(jra_data$開催日)

# キタサンブラック号の前走からの出走間隔を算出する (2016ジャパンカップ時点)
kitasan_black_record <- subset(jra_data, 馬名 == "キタサンブラック")[,c("開催日", "レース名")]

kitasan_black_record[nrow(kitasan_black_record),]$開催日 -  kitasan_black_record[nrow(kitasan_black_record)-1,]$開催日
Time difference of 48 days # 前走から48日経過

このコンテンツで扱っている競馬データでは、年月日以上の細かさは含まれていないため、「開催日」列は Date 型として取り扱うことにします。

# あらためてデータを読み込み直しておく
jra_data <- read.csv("jra_race_result.csv")
jra_data$開催日 <- as.Date(jra_data$開催日)

時刻型データに関する詳細は、R-Source 16. 種々のベクトルなどを参考にしてください。

一通り、データが正常にオブジェクトに格納されていることを確認したら、そのデータに対してさまざまな統計関数を適用してみましょう。

主な基本統計関数

ここからようやく、「統計学」的な内容に入っていきます。が、このコンテンツでは、各種の指標(統計量)についての数学的な解説は極力行わず[8]、Rの関数の書式と、出力の解釈の仕方について説明していきます。

 合計: sum 関数

合計、ですね。説明するまでもないでしょう。Rでは sum 関数で算出できます。さっそく、競馬データに適用してみましょう。ここでは、武豊騎手が2015年に稼いだ賞金額の合計を算出してみます。「武豊騎手の」「2015年の」「賞金額」を抽出して合計する必要があるので、プログラムは以下のようになります。

# 武豊騎手の2015年の賞金額だけを取り出す
# 姓と名の間に半角スペースが入るので注意
yutaka_take_earn <- subset(jra_data,騎手 == "武 豊" & (開催日 >= "2015-01-01" & 開催日 <= "2015-12-31"))$賞金
head(yutaka_take_earn, n=10) # データの先頭部分を表示する
[1]  500  750    0   60  250  728    0 1050  200   75

# yutaka_take_earnオブジェクト内の値を合計する
sum(yutaka_take_earn)
[1] 218267.5 # 21億8267万5000円!

武豊騎手は、2015年に約22億円もの賞金を獲得していることがわかりました。実際には、この金額はレースの賞金で、騎手の取り分(進上金)は概ね5%とされているので、武豊騎手が実際に手にした(かもしれない)金額は、上記のプログラムを改変して、以下のように求められます。

sum(yutaka_take_earn) * 0.05
[1] 10913.38 # 1億913万3800円

それでも、年間1億円余りを獲得していることになります。武豊騎手の場合、このJRAでの賞金のほかに、海外や地方競馬でも数多くの大レースに勝利していますので、実際にはより多くの収入があるものと思われます。ちなみに、2015年は中央競馬でフェブラリーステークスをコパノリッキーで、香港カップをエイシンヒカリで制しています(海外レースは集計対象外)。コパノリッキーの父ゴールドアリュール、エイシンヒカリの父ディープインパクトともに、父仔2代ともに武豊騎手が騎乗しての勝利です。

武豊騎手とウオッカ号(先頭)。2009年毎日王冠にて[9]

武豊騎手とコパノリッキー号。2015年JBCクラシックにて

ちなみに、武豊騎手だけでなく、JRAで騎乗経験のある騎手の2015年の賞金獲得額をランキングにすると、以下のようになります。[10]

       騎手 獲得賞金額
       <fctr>      <dbl>
1  M.デムーロ   271891.0
2   戸崎 圭太   268105.0
3   岩田 康誠   254885.5
4   福永 祐一   228641.5
5     浜中 俊   225884.0
6  C.ルメール   219138.5
7       武 豊   218267.5
8   川田 将雅   217422.0
9   蛯名 正義   167057.0
10  田辺 裕信   151935.5

なお、地方競馬データでは、賞金額は1着から最後着まで、各レースの1着賞金額が入っているため、上記のような処理では獲得賞金額は算出できません。これは、地方競馬の場合、主催者によって2着以下の賞金の配分割合が異なり、データ収集時点では正確な金額が入れられないためです。実際に予測などを行う際には、着順に応じて配分するなど、適切な処理が必要になります。

ここでは、特に説明なく head 関数を使っています。head 関数は名前の通り、データの先頭部分を出力する関数です。n=行数 オプションを指定することで、先頭から任意の行数を出力できます。デフォルトは、中途半端ですが6行です。同様に、データの末尾部分を出力する tail 関数があります。大量のデータを扱う際にはよく使いますので、知っておくとよいでしょう。

 平均: mean 関数

平均も、日常生活で頻繁に使う統計量の1つです。ExcelではAVERAGE関数ですが、Rでは mean 関数です。AverageもMeanも、どちらも平均をあらわす英単語です。ここでは、各競馬場の芝コース、良馬場、1600m(マイル)の平均勝ち時計(タイム)を算出し、比較してみましょう。

※札幌、函館、福島、小倉競馬場は1600mの設定なし
nakayama_mile_time <- subset(jra_data, 競馬場 == "中山" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(nakayama_mile_time)
[1] 95.27968 # 1分35秒3

tokyo_mile_time <- subset(jra_data, 競馬場 == "東京" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(tokyo_mile_time)
[1] 94.88276 # 1分34秒9

kyoto_mile_time <- subset(jra_data, 競馬場 == "京都" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(kyoto_mile_time)
[1] 94.55189 # 1分34秒6

hanshin_mile_time <- subset(jra_data, 競馬場 == "阪神" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(hanshin_mile_time)
[1] 95.0708 # 1分35秒1

chukyo_mile_time <- subset(jra_data, 競馬場 == "中京" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(chukyo_mile_time)
[1] 95.85714 # 1分35秒9

niigata_mile_time <- subset(jra_data, 競馬場 == "新潟" & コース == "芝" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(niigata_mile_time)
[1] 95.1243 # 1分35秒1

競馬場によって、同じ距離、同じ馬場のレースでも勝ちタイムが違うことがわかります。京都競馬場が特に速く、中京競馬場が最も遅くなっています。もちろん、競馬場ごとに芝の品種や生育状況、天候、開催期間、出走馬のレベル(重賞が当該距離で設定されているか)などさまざまな要素が影響した結果ですが、「競馬場ごとに特性が異なる」ことを知ることは、競馬の予想・予測を行うための基礎知識といえるでしょう。

同じように地方競馬データでも競馬場ごとの平均勝ちタイムを算出してみましょう。

monbetsu_mile_time <- subset(nar_data, 競馬場 == "門別" & コース == "ダ" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(monbetsu_mile_time)
...中略...
kochi_mile_time <- subset(nar_data, 競馬場 == "高知" & コース == "ダ" & 距離 == 1600 & 着順 == 1 & 馬場状態 == "良")$タイム
mean(kochi_mile_time)

2013年限りで廃止された福山競馬場を含め、地方競馬では門別、盛岡、水沢、浦和、船橋、大井[11]、川崎、名古屋、笠松、福山、高知にマイルの設定があります。平均勝ちタイムをまとめると以下のようになります。

   競馬場 平均勝ち時計
   <fctr>        <dbl>
1    門別     105.2123
2    盛岡     100.8630
3    水沢     104.8538
4    浦和     103.2529
5    船橋     102.9424
6    大井     103.3807
7    川崎     104.1479
8  名古屋     104.5384
9    笠松     103.4904
10   福山     106.6357
11   高知     108.0500

どの競馬場でも、平均勝ち時計は1分40秒を超えています。芝とダートの違い、出走馬のレベルの違いもありますが、一般に地方競馬は馬に負担がかからないように砂が深く(=クッションが効く)なっており、そのぶんタイムは出にくくなっています。

ちなみに、具体的な方法は第5回(未公開)で取り上げています取り上げる予定ですが、中央競馬の上記条件における勝ちタイムの分布を可視化すると以下のようになります。グラフは、右の凡例をクリックすることで、データの表示・非表示が切り替えられます。また、任意の場所をダブルクリックすると、ズームします。これも、すべてR(とplotlyのオフライン版)で作成できます(参考)。


中央競馬における芝・1600m・良馬場の勝ち時計の競馬場別分布(棒の幅は0.2秒単位)

この結果からは、競馬場ごとに勝ち時計の分布のしかたが異なっていることが読み取れます。また、多くの競馬場で、1分34秒0から1分34秒5の範囲に勝ち時計が集中していることがわかります。しかし、先ほど算出した平均値では、より遅い1分34秒台後半から1分35秒台という値がほとんどでした。これは、何件かの極端に遅い勝ち時計に平均が引っ張られたことによります。データに含まれる、他の値と比べて極端に大きいあるいは小さい値[12]のことを外れ値といいます。実際に、(良馬場ではにわかに考え難い)勝ち時計が1分37秒5以上のレースを抽出すると、すべてが2歳戦でした。2歳馬は競走能力が未成熟であること、また「レースとは何か」を教育する[13]場合もあることから、特にペースが遅くなりやすく、結果として勝ち時計も遅くなる傾向にあります。

データを分析する際には、このような外れ値をあらかじめ除外する(例えば2歳戦とそれ以外を分けるなど)か、次に紹介する中央値を合わせて確認し、外れ値によって誤った判断をしないように気を付けましょう。

 中央値: median 関数

中央値は、平均と同じように“データの中心付近”をあらわす統計量で、メディアン(Median)とも呼ばれます。Rでは median 関数を使います。中央値は、「データを並べ替えたときにちょうど真ん中(中央)に来る値」と定義されます。平均と異なり、真ん中の値がそのまま使われます。なお、データの個数が偶数である場合(例えば100個)は、真ん中に近い2つの値の平均を中央値とします。

ここでも、先ほどと同様にJRAの各競馬場の芝コース、良馬場、1600m(マイル)の勝ち時計(タイム)の中央値を算出してみます。すでに、tokyo_mile_time など各競馬場ごとのタイムのベクトルは作成していますので、ここでは中央値の算出のみ行います。

# 中山競馬場の勝ち時計の中央値を算出
median(nakayama_mile_time)
[1] 95 # 1分35秒0

# 東京競馬場の勝ち時計の中央値を算出
median(tokyo_mile_time)
[1] 94.75 # 1分34秒8

# 京都競馬場の勝ち時計の中央値を算出
median(kyoto_mile_time)
[1] 94.5 # 1分34秒5

# 阪神競馬場の勝ち時計の中央値を算出
median(hanshin_mile_time)
[1] 94.9 # 1分34秒9

# 中京競馬場の勝ち時計の中央値を算出
median(chukyo_mile_time)
[1] 95.85 # 1分35秒9

# 新潟競馬場の勝ち時計の中央値を算出
median(niigata_mile_time)
[1] 95 # 1分35秒0

結果的には、平均と大きく変わらない値ですが、中山と阪神では、それぞれ0秒2程度タイムが速くなりました。同様に、地方競馬についても算出すると以下のようになります。

   競馬場 勝ち時計の中央値
   <fctr>            <dbl>
1    門別           105.30
2    盛岡           100.80
3    水沢           104.80
4    浦和           103.20
5    船橋           103.00
6    大井           103.40
7    川崎           104.20
8  名古屋           104.45
9    笠松           103.50
10   福山           106.50
11   高知           108.20

こちらも、平均と大きな差はありませんが、マイルGI「マイルチャンピオンシップ南部杯」が開催される盛岡競馬場や同じくGI「かしわ記念」が開催される船橋競馬場の中央値が速いタイムになっています。その他、笠松競馬場も他地区と比べて比較的速い傾向にあります。


左: 盛岡競馬場2016南部杯、中: 船橋競馬場2009かしわ記念、右: 笠松競馬場(2016年3月)

データ分析をする際には、平均値と中央値を比較し、大きな乖離があるようであれば、外れ値の存在を疑うような習慣を身につけるとよいでしょう。

 分散: var 関数

続いて、データのばらつきを示す統計量を紹介します。分散(Variance)は、標本(1つ1つのデータ)が平均からどれくらい散らばっているかをあらわす値です。分散が大きいほど、平均から大きく離れたデータが存在することになります。Rでは var 関数で分散を算出できます。

ここでは、レースの格(クラス、グレード)ごとに、1着馬のオッズのばらつきに違いがあるか見てみましょう。分散が大きいと、極端な大穴が出やすい条件であると言えます。筆者が収集しているデータでは、レースの条件(500万下、1000万下、1600万下、オープン)を取得していないので、ここでは賞金で大まかに分類することにします。なお、賞金区分については東京都馬主会のWebサイトを参考にしました。

# 新馬、未勝利クラス勝ち馬のオッズの分散を算出
make_debut_odds <- subset(jra_data, (grepl("未勝利", レース名) | grepl("新馬", レース名)) & !grepl("障害", レース名) & 着順 == 1)$オッズ
var(make_debut_odds)
[1] 441.4362

# 500万下クラス勝ち馬のオッズの分散を算出
u500_odds <- subset(jra_data, 賞金>=700 & 賞金<=980 & !grepl("新馬", レース名) & !grepl("障害", レース名) & 着順 == 1)$オッズ
var(u500_odds)
[1] 313.4884

# 1000万下クラス勝ち馬のオッズの分散を算出
u1000_odds <- subset(jra_data, 賞金 >= 990 & 賞金 <= 1480 & !grepl("障害", レース名) & 着順 == 1)$オッズ
var(u1000_odds)
[1] 230.7087

# 1600万下クラス勝ち馬のオッズの分散を算出
u1600_odds <- subset(jra_data, 賞金 >= 1740 & 賞金 <= 1790 & !grepl("障害", レース名) & 着順 == 1)$オッズ
var(u1600_odds)
[1] 624.9297

# オープンクラス勝ち馬のオッズの分散を算出
open_odds <- subset(jra_data, 賞金 > 1790 & !grepl("障害", レース名) & 着順 == 1)$オッズ
var(open_odds)
[1] 353.3355

この結果からは、最も馬券的に固いのは1000万下条件で、逆に最も荒れるのは1600万下条件であることがわかります。確実に当てに行くなら1000万下条件のレースに絞って、大穴を狙いに行くなら1600万下条件のレースを選ぶとよいでしょう。当たるかどうかは知りませんが。

ちなみに、個別のレースで見ると、最もオッズが高かったのは、2016年4月10日阪神3Rの3歳未勝利戦で、15番人気のタガノインペーロが勝って、単勝オッズは433.9倍(43390円)もつきました。2016年デビューの荻野極騎手の初勝利でもありました。

 標準偏差: sd 関数

標準偏差も、分散と同様データのばらつきをあらわす統計量です。分散の定義式と標準偏差の定義式を比べると、以下のようになります。統計量の定義式は、分野や書籍の執筆者によって、使用する文字などが異なりますが、ここでは、筆者の手元にあった統計検定3級テキストP. 74の式を採用しました。

分散
[res=320]\[s^{2} = \frac{1}{n} \sum_{i=1}^{n} (x_{i} - \bar{x})^{2}\]
標準偏差
[res=320]\[s = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (x_{i} - \bar{x})^{2} }\]

(ここで [res=180]$n$ はデータの件数、[res=180]$x$ はデータ1件1件の値、[res=180]$\bar{x}$ はデータの平均)

一見してわかるように、標準偏差は分散の平方根をとった値と定義されます。分散では、式に2乗するプロセスが含まれる[14]ため、値が大きくなりがちです。また、2乗すると元の単位(メートル、キログラムなど)も使えなくなります。そこで、標準偏差では分散の平方根をとることで数値の巨大化を抑え、また元の単位を使えるようにしています。

様々な統計手法の内部では分散が使われていることも多いですが、ばらつきをあらわす統計量としては、標準偏差のほうが一般的に[15]広く使われています。Rでは sd 関数で標準偏差を算出できます。先ほどの各条件ごとのオッズ分布を使い、標準偏差を算出してみましょう。

# 新馬・未勝利クラス勝ち馬のオッズの標準偏差を算出
sd(make_debut_odds)
[1] 21.01038
# 念のため、「分散の平方根が標準偏差」であることを確認する
sqrt(var(make_debut_odds)) # sqrt = Square Rootは平方根をとる関数
[1] 21.01038

# 500万下クラス勝ち馬のオッズの標準偏差を算出
sd(u500_odds)
[1] 17.7056

# 1000万下クラス勝ち馬のオッズの標準偏差を算出
sd(u1000_odds)
[1] 15.1891

# 1600万下クラス勝ち馬のオッズの標準偏差を算出
sd(u1600_odds)
[1] 24.99859

# オープンクラス勝ち馬のオッズの標準偏差を算出
sd(open_odds)
[1] 18.79722

当然ながら、分散と傾向は変わりませんが、標準偏差では数値に「倍」という単位をつけて見ることができます。

ここでは、分散と標準偏差を使い、クラスごとの1着馬のオッズのばらつきを示しました。別のアプローチとして、ヒストグラムで可視化してみます。グラフは、右の凡例をクリックすることで、データの表示・非表示が切り替えられます。また、任意の場所をダブルクリックすると、ズームします。


中央競馬におけるクラス別の1着馬オッズ分布(棒の幅は2倍単位)

可視化すると、また違った傾向が見えてきます。実際には各条件で、ほとんどの場合は勝ち馬のオッズは10倍以内であり、それを超えるような穴馬券はまずない、ということがわかります。[16]特に新馬・未勝利戦では全レース中、オッズ2倍以下の馬が勝つ割合が30%を超えており、グリグリ[17]の人気馬には逆らわないほうがよさそうです。ただし、“まずない”穴馬券が“まれにある”ために、全体の平均およびそこから導き出される分散、標準偏差が大きくなっています。

ちなみに、(標準偏差とまったく関係ありませんが)全レースを通じて、オッズ100倍(万馬券)以上の馬が勝つ確率は、約0.27%でした。

length(subset(jra_data, オッズ >= 100 & 着順 == 1)$オッズ)/length(subset(jra_data, オッズ >= 100)$オッズ)*100
[1] 0.2671394

また、オッズではなく人気で見た場合、1番人気馬の勝率は32%でした。特に、オッズ1倍台の1番人気になると、勝率は約49%まで上がります。[18]逆に、オッズ2倍台以上の場合、勝率が約28%まで下がります。

# 1番人気の勝率
length(subset(jra_data, 人気==1 & 着順 == 1)$オッズ) / length(subset(jra_data, 人気 == 1)$オッズ)*100
[1] 32.08166

# オッズ1倍台の1番人気の勝率
length(subset(jra_data, 人気 == 1 & 着順 == 1 & オッズ < 2)$オッズ) / length(subset(jra_data, 人気 == 1 & オッズ < 2)$オッズ)*100
[1] 49.42446

# オッズ2倍台以上の1番人気の勝率
length(subset(jra_data, 人気 == 1 & 着順 == 1 & オッズ >= 2)$オッズ) / length(subset(jra_data, 人気 == 1 & オッズ >= 2)$オッズ)*100
[1] 27.55121

このあたりの特徴を加味して、1番人気馬の取捨選択を検討するとよいでしょう。

 度数の集計: table 関数

さて、ここまでデータ全体の特徴を、平均や標準偏差といった1つの値で表現していましたが、ここではデータ全体である“階級”の値が何件あらわれたかを集計する方法を紹介します。いわゆる集計表です。

Rでは集計表を作成する方法がいくつかあります。その中で最も基本的なものに table 関数があります。table 関数の使い方はシンプルです。

table(データ)
(例)
table(subset(jra_data, 着順 == 1)$騎手) # 騎手の勝利数を集計
table(subset(jra_data, 着順 == 1)$馬名) # 競走馬の勝利数を集計
# 武豊騎手の競馬場別の勝率を集計する
# round関数で小数点以下2桁に丸める
round(table(subset(jra_data, 騎手 == "武 豊" & 着順 == 1)$競馬場) / table(subset(jra_data, 騎手 == "武 豊")$競馬場),2)

table 関数の返り値は、sort 関数などで並べ替えることができます。

sort(table(...)) # 昇順
... 
      C.ルメール        蛯名 正義        内田 博幸            武 豊
             324              339              344              355
         浜中 俊        岩田 康誠        川田 将雅        福永 祐一
             404              413              437              475
       戸崎 圭太
             558

sort(table(...), decreasing=TRUE) # 降順
ゴールドアクター ストレイトガール ニホンピロバロン   エイシンヒカリ
               9                9                9                8
キタサンブラック       サナシオン スマートレイアー     ディサイファ
               8                8                8                8
  ビッグアーサー   ミッキーアイル
               8                8
...

sort(round(table(...)), decreasing=TRUE)
函館 京都 小倉 中京 阪神 中山 東京 札幌 新潟 福島
0.25 0.15 0.14 0.13 0.12 0.10 0.10 0.05 0.00 0.00

とすることで、度数の昇順または降順で並べ替えられます。2013年から2016年11月までの約4年間で最も勝った騎手は、大井競馬から移籍した戸崎圭太騎手でした。また、最も多く勝利した馬は期間内に9勝しており、その中には、殷々たる名馬に並び、2015年11月の障害デビューから1年間、8戦6勝2着2回と大活躍しているニホンピロバロン号もいます。[19]また、武豊騎手は(騎乗数は少ないですが)函館競馬場、京都競馬場[20]、小倉競馬場などを得意としています。逆に、新潟競馬場、札幌競馬場は少し苦手としているようです。武豊騎手は、夏競馬のシーズンは小倉競馬場を主戦場にしていますので、新潟や札幌へは、どうしてもという遠征で騎乗することが多いと思われますが、その際の取捨選択には注意が必要かもしれません。

戸崎圭太騎手。2012年東京大賞典後のフリオーソ・ボンネビルレコード引退式にて


table 関数では度数が集計できますが、割合を集計するには prop.table(Probability)関数を使います。

# 性別の勝率を集計する
sort(prop.table(table(subset(jra_data,着順==1)$性別)))
        セ         牝         牡
0.02936378 0.32945277 0.64118345

# 戸崎圭太騎手の勝ち鞍の競馬場別の割合を集計する
sort(round(prop.table(table(subset(jra_data, 騎手 == "戸崎 圭太" & 着順 == 1)$競馬場)),2))
小倉 札幌 函館 中京 京都 阪神 福島 新潟 中山 東京
0.00 0.01 0.01 0.03 0.04 0.06 0.07 0.13 0.30 0.36

関数が入れ子になって複雑に見えるかもしれませんが、table 関数の集計結果を prop.table 関数に渡すことで、割合に変換しています。全レースをトータルすると、牡馬と牝馬の勝率はほぼ2:1になっています。また、戸崎圭太騎手は、勝利数の3分の2が東京、中山で占められており、新潟、福島も合わせると86%が東日本エリアでの勝利です。

 最頻値: which.max(table(...)) 関数

最頻値は、データの中で最も多く出現する値のことです。その性質上、同一の値が出現する可能性が低いオッズやタイムなどの連続変数にはあまり適しませんが、枠番や斤量など、カテゴリカルあるいはそれに近い離散値などから算出できます。Rの標準関数には、最頻値を算出する関数はないので、上述の table 関数と引数に与えたベクトル(状のオブジェクト)の最大値の位置(要素番号)を返す which.max 関数を組み合わせて算出します。

# 中山・芝・1200mで最も勝利数の多い枠番を抽出
which.max(table(subset(jra_data, 競馬場 == "中山" & コース == "芝" & 距離 == 1200 & 着順 == 1)$枠番))
6
6

# 浦和1600mで最も勝利数の多い馬番を抽出
# あらかじめ地方競馬データを読み込んでおく必要がある
which.max(table(subset(nar_data, 競馬場 == "浦和" & 距離 == 1600 & 着順 == 1)$馬番))
1 
1

中山・芝・1200mの条件では、GIスプリンターズステークスなどがおこなわれます。スタートしてすぐ3コーナーに入るため、一般に外枠不利と言われています。では内枠が有利かというと、最頻値は6枠(出走頭数によりますが、おおむね馬番で8番から12番前後)でした。内枠は、外から続々と他馬が寄ってくるため、進路がなくなるリスクがあります。そのような相互関係から、ちょうど中ほどの6枠がスムーズにレースを運び、勝利するケースが多いのかもしれません。

一方、地方競馬の浦和競馬場・1600mの条件でも最頻値を算出すると、1枠が最も勝利数が多いという結果になりました。実は、浦和競馬場の1600mは、「3コーナーと4コーナーの中間地点で、しかもコース幅員が狭い」というとんでもない場所がスタート地点になっています。[21]そのため、内枠から先行しない限り勝利のチャンスはない、とまで言われるような極端な条件になっています。その結果、馬の実力も脚質[22]も関係なく、とにかく内枠を買う、というのが攻略法になっており、データもそれを裏付けています。


浦和競馬場(2010年4月)。左画像の赤丸のあたりが1600mのスタート地点

 相関係数: cor 関数

ここまで、オッズや着順など、1変量のデータについてさまざまな統計量を算出する方法を紹介しましたが、ここでは2変量以上のデータ間の関係について知るための相関係数を算出します。

相関係数は、-1から1の間の値を取る統計量です。相関係数が1に近ければ近いほど、「一方のデータが増加・上昇すると、もう一方のデータも増加・上昇しやすい」傾向があります。逆に、-1に近いほど、「一方のデータが増加・上昇すると、もう一方のデータは減少・下降しやすい」傾向があります。

相関係数の捉え方についてのイメージ


ここでは「弱い」「中程度」「強い」という表現を使っていますが、相関係数がどの値になれば中程度なのか、強いのか、ということに明確な基準はありません。しかし、概ね“0〜0.3:無相関・弱い相関”“0.4〜0.6:中程度の相関”“0.7〜1:強い相関”といったくらいで捉えることが多いでしょう。

Rで相関係数を算出するには、cor 関数を使います。

cor(オブジェクト1, オブジェクト2)

または

cor(オブジェクト)

2つのベクトルの間の相関係数を算出する場合などは、対象のオブジェクトをカンマで区切って指定します。一方、データフレームや行列など、複数列の(数値だけからなる)オブジェクトの中での相関係数を算出する場合は、対象となるデータフレームなどをそのまま指定できます。

ここでは、中山・芝・1200mにおける、走破タイムと賞金の間の相関係数を算出してみましょう。基本的には、走破タイムが速い馬ほど、格の高いレース(重賞)で好走しているはずなので、この関係は正の相関があるものと想像されます。

nakayama_1200_time_prize <- subset(jra_data, 競馬場 == "中山" & コース == "芝" & 距離 == 1200)[,c("タイム", "賞金")]
cor(nakayama_1200_time_prize)
           タイム       賞金
タイム  1.0000000 -0.2805736
賞金   -0.2805736  1.0000000

結果は、相関係数が-0.28でした。これは、「タイムが速くなると獲得賞金が多くなる」という、上記の仮説を裏付けるものです。しかし、相関係数自体がそれほど大きなものではなく、「弱い相関」程度です。これは、ペースや馬場状態などにより、必ずしもレースの格が上がればタイムも速くなるものではないことを意味します。

なお、着順や人気など、少なくとも一方が「順位データ」(1位、2位...と続き、間の値はない)である場合の相関については、cor 関数をそのまま使ってもエラーは出ませんが、統計学的により正確な方法として、順位相関係数を使うほうがよいでしょう[23]。順位相関係数は、cor 関数に method = "spearman" とオプションを指定すれば算出できます[24]。これは、Spearmanの順位相関係数を算出するものです。ここでは、中山・芝・1200mにおける(単勝)人気と着順(いずれも順位データ)の間の順位相関係数を算出してみましょう。

nakayama_1200_popularity_result <- subset(jra_data, 競馬場 == "中山" & コース == "芝" & 距離 == 1200)[,c("人気", "着順")]
cor(nakayama_1200_popularity_result, method = "spearman")
          人気      着順
人気 1.0000000 0.5179042
着順 0.5179042 1.0000000

この結果からは、人気がそのまま着順になることはない、ということがわかります(当たり前ですが)。

なお、ここでは2変量の場合の相関係数を例示しましたが、データフレームの列数が3列以上である場合も、同様にオブジェクト名を指定すれば上記の相関行列が3x3、4x4と拡張されて出力されます。

まとめ

この記事では、競馬データに対して、Rの基本統計関数を適用し、データの特性を把握することを紹介してきました。他にも、統計学の領域では尖度、歪度、範囲、四分位数などさまざまな統計量が使われますが、いずれもRの関数で容易に算出できます。

実際に競馬の予想(予測)をおこなうには、より高度な統計手法を駆使しなければならないですが、集めたデータにいきなり難しい手法を適用して、結果だけ見るというのは、データ分析において最も失敗しやすいパターンの1つです。高度な分析が先にあったとしても、まずは基本的な統計量を算出し、データの特徴を丹念に見ていくことで、分析の方向性が明確になったり、データの不備に気づくことができます。[25]

分析の品質を高めるためにも、まずは基本的な統計量のチェックから始める、という習慣を身につけましょう。


注釈

  • [1]Windowsのバージョンによって呼び方は異なるかもしれません。
  • [2]あるいは "\\" と2つ重ねます。
  • [3]本来は、コマンドでデータの先頭部分を確認するなどして、ファイル形式を特定しないといけないですが。
  • [4]UNIX / Linuxユーザにはややこしいですが、dirがディレクトリ内の一覧、lsがオブジェクトの一覧です。
  • [5]サラブレッドにもブチコ、マーブルケーキきょうだいなど、白毛にブチ模様がある馬もいますが、JRAの登録上は白毛になっています。
  • [6]競走馬のデビュー戦のこと。最近(といっても2008年からですが)は「メイクデビュー」とも言います。
  • [7]本来、データ収集プログラムの段階で対処しておけばよいのですが。
  • [8]だってできないので。
  • [9]このあと、後ろのカンパニーに交わされてしまいますが。
  • [10]JRAホームページのデータ http://jra.jp/datafile/leading/j2015.html と微妙に金額が異なりますが、JRAでは「付加賞」という賞金を加算しているのに対し、データ収集元のYahoo!スポーツナビでは本賞金のみが記載されているためです。
  • [11]大井には内周り、外周りの2つのコース設定がありますが、筆者が収集しているデータでは両者を同一としています。
  • [12]何をもって極端に大きい、小さいと判定するかは難しいですが。
  • [13]何も考えずにただ突っ走る競馬を学習してしまうと、後々レースレベルが上がった時に対応できなくなるため、馬群の中で折り合い、騎手の指示に従ってスパートする、ということを覚えさせるような騎乗が、2歳戦では多くあります。
  • [14]分散では平均からの個別の値の距離を算出するため、合計がマイナスになることも考えられます。ただ、「負のばらつき」というのは概念として理解しづらいため、2乗することで強制的に正の値にしています。
  • [15]これも工学系、理学系、ビジネス系などいろいろあって、何が一般的か、というのも明確ではありませんが。
  • [16]だから穴なんですが。
  • [17]競馬新聞で本命印◎がたくさんついていることをグリグリと言います。
  • [18]実際には、オッズ1倍台で1番人気ではないケースが存在しないので、実質オッズ1倍台の勝率です。
  • [19]惜しくも先日、屈腱炎を発症し長期休養することになりました。ちなみに本馬は、4代母ニホンピロアスターから一貫して「ニホンピロ」の冠で所有されている、ニホンピロ5代目という、競馬のロマンを感じる血統です。
  • [20]キャリア全体でも、京都競馬場だけで1000勝しているなど、非常に得意としています。
  • [21]フルゲートも11頭と、地方競馬の中でも特に少ない設定になっています。
  • [22]逃げ、先行、差し、追い込みなど。他にマクりや自在といった表現もあります。
  • [23]一方が連続値、もう一方が離散値の場合、連関係数を使ったほうがよいのかもしれませんが。
  • [24]実際にはもうひとつ、Kendallの方法(method = "kendall")も指定できますが、あまり一般的ではありません。
  • [25]実際、筆者もこの記事を書く中で、「なんでnetkeiba.comで通算9勝なのに、手元の集計結果では10勝してるんだ?」とデータを見返し、重複行の存在に気づきました。まぁデータを公開する前にチェックしろという話ですが。