バスケやデータの雑記帳

バスケやデータ分析について徒然なるままに

FIBA Basketball World Cup 2023をデータ分析してみた(Part.3|LightGBM+SHAP分析)

本記事は、全5回に分けてFIBAバスケットボールワールドカップ2023の全92試合を分析してみたシリーズ第3回目です。

  1. 相関分析・可視化分析|勝敗と相関が高いスタッツの項目は何か?勝利したチームと敗北したチームのスタッツの差はどう違うのか?
  2. 決定木分析|勝利するチームのスタッツの条件は?(その1)
  3. LightGBM+SHAP分析|勝利するチームのスタッツの条件は?(その2)
  4. 次元削減分析(主成分分析・t-SNE・UMAP)|スタッツから見る大会参加チームの特徴は?(その1)
  5. 因子分析+クラスタリング|スタッツから見る大会参加チームの特徴は?(その2)

前回は、決定木分析を行い、勝利する際のスタッツの条件分岐点を探ってみました。 bballdatanote.hatenablog.com

今回はLightGBMでスタッツから勝敗を予測するMLモデルを作り、さらにSHAPというMLモデルを解釈するための手法で分析を行っていきます。決定木よりもより高精度に勝敗を予測できる(はずの)MLモデルを使うことで、改めて違う観点から勝利する際のスタッツの条件分岐点を探っていきます。

目次は次の通りです。

データの準備・前処理

前回同様、大会公式サイトのデータを用いて、以下のように試合別・チーム別のデータに集約していきます。また、追加スタッツとしてFour FactorsとField Goal Percentageも出しておきます。

team_df.head()
GAME_KEY TEAM GAME_POSITION RESULT OREB DREB REB AST PF TO ST BLK FG_MADE FG_ATTEMPT 2PTS_MADE 2PTS_ATTEMPT 3PTS_MADE 3PTS_ATTEMPT FT_MADE FT_ATTEMPT eFG% TO% FTR DREB_OPPOSITE OREB% 2PTS% 3PTS% FT%
0 試合日1_ドイツ_日本 日本 right LOSE 6 24 30 17 16 12 5 3 23 65 17 30 6 35 11 17 0.400000 0.142045 0.261538 36 0.142857 0.566667 0.171429 0.647059
1 試合日1_南スーダン_プエルトリコ 南スーダン left LOSE 8 27 35 23 23 19 9 5 34 67 24 41 10 26 18 24 0.582090 0.196769 0.358209 26 0.235294 0.585366 0.384615 0.750000
2 試合日1_スペイン_コートジボワール スペイン left WIN 14 28 42 29 14 17 7 5 34 64 23 35 11 29 15 22 0.617188 0.187472 0.343750 16 0.466667 0.657143 0.379310 0.681818
3 試合日2_オーストラリア_ドイツ ドイツ right WIN 5 20 25 18 19 12 9 3 31 61 20 31 11 30 12 15 0.598361 0.150754 0.245902 22 0.185185 0.645161 0.366667 0.800000
4 試合日2_レバノン_カナダ レバノン left LOSE 6 10 16 19 18 22 12 1 30 62 22 43 8 19 5 5 0.548387 0.255220 0.080645 24 0.200000 0.511628 0.421053 1.000000

LightGBM+SHAP分析

それでは分析をしていきます。いつも通り、勝敗(RESULT)がそのままではWINとLOSEで相関係数を計算できないので、WINの場合は1、LOSEの場合は0としておきます。

df = team_df.clone().with_columns(
    RESULT_INT = pl.col("RESULT").map_dict({"WIN": 1, "LOSE": 0})
)

次に、勝敗と比較するスタッツを前回同様に指定しておきます。

feature_cols = [
    'OREB', # オフェンスリバウンド
    'DREB', # ディフェンスリバウンド
    'AST', # アシスト
    'PF', # ファール
    'TO', # ターンオーバー
    'ST', # スティール
    'BLK', # ブロック
    '2PTS_ATTEMPT', # 2ポイントの試投数
    '3PTS_ATTEMPT', # 3ポイントの試投数
    'FT_ATTEMPT',  # フリースローの試投数
    'eFG%', # efficient Field Goal Percentage(Four Factors)
    'TO%', # Turn Over Percentage (Four Factors)
    'FTR', # Free Throw Rate (Four Factors)
    'OREB%', # Offensive Rebound Percentage (Four Factors)
    '2PTS%', # 2ポイント成功率
    '3PTS%', # 3ポイント成功率
    'FT%', # フリースロー成功率
]

汎化性能の評価

LightGBM自体の汎化性能を見るために、TrainデータとTestデータに分けておきます。試合(GAME_KEY)レベルでTrainかTestに分けたいので、通常利用するtrain_test_splitではなく、GroupShuffleSplitを利用します。

from sklearn.model_selection import GroupShuffleSplit 

# Train/Testに分ける
df = df.to_pandas()
splitter = GroupShuffleSplit(test_size=0.3, random_state=0)
split = splitter.split(df, groups=df["GAME_KEY"])
train_inds, test_inds = next(split)

train_df = df.iloc[train_inds]
test_df = df.iloc[test_inds]

# 特徴量Xと目的変数yに分ける
train_X = train_df[feature_cols]
train_y = train_df["RESULT_INT"]
test_X = test_df[feature_cols]
test_y = test_df["RESULT_INT"]

次に、LightGBMを学習させ、汎化性能を見ておきます。どれくらいの確率で勝敗を予測できるMLモデルなのかを確認するためです。一般的には、決定木(DecisionTree)よりもLightGBMの方がより優れた性能を発揮することが多いです。

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, confusion_matrix
import lightgbm as lgb
from lightgbm import LGBMClassifier

# 2値分類のモデルの評価用関数
def show_binary_classification_score(cls, x, y):
    pred = cls.predict(x)
    pred_prob = cls.predict_proba(x)[:, 1]
    
    # 各種精度指標を計算
    acc = accuracy_score(y, pred)
    precision = precision_score(y, pred)
    recall = recall_score(y, pred)
    f1 = f1_score(y, pred)
    auc = roc_auc_score(y, pred)
    conf_mat = confusion_matrix(y, pred)
    conf_mat = pd.DataFrame(conf_mat, index=["LOSE", "WIN"], columns=["LOSE", "WIN"])
    
    # 各種精度指標を表示
    print(f"Accuracy:\t{acc}")
    print(f"Precision:\t{precision}")
    print(f"Recall:\t{recall}")
    print(f"F1:\t{f1}")
    print(f"AUC:\t{auc}")
    print(f"混同行列:\n{conf_mat}")

# Trainデータで学習
lgb_model = LGBMClassifier(verbose=-1, random_state=0)
lgb_model = lgb_model.fit(train_X, train_y)

# Testデータで評価
show_binary_classification_score(lgb_model, test_X, test_y)
Accuracy:    0.8392857142857143
Precision:   0.8275862068965517
Recall:      0.8571428571428571
F1:          0.8421052631578947
AUC:         0.8392857142857143
混同行列:
      LOSE  WIN
LOSE    23    5
WIN      4   24

前回の決定木ではAccuracyが約0.73、F1も約0.75だったので、およそ10%程度精度が高いMLモデルとなっていそうです。 それでは、SHAPにかける前の事前情報として、LightGBMの特徴量重要度(Feature Importance)を見ていきます。どのスタッツが予測にどの程度寄与しているかを示すものです。

lgb.plot_importance(lgb_model, figsize=(15,10), importance_type='gain')

前回同様、DREBとeFG%が重要であるという結果が見て取れますね。

SHAPによる分析

それでは、さっそくSHAPによる分析を行っていきましょう。 前回同様、全てのデータを使って分析していきます。

まずは、各スタッツがどの程度勝敗に寄与していたかというshap値(shap_values)を求めておきます。

import shap

shap.initjs()
explainer = shap.TreeExplainer(lgb_model)

X = df[feature_cols]
y = df["RESULT_INT"]
shap_values = explainer.shap_values(X=X)

次に、各スタッツの指標とshap値の関係性を可視化していきます。※他にも色んな可視化の方法がありますが、今回の目的からは外れるため、スキップします。

概観

まずは全体を概観しましょう。

n_rows = 5
n_cols = 4

fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(20, 20), tight_layout=True)
for idx, col in enumerate(feature_cols):
    i = idx // n_cols
    j = idx % n_cols
    shap.dependence_plot(ind=col, shap_values=shap_values[1], features=X, feature_names=feature_cols, ax=axes[i, j], show=False)
    axes[i, j].axhline(0, color="gray", linestyle="--")
    
plt.show()

それぞれのグラフの横軸がスタッツそのものの値、縦軸がshap値です。shap値が大きいほど、勝利に寄与しているという読み方となります。また、shap値が0のラインである点線よりも上の場合は勝利に対してプラスの方向に寄与していることを示します。 それぞれの点で赤や青の色がついていますが、今回は簡単のため、一旦無視して分析を進めます。

なお、全てのグラフがとても興味深い結果を示していますが、今回もいくつか気になるものをピックアップして分析していこうと思います。

ディフェンスリバウンド

まずは特徴量重要度でも高い値を示していた、DREBについてです。23本あたりを境に、明確に勝利にプラスに働いているか、マイナスに働いているかが分かれています。ただし、30本を超えてくるとshap値も横ばいとなっているため、これは単純に試合のペースが早くなってリバウンドの総数が増えただけで、ディフェンスでシュートを落とさせたということではないことを示しているかもしれません。

eFG%

次に、2番目に高い特徴量重要度を示していた、eFG%についてです。決定木分析でも58.4%を境に勝敗が別れていましたが、今回の分析でもその値付近でshap値が0を超えるかどうかが決まっています。また、50%から70%くらいまではeFG%が高いほどshap値も高くなる傾向であることが見て取れます。

逆に言うと、それ以下、あるいはそれ以上となるともはや勝敗への影響は(eFG%というスタッツだけで見ると)一定ということなのかもしれません。

ティー

他でshap値がある程度大きい所ではST(スティール)が挙げられます。こちらは10本を超えてくると(DEFがそれだけ厳しいと)勝利に対してプラスの影響が一気に出てくるようです。

3ポイント成功率

また、興味深いグラフだなと感じたのは、3PTS%(スリーポイントの成功率)です。日本代表監督であるトム・ホーバス氏は、女子代表を率いた東京オリンピックの際も、男子代表を率いた今大会の際も40%を目指すということを掲げていました。 実際にshap値で見てみると40%位を境に、一貫してshap値はプラスの値を取るように(勝利に寄与するように)なっています。30%を超えてもプラスになっているケースは見受けられますが、一貫してプラスになるという面ではやはり40%というのが目標とすべき値のようです。

まとめ

今回は、LightGBMとSHAPを用いた分析で、勝敗チーム間のスタッツの違いを見てきました。決定木よりも更に高精度に勝敗を予測できるMLモデルを利用した場合に、どのようなスタッツが勝利に寄与しているのかを概観することが出来ました。

また、どこまでの値を目標とすればよいのかについても一定の示唆が得られる分析結果だったかと思います。個人的にはトム・ホーバス氏が掲げるスリーポイントの成功率40%の正当性が確認できた部分が非常に興味深かったです。

次回以降は、次元圧縮分析・因子分析を利用して、各チームの特徴をマッピングし、どんなチームが似ていると判断できるのか、考察していこうと思います。