TECH MEMO

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

世界カーリング選手権データの可視化②(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