バスケやデータの雑記帳

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

FIBA Basketball World Cup 2023をデータ分析してみた(Part.1|相関分析・可視化分析)

日本男子代表がアジア1位になり、パリ五輪出場権を獲得したFIBAバスケットボールワールドカップ2023。代表の活躍に胸を踊らせている中、次のツイートに触発され、自分も全92試合についてデータ分析してみたのでその備忘です。

私の場合は日本にフォーカスするのではなく、全チームを比較した場合に、強いチームはどんなスタッツを残していたのかについて分析してみました。色々と分析してみたら長くなったので、全5回に分けていきます。 今回は第1回目です。

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

目次は次の通りです。

データの準備

まずは、各チームのスタッツを大会公式サイトから取得してきます。取得する際には、以下の記事でまとめたスクレイピング方法を使いました。なお、公式サイトの規約上、データの複製・頒布及びそれに準じる行為は認められていないようなので、ソースコード及び取得データ自体の公開は控えておきます。

bballdatanote.hatenablog.com

取得した結果は、以下のようなものとなります。どの試合で、各チームのどの選手がどんなスタッツを残したのかというデータです。なお、今後分析がしやすいように、GAME_POSITION(試合表示が左側のチームなのか右側のチームなのか)やRESULT(WINかLOSE)などをフラグ立てしています。

PHASE GAME_KEY TEAM GAME_POSITION RESULT PLAYERS_NUMBER PLAYERS PTS OREB DREB REB AST PF TO ST BLK P_M EFF FG_MADE FG_ATTEMPT 2PTS_MADE 2PTS_ATTEMPT 3PTS_MADE 3PTS_ATTEMPT FT_MADE FT_ATTEMPT PLAY_TIME
0 FirstRoundグループA 試合日1_アンゴラ_イタリア アンゴラ left LOSE 0 Eduardo Francisco 3.0 2.0 3.0 5.0 0.0 4.0 0.0 0.0 0.0 6.0 4.0 1.0 4.0 1.0 4.0 0.0 0.0 1.0 2.0 1002
1 FirstRoundグループA 試合日1_アンゴラ_イタリア アンゴラ left LOSE 1 Gerson Domingos 4.0 0.0 2.0 2.0 3.0 0.0 1.0 1.0 0.0 -10.0 2.0 1.0 8.0 1.0 2.0 0.0 6.0 2.0 2.0 1098
2 FirstRoundグループA 試合日1_アンゴラ_イタリア アンゴラ left LOSE 2 Dimitri Maconda 0.0 0.0 0.0 0.0 0.0 1.0 3.0 0.0 0.0 -11.0 -3.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 443
3 FirstRoundグループA 試合日1_アンゴラ_イタリア アンゴラ left LOSE 3 Gerson Goncalves 7.0 0.0 3.0 3.0 4.0 2.0 0.0 0.0 0.0 7.0 5.0 3.0 12.0 3.0 6.0 0.0 6.0 1.0 1.0 1709
4 FirstRoundグループA 試合日1_アンゴラ_イタリア アンゴラ left LOSE 5 Childe Dundao 19.0 1.0 2.0 3.0 3.0 2.0 3.0 2.0 0.0 -12.0 17.0 6.0 12.0 2.0 4.0 4.0 8.0 3.0 4.0 1766

データの前処理

それでは、早速分析のための前処理から行っていきます。まずは必要なライブラリをインポートし、データを読み込みます。

# データハンドリング系
import polars as pl
import numpy as np
import pandas as pd

# 可視化系
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
%matplotlib inline

# その他諸々
from pathlib import Path
from tqdm.notebook import tqdm
import warnings
warnings.filterwarnings("ignore")
df = pl.read_csv("[ファイル名].csv")

読み込んだデータは上記に掲載しているものです。 今は試合別・チーム別・選手別のデータになっているので、これをチーム間の勝敗比較ができるように、試合別・チーム別のデータに集約していきます。また、追加スタッツとしてFour FactorsとField Goal Percentageも出しておきます。

前処理用の関数(ここをクリックすると表示されます)

def process_team_stats(df, key_cols=["GAME_KEY", "TEAM", "GAME_POSITION", "RESULT"]):
    tmp_df = df.clone()
    tmp_df = _groupby_team_stats(tmp_df, key_cols)
    tmp_df = _calc_eFG_percentage(tmp_df)
    tmp_df = _calc_TO_percentage(tmp_df)
    tmp_df = _calc_FT_rate(tmp_df)
    tmp_df = _calc_OREB_percentage(tmp_df)
    tmp_df = _calc_FG_percentage(tmp_df)
    return tmp_df

def _groupby_team_stats(df, key_cols):
    numeric_cols = [col for col in df.columns if df[col].is_numeric()]
    remove_cols = [col for col in df.columns if col in IGNORE_NUMERICAL_COLS]
    
    team_df = df.clone().group_by(
        key_cols
    ).agg(
        pl.sum(numeric_cols)
    )
    team_df = team_df.drop(remove_cols)

    return team_df

def _calc_eFG_percentage(df):
    tmp_df = df.clone()
    
    tmp_df = tmp_df.with_columns(
        ((pl.col("FG_MADE") + 0.5*pl.col("3PTS_MADE")) / pl.col("FG_ATTEMPT")).alias("eFG%")
    )
    return tmp_df

def _calc_TO_percentage(df):
    tmp_df = df.clone()
    tmp_df = tmp_df.with_columns(
        (pl.col("TO") / (pl.col("FG_ATTEMPT") + 0.44*pl.col("FT_ATTEMPT") + pl.col("TO"))).alias("TO%")
    )
    return tmp_df
    
def _calc_FT_rate(df):
    tmp_df = df.clone()
    tmp_df = tmp_df.with_columns(
        FTR = pl.col("FT_ATTEMPT") / pl.col("FG_ATTEMPT")
    )
    return tmp_df
    
def _calc_OREB_percentage(df):
    tmp_df = df.clone()

    if not "DREB_OPPOSITE" in tmp_df.columns:  
        tmp_df = tmp_df.with_columns(
            OPPOSITE = pl.col("GAME_POSITION").map_dict({
                "left": "right",
                "right": "left"
            })
        )
        opposite_df = tmp_df[["GAME_KEY", "GAME_POSITION", "DREB"]].clone().with_columns(
            pl.col("DREB").alias("DREB_OPPOSITE")
        ).drop("DREB")
        tmp_df = tmp_df.join(
            opposite_df,
            left_on=["GAME_KEY", "OPPOSITE"],
            right_on=["GAME_KEY", "GAME_POSITION"],
            how="left"
        )
    
    tmp_df = tmp_df.with_columns(
        (pl.col("OREB") / (pl.col("OREB") + pl.col("DREB_OPPOSITE"))).alias("OREB%")
    )
    
    if "OPPOSITE" in tmp_df.columns:
        tmp_df = tmp_df.drop("OPPOSITE")
    
    return tmp_df

def _calc_FG_percentage(df):
    tmp_df = df.clone()
    for fg in ["2PTS", "3PTS", "FT"]:
        tmp_df = tmp_df.with_columns(
            (pl.col(f"{fg}_MADE") / pl.col(f"{fg}_ATTEMPT")).alias(f"{fg}%")
        )
    return tmp_df

team_df = process_team_stats(df)

前処理した結果、次のようなデータになっています。

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

相関分析

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

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

次に、勝敗と比較するスタッツを指定します。シュートを決めた本数や、合計リバウンド、2PTと3PTの合計試投数と相関係数を取ってもあまり示唆は得られないので、それ以外のスタッツを指定します。※要は、できるだけ結果そのものというよりはチームの能力を表していそうなスタッツを見ることにします。

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%', # フリースロー成功率
]
corr_cols = feature_cols + [`RESULT_INT`]

相関係数のヒートマップを表示させます。赤いほど相関係数が高く、青いほど相関係数が低く表示されています。

corr_df = (eda_df[corr_cols]
           .corr()
           .with_columns(INDEX_COL=pl.Series(corr_cols))
           .to_pandas()
           .set_index("INDEX_COL")
)
plt.figure(figsize=(12, 8))
sns.heatmap(corr_df, cmap="coolwarm", annot=True, fmt=".2f")
plt.show()

一番下の行のRESULT_INTを見ると、いわゆるミスのTOTO%や自分たちにペナルティを与えられるPF が青いので、この数値が大きいほど相関係数が低い(→敗北につながっている)ことや、相手にセカンド・チャンスを与えないDREBや効率よくシュートを決められているかを示すeFG%2PTS%・'3PT%'が赤いので、この数値が大きいほど相関係数が高い(→勝利につながっている)傾向が見て取れます。 なんとなく肌感に合っているような結果が得られました。

可視化分析

次に、もう少し生データに近い形で、箱ひげ図を使って勝利した場合と敗北した場合のスタッツを比較していきます。

概観

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
    sns.boxplot(x=col, y="RESULT", data=eda_df.to_pandas(), ax=axes[i, j])
    axes[i, j].set_title(f"{col} x RESULT")
    
plt.show()

まずは一気にプロットして概観を把握してみます。先程の相関分析で値が大きい/小さかったスタッツほど、勝敗でデータの分布が分かれていそうです。 気になったものについて拡大して見ていきます。

オフェンスリバウンド

まずはオフェンスリバウンドです。こちらは意外と勝敗では差がついていなさそうです。これは、(今回のワールドカップにおいては)セカンドチャンスを頑張るよりも、以下に効率よくシュートを決められるかの方が重要だったということかもしれません。

ディフェンスリバウンド

次にディフェンスリバウンドです。こちらは逆に明らかに勝利チームの方が上回っていそうです。相手にセカンド・チャンスを与えず、自分たちのオフェンスにつなげる事が重要だということを物語っていそうです。

シュートの試投数

次に2ポイントと3ポイントの試投数ですが、ほぼ差が見られないとはいえ、敗北チームのほうがやや2ポイントの試投数が多く、勝利チームの方がやや3ポイントの試投数が多いようです。このあたりは、日本がフォーカスしていた3ポイントの重要性を示しているのかもしれません。

Four Factors

最後にFour Factorsについてまとめて見てみます。相関分析でもわかっていましたが、やはりeFG%で差がついているのが見て取れます(勝利チームの中央値は約60%、敗北チームの中央値は約50%)。

また、オフェンスリバウンドの実績値で見た際にはそんなに差がついていませんでしたが、相手チームとの相対感を示すOREB%では差が生じています。オフェンスリバウンドが多い = シュートをよく外しているという場合もありうるので、実績値で見ると差がつかない場合もあるということかもしれません。反対に、相手にディフェンスリバウンドを取らせず、自分たちがオフェンスリバウンドを取れているか?という指標であるOREB%だと差がよく出てくるということで、この辺がFour Factorsがゲームの分析に優れた指標であると言われている部分なのかなと思いました。

まとめ

比較的単純な相関分析・可視化を通して勝敗との相関が高い/低いスタッツや、勝敗チーム間のスタッツの違いを見てきました。ミスしない・シュートを効率よく決める・リバウンドを制するといったごく当たり前のことを出来たチームが勝っているという、ある種当たり前のことではありますが、定量的な傾向として見て取ることが出来ました。

また、Four Factorsを見ることで、より先鋭的にゲームを評価できそうな部分も見られました。

次回以降は、もうすこし機械学習や統計的な分析手法を使って深掘り分析した結果をまとめていきます。