<jupyter_start><jupyter_text>NBA positions are out of date. Can we update them? Let's use clustering to find some natural grouping of players. The features are gathered from stats.nba.com. The features will look different than typical basketball statistics. Usually the statistics ESPN puts on the screen are measuring how well the player executes. Consider field goal percentage (FG%) which measures what percentage of shots are made. This statistic is measuring how well a player can make baskets. But what if we're more interested in *how often* the player shoots. This is not measuring their ability to execute but their role on the team. The features used in this analysis follow the same trend.<jupyter_code>import pandas as pd
from sklearn.cluster import KMeans
from sklearn.cluster import DBSCAN
from sklearn.mixture import GaussianMixture
import numpy as np
import matplotlib.pyplot as plt
df = pd.read_csv('C:/projects/summer2020/nba-clustering/data/data(w_FGA).csv')
df = df.fillna(value=0)
df = df.set_index('PLAYER')
minutes_played = df['MIN']
X = df.drop(columns='MIN')
X
%matplotlib inline
possible_k = list(range(1,11))
inertia = []
for k in possible_k:
    temp_kmeans = KMeans(n_clusters=k).fit(X)
    inertia.append(temp_kmeans.inertia_)
plt.bar(possible_k, inertia, color='darkorange', width=0.5)
plt.show()
k = 3
kmeans = KMeans(n_clusters=k).fit(X)
labels = kmeans.labels_
minutes_played = minutes_played.to_frame()
minutes_played.insert(1, 'LABELS', labels)
def get_top_players(df, label):
    df_label = df[df['LABELS'] == label]
    top_10_bool = df_label.loc[:, 'MIN'].rank(ascending=False, method='first') <= 10.0
    top_10 = df_label[top_10_bool].sort_values(by='MIN', ascending=False)
    return list(top_10.index)
top_10_lists = []
for label in range(k):
    top_10_lists.append(get_top_players(minutes_played, label))
pd.DataFrame(top_10_lists).transpose()
cluster, counts = np.unique(labels, return_counts=True)
[print('Group ' + str(cluster[i]) + ': ' + str(counts[i]) + ' elements') for i in range(len(cluster))]
labeled_X = X.copy()
labeled_X.insert(0, 'LABELS', labels)<jupyter_output><empty_output><jupyter_text>Let's look at the data distribution for each cluster, feature combination. Each feature has three lines, a blue line for cluster 0, gold line for cluster 1, and magenta line for cluster 2. The line begins at the 25th percentile of the cluster's distribution over the feature. The line ends at the 75th percentile. The point on the line is the mean of the data.<jupyter_code>df.columns
median_k = np.median(list(range(k)))
line_colors = ['c', 'darkorange', 'm']
fig, axs = plt.subplots(ncols=3, figsize=(20, 4))
feature_list = list(df.columns)
feature_list.remove('MIN')
for p in range(3):
    var_names = feature_list[8*p : 8*(p+1)]
    for i in range(k):
        temp_df = labeled_X[labeled_X['LABELS'] == i]
        for j in range((8*p+1), 8*(p+1)+1):
            feat_num = j
            col_data = temp_df.iloc[:,feat_num]
            col_stats = col_data.describe()
            x_val = feat_num + 0.1*(i - median_k)
            axs[p].plot([x_val, x_val], [col_stats['25%'], col_stats['75%']], line_colors[i], linewidth=2)
            axs[p].plot(x_val, col_stats['mean'], 'k.')
            axs[p].set_ylim([0,1])
    axs[p].set_xticklabels(['']+var_names)
    plt.setp(axs[p].xaxis.get_majorticklabels(), rotation=70)<jupyter_output><empty_output><jupyter_text>There are a number of interesting observations from these plots. 1. The first is the miles traveled on both offense and defense are similar between the three clusters.2. Some of the features could be redundant i.e. the POST-UP% and POST UPS.I will remove some of the features and run it again to see how the results differ.<jupyter_code>new_X = df.drop(columns=['MIN', 'POST-UP %', 'DIST MILES OFF', 'DIST MILES DEF'])
k = 3
kmeans = KMeans(n_clusters=k).fit(new_X)
labels = kmeans.labels_
minutes_played = df['MIN']
minutes_played = minutes_played.to_frame()
minutes_played.insert(1, 'LABELS', labels)
top_10_lists = []
for label in range(k):
    top_10_lists.append(get_top_players(minutes_played, label))
pd.DataFrame(top_10_lists).transpose()
cluster, counts = np.unique(labels, return_counts=True)
[print('Group ' + str(cluster[i]) + ': ' + str(counts[i]) + ' elements') for i in range(len(cluster))]
labeled_X = new_X.copy()
labeled_X.insert(0, 'LABELS', labels)<jupyter_output><empty_output><jupyter_text>We'll plot the distribution for each cluster and feature combination with the reduced feature set.<jupyter_code>median_k = np.median(list(range(k)))
line_colors = ['c', 'darkorange', 'm']
fig, axs = plt.subplots(ncols=3, figsize=(20, 4))
for p in range(3):
    var_names = list(new_X.columns[8*p: 8*(p+1)])
    for i in range(k):
        temp_df = labeled_X[labeled_X['LABELS'] == i]
        for j in range((8*p+1), 8*(p+1)+1):
            if j >= temp_df.shape[1]:
                break
            feat_num = j
            col_data = temp_df.iloc[:,feat_num]
            col_stats = col_data.describe()
            x_val = feat_num + 0.1*(i - median_k)
            axs[p].plot([x_val, x_val], [col_stats['25%'], col_stats['75%']], line_colors[i], linewidth=2)
            axs[p].plot(x_val, col_stats['mean'], 'k.')
            axs[p].set_ylim([0,1])
    axs[p].set_xticklabels(['']+var_names)
    plt.setp(axs[p].xaxis.get_majorticklabels(), rotation=70)<jupyter_output><empty_output><jupyter_text>These graphics give us good insights into the characteristics of each player group. Here are some highlights:Group 0:- smaller size (for an NBA player)- typically shoot the most- time of possession is much longer- dribble a lotGroup 1:- average size- the only thing they do most is three point attempts- the only thing they do least is touches in a game- I wish I had more to say. I'm sure they're great guys.Group 2:- bigger size- shoot the most within eight feet of the hoop- defend FGA the most- dribble the least  What do we do with this information?The dialogue surrounding the game should evolve with the game itself. What is a Power Forward? I still think of <NAME> (who played in the 90's) when I hear the term. His style - and that of his colleagues - fit the name "Power Forward". They were strong, large players that played up front by the basket. They differed from a Center in the way they would run the court and shoot 12 foot jump shots. During that era the differences of all five players seemed more distinct. Today's game doesn't feel that way. The five players playing on a team look like a combination of the players described above. Some of the most common terms for the three groups above are Guards, Three and D's, and Bigs, respectively. Let's look at the team with the best regular season record from the 19-20 season and their most played 5-man lineup.<jupyter_code>bucks_lineup = ['<NAME>', '<NAME>', '<NAME>', '<NAME>', '<NAME>']
labeled_X.loc[bucks_lineup, 'LABELS']<jupyter_output><empty_output><jupyter_text>This team benefits from <NAME> playing as a Big with skills of a Guard. The other Big on this team, <NAME>, is also unusual for a Big. He is an excellent three point shooter despite being a Big. This is a well balanced team that has pieces that fit well together.  I think the natural next step is to wonder if there is a five person combination of these three types of players that performs better than the rest. To do that you need to control for player skill variability as well as coaching style. Successfuly answering that question would be a great tool for team decision makers when making roster additions or changes. I'll leave that for the next project. I think using features that tell the story of what a player does instead of how well they do it produces more intuitive clustering of players. This establishes a good foundation for further research into what an ideal five person line-up looks like.  Appendix A: Principal Component AnalysisDespite the feature space not being large we'll do PCA for visual reasons and in case we can glean more from the data.<jupyter_code>from sklearn.decomposition import PCA
pca = PCA(n_components=10)
principalComponents = pca.fit_transform(X)
features = range(pca.n_components_)
plt.bar(features, pca.explained_variance_ratio_, color='darkorange')
plt.xlabel('PCA features')
plt.ylabel('variance %')
plt.xticks(features)
PCA_components = pd.DataFrame(principalComponents)
plt.scatter(PCA_components[0], PCA_components[1], alpha=.1, color='darkorange')
plt.xlabel('PCA 1')
plt.ylabel('PCA 2')<jupyter_output><empty_output>
StarcoderdataJupyterStructuredCleanDedup