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%)よりも小さくなる結果となりました。

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