FIBA Basketball World Cup 2023をデータ分析してみた(Part.1|相関分析・可視化分析)
日本男子代表がアジア1位になり、パリ五輪出場権を獲得したFIBAバスケットボールワールドカップ2023。代表の活躍に胸を踊らせている中、次のツイートに触発され、自分も全92試合についてデータ分析してみたのでその備忘です。
📊バスケW杯日本代表アドバンスドスタッツ
— 柳鳥 亮@データスタジアム (@ds_yanadori) 2023年9月5日
eFG%: 52.0%
相手eFG%: 51.3%
視界がにじみます。ついに日本がeFG%で相手を上回る時代が!五輪では約10%下回っていました。
昨季B1でCS進出8チーム中7チームがeFG%で相手を上回っていました。強者の数字!@chrisnewtokyoさん、後押しありがとうございます! https://t.co/xYEfO6qXE6
私の場合は日本にフォーカスするのではなく、全チームを比較した場合に、強いチームはどんなスタッツを残していたのかについて分析してみました。色々と分析してみたら長くなったので、全5回に分けていきます。 今回は第1回目です。
- 相関分析・可視化分析|勝敗と相関が高いスタッツの項目は何か?勝利したチームと敗北したチームのスタッツの差はどう違うのか?
- 決定木分析|勝利するチームのスタッツの条件は?(その1)
- LightGBM+SHAP分析|勝利するチームのスタッツの条件は?(その2)
- 次元削減分析(主成分分析・t-SNE・UMAP)|スタッツから見る大会参加チームの特徴は?(その1)
- 因子分析|スタッツから見る大会参加チームの特徴は?(その2)
目次は次の通りです。
データの準備
まずは、各チームのスタッツを大会公式サイトから取得してきます。取得する際には、以下の記事でまとめたスクレイピング方法を使いました。なお、公式サイトの規約上、データの複製・頒布及びそれに準じる行為は認められていないようなので、ソースコード及び取得データ自体の公開は控えておきます。
取得した結果は、以下のようなものとなります。どの試合で、各チームのどの選手がどんなスタッツを残したのかというデータです。なお、今後分析がしやすいように、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
を見ると、いわゆるミスのTO
・TO%
や自分たちにペナルティを与えられる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を見ることで、より先鋭的にゲームを評価できそうな部分も見られました。
次回以降は、もうすこし機械学習や統計的な分析手法を使って深掘り分析した結果をまとめていきます。