{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Redefining the Roster\n*Using unsupervised machine learning to cluster NCAA players by their stats.*\n\nMost people who watch March Madness are familiar with the standard positions of basketball: guards, forwards, and centers. But as basketball continues to evolve, the lines that define players have become more blurred. In a 2013 article, Myron Medcalf questioned if the traditional positions were even still relevant. Metcalf said, \"The descriptors that made sense years ago feel inadequate today. Point guards, shooting guards, small forwards, power forwards and centers -- in some cases -- have been replaced by young men who simply call themselves players. [1]\"\n\nMy question, then is: What does the data show us about player positions? Would it be possible to categorize players' positions based solely on the data? After defining these \"positions\" what types of rosters fare well in the March Madness tournament?\n\nUsing an unsupervised machine learning technique called clustering, I've identified 5 distinct types of college basketball players. They are: Ball Distributors, Elite Bigs, Shot Takers, 3 Point Specialists, and Paint Dominators. In this notebook, I'll go through the process used to identify these groups, and go into more detail about what makes each unique. So let's let the data tell the story of the players who rule March Madness!"},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pylab as plt\nimport seaborn as sns\nfrom tqdm.notebook import tqdm\n\nfrom sklearn.cluster import KMeans\nfrom sklearn.manifold import TSNE\nfrom sklearn.preprocessing import StandardScaler\nimport umap\n\nimport lightgbm as lgb\n\nfrom scipy.spatial.distance import cdist \n\nimport time\n\nimport matplotlib.pylab as plt\nimport matplotlib as mpl\nfrom matplotlib.patches import Circle, Rectangle, Arc\n\nimport plotly.express as px\n\nfrom IPython.display import display, HTML\n\nimport warnings\nwarnings.filterwarnings('ignore')\n\nsns.set(rc={'figure.figsize':(15, 5)})\npalette = sns.color_palette(\"bright\", 10)\nsns.set_style(\"whitegrid\")\n\npd.set_option('max_columns', 500)\npd.set_option('max_rows', 500)\npd.options.display.float_format = '{:.2f}'.format\n# plt.rcParams['figure.figsize'] = (15.0, 5.0)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"def create_ncaa_half_court(ax=None, three_line='mens', court_color='#dfbb85',\n                           lw=3, lines_color='black', lines_alpha=0.5,\n                           paint_fill='blue', paint_alpha=0.4,\n                          inner_arc=False):\n    \"\"\"\n    Version 2020.2.19\n\n    Creates NCAA Basketball Half Court\n    Dimensions are in feet (Court is 97x50 ft)\n    Created by: Rob Mulla / https://github.com/RobMulla\n\n    * Note that this function uses \"feet\" as the unit of measure.\n    * NCAA Data is provided on a x range: 0, 100 and y-range 0 to 100\n    * To plot X/Y positions first convert to feet like this:\n    ```\n    Events['X_'] = (Events['X'] * (94/100))\n    Events['Y_'] = (Events['Y'] * (50/100))\n    ```\n    ax: matplotlib axes if None gets current axes using `plt.gca`\n    \n    three_line: 'mens', 'womens' or 'both' defines 3 point line plotted\n    court_color : (hex) Color of the court\n    lw : line width\n    lines_color : Color of the lines\n    lines_alpha : transparency of lines\n    paint_fill : Color inside the paint\n    paint_alpha : transparency of the \"paint\"\n    inner_arc : paint the dotted inner arc\n    \"\"\"\n    if ax is None:\n        ax = plt.gca()\n\n    # Create Pathes for Court Lines\n    center_circle = Circle((50/2, 94/2), 6,\n                           linewidth=lw, color=lines_color, lw=lw,\n                           fill=False, alpha=lines_alpha)\n    hoop = Circle((50/2, 5.25), 1.5 / 2,\n                       linewidth=lw, color=lines_color, lw=lw,\n                       fill=False, alpha=lines_alpha)\n\n    # Paint - 18 Feet 10 inches which converts to 18.833333 feet - gross!\n    paint = Rectangle(((50/2)-6, 0), 12, 18.833333,\n                           fill=paint_fill, alpha=paint_alpha,\n                           lw=lw, edgecolor=None)\n    \n    paint_boarder = Rectangle(((50/2)-6, 0), 12, 18.833333,\n                           fill=False, alpha=lines_alpha,\n                           lw=lw, edgecolor=lines_color)\n    \n    arc = Arc((50/2, 18.833333), 12, 12, theta1=-\n                   0, theta2=180, color=lines_color, lw=lw,\n                   alpha=lines_alpha)\n    \n    block1 = Rectangle(((50/2)-6-0.666, 7), 0.666, 1, \n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    block2 = Rectangle(((50/2)+6, 7), 0.666, 1, \n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    ax.add_patch(block1)\n    ax.add_patch(block2)\n    \n    l1 = Rectangle(((50/2)-6-0.666, 11), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    l2 = Rectangle(((50/2)-6-0.666, 14), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    l3 = Rectangle(((50/2)-6-0.666, 17), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    ax.add_patch(l1)\n    ax.add_patch(l2)\n    ax.add_patch(l3)\n    l4 = Rectangle(((50/2)+6, 11), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    l5 = Rectangle(((50/2)+6, 14), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    l6 = Rectangle(((50/2)+6, 17), 0.666, 0.166,\n                           fill=True, alpha=lines_alpha,\n                           lw=0, edgecolor=lines_color,\n                           facecolor=lines_color)\n    ax.add_patch(l4)\n    ax.add_patch(l5)\n    ax.add_patch(l6)\n    \n    # 3 Point Line\n    if (three_line == 'mens') | (three_line == 'both'):\n        # 22' 1.75\" distance to center of hoop\n        three_pt = Arc((50/2, 6.25), 44.291, 44.291, theta1=12,\n                            theta2=168, color=lines_color, lw=lw,\n                            alpha=lines_alpha)\n\n        # 4.25 feet max to sideline for mens\n        ax.plot((3.34, 3.34), (0, 11.20),\n                color=lines_color, lw=lw, alpha=lines_alpha)\n        ax.plot((50-3.34, 50-3.34), (0, 11.20),\n                color=lines_color, lw=lw, alpha=lines_alpha)\n        ax.add_patch(three_pt)\n\n    if (three_line == 'womens') | (three_line == 'both'):\n        # womens 3\n        three_pt_w = Arc((50/2, 6.25), 20.75 * 2, 20.75 * 2, theta1=5,\n                              theta2=175, color=lines_color, lw=lw, alpha=lines_alpha)\n        # 4.25 inches max to sideline for mens\n        ax.plot( (4.25, 4.25), (0, 8), color=lines_color,\n                lw=lw, alpha=lines_alpha)\n        ax.plot((50-4.25, 50-4.25), (0, 8.1),\n                color=lines_color, lw=lw, alpha=lines_alpha)\n\n        ax.add_patch(three_pt_w)\n\n    # Add Patches\n    ax.add_patch(paint)\n    ax.add_patch(paint_boarder)\n    ax.add_patch(center_circle)\n    ax.add_patch(hoop)\n    ax.add_patch(arc)\n    \n    if inner_arc:\n        inner_arc = Arc((50/2, 18.833333), 12, 12, theta1=180,\n                             theta2=0, color=lines_color, lw=lw,\n                       alpha=lines_alpha, ls='--')\n        ax.add_patch(inner_arc)\n\n    # Restricted Area Marker\n    restricted_area = Arc((50/2, 6.25), 8, 8, theta1=0,\n                        theta2=180, color=lines_color, lw=lw,\n                        alpha=lines_alpha)\n    ax.add_patch(restricted_area)\n    \n    # Backboard\n    ax.plot(((50/2) - 3, (50/2) + 3), (4, 4),\n            color=lines_color, lw=lw*1.5, alpha=lines_alpha)\n    ax.plot( (50/2, 50/2), (4.3, 4), color=lines_color,\n            lw=lw, alpha=lines_alpha)\n\n    # Half Court Line\n    ax.axhline(94/2, color=lines_color, lw=lw, alpha=lines_alpha)\n\n    \n    # Plot Limit\n    ax.set_xlim(0, 50)\n    ax.set_ylim(0, 94/2 + 2)\n    ax.set_facecolor(court_color)\n    ax.set_xticks([])\n    ax.set_yticks([])\n    ax.set_xlabel('')\n    return ax","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"###########\n# Data Prep\n###########\n\nMENS_PBP_DIR = '../input/march-madness-analytics-2020/MPlayByPlay_Stage2'\n\nMPlayers = pd.read_csv(f'{MENS_PBP_DIR}/MPlayers.csv', error_bad_lines=False)\nMTeamSpelling = pd.read_csv('../input/march-madness-analytics-2020/MDataFiles_Stage2/MTeamSpellings.csv',\n                            engine='python')\nmens_events = []\nfor year in [2015, 2016, 2017, 2018, 2019]:\n    mens_events.append(pd.read_csv(f'{MENS_PBP_DIR}/MEvents{year}.csv'))\nMEvents = pd.concat(mens_events)\n\nMTeams = pd.read_csv('../input/march-madness-analytics-2020/MDataFiles_Stage2/MTeams.csv')\nMPlayers = MPlayers.merge(MTeams[['TeamID','TeamName']], on='TeamID', how='left')\n\narea_mapping = {0: np.nan,\n                1: 'under basket',\n                2: 'in the paint',\n                3: 'inside right wing',\n                4: 'inside right',\n                5: 'inside center',\n                6: 'inside left',\n                7: 'inside left wing',\n                8: 'outside right wing',\n                9: 'outside right',\n                10: 'outside center',\n                11: 'outside left',\n                12: 'outside left wing',\n                13: 'backcourt'}\n\nMEvents['Area_Name'] = MEvents['Area'].map(area_mapping)\n\n# Normalize X, Y positions for court dimentions\n# Court is 50 feet wide and 94 feet end to end.\nMEvents['X_'] = (MEvents['X'] * (94/100))\nMEvents['Y_'] = (MEvents['Y'] * (50/100))\n\nMEvents['X_half'] = MEvents['X']\nMEvents.loc[MEvents['X'] > 50, 'X_half'] = (100 - MEvents['X'].loc[MEvents['X'] > 50])\nMEvents['Y_half'] = MEvents['Y']\nMEvents.loc[MEvents['X'] > 50, 'Y_half'] = (100 - MEvents['Y'].loc[MEvents['X'] > 50])\n\nMEvents['X_half_'] = (MEvents['X_half'] * (94/100))\nMEvents['Y_half_'] = (MEvents['Y_half'] * (50/100))\n\n# Merge Player name onto events\nMEvents = MEvents.merge(MPlayers,\n              how='left',\n              left_on='EventPlayerID',\n              right_on='PlayerID')\n\n# Only Look at Events with Player assoicated and X, Y position\nMPlayerEvents = MEvents.query('EventPlayerID != 0 and X > 0')\n\n# Create GameId\nMPlayerEvents['GameId'] = \\\n    MPlayerEvents['Season'].astype('str') + '_' + \\\n    MPlayerEvents['DayNum'].astype('str') + '_' + \\\n    MPlayerEvents['WTeamID'].astype('str') + '_' + \\\n    MPlayerEvents['LTeamID'].astype('str')\n\nEventPlayerSeasonCombo = MPlayerEvents[['EventPlayerID','Season']].drop_duplicates().reset_index(drop=True)\n\n# Expand MPlayers to have a row for each player\nMPlayerSeason = MPlayers.merge(EventPlayerSeasonCombo,\n               left_on=['PlayerID'],\n               right_on=['EventPlayerID'],\n              validate='1:m') \\\n    .drop('EventPlayerID', axis=1)\n\nMPlayerSeason['PlayerID_Season'] = MPlayerSeason['PlayerID'].astype('int').astype('str') + '_' + MPlayerSeason['Season'].astype('str')\nMPlayerEvents['PlayerID_Season'] = MPlayerEvents['PlayerID'].astype('int').astype('str') + '_' + MPlayerEvents['Season'].astype('str')\n\nMPlayerSeasonStats = pd.read_csv('../input/ncaa-mplayerseasonstats/MPlayerSeasonStats.csv')\n\nMPlayerSeasonStats.loc[MPlayerSeasonStats['Weight'] < 150, 'Weight'] = 150 # Fix for zero weights\n\nMPlayerSeasonStats['Position'] = MPlayerSeasonStats['Pos'].map({'F':'Forward',\n                                                      'G':'Guard',\n                                                      'C':'Center'})\nMPlayerSeasonStats['RSCI Top 100 Number'] = MPlayerSeasonStats['RSCI Top 100'].str.split(' ', expand=True)[0].fillna(1000).astype('int')\n\nKAGGLE_PER_GAME_FEATURES = [c for c in MPlayerSeasonStats if 'per_game' in c and 'ShotCount' in c]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"f_name_dict = { 'MP' : 'Minutes Played',\n                 'FG' : 'Field Goals',\n                 'FGA':'Field Goal Attempts',\n                 'FG%' : 'Field Goal Percentage',\n                 '2P' : '2-Point Field Goals',\n                 '2PA': '2-Point Field Goal Attempts',\n                 '2P%': '2-Point Field Goal Percentage',\n                 '3P' : '3-Point Field Goals',\n                 '3PA' : '3-Point Field Goal Attempts',\n                 '3P%' : '3-Point Field Goal Percentage',\n                 'FT' : 'Free Throws',\n                 'FTA' : 'Free Throw Attempts',\n                 'FT%' : 'Free Throw Percentage',\n                 'ORB' : 'Offensive Rebounds',\n                 'DRB':'Defensive Rebounds',\n                 'TRB' : 'Total Rebounds',\n                 'AST' : 'Assists',\n                 'STL' : 'Steals',\n                 'BLK' : 'Blocks',\n                 'TOV' : 'Turnovers',\n                 'PF' : 'Personal Fouls',\n                 'PTS' : 'Points'}\n\n# Make Features Per Minute Played\nfor f in ['FG', 'FGA', '2P', '2PA', '3P', '3PA', 'FTA',\n          'ORB', 'DRB',  'TRB', 'AST', 'STL', 'BLK', 'TOV', 'PF', 'PTS'] + KAGGLE_PER_GAME_FEATURES:\n    if f in f_name_dict:\n        MPlayerSeasonStats[f_name_dict[f] + ' Per Minute'] = MPlayerSeasonStats[f] / MPlayerSeasonStats['MP']\n    else:\n        MPlayerSeasonStats[f] = MPlayerSeasonStats[f] / MPlayerSeasonStats['MP']","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"# Features\nKAGGLE_PER_GAME_FEATURES = [c for c in MPlayerSeasonStats if 'per_game' in c and 'ShotCount' in c]\nSR_PER_GAME_FEATURES = ['MP', 'FG', 'FGA', 'FG%', '2P', '2PA', '2P%', '3P',\n                        '3PA', '3P%', 'FT', 'FTA',\n                        'FT%', 'ORB', 'DRB', 'TRB', 'AST',\n                        'STL', 'BLK', 'TOV', 'PF',\n                        'PTS', 'HeightFeet','Weight']\nPLAYER_FEATS = ['Position', 'PlayerID_Season','FirstName',\n                'LastName','HeightFeet','TeamName','Season',\n                'G', 'GS','PTS','TRB','HeightFeet','FTA','STL']\n\nFEATURES = ['Field Goal Attempts Per Minute',\n            '2-Point Field Goals Per Minute',\n            '3-Point Field Goals Per Minute',\n            'Free Throw Attempts Per Minute',\n            'Offensive Rebounds Per Minute',\n            'Defensive Rebounds Per Minute',\n            'Assists Per Minute',\n            'Blocks Per Minute',\n            'Turnovers Per Minute',\n            'Personal Fouls Per Minute',\n           ]\n\nMPlayerSeasonStats_ = \\\n    MPlayerSeasonStats \\\n    .query('GS > 20 and EventCount >= 0') \\\n    [FEATURES + PLAYER_FEATS] \\\n    .dropna() \\\n    .copy()\n\nX = MPlayerSeasonStats_[FEATURES].values\nss = StandardScaler()\nX = ss.fit_transform(X)\ny = MPlayerSeasonStats_['PlayerID_Season'].values\n\n# Create 5 Player Clusters\nmodel = KMeans(n_clusters=5, random_state=529)\nkmeans_pred = model.fit_predict(X)\nMPlayerSeasonStats_['kmeans_Cluster'] = kmeans_pred\nMPlayerSeasonStats_['Data_Position'] = MPlayerSeasonStats_['kmeans_Cluster'] + 1\n\ncluster_names_dict = {1 : 'Shot Taker',\n                      2 : 'Skilled Big',\n                      3 : '3 Point Specialist',\n                      4 : 'Ball Distributor',\n                      5 : 'Paint Dominator'}\n\nMPlayerSeasonStats_['Cluster_Name'] = MPlayerSeasonStats_['Data_Position'].map(cluster_names_dict)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"This plot shows only two dimensions (Points vs Rebounds) for the 5 roster positions that I've created. It looks a little bit messy, but its helpful to see how the shape of these clusters are formed by the features used. Keep in mind this only shows two features, and the clusters are formed using 10 features!"},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"data = MPlayerSeasonStats_.merge(MPlayerSeasonStats[['PlayerID_Season','TRB','FGA']])\nfrom matplotlib.patches import Ellipse\n\nx_feat = 'TRB'\ny_feat = 'PTS'\n\nfig, ax = plt.subplots(figsize=(15, 15))\ncolor_pal = [x['color'] for x in plt.rcParams['axes.prop_cycle']]\n\nidx = 0\nfor i, d in data.query('GS > 20').groupby('Cluster_Name'):\n    d.plot(x='PTS', y='TRB', kind='scatter', ax=ax,\n           color=color_pal[idx], alpha=0.5, label=f'{i}', s=25)\n    idx += 1\nidx = 0\n\nax.annotate('Zion Williamson 2019 (Skilled Big)',\n            xy=(22.60, 8.90), xycoords='data',\n            xytext=(0.99, 0.65), textcoords='axes fraction',\n            arrowprops=dict(facecolor='black', shrink=0.05),\n            horizontalalignment='right', verticalalignment='top')\n\nax.annotate('Lonzo Ball 2017 (Ball Distributor)',\n            xy=(14.60, 6), xycoords='data',\n            xytext=(0.98, 0.51), textcoords='axes fraction',\n            arrowprops=dict(facecolor='black', shrink=0.05),\n            horizontalalignment='right', verticalalignment='top')\n\nax.annotate('Aaron Harrison 2015 (3Pt Specialist)',\n            xy=(11.00, 2.6), xycoords='data',\n            xytext=(0.5, 0.05), textcoords='axes fraction',\n            arrowprops=dict(facecolor='black', shrink=0.05),\n            horizontalalignment='right', verticalalignment='top')\n\nax.annotate('Kaleb Tarczewski 2016 (Paint Dominator)',\n            xy=(9.40, 9.30), xycoords='data',\n            xytext=(0.32, 0.72), textcoords='axes fraction',\n            arrowprops=dict(facecolor='black', shrink=0.05),\n            horizontalalignment='right', verticalalignment='top')\n\nax.annotate('Romeo Langford 2019 (Shot Taker)',\n            xy=(16.50, 5.40), xycoords='data',\n            xytext=(0.95, 0.10), textcoords='axes fraction',\n            arrowprops=dict(facecolor='black', shrink=0.05),\n            horizontalalignment='right', verticalalignment='top')\n\nplt.title('Data Driven Positions: Rebounds vs Points', fontsize=25)\nplt.xlabel('Points / Game')\nplt.ylabel('Total Rebounds / Game')\nplt.legend()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Features and Approach\n\nTo pair down the data, I only focused on the Men's numbers. I pulled various stats for each player/season. These statistics were normalized by the number of minutes played by each player- I didn't want to have clusters forming around the amount of time a player was in the game.  Rather, I wanted the statistics to be about what the player did with the amount of time he was on the court. Also noteable: I filtered down to include only those players who started at least 20 games in a given season.\n\n**Features Used:**\n- Field Goal Attempts\n- 2-Point Field Goals\n- 3-Point Field Goals\n- Free Throw Attempts\n- Offensive Rebounds\n- Defensive Rebounds\n- Assists\n- Blocks\n- Turnovers\n- Personal Fouls\n\nI found that KMeans clustering worked the best for this case, as it tended to do a better job of balancing the clusters. I had to choose the *number* of clusters I wanted the algorithm to produce- after many tests, I found that five produced the best result. I've hidden the code, but you can expand the cells below to look in more detail at how the algorithm was set up."},{"metadata":{},"cell_type":"markdown","source":"## Comparing the Groups in Detail\nBy plotting out the average stat for each cluster, it's immediately clear how they differ. Notice that some clusters tend to lean more defensive, with high numbers of blocks and rebounds- while others lean more offensive. I used these analyases to name the groups. "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"MPlayerSeasonStats_[FEATURES + ['Cluster_Name']].groupby('Cluster_Name') \\\n    .mean().sort_index(ascending=False).T \\\n    .plot(kind='barh', figsize=(15, 12),\n          title='Average Stats by Group')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Another way to compare the groups is by using boxplots. This is allows me to see the standard deviation and outliers for each statistic. For instance, notice that the players at the extreme high end of 2-Point field goals are in the \"Skilled Bigs\" group."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"clus_name_short ={'3 Point Specialist': '3PT Spec',\n                  'Shot Taker':'Shot Taker',\n                  'Ball Distributor' : 'Distrib',\n                  'Skilled Big':'SBig',\n                  'Paint Dominator': 'PaintDom'}\n\nMPlayerSeasonStats_['Cluster_Name_Short'] = MPlayerSeasonStats_['Cluster_Name'].map(clus_name_short)\n\nfig, axs = plt.subplots(3, 3, figsize=(15, 10))\naxs = axs.flatten()\nidx = 0\nfor f in FEATURES[:9]:\n    sns.boxplot(x='Cluster_Name_Short', y=f, data=MPlayerSeasonStats_, ax=axs[idx])\n    axs[idx].set_title(f)\n    axs[idx].set_xlabel('')\n    axs[idx].set_ylabel('')\n    idx += 1\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"\n# The Lineup!\nBelow I'll take a deeper dive into each group to explore what makes them unique. Before I do that it's helpful to take a quick look at each positions' shot locations side by side."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"fig, axs = plt.subplots(1, 5, figsize=(18, 3.6))\naxs = axs.flatten()\nidx = 0\nfor cluster in MPlayerSeasonStats_['Cluster_Name'].unique():\n    playerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\n    Made = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n        .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\n    axs[idx] = create_ncaa_half_court(ax=axs[idx],\n                                 three_line='mens',\n                                 court_color='black',\n                                      lines_color='white',\n                                 paint_alpha=0,\n                                 inner_arc=True)\n    #     hb1 = axs[idx].plot(x=Turnover.query('Y_ != 0')['Y_half_'],\n    #                      y=Turnover.query('Y_ != 0')['X_half_'],\n    #                      gridsize=10, bins='log', cmap='inferno')\n    Made.query('Y_ != 0').plot(x='Y_half_',\n                               y='X_half_',\n                               kind='scatter',\n                               ax=axs[idx],\n                               alpha=0.1)\n    axs[idx].set_title(f'{cluster}', size=15)\n    axs[idx].set_xlabel('')\n    axs[idx].set_ylabel('')\n    axs[idx].set_ylim(0, 38)\n    idx+= 1\nfig.suptitle('Shot Selection by Group', y=1.05, fontsize=15)\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# The Skilled Big\nPlayers in this group tend to be the tallest on the court. They take the second most shots behind the \"Shot Taker\" group. They are more likely to take 2 point shots over 3-pointers, and they also have high average foul stats. They tend to make the most free throws, as they are often fouled."},{"metadata":{"trusted":true},"cell_type":"code","source":"MPlayerSeasonStats_ = MPlayerSeasonStats_.loc[:,~MPlayerSeasonStats_.columns.duplicated()]\nax = MPlayerSeasonStats_.groupby('Cluster_Name')['HeightFeet'].mean().sort_values() \\\n    .plot(kind='barh',\n          xlim=(6, 6.8),\n          title='Skilled bigs are the tallest of all groups')\nax.patches[4].set_color('orange')\nax.set_xlabel('Player Height (Feet)')\nax.set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cluster = 'Skilled Big'\n\nfig, axs = plt.subplots(1, 2, figsize=(14, 7))\naxs = axs.flatten()\n\nidx = 0\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nShots = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\naxs[idx] = create_ncaa_half_court(ax=axs[idx],\n                             three_line='mens',\n                             court_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nhb1 = axs[idx].hexbin(x=Shots.query('Y_ != 0')['Y_half_'],\n                 y=Shots.query('Y_ != 0')['X_half_'],\n                 gridsize=20, bins='log', cmap='inferno')\naxs[idx].set_title(f'Shot Attempts: {cluster}', size=15)\n\naxs[idx].set_ylim(0, 40)\naxs[idx].set_xlim(1, 49)\nplt.tight_layout()\n\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nMade = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\"')\naxs[idx+1] = create_ncaa_half_court(ax=axs[idx+1],\n                             three_line='mens',\n                             court_color='black',\n                                  lines_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\n#     hb1 = axs[idx].plot(x=Turnover.query('Y_ != 0')['Y_half_'],\n#                      y=Turnover.query('Y_ != 0')['X_half_'],\n#                      gridsize=10, bins='log', cmap='inferno')\nMade.query('Y_ != 0').plot(x='Y_half_',\n                 y='X_half_',\n              kind='scatter', ax=axs[idx+1], alpha=0.3)\naxs[idx+1].set_title(f'Made Shots: {cluster}', size=15)\n\naxs[idx+1].set_ylim(0, 40)\naxs[idx+1].set_xlim(1, 49)\naxs[idx+1].set_xlabel('')\naxs[idx+1].set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The Skilled Bigs tend to be highly ranked coming out of high school. The number 1 high school recruit in 2014, 2015, 2016, and 2017 all fall into this category."},{"metadata":{},"cell_type":"markdown","source":"## Notable \"Skilled Bigs\"\n| FirstName    | LastName             | TeamName   | Season | Position | RSCI Top 100 |\n|--------------|----------------------|------------|--------|----------|--------------|\n| Jahlil       | Okafor               | Duke       | 2015   | Center   | 1 (2014)     |\n| Marvin       | Bagley III           | Duke       | 2018   | Forward  | 1 (2017)     |\n| Josh         | Jackson              | Kansas     | 2017   | Guard    | 1 (2016)     |\n| Ben          | Simmons              | LSU        | 2016   | Forward  | 1 (2015)     |\n| Jaylen       | Brown                | California | 2016   | Forward  | 3 (2015)     |\n| Deandre      | Ayton                | Arizona    | 2018   | Forward  | 3 (2017)     |\n| Mohamed      | Bamba                | Texas      | 2018   | Forward  | 4 (2017)     |\n| Zion         | Williamson           | Duke       | 2019   | Forward  | 4 (2018)     |\n| Ivan         | Rabb                 | California | 2016   | Forward  | 5 (2015)     |\n| Karl-Anthony | Towns                | Kentucky   | 2015   | Forward  | 5 (2014)     |\n\n(Expand next cell to show data source)"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"Cluster_Name = 'Skilled Big'\ngroup_players = MPlayerSeasonStats_.query('Cluster_Name == @Cluster_Name')['PlayerID_Season'].unique()\ndisplay(MPlayerSeasonStats.loc[MPlayerSeasonStats['PlayerID_Season'] \\\n                       .isin(group_players)].sort_values('RSCI Top 100 Number') \\\n    [['FirstName','LastName','TeamName',\n      'Season','Position','RSCI Top 100']].drop_duplicates(subset=['FirstName','LastName']).head(10).style.hide_index())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# The Shot Taker\nThese players take more shots than any other group by a good margin. Unlike the 3 point specalists, they take a balance of both 2 and 3-pointers. Interestingly, while this player takes more shots than all other groups, they are typically still in the middle of the pack with regard to their shooting percentage."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"fig, (ax, ax1) = plt.subplots(1, 2, figsize=(15, 4))\nfig.suptitle('High shot count but average shooting %', fontsize=15, y=1.03)\nMPlayerSeasonStats[['PlayerID_Season','FGA']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['FGA'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax)\n# ax.set_title('Almost 2x more assists than the next group', fontsize=15)\nax.set_xlabel('Field Goal Attempts Per Game')\nax.set_ylabel('')\nax.patches[4].set_color('orange')\n\n\nMPlayerSeasonStats[['PlayerID_Season','FG%']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['FG%'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax1)\n# ax1.set_title('Almost 2x more assists than the next group', fontsize=15)\nax1.set_xlabel('Field Goal %')\nax1.set_ylabel('')\nax1.patches[2].set_color('orange')\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cluster = 'Shot Taker'\n\nfig, axs = plt.subplots(1, 2, figsize=(14, 7))\naxs = axs.flatten()\n\nidx = 0\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nShots = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\naxs[idx] = create_ncaa_half_court(ax=axs[idx],\n                             three_line='mens',\n                             court_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nhb1 = axs[idx].hexbin(x=Shots.query('Y_ != 0')['Y_half_'],\n                 y=Shots.query('Y_ != 0')['X_half_'],\n                 gridsize=20, bins='log', cmap='inferno')\naxs[idx].set_title(f'Shot Attempts: {cluster}', size=15)\n\naxs[idx].set_ylim(0, 40)\naxs[idx].set_xlim(1, 49)\nplt.tight_layout()\n\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nMade = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\"')\naxs[idx+1] = create_ncaa_half_court(ax=axs[idx+1],\n                             three_line='mens',\n                             court_color='black',\n                                  lines_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\n#     hb1 = axs[idx].plot(x=Turnover.query('Y_ != 0')['Y_half_'],\n#                      y=Turnover.query('Y_ != 0')['X_half_'],\n#                      gridsize=10, bins='log', cmap='inferno')\nMade.query('Y_ != 0').plot(x='Y_half_',\n                 y='X_half_',\n              kind='scatter', ax=axs[idx+1], alpha=0.3)\naxs[idx+1].set_title(f'Made Shots: {cluster}', size=15)\n\naxs[idx+1].set_ylim(0, 40)\naxs[idx+1].set_xlim(1, 49)\naxs[idx+1].set_xlabel('')\naxs[idx+1].set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"When looking at some highly ranked players in this group, I notice that many of them end up landing at Duke. Maybe Mike Krzyzewski prefers these types of players? "},{"metadata":{},"cell_type":"markdown","source":"## Notable \"Shot Takers\"\n| FirstName | LastName | TeamName   | Season | Position | RSCI Top 100 |\n|-----------|----------|------------|--------|----------|--------------|\n| RJ        | Barrett  | Duke       | 2019   | Forward  | 1 (2018)     |\n| Cam       | Reddish  | Duke       | 2019   | Forward  | 2 (2018)     |\n| Jayson    | Tatum    | Duke       | 2017   | Forward  | 3 (2016)     |\n| Stanley   | Johnson  | Arizona    | 2015   | Forward  | 3 (2014)     |\n| Brandon   | Ingram   | Duke       | 2016   | Guard    | 4 (2015)     |\n| Romeo     | Langford | Indiana    | 2019   | Guard    | 5 (2018)     |\n| Markelle  | Fultz    | Washington | 2017   | Guard    | 5 (2016)     |\n| Collin    | Sexton   | Alabama    | 2018   | Guard    | 6 (2017)     |\n| De'Aaron  | Fox      | Kentucky   | 2017   | Guard    | 6 (2016)     |\n| Kelly     | Oubre    | Kansas     | 2015   | Guard    | 8 (2014)     |"},{"metadata":{"_kg_hide-input":true,"trusted":true,"_kg_hide-output":true},"cell_type":"code","source":"Cluster_Name = 'Shot Taker'\ngroup_players = MPlayerSeasonStats_.query('Cluster_Name == @Cluster_Name')['PlayerID_Season'].unique()\ndisplay(MPlayerSeasonStats.loc[MPlayerSeasonStats['PlayerID_Season'] \\\n                       .isin(group_players)].sort_values('RSCI Top 100 Number') \\\n    [['FirstName','LastName','TeamName',\n      'Season','Position','RSCI Top 100']] \\\n        .drop_duplicates(subset=['FirstName','LastName']).head(10).style.hide_index())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# The Ball Distributor\n\nBall Distributors are unique in that they have many more assists than the other four groups. They tend to have lower average stats for defensive actions like rebounds and blocks."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"ax = MPlayerSeasonStats[['PlayerID_Season','AST']] \\\n    .merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['AST'].mean() \\\n    .sort_values() \\\n    .plot(kind='bar', rot=0)\nax.set_title('1.5x more assists than the next group', fontsize=15)\nax.set_ylabel('Assists Per Game')\nax.set_xlabel('')\nax.patches[4].set_color('orange')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"cluster = 'Ball Distributor'\n\nfig, axs = plt.subplots(1, 2, figsize=(14, 7))\naxs = axs.flatten()\n\nidx = 0\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nShots = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\naxs[idx] = create_ncaa_half_court(ax=axs[idx],\n                             three_line='mens',\n                             court_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nhb1 = axs[idx].hexbin(x=Shots.query('Y_ != 0')['Y_half_'],\n                 y=Shots.query('Y_ != 0')['X_half_'],\n                 gridsize=20, bins='log', cmap='inferno')\naxs[idx].set_title(f'Shot Attempts: {cluster}', size=15)\n\naxs[idx].set_ylim(0, 40)\naxs[idx].set_xlim(1, 49)\nplt.tight_layout()\n\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nMade = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\"')\naxs[idx+1] = create_ncaa_half_court(ax=axs[idx+1],\n                             three_line='mens',\n                             court_color='black',\n                                  lines_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\n#     hb1 = axs[idx].plot(x=Turnover.query('Y_ != 0')['Y_half_'],\n#                      y=Turnover.query('Y_ != 0')['X_half_'],\n#                      gridsize=10, bins='log', cmap='inferno')\nMade.query('Y_ != 0').plot(x='Y_half_',\n                 y='X_half_',\n              kind='scatter', ax=axs[idx+1], alpha=0.3)\naxs[idx+1].set_title(f'Made Shots: {cluster}', size=15)\n\naxs[idx+1].set_ylim(0, 40)\naxs[idx+1].set_xlim(1, 49)\naxs[idx+1].set_xlabel('')\naxs[idx+1].set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Notable \"Ball Distributors\"\n\n| FirstName | LastName   | TeamName | Season | Position | RSCI Top 100 |\n|-----------|------------|----------|--------|----------|--------------|\n| Lonzo     | Ball       | UCLA     | 2017   | Guard    | 3 (2016)     |\n| Trevon    | Duval      | Duke     | 2018   | Guard    | 5 (2017)     |\n| Andrew    | Harrison   | Kentucky | 2015   | Guard    | 5 (2013)     |\n| Tyus      | Jones      | Duke     | 2015   | Guard    | 7 (2014)     |\n| Isaiah    | Briscoe    | Kentucky | 2016   | Guard    | 11 (2015)    |\n| Kasey     | Hill       | Florida  | 2015   | Guard    | 11 (2013)    |\n| Troy      | Brown      | Oregon   | 2018   | Forward  | 12 (2017)    |\n| Derryck   | Thornton   | USC      | 2019   | Guard    | 13 (2015)    |\n| Wayne     | Selden Jr. | Kansas   | 2015   | Guard    | 13 (2013)    |\n| Tre       | Jones      | Duke     | 2019   | Guard    | 13 (2018)    |"},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"Cluster_Name = 'Ball Distributor'\ngroup_players = MPlayerSeasonStats_ \\\n    .query('Cluster_Name == @Cluster_Name')['PlayerID_Season'].unique()\ndisplay(MPlayerSeasonStats.loc[MPlayerSeasonStats['PlayerID_Season'] \\\n                       .isin(group_players)].sort_values('RSCI Top 100 Number') \\\n    [['FirstName','LastName','TeamName',\n      'Season','Position','RSCI Top 100']] \\\n            .drop_duplicates(subset=['FirstName','LastName']).head(10).style.hide_index())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# The Three Point Specialist\n\nThe name says it all. These players love to take three point shots. One of the most notable characteristics of a \"Three Point Specialist\" is that he attempts far fewer 2-pointers than does a \"Shot Taker\"."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"fig, (ax, ax1) = plt.subplots(1, 2, figsize=(15, 4))\nfig.suptitle('Large disparity between 3 and 2 point shots', fontsize=15, y=1.03)\nMPlayerSeasonStats[['PlayerID_Season','3P']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['3P'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax)\n# ax.set_title('Almost 2x more assists than the next group', fontsize=15)\nax.set_xlabel('3 Point Shots Per Game')\nax.set_ylabel('')\nax.patches[4].set_color('orange')\n\n\nMPlayerSeasonStats[['PlayerID_Season','2P']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['2P'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax1)\n# ax1.set_title('Almost 2x more assists than the next group', fontsize=15)\nax1.set_xlabel('2 Point Shots Per Game')\nax1.set_ylabel('')\nax1.patches[0].set_color('orange')\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cluster = '3 Point Specialist'\n\nfig, axs = plt.subplots(1, 2, figsize=(14, 7))\naxs = axs.flatten()\n\nidx = 0\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nShots = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\naxs[idx] = create_ncaa_half_court(ax=axs[idx],\n                             three_line='mens',\n                             court_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nhb1 = axs[idx].hexbin(x=Shots.query('Y_ != 0')['Y_half_'],\n                 y=Shots.query('Y_ != 0')['X_half_'],\n                 gridsize=20, bins='log', cmap='inferno')\naxs[idx].set_title(f'Shot Attempts: {cluster}', size=15)\n\naxs[idx].set_ylim(0, 40)\naxs[idx].set_xlim(1, 49)\nplt.tight_layout()\n\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nMade = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\"')\naxs[idx+1] = create_ncaa_half_court(ax=axs[idx+1],\n                             three_line='mens',\n                             court_color='black',\n                                  lines_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\n#     hb1 = axs[idx].plot(x=Turnover.query('Y_ != 0')['Y_half_'],\n#                      y=Turnover.query('Y_ != 0')['X_half_'],\n#                      gridsize=10, bins='log', cmap='inferno')\nMade.query('Y_ != 0').plot(x='Y_half_',\n                 y='X_half_',\n              kind='scatter', ax=axs[idx+1], alpha=0.3)\naxs[idx+1].set_title(f'Made Shots: {cluster}', size=15)\n\naxs[idx+1].set_ylim(0, 40)\naxs[idx+1].set_xlim(1, 49)\naxs[idx+1].set_xlabel('')\naxs[idx+1].set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Notable \"3 Point Specialists\"\n| FirstName | LastName  | TeamName    | Season | Position | RSCI Top 100 |\n|-----------|-----------|-------------|--------|----------|--------------|\n| Aaron     | Harrison  | Kentucky    | 2015   | Guard    | 6 (2013)     |\n| Malik     | Newman    | Kansas      | 2018   | Guard    | 8 (2015)     |\n| Quentin   | Grimes    | Kansas      | 2019   | Guard    | 8 (2018)     |\n| Rasheed   | Sulaimon  | Maryland    | 2016   | Guard    | 12 (2012)    |\n| Gary      | Trent Jr. | Duke        | 2018   | Guard    | 14 (2017)    |\n| Antonio   | Blakeney  | LSU         | 2016   | Guard    | 15 (2015)    |\n| Rodney    | Purvis    | Connecticut | 2017   | Guard    | 17 (2012)    |\n| Sam       | Dekker    | Wisconsin   | 2015   | Forward  | 19 (2012)    |\n| Jabari    | Bird      | California  | 2016   | Guard    | 20 (2013)    |\n| Joshua    | Langford  | Michigan St | 2017   | Guard    | 20 (2016)    |"},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"Cluster_Name = '3 Point Specialist'\ngroup_players = MPlayerSeasonStats_.query('Cluster_Name == @Cluster_Name')['PlayerID_Season'].unique()\ndisplay(MPlayerSeasonStats.loc[MPlayerSeasonStats['PlayerID_Season'] \\\n                       .isin(group_players)].sort_values('RSCI Top 100 Number') \\\n    [['FirstName','LastName','TeamName',\n      'Season','Position','RSCI Top 100']].drop_duplicates(subset=['FirstName','LastName']).head(10).style.hide_index())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# The Paint Dominator\nYou won't see these players putting up a lot of 3-pointers, but what they do well is control the paint. They come in right behing the \"Skilled Bigs\" in rebounds and blocks."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"fig, (ax, ax1) = plt.subplots(1, 2, figsize=(15, 4))\nfig.suptitle('Similar to the Skilled Big - with less shooting', fontsize=15, y=1.03)\nMPlayerSeasonStats[['PlayerID_Season','BLK']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['BLK'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax)\nax.set_xlabel('Blocks Per Game')\nax.set_ylabel('')\nax.patches[3].set_color('orange')\n\n\nMPlayerSeasonStats[['PlayerID_Season','2P']].merge(MPlayerSeasonStats_[['PlayerID_Season','Cluster_Name']]) \\\n    .groupby('Cluster_Name')['2P'].mean() \\\n    .sort_values() \\\n    .plot(kind='barh', rot=0, ax=ax1)\nax1.set_xlabel('2 Point Shots Per Game')\nax1.set_ylabel('')\nax1.patches[2].set_color('orange')\nplt.tight_layout()\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"cluster = 'Paint Dominator'\n\nfig, axs = plt.subplots(1, 2, figsize=(14, 7))\naxs = axs.flatten()\n\nidx = 0\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nShots = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\" or EventType == \"miss2\" or EventType == \"miss3\"')\naxs[idx] = create_ncaa_half_court(ax=axs[idx],\n                             three_line='mens',\n                             court_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nhb1 = axs[idx].hexbin(x=Shots.query('Y_ != 0')['Y_half_'],\n                 y=Shots.query('Y_ != 0')['X_half_'],\n                 gridsize=20, bins='log', cmap='inferno')\naxs[idx].set_title(f'Shot Attempts: {cluster}', size=15)\n\naxs[idx].set_ylim(0, 40)\naxs[idx].set_xlim(1, 49)\nplt.tight_layout()\n\nplayerid_seasons = MPlayerSeasonStats_.loc[MPlayerSeasonStats_['Cluster_Name'] == cluster]['PlayerID_Season'].unique()\nMade = MPlayerEvents.loc[MPlayerEvents['PlayerID_Season'].isin(playerid_seasons)] \\\n    .query('EventType == \"made2\" or EventType == \"made3\"')\naxs[idx+1] = create_ncaa_half_court(ax=axs[idx+1],\n                             three_line='mens',\n                             court_color='black',\n                                  lines_color='white',\n                             paint_alpha=0,\n                             inner_arc=True)\nMade.query('Y_ != 0').plot(x='Y_half_',\n                 y='X_half_',\n              kind='scatter', ax=axs[idx+1], alpha=0.3)\naxs[idx+1].set_title(f'Made Shots: {cluster}', size=15)\n\naxs[idx+1].set_ylim(0, 40)\naxs[idx+1].set_xlim(1, 49)\naxs[idx+1].set_xlabel('')\naxs[idx+1].set_ylabel('')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Notable \"Paint Dominators\"\n\n| FirstName | LastName   | TeamName  | Season | Position | RSCI Top 100 |\n|-----------|------------|-----------|--------|----------|--------------|\n| Kaleb     | Tarczewski | Arizona   | 2015   | Center   | 7 (2012)     |\n| Alex      | Poythress  | Kentucky  | 2016   | Forward  | 8 (2012)     |\n| Marques   | Bolden     | Duke      | 2019   | Center   | 11 (2016)    |\n| Kevon     | Looney     | UCLA      | 2015   | Forward  | 11 (2014)    |\n| Trey      | Lyles      | Kentucky  | 2015   | Forward  | 12 (2014)    |\n| Wenyen    | Gabriel    | Kentucky  | 2017   | Forward  | 13 (2016)    |\n| Omari     | Spellman   | Villanova | 2018   | Forward  | 17 (2016)    |\n| Dajuan    | Coleman    | Syracuse  | 2016   | Forward  | 18 (2012)    |\n| Nick      | Richards   | Kentucky  | 2018   | Forward  | 18 (2017)    |\n| Devin     | Robinson   | Florida   | 2017   | Forward  | 20 (2014)    |"},{"metadata":{"trusted":true,"_kg_hide-input":true,"_kg_hide-output":true},"cell_type":"code","source":"Cluster_Name = 'Paint Dominator'\ngroup_players = MPlayerSeasonStats_.query('Cluster_Name == @Cluster_Name')['PlayerID_Season'].unique()\ndisplay(MPlayerSeasonStats.loc[MPlayerSeasonStats['PlayerID_Season'] \\\n                       .isin(group_players)].sort_values('RSCI Top 100 Number') \\\n    [['FirstName','LastName','TeamName',\n      'Season','Position','RSCI Top 100']] \\\n        .drop_duplicates(subset=['FirstName','LastName']).head(10).style.hide_index())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Schools and Player Types\nTaking these clusters, lets take a look at some of the top basketball schools and which types of players their rosters have consisted of in recent seasons. The data below shows the numbers per teams since the 2015 season. It looks like certain schools may have a preference for specific types of players."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"cmap=sns.light_palette((210, 90, 60), input=\"husl\", as_cmap=True)\nTOP_BBALL_SCHOOLS = ['Kentucky','North Carolina','Duke','Villanova','Kansas','UCLA','Connecticut','Indiana',\n               'Louisville','Virginia','Florida']\nd = MPlayerSeasonStats_.query('TeamName in @TOP_BBALL_SCHOOLS') \\\n    .groupby(['TeamName','Cluster_Name'])['Position'].count() \\\n    .unstack('TeamName')\\\n    .T.sort_values(['3 Point Specialist','Ball Distributor','Paint Dominator','Shot Taker','Skilled Big']).T\nd.index.name = ''\nd.style.background_gradient(cmap='BuGn', axis=0)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## What Types of Rosters Fare well in March Madness?\nIt's also interesting to see if there are any differences in rosters amongst winning and losing tournament teams. To do this I simply count the number of players of each position for winning and losing teams and plot the difference. The results show that winning teams have more \"Skilled Big\" and \"Ball Distributor\" players. Losing teams however have much more \"Shot Taker\" players. This makes sense because shot takers don't tend to have high shooting percentage."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"# Add Team ID\nMTourneyResults = pd.read_csv('../input/march-madness-analytics-2020/MDataFiles_Stage2/MNCAATourneyCompactResults.csv')\nMPlayerSeasonStats_ = MPlayerSeasonStats_.merge(MTeams[['TeamID','TeamName']], how='left')\nTourneyWinTeams = MPlayerSeasonStats_[['TeamID','Season','Cluster_Name']] \\\n    .merge(MTourneyResults, left_on=['Season','TeamID'], right_on=['Season','WTeamID'])\nTourneyLossTeams = MPlayerSeasonStats_[['TeamID','Season','Cluster_Name']] \\\n    .merge(MTourneyResults, left_on=['Season','TeamID'], right_on=['Season','LTeamID'])\nTourneyTeams = TourneyWinTeams.groupby('Cluster_Name')[['TeamID']].count() \\\n    .rename(columns={'TeamID':'Winning Team Rosters'})\nTourneyTeams['Losing Team Rosters'] = TourneyLossTeams.groupby('Cluster_Name')[['TeamID']].count().values\nTourneyTeams['Diff'] = TourneyTeams['Winning Team Rosters'] - TourneyTeams['Losing Team Rosters']\nTourneyTeams['Winning Team Makeup'] = TourneyTeams['Winning Team Rosters'] / TourneyTeams['Winning Team Rosters'].sum()\nTourneyTeams['Losing Team Makeup'] = TourneyTeams['Losing Team Rosters'] / TourneyTeams['Losing Team Rosters'].sum()\nTourneyTeams['Makeup Diff'] = TourneyTeams['Winning Team Makeup'] - TourneyTeams['Losing Team Makeup']\nax = TourneyTeams['Makeup Diff'].plot(figsize=(15, 5), kind='barh', rot=0, title='Tourney Winning vs. Losing Team Roster Differential')\nax.axvline(0, color='black')\nax.grid(False)\nax.set_xlabel('<----- Losing Tourney Teams have more | Winning Tourney Teams have more ---->')\nax.set_ylabel('')\nax.patches[0].set_color('lightcoral')\nax.patches[1].set_color('seagreen')\nax.patches[2].set_color('lightcoral')\nax.patches[3].set_color('lightcoral')\nax.patches[4].set_color('seagreen')\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Interactive 3D Visualization of Clusters\nThe last visualization is a fun way to explore these clusters in 3D: Each dot in this plot represents a player/season. I used an algorithm called tSNE (t-distributed Stochastic Neighbor Embedding) - to reduce the data into three dimentions. You can use your mouse to rotate and zoom into the plot.  Hover over a dot to see the player's name and team."},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"sns.set_style(\"whitegrid\")\n\ntsne = TSNE(n_components=3, verbose=0, perplexity=2, n_iter=250, random_state=529)\nX_tsne = MPlayerSeasonStats_[FEATURES + ['Data_Position']].values\n\ntsne_results = tsne.fit_transform(X_tsne)\nMPlayerSeasonStats_['tsne-3d-one'] = tsne_results[:,0]\nMPlayerSeasonStats_['tsne-3d-two'] = tsne_results[:,1]\nMPlayerSeasonStats_['tsne-3d-three'] = tsne_results[:,2]","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"fig = px.scatter_3d(MPlayerSeasonStats_.merge(MPlayerSeasonStats[['PlayerID_Season','Height']]),\n                    x='tsne-3d-one',\n                    y='tsne-3d-two',\n                    z='tsne-3d-three',\n                    size_max=5,\n                    color='Cluster_Name',\n                    hover_data=[\"FirstName\", \"LastName\",\"TeamName\",\"Height\"],\n                    opacity=0.7)\n# fig.update_layout(hovermode=\"FirstName\")\nfig.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Thanks!\nThanks for taking the time to read my report. I hope you had as much fun reading it as I did making it!"},{"metadata":{"trusted":true},"cell_type":"markdown","source":"# References\n[1] https://www.espn.com/mens-college-basketball/story/_/id/9799167/traditional-positions-being-eliminated-college-basketball\n\nData Prep Notebooks:\n- https://www.kaggle.com/robikscube/march-madness-2020-sports-reference-com-scrape\n- https://www.kaggle.com/robikscube/rob-s-march-madness-2020-data-prep"}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4}