こんにちは。
父ラボ運営者のMAMEです。
プログラミング能力0から、機械学習、AI、データサイエンスをPythonで学び始めて3ヶ月。
いよいよ、データ分析スキルをどこまで高めることが出来たか確認する最終課題に取り組んでいます。

テーマは自由なので何に取り組むべきか悩むこと数時間。やはり興味のあることにしたいということで、ここ数年改めて見始めている全米プロバスケットボールリーグ(NBA)のデータ分析に取り組むことにしました。折しも日本人初のドラフト1巡目指名で話題の八村塁(はちむらるい)選手で日本のバスケット熱も盛り上がっています。下の写真もかっこいいですよね〜!

八村塁選手の画像日本代表での八村塁選手(出展:wikipedia)

データ分析の目的とパラメータの検討

またデータ分析するにあたって、まずは目的を明らかにしたいと思います。
そこは、八村選手のような新人バスケットボールプレイヤーがどのようにしたらNBAで成功できるか?を明らかにして提言することにしようと考えました。

  • 成功の定義=優勝回数や人気など色々とあるのでしょうが、簡単に数値化できそうなサラリー(給料)の高さで定義します。
  • 分析パラメータ=バスケットの成績、所属チーム、ポジション、SNSでの人気度など

こういった分析を行うためのデータを準備する必要があります。
kaggleでは様々なプロジェクトが行われており、それに伴うデータセットも存在しています。
NBAに関するデータも次のようなものがあります。

https://www.kaggle.com/drgilermo/nba-players-stats
https://www.kaggle.com/data/52669
https://www.kaggle.com/justinas/nba-players-data

Kaggleでアカウントを作成すればこれらのデータセットを使って分析することが可能となります。
アカウント作成し、データをダウンロードしてみました。

まずはデータを見てみましょう

NBAのデータセット

Kaggleからダウンロードしたデータをまず確認してみました。
多くの種類のデータがありますが、今回必要なデータは「バスケットの成績、所属チーム、ポジション、SNSでの人気度など」が入ったデータ、もしくはそれぞれのデータが入っているデータを結合したデータです。できればひとまとめになっていると楽です。。。)

これらをざっと確認したのち今回は、https://www.kaggle.com/noahgift/social-power-nbaより入手したnba_2017_players_with_salary_wiki_twitter.csvを分析データとして用いることにしました。

まずは諸々必要そうなライブラリと、データを読み込み、データを訓練データとテストデータにわけてみます。訓練データ70%、テストデータ30%です。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
%matplotlib inline

nbadf = pd.read_csv('./nba_2017_players_with_salary_wiki_twitter2.csv')
train_df, test_df = train_test_split(nbadf, train_size=0.7, random_state=1)

次にtrain_df, test_dfの全ての特徴量について、infoメソッドを利用して欠損値の有無を確認します。
欠損データがあれば補完しておきます。結果が長いので省略。

train_df.info()
test_df.info()

どの特徴量がサラリー(SALARY_MILLIONS)に影響を与えるかを調査

まず所属チームごとのサラリー平均に差があるかを調査します。データが全チーム、全選手というわけではないので偏りが出そうですが、所属するチームによってサラリーが良い、悪いというのは出てきうることなのかなと。

train_df[["TEAM", "SALARY_MILLIONS"]].groupby(["TEAM"], as_index=False).mean().sort_values(by="SALARY_MILLIONS", ascending=False)

結果としては次のようにチームにより差はありそうです。
CLE(クリーブランドキャバリアーズ)、GS(ゴールデンステートウォーリアーズ)の2チームはこのデータの当時東西のトップチームでしたので、そこに所属している選手のサラリーがよかったということでしょうか。
4位のWSH(ワシントンウィザーズ)はなんと八村選手が所属するチーム。八村選手なかなかよいのでは?

次のポジションごとにサラリーの差があるかを見てみましょう。
バスケットボールにはセンター(C)、パワーフォワード(PF)、スモールフォワード(SF)、シューティングガード(SG)、ポイントガード(PG)といったポジションがあります。この辺詳しく知りたい方は「スラムダンク」をご覧ください。

train_df[["POSITION", "SALARY_MILLIONS"]].groupby(["POSITION"], as_index=False).mean().sort_values(by="SALARY_MILLIONS", ascending=False)

データは十分ではないかもしれないが、ポジションによる差もありそうですね。
ちなみに八村選手はパワーフォワード(PF)、スモールフォワード(SF)あたりのポジションで出場していることが多そうです。
スモールフォワード(SF)おすすめです。

続いて年齢について。何歳くらいにピークがあるといいのでしょうかね?

#年齢のヒストグラム
train_df["AGE"].hist(bins=20) # 範囲(階級)の細かさは20
plt.show()

25才くらいでピークがあるようです。意外と若いですね。

これまでのようにひとつひとつ見ていってもよいのですが、ヒートマップ(関連図)を作成し、どのパラメータが関連性があるかを見る方法もあるということを教わりました。

import seaborn as sns
sns.set()
fig, ax = plt.subplots(figsize=(12, 9)) 
sns.heatmap(train_df.corr())
plt.title("Correlation heatmap (training set)")

結果下図のように、年齢、得点に関するパラメータ、出場時間、出場ゲームでの勝率、ページビュー(Wikipediaの)がサラリーに影響しそうです。


そこで、年齢、出場ゲームでの勝率とサラリーの関係を調査してみます。

sns.scatterplot(x="AGE", y="SALARY_MILLIONS", hue="WINS_RPM", data=train_df)

こちらを見ると、年齢は25-30前半までがサラリーが高く、各年代では出場ゲームの勝率の高さもサラリーに影響していそうです。


次は、1試合当たりの出場時間、PIE(Player Impact Estimate)とサラリーの関係を調査してみます。

sns.scatterplot(x="MPG", y="SALARY_MILLIONS", hue="PIE", data=train_df)

1試合当たりの出場時間の長さによりサラリーが高い選手が出現しています。いい選手ほど長い時間出場するということでしょうが、長い時間出場できる選手になるというのも重要なファクターになりそうです。


次は、得点、ターンノーバー数(ボールを失った回数)とサラリーの影響を調査します。

sns.scatterplot(x="POINTS", y="SALARY_MILLIONS", hue="TOV", data=train_df)

得点が高いほどサラリーの高い選手が現れる傾向あります。これは直感的に理解しやすいですね。
一方、ターンノーバー数(ボールを失った回数)の高さもある程度そう感が見られそうです。これは直感的に逆なイメージですが、おそらく多くボールを持つエース級の選手ほどターンノーバー数が高くなるためサラリーの高さと関連しているのかもしれません。


最後にWikipediaのページビュー数とサラリーの影響を調査します。

sns.scatterplot(x="PAGEVIEWS", y="SALARY_MILLIONS", hue="TOV", data=train_df)
plt.xscale('log')
plt.show()

こちらも相関はみられそうです。人気選手ほど高いサラリーということかもしれません。目立つのが吉。

データ確認後にデータの前処理を行う

データセットよりプレーヤー名、通し番号等不要なデータを削除します。

#1. train_dfから、PLAYERとRkの特徴量を削除する
train_df = train_df.drop(["Rk", "PLAYER"], axis=1)
#2. test_dfから、PLAYERとRkの特徴量を削除します。
test_df = test_df.drop(["Rk", "PLAYER"], axis=1)

以下で行う分析は回帰分析なので、 データ内に文字列があると分析できません。
そのため文字データを数値データに変換します

(参考サイト:pandasでカテゴリ変数をダミー変数に変換(get_dummies)

#文字データを数値データに変換する①。
train_df = pd.get_dummies(train_df, columns=['POSITION'])
test_df = pd.get_dummies(test_df, columns=['POSITION'])

# 不要列を削除
train_df = train_df.drop("Unnamed: 0", axis=1)
test_df = test_df.drop("Unnamed: 0", axis=1)

train_df.head()

すると下のように各ポジションに当てはまる選手のところに1が入力され、当てはまらない箇所には0が入力されます。

続いて同様にチーム名の文字列データを数値データに変換します。
こちらも上と同様に.getdummiesを使用して変換したかったのですが、データ数が少なく訓練データとテストデータで出現するチーム数が異なってしまい、変換できませんでした
そこで下のように手動で各チームに数字を設定し、変換しました。
(参考:【Pythonステップアップ!】高階関数mapの便利な使い方
(参考:pandasで欠損値NaNを除外(削除)・置換(穴埋め)・抽出

#文字データを数値データに変換する②。
for dataset in [train_df,test_df]:
    dataset['TEAM'] = dataset['TEAM'].map(lambda x:{'CLE':31, 'POR':30, 'GS':29, 'LAC':28, 'WSH':27, 'DAL':26, 'HOU':25, 'NY':24, 'SA':23, 'MEM':22, 'NO':21, 'ATL':20, 'CHA':19, 'MIL':18, 'OKC':17, 'ORL':16, 'IND':15, 'TOR':14, 'DET':13, 'BOS':12, 'LAL':11, 'CHI':10, 'PHX':9, 'MIN':8, 'PHI':7, 'UTAH':6, 'DEN':5, 'MIA':4, 'BKN':3, 'SAC':2, 'Transferee':1}[x]).astype(int)
    dataset["TEAM"] = dataset["TEAM"].fillna(0)
# 先頭行を抽出
train_df.head()

*この記述がブサイクだったので、最終的にはプログラムのはじめに下記のように記述することとしました。すっきり!!

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
%matplotlib inline

nbadf = pd.read_csv('./nba_2017_players_with_salary_wiki_twitter2.csv')
train_df, test_df = train_test_split(nbadf, train_size=0.7, random_state=1)

nbadf = nbadf.drop(["Rk", "PLAYER"], axis=1)
nbadf = pd.get_dummies(nbadf, columns=['TEAM'])
nbadf = pd.get_dummies(nbadf, columns=['POSITION'])

train_df, test_df = train_test_split(nbadf, train_size=0.7, random_state=1)

train_df = train_df.drop("Unnamed: 0", axis=1)
test_df = test_df.drop("Unnamed: 0", axis=1)

print(train_df.head())

前処理の最後に訓練データとテストデータを完成させます。
これで一旦準備完成です。いや〜ここまで長かった。

#訓練データと検証データを準備する。

# 1. X_trainには、SALARY_MILLIONSを除いたtrain_dfを代入します。
X_train = train_df.drop("SALARY_MILLIONS", axis=1)
# 2. Y_trainには、SALARY_MILLIONSのみが入ったtrain_dfを代入します。
Y_train = train_df["SALARY_MILLIONS"]

# 1. X_testには、SALARY_MILLIONSを除いたtest_dfを代入します。
X_test = test_df.drop("SALARY_MILLIONS", axis=1)
# 2. Y_testには、SALARY_MILLIONSのみが入ったtest_dfを代入します。
Y_test = test_df["SALARY_MILLIONS"]

いよいよデータ分析を行います


データ分析は、Pythonの機械学習ライブラリ「scikit-learn(サイキットラーン)」を用いて行いました。scikit-learnについては下記サイトをご参考ください。
これを使うことで、私のような素人もほぼ一瞬で「機械学習やってます」と言うことができます。
機械学習やってます。えっへん。

機械学習のライブラリ!scikit-learnとは【初心者向け】
scikit-learn(sklearn)の使い方

データ分析は「回帰」で行うか?「分類」で行うか?

データ分析も大きく分けると「回帰」と「分類」に分かれるかと思います
乱暴に説明すると「回帰」は連続した数値を予測するもの、「分類」は非連続的な状態を予測するものかと。今回は連続した数値であるサラリー(給料)を予測するものですので、回帰分析を行いました

線形重回帰分析

まずは回帰分析で最もシンプルと言える線形重回帰分析を行います。

誰でも出来る!!scikit-learn(sklearn)で重回帰分析しちゃう
Pythonで基礎から機械学習 「重回帰分析」
Scikit-learn で線形回帰

結果もあわせて記しています。決定係数が訓練データで0.76ですが、テストデータでは0.25程度とひどいもんでした。
各パラメータの係数はMPG、MPなどの出場時間に関するもの、eFG%、FG%、FGなど得点に関するもので高い値が出てきており、それっぽいです。

# 必要なモジュールのインポート
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.linear_model import Lasso
from sklearn.linear_model import Ridge
from pandas import Series,DataFrame

# 線形回帰
model = LinearRegression()
model.fit(X_train, Y_train)
print("線形回帰_test:{}".format(model.score(X_test, Y_test)))
print("線形回帰_train:{}".format(model.score(X_train, Y_train)))

#係数を表示
pd.set_option("display.max_rows", 101)
df3=pd.DataFrame({'項目':X_train.columns,'係数':np.abs(model.coef_)}).sort_values(by='係数',ascending=False) 
df3.head()

##結果はこちら##
線形回帰_test:0.24891413727408662
線形回帰_train:0.7611534800620875

    項目     係数
25	MPG	124.417145
1	MP	124.123782
11	eFG%	39.908417
4	FG%	39.635531
2	FG	13.381686
ラッソ回帰・リッジ回帰

より高い精度で予測できるモデルを探すために、他の回帰分析を試行します。
ここではラッソ回帰、リッジ回帰を試みました。
パラメータ調整は行っていませんが、結果は下程度。もう一息かな。

【機械学習】ラッソ回帰・リッジ回帰について メモ
ラッソ(Lasso)回帰とリッジ(Ridge)回帰をscikit-learnで使ってみる

# ラッソ回帰
model = Lasso()
model.fit(X_train, Y_train)
print("ラッソ回帰_test:{}".format(model.score(X_test, Y_test)))
print("ラッソ回帰_train:{}".format(model.score(X_train, Y_train)))
print(model.coef_) 

# リッジ回帰
model = Ridge()
model.fit(X_train, Y_train)
print("リッジ回帰_test:{}".format(model.score(X_test, Y_test)))
print("リッジ回帰_train:{}".format(model.score(X_train, Y_train)))
print(model.coef_) 

##決定係数を表示##
ラッソ回帰_test:0.6162342051941998
ラッソ回帰_train:0.5759855670506375

リッジ回帰_test:0.49783459136663233
リッジ回帰_train:0.7304529474387433
ランダムフォレスト(回帰)

私の引き出しも残るは一つとなりましたが、ランダムフォレスト回帰分析です。
ランダムフォレストにも分類(RandomForestClassifier)と、回帰(RandomForestRegressor)がありますが、今回は後者です。
この結果、今回の中では最も高い、訓練データで決定係数0.93、テストデータで決定係数0.62が出ました。

なお、ランダムフォレストでは「.feature_importances_」で各変数の重要度を調べることができます。これで調べた結果も下に示していますが、AGE(年齢)、FGA(シュート試投数)、POINTS(得点)、TWITTER_FAVORITE_COUNT(Twitterお気に入り数)、PIE(Player Impact Estimate)などが上位5要素となりました。

TWITTER_FAVORITE_COUNTなどが上位にきているのが面白いですよね。これからすると、シュートをたくさん打ち、得点を重ね、SNSで活躍できるいい年齢の選手が高額なサラリーを獲得できるということです。わかったかな八村選手

(参考サイト)
機械学習手法「ランダムフォレスト」で回帰分析にチャレンジ
Pythonでデータ分析:ランダムフォレスト
“PIE” ってなに?

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import r2_score

model = RandomForestRegressor()
model.fit(X_train, Y_train)

print("RandomForestRegressor_test:{}".format(model.score(X_test, Y_test)))
print("RandomForestRegressor_train:{}".format(model.score(X_train, Y_train)))
importance = pd.DataFrame({'項目':X_train.columns,'係数':model.feature_importances_}).sort_values(by='係数',ascending=False) 
print(importance.head(10))

##結果を表示##
RandomForestRegressor_test:0.6245101754582717
RandomForestRegressor_train:0.9306695685031731

                        項目        係数
0                      AGE  0.137554
3                      FGA  0.103643
23                  POINTS  0.088006
34  TWITTER_FAVORITE_COUNT  0.056160
30                     PIE  0.051986
33               PAGEVIEWS  0.051441
12                      FT  0.048762
2                       FG  0.046203
9                      2PA  0.039449
13                     FTA  0.035569

ランダムフォレスト(回帰)について学習曲線を検討する

ここまでで、RandomForestRegressorが良さそうなのがわかりましたが、訓練データの精度に比べ、テストデータの精度が低い課題が残っています。
こちらに関しては原因を調査してみましょう。

(こちらのサイトを参考にしました)
AI(機械学習)のモデル精度向上に効く!学習曲線と検証曲線を入門

#RandomForestRegressorについて学習曲線を作成してみる

%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn; seaborn.set()
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.ensemble import RandomForestRegressor
from sklearn.pipeline import Pipeline
from sklearn.model_selection import learning_curve


#パイプラインを用いてデータのスケール変換を行い、RandomForestRegressor回帰を準備する
pipe_lr = Pipeline([('scl',StandardScaler()), ('clf',RandomForestRegressor())])

#learning_curve関数で交差検証による正解率を算出する
train_sizes, train_scores,test_scores = \
learning_curve(estimator=pipe_lr, X=X_train, y=Y_train, train_sizes=np.linspace(0.1,1.0,10), cv=10, n_jobs=1)

train_mean = np.mean(train_scores,axis=1)
train_std = np.std(train_scores,axis=1)
test_mean = np.mean(test_scores,axis=1)
test_std = np.std(test_scores,axis=1)

#学習曲線を描画する
plt.plot(train_sizes,train_mean, color='blue', marker='o', markersize=5, label='training accuracy')
plt.plot(train_sizes,test_mean, color='green',linestyle='--', marker='s', markersize=5, label='validation accuracy')

#fill_between関数で平均±標準偏差の幅を塗りつぶす
#トレーニングデータのサイズtrain_sizes,透明度alpha、カラー'blue'を引数に指定
plt.fill_between(train_sizes,train_mean+train_std,train_mean-train_std,alpha=0.15,color='green')
plt.fill_between(train_sizes, test_mean + test_std, test_mean - test_std, alpha=0.15,color='green')

#グラフの見た目を整える
plt.title('learning curve', fontsize=17)
plt.xlabel('Number of training samples', fontsize=17)
plt.ylabel('Accuracy', fontsize=17)
plt.legend(loc='lower right', fontsize=17)
plt.ylim([-1.0,1.0])

#グラフを表示
plt.show()

こちらの図のように、訓練データは精度良くフィッティングしていますが、テストデータについてはいまいちのようです。
参考サイトによると、この状態は過学習=「バリアンスが高い(ハイバリアンス)」のようです
これを解消するには、特徴量を減らして過剰なフィッティングを抑えるなどが効果的とのことです。
まだまだ改善可能そうですね。

パラメータサーチでモデルを最適化

テストデータの精度向上のため、RandomForestRegressorのパラメータサーチを試みました。
なお、ネット情報によるとRandomForestRegressorは初期状態でもまずまずの精度を持っているようです。
パラメータサーチのために用いたのはGridSearchCVです。
結果テストデータの精度は0.64程度となり、0.01程度の改善がみられました。

from sklearn import datasets
import numpy as np
import matplotlib.pyplot as plt
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import r2_score

#パラメータサーチする!!
model = RandomForestRegressor()
parameters = {
    'n_estimators'      : [1, 2, 3, 10, 100, 200, 500],
    'max_features'      : ['auto'],
    'random_state'      : [0],
    'min_samples_split' : [2, 3, 4, 5, 6,  7],
    'max_depth'         : [None, 3, 5, 10, 15, 20, 25, 30]
}

# グリッドサーチでパラメーターサーチ
clf = GridSearchCV(model, parameters, cv=2, n_jobs=-1)
clf.fit(X_train, Y_train)

clf.best_estimator_.score(X_train,Y_train)
print("RandomForestRegressor_test:{}".format(clf.score(X_train, Y_train)))

clf.best_estimator_.score(X_test,Y_test)
print("RandomForestRegressor_train:{}".format(clf.score(X_test, Y_test)))

print("ベストパラメータ",clf.best_params_)

##結果を出力##
RandomForestRegressor_test:0.9233922479182293
RandomForestRegressor_train:0.6389191028559318
ベストパラメータ {'max_depth': 10, 'max_features': 'auto', 'min_samples_split': 4, 'n_estimators': 500, 'random_state': 0}

XGBoostで最後に分析

xgboostでの分析にもチャレンジしました。
パラメータサーチが十分でないかもしれませんが、結果はRandomForestRegressorと同程度でした。

(参考サイト)
xgboost: テーブルデータに有効な機械学習モデル
XGBoostのハイパーパラメータをチューニングする

import xgboost as xgb

# xgboostモデルの作成
reg = xgb.XGBRegressor()
reg.fit(X_train, Y_train)

# ハイパーパラメータ探索
reg_cv = GridSearchCV(reg, {'max_depth': [1, 2, 3], 'n_estimators': [10, 20, 30, 40, 50, 60]},verbose=1)
reg_cv.fit(X_train, Y_train)
print(reg_cv.best_params_)

reg = xgb.XGBRegressor(**reg_cv.best_params_)
reg.fit(X_train, Y_train)

print("xgb_test:{}".format(reg.score(X_test, Y_test)))
print("xgb_train:{}".format(reg.score(X_train, Y_train)))

##結果を表示##
{'max_depth': 2, 'n_estimators': 50}
xgb_test:0.6271841369675475
xgb_train:0.8490468301129915

最後に

八村選手へ。とにかく貪欲に得点を取りに行って下さい。SNSも更新してください。
そうしたら間違いなく高級取りや!

↓↓微力ながら八村選手の給料に貢献。↓↓
https://www.instagram.com/rui_8mura/
https://twitter.com/rui_8mura?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor

以上で3ヶ月で学習したデータ分析の成果報告を終了します。
いざ書いてみると大したことはしていないように感じますが、3ヶ月前の全くのプログラミング経験ゼロの自分からみたらすごいことしているように見えるでしょうね。

これもAIDEMYの講師の皆様のご指導の賜物です。
どんな質問にも丁寧に対応頂きありがとうございました。