FIBA Basketball World Cup 2023をデータ分析してみた(Part.5|因子分析+k-meansクラスタリング)
本記事は、全5回に分けてFIBAバスケットボールワールドカップ2023の全92試合を分析してみたシリーズ第5回目です。
- 相関分析・可視化分析|勝敗と相関が高いスタッツの項目は何か?勝利したチームと敗北したチームのスタッツの差はどう違うのか?
- 決定木分析|勝利するチームのスタッツの条件は?(その1)
- LightGBM+SHAP分析|勝利するチームのスタッツの条件は?(その2)
- 次元削減分析(主成分分析・t-SNE・UMAP)|スタッツから見る大会参加チームの特徴は?(その1)
- 因子分析|スタッツから見る大会参加チームの特徴は?(その2)
前回は、主成分分析・t-SNE・UMAP等の手法から次元削減を行い、各チームを2次元にプロットしてみました。
今回は因子分析を行うことで、各チームの特徴をスコア化し、更にクラスタリングしてみることで、大会の参加チームの特徴がうまく分かれそうか?そして、日本はどのチームに似ていたのかを分析してみたいと思います。 前回の次元削減分析と因子分析はパッと見は似ていますが、ざっくりと以下のような違いがあります。雑ではありますが、計算の方向性が逆になっていると理解すると良いと思います。
- 次元削減分析|スタッツをいくつかのスコアに束ねる手法
- 因子分析|スタッツは、その背後にある因子が表面化したものであると捉え、スタッツから各因子を逆算する手法
※なお、クラスタリング自体は単体で行うことも、次元削減分析と組み合わせて行うこともできます。
目次は次の通りです。
データの準備・前処理
前回同様、大会公式サイトのデータから、因子分析用に前処理しておきます。
summary_team_df.head()
TEAM | OREB | DREB | REB | AST | PF | TO | ST | BLK | FG_MADE | ... | DREB_OPPOSITE | eFG% | TO% | FTR | OREB% | 2PTS% | 3PTS% | FT% | RANK | TEAM_LABEL | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | ドイツ | -0.357255 | 0.654719 | 0.281435 | 0.767749 | -0.728020 | -1.242350 | 0.747780 | -0.651354 | 1.302677 | ... | -1.282535 | 1.265120 | -1.173782 | -0.376622 | 0.353542 | 1.389546 | 0.742347 | 0.727326 | 1 | 1_ドイツ |
1 | セルビア | -1.111176 | 0.213180 | -0.522665 | 1.171533 | -1.289234 | -1.378872 | 1.539473 | 0.057603 | 1.384190 | ... | -1.779762 | 1.705373 | -1.193995 | 0.922567 | -0.148243 | 2.132540 | 0.676955 | 0.596071 | 2 | 2_セルビア |
2 | カナダ | 0.739356 | 0.102796 | 0.535361 | 0.678020 | -0.104448 | -0.969306 | 0.351933 | -0.415035 | 1.343433 | ... | -0.835031 | 1.201705 | -1.224908 | 0.878006 | 1.096495 | 0.787233 | 1.174016 | 0.484411 | 3 | 3_カナダ |
3 | アメリカ合衆国 | -0.288717 | 2.200104 | 1.508746 | 1.261262 | -0.915091 | 0.395914 | 1.440512 | 2.420790 | 2.362344 | ... | -0.735585 | 1.481867 | -0.190144 | 1.239070 | 0.116469 | 1.307604 | 1.113105 | 0.795034 | 4 | 4_アメリカ合衆国 |
4 | ラトビア | -1.590943 | -0.228358 | -1.157481 | 1.440722 | 0.207337 | -1.651916 | -0.736646 | -0.060557 | 1.139651 | ... | -0.835031 | 1.693414 | -1.281602 | -1.751610 | -1.183893 | 1.218891 | 1.526417 | -0.100256 | 5 | 5_ラトビア |
5 rows × 27 columns
因子分析
それでは、実際に因子分析を行っていきます。因子分析用のライブラリをインポートしておきます。
poetry add factor_analyzer
因子数の決定
まずは因子数を仮置きしつつ、スクリープロットを描いて固有値を確認します。ガットマン基準より、固有値が1以上である数を因子数とします。 なお、回転法はプロマックス回転、母数推定には最尤法を指定しておきます。
from factor_analyzer import FactorAnalyzer feature_cols = [ 'OREB', 'DREB', 'AST', 'PF', 'TO', 'ST', 'BLK', '2PTS_ATTEMPT', '3PTS_ATTEMPT', 'FT_ATTEMPT', 'eFG%', 'TO%', 'FTR', 'OREB%', '2PTS%', '3PTS%', 'FT%', ]
N = 2 # 仮置き X = summary_team_df[feature_cols] embedded_cols = [f"embedded_{i+1}" for i in range(N)] # 因子分析の実行 fa = FactorAnalyzer(n_factors=N, rotation="promax", method="ml") fa.fit(X) # スクリープロットによる因子数の確認(ガットマン基準の場合、固有値1以上の因子を採用) eigen_values = fa.get_eigenvalues() eigen_x = list(range(1, len(eigen_values[0])+1)) print("スクリープロット") plt.figure(figsize=(12, 8)) plt.plot(eigen_x, eigen_values[0], marker="o") plt.hlines(y=1, xmin=0, xmax=20, linestyles="dashed") plt.ylabel("固有値") plt.title("スクリープロット") plt.show()
因子数は6因子を想定することにします。
因子負荷量の確認
それでは、6因子を仮定して、因子分析を進めていきます。まずは因子負荷量を確認し、各因子がどのような因子なのかを解釈していきます。
N = 6 # 因子分析の実行 fa = FactorAnalyzer(n_factors=N, rotation="promax", method="ml") fa.fit(X) # 因子負荷量の確認 loading_df = pd.DataFrame(fa.loadings_, columns=embedded_cols, index=feature_cols) plt.figure(figsize=(12, 8)) sns.heatmap(loading_df, cmap="coolwarm", annot=True, fmt=".2f") plt.title("因子負荷量") plt.show()
絶対値が比較的大きいスタッツを見比べてみると、各因子は次のような意味を持っているようです。
- embedded_1|この因子が高いとTOが増える。TOのしやすさ
- embedded_2|この因子が高いとDREB・BLK・2PTS%が高くなる。また、PFが少なくなる。インサイドの強さ(高さ?)
- embedded_3|この因子が高いとFTが増える。FTの多さ
- embedded_4|この因子が高いとAST・eFG%・3PTS%が高くなる。シュート効率の良さ
- embedded_5|この因子が高いとOREBが増える。OREBのアグレッシブさ
- embedded_6|この因子が高いと2PTS_ATTEMPTが増え、2PTS%が小さくなる。2PTS偏重
寄与率の確認
各因子の寄与率も見ておきます。
# 因子寄与・因子寄与率・累積寄与率の確認 variance_df = pd.DataFrame(fa.get_factor_variance(), index=["因子寄与", "因子寄与率", "累積寄与率"], columns=embedded_cols) print("因子の寄与") display(variance_df)
embedded_1 | embedded_2 | embedded_3 | embedded_4 | embedded_5 | embedded_6 | |
---|---|---|---|---|---|---|
因子寄与 | 2.333270 | 2.329368 | 2.206963 | 2.155444 | 1.918768 | 1.009655 |
因子寄与率 | 0.137251 | 0.137022 | 0.129821 | 0.126791 | 0.112869 | 0.059391 |
累積寄与率 | 0.137251 | 0.274273 | 0.404094 | 0.530885 | 0.643754 | 0.703145 |
6因子で元のスタッツの分散の約7割を説明できているようです。
因子得点の確認
それでは、各チームの因子得点を取得します。この得点を元に、クラスタリングにかけていきます。
# 因子得点の確認 score_df = pd.DataFrame(fa.transform(X), columns=embedded_cols, index=label_txt) print("因子得点") display(score_df)
embedded_1 | embedded_2 | embedded_3 | embedded_4 | embedded_5 | embedded_6 | |
---|---|---|---|---|---|---|
1_ドイツ | -1.300328 | 1.179297 | -0.244862 | 0.934883 | 0.062245 | -0.555002 |
2_セルビア | -1.372017 | 1.672746 | 0.926903 | 1.157072 | -0.625907 | -1.059549 |
3_カナダ | -1.262478 | 0.699717 | 1.095309 | 1.279031 | 1.028844 | 0.151513 |
4_アメリカ合衆国 | 0.126095 | 1.976963 | 1.630710 | 1.190871 | -0.092600 | 1.740469 |
5_ラトビア | -1.518292 | 0.978369 | -1.650575 | 1.576361 | -1.171244 | -1.346630 |
6_リトアニア | -0.586678 | 0.313404 | -0.603971 | 1.413674 | 0.967860 | -0.782937 |
7_スロベニア | -0.198929 | 0.079474 | 1.652097 | 0.567348 | 0.190069 | -1.185422 |
8_イタリア | -0.696013 | -0.022033 | -0.969312 | -0.908474 | 0.382515 | 0.201561 |
9_スペイン | -0.262429 | 0.187576 | 0.731657 | 0.665695 | 1.153473 | -1.247843 |
10_オーストラリア | -0.727880 | 1.143794 | -0.344179 | 0.560399 | 0.050008 | 0.692050 |
11_モンテネグロ | -0.058929 | -0.084426 | -0.560019 | -1.193360 | 1.423290 | 0.881684 |
12_プエルトリコ | 0.336869 | -0.224440 | 0.255571 | 0.553201 | 0.560770 | 1.336374 |
13_ブラジル | -0.672562 | -0.034382 | 0.173693 | -0.153772 | 0.611894 | -0.775313 |
14_ドミニカ共和国 | 0.263081 | -0.395316 | 1.087932 | -0.281374 | 0.038062 | 0.730990 |
15_ギリシャ | -2.009215 | -0.669822 | -0.929280 | -0.335854 | -0.418754 | -0.328830 |
16_ジョージア | 2.026440 | -0.117894 | 0.085884 | -0.946016 | -0.714204 | -0.143693 |
17_南スーダン | -0.997016 | -0.194253 | 0.721846 | 1.198105 | -0.699037 | -0.061825 |
18_フランス | 1.409952 | 1.476136 | 0.072627 | 0.247088 | 0.818498 | -1.355281 |
19_日本 | 0.440371 | 0.691697 | 0.025749 | -0.502517 | -1.569685 | 0.496960 |
20_エジプト | 1.270005 | 1.105493 | 0.038276 | -0.730704 | 0.257615 | 1.372383 |
21_フィンランド | 0.877145 | 0.869527 | -1.176184 | 0.181314 | 0.793089 | -0.368637 |
22_ニュージーランド | 1.515544 | -1.078821 | 1.044124 | 0.679003 | 0.057723 | 0.652533 |
23_レバノン | 1.011710 | -0.440044 | -0.682162 | 0.759718 | -0.776703 | -1.314634 |
24_フィリピン | 0.716270 | 0.270888 | -0.226830 | -0.901639 | 0.086573 | 1.068112 |
25_メキシコ | 0.348967 | -0.738434 | -0.253861 | 0.388717 | -1.425465 | 0.286682 |
26_アンゴラ | -0.927790 | -1.298743 | 1.633542 | -2.649551 | 2.652439 | 1.937343 |
27_コートジボワール | -0.396090 | -2.066069 | -0.838843 | -0.199135 | -1.093058 | -0.474282 |
28_カーボベルデ | 0.556580 | -0.601142 | -0.107800 | -1.750730 | 1.098031 | 1.293462 |
29_中国 | 0.407826 | -0.535705 | 0.668903 | -0.338006 | -2.390146 | -1.115700 |
30_ベネズエラ | 0.112124 | -0.796972 | -2.586102 | -0.156477 | -0.036876 | -0.534536 |
31_イラン | -0.195358 | -2.133336 | -1.545377 | -1.431229 | -0.239887 | -0.737360 |
32_ヨルダン | 1.763028 | -1.213248 | 0.874536 | -0.873640 | -0.979431 | 0.545358 |
クラスタリング
それではクラスタリングを行っていきます。今回はk-means++という手法を用いていきます。まず最初に、クラスタ数を決める必要があるため、エルボー法とシルエット法の2つの手法から、適切なクラスタ数を判別していきます。
エルボー法の場合は折れ線グラフがガクッと落ちこむあたりをクラスタ数として設定します。また、シルエット法の場合は、各クラスタのシルエットが綺麗に分かれて描画されたクラスタ数を採用します。
# クラスタリング用ライブラリ from sklearn.cluster import KMeans from sklearn.metrics import silhouette_samples # シルエット法の描画用関数 def show_silhouette(X, fitted_model): cluster_labels = np.unique(fitted_model.labels_) num_clusters = cluster_labels.shape[0] silhouette_vals = silhouette_samples(X, fitted_model.labels_) # シルエット係数の計算 # 可視化 y_ax_lower, y_ax_upper = 0, 0 y_ticks = [] for idx, cls in enumerate(cluster_labels): cls_silhouette_vals = silhouette_vals[fitted_model.labels_==cls] cls_silhouette_vals.sort() y_ax_upper += len(cls_silhouette_vals) cmap = cm.get_cmap("Spectral") rgba = list(cmap(idx/num_clusters)) # rgbaの配列 rgba[-1] = 0.7 # alpha値を0.7にする plt.barh( y=range(y_ax_lower, y_ax_upper), width=cls_silhouette_vals, height=1.0, edgecolor='none', color=rgba) y_ticks.append((y_ax_lower + y_ax_upper) / 2.0) y_ax_lower += len(cls_silhouette_vals) silhouette_avg = np.mean(silhouette_vals) plt.axvline(silhouette_avg, color='orangered', linestyle='--') plt.xlabel('silhouette coefficient') plt.ylabel('cluster') plt.yticks(y_ticks, cluster_labels + 1) plt.show() # 2~10のクラスタ数でクラスタリング sse_list = [] # エルボー法用 min_n = 2 max_n = 10 for i in range(min_n, max_n): model = KMeans(n_clusters=i, random_state=0, init="k-means++") model.fit(score_df) sse_list.append(model.inertia_) show_silhouette(score_df.to_numpy(), model) # シルエット法 plt.plot(range(min_n, max_n), sse_list, marker="o") plt.xlabel("number of cluseters") plt.ylabel("sum of squared errors") plt.title("エルボー法") plt.show()
まず、エルボー法から見てみるとなんとも言えない結果となりました。
次に、シルエット法です。一部結果を抜粋しています。こちらもなんとも言えない結果ではありますが、クラスタ数3が一番きれいにシルエットが出ているため、今回はクラスタ数3を採用することにします。
さて、それでは実際にクラスタリングを行い、結果を解釈していきます。
# クラスタ数3に設定 model = KMeans(n_clusters=3, random_state=0, init="k-means++") model.fit(score_df) score_df["cluster"] = model.predict(score_df) score_df["cluster_str"] = score_df["cluster"].astype("str") # 箱ひげ図のプロット用にクラスタの値を文字列化 # クラスタごとの因子得点の分布を確認 n_rows = 2 n_cols = 3 fig, axes = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(10, 10), tight_layout=True) for idx, col in enumerate(embedded_cols): i = idx // n_cols j = idx % n_cols sns.boxplot(x=col, y="cluster_str", data=score_df, ax=axes[i, j]) axes[i, j].set_title(f"{col} x クラスタ") plt.show() score_df = score_df.drop("cluster_str", axis=1) # 以後不要なため、削除
ここで、先程解釈した各因子の情報を再掲しておきます。
- embedded_1|この因子が高いとTOが増える。TOのしやすさ
- embedded_2|この因子が高いとDREB・BLK・2PTS%が高くなる。また、PFが少なくなる。インサイドの強さ(高さ?)
- embedded_3|この因子が高いとFTが増える。FTの多さ
- embedded_4|この因子が高いとAST・eFG%・3PTS%が高くなる。シュート効率の良さ
- embedded_5|この因子が高いとOREBが増える。OREBのアグレッシブさ
- embedded_6|この因子が高いと2PTS_ATTEMPTが増え、2PTS%が小さくなる。2PTS偏重
こうして各クラスタごとに因子の違いを見てみると、
- クラスタ0|embedded_1(TOの多さ)とembedded_6(2PTS偏重)が高く、embedded_4(シュート効率の良さ)が低いです。ミスが多く、2PTS偏重でシュートの効率が悪いということで、うまく得点ができずに苦しんだチーム(≒下位チーム)が含まれているであろうと解釈できそうです。
- クラスタ1|基本的にはどの因子でも平均的な値を残していそうです。強いて言うなら、embedded_2(インサイドの強さ?)とembedded_3(FTの多さ)がやや低いので、インサイドで苦労し、FTでの得点が少なかったチームが多そうです。
- クラスタ2|クラスタ2はembedded_4(シュート効率の良さ)が高く、しっかりと得点を稼いだ上位チームが入っていそうです。
それでは、上記の情報を踏まえ、具体的にどのチームがどのクラスタだったかを因子得点と共に見ていきます。
# クラスタ数と因子得点 score_df = score_df.sort_values(by="cluster") plt.figure(figsize=(12, 8)) sns.heatmap(score_df, cmap="coolwarm", annot=True, fmt=".2f") plt.title("クラスタ別因子得点") plt.show()
事前の解釈通り、クラスタ0は下位リーグに回ったチームが多く、クラスタ2が上位チーム、クラスタ1がその中間という結果になりました。
今回の分析結果としては、日本はクラスタ0に分類されていますが、ヒートマップの色を見てもクラスタ0の中では少し特殊な因子得点となっている(embedded_2が高く、embedded_5が小さい等)ため、クラスタ0ど真ん中という感じではなさそうです。
まとめ
因子分析からクラスタリングまでの一連の流れを通して、参加チームの特徴を分析しました。やはり、一貫してミスが少なく、効率の良いシュートを打てているチームが上位チームに入っているということが、改めて今回の分析からもあらわになりました。
今回でFIBA Basketball World Cup 2023の分析は一段落しました。今後は、他のアドバンスドスタッツも分析対象に含める、他の大会・リーグでも分析してみるなど気が向いたら色々とやっていきたいと思います。