{"cells":[{"metadata":{},"cell_type":"markdown","source":"# Betting in the Play\n> ## Basketball Live Prediction & Analytics with Play-by-play data"},{"metadata":{},"cell_type":"markdown","source":"![intro_pic](https://www.legitgamblingsites.com/wp-content/uploads/2019/02/College-Concepts.png)"},{"metadata":{},"cell_type":"markdown","source":"# Introduction"},{"metadata":{},"cell_type":"markdown","source":"> ### All you need to know about in-play betting in basketball"},{"metadata":{},"cell_type":"markdown","source":"In-play betting, a.k.a live betting or in-play wagering, is simply wagering on a game while it’s happening. In-pkay betting explodes in U.S. lately as it is a really fun way that make bettors continue to be engaged in the game, and it gives chance bettors who misses placing a bet before the games begin. <sup>[1]</sup> In basketball, the in-play betting is mostly in the forms of betting in final scores, in spread points <sup>[2]</sup>, or in winning teams. \n\nHowever, in-play betting still has a strong potential to growth as it only takes less than 20% of the market in Nevada sportsbooks, <sup>[1]</sup>  which consits of most of legal betting sites in the state. One important reason is that a lot of sportsbook operators haven't acquire the technology to deplore it. \n\n[1]: https://www.thelines.com/betting/in-play/\n[2]: https://www.thelines.com/betting/point-spread/\n"},{"metadata":{},"cell_type":"markdown","source":">### Explore in-play betting with March Madness Data"},{"metadata":{},"cell_type":"markdown","source":"Here comes the opportunity for data science. In this notebook, I'm going to :\n> 1. Construct live prediction models minute by minute, utilizing linear regression models with L1 and L2 norm, to **predict the game final scores** with March Madness Men's play-by-play data.     \njump to conclusion : [live prediction models](#models)\n      \n> 2. **Explore the \"game changing\" events** and the best team strategy, e.g. when to make a technical foul or what is a good winning strategy in the last 10 minutes of the game,based on the regression results.     \njump to conclusion : [game changing events](#game-changing)\n\n> * and here is how I re-construct the play-by-play data into game-based data minute-by minute:       \n    jump to data aggragation : [data pre-processing](#data-preprocessing)"},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true,"_kg_hide-input":true,"_kg_hide-output":false,"scrolled":true},"cell_type":"code","source":"import numpy as np\nfrom time import time\nimport pandas as pd\nimport seaborn as sn\nimport matplotlib.pyplot as plt\nfrom sklearn import preprocessing\nfrom sklearn.linear_model import LinearRegression\nfrom sklearn.linear_model import Ridge\nfrom sklearn.linear_model import Lasso\nimport statsmodels.api as stat\nimport sys\nimport warnings  \n\nfrom statsmodels.genmod.generalized_estimating_equations import GEE\nfrom statsmodels.genmod.cov_struct import (Exchangeable,\n    Independence,Autoregressive)\nfrom statsmodels.genmod.families import Poisson\n\nwarnings.filterwarnings('ignore')\npd.set_option('display.max_columns', None)\npd.set_option('display.width', 1000)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Part 1 - Data Description"},{"metadata":{},"cell_type":"markdown","source":"> ### Exploring 2015-2019 March Madness men's play-by-play data"},{"metadata":{},"cell_type":"markdown","source":"As we are building up a live prediction model, every single play (i.e., miss a 2-point shoot, fouled by the other team, or a player is substitued by another player) matters to the team performance, thus affect the final scores and the winning odds. Therefore, play-by-play data is of great use in building up the model. \n\nLuckily, we have the March Madness MEvents & WEvents data from 2015-2019 that list the play-by-play event logs for more than 99.5% of games from that season<sup>[3]</sup> . Each event is assigned to either a team or a single one of the team's players.\n\nTo start with, let's get a breif description of 2015-2019 March Madness Men's play-by-play data, which is the **tarining data** I'm going to use in latter model construction . \n\n[3]: https://www.kaggle.com/c/march-madness-analytics-2020/data\n"},{"metadata":{},"cell_type":"markdown","source":"MEvents data consists of each play, events including the event types and the sub event types. For example, if a player performs a defensive rebound, the it is represented as : `EventType-reb` with the `SubEventType - def` (the second row of the data represents this specific play). "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"mens_events = []\nMENS_DIR = '/kaggle/input/march-madness-analytics-2020/MPlayByPlay_Stage2'\nfor year in [2015, 2016, 2017, 2018, 2019]:\n    mens_events.append(pd.read_csv(f'{MENS_DIR}/MEvents{year}.csv'))\nMEvents = pd.concat(mens_events)\n#print(\"2015-2019 Mevents data volume - {} columns, {} rows\".format(MEvents.shape[1],MEvents.shape[0]))\nMEvents.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"here is a brief description of all variables in the MEvents data, it consists of 13 million data entries 17 attributes spreading from 2014-15 season to 2018-19 season.     \nExcept for `EventSubType`, all data point is complete without missing value. As for the `EventSubType`, this is because the more and more sub events are added in the latter seasons. I'll fill all the NAs to 0 in the cumu-count aggregation process.   "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"def brief(data,columns):\n    print(\"-------------------------------------------------------------------------\")\n    print('data has {} rows {} attributes'.format(len(data),len(data.columns)))\n    print(\"-------------------------------------------------------------------------\") \n    print(data.columns)\n#     print(\"-------------------------------------------------------------------------\") \n#     print('data dtypes:\\n')\n#     print(data.dtypes)\n    print(\"-------------------------------------------------------------------------\")\n    describe = data[columns].describe().transpose().reset_index()\n    describe.rename(columns={'index':'Attribute_name'},inplace=True)\n    #print([data[col].nunique() for col in data.columns]) \n    pd.options.display.float_format = '{:,.2f}'.format\n    pd.options.display.width = 200\n    print(pd.DataFrame(describe[['Attribute_name','mean','std','min','max']]))\n    print(\"-------------------------------------------------------------------------\") \n    print(\"Unique values:\")\n    print([(col,data[col].nunique())for col in data.columns])\n    print(\"-------------------------------------------------------------------------\") \n    print(\"missing values:\")\n    print( \"; \".join([\"-\".join(x) for x in zip(data.columns,data.isna().sum(axis=0).astype(str).values)]))\n    print(\"-------------------------------------------------------------------------\")\n    print(\"Most common value top 3:\")\n    print([(col,str(data[col].value_counts().head(3).index).\\\n            strip('Int64Index([\\',\\'], dtype=\\'int64\\'))').strip('Float64Index([\\',\\'], dtype=\\'floa\"\\'))')\\\n            .strip('], dtype=\\'objec\")')) \\\n           for col in data.columns])\n    \ncolumns = ['Season', 'DayNum','WFinalScore', 'LFinalScore', \n           'WCurrentScore', 'LCurrentScore', 'ElapsedSeconds',\n           'X', 'Y', 'Area']\nbrief(MEvents,columns)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"In 2015-2019 Men play-by-play data, there are altogether 16 events, followed by 52 subevents (and one `SubEventType` as nan). "},{"metadata":{"_kg_hide-input":true,"trusted":true,"scrolled":true},"cell_type":"code","source":"def categorical(data):\n    for col in data.columns:\n        unique = pd.unique(data[col])\n        print(\"there are {} unique value of variable {}:\".format(len(unique),col))\n        print(unique)\ncategorical(MEvents[['EventType','EventSubType']])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Here are the top-10 most frequent `EventType` and `EventSubType` in MEvents as a glimpse to the common events happening in the basketball games: "},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def plot_most_freq(Events,col):\n    plt.style.use('fivethirtyeight')\n    Events['counter'] = 1\n    Events.groupby(col)['counter'] \\\n    .sum() \\\n    .sort_values(ascending=False).iloc[:10] \\\n    .plot(kind='bar',\n         #color=mypal[2],\n        color='#ed6663',alpha = 0.7,label=col+' frequency')\n    \n    plt.xticks(rotation=1)\n    \n#fig = plt.figure(1)\n# plt.rcParams['xtick.labelsize']=18\n# plt.rcParams['ytick.labelsize']=18\nplt.style.use('fivethirtyeight')\nf, axs = plt.subplots(1,1,figsize=(20,10))\nplot_most_freq(MEvents,'EventType')\nplt.title('TOP 10 Most Freqent Events',fontsize = 24)\nplt.xlabel('Event Type',fontsize=20)\nplt.ylabel('Counts',fontsize=20)\nplt.legend(fontsize=20)\naxs.tick_params(axis='both', which='major', labelsize=20)\nplt.style.use('fivethirtyeight')\nf, axs = plt.subplots(1,1,figsize=(20,10))\nplot_most_freq(MEvents,'EventSubType')\nplt.title('TOP 10 Most Freqent Sub Events',fontsize=24)\nplt.xlabel('Sub Event Type',fontsize=20)\nplt.ylabel('Counts',fontsize=20)\naxs.tick_params(axis='both', which='major', labelsize=20)\nplt.legend(fontsize=20)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Also, as time is a very important variable in the model construction, let's explore more about the `ElapsedSeconds` variable in the dataset. `ElapsedSeconds` represents the number of seconds that have elapsed from the start of the game until the event occurred. With a 20-minute half, that means that an `ElapsedSeconds` value from 0 to 1200 represents an event in the first half, a value from 1200 to 2400 represents an event in the second half, and a value above 2400 represents an event in overtime. For example, since overtime periods are five minutes long (that's 300 seconds), a value of 2699 would represent one second left in the first overtime. <sup>[3]</sup>     \n\nUnlike normal basketball games that consist of 4 quarters, college basketball games consist of 2 halves, thus the main game time is on average 40 mins. As shown in the histogram, the major recorded `ElapsedSeconds` are distributed before 2400 seconds. The time between 2401 and 3600 seconds is the overtime game, and we can see a significant data-size shrink in overtime. \n\n[3]:https://www.kaggle.com/c/march-madness-analytics-2020/data"},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"fig = plt.figure(figsize = (20,10))\nplt.hist(MEvents['ElapsedSeconds'],bins=60,\n         label=\"data frequency\",\n         color = '#0f4c81',\n         alpha = 0.3,density = False)\n\nplt.title('Distribution of Elapsed Seconds',fontsize=24)\nplt.xlabel('Elapsed Seconds',fontsize=20)\nplt.ylabel('Frequency',fontsize=20)\nplt.style.use('fivethirtyeight')\nplt.legend(fontsize=20)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Part 2 - Data Pre-processing"},{"metadata":{},"cell_type":"markdown","source":"> ### Game-based aggregation in every minute"},{"metadata":{},"cell_type":"markdown","source":"<div id='data-preprocessing'>\nAs we are going to predict the game-by-game result, the first step of data engineering is transforming the play-by-play data to game-based data.\nHere I used the frequency as the aggregation method, calculating the cumulative frequency for each `EventType` and `SubEventType` grouped by `ElapsedMinutes` in each game. \n</div>\n"},{"metadata":{},"cell_type":"markdown","source":"To aggregate the game-based data in each minute for both the winning team and losing team in the game, we can: \n \n> step 1 : aggregate the frequency of each Event & subevent grouping by season, date, winning team id losing team id, event team id and elapsed time in minutes    \n     \n> step 2 : merge two team's data based on season, day number, winning team id losing team id    \n      \n> step 3 : then, each game will be represented by 2 data rows, one for winning team and one for losing team. The `FinalScore` is the dependent variable that we'd like to predict.     \n     \n> step 4 : in each data entry, events with \"_x\" represent the current team and its id is `EventTeamID_x`; events with \"_y\" represents the opposite team in the game with the id as `EvenTeamID_y`     "},{"metadata":{"_kg_hide-input":true,"trusted":true,"scrolled":true},"cell_type":"code","source":"def get_game_based_dat(event_df,gp_para,type_para):\n    '''\n    create game-based event data\n    '''\n    ## for EventType\n    event_df['count'] = 1\n    type_para.remove('EventSubType')\n    events = event_df[type_para+['count']].groupby(gp_para+['EventType']).sum()\n    events = events.unstack(level=-1).fillna(0)\n    events = pd.DataFrame(events.to_records())\n    events.columns = [hdr.replace(\"('count', \", \"\").replace(\")\", \"\").replace('\\'','') \\\n                         for hdr in events.columns]\n\n    ## for EventSubType\n    type_para.remove('EventType')\n    type_para.append('EventSubType')\n    sub_events = event_df[type_para+['count']].groupby(gp_para+['EventSubType']).count()\n    sub_events = sub_events.unstack(level=-1).fillna(0)\n    sub_events = pd.DataFrame(sub_events.to_records())\n    sub_events.columns = [hdr.replace(\"('count', \", \"sub_\",).replace(\")\", \"\").replace('\\'','') \\\n                         for hdr in sub_events.columns]\n    \n    # merge events and subevents\n    events = events.merge(sub_events)\n\n    winevent = events[events['WTeamID']==events['EventTeamID']]\n    losevent = events[events['LTeamID']==events['EventTeamID']]\n\n    gp_para.remove('EventTeamID')\n    win_merge = winevent.merge(losevent,left_on = gp_para,right_on = gp_para)\n    win_merge['victory'] = 1\n    los_merge = losevent.merge(winevent,left_on = gp_para,right_on = gp_para)\n    los_merge['victory'] = 0\n    gameEvents = pd.concat([win_merge,los_merge],axis = 0)\n\n    return gameEvents\n\ndef get_final_scores(df,gp_para,target_para):\n    type_para = gp_para + target_para\n    final_score = df[type_para].groupby(gp_para).max()\n    final_score = pd.DataFrame(final_score.to_records())\n      \n    final_score['victory'] = 1\n    w_final_score = final_score[gp_para+['victory',target_para[0]]].\\\n    rename(columns={target_para[0]:target_para[0][1:]})\n    \n    l_final_score = final_score\n    l_final_score['victory'] = 0\n    l_final_score = final_score[gp_para+['victory',target_para[1]]].\\\n    rename(columns={target_para[1]:target_para[1][1:]})\n    \n    final_score = pd.concat([w_final_score,l_final_score],axis = 0)\n    \n    return final_score        \n\ndef get_current_scores(df,gp_para,target_para):\n    type_para = gp_para + target_para\n    final_score = df[type_para].groupby(gp_para).max()\n    final_score = pd.DataFrame(final_score.to_records())\n     \n    final_score['victory'] = 1\n    w_final_score = final_score.\\\n    rename(columns={target_para[0]:target_para[0][1:]+'_x',target_para[1]:target_para[1][1:]+'_y'})\n    l_final_score = final_score\n    l_final_score['victory'] = 0\n    l_final_score = final_score.\\\n    rename(columns={target_para[0]:target_para[0][1:]+'_y',target_para[1]:target_para[1][1:]+'_x'})\n    final_score = pd.concat([w_final_score,l_final_score],axis = 0)\n\n    return final_score\n    \n\ndef get_timebucket():\n    time_bucket = {min:[i for i in range((min-1)*60+1,min*60+1)] for min in range(1,61)}\n    time_bucket[1].append(0)\n    return time_bucket\n\ndef find_bucket(x,time_bucket):\n    for key in time_bucket.keys():\n        if x in time_bucket[key]:\n            return key\n    return None\n\ndef pre_processing(events,gp_para,type_para):\n    time_bucket = get_timebucket()\n    events['ElapsedMinutes'] = events['ElapsedSeconds'].apply(lambda x: find_bucket(x,time_bucket))\n    gameEventsMin = get_game_based_dat(events,gp_para,type_para)\n    cumu_cols = set(gameEventsMin.columns)-{'EventTeamID_x','EventTeamID_y','ElapsedMinutes'}\n    cumuEventsMin = gameEventsMin[cumu_cols].groupby(['Season','DayNum','WTeamID','LTeamID','victory']).cumsum()\n    \n    EventsMin = pd.concat([gameEventsMin[['Season','DayNum',\n              'WTeamID','LTeamID','EventTeamID_x',\n              'EventTeamID_y','ElapsedMinutes','victory']],cumuEventsMin],axis = 1)\n    \n    final_score = get_final_scores(events,\n                             gp_para = ['Season','DayNum','WTeamID','LTeamID'],\n                             target_para = ['WFinalScore','LFinalScore'])\n    current_score = get_current_scores(events,\n                               gp_para = ['Season','DayNum','WTeamID','LTeamID','ElapsedMinutes'],\n                             target_para = ['WCurrentScore','LCurrentScore'])\n    \n    EventsMin = EventsMin.merge(final_score)\n    EventsMin = EventsMin.merge(current_score)\n     \n    return EventsMin\n\n\ndef get_column_info(gameEvents):\n    g_info = gameEvents.columns[:4].values\n    x_info = [i for i in gameEvents.columns.values if '_x' in i]\n    x_info.remove('CurrentScore_x')\n    x_info.remove('EventTeamID_x')\n    y_info = [i for i in gameEvents.columns.values if '_y' in i]\n    y_info.remove('CurrentScore_y')\n    y_info.remove('EventTeamID_y')\n    return x_info,y_info\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"After data pre-processing, our data structure changed to the frame below, consist of 2.2 million data entries.    \nWith the aggregation process, each game-based data row consists of both winning team play-by-play information and losing team play-by-play information, as well as their current scores.  "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"full_mins_dat = []\nfor season in [2015,2016,2017,2018,2019]:\n    gp_para = ['Season','DayNum','WTeamID','LTeamID','EventTeamID','ElapsedMinutes']\n    type_para = gp_para + ['EventType','EventSubType']\n    game_season = MEvents[MEvents['Season'] == season]\n    #print(game_season.shape)\n    game_min_dat = pre_processing(game_season,gp_para,type_para)\n    full_mins_dat.append(game_min_dat)\n\nEvents_Min = pd.concat(full_mins_dat,axis = 0)\n    ","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true,"trusted":true,"scrolled":true},"cell_type":"code","source":"print(\"the aggregated event shape is :\", Events_Min.shape)\nx_info,y_info = get_column_info(Events_Min)\nprint(\"game info: \\n\",gp_para+['EventTeamID_y','EventTeamID_x'])\nprint(\"team x info: \\n\",x_info)\nprint(\"team y info: \\n\",y_info)\ny_labels = list(set(Events_Min.columns)-set(x_info)-set(y_info)\\\n                -set(gp_para)-{'EventTeamID_y','EventTeamID_x'})\nprint(\"dependent variables: \\n\",y_labels)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Here is the data distribution in minutes, just as shown in the second distribution before, major data points regarding minute spread in the area before 40 minutes as a full game is approximately 40-minute long.  Events that happened between 41 minutes to 60 minutes are considered to be in the overtime game. But with a significant drop in data sample size, I will **focus on the main game prediction** in this report.   "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"fig = plt.figure(figsize=(20,10))\nplt.hist(Events_Min['ElapsedMinutes'],bins=60,\n         label=\"data frenquency\",\n         color = '#0f4c81',\n         alpha = 0.5)\nplt.title('Data Distribution in Minutes',fontsize=24)\nplt.xlabel('Elapsed Minutes',fontsize=20)\nplt.ylabel('Frequency',fontsize=20)\nplt.style.use('fivethirtyeight')\nplt.legend(fontsize=20)\nplt.show()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Part 3 : In-play Live Modeling"},{"metadata":{},"cell_type":"markdown","source":">### Modeling with Linear Regression"},{"metadata":{},"cell_type":"markdown","source":"<div id=\"models\">\nFirst of, with all the data aggregated, let's construct linear regression models with L1 and L2 norm.  \nInstead of generating a single regression model, I generated 40 models using the cumulative aggregated data in each minute of the main game with Lasso, Ridge regression models, compared with default models that use the average as the prediction. We are expecting the model prediction is more and more accurate when the game keeps going on.    \n</div>"},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"# cross validation\ndef cv_split(data,kfold):\n    dat = data.sample(frac=1,random_state=888).reset_index()\n    dat = dat.iloc[:,1:]\n    fsize = len(dat)//kfold\n    lsize = len(dat)%kfold\n    fold_label = []\n    for k in range(kfold-1):\n        fold_label += [k for i in range(fsize)]\n    fold_label += [kfold-1 for i in range(fsize+lsize)]\n    dat['kfold'] = fold_label\n    return dat\n\n\n# Ridge Regression\ndef get_pred_ridge(train,test,penalty=None):\n    lr = Ridge()\n    lr.fit(X=train.iloc[:,:-1],y=train.iloc[:,-1])\n    predict = pd.DataFrame(data= lr.predict(test.iloc[:, :-1]),columns=['prediction'])\n    predict['true output'] = np.array(test.iloc[:,-1]).reshape(len(test),1)\n    return predict\n\n\n# Lasso Regression\ndef get_pred_lasso(train,test,penalty=None):\n    lr = Lasso()\n    lr.fit(X=train.iloc[:,:-1],y=train.iloc[:,-1])\n    predict = pd.DataFrame(data= lr.predict(test.iloc[:, :-1]),columns=['prediction'])\n    predict['true output'] = np.array(test.iloc[:,-1]).reshape(len(test),1)\n    return predict\n\ndef cv_mse(cv_data,model,para):\n    agg_pre = pd.DataFrame(columns=['prediction','true output'])\n    for k in cv_data.iloc[:,-1].unique():\n        test = cv_data[cv_data.iloc[:,-1]==k].iloc[:,:-1]\n        #print(test.iloc[:4,-1])\n        train = cv_data[cv_data.iloc[:,-1]!=k].iloc[:,:-1]\n        #print(train.iloc[:4,-1])\n        pred = model(train,test,para)\n        agg_pre = pd.concat([agg_pre,pred],axis = 0)\n    mse = np.mean((agg_pre['prediction'] - agg_pre['true output'])**2)\n    return mse\n\n\ndef lr_by_time(df,train_label,model,interval,kfold):\n    iteration = df['ElapsedMinutes'].max()//interval\n    #print(iteration)\n    mse_df = []\n    for i in range(1,iteration+1):\n        train_dat = df[df['ElapsedMinutes']==i*interval][train_label]\n        #print(\"data szie in {} minutes : {}\".format(i*interval,train_dat.shape[0]))\n        cv_dat = cv_split(train_dat,kfold)\n        mse = cv_mse(cv_dat,model,None)\n        mse_df.append([i*interval,mse,train_dat.shape[0]])\n    mse_df = pd.DataFrame(mse_df,columns = ['ElapsedMinutes','MSE','DSize'])\n    return mse_df        \n\n\ndef get_default_mode(df,interval):\n    iteration = df['ElapsedMinutes'].max()//interval\n    mse_df = []\n    for i in range(1,iteration+1):\n        train_dat = df[df['ElapsedMinutes']==i*interval][train_label]\n        predict_val = train_dat['FinalScore'].mean()\n        mse = np.mean((train_dat['FinalScore'] - predict_val)**2)\n        mse_df.append([i*interval,mse])\n    mse_df = pd.DataFrame(mse_df,columns = ['ElapsedMinutes','MSE'])\n    return mse_df\n        \n\ncols = ['#1b262c','#0f4c81','#888888']\ndef get_reg_fig(df_list,model_name,cols):\n    plt.style.use('fivethirtyeight')\n    i = 0\n    f, axs = plt.subplots(1,1,figsize=(20,10))\n    for df in df_list:\n        df_main = df[df['ElapsedMinutes']<=40]  \n        plt.plot(df_main['ElapsedMinutes'],df_main['MSE'].apply(np.sqrt),\n                 label=model_name[i],color = cols[i],alpha=0.8)\n        i += 1\n    plt.title('Models RMSE in Main Game',fontsize = 24)\n    plt.xlabel('Elapsed Minutes',fontsize=20)\n    plt.ylabel('RMSE',fontsize=20)\n    axs.tick_params(axis='both', which='major', labelsize=20) \n    plt.legend(fontsize=20)\n    '''\n    f, axs = plt.subplots(1,1,figsize=(20,7))\n    i = 0    \n    for df in df_list:\n        df_over = df[df['ElapsedMinutes']>40]\n        plt.plot(df_over['ElapsedMinutes'],df_over['MSE'].apply(np.sqrt),\n                 label=model_name[i],color = cols[i],alpha=0.8)\n        plt.title('Models RMSE in Overtime Game')\n        i += 1\n    plt.legend()\n    '''\n\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The model prediction result follows our expectation, that with more data aggregated when the game marching forward.   \nAccording to the RMSE result, the is likely to make a [-10,+10] prediction mistake to the final scores in the first 10 minutes. Later on, the prediction mistake interval decreased to approximately [-8,+8] with the event data of the first half. The prediction mistake interval keeps decreasing to approximately [-6,+6] before the final betting time for most of the live wagering - 5 minutes before the game ends. "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"Events_Min.shape\nEvents_Min = Events_Min.fillna(0)\n\nx_info,y_info = get_column_info(Events_Min)\ntrain_label = x_info + y_info +['FinalScore']        \n\nmse_df_ridge = lr_by_time(Events_Min[Events_Min['ElapsedMinutes']<=40],train_label,get_pred_ridge,1,20)\nmse_df_lasso = lr_by_time(Events_Min[Events_Min['ElapsedMinutes']<=40],train_label,get_pred_lasso,1,20)\nmse_df_default = get_default_mode(Events_Min[Events_Min['ElapsedMinutes']<=40],1)\ndf_list = [mse_df_ridge,mse_df_lasso,mse_df_default]\n\n# default model regression \nmodel_name = ['Ridge','Lasso','default']\nget_reg_fig(df_list,model_name,cols)\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Now it is time for validating the model in 2020 men's play-by-play data. \nThe result turns out to be pretty good, with no over-fitting in the testing data. The reason for the low error rate for the 2020 validation data could be the cancelation of the 2020 season. Compare the 2 default models (take the average as prediction), we can see that the 2020 `FinalScore` mean is lower than the mean of 2015-2019. This could result from that the 2020 season got canceled in the middle due to the pandemic, thus the data distribution is different from the previous seasons.  "},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def validation_mse(df,test,train_label,m_instance,interval):\n    iteration = df['ElapsedMinutes'].max()//interval\n    #print(iteration)\n    mse_ls = []\n    for i in range(1,iteration+1):\n        train_dat = df[df['ElapsedMinutes']==i*interval][train_label]\n        test_dat =  test[test['ElapsedMinutes']==i*interval][train_label]\n        lr = m_instance\n        X = train_dat.iloc[:,:-1]\n        y = train_dat.iloc[:,-1]\n        lr.fit(X,y)\n        pred = pd.DataFrame(data= lr.predict(test_dat.iloc[:, :-1]),columns=['prediction'])\n        pred['true output'] = np.array(test_dat.iloc[:,-1]).reshape(len(test_dat),1)\n        mse = np.mean((pred['true output']-pred['prediction'])**2)\n        mse_ls.append([i*interval,mse])\n    mse_df = pd.DataFrame(mse_ls,columns=['ElapsedMinutes','MSE'])\n    return mse_df      \n\ngp_para = ['Season','DayNum','WTeamID','LTeamID','EventTeamID','ElapsedMinutes']\ntype_para = gp_para + ['EventType','EventSubType']\n# get 2020 data\nyear = '2020'\nEvents_20 = pd.read_csv(f'{MENS_DIR}/MEvents{year}.csv')\nEvents_20_Min = pre_processing(Events_20,gp_para,type_para)\n\nx_info,y_info = get_column_info(Events_Min)\ntrain_label = x_info + y_info +['FinalScore']\n\nmse_validation = validation_mse(Events_Min[Events_Min['ElapsedMinutes']<=40],\n                                  Events_20_Min[Events_20_Min['ElapsedMinutes']<=40],train_label,Ridge(),1)\n\ncols = ['#1b262c','#f79071','#888888','#ffc38b']\n#rmse_validation['MSE'] = rmse_validation['RMSE'].apply(lambda x: x**2)\nmse_df_default_20 = get_default_mode(Events_20_Min[Events_20_Min['ElapsedMinutes']<=40],1)\ndf_list = [mse_df_ridge,mse_validation,mse_df_default,mse_df_default_20]\nmodel_name = ['Training set - 2015~19','Testing set - 2020','default with 2015~19 data',\n              'default with 2020 data']\nget_reg_fig(df_list,model_name,cols)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Part 4 : Go Further from Linear Regression"},{"metadata":{},"cell_type":"markdown","source":">### What are the game-changing moments?"},{"metadata":{},"cell_type":"markdown","source":"<div id=\"game-changing\">\nThe next question is, what are the most important play-by-play events that influenced the final score?   \nNext, by further examing the coefficients in the models, let's explore what are the game-changing moments in the game.  \n</div>"},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"def critical_vars(df,train_label,m_instance,interval):\n    iteration = df['ElapsedMinutes'].max()//interval\n    #print(iteration)\n    coef = []\n    for i in range(1,iteration+1):\n        train_dat = df[df['ElapsedMinutes']==i*interval][train_label]\n        #print(\"data szie in {} minutes : {}\".format(i*interval,train_dat.shape[0]))\n        lr = m_instance\n        X = train_dat.iloc[:,:-1]\n        y = train_dat.iloc[:,-1]\n        lr.fit(X,y)\n        c_df = pd.DataFrame(zip([i*interval for n in range(len(X))],X.columns, lr.coef_),\n                        columns=['ElapsedMinutes','variables','coef']).\\\n        sort_values('coef',ascending=False).dropna()\n        c_df = pd.concat([c_df[:5],c_df[-5:]],axis = 0)\n        #print(c_df)\n        coef.append(c_df)\n    #coef_df = pd.concat(coef,axis = 1)\n    return coef","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's look into the Ridge regression result for the main game.        \n        \nThe table below displays the top 5 events that have the most positive effect for a team's final score during the main game, as well as top 5 events that have the most negative effect for a team's final score, according to the coefficients of the Ridge Regressions from 10-mins data, 20-mins data, 30-mins data, and 40-mins data.     \n    \n    \nAltogether, 24 events can be considered as **game-changing** during the main game. With the time-to-time coefficient change information given by the model, there are a lot of interesting facts that we can implicate from the data, most of them are strongly related to how to adjust team strategy over time and of the great importance of winning the game. "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"x_info,y_info = get_column_info(Events_Min)\ntrain_label = x_info + y_info +['FinalScore'] \n\ncoef = critical_vars(Events_Min.query('ElapsedMinutes<=40'),train_label,Ridge(),10)\ncoef_df = pd.concat(coef,axis =1).fillna('na').reset_index().drop('index',axis = 1)\nfull_coef = critical_vars(Events_Min.query('ElapsedMinutes<=40'),train_label,Ridge(),1)\nfull_coef_df = pd.concat(full_coef,axis =0).fillna('na')\ncoef_df","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Here are the interesting and important facts that we can implicate from the data:\n> **1. It is very important to make a 3-point shot in the beginning!**    \nThe more time elapsed, the less important a 3 points shot is, as the `coef.` drops from **3.33** to **2.69** while a game is playing.   \nWhile 2-point shots are worth their values during the middle of the game; and a 1-point shot (mostly for a free shot) is vital at the last minute of the game, occurs only once in the top-5 board in the 40-minute regression model with the coefficient of **0.85**.  "},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"cols = ['#00a8cc','#0f4c81','#ed6663','#f79071','#1b262c']\ndef get_coef_fig(var_names,coef_df,cols):\n    plt.style.use('fivethirtyeight')\n    i = 0\n    f, axs = plt.subplots(1,1,figsize=(20,10))\n    for var in var_names:\n        tmp = coef_df[coef_df['variables']==var]\n        if len(tmp)>1:\n            plt.plot(tmp['ElapsedMinutes'],tmp['coef'],\n                 label=var,color = cols[i],alpha=0.8)\n            i += 1\n       \n        elif len(tmp)==1:\n            plt.plot(tmp['ElapsedMinutes'],tmp['coef'],\n                 label=var,color = cols[i],alpha=0.8, marker='o', markersize=10)\n            i += 1\n    plt.title('Coefficients for '+ ','.join(var_names),fontsize=24)\n    plt.xlabel('Elapsed Minutes',fontsize=20)\n    plt.ylabel('Coefficient',fontsize=20)\n    plt.legend(fontsize = 20)\n    axs.tick_params(axis='both', which='major', labelsize=20)\n    plt.show()\n\n\nmade = ['made1_x','made2_x','made3_x']\nget_coef_fig(made,full_coef_df,cols)\n#tmp = full_coef_df[full_coef_df['variables'].isin(made)]\n#fig, ax = plt.subplots(figsize=(15,7))\n#tmp.groupby('variables').plot('ElapsedTime','coef')\n\n","execution_count":null,"outputs":[]},{"metadata":{"_kg_hide-input":true},"cell_type":"markdown","source":"> **2. Don't make the 5-second violation in the beginning.**     \nThe effect of making a 5-second violation (`sub_5sec_x`)in the first 5 minutes is disastrous, which has the potential to \ncause more than 6 points lost to the final score.      \nHowever, in the second half of the game, a 5-second violation seems to have a positive effect on the final score.  "},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"sec = ['sub_5sec_x']\nget_coef_fig(sec,full_coef_df,cols = ['#0f4c81'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> **3. A foul act could be harmful, even for technical ones.**    \nA bench technical foul (identified as `sub_bente_x`) is beneficial in the middle of the game. with a `coef.` of 2.63.    \nHowever, a team should be careful about the foul act, even if it is a technical foul (identified as `sub_admte_x`), as the administrative-technical foul is harmful with the `coef.` of -1.63 in the first half of the game.   "},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"foul = ['sub_bente_x','sub_admte_x']\nget_coef_fig(foul,full_coef_df,cols=['#0f4c81','#ed6663'])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> **4. It is okay to lose the jump balls when the game approaches the end.**   \n    The side effect of losing a jump ball (`sub_lost_x`) is mitigated as the game gets more and more intense in the end.   "},{"metadata":{"_kg_hide-input":true,"trusted":true},"cell_type":"code","source":"jumpball = ['sub_lost_x']\nget_coef_fig(jumpball,full_coef_df,cols=['#0f4c81','#ed6663'])\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> **5. Don't give a 3-free-throw chance to the other team in the last 10 minutes**   \nIn the last 10 minutes, even if your opponent misses a free shot out of 3 free throws, your final score will still get effected by -1 score. The benefit of your opponent missing 3 out of 3 to your team's final score decreases in the last 10 minutes."},{"metadata":{"trusted":true,"_kg_hide-input":true},"cell_type":"code","source":"missfree = ['sub_1of3_y','sub_2of3_y','sub_3of3_y']\nget_coef_fig(missfree,full_coef_df,cols=['#00a8cc','#0f4c81','#ed6663'])\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"> References:     \n[1] : What Is In Play Sports Betting?\nhttps://www.thelines.com/betting/in-play/    \n[2] :  What Is Point Spread Betting?\nhttps://www.thelines.com/betting/point-spread/    \n[3] : Google Cloud & NCAA® March Madness Analytics - Data Description\nhttps://www.kaggle.com/c/march-madness-analytics-2020/data"}],"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}