TECH MEMO

技術的な内容に関する個人的なメモ

カーリングで後攻をとった場合の勝率への影響を傾向スコアを用いて算出する

前回の投稿からだいぶ経ってしまいましたが、もうすぐ平昌オリンピックですので、また世界選手権のデータを少し分析してみました。

前回の投稿では、ベイズ推論によるアプローチで、先攻の場合と後攻の場合で勝率に差が存在するのかを確かめてみました。

ただ、勝率には先攻・後攻以外に、自チームおよび対戦チームの強さが大きく影響している(はず)です。そのため、単純に先攻の場合と後攻の場合の勝率を集計しただけでは、それが後攻をとったことによる効果を表しているとは言えません。

そこで、傾向スコアによる共変量(チームの強さ)調整を行って、後攻をとった場合に、先攻の時に比べてどのくらい勝率が上がるかを調べてみます。
(傾向スコアを使ってみたかっただけなので、色々と設定が甘いところがありますが、その辺はご容赦を。。。)

なお、今回も前回と同様に、2002年〜2016年までの15年間の男女世界選手権のデータが、変数result_score_df(pandasのDataFrame型)に以下のような形式でデータが格納されている前提で進めます。

f:id:curlyst:20170729184315p:plain

1. チームの強さって・・・

 共変量調整を行おうと思って困ったのが、「チームの強さをどう定義するか」ってところ。世界ランキングとか使えば良いのかもしれないですが、データを持っていない。。。

そこで、今回は各大会のラウンドロビン(予選の全チームでのリーグ戦)での勝率を、その大会での各チームの強さとしました。

round_robin_result_df = result_score_df[result_score_df["draw"].str.contains("Draw")]

# 勝ち数
win_game_num_df = round_robin_result_df[round_robin_result_df["result"]==1].groupby(["year", "gender", "team"], as_index=False)["draw"].count()
win_game_num_df.rename(columns={"draw":"win"}, inplace=True)
# 試合数
game_num_df = round_robin_result_df.groupby(["year", "gender", "team"], as_index=False)["draw"].count()
game_num_df.rename(columns={"draw":"total"}, inplace=True)

team_df = round_robin_result_df[["year", "gender", "team"]].drop_duplicates()
team_df = pd.merge(left=team_df, right=game_num_df, on=["year", "gender", "team"], how="left")
team_df = pd.merge(left=team_df, right=win_game_num_df, on=["year", "gender", "team"], how="left")

# データがない場合は0
team_df.fillna(value=0, inplace=True)

# チームの強さ(予選での勝率)
team_df["strength"] = team_df["win"] / team_df["total"]

# 不要なカラムは除く
team_df = team_df[["year", "gender", "team", "strength"]]

team_df

実行結果(一部抜粋)

f:id:curlyst:20180102100842p:plain

なんとなく強さを表しているっぽくなりましたかね。。。

とりあえず強さを求めたところで、一旦、データ形式を変換しておきました。

# 先攻・後攻がわからないゲームは除く
print("--- data size ---")
print("data size: (%d, %d)" % result_score_df.shape)
filtered_result_score_df = result_score_df[result_score_df["hammer"] != -1]
print("data size(filetered): (%d, %d)" % filtered_result_score_df.shape)

# 試合毎に変換
hammer_team_result_score_df = filtered_result_score_df[filtered_result_score_df["hammer"]==1][["year", "gender", "game_ID", "team", "result"]]
hammer_team_result_score_df = pd.merge(left=hammer_team_result_score_df, right=team_df, on=["year", "gender", "team"])
hammer_team_result_score_df.rename(columns={"team":"hammer_team", "result":"hammer_result", "strength":"hammer_strength"}, inplace=True)
not_hammer_team_result_score_df = filtered_result_score_df[filtered_result_score_df["hammer"]==0][["year", "gender", "game_ID", "team", "result"]]
not_hammer_team_result_score_df = pd.merge(left=not_hammer_team_result_score_df, right=team_df, on=["year", "gender", "team"])
not_hammer_team_result_score_df.rename(columns={"team":"not_hammer_team", "result":"not_hammer_result", "strength":"not_hammer_strength"}, inplace=True)

game_result_df = pd.merge(left=hammer_team_result_score_df, right=not_hammer_team_result_score_df, on=["year", "gender", "game_ID"])
game_result_df

実行結果(一部抜粋)

f:id:curlyst:20180102101317p:plain

2. 先行/後攻別の勝率(復習)

まずは復習として、そもそも先攻と後攻の勝率がどうだったかを集計しておきます。

# 全試合数
game_num = game_result_df.shape[0]

# 勝ち試合数:先攻チーム
not_hammer_win_game_num = game_result_df[game_result_df["not_hammer_result"]==1].shape[0]
# 勝ち試合数:後攻チーム
hammer_win_game_num = game_result_df[game_result_df["hammer_result"]==1].shape[0]

not_hammer_winning_percentage = not_hammer_win_game_num / game_num
hammer_winning_percentage = hammer_win_game_num / game_num
print(u"先攻チーム勝率: %f" % (not_hammer_winning_percentage))
print(u"後攻チーム勝率: %f" % (hammer_winning_percentage))
print(u"差分: %f" % (hammer_winning_percentage - not_hammer_winning_percentage))

実行結果

先攻チーム勝率: 0.431330
後攻チーム勝率: 0.568670
差分: 0.137339

単純に集計しただけだと、後攻をとった場合、先攻の時に比べて約14%勝率が上がることになります。

3. 傾向スコアの前に層別解析を実施

 いきなり傾向スコアを使って共変量調整をする前に、共変量が2変数しかないので、自チームの強さと対戦チームの強さを使って、層別解析を実施しました。

まず分析しやすいようにデータ形式を変換します。

column_list = ["result", "team", "strength", "opponent_team", "opponent_strength", "hammer"]
# 先攻チーム観点での整理
not_hammer_df = game_result_df[["not_hammer_result", "not_hammer_team", "not_hammer_strength", "hammer_team", "hammer_strength"]].copy()
not_hammer_df["hammer"] = [0] * not_hammer_df.shape[0]
not_hammer_df.columns = column_list
# 後攻チーム観点での整理
hammer_df = game_result_df[["hammer_result", "hammer_team", "hammer_strength", "not_hammer_team", "not_hammer_strength"]].copy()
hammer_df["hammer"] = [1] * not_hammer_df.shape[0]
hammer_df.columns = column_list

calc_target_df = pd.concat([not_hammer_df, hammer_df])
calc_target_df["result"] = calc_target_df["result"].apply(lambda x: 0 if x == -1 else 1)
calc_target_df

実行結果

f:id:curlyst:20180102105008p:plain

 各変数とも0.2刻みで、層毎に後攻の場合と先攻の場合の勝率(の平均値)の差を算出し、最後にそれらの平均値を後攻をとった場合の平均的な効果として算出しました。

# 0.2刻み(各変数を5分割)
interval1 = np.arange(0, 1.2, 0.2)
interval2 = np.arange(0, 1.2, 0.2)

match_list = []
for i1 in range(0, len(interval1)-1):
    for i2 in range(0, len(interval2)-1):
        # 先攻
        not_hammer_df = calc_target_df[(calc_target_df["hammer"]==0)
                                        & (interval1[i1] <= calc_target_df["strength"])
                                        & (calc_target_df["strength"] < interval1[i1+1])
                                        & (interval2[i2] <= calc_target_df["opponent_strength"])
                                        & (calc_target_df["opponent_strength"] < interval2[i2+1])]
        # 後攻
        hammer_df = calc_target_df[(calc_target_df["hammer"]==1)
                                    & (interval1[i1] <= calc_target_df["strength"])
                                    & (calc_target_df["strength"] < interval1[i1+1])
                                    & (interval2[i2] <= calc_target_df["opponent_strength"])
                                    & (calc_target_df["opponent_strength"] < interval2[i2+1])]
        # 先攻・後攻ともデータがある場合
        if (not_hammer_df.shape[0] > 0) and (hammer_df.shape[0] > 0):
            # 介入効果を算出
            match_list.append(hammer_df['result'].mean() - not_hammer_df['result'].mean())

print("後攻をとった場合に勝率に与える効果:")
print(np.mean(match_list))

実行結果

後攻をとった場合に勝率に与える効果:
0.0647783026335

単純に集計した時は14%くらいだったのが、半分以下になりました。

4. 傾向スコアの算出

 それでは今回の目的である傾向スコアを算出します。今回はロジスティック回帰を用い、共変量(自チームの強さ、対戦チームの強さ)から後攻を取る確率を算出します。

# 傾向スコアの算出
y = calc_target_df["hammer"]
X = calc_target_df[["strength", "opponent_strength"]]
lr_model = sm.Logit(y, X)
lr_result = lr_model.fit()
lr_result.summary()

実行結果

f:id:curlyst:20180102230716p:plain

モデルの精度としては、かなりイマイチですね。。。

5. 傾向スコアによる層別解析

 「3. 層別解析」では自チームの強さ、対戦チームの強さの2変数を用いて、層別解析を実施しましたが、同様の分析を傾向スコアの1変数のみを用いて実施します。

ここでは傾向スコアを0.05刻みで分割し、各分割において先攻の場合の勝率と後攻の場合の勝率の差分を計算して、それらの平均値を算出することで、後攻をとった場合の効果を算出しました。

y = calc_target_df["result"]
z1 = calc_target_df["hammer"]
ps = calc_target_df["PS"]
table = pd.concat([ps, z1, y], axis=1)

# 0.05刻み
interval = np.arange(0, 1.05, 0.05)

match_list = []
for i in range(0, len(interval)-1):
    # 先攻(非介入群)
    not_hammer_table = table[(table['hammer']==0) & (interval[i] <= table['PS']) & (table['PS'] < interval[i+1])]
    # 後攻(介入群)
    hammer_table = table[(table['hammer']==1) & (interval[i] <= table['PS']) & (table['PS'] < interval[i+1])]
    
    # 先攻・後攻ともデータがある場合
    if (len(not_hammer_table) > 0) & (len(hammer_table) > 0):
        # 介入効果を算出
        match_list.append(hammer_table['result'].mean() - not_hammer_table['result'].mean())

print("後攻をとった場合に勝率に与える効果:")
print(np.mean(match_list))

実行結果

後攻をとった場合に勝率に与える効果:
0.0433670012741

約4%と層別解析で算出した効果より小さくなってしまいました。

6. IPW定量の算出

 傾向スコアを用いた共変量調整のもう一つの方法として、IPW定量による効果の推定を行いました。

y = calc_target_df["result"]
z1 = calc_target_df["hammer"]
ps = calc_target_df["PS"]

ipwe1 = sum((z1 * y) / ps) / sum(z1 / ps)
ipwe0 = sum(((1 - z1) * y) / (1 - ps)) / sum((1 - z1) / (1 - ps))
print("後攻をとった場合に勝率に与える効果:")
print(ipwe1 - ipwe0)

実行結果

後攻をとった場合に勝率に与える効果:
0.0785840303549

約8%と、通常の層別解析や、傾向スコアによる層別解析に比べて大きくなりました。

7. まとめ

 今回は後攻をとった場合の効果の算出に「層別解析」「傾向スコアによる層別解析」「IPW定量」の3パターン実施しましたが、いずれも数%の効果(勝率が上がる)となり、単純に集計した場合(約14%)よりも小さくなる結果となりました。

ただ、今回は傾向スコアを使ってみたかっただけなので、問題設定(共変量の選択)をかなり単純にしてしまっているため、傾向スコアを算出したモデルの精度もかなり怪しいものでした。なので、ちゃんと効果を算出するには、傾向スコアの前に問題設定をきちんと検討する必要がありそうですね。

PyMCによるベイズ推論 〜世界カーリング選手権における先行/後攻別の勝率の分析〜

前回の記事で世界カーリング選手権における先行/後攻別の勝率を算出し、勝率の差が誤差ではないか確かめるために、検定(二項検定、χ2検定)を実施しました。

上記に対して、今回はより直感的な理解ができるベイズ推論によるアプローチで、勝率に差が存在するのか確かめていきたいと思います。
今回も対象はいつもと同様に2002年〜2016年までの15年間の男女世界選手権のデータです。
変数result_score_df(pandasのDataFrame型)に以下のような形式でデータが格納されている前提で進めます。

f:id:curlyst:20170729184315p:plain

なお、今回の内容を実施するにあたって、以下の書籍を参考にしました。

Pythonで体験するベイズ推論 PyMCによるMCMC入門

Pythonで体験するベイズ推論 PyMCによるMCMC入門

  • 作者: キャメロンデビッドソン=ピロン,玉木徹
  • 出版社/メーカー: 森北出版
  • 発売日: 2017/04/06
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る

1. 先行/後攻別の勝率

まずはデータ全体における先行/後攻での勝率の差ですが、前回記事では二項検定を実施して、後攻の勝率が二項分布(p=0.5)から有意に偏っているか確かめました。その結果、偏りが存在し、後攻の勝率はp=0.5とは異なる(先行と後攻で勝率は異なる)となりました。

上記についてのベイズ推論によるアプローチでは、まず後攻の勝率に関する事前分布をモデリングします。勝率については何も確信がないので、ここでは一様分布に従うと仮定します。

# 後攻(hammer)チームの勝率(事前分布)
p_hammer = pm.Uniform('p_hammer', lower=0, upper=1)

次に実際の勝ち負けのデータ(観測データ)をモデルに組み込みます。試合の勝ち負けに関する確率変数には、二値変数についての確率分布であるベルヌーイ分布*1を用います。

# 観測データ
hammer_result_list = result_score_df[result_score_df["hammer"] == 1]["result"].tolist()
hammer_result_list = [True if x == 1 else False for x in hammer_result_list]

# 観測データを設定
obs = pm.Bernoulli("obs", p_hammer, value=hammer_result_list, observed=True)

これでモデルの設定は完了したので、以下を実行し、MCMC*2により勝率に関する事後分布から、データをサンプリングします。ここではサンプリングを10000回、バーンイン期間を1000としました。

mcmc = pm.MCMC([p_hammer, obs])
mcmc.sample(10000, 1000)

実行結果

[-----------------100%-----------------] 10000 of 10000 complete in 1.1 sec

サンプリングされた勝率のヒストグラムを生成した結果を以下に示します。赤色の縦の点線は0.5(勝率5割)を示しており、大部分のデータが勝率0.5以上に分布している(先行に比べて後攻の方が勝率が良い)ことがわかります。

f:id:curlyst:20170730200443p:plain

次に勝率がx割を上回る確率がどの程度かを算出してみます。

# 5割5分を超える確率
print("p > 0.55: %.2f" % (mcmc.trace("p_hammer")[:] - 0.55 >= 0).mean())
# 5割6分を超える確率
print("p > 0.56: %.2f" % (mcmc.trace("p_hammer")[:] - 0.56 >= 0).mean())
# 5割7分を超える確率
print("p > 0.57: %.2f" % (mcmc.trace("p_hammer")[:] - 0.57 >= 0).mean())
# 5割8分を超える確率
print("p > 0.58: %.2f" % (mcmc.trace("p_hammer")[:] - 0.58 >= 0).mean())

実行結果

p > 0.55: 0.94
p > 0.56: 0.77
p > 0.57: 0.43
p > 0.58: 0.16

後攻チームが勝率5割5分以上になる確率が94%となり、先行チームに比べて後攻チームが有利であることに対する確信度はかなり高いと考えられます。

2. 日本、カナダにおける先行/後攻別の勝率

それでは次に国別の先行/後攻における勝率について、日本とカナダを対象に調べていきます。前回の記事では、χ2検定を実施した結果、日本は先行/後攻が勝敗と関連はしておらず、カナダは関連しているという結果になりました。

ベイズ推論によるアプローチでは、先行での勝率、後攻での勝率、後攻での勝率ー後攻での勝率、の3つについて事後分布を求めます。先ほどと同様に、まず各勝率を一様分布によってモデリングします。

#--- 日本 ---
p_hammer_jp = pm.Uniform('p_hammer_jp', lower=0, upper=1)
p_not_hammer_jp = pm.Uniform('p_not_hammer_jp', lower=0, upper=1)

#--- カナダ ---
p_hammer_ca = pm.Uniform('p_hammer_ca', lower=0, upper=1)
p_not_hammer_ca = pm.Uniform('p_not_hammer_ca', lower=0, upper=1)

次に実際の勝敗データをモデルに組み込みます。こちらも先ほどと同様にベルヌーイ分布を用います。

# 日本
result_score_jp_df = result_score_df[result_score_df["team"] == "Japan"]
hammer_result_jp_list = result_score_jp_df[result_score_jp_df["hammer"] == 1]["result"].tolist()
hammer_result_jp_list = [True if x == 1 else False for x in hammer_result_jp_list]
not_hammer_result_jp_list = result_score_jp_df[result_score_jp_df["hammer"] == 0]["result"].tolist()
not_hammer_result_jp_list = [True if x == 1 else False for x in not_hammer_result_jp_list]

# カナダ
result_score_ca_df = result_score_df[result_score_df["team"] == "Canada"]
hammer_result_ca_list = result_score_ca_df[result_score_ca_df["hammer"] == 1]["result"].tolist()
hammer_result_ca_list = [True if x == 1 else False for x in hammer_result_ca_list]
not_hammer_result_ca_list = result_score_ca_df[result_score_ca_df["hammer"] == 0]["result"].tolist()
not_hammer_result_ca_list = [True if x == 1 else False for x in not_hammer_result_ca_list]

国別の勝敗に関するベイズ推論によるアプローチでは、先行/後攻の勝率だけでなく、それらの差分(後攻の勝率ー先行の勝率)の事後分布も推定するため、差分に関する確率変数を定義します。

# 日本
@pm.deterministic
def p_diff_jp(p_hammer_jp=p_hammer_jp, p_not_hammer_jp=p_not_hammer_jp):
    return p_hammer_jp - p_not_hammer_jp

# カナダ
@pm.deterministic
def p_diff_ca(p_hammer_ca=p_hammer_ca, p_not_hammer_ca=p_not_hammer_ca):
    return p_hammer_ca - p_not_hammer_ca

最後にMCMCにより事後分布からデータをサンプリングします。

# 日本
mcmc_jp = pm.MCMC([p_hammer_jp, p_not_hammer_jp, obs_hammer_jp, obs_not_hammer_jp, p_diff_jp])
mcmc_jp.sample(10000, 1000)

# カナダ
mcmc_ca = pm.MCMC([p_hammer_ca, p_not_hammer_ca, obs_hammer_ca, obs_not_hammer_ca, p_diff_ca])
mcmc_ca.sample(10000, 1000)

それでは日本、カナダそれぞれの勝率に関する事後分布を見ていきます。

まず日本ですが、後攻の勝率、先行の勝率、後攻と先行の差分、のそれぞれに関するヒストグラムを作成すると以下のようになります。先行と後攻で分布にあまり違いがなく、分布の範囲も重複している部分が多いため、差分についても0の周辺に分布した形になっています。先行の方が勝率が良いケース(差分が負)も多く存在し、日本は先行と後攻で差があまりないことが分かります。

f:id:curlyst:20170804164806p:plain

次にカナダにですが。カナダの勝率に関するヒストグラムを作成すると、以下のように後攻の勝率と先行の勝率の分布の範囲がずれており、後攻の方が0.1ほど大きくなっています。そのため、差分についても大半が0より大きい部分で分布しており、後攻の勝率の方が良い可能性が高いことを示しています。

f:id:curlyst:20170804165822p:plain

3. まとめ

今回は前回と同じデータを対象に、前回は検定によって分析していたところを、ベイズ推論によるアプローチで分析してみました。分析結果(結論)についてはどちらの方法でも変わらないですが、ベイズ推論によるアプローチでは、勝率の分布が実際にグラフとして可視化できるため、直感的に分かりやすいのではないかと思います。

ただ、難点として、ベイズ統計を知らない人にとっては「勝率の分布」とか言われても、中々理解するのは厳しいですかね...?

世界カーリング選手権データの可視化②(pandasによるデータ集計・可視化)

今回も世界選手権のデータを集計・可視化したいと思います。今回は各試合での先攻・後攻や、各エンドでの得点によって勝敗がどう変わってくるかを見ていきます。

対象は前回の記事と同様に2002年〜2016年までの15年間の男女世界選手権のデータです。なので、データの読み込みについては前回の記事を参照してください。

1. 先攻と後攻で勝率は違うのか?

カーリングでは最後にストーンを投げる後攻が有利です。そのため試合前に実施するLSD*1において勝ったチームは、多くの場合、後攻を選択します。では、どのくらい勝率が変わってくるかを見てましょう。

まずデータの中で先攻・後攻が不明な試合は今回の集計対象から除外します。

# 先攻・後攻がわからないゲームは除く
filtered_result_score_pd = result_score_pd[result_score_pd["hammer"] != -1]

まずは全試合を対象に、先攻・後攻チームそれぞれの勝敗数を集計し、勝率を計算します。

# 勝敗数のカウント
cross_table = filtered_result_score_pd.pivot_table(index="hammer", columns="result", values="draw", aggfunc="count")
cross_table["winning_percentage"] = cross_table[1] / (cross_table[-1] + cross_table[1])
print "全試合:"
print cross_table

実行結果:

全試合:
result    -1     1  winning_percentage
hammer                                
0       1060   804             0.43133
1        804  1060             0.56867

resultの"-1"は負け、"1"は勝ちを表し、hammerの"0"は先攻、"1"は後攻を表しています。
先攻の勝率は43%、後攻の勝率は57%と、勝率を比較すると10%程度の差があります。

なんとなく先攻と後攻で勝率に差があると言えそうがですが、統計的にも差があると言えるのか(誤差の範囲ではないか)確かめるために二項検定を行ってみます。

二項検定についてWikipedia*2から引用すると、

2つのカテゴリに分類されたデータの比率が、理論的に期待される分布から有意に偏っているかどうかを、二項分布を利用して調べる統計学的検定

です。ここでは、2つのカテゴリは"先攻"と"後攻"、期待される分布は二項分布(p=0.5)となります。

今回実施する二項検定では、帰無仮説、対立仮説は以下のようになります。

 帰無仮説:先行と後攻で勝率は変わらない(先攻、後攻ともに勝率は50%)
 対立仮説:先攻と後攻で勝率は異なる

それでは実際に二項検定を実行してみましょう。

# 二項検定
p_value = scipy.stats.binom_test([cross_table.ix[1, -1], cross_table.ix[1, 1]], p=0.5)
print "\n二項検定(帰無仮説:先行と後攻で勝率は変わらない, p=0.5):"
print "p value: {0}".format(p_value)

実行結果:

二項検定(帰無仮説:先行と後攻で勝率は変わらない, p=0.5):
p value: 3.31207270291e-09

p値は3.3e-9となり、帰無仮説は有意水準5%で棄却されるため、"先攻と後攻で勝率は異なる"ということになります。
やっぱり後攻は有利なんですね。
(強いチーム→LSDも強い→後攻をとる可能性が高い、と考えられるので、本当はこんな単純にはいかないですが。。。)

ちなみに男女別に勝率を計算してみると、以下のようになります。

# 男女別
mens_result_score_pd = filtered_result_score_pd[filtered_result_score_pd["gender"] =="Men"]
mens_cross_table = mens_result_score_pd.pivot_table(index="hammer", columns="result", values="draw", aggfunc="count")
mens_cross_table["winning_percentage"] = mens_cross_table[1] / (mens_cross_table[-1] + mens_cross_table[1])
print "\n男子:"
print mens_cross_table

womens_result_score_pd = filtered_result_score_pd[filtered_result_score_pd["gender"] =="Women"]
womens_cross_table = womens_result_score_pd.pivot_table(index="hammer", columns="result", values="draw", aggfunc="count")
womens_cross_table["winning_percentage"] = womens_cross_table[1] / (womens_cross_table[-1] + womens_cross_table[1])
print "\n女子:"
print womens_cross_table

実行結果:

男子:
result   -1    1  winning_percentage
hammer                              
0       569  431               0.431
1       431  569               0.569

女子:
result   -1    1  winning_percentage
hammer                              
0       491  373            0.431713
1       373  491            0.568287

あまり変わらないですね。。。

2. 国別の先行/後攻別の勝率

次に国別に先行/後攻で勝率に差があるかを見ていきます。

まずは日本とカナダの勝率を見てみましょう。

# 国別に集計
cross_table = filtered_result_score_pd.pivot_table(index=["team", "hammer"], columns="result", values="draw", aggfunc="count")

# NaNは0勝または0敗に変換
cross_table[1] = [x if x == x else 0 for x in cross_table[1]]
cross_table[-1] = [x if x == x else 0 for x in cross_table[-1]]

# indexを振り直す
cross_table.reset_index(inplace=True)

# 勝率を計算
cross_table["winning_percentage"] = cross_table[1] / (cross_table[-1] + cross_table[1])

# 日本、カナダの勝率
print "勝率(Japan):"
print cross_table.ix[cross_table["team"] == "Japan"][["hammer", -1, 1, "winning_percentage"]]
print "\n勝率(Canada):"
print cross_table.ix[cross_table["team"] == "Canada"][["hammer", -1, 1, "winning_percentage"]]

実行結果:

勝率(Japan):
result  hammer    -1     1  winning_percentage
22           0  65.0  40.0            0.380952
23           1  57.0  39.0            0.406250

勝率(Canada):
result  hammer    -1      1  winning_percentage
4            0  47.0  118.0            0.715152
5            1  36.0  162.0            0.818182

カナダは1割近く勝率が違うのに対し、日本はほとんど差がないように見えます。(カナダは強いですね。。。)
勝率の差に、本当に差があるのか、誤差の範囲であるのかを確かめるために、χ2検定を実施してみます。

(ピアソンの)χ2検定に関して、独立性の検定についてWikipedia*3から引用すると、

2つの変数に対する2つの観察(2x2分割表で表される)が互いに独立かどうかを検定する。例えば、「別の地域の人々について、選挙である候補を支持する頻度が違う」かどうかを検定する方法

です。帰無仮説、対立仮説は以下のようになります。

 帰無仮説:2 つの変数(勝敗と先行/後攻)は独立に分布しており、関連していない
 対立仮説:2 つの変数(勝敗と先行/後攻)は独立に分布しておらず、関連している

それでは、日本とカナダに関してχ2検定を実施してみましょう。

# カイ二乗検定
print "\nx2検定(勝敗と先行/後攻)"

# 日本
cross_tab_jp = cross_table.ix[cross_table["team"] == "Japan"][[-1, 1]]
cross_tab_jp.index = cross_table[cross_table["team"] == "Japan"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_jp)
print "日本:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

# カナダ
cross_tab_ca = cross_table.ix[cross_table["team"] == "Canada"][[-1, 1]]
cross_tab_ca.index = cross_table[cross_table["team"] == "Canada"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_ca)
print "\nカナダ:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

実行結果:

x2検定(勝敗と先行/後攻)
日本:
  x2値:0.049386195454
  p値:0.82413480183

カナダ:
  x2値:4.84846062823
  p値:0.0276708165193

有意水準5%で日本は帰無仮説を棄却できず、カナダは棄却されます。カナダについては勝敗と先行/後攻が独立ではなく、関連している(後攻の方が勝率が良い)と言えますが、日本については必ずしもそうとは言えない、といった結果になりました。

次に各国の先行(Hammer)/後攻別の勝率を散布図にプロットしてみます。

# 先行・後攻の勝率リスト
hammer_win_percentage_list = cross_table[cross_table["hammer"] == 1]["winning_percentage"]
not_hammer_win_percentage_list = cross_table[cross_table["hammer"] == 0]["winning_percentage"]

# 散布図の描画
print "\n各国の先攻・後攻での勝率の散布図:"
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.scatter(hammer_win_percentage_list, not_hammer_win_percentage_list, alpha=0.5)

# 軸ラベル
ax.set_xlabel("Hammer Winning Percentage")
ax.set_ylabel("Not Hammer Winning Percentage")

# 描画範囲の設定
x_min, x_max = ax.get_xlim()
y_min, y_max = ax.get_ylim()
if x_min < y_min:
    y_min = x_min
else:
    x_min = y_min
if x_max < y_max:
    x_max = y_max
else:
    y_max = x_max
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)

# 縦横比の調整
ax.set_aspect(aspect='equal')

# グリッド線の描画
plt.grid()

# 描画
plt.show()

実行結果:
f:id:curlyst:20170607230655p:plain

x軸が後攻の勝率、y軸が先行の勝率を表しています。各軸の描画範囲を揃えているので、対角線より左上にプロットされている国は、先行の方が勝率が良い国になります。

# 先攻の方が勝率が高い国
hammer_pd = cross_table[cross_table["hammer"] == 1][["team", "winning_percentage"]]
hammer_pd.columns = ["team", "hammer_winning_percentage"]
not_hammer_pd = cross_table[cross_table["hammer"] == 0][["team", "winning_percentage"]]
not_hammer_pd.columns = ["team", "not_hammer_winning_percentage"]
win_percentage_pd = pd.merge(hammer_pd, not_hammer_pd, on="team")
print "\n先攻の勝率 > 後攻の勝率"
print win_percentage_pd[win_percentage_pd["hammer_winning_percentage"] < win_percentage_pd["not_hammer_winning_percentage"]]

実行結果:

先攻の勝率 > 後攻の勝率
              team  hammer_winning_percentage  not_hammer_winning_percentage
0        Australia                   0.300000                       0.416667
4   Czech Republic                   0.268657                       0.296296
12           Korea                   0.305556                       0.339623
15     New Zealand                   0.533333                       0.588235

上記の4カ国は先行のときの勝率の方が高くなっていますが、勝率の差が小さい国もあります。そこで、先ほどと同様に、勝敗と先行・後攻に関係があるか、χ2検定を実施してみます。

# カイ二乗検定
print "\nx2検定(勝敗と先行/後攻)"

# オーストラリア
cross_tab_au = cross_table.ix[cross_table["team"] == "Australia"][[-1, 1]]
cross_tab_au.index = cross_table[cross_table["team"] == "Australia"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_au)
print "オーストラリア:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

# チェコ
print 
cross_tab_cz = cross_table.ix[cross_table["team"] == "Czech Republic"][[-1, 1]]
cross_tab_cz.index = cross_table[cross_table["team"] == "Czech Republic"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_cz)
print "チェコ:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

# 韓国
print 
cross_tab_kr = cross_table.ix[cross_table["team"] == "Korea"][[-1, 1]]
cross_tab_kr.index = cross_table[cross_table["team"] == "Korea"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_kr)
print "韓国:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

# ニュージーランド
print 
cross_tab_nz = cross_table.ix[cross_table["team"] == "New Zealand"][[-1, 1]]
cross_tab_nz.index = cross_table[cross_table["team"] == "New Zealand"]["hammer"]
x2, p, dof, expected = scipy.stats.chi2_contingency(cross_tab_nz)
print "ニュージーランド:"
print "  x2値:{0}".format(x2)
print "  p値:{0}".format(p)

実行結果:

x2検定(勝敗と先行/後攻)
オーストラリア:
  x2値:0.236532738095
  p値:0.626721646734

チェコ:
  x2値:0.017640578753
  p値:0.894337444691

韓国:
  x2値:0.0112660094219
  p値:0.915470117005

ニュージーランド:
  x2値:0.00199190787426
  p値:0.964401596142

いずれのp値も大きく、有意水準5%で帰無仮説は棄却されず、先行・後攻の違いが勝敗と関係しているとは言い難い結果になりました。
(やっぱり、先行の方が勝率が良いなんて、中々ないですよね。。。)

3. 1エンド目の結果と勝率

最後に1エンド目の結果(得点)と勝率の関係を見ていきましょう。

集計する前に、まずは前処理として、得点のパターンを以下の7通りに変換します。

# 分類
1 3点以上の得点 3
2 2点の得点 2
3 1点の得点 1
4 0点 0
5 1点の失点 -1
6 2点の失点 -2
7 3点以上の失点 -3


# 前処理
# endのカラム名リスト
end_column_list = filter(lambda x: re.match(r"end*", x), list(result_score_pd.columns))

# 各エンドの型を数値に変換
for end in end_column_list:
    if result_score_pd[end].dtype != np.int:
        result_score_pd[end] = result_score_pd[end].apply(lambda x: int(x) if isinstance(x, str) and x.isdigit() else -10)

# 0点を失点とブランクエンド(0)に分ける
renew_result_score_pd = result_score_pd.copy()
column_list = list(renew_result_score_pd.columns)
for row_idx in range(0, renew_result_score_pd.shape[0], 2):
    row1 = renew_result_score_pd.iloc[row_idx, :]
    row2 = renew_result_score_pd.iloc[row_idx+1, :]
    
    # 同じゲームのレコードであるか確認
    if row1["draw"] != row2["draw"]:
        print "draw is different: row {0} and {1}".format(row1["draw"], row2["draw"])
    elif row1["sheet"] != row2["sheet"]:
        print "sheet is different: row {0} and {1}".format(tmp_result_score_pd["draw"][row_idx], tmp_result_score_pd["draw"][row_idx+1])
    
    for target_column in end_column_list:
        target_column_idx = column_list.index(target_column)

        end_score1 = row1[target_column_idx]
        end_score2 = row2[target_column_idx]
        # ブランクエンドかチェック
        blank_end = False
        if end_score1 == end_score2 == 0:
            blank_end = True
        # 失点の場合
        if not blank_end:
            if end_score1 == 0:
                #end_score1 = -1 * renew_result_score_pd.iloc[row_idx+1, target_column_idx]
                end_score1 = -1 * end_score2
                renew_result_score_pd.iloc[row_idx, target_column_idx] = end_score1
            if end_score2 == 0:
                #end_score2 = -1 * renew_result_score_pd.iloc[row_idx, target_column_idx]
                end_score2 = -1 * end_score1
                renew_result_score_pd.iloc[row_idx+1, target_column_idx] = end_score2
        # 3点以上はビッグエンドとして3にまとめる
        if end_score1 >= 3:
            renew_result_score_pd.iloc[row_idx, target_column_idx] = 3
        if end_score1 <= -3:
            renew_result_score_pd.iloc[row_idx, target_column_idx] = -3
        if end_score2 >= 3:
            renew_result_score_pd.iloc[row_idx+1, target_column_idx] = 3
        if end_score2 <= -3:
            renew_result_score_pd.iloc[row_idx+1, target_column_idx] = -3

それでは1エンド目の得点有無による勝率を集計してみます。

winning_percentage_pd = None
not_hammer_winning_percentage_pd = None
hammer_winning_percentage_pd = None

for target_column in end_column_list:
    end_scoring_pd = renew_result_score_pd.pivot_table(index=target_column, columns="result", values="draw", aggfunc="count")
    end_scoring_pd.reset_index(inplace=True)
    end_scoring_pd.columns.name = "#"
    end_scoring_pd = end_scoring_pd.where(end_scoring_pd == end_scoring_pd, 0)
    end_scoring_pd["winning_percentage"] = end_scoring_pd[1] / (end_scoring_pd[-1] + end_scoring_pd[1])
    end_scoring_pd = end_scoring_pd[[target_column, "winning_percentage"]]
    if winning_percentage_pd is None:
        winning_percentage_pd = end_scoring_pd
        winning_percentage_pd.columns = ["score", target_column]
    else:
        end_scoring_pd.columns = ["score", target_column]
        winning_percentage_pd = pd.merge(winning_percentage_pd, end_scoring_pd, on="score", how="outer")

    # 先行の場合
    not_hammer_end_scoring_pd = renew_result_score_pd[renew_result_score_pd["hammer"] != 1].pivot_table(index=target_column, columns="result",
                                                                                                      values="draw", aggfunc="count")
    not_hammer_end_scoring_pd.reset_index(inplace=True)
    not_hammer_end_scoring_pd.columns.name = "#"
    not_hammer_end_scoring_pd = not_hammer_end_scoring_pd.where(not_hammer_end_scoring_pd == not_hammer_end_scoring_pd, 0)
    not_hammer_end_scoring_pd["winning_percentage"] = not_hammer_end_scoring_pd[1] / (not_hammer_end_scoring_pd[-1] + not_hammer_end_scoring_pd[1])
    not_hammer_end_scoring_pd = not_hammer_end_scoring_pd[[target_column, "winning_percentage"]]
    if not_hammer_winning_percentage_pd is None:
        not_hammer_winning_percentage_pd = not_hammer_end_scoring_pd
        not_hammer_winning_percentage_pd.columns = ["score", target_column]
    else:
        not_hammer_end_scoring_pd.columns = ["score", target_column]
        not_hammer_winning_percentage_pd = pd.merge(not_hammer_winning_percentage_pd, not_hammer_end_scoring_pd, on="score", how="outer")

    # 後攻の場合
    hammer_end_scoring_pd = renew_result_score_pd[renew_result_score_pd["hammer"] == 1].pivot_table(index=target_column, columns="result",
                                                                                                  values="draw", aggfunc="count")
    hammer_end_scoring_pd.reset_index(inplace=True)
    hammer_end_scoring_pd.columns.name = "#"
    hammer_end_scoring_pd = hammer_end_scoring_pd.where(hammer_end_scoring_pd == hammer_end_scoring_pd, 0)
    hammer_end_scoring_pd["winning_percentage"] = hammer_end_scoring_pd[1] / (hammer_end_scoring_pd[-1] + hammer_end_scoring_pd[1])
    hammer_end_scoring_pd = hammer_end_scoring_pd[[target_column, "winning_percentage"]]
    if hammer_winning_percentage_pd is None:
        hammer_winning_percentage_pd = hammer_end_scoring_pd
        hammer_winning_percentage_pd.columns = ["score", target_column]
    else:
        hammer_end_scoring_pd.columns = ["score", target_column]
        hammer_winning_percentage_pd = pd.merge(hammer_winning_percentage_pd, hammer_end_scoring_pd, on="score", how="outer")
    
winning_percentage_pd = winning_percentage_pd.set_index("score")
not_hammer_winning_percentage_pd = not_hammer_winning_percentage_pd.set_index("score")
hammer_winning_percentage_pd = hammer_winning_percentage_pd.set_index("score")

merge_win_per_pd = pd.DataFrame([winning_percentage_pd["end1"], not_hammer_winning_percentage_pd["end1"], hammer_winning_percentage_pd["end1"]]).T
merge_win_per_pd.columns = ["all", "not_hammer", "hammer"]

print "1エンド目の得点別勝率(全体、先行、後攻):"
print merge_win_per_pd

実行結果:

1エンド目の得点別勝率(全体、先行、後攻):
            all  not_hammer    hammer
score                                
-3     0.154472    0.130000  0.260870
-2     0.247031    0.247340  0.244444
-1     0.468790    0.505282  0.373272
 0     0.500000    0.421245  0.579336
 1     0.531210    0.619910  0.496454
 2     0.752969    0.750000  0.753351
 3     0.845528    0.739130  0.870000

f:id:curlyst:20170611170031p:plain

先行(not hammer)、後攻(hammer)ともに基本は右肩上がりで、多く得点する方が勝率が上がっていますが、0点(ブランクエンド)や1点しか取れない(取られない)場合の勝率が先行と後攻で傾向が少し異なっています。
有利な後攻の場合、基本は2点以上取るように作戦をたてて、1点しか取れない(取らされる)場合はブランクにします。逆に先行のチームは1点取らせるように作戦をたてます。グラフを見てみると、後攻チームは1点取る(取らされる)よりもブランクエンドの場合の方が勝率がよく、逆に先行チームはブランクエンドよりも、1失点の方が勝率が高くなっています。

てか後攻で3点以上とった場合の勝率が高すぎですね。。。

4.まとめ

今回は、先行・後攻別の勝率や、1エンド目の得失点による勝率の違いを見てみました。
次からはもう少し統計や機械学習の勉強を絡めていきたいですね。。。(少し趣味に走りすぎてるかな。。。)

*1:Last Stone Drawの略。試合前の練習後に代表者2名がストーンを投げて、ハウスの中心からの距離を計測します。2投の合計値が短いチームが先攻・後攻を選ぶことができます。

*2:「二項検定」『フリー百科事典 ウィキペディア日本語版』、2015年10月29日 (木) 11:42 UTC、URL: https://ja.wikipedia.org/wiki/%E4%BA%8C%E9%A0%85%E6%A4%9C%E5%AE%9A

*3:カイ二乗検定」『フリー百科事典 ウィキペディア日本語版』、2016年11月7日 (月) 15:12 UTC、URL: https://ja.wikipedia.org/wiki/%E3%82%AB%E3%82%A4%E4%BA%8C%E4%B9%97%E6%A4%9C%E5%AE%9A

世界カーリング選手権データの可視化①(pandasによるデータ集計・可視化)

前回の記事からだいぶ経ってしまいましたが、pandas,matplotlibを使って、少しデータを集計・可視化したいと思います。
可視化するデータは以下で取得したCSV形式の試合結果データ(xxx_result_score.csv)です。

curlyst.hatenablog.com

なお、今回対象とするデータは、2002年〜2016年までの15年間の男女世界選手権のデータです。
(ただし、女子の2009年と2011年のデータに関しては、各試合の詳細データが取得できなかったため、今回は対象外としています。)

1. データの読み込み

まずはデータが格納されたディレクトリから再帰的にファイルを探索して、各世界選手権データのファイルパスを取得します。

# 入力ディレクトリ
input_dir_path = "<ファイルを格納してあるディレクトリパスを指定>"
input_file_ext = "results_score.csv"

input_file_path_list = []
for root, dirs, files in os.walk(input_dir_path):
    for file in files:
        if input_file_ext in file:
            input_file_path_list.append(os.path.join(root, file))


上記で取得した各ファイルパスに対して、1ファイルずつ順に読み込んでいき、データをDataFrame(result_score_pd)に格納していきます。
なお、以下では読み込んだデータをDataFrameに格納する前に、以下の2つの処理を実施しています。

  • 勝敗フラグ(勝ち:1, 引き分け:0, 負け:-1)の付与
  • 年、性別カラムの追加


# 各試合のスコアデータを格納するDataFrame
result_score_pd = None

# 各ファイルを順に読み込み、DataFrameに格納する
for file_path in input_file_path_list:
    # CSVファイルの読み込み
    tmp_result_score_pd = pd.read_csv(file_path)
    
    # 勝敗フラグの付与(勝ち:1, 引き分け:0, 負け:-1)
    result_score_list = []
    for row_idx in range(0, tmp_result_score_pd.shape[0], 2):
        score1 = tmp_result_score_pd["score(total)"][row_idx]
        score2 = tmp_result_score_pd["score(total)"][row_idx+1]
        
        if tmp_result_score_pd["draw"][row_idx] != tmp_result_score_pd["draw"][row_idx+1]:
            print "draw is different: row {0} and {1}".format(tmp_result_score_pd["draw"][row_idx], tmp_result_score_pd["draw"][row_idx+1])
        elif tmp_result_score_pd["draw"][row_idx] != tmp_result_score_pd["draw"][row_idx+1]:
            print "sheet is different: row {0} and {1}".format(tmp_result_score_pd["draw"][row_idx], tmp_result_score_pd["draw"][row_idx+1])
        
        if score1 < score2:
            result_score_list.append(-1)
            result_score_list.append(1)
        elif score1 == score2:
            result_score_list.append(0)
            result_score_list.append(0)
        else:
            result_score_list.append(1)
            result_score_list.append(-1)

        # Hammerの修正(先行/後攻がわからない場合)
        hammer1 = tmp_result_score_pd["hammer"][row_idx]
        hammer2 = tmp_result_score_pd["hammer"][row_idx+1]
        if hammer1 == hammer2:
            tmp_result_score_pd["hammer"][row_idx] = -1
            tmp_result_score_pd["hammer"][row_idx+1] = -1
    
    tmp_result_score_pd["result"] = result_score_list
    
    # 年度、性別の取得
    tmp_list = re.findall(r"[0-9]+", file_path)
    if len(tmp_list) != 1 and len(tmp_list[0]) != 4:
        print "Year is missing: {0} {1}".format(file_path, year)
    year = tmp_list[0]
    
    gender = None
    if "Women" in file_path:
        gender = "Women"
    elif "Men" in file_path:
        gender = "Men"
    else:
        print "Gender is missing: row {0}.".format(row_idx)
    
    tmp_result_score_pd["year"] = [year] * tmp_result_score_pd.shape[0]
    tmp_result_score_pd["gender"] = [gender] * tmp_result_score_pd.shape[0]
    
    # DataFrameに追加
    if result_score_pd is None:
        result_score_pd = tmp_result_score_pd
    else:
        result_score_pd = pd.concat([result_score_pd, tmp_result_score_pd])

# データの年数
year_list = list(set(result_score_pd["year"]))
year_list.sort()
print "the number of years : {0}".format(len(year_list))
print year_list

# 年×男or女
year_gender_pd = result_score_pd[["year", "gender"]]
year_gender_pd = year_gender_pd.drop_duplicates()
year_gender_pd = year_gender_pd.sort_values(by=["year", "gender"])
print "\nthe number of competition : {0}".format(year_gender_pd.shape[0])
year_gender_list = []
for value in year_gender_pd.values:
    year_gender_tuple = tuple(value)
    year_gender_list.append(year_gender_tuple)
print year_gender_list

上記を実行すると以下のように出力されます。

the number of years : 15
['2002', '2003', '2004', '2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016']

the number of competition : 28
[('2002', 'Men'), ('2002', 'Women'), ('2003', 'Men'), ('2003', 'Women'), ('2004', 'Men'), ('2004', 'Women'), ('2005', 'Men'), ('2005', 'Women'), ('2006', 'Men'), ('2006', 'Women'), ('2007', 'Men'), ('2007', 'Women'), ('2008', 'Men'), ('2008', 'Women'), ('2009', 'Men'), ('2010', 'Men'), ('2010', 'Women'), ('2011', 'Men'), ('2012', 'Men'), ('2012', 'Women'), ('2013', 'Men'), ('2013', 'Women'), ('2014', 'Men'), ('2014', 'Women'), ('2015', 'Men'), ('2015', 'Women'), ('2016', 'Men'), ('2016', 'Women')]

	finish file readeing: 0.87944817543 [sec]

なお、DataFrameには以下のようにデータが格納されます。

f:id:curlyst:20170106003330p:plain


2. データの集計・可視化

データの読み込みができたので、まずはデータの概要を把握するために、簡単な集計・可視化を行っていきます。

2-1. 全試合数

2002年〜2016年までの全28大会で、合計何試合が行われたかを集計します。

# 試合数
game_num = result_score_pd.shape[0] / 2
print "全試合数: {0}".format(game_num)

実行結果

全試合数: 1873

15年間で男女合わせて1873試合が世界選手権で行われています。カーリングの世界選手権では予選はリーグ戦なので、試合数は多いですね。

2-2. 出場チーム(国)数の推移

各大会で何カ国が出場しているかを集計します。

# 各大会のチーム数
print "\n各大会の出場国数: "
team_num_pd = pd.pivot_table(data=result_score_pd, index="gender", columns="year", values="team", aggfunc=lambda x: len(x.unique()))
print team_num_pd

実行結果

各大会の出場国数: 
year    2002  2003  2004  2005  2006  2007  2008  2009  2010  2011  2012  2013  2014  2015  2016 
gender                                                                     
Men     10.0  10.0  10.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  12.0  
Women   10.0  10.0  10.0  12.0  12.0  12.0  12.0   NaN  12.0   NaN  12.0  12.0  12.0  12.0  12.0 

男女共、各年での出場チーム数は同じで、2002年〜2004年までは10チーム、2005年からは12チームが出場しています。

2-3. 各国の出場回数と勝率

各国が男女合わせて何回世界選手権に出場しているか、また各国の勝率を可視化してみます。

# 各国の出場回数
team_entry_num_pd = pd.pivot_table(data=result_score_pd, index="team", columns="gender", values="year", aggfunc=lambda x: len(x.unique()))
team_entry_num_pd = team_entry_num_pd.fillna(0)
team_entry_num_pd["total"] = team_entry_num_pd["Men"] + team_entry_num_pd["Women"]
team_entry_num_pd = team_entry_num_pd.sort_values(by="total", ascending=False)

ax = team_entry_num_pd.plot.bar(y=["Men", "Women"], alpha=0.8, stacked=True)
ax.set_yticks(range(0, 30, 1), minor=True)
ax.grid('on', which='minor', axis='y')
ax.set_ylabel("num of entry", fontsize=13)
ax.set_xlabel("country", fontsize=13)
ax.tick_params(labelsize=13)

実行結果
f:id:curlyst:20170106005547p:plain

カーリングの盛んなカナダや、発祥地であるスコットランドは全大会に出場しています。
日本も全体の9番目に位置しており、がんばっていますね。

次に各国の勝率を可視化します。

# 各国の勝率の計算
win_lose_num_pd = pd.pivot_table(data=result_score_pd, index="team", columns="result", values="draw", aggfunc="count")
win_lose_num_pd = win_lose_num_pd.fillna(0)
win_lose_num_pd.columns = ["lose", "win"]
win_lose_num_pd["total"] =  win_lose_num_pd["lose"] + win_lose_num_pd["win"]
win_lose_num_pd["winning percentage"] = win_lose_num_pd["win"] / win_lose_num_pd["total"]
win_lose_num_pd = win_lose_num_pd.sort_values(by="winning percentage", ascending=False)

# 勝ち数・負け数の棒グラフ
ax = win_lose_num_pd.plot.bar(y=["win", "lose"], alpha=0.8, figsize=(10, 5))
ax.set_ylabel("num of games", fontsize=13)
ax.set_xlabel("country", fontsize=13)
ax.tick_params(labelsize=13)
# 勝率の折れ線グラフ(2軸)
ax2 = ax.twinx()
win_lose_num_pd.plot(ax=ax2, y="winning percentage", color="red", marker="o", markersize=5, legend=False)
ax2.set_ylabel("winning percentage", fontsize=13)
ax2.set_ylim([0.0, 1.0])
ax2.tick_params(labelsize=13)
ax.grid()
ax2.set_xlim([-1, win_lose_num_pd.shape[0]])

実行結果
f:id:curlyst:20170110002021p:plain

赤い折れ線が勝率を表しています。
勝率を見るとカナダが圧倒的で約7割7分と、やはり強いですね。
日本は負け越し。。。日本がんばれ!!

3. まとめ

今回は最初なので、まずはpadasを用いてデータの読み込みから簡単な可視化を実施しました。
次回はもう少し掘り下げた集計・可視化ができればと思います。

世界カーリング選手権のデータをCSV形式で取得する

統計とかの勉強用に何か面白いデータがないか探していたところ、以下のサイトにオリンピックや世界選手権のカーリングのデータを発見!

results.worldcurling.org

ただ、CSV形式のデータなどは用意されておらず、REST APIhttp://resultsapi.worldcurling.org/)に何度アクセスしても、データを取得できず。。。(誰か取得方法を知っていたら教えてください。。。)

なので、Pythonを使ってHTMLをパースして、データを抽出することにしました。

1. ライブラリの読み込み

世界カーリング選手権のページのHTMLの取得にはrequestsパッケージ、取得したHTMLのパースにはlxmlパッケージを利用しました。

import requests
import csv
from lxml import html

2. HTMLソースの取得

HTMLソースの取得は簡単で、requestパッケージのgetメソッドを使ってGETリクエストを送信すれば良いです。
とりあえず世界カーリング選手権2016(男子)のデータを取得します。

# 取得するページのURL
base_url = "http://results.worldcurling.org"
URL = base_url + "/Championship/Details/555"

# 取得
result = requests.get(URL)

3. HTMLソースからデータを取得

3-1. トーナメント名、Resultsタブの取得

ここからHTMLをパースしてデータを抽出していくのですが、基本的には以下の2つの操作を繰り返し実行することで、HTMLをパースしました。

  1. 所望のHTML要素が出現するまで子要素を順にたどっていく(lxmlパッケージ、getchildren()関数)
  2. XPathを用いて所望のHTML要素を取得する(lxmlパッケージ、xpath()関数)

まずはroot要素からXPathを使って、トーナメント名があるHTML要素(h2)、試合結果が格納されているHTML要素(div)を取得します。WCFのサイトでは、以下のように6個のタブで構成されており、試合結果はResultsタブにあります。

f:id:curlyst:20160718233031p:plain
[出典:http://results.worldcurling.org/Championship/Details/555

以下ではXPathを用いてResultsタブのHTML要素(div)を取得し、後述するget_results_data()でデータを抽出しています。

# HTMLのrootを取得
root = html.fromstring(result.content)

# トーナメント名
h2_element = root.xpath('//h2')[0]
tournament_name = h2_element.text.strip()

# Resultsタブを取得
results_tab_list = root.xpath('//div[contains(@class, "tab-pane fade") and @id="inforesults"]')
if len(results_tab_list) > 1:
    print "deata acquisition error : Result tab"
results_tab = results_tab_list[0]
game_info_list, player_stats_list = get_results_data(results_tab, base_url)

3-2. 各ゲームのスコアの取得

Resultsタブの中では、リンク「Show all games」をクリックすると、すべてのゲームの結果が表示されるようになっています。そのため、以下では、まずリンク先のURLを取得し、GETリクエストを送信して、ゲーム結果のHTMLを取得します。

ゲーム結果には、①スコア情報と、②各選手のショット率のデータが含まれています。

① スコア情報
スコア情報では、「ドロー」「シート」「チーム名」「ハンマーの有無」「各エンドの得点」「合計得点」の6種類のデータを取得します。各データの取得にはXPathを利用し、それぞれが格納されているHTML要素のclass属性を指定することで、該当するHTML要素が取得できます。

② 各選手のショット率
各選手のショット率は、「チーム名」「選手名」「ショット率」のデータを取得します。XPathとgetchildren()関数を用いて、子要素を辿っていくことで、各データを取得します。

取得したデータは、いずれもListに格納して、呼び出し元に返します。

def get_results_data(results_tab, base_url):
    '''
    Resultsタブのデータを取得

    Keyword arguments:
    results_tab --- html data of Results tab
    base_url --- url of WCF Results & Statistics Page
    '''
    # <a>を取得
    a_element_list = results_tab.xpath(".//a")
    # Show all games
    a_element = a_element_list[-1]
    url = base_url + a_element.get("href")
    
    # データのHTMLを取得
    all_data_html = requests.get(url)
    
    # HTMLのrootを取得
    root = html.fromstring(all_data_html.content)
    root_children = root.getchildren()
    game_info_list = []
    player_stats_list = []
    for idx in range(1, len(root_children)):
        child = root_children[idx]

        #--- ゲームのスコア情報を取得 ---
        table_list = child.xpath('.//table[@class="table game-table"]')
        if len(table_list) > 1:
            print "deata acquisition error : game table data"
        table = table_list[0]
        table_children = table.getchildren()
        thread = table_children[0]
        tbody = table_children[1]

        # Draw X を取得
        grandchild = thread.getchildren()[0].getchildren()[0]
        draw = grandchild.text.strip()

        # シートを取得
        tr_element_list = tbody.getchildren()
        td_element_list = tr_element_list[0].xpath('.//td[@class="game-sheet"]')
        if len(td_element_list) > 1:
            print "data acquisition error : sheet name"
        td_element = td_element_list[0]
        sheet_name = td_element.text.strip()

        for tr_element in tr_element_list:
            # チーム名
            td_element_list = tr_element.xpath('.//td[@class="game-team"]')
            if len(td_element_list) > 1:
                print "data acquisition error : team"
            team = td_element_list[0].text.strip()

            # ハンマー
            td_element_list = tr_element.xpath('.//td[@class="game-hammer"]')
            if len(td_element_list) > 1:
                print "data acquisition error : hammer"
            hammer_symbol = td_element_list[0].text.strip()
            hammer = "0"
            if hammer_symbol == '*':
                hammer = "1"

            # スコア
            td_element_list = tr_element.xpath('.//td[contains(@class, "game-end10")]')
            score_list = []
            end_num = len(td_element_list)
            if end_num != 12:
                print "data acquisition error : score"
            score_list = []
            for td_element in td_element_list:
                score_list.append(td_element.text.strip())

            # スコア(トータル)
            td_element_list = tr_element.xpath('.//td[contains(@class, "game-total")]')
            if len(td_element_list) > 1:
                print "data acquisition error : score(total)"
            score_total = td_element_list[0].text.strip()

            # Drow,シート,チーム名,ハンマー,スコア,スコアトータル
            game_info = draw + "," + sheet_name + "," + team + "," + hammer + "," + ",".join(score_list) + "," + score_total
            game_info = []
            game_info.append(draw)
            game_info.append(sheet_name)
            game_info.append(team)
            game_info.append(hammer)
            game_info.extend(score_list)
            game_info.append(score_total)
            game_info_list.append(game_info)

        #--- ゲームの各選手のショット率を取得 ---
        div_col_md_12_list = child.xpath('./div[@class="col-md-12"]')
        if len(div_col_md_12_list) > 1:
            print "deata acquisition error : player stats (col-md-12)"
        div_col_md_12 = div_col_md_12_list[0]
        div_col_md_6_list = div_col_md_12.xpath('./div[@class="col-md-6"]')
        if len(div_col_md_6_list) != 2:
            print "deata acquisition error : player stats (col-md-6)"

        # 先行・後攻チームの選手のデータを取得
        for div_col_md_6 in div_col_md_6_list:
            div_list = div_col_md_6.getchildren()

            # チーム名
            div_team_name = div_list[0]
            h5_element = div_team_name.getchildren()[0]
            team_name = h5_element.text.strip()

            player_dict = {}
            shot_rate_dict = {}
            for idx in range(1, 6):
                if 1+(idx-1)*3 > len(div_list) - 1:
                    break
                    
                # ポジション
                div_position = div_list[1+(idx-1)*3]
                position = div_position.text.strip()
                if "(" in position:
                    parenthesis_idx = position.find("(")
                    position = position[0:parenthesis_idx].strip()
                # 選手
                div_player = div_list[2+(idx-1)*3]
                a_element = div_player.getchildren()[0]
                player_name = a_element.text.strip()
                player_dict[position] = player_name
                # ショット率
                div_shot_rate = div_list[3+(idx-1)*3]
                shot_rate = div_shot_rate.text.strip()
                shot_rate_dict[position] = shot_rate
                
            # Draw,シート,チーム名,
            # Skip選手名,Third選手名,Second選手名,Lead選手名,Alternate選手名,
            # Skipショット率,Thirdショット率,Secondショット率,Leadショット率,Alternateショット率
            player_stats = []
            player_stats.append(draw)
            player_stats.append(sheet_name)
            player_stats.append(team_name)
            fourth_name = player_dict.get("Skip")
            if fourth_name is None:
                fourth_name = player_dict.get("Fourth")
            player_stats.append(fourth_name)
            player_stats.append(player_dict.get("Third"))
            player_stats.append(player_dict.get("Second"))
            player_stats.append(player_dict.get("Lead"))
            player_stats.append(player_dict.get("Alternate"))
            fourth_shot_rate = shot_rate_dict.get("Skip")
            if fourth_shot_rate is None:
                fourth_shot_rate = shot_rate_dict.get("Fourth")
            player_stats.append(fourth_shot_rate)
            player_stats.append(shot_rate_dict.get("Third"))
            player_stats.append(shot_rate_dict.get("Second"))
            player_stats.append(shot_rate_dict.get("Lead"))
            player_stats.append(shot_rate_dict.get("Alternate"))
            player_stats_list.append(player_stats)
            
    return game_info_list, player_stats_list

4. CSVファイルの出力

取得した各データは、それぞれ以下のファイル名でCSVファイルを出力します。

 ① スコア情報:<大会名>_results_score.csv
 ② 各選手のショット率:<大会名>_results_player.csv

選手名にマルチバイト文字が使われているため、出力の際にはutf8に変換しています。

output_dir = u"/Users/takayuki/Documents/work/Study/data"

#--- Rankingタブ ---
output_file_path = output_dir + "/" + tournament_name + "_ranking.csv"
output_file = open(output_file_path, "w")
csv_writer = csv.writer(output_file)
header = u"team,ranking,win,lose,member1,member2,member3,member4,member5"
column_name_list = header.split(',')
ranking_data.insert(0, column_name_list)
for row in ranking_data:
    tmp_row = map(lambda x: x if x is not None else "-", row)
    output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row)
    csv_writer.writerow(output_row_utf8)
output_file.close()

#--- Resultsタブ ---
# スコア
output_file_path = output_dir + "/" + tournament_name + "_results_score.csv"
output_file = open(output_file_path, "w")
csv_writer = csv.writer(output_file)
header = u"draw,sheet,team,hammer,end1,end2,end3,end4,end5,end6,end7,end8,end9,end10,extra1,extra2,score(total)"
column_name_list = header.split(',')
game_info_list.insert(0, column_name_list)
for row in game_info_list:
    tmp_row = map(lambda x: x if x is not None else "-", row)
    output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row)
    csv_writer.writerow(output_row_utf8)
output_file.close()
# 選手のデータ
output_file_path = output_dir + "/" + tournament_name + "_results_player.csv"
output_file = open(output_file_path, "w")
csv_writer = csv.writer(output_file)
header = u"draw,sheet,team,Fourth name,Third name,Second name,Lead name,Alternate name,Fourth shot rate,Third shot rate,Second shot rate,Lead shot rate,Alternate shot rate"
column_name_list = header.split(',')
player_stats_list.insert(0, column_name_list)
for row in player_stats_list:
    tmp_row = map(lambda x: x if x is not None else "-", row)
    output_row_utf8 = map(lambda x: x.encode('utf8'), tmp_row)
    csv_writer.writerow(output_row_utf8)
output_file.close()

5. さいごに

一応、最初のURLを他の大会に変更すれば、他の大会のデータも取得できます。(たぶん。。。動かなかったら誰か教えてください。。。)

今後は取得したデータを使って、とりあえず可視化からやっていきたいと思います。

Deep LearningでIrisデータを分類し、Dropoutの効果を確かめる

Deep Learningはなんとなく知ってはいたけど、実際に動かしてみたことがなかったので、まずはIrisデータを分類してみました。ついでに、Dropoutの有無で学習結果がどう違うのか確認しました。

なお、Deep Learningの実装にはChainerを利用しまいた。本当はTensorFlowとどちらを使おうか迷ったのですが、実際にDeep Learningを使うとしたら所諸事情によりWindowsで実行する必要があるので、Windowsにインストールしている記事*1のあったChainerを選択しました。
(TensorFlowもWindows上で動かす方法はあるみたいなのですが、Dockerを入れるなど、多少手間が必要そうなので。。。)

1. ライブラリのインポート

Chainerの他に、データの読み込みをPandas、標準化をScipy、グラフ描画をmatplotlibで実施したので、それらもインポートします。

import pandas as pd
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
from sklearn.cross_validation import train_test_split
from sklearn import datasets

import chainer
from chainer import Variable, FunctionSet, optimizers
import chainer.functions  as F
import chainer.links as L

2. データの準備

Irisデータはscikit-learnのdatasetsを使うと簡単に読み込めます。ここで説明変数の標準化を行った後に、学習用とテスト用にデータを分割しておきます。なお、変数の型がそのままだと後でChainerに怒られるので、目的変数はnp.int32、説明変数はnp.float32に変換しておきます。

# Irisデータの読み込み
iris = datasets.load_iris()

# 目的変数
target_data = iris.target.astype(np.int32)
    
# 説明変数
tmp_predictor_data = sp.stats.zscore(iris.data, axis=0)  # 標準化
predictor_data = tmp_predictor_data.astype(np.float32)

# 学習データとテストデータに分割
test_size = 30
data_num = len(target_data)
predictor_train, predictor_test, target_train, target_test = train_test_split(predictor_data, target_data, test_size=test_size, stratify=target_data)
train_data_num = len(target_train)
test_data_num = len(target_test)

3. モデルの定義

いよいよここからChainerを使ってDeep Learning(多層パーセプトロン)を実装します。まずはモデルの定義をするのですが、chainer.Chainを継承して作成します。

作成するクラスでは、__init__()と__call__()を定義します。

  • __init___() :多層パーセプトロンの各層を定義します。今回は中間層が2層のパーセプトロンを利用するため、中間層2つと出力層1つを定義しました。それぞれ全結合のため、Linear()を用いています。
  • __call__(): 順伝搬の計算処理(多層パーセプトロンの構造)を定義します。学習時にDropoutを利用するため、2つの中間層の計算の際にdropout()を用いています。また、各層の活性化関数はReLUを利用しました。

上記2つの関数の他に、学習時のDropoutの利用有無を切り替えるために、(少し冗長ですが)2つのフラグtrian、drop_outを定義しました。

class MultiLayerPerceptron(chainer.Chain):
    '''
    モデルの定義(多層パーセプトロン)
    '''
    
    def __init__(self, input_dim, n_units, output_dim, train=True, drop_out_ratio=0.3):
        '''
        コンストラクタ
        '''
        # 多層パーセプトロンの各層の定義
        super(MultiLayerPerceptron, self).__init__(
            l1=F.Linear(input_dim, n_units),
            l2=F.Linear(n_units, n_units),
            l3=F.Linear(n_units, output_dim)
        )
        # 学習の場合:True
        self.__train = train
        # drop outの実施有無
        self.__drop_out = True
        # drop outの比率
        self.drop_out_ratio = drop_out_ratio
    
    def __call__(self, x):
        '''
        順伝搬(foward)計算
        '''
        # 入力層から順に計算(多層パーセプトロンの構造)
        drop_out = self.__train and self.__drop_out 
        h1 = F.dropout(F.relu(self.l1(x)), train=drop_out, ratio=self.drop_out_ratio)
        h2 = F.dropout(F.relu(self.l2(h1)), train=drop_out, ratio=self.drop_out_ratio)
        y  = self.l3(h2)
        
        return y
    
    # 学習の場合;True
    def __get_train(self):
        return self.__train
    
    def __set_train(self, train):
        self.__train = train
        
    train = property(__get_train, __set_train)
    
    # Dropoutを使用する場合:True
    def __get_drop_out(self):
        return self.__drop_out
    
    def __set_drop_out(self, drop_out):
        '''
        drop outフラグの設定
        '''
        self.__drop_out = drop_out
    
    drop_out = property(__get_drop_out, __set_drop_out)

4. 学習クラスの定義

学習を行うクラスでは、主に以下の2つの関数を定義します。

  • learn():ミニバッチで学習を行います。各バッチデータで学習を行い、誤差(交差エントロピー関数の値)と精度を取得してリストに格納して、最後に平均値に直して呼び出し元に返します。
  • evaluate():テストデータを用いた評価用の関数。テストデータに対して分類を行い、誤差と精度を呼び出し元に返します。
class MiniBatchLearner:
    '''
    ミニバッチによる学習を行うクラス
    '''
    
    def __init__(self, optimizer, epoch_num, batch_size):
        '''
        コンストラクタ
        '''
        #--- インスタンス変数 ---
        # optimizer
        self.__optimizer = None
        # 学習の繰り返し数
        self.__epoch_num = None
        # バッチサイズ
        self.__batch_size = None
        # 学習データ、テストデータの誤差、精度(正答率)
        self.__train_loss = None
        self.__train_acc  = None
        self.__test_loss = None
        self.__test_acc  = None
        # パラメータの初期化
        self.set_param(optimizer, epoch_num, batch_size)
        self.__init_loss_acc()
    
    def set_param(self, optimizer, epoch_num, batch_size):
        self.__optimizer = optimizer
        self.__epoch_num = epoch_num
        self.__batch_size = batch_size

    def __init_loss_acc(self):
        self.__train_loss = []
        self.__train_acc = []
        self.__test_loss = []
        self.__test_acc = []
    
    def learn(self, model, predictor_train_data, target_train_data, drop_out=True):
        '''
        学習の実施
        '''
        self.__init_loss_acc()
        
        # 学習データのインデックス(ランダム)
        train_data_num = len(target_train_data)
        perm = np.random.permutation(train_data_num)
        
        sum_accuracy = 0
        sum_loss = 0
        
        # 学習モード
        model.predictor.train = True
        model.predictor.drop_out = drop_out
        
        # バッチサイズごとに学習
        for idx in xrange(0, train_data_num, self.__batch_size):
            predictor_batch = chainer.Variable(predictor_train_data[perm[idx:idx+self.__batch_size]])
            target_batch = chainer.Variable(target_train_data[perm[idx:idx+self.__batch_size]])
            
            # 勾配を初期化
            model.zerograds()
            # 順伝播させて誤差と精度を算出
            loss = model(predictor_batch, target_batch)
            acc = model.accuracy
            # 誤差逆伝播で勾配を計算
            loss.backward()
            # 更新
            self.__optimizer.update()
    
            self.__train_loss.append(loss.data)
            self.__train_acc.append(acc.data)
            sum_loss += float(loss.data) * len(target_batch)
            sum_accuracy += float(acc.data) * len(target_batch)
        
        # 訓練データの誤差と、正解精度を返す
        train_mean_loss = sum_loss / train_data_num
        train_mean_acc = sum_accuracy / train_data_num
        return train_mean_loss, train_mean_acc

    def evaluate(self, model, predictor_test_data, target_test_data):
        sum_accuracy = 0
        sum_loss     = 0
        
        # 評価モード
        model.predictor.train = False
        
        predictor_batch = chainer.Variable(predictor_test_data)
        target_batch = chainer.Variable(target_test_data)
        
        # 順伝播させて誤差と精度を算出
        loss = model(predictor_batch, target_batch)
        acc = model.accuracy
        
        test_data_mum = len(target_test_data)
        sum_loss = float(loss.data) * test_data_mum
        sum_accuracy = float(acc.data) * test_data_mum
        
        # テストデータでの誤差と正解精度を返す
        return float(loss.data), float(acc.data)

また、今回は後々でのコードの見易さのために、以下の2つの関数を作成しました。

  • train_and_eval():学習とテストデータによる評価を指定した回数(n_epoch)だけ実行。各回での誤差と精度は配列に格納しておき、戻り値として呼び出し元に返す。
  • draw_loss_and_acc():各回での学習時の誤差と精度、評価時の誤差と精度を描画。
# 誤差、精度を格納
train_mean_loss_list = []
train_mean_acc_list = []
test_mean_loss_list = []
test_mean_acc_list = []

def train_and_eval(batchsize, n_epoch, print_epoch, drop_out, drop_out_ratio, intermediate_layer_num):
    train_mean_loss_list = []
    train_mean_acc_list = []
    test_mean_loss_list = []
    test_mean_acc_list = []
    
    # モデルのインスタンス定義
    tmp_model = MultiLayerPerceptron(4, intermediate_layer_num, 3, drop_out_ratio=drop_out_ratio)
    mlp_model = L.Classifier(tmp_model)
    
    # Optimizerの定義
    optimizer = optimizers.Adam()
    optimizer.setup(mlp_model)
    
    # 学習クラスのインスタンス定義
    mb_learner = MiniBatchLearner(optimizer=optimizer, epoch_num=n_epoch, batch_size=batchsize)
    
    # Learning loop
    for epoch in xrange(1, n_epoch+1):
        if epoch % print_epoch == 0:
            print 'epoch', epoch
            
        # training
        train_mean_loss, train_mean_acc = mb_learner.learn(mlp_model, predictor_train, target_train, drop_out)
        
        if epoch % print_epoch == 0:
            print 'train mean loss={}, accuracy={}'.format(train_mean_loss, train_mean_acc)
            
        # evaluation
        test_mean_loss, test_mean_acc = mb_learner.evaluate(mlp_model, predictor_test, target_test)
        if epoch % print_epoch == 0:
            print 'test  mean loss={}, accuracy={}'.format(test_mean_loss, test_mean_acc)
        
        train_mean_loss_list.append(train_mean_loss)
        train_mean_acc_list.append(train_mean_acc)
        test_mean_loss_list.append(test_mean_loss)
        test_mean_acc_list.append(test_mean_acc)
    
    return train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list


def draw_loss_and_acc(train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list, xlabel="epoch", set_ylim=True):
    fig = plt.figure(figsize=(14, 5))
    
    # train
    ax1 = fig.add_subplot(1,2,1)
    plt.plot(range(len(train_mean_loss_list)), train_mean_loss_list)
    plt.plot(range(len(train_mean_acc_list)), train_mean_acc_list)
    plt.legend(["train_loss","train_acc"],loc=1)
    plt.title("Loss / Accuracy of Iris class recognition.")
    plt.xlabel(xlabel)
    plt.ylabel("loss/accracy")
    if set_ylim:
        ax1.set_ylim([0, 1.2])
    plt.grid()
    
    # test
    ax2 = fig.add_subplot(1,2,2)
    plt.plot(range(len(test_mean_loss_list)), test_mean_loss_list)
    plt.plot(range(len(test_mean_acc_list)), test_mean_acc_list)
    plt.legend(["test_loss","test_acc"],loc=1)
    plt.title("Loss / Accuracy of Iris class recognition.")
    plt.xlabel(xlabel)
    plt.ylabel("loss/accracy")
    if set_ylim:
        ax2.set_ylim([0, 1.2])
    plt.grid()
    
    plt.show()

5. 学習の実施と結果の描画

それでは準備が整ったので、実際にIrisデータを用いて学習を行い、結果(学習・評価時の誤差と精度)を描画します。今回は前提として、以下のパラメータを用いました。

# 基本パラメータ
batchsize = 10                 # 確率的勾配降下法で学習させる際の1回分のバッチサイズ
n_epoch   = 2000               # 学習の繰り返し回数
print_epoch = n_epoch / 2      # printするepoch数
drop_out_ratio = 0.3           # drop_outの比率
intermediate_layer_num = 8     # 中間層のノード数


まずDropoutを利用して、学習を実施します。

# drop outあり
drop_out = True
train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list = train_and_eval(batchsize, n_epoch, print_epoch, drop_out, drop_out_ratio, intermediate_layer_num)
draw_loss_and_acc(train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list)

f:id:curlyst:20160706233650p:plain

左図が学習時の誤差と精度、右図が評価時の誤差と精度になります。学習時はDropoutを行っているため、誤差・精度ともに値が振動していますが、学習を重ねていくうちに値が小さくなっています。
評価時については、700〜800回程度までは誤差・精度ともに値は徐々に小さくなっていますが、700〜800回くらいを超えたあたりから徐々に誤差が増え始めており、若干、過学習気味になっています。

次にDropoutなしで学習します。

# drop outなし
drop_out = False
train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list = train_and_eval(batchsize, n_epoch, print_epoch, drop_out, drop_out_ratio, intermediate_layer_num)
draw_loss_and_acc(train_mean_loss_list, train_mean_acc_list, test_mean_loss_list, test_mean_acc_list)

f:id:curlyst:20160707000319p:plain

Dropoutを利用した場合に比べて、学習時の収束は早くなっていますが、評価時の誤差を見ると、一旦減った後に急激に増加しており、過学習を起こしていることが分かります。

まとめ

今回はWindowsで使いたいというモチベーションがあったため、Chainerを用いて多層パーセプトロンを実装し、Irisデータを分類しました。また、Dropoutの有無による、誤差・精度の挙動の違いも見てみました。

ただ、Deep Learningのライブラリは他にもたくさんあり、最近、Keras(+Tensorflow)がなんか良い感じみたいなので、次は(いつになるかわかりませんが。。。)Kerasも利用してみたいなぁ、と思っています。