3
1

NBAのスタッツ分析~主成分分析による可視化~

Posted at

NBAのスタッツを主成分分析で分析してみる

概要

今回は、NBAの2023-2024のシーズンの選手のスタッツの結果を使って、主成分分析を行った。この記事では、主成分分析を数学的に考えるのではなく、実践的に主成分分析を用いて分析することに力を入れている。
主成分分析の数学的な意味について詳しく理解したい方は、以下のような記事で説明されているので参考にしてください。

方法 ~ Google colabを用いて主成分分析する~ 

主成分分析などの分析は、PythonやRを用いて行われることが多い。今回は、環境構築不要な、GoogleColabで行いました。※Pythonを用いて主成分分析をする場合、Jupyter Notebook形式での分析をおすすめします。

GoogleColabの使い方はこちらの生地を参考に

データの取得方法

以下のサイトより、2023-2024シーズンの36分あたりの選手ごとのスタッツのデータを取得した。

以下の写真のHide Partical Rowsを押して、シーズン中にトレードされた選手などは、トータルの結果を使う。
image.png

以下のように、csvファイルをexportしてダウンロードする。
image.png

nba_player_stats.csvという名前で保存した。

データの変数は以下のようになっている。

  • Rk: ランキング(選手名の名前順)
  • Player: 選手名
  • Pos: ポジション
  • Age: 年齢
  • Tm: チーム
  • G: 出場試合数
  • GS: 先発出場試合数
  • MP: 出場時間(分)
  • FG: フィールドゴール成功数
  • FGA: フィールドゴール試投数
  • FG%: フィールドゴール成功率
  • 3P: 3ポイントシュート成功数
  • 3PA: 3ポイントシュート試投数
  • 3P%: 3ポイントシュート成功率
  • 2P: 2ポイントシュート成功数
  • 2PA: 2ポイントシュート試投数
  • 2P%: 2ポイントシュート成功率
  • FT: フリースロー成功数
  • FTA: フリースロー試投数
  • FT%: フリースロー成功率
  • ORB: オフェンシブリバウンド数
  • DRB: ディフェンシブリバウンド数
  • TRB: トータルリバウンド数
  • AST: アシスト数
  • STL: スティール数
  • BLK: ブロック数
  • TOV: ターンオーバー数
  • PF: パーソナルファウル数
  • PTS: 得点
  • Player-additional: 選手の追加情報? 特に意味はない

ファイルを読み込んでまずは前処理

pandsを用いて、csvファイルを読みこむ

import pandas as pd

nba_stats = pd.read_csv('your_file_pass/nba_player_stats.csv')
print(nba_stats)

選手の中には、プレイタイムが少ない選手もいる。

今回は、ある程度試合に出てくる主要な選手のみデータを使うことにした。
具体的に、出場試合数が、全試合の半分の65試合以上(シーズンMVPやオールNBAチーム選出のため必要な試合数)出場している選手のみを使う。

nba_stats = nba_stats[(nba_stats['G'] >= 65) ]
#indexを貼りなおす
nba_stats = nba_stats.reset_index(drop=True)
#新ためてデータを確認
print(nba_stats)

リーポイントシュートを一本も打ってなくて、3P%が欠損の人がいる この選手たちの3P%は0とする。
ちなみに、65試合以上出場でて、スリーポイントの試投数の選手は以下の2選手でした。
・Daniel Gafford
・Ivica Zubac

#欠損値を0で補完
nba_stats = nba_stats.fillna(0)

主成分分析に使わないデータを外す。('FGA','3PA','2PA',"FTA",'TRB'は成功数と確率から導られるため、使わないようにする。)

data_for_pca = nba_stats.drop(['Rk','Player','Pos', 'Age','G','GS', 'MP','Tm', 'FGA','3PA','2PA',"FTA",'TRB','Player-additional'], axis=1)

前処理ができたので、主成分分析を開始する。

後にposition別、選手名を入れて可視化を行いやすいために、主成分得点のデータと、選手名とポジションが入ったデータも結合している。

from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt

# データの標準化
scaler = StandardScaler()
scaled_data = scaler.fit_transform(data_for_pca)

# PCAの実行
pca = PCA(n_components=2)  # 主成分を2つに設定
principal_components = pca.fit_transform(scaled_data)

# 主成分得点をデータフレームに追加
pca_df = pd.DataFrame(data=principal_components, columns=['PC1', 'PC2'])
pca_df['Player'] = nba_stats['Player']
pca_df['Pos'] = nba_stats['Pos']

# ポジションごとの色設定
positions = pca_df['Pos'].unique()
colors = plt.cm.get_cmap('tab10', len(positions))

# 結果の確認
print(pca_df)

後で全て可視化を行うが、ちなみに選手ごとの主成分得点はこのように計算できている。

image.png

次に、まずはこの第二主成分までで、どれだけデータを表せているかを確認するために、累積寄与率をみてみる。

import numpy as np
# 累積寄与率の計算
explained_variance = pca.explained_variance_ratio_
cumulative_explained_variance = np.cumsum(explained_variance)

# 累積寄与率の表示
print('累積寄与率:')
for i, value in enumerate(cumulative_explained_variance):
  print(f'主成分{i+1}: {value*100:.2f}%')

以下のように出力される。
累積寄与率:
主成分1: 33.28%
主成分2: 60.30%

ここで、第二主成分まで、だいたいデータの6割ほどあわされていることがわかる。一般的に70~80%までの主成分を用いるべきと言われているが、今回は変数の数も多く、あくまでも、可視化をすることを目的に、主成分分析を行っているため、第二主成分までの累積寄与率が60%でたということは、意味のある可視化であるだろうということが予測できる。

結果の可視化を行う

次に、負荷量のプロットを行う。

# 変量プロットのベクトルを追加
loadings = pca.components_.T * np.sqrt(pca.explained_variance_)

for i, feature in enumerate(['FG', 'FG%', '3P', '3P%', '2P', '2P%', 'FT', 'FT%', 'ORB', 'DRB', 'AST',
       'STL', 'BLK', 'TOV', 'PF', 'PTS']):
    plt.arrow(0, 0, loadings[i, 0], loadings[i, 1], color='r', alpha=0.5, head_width=0.05, head_length=0.1)
    plt.text(loadings[i, 0] * 1.15, loadings[i, 1] * 1.15, feature, color='r', ha='center', va='center')

plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA of NBA Player Stats (Players with >= 65 Games)')
plt.grid(True)
plt.show()

結合は以下のようになる。
image.png

これを似た変数を三つに分けてみる。

image.png

1番で囲ったグループには以下のような特徴が見える。
・3P,3P%,FT%に相関があり、これは言わるゆるシュート力(正確性)があるようなベクトルであるということがわかる。

2番のグループには以下ような特徴が見える。
・FT、FG,2Pなどが多いことから、単純に試合で良くシュートを決めていて、PTS(点数)をよくとっているようなベクトルであることが分かる。また、その分そのような選手たちは、ボールをよく保持していることが多く、ターンオーバーの数(ボールを相手に失う)やアシスト数が多くなっていることがなっていると推測できる。

3番のグループには以下のような特徴が見える。
・リバウンドの数(シュートが外れたのを取る数)や、ブロックの数が多く、いわゆるセンター(基本的にはゴール下にることが多い)的なグループなのだろうと推測できる。
・また、ゴール下でのシュートが多いため、比較的に、2P%やFG%も高く、ゴール下での激しいディフェンスによるPF(ファール)も多いのだろうと推測できる。

これを踏まえて、選手の主成分得点をみてみる
まずは、ポジションごとにみてみる。

import matplotlib.pyplot as plt

# 2Dプロットの設定
plt.figure(figsize=(14, 10))

for i, position in enumerate(positions):
    pos_data = pca_df[pca_df['Pos'] == position]
    plt.scatter(pos_data['PC1'], pos_data['PC2'], label=position, color=colors(i), s=50, alpha=0.6, edgecolors='w', linewidth=0.5)

plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA of NBA Player Stats (Players with >= 65 Games)')
plt.legend()
plt.grid(True)
plt.show()

結果は以下のようになる。

image.png

次に、選手の名前も表示してみる。

# 2Dプロットの設定
plt.figure(figsize=(20, 15))

for i, position in enumerate(positions):
    pos_data = pca_df[pca_df['Pos'] == position]
    plt.scatter(pos_data['PC1'], pos_data['PC2'], label=position, color=colors(i), s=50, alpha=0.6, edgecolors='w', linewidth=0.5)

# 全選手の名前を表示
for i, player in pca_df.iterrows():
    plt.annotate(player['Player'], (player['PC1'], player['PC2']), fontsize=8, alpha=0.7)

plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA of NBA Player Stats (Players with >= 65 Games)')
plt.legend()
plt.grid(True)
plt.show()

どうしても、選手が固まっている所は見にくい

image.png

また以下のように三つのグループにわけてみた。

image.png

実際に自分の知っている範囲で選手の特徴をみることができた。

1番のグループ:優秀な3P シューター
2番のグループ:いわゆるスター選手、やはり得点を沢山とっている選手がここに多い
3番のグループ:強いセンター

また、実際にこのプロットで近い所にいる選手は、似たような特徴をもった選手が多いこともわかる。

ある特定の選手がどこに位置しているかを知りたければ以下のように名前を指定して見ることができる。
ここでは、日本を代表する八村塁がどこにいるか確認する。

# 2Dプロットの設定
plt.figure(figsize=(14, 10))

for i, position in enumerate(positions):
    pos_data = pca_df[pca_df['Pos'] == position]
    plt.scatter(pos_data['PC1'], pos_data['PC2'], label=position, color=colors(i), s=50, alpha=0.6, edgecolors='w', linewidth=0.5)

# 八村累の名前を表示
for i, player in pca_df.iterrows():
    if player['Player'] == 'Rui Hachimura':
      plt.annotate(player['Player'], (player['PC1'], player['PC2']), fontsize=8, alpha=0.7)

plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('PCA of NBA Player Stats (Players with >= 65 Games)')
plt.legend()
plt.grid(True)
plt.show()

結果はちょうど第一主成分、第二主成分ともに、0に近い値になっている。
この65試合以上出場した選手のなかでは、どのスタッツに対しても、平均だということだろう。このような結果になったのは、3Pも優秀で、リバウンドなどもそこそことっているかもしれない。
個人的には、来シーズンは、スター選手のようなスタッツを残してほしい。
image.png

感想

このような、分析をしてみて、私自身は以下のような点が面白いと感じた。
選手のスタッツ全体の特徴を一目でわかる。また、スター選手などがほかの選択と比べてどのように、スタッツ的に外れているかを可視化できて楽しいと思った。
2P(2Pシュート成功数)と2P%, FG(フィールドゴール成功数)とFG%は、似ている変数なのかなと思っていたが、実際には違う結果が分かり、それを自分でなぜこのような結果が得られたのかと考えてみることが面白かった。
このように、大量のデータをただ眺めていても分からないようなことが、主成分分析などの分析によりわかるようになることがこの主成分分析の魅力だなと感じた。また、自分の興味があることについて分析してみたいな思った。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1