{"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_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style=\"background-color:cyan\"> <font size=\"5\"> <b> 0. Outline </b> </font> </div>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> 1. Introduction </b> </font> </div>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> 2. Estimate Expected Finish Position (EFP) </b> </font> </div>\n<ol>\n2.1 \tWhat is Expected Finish Position and why should we care about it?<br>\n2.2 \tFeature Space: how is Expected Finish Position estimated?<br>\n2.3 \tThe EFP Model<br>\n</ol>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> 3. Aggregated Expected Finish Position as a proxy of jockey performance </b> </font> </div>\n<ol>\n3.1 \tHow does Expected Finish Position change throughout races?<br>\n3.2 \tAggregated EFP and Decayed Aggregated EFP: a proxy of jockey performance<br>\n</ol>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> 4. Model explainability to detect beneficial and harmful patterns </b> </font> </div>\n<ol>\n4.1 \tWhat features do influence HORACE EFP predictions the most?<br>\n4.2 \tWhat is the impact of features on Expected Finish Position in different stages of a race?<br>\n4.3 \tAggregated EFP and Decayed Aggregated EFP: a proxy of jockey performance<br>\n</ol>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> 5. Wrap-up and future directions </b> </font> </div>\n<div style=\"background-color:lightcyan\"> <font size=\"3\"> <b> References </b> </font> </div>\n","metadata":{}},{"cell_type":"markdown","source":"___\n\n# <div style=\"background-color:cyan\"> <b>1.0 Introduction </b> </div>\n\nWe devised a <b>compound assessment framework</b> that learns from the data made available by NYRA and NYTHA and generates a number of metrics and sub-metrics for the evaluation of horse-jockey behavior and the in-depth explainability of horse racing patterns. \n\nThe developed approach is divided into 4 substeps:\n1. A methodology to estimate Expected Finish Position (EFP) is established and validated\n2. A metric founded on EFP predictions is devised to evaluate and compare horse-jockey performance\n3. What are the features that affect EFP the most? Deepdive into model explainability to detect strengths and weaknesses of jockey behavior.\n4. Develop an EFP estimation model that exclusively adopts speed/acceleration features and discards positional factors.","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport plotly\nimport plotly.express as px\nimport plotly.graph_objects as go\nplotly.offline.init_notebook_mode(connected = True)\n\n!pip install utm\nimport utm\nfrom tqdm.notebook import tqdm\ntqdm.pandas()\nimport copy\nimport math\nimport gc\nfrom sklearn.model_selection import train_test_split, GroupShuffleSplit\nfrom catboost import CatBoostRegressor\nimport shap\nshap.initjs()\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"___\n# <div style=\"background-color:cyan\"> <b>2.0 Estimate Expected Finish Position (EFP) </b> </div>\n","metadata":{}},{"cell_type":"markdown","source":"### 2.1 What is Expected Finish Position and why should we care about it?\nExpected Finish Position represents where, in a rank composed of all the horse-jockey pairs participating in a horse race and at a given trakus index, a given horse-jockey pair is expected to place itself in view of the features characterizing that specific trakus index; these features include horse-jockey pair -specific trakus-dependent properties (current position, current speed, cumulative speed reached so far, ...), opponents' trakus-dependent properties, and structural trakus-independent features of the race and horse-jockey pairs themselves.\n\nRaces have a mutable number of horse-jockey pairs, though, hence modelling the EFP as a multiclass response variable is conceptually tough. To overcome this obstacle, we decide to map rankings in continuous bins in the 0-1 range, training the model on this response variable, then make predictions in batch (all horse-jockey pairs per each batch) and re-map them back to discrete Expected Finish Position values by accounting for the conditional ranking of continuous scores. For example, a race composed of 5 horse-jockey pairs should model final positioning with micro-ranges of 1/(5-1) = 0.25: thus ending up with an original positioning vector of (1, 2, 3, 4, 5) being converted to (0, 0.25, 0.50, 0.75, 1). For models to effectively be able to draw meaningful estimates, the number of total participants should be included as a feature.\nThe formula to execute the continuization of the response variable is the following:\n\n<div style='text-align:center'><font size=4> $\\lambda = \\frac{1}{N-1} $</font></div><br>\n<div style='text-align:center'><font size=4> $ y_{remapped} = \\lambda(y - 1) = \\frac{y - 1}{N - 1}$,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$y \\in \\mathbb{N}, y\\in[1, N]$</font></div><br>\n\nWhere:\n* $\\lambda $ is the scaling factor\n* $N$ is the number of horse-jockey pairs participating in the race\n\n<font size=2><i>Note: the remapping preserves the original \"the lower the better\" fashion of the original response variable.</i></font>","metadata":{}},{"cell_type":"markdown","source":"### 2.2 Feature Space: how is Expected Finish Position estimated?\n\nFor each race, an Expected Finish Position is estimated after each 0.25 seconds (i.e., for each trakus index value) for every single horse-jockey pair. The following features are used by the machine learning model to estimate EFP:\n- Seconds passed since race start\n- Current horse-jockey pair position (w.r.t. opponent pairs)\n- Best position reached\n- Worst position reached\n- Total race distance\n- Distance travelled by horse-jockey pair so far\n- Max speed reached so far\n- Current speed\n- Cumulative speed\n- Average Speed\n- Current direction\n- Previous trakus direction (i.e., angle)\n- Max acceleration reached so far\n- Current acceleration\n- Average acceleration\n- Track condition (dummies)\n- Race type (dummies)\n- Weight carried\n- Closest Opponent (1CO) Features (Distance from 1CO, 1CO max speed, current speed, cumulative speed, average speed, max acceleration, current acceleration, average acceleration, current position, best position reached, worst position reached, ...)\n- Second Closest Opponent (2CO) Features (...)\n- Third Closest Opponent (3CO) Features (...)\n\nThe attribute \"Conditional\" in HORACE was not randomly chosen: at each trakus index i, a horse-jockey pair is associated to an Expected Finish Position score which depends on its positional features and energy-related features, and on both categories of features characterizing its three closest opponents. At the same time, an Expected Finish Position score is estimated for each of the opponent horse-jockey pairs, including the three closest opponents; to do so, positional and energy-related features of the respective three closest opponents (including, potentially, the first horse-jockey pair at stake) are accounted for in the estimation procedure.\n\n\n","metadata":{}},{"cell_type":"markdown","source":"### 2.3 The EFP Model\n\nTo estimate the Expected Finish Position in a trakus-wise fashion (i.e., every 0.25 seconds), we decide to adopt a highly versatile machine learning model belonging to the family of Gradient Boosting Machines: CatBoost [1]; based on a permutation-driven ordered-boosting algorithm and able to deal with NaN values without shutting down, this specific model combines strong predictive power with a remarkable training and inference speed to yield the perfect machine for our purposes: execute real-time predictions that account for a wide number of variables and interrelationships among horse-jockey pairs to produce meaningful Expected Finish Position estimates.","metadata":{}},{"cell_type":"code","source":"# Split dataset to train and validate the model\n\nprocessed_dataset = pd.read_csv(\"../input/processed-dataset-bdd/processed_dataset_final.csv\")\n\nprocessed_dataset['program_number'] = processed_dataset['program_number'].astype(str) #convert potential int PNs to str\nprogram_numbers = {'1': '01', '1  ': '01', '10': '10', '10 ': '10', '11': '11', '11 ': '11', '12': '12', '12 ': '12',\n           '13': '13', '13 ': '13', '14': '14', '14 ': '14', '15': '15', '15 ': '15', '16': '16', '16 ': '16',\n           '1A ': '01A', '1X ': '01X', '2': '02', '2  ': '02', '2B ': '02B', '3': '03', '3  ': '03', '3X ': '03X',\n           '4': '04', '4  ': '04', '5': '05', '5  ': '05', '6': '06', '6  ': '06', '7': '07', '7  ': '07',\n           '8': '08', '8  ': '08', '9': '09', '9  ': '09', '1A': '01A', '2B': '02B', '3X': '03X', '1X': '01X',\n            1: '01', 2:'02', 3:'03',4:'04',5:'05',6:'06',7:'07',8:'08',9:'09',10:'10',11:'11',12:'12',13:'12',14:'14',15:'15',16:'16',\n            '01':'01','02':'02','03':'03','04':'04','05':'05','06':'06','07':'07','08':'08','09':'09','10':'10','11':'11',\n            '12':'12','13':'13','01A':'01A','02B':'02B','03X':'03X','01X':'01X'}\nprocessed_dataset['program_number'] = processed_dataset['program_number'].map(program_numbers)\nprocessed_dataset = processed_dataset.sort_values(['race_date','race_number','program_number','trakus_index'], ascending=[True, True, True, True]).reset_index(drop=True)\n\nfeature_space = ['seconds_passed',\n                'current_position','best_position','worst_position','previous_position',\n                'run_up_distance',\n                'max_speed','current_speed','cumulative_speed','average_speed','previous_speed',\n                'current_direction','previous_direction',\n                'max_acceleration','curent_acceleration','cumulative_acceleration','average_acceleration','previous_acceleration',\n                'number_of_participants',\n                'weight_carried',\n               'distance_1CO', 'max_speed_1CO', 'current_speed_1CO', 'cumulative_speed_1CO', 'average_speed_1CO','previous_speed1CO',\n               'max_acceleration_1CO', 'current_acceleration_1CO', 'cumulative_acceleration_1CO', 'average_acceleration_1CO','previous_acceleration1CO',\n               'previous_position_1CO', 'previous_direction_1CO',  \n                'current_position_1CO', 'best_position_1CO', 'worst_position_1CO','lat_dis_1CO','weight_carried_1CO',\n               'distance_2CO', 'max_speed_2CO', 'current_speed_2CO', 'cumulative_speed_2CO', 'average_speed_2CO','previous_speed2CO',\n               'max_acceleration_2CO', 'current_acceleration_2CO', 'cumulative_acceleration_2CO', 'average_acceleration_2CO','previous_acceleration2CO',\n               'previous_position_2CO', 'previous_direction_2CO',  \n                'current_position_2CO', 'best_position_2CO', 'worst_position_2CO','lat_dis_2CO','weight_carried_2CO',\n               'distance_3CO', 'max_speed_3CO', 'current_speed_3CO', 'cumulative_speed_3CO', 'average_speed_3CO','previous_speed3CO',\n               'max_acceleration_3CO', 'current_acceleration_3CO', 'cumulative_acceleration_3CO', 'average_acceleration_3CO','previous_acceleration3CO',\n               'previous_position_3CO', 'previous_direction_3CO',  \n                'current_position_3CO', 'best_position_3CO', 'worst_position_3CO','lat_dis_3CO','weight_carried_3CO',\n                'track_condition__GD ', 'track_condition__MY ',\n               'track_condition__SY ', 'race_type__AOC', 'race_type__CLM',\n               'race_type__MCL', 'race_type__MSW', 'race_type__SHP', 'race_type__SOC',\n               'race_type__STK', 'race_type__STR', 'race_type__WCL']\n\nsplitter = GroupShuffleSplit(test_size=0.2, n_splits=1, random_state = 42)\nsplit = splitter.split(processed_dataset, groups=processed_dataset[['race_date']])\ntrain_inds, test_inds = next(split)\n\ntrain = processed_dataset.iloc[train_inds]\ntest = processed_dataset.iloc[test_inds]\n\nX_tr, y_tr = train[feature_space], train['final_position']\nX_te, y_te = test[feature_space], test['final_position']\n\ngc.collect()","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"model = CatBoostRegressor(\n    n_estimators=100,\n    learning_rate=0.08, \n    random_state=42,\n    verbose=False\n)\n\nmodel.fit(X_tr, y_tr)\n\ny_pred = model.predict(X_te)\n\nresults = []\nfor pred, gt in zip(y_pred, y_te):\n    results.append((pred, gt))\n\nresults = pd.DataFrame(results, columns=['Pred','GT'])\ncorr = results.corr()['GT'][0]\n\ntest['finish_position'] = y_te\ntest['expected_finish_position'] = y_pred\nprint(f\"Pearson correlation between final_position scores and estimated EFP scores:\\n{corr}\")","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Model performance upon validation indicates that the proposed methodology actually works for the EFP estimation. In the next steps we will go deeper into model explainability, trying to understand what are the influencing factors for EFP estimation and developing metrics for a comprehensive assessment framework of horse-jockey performance.","metadata":{}},{"cell_type":"markdown","source":"___\n# <div style=\"background-color:cyan\"> <b>3.0 Aggregated Expected Finish Position as a proxy of jockey performance </b> </div>\n","metadata":{}},{"cell_type":"markdown","source":"### 3.1 How does Expected Finish Position change throughout races?\n\nBefore moving to the establishment of the HORACE-EFP-based assessment metric, let us observe how the model behaves on an unseen race, and how Expected Finish Position changes with the unfolding of events.","metadata":{}},{"cell_type":"code","source":"df_wcoord = pd.read_csv(\"../input/big-data-derby-2022/nyra_2019_complete.csv\")\ndf_wcoord.columns = ['track_id', 'race_date', 'race_number', 'program_number', 'trakus_index', \n             'latitude', 'longitude', 'distance_id', 'course_type', 'track_condition', \n             'run_up_distance', 'race_type', 'post_time', 'purse', 'weight_carried', \n             'jockey', 'odds', 'position_at_finish']\n\ndf_wcoord['program_number'] = df_wcoord['program_number'].astype(str)\nprogram_numbers = {'1': '01', '1  ': '01', '10': '10', '10 ': '10', '11': '11', '11 ': '11', '12': '12', '12 ': '12',\n           '13': '13', '13 ': '13', '14': '14', '14 ': '14', '15': '15', '15 ': '15', '16': '16', '16 ': '16',\n           '1A ': '01A', '1X ': '01X', '2': '02', '2  ': '02', '2B ': '02B', '3': '03', '3  ': '03', '3X ': '03X',\n           '4': '04', '4  ': '04', '5': '05', '5  ': '05', '6': '06', '6  ': '06', '7': '07', '7  ': '07',\n           '8': '08', '8  ': '08', '9': '09', '9  ': '09', '1A': '01A', '2B': '02B', '3X': '03X', '1X': '01X',\n            1: '01', 2:'02', 3:'03',4:'04',5:'05',6:'06',7:'07',8:'08',9:'09',10:'10',11:'11',12:'12',13:'12',14:'14',15:'15',16:'16',\n            '01':'01','02':'02','03':'03','04':'04','05':'05','06':'06','07':'07','08':'08','09':'09','10':'10','11':'11',\n            '12':'12','13':'13','01A':'01A','02B':'02B','03X':'03X','01X':'01X'}\ndf_wcoord['program_number'] = df_wcoord['program_number'].map(program_numbers)\ndf_wcoord = df_wcoord.sort_values(['race_date','race_number','program_number','trakus_index'], ascending=[True, True, True, True]).reset_index(drop=True)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"track = 'AQU'\ndate = '2019-01-19'\nrace_number = 1\n\ndf_wcoord_byrace = df_wcoord[(df_wcoord['race_date'] == date) & (df_wcoord['race_number'] == race_number)].reset_index(drop=True)\ntest_byrace = test[(test['race_date'] == date) & (test['race_number'] == race_number)].reset_index(drop=True)\n\ntest_byrace['latitude'] = df_wcoord_byrace['latitude']\ntest_byrace['longitude'] = df_wcoord_byrace['longitude']\n\n\nlat = test_byrace.latitude.sort_values()\nmin_lat = lat.iloc[0]\nmax_lat = lat.iloc[-1]\n\nlong = test_byrace.longitude.sort_values()\nmin_long = long.iloc[0]\nmax_long = long.iloc[-1]\n\ncolors = {'01':'purple',\n          '02':'lime',\n          '03':'orange',\n          '04':'cyan',\n          '05':'red',\n          '06':'blue', \n          '07':'white'}\n\n\nfig = px.scatter(test_byrace.sort_values('trakus_index'), \n           x=\"longitude\", \n           y=\"latitude\", \n           animation_frame=\"trakus_index\", \n           animation_group=\"program_number\",\n           color = \"program_number\",\n           range_x = [min_long, max_long],\n           range_y = [min_lat, max_lat],\n          )\n\nfor i in range(len(fig.frames)):\n    for k in range(len(fig.frames[i]['data'])):\n        fig.frames[i]['data'][k]['marker']['color'] = colors[fig.frames[i]['data'][k]['name']]\nfor point in fig.data:\n    point['marker']['color'] = colors[point['name']]\n    \nfig.update_layout(\n    title={\n        'text': f\"Date: {date} - Race: {race_number}; Track: {track}\",\n        'y':0.90,\n        'x':0.5}, \n    showlegend=True,\n    template='plotly_dark',\n    width=1000,\n    )\nfig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 50\nfig.layout.updatemenus[0].bordercolor='black'\nfig.layout.updatemenus[0].bgcolor='black'\nfig.show()\n\n#_________\n\nidx = []\nfor tkidx,efp in zip(test_byrace['trakus_index'], test_byrace['expected_finish_position']):\n    if tkidx < 12:\n        k = 0\n    else:\n        k = efp\n    idx.append(k)\n# test_byrace['expected_finish_position'] = idx\n\nfig = px.line(test_byrace, x = \"trakus_index\", y = \"expected_finish_position\", title = \"EFP - by Trakus Index\", \n              animation_frame=\"trakus_index\", color='program_number',\n              range_x=[0,max(test_byrace['trakus_index'])+2], \n              range_y=[-0.1,1.1])\nfig.update_traces(marker=dict(\n            size=20,\n            line=dict(\n                color='black',\n                width=5\n            )))\n\nfig.update_layout(\n    title={\n        'text': \"Expected Finish Position - by Trakus Index\",\n        'y':0.90,\n        'x':0.5,\n        'xanchor': 'center',\n        'yanchor': 'top'}, \n    showlegend=True,\n    template='plotly_dark',\n    width=1000,\n    )\nfig.layout.xaxis.title.text = 'Trakus Index (0.25s)'\n\nfor i in range(len(fig.frames)):\n    for k in range(len(fig.frames[i]['data'])):\n        fig.frames[i]['data'][k]['line']['color'] = colors[fig.frames[i]['data'][k]['name']]\nfor point in fig.data:\n    point['line']['color'] = colors[point['name']]\n    \nfor i in range(1, len(fig.frames)):\n    for k in range(len(fig.frames[i]['data'])):\n        fig.frames[i]['data'][k]['x'] = np.concatenate((fig.frames[i-1]['data'][k]['x'], fig.frames[i]['data'][k]['x']))\n        fig.frames[i]['data'][k]['y'] = np.concatenate((fig.frames[i-1]['data'][k]['y'], fig.frames[i]['data'][k]['y']))\n\nfig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 50\nfig.layout.updatemenus[0].bordercolor='black'\nfig.layout.updatemenus[0].bgcolor='black'\nfig.show()","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"It is astonishing to notice how similar the expected final ranking of some of the participants predicted in the initial stages of the race are to the final standings, with horses 01 and 05 ranked among the first and 06 ending up at the last place. There is much less variance in EFP continuous scores in the initial stages of the race, though, and this mainly implies that model expectations concerning final standings can vary more at the beginning of a race compared to advanced stages when it has more data concerning speed and acceleration at its disposal.\n\nWorth noticing also how, at around trakus_index = 185, horse 07 has an apparent advantage over 04 and, nevertheless, its Expected Finish Position inexorably degenerates until 04 outperforms it. Since the two given horse-jockey pairs were close to each other, the model had extensive information about the whole race regarding both of them when it came to estimate their EFP. Indeed, the model observes the current and previous status of the three closest opponents, and this is done for all of them separately; hence, the estimates for each of the 7 distinct horses participating in the aforementioned race are produced conditionally on the opponents' present and past features (hence the attribute \"Conditional\" in HORACE, as mentioned in section 2.2).","metadata":{}},{"cell_type":"code","source":"fig = px.area(test_byrace, x = \"trakus_index\", y = \"expected_finish_position\", title = \"EFP - by Trakus Index\", \n              color='program_number',\n              range_x=[0,max(test_byrace['trakus_index'])+2], \n              range_y=[-1.1,4.1])\nfig.update_traces(marker=dict(\n            size=20,\n            line=dict(\n                color='black',\n                width=5\n            )))\n\nfig.update_layout(\n    title={\n        'text': f\"Expected Finish Position Magnitude - by Trakus Index<br>Date: {date} - Race: {race_number}; Track: {track}\",\n        'y':0.90,\n        'x':0.5,\n        'xanchor': 'center',\n        'yanchor': 'top'}, \n    showlegend=True,\n    template='plotly_dark',\n    width=1000,\n    )\nfig.layout.xaxis.title.text = 'Trakus Index (0.25s)'\n\nfor point in fig.data:\n    point['line']['color'] = colors[point['name']]\n    \nfig.show()","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Essentially, the aEFP of each horse-jockey pair is represented by the magnitude of the area below the associated line in the previous plot.\nEventually, the aEFP is computed as follows:\n\n<div style='text-align:center'><font size=6> $aEFP = \\frac{1}{t}\\sum_{i=1}^{t}EFP_{i}  $</font></div><br>\n\nBeyond a raw aEFP, we decide to also introduce a decay factor $\\phi$ that gradually reduces the weight of later EFP values on the aggregated EFP; the purpose of this latter metric, which is denoted as DaEFP, is to have at our disposal a proxy of horse-jockey performance that is not too influenced by the final positions, on the contrary being able to confidently assess a horse-jockey pair performance in a race despite the very final standings.\n\n<div style='text-align:center'><font size=6> $DaEFP = \\frac{1}{t}\\sum_{i=1}^{t}EFP_{i} \\cdot D_{\\phi, i}  $</font></div><br>\n\nWhere the decay coefficient $D_{\\phi, i}$ is defined as follows:\n\n<div style='text-align:center'><font size=6> $D_{\\phi,i} = D_{\\phi,i-1} \\cdot (1 - \\phi), $&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$ D_{\\phi,0} = 1 $</font></div><br>\n\nWith $i$ denoting a given trakus index, $t$ the total amount of trakus indices:\n\n<div style='text-align:center'><font size=6> $i \\in [1, t]  $</font></div><br>\n\n\n\n\n","metadata":{}},{"cell_type":"code","source":"hj_pairs = {}\ndecay_factor = 3e-3\ndecay = 1\nfor trakus_idx in test_byrace['trakus_index'].unique():\n    trakus_data = test_byrace[test_byrace['trakus_index'] == trakus_idx].reset_index(drop=True)\n    decay = decay * (1 - decay_factor)\n    for hj_pair in trakus_data['program_number']:\n        hj_EFP = trakus_data[trakus_data['program_number'] == hj_pair].reset_index(drop=True)['expected_finish_position'][0]\n        min_ = min(trakus_data['expected_finish_position'])\n        max_ = max(trakus_data['expected_finish_position'])\n        if hj_pair not in hj_pairs.keys():\n            hj_pairs[hj_pair] = [hj_EFP, hj_EFP*decay]\n        else:\n            hj_pairs[hj_pair] = [hj_pairs[hj_pair][0] + hj_EFP, hj_pairs[hj_pair][1] + hj_EFP*decay]\n\nscores = []\nfor hj in hj_pairs.keys():\n    scores.append((hj, hj_pairs[hj][0] / trakus_idx, hj_pairs[hj][1] / trakus_idx))\nfinal_scores = pd.DataFrame(scores, columns=['Horse','aEFP','DaEFP']).sort_values(by=\"aEFP\", ascending=True)\nfinal_scores.style.highlight_min().set_caption(f\"Aggregated EFP<br>Date: {date} - Race: {race_number}; Track: {track}<br>Decay Factor $\\phi = 0.003$ \").set_precision(3).set_properties(**{'border': '1.3px solid black',\n                                                      'color': 'black'}).hide_index()\n","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The previous table shows a mismatch between the ranking of aEFP and DaEFP for horse 02: despite having the same aEFP as 04, its Decayed aggregated Expected Finish Position is lower (the lower, the better); the decay factor indeed degrades the weight of EFP estimates associated to high trakus indices, and this suggests that horse 02 was stronger than 04 before the last stages of the race (as the previous plots confirm, after all).","metadata":{}},{"cell_type":"markdown","source":"___\n# <div style=\"background-color:cyan\"> <b>4.0 Model explainability to detect beneficial and harmful patterns</b> </div>\n\nLet us now investigate what are the features that most affect changes in Expected Finish Position. Is it a matter of acceleration patterns? Are there specific time frames throughout races that see EFP changing the most?\n<br>\nMore specifically: given a horse-jockey pair and an associated Expected Finish Position, what are the factors that determine a positive/negative outcome in model predictions? In this section we will go deep into explainability of HORACE EFP model to study and address the aforementioned questions and more.\n\n### 4.1 What features do influence HORACE EFP predictions the most?\n","metadata":{}},{"cell_type":"markdown","source":"We can reasonably expect positional features (e.g., current position w.r.t. opponents) to be of paramount importance for the model to understand where the horse-jockey pair is and where it is heading, but what are the other variables that drive model predictions and, further, are the estimates concerning a horse-jockey pair prevalently influenced by features related to that specific horse-jockey pair or are opponent features taken into account by the machine, as well?\n\n","metadata":{}},{"cell_type":"code","source":"feat_imp = model.get_feature_importance()\nfeatImp = []\nfor var, imp in zip(feature_space, feat_imp):\n    featImp.append((var, imp))\nfeatImp = pd.DataFrame(featImp, columns=['Feature','Importance']).sort_values(by=\"Importance\").reset_index(drop=True)\nplt.xticks(rotation='horizontal')\nplt.barh(featImp.Feature[60:], featImp.Importance[60:], color=\"red\")\nplt.title(\"HORACE EFP Model - Feature Importance\")\nplt.show()","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"explainer = shap.TreeExplainer(model)\nshap_values = explainer.shap_values(X_te)\nshap.summary_plot(shap_values, X_te)","metadata":{"_kg_hide-input":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The previous two charts show the feature importance and the SHAP [2] value, respectively, associated with each input feature used by the model. While feature importance is a method embedded within CatBoost implementation and yields the relevance of each feature during training, SHAP works at test level detecting the <b>impact each feature has on final inference estimates</b>.\n\nWe can indeed notice that, as expected, <b>positional features play a crucial role</b> in model predictions, with current horse position being the highest-impact feature. We can draw some further interesting insights, though:\n* Positional features <b>of opponents</b> are carefully observed by the model (distance of opponents)\n* <b>Energy-related features</b> (Speed, Acceleration) are significant in the estimation process, as well, both concerning the horse-jockey pair at stake and its opponents\n\nWith the previous intuitions in mind and observing the SHAP plot, we can unveil the pattern through which the model carries out EFP estimates: given a horse-jockey pair and for each trakus index, the machine observes both its pair of interest and the closest opponents, develops a <b>hierarchical framework representing where the players are and how their current position relates to each other</b>; then, it potentially <b>evaluates the energy reserves</b> of the contestants and, accounting for how they performed so-far, makes its predictions.\n","metadata":{}},{"cell_type":"markdown","source":"### 4.2 What is the impact of features on Expected Finish Position in different stages of a race?\n\nWe might want to understand the temporal nature of relationships between Expected Finish Position and other features, either for explainability purposes or for riders to devise better racing strategies. To achieve this, let us compare the SHAP values taken for three distinct stages obtained by bucketing the trakus indices in three slices: \n<div style='text-align:center'><font size=4> $Bucket_{1}: t_{idx} \\in  \\left[1, \\frac{\\underset{t}{\\arg\\max}}{3}\\right[    $</font></div><br>\n<div style='text-align:center'><font size=4> $Bucket_{2}: t_{idx} \\in  \\left[\\frac{\\underset{t}{\\arg\\max}}{3}, \\frac{2(\\underset{t}{\\arg\\max})}{3}\\right[    $</font></div><br>\n<div style='text-align:center'><font size=4> $Bucket_{3}: t_{idx} \\in  \\left[\\frac{2(\\underset{t}{\\arg\\max})}{3}+1, \\underset{t}{\\arg\\max}\\right]    $</font></div><br>\n\nIndeed, we notice from the previous figure that, despite overall rankings appear to hold relatively stable throughout a race, there are three main changes in how estimates are produced: \n* The first bucket is characterized by predictions with <b>low fluctuations</b> and <b>low variance</b>, and it makes perfectly sense: the data available at this stage contains relatively less signal and differences among horse-jockey pairs are minimal.\n* The second bucket becomes more unstable and hosts the <b>highest density of EFP variations</b>.\n* Despite hosting some fluctuations, the third stage is characterized by a <b>relatively low amount of variations</b>.","metadata":{}},{"cell_type":"code","source":"# # Compare SHAP values between three temporal buckets\n\n# max_trakus = []\n\n# store = []\n# test_ri = test.reset_index(drop=True)\n# for race, date in zip(test_ri['race_number'], test_ri['race_date']):\n    \n#     combination = f\"{race}|{date}\"\n#     if combination in store:\n#         continue\n#     store.append(combination)\n    \n\n#     race_data = test_ri[(test_ri['race_number'] == race) & (test_ri['race_date'] == date)]\n#     total_rows = len(race_data['trakus_index'])\n#     for i in range(total_rows):\n#         max_trakus.append(max(race_data['trakus_index']))\n\n# test['max_trakus'] = max_trakus        \n# test_bucket1 = test[test['trakus_index'] < test['max_trakus']/3]\n# test_bucket2 = test[(test['trakus_index'] >= test['max_trakus']/3) & (test['trakus_index'] < (2/3)*test['max_trakus'])]\n# test_bucket3 = test[(test['trakus_index'] >= (2/3)*test['max_trakus']) & (test['trakus_index'] < test['max_trakus'])]\n\n# X_te1, y_te1 = test_bucket1[feature_space], test_bucket1['final_position']\n# X_te2, y_te2 = test_bucket2[feature_space], test_bucket2['final_position']\n# X_te3, y_te3 = test_bucket3[feature_space], test_bucket3['final_position']\n\n# shap_values1 = explainer.shap_values(X_te1)\n# shap_values2 = explainer.shap_values(X_te2)\n# shap_values3 = explainer.shap_values(X_te3)\n\n# # shap.summary_plot(shap_values1, X_te1, title=\"SHAP values for EFP - First Stage\")\n# # shap.summary_plot(shap_values2, X_te2, title=\"SHAP values for EFP - Intermediate Stage\")\n# # shap.summary_plot(shap_values3, X_te3, title=\"SHAP values for EFP - Final Stage\")\n\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"![shap_bystage.PNG](https://github.com/Mauromar00/BDD/blob/main/shap_bystage.PNG?raw=true)\n","metadata":{}},{"cell_type":"markdown","source":"We can easily notice how positional features accrue importance in more advanced stages, with 5 out of the first 6 features having a positional nature in the final stage: they were 4 out of 6 in the intermediate stage, and 3 out of 6 in the initial stage. <b>Speed and acceleration</b>, both concerning the horse-jockey pair of interest and its opponents, always plays an important role in the estimation procedure, <b>regardless of the stage</b>.","metadata":{}},{"cell_type":"markdown","source":"### 4.2 What role do Speed and Acceleration play in the estimation of Expected Finish Position and, ultimately, Actual Finish Position?\n\n","metadata":{}},{"cell_type":"markdown","source":"To understand the real impact energy features (Speed and Acceleration) exert on the estimation process of Expected Finish Position, we use the same identical model adopted to estimate EFP using both positional and energy-related features, but we perturb its feature space by dropping all the positional features. This way we end up with an Expected Finish Position conditional estimator (that still accounts for the features of the three closest opponents), but only speed/acceleration-focused.","metadata":{}},{"cell_type":"code","source":"feature_space = ['seconds_passed',\n                'max_speed','current_speed','cumulative_speed','average_speed','previous_speed',\n                'max_acceleration','curent_acceleration','cumulative_acceleration','average_acceleration','previous_acceleration',\n                'number_of_participants',\n                'max_speed_1CO', 'current_speed_1CO', 'cumulative_speed_1CO', 'average_speed_1CO','previous_speed1CO',\n               'max_acceleration_1CO', 'current_acceleration_1CO', 'cumulative_acceleration_1CO', 'average_acceleration_1CO','previous_acceleration1CO',\n                'max_speed_2CO', 'current_speed_2CO', 'cumulative_speed_2CO', 'average_speed_2CO','previous_speed2CO',\n               'max_acceleration_2CO', 'current_acceleration_2CO', 'cumulative_acceleration_2CO', 'average_acceleration_2CO','previous_acceleration2CO',\n               'max_speed_3CO', 'current_speed_3CO', 'cumulative_speed_3CO', 'average_speed_3CO','previous_speed3CO',\n               'max_acceleration_3CO', 'current_acceleration_3CO', 'cumulative_acceleration_3CO', 'average_acceleration_3CO','previous_acceleration3CO']\n\n\nX_tr, y_tr = train[feature_space], train['final_position']\nX_te, y_te = test[feature_space], test['final_position']\n\ngc.collect()\n\nmodel = CatBoostRegressor(\n    n_estimators=100,\n    learning_rate=0.08, \n    random_state=42,\n    verbose=False\n)\n\nmodel.fit(X_tr, y_tr)\n\ny_pred = model.predict(X_te)\n\nresults = []\nfor pred, gt in zip(y_pred, y_te):\n    results.append((pred, gt))\n\nresults = pd.DataFrame(results, columns=['Pred','GT'])\ncorr = results.corr()['GT'][0]\nprint(f\"Pearson correlation between final_position scores and estimated EFP scores:\\n{corr}\")","metadata":{"_kg_hide-input":true,"_kg_hide-output":false},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Results are promising: the model is able to predict a revisited Expected Finish Position by only adopting energy-related features, with a Pearson correlation score between EFP and AFP of around 0.460. Such a score obtained only using speed and acceleration features clearly testifies the crucial role played by such features, even in the previously-trained model: reasonably, shap values associated with Speed and Acceleration variables were likely to be lower than positional features due to multicollinearity between them, thus hiding the true potential behind speed/acceleration features.","metadata":{}},{"cell_type":"code","source":"# # Compare SHAP values between three temporal buckets\n\n# max_trakus = []\n\n# store = []\n# test_ri = test.reset_index(drop=True)\n# for race, date in zip(test_ri['race_number'], test_ri['race_date']):\n    \n#     combination = f\"{race}|{date}\"\n#     if combination in store:\n#         continue\n#     store.append(combination)\n    \n\n#     race_data = test_ri[(test_ri['race_number'] == race) & (test_ri['race_date'] == date)]\n#     total_rows = len(race_data['trakus_index'])\n#     for i in range(total_rows):\n#         max_trakus.append(max(race_data['trakus_index']))\n\n# test['max_trakus'] = max_trakus        \n# test_bucket1 = test[test['trakus_index'] < test['max_trakus']/3]\n# test_bucket2 = test[(test['trakus_index'] >= test['max_trakus']/3) & (test['trakus_index'] < (2/3)*test['max_trakus'])]\n# test_bucket3 = test[(test['trakus_index'] >= (2/3)*test['max_trakus']) & (test['trakus_index'] < test['max_trakus'])]\n\n# X_te1, y_te1 = test_bucket1[feature_space], test_bucket1['final_position']\n# X_te2, y_te2 = test_bucket2[feature_space], test_bucket2['final_position']\n# X_te3, y_te3 = test_bucket3[feature_space], test_bucket3['final_position']\n\n# shap_values1 = explainer.shap_values(X_te1)\n# shap_values2 = explainer.shap_values(X_te2)\n# shap_values3 = explainer.shap_values(X_te3)\n\n# # shap.summary_plot(shap_values1, X_te1, title=\"SHAP values for EFP - First Stage\")\n# # shap.summary_plot(shap_values2, X_te2, title=\"SHAP values for EFP - Intermediate Stage\")\n# # shap.summary_plot(shap_values3, X_te3, title=\"SHAP values for EFP - Final Stage\")\n\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"![shap_bystage2.PNG](https://github.com/Mauromar00/BDD/blob/main/shap_bystage2.PNG?raw=true)\n","metadata":{}},{"cell_type":"markdown","source":"The predictive power of the revisited EFP model appears to be relatively uninformed and straightforward in the very first stages of a race, improving afterwards when aggregate data (average, cumulative) become available and more informative. ","metadata":{}},{"cell_type":"markdown","source":"___\n\n# <div style=\"background-color:cyan\"> <b>5. Wrap Up and Future Directions </b> </div>\n\nLeveraging the dataset provided by NYTHA and NYRA, we developed a framework for the in-depth assessment of horse-jockey pairs in the horse racing world. Not only HORACE provides reliable real-time estimates for how well a given horse-jockey pair is performing in the course of a race, but it also allows to draw actionable insights that go deep into a specific horse-jockey pair performance. \n\nMore specifically, HORACE produces updated predictions on Expected Finish Position of horse-jockey pairs once every 0.25 seconds, thus allowing to detect patterns in EFP changes and identify beneficial and harmful situations a horse-jockey pair might face; the development of aggregate sub-metrics founded on EFP variations allow for a thorough assessment and unveiling of a wide range of Horse Racing facets. Eventually, HORACE allows to deepdive on Speed and Acceleration features, training exclusively on a feature set free of positional features thus measuring the relevance of their beneficial effects and ending up being able to assess the quality of speed/acceleration pattern instances.\n\nThe usage of SHAP values was extremely fruitful to identify feature importance for HORACE EFP models, guiding us throughout a thorough analysis of what factors actually play a role in the Expected Finish Position estimation process.\n\nFuture work include, upon integration with domain-specific data, the investigation of relationships between Expected Finish Position patterns and Injury occurrence, and more broadly an integration of HORACE insights and actionability with a focus oriented towards horse health and safety throughout races.","metadata":{}},{"cell_type":"markdown","source":"___\n# References\n\n* [1] CatBoost: unbiased boosting with categorical features, L. Prokhorenkova, G. Gusev, A. Vorobev, A. V. Dorogush, A. Gulin; <br> Arxiv link: https://arxiv.org/pdf/1706.09516\n* [2] A Unified Approach to Interpreting Model Predictions, S. Lundberg, S. Lee; <br> ArXiV link: https://arxiv.org/pdf/1705.07874\n","metadata":{}},{"cell_type":"markdown","source":"___\n## <i> Code </i>\nALl the code used to preprocess the dataset is contained in the following hidden snippet; all the code used to carry out the analysis is contained in the hidden snippets appearing in the notebook.","metadata":{}},{"cell_type":"code","source":"#### This snippet contains all the code to preprocess the dataset\n\n# '''Add positional and distance features'''\n\n## Advice: before starting working with large amounts of data, it is good practice to reduce its memory consumption to avoid memory issues\n\n# df = pd.read_csv(\"../input/big-data-derby-2022/nyra_2019_complete.csv\")\n\n# df.columns = ['track_id', 'race_date', 'race_number', 'program_number', 'trakus_index', \n#              'latitude', 'longitude', 'distance_id', 'course_type', 'track_condition', \n#              'run_up_distance', 'race_type', 'post_time', 'purse', 'weight_carried', \n#              'jockey', 'odds', 'position_at_finish']\n\n# dataset = df[['latitude','longitude',\n#               'race_date','race_number','program_number',\n#               'trakus_index',\n#               'track_id','course_type','track_condition','race_type','run_up_distance','weight_carried',\n#               'position_at_finish']]\n\n# dataset = dataset.sort_values(['track_id', 'race_date', 'race_number', 'program_number', 'trakus_index']).reset_index(drop=True)\n\n# # Credit to user \"shapital\", functions to process coordinates derived from his public notebook: https://www.kaggle.com/code/shapital/calculating-speed-turning-angle-and-position/notebook\n\n# # Use UTM package to convert tracking data from GCS (Geographic Coordinate System) to UTM (Universal Transverse Mercator Coordinate System)\n# dataset['utm_coordinates'] = np.array(utm.from_latlon(dataset['latitude'].values, dataset['longitude'].values)[:2]).T.tolist()\n\n\n# dataset_prev = dataset[['track_id', 'race_date', 'race_number', 'trakus_index', 'program_number', 'utm_coordinates']].copy()\n# dataset_prev['trakus_index'] = dataset_prev['trakus_index'] + 1\n# dataset = pd.merge(dataset, dataset_prev, on=['track_id', 'race_date', 'race_number', 'program_number', 'trakus_index'], suffixes=['', '_prev'], how='left')\n\n# dataset['prev_curr_utm'] = dataset.progress_apply(lambda row: [np.array(row['utm_coordinates_prev']), np.array(row['utm_coordinates'])], axis=1)\n\n# def lateral_distance(p1, p2, p3): \n#     return round(np.cross(p2-p1, p1-p3) / np.linalg.norm(p2-p1), 2)\n\n# def longitudinal_distance(p1, p2, p3): \n#     return round(np.dot(p2-p1, p3-p2) / np.linalg.norm(p2-p1), 2)\n\n# def get_lateral_distances(points): \n#     return [[lateral_distance(point1[0], point1[1], point2[1]) for point2 in points.values] for point1 in points.values]\n\n# def get_longitudinal_distances(points): \n#     return [[longitudinal_distance(point1[0], point1[1], point2[1]) for point2 in points.values] for point1 in points.values]\n\n# dataset['lateral_distances'] = dataset.groupby(['track_id', 'race_date', 'race_number', 'trakus_index'])['prev_curr_utm'].progress_transform(get_lateral_distances)\n# dataset['longitudinal_distances'] = dataset.groupby(['track_id', 'race_date', 'race_number', 'trakus_index'])['prev_curr_utm'].progress_transform(get_longitudinal_distances)\n\n# def get_position(longitudinal_distances):\n#     position = (np.array(longitudinal_distances) >= 0).sum()\n#     return np.nan if position == 0 else position\n\n# dataset['position'] = dataset['longitudinal_distances'].progress_apply(get_position)\n\n# dataset.to_csv(\"./dataset.csv\", index=False)\n\n# '''Define functions to process the dataset'''\n\n# def get_current_speed(hj_race_data, trakus_idx):\n#     if trakus_idx == 1:\n#         current_speed = 0\n#     else:\n#         previous_UTM_coord = hj_race_data[hj_race_data['trakus_index'] == trakus_idx - 1]['utm_coordinates'].reset_index(drop=True)[0]\n#         current_UTM_coord = hj_race_data[hj_race_data['trakus_index'] == trakus_idx]['utm_coordinates'].reset_index(drop=True)[0]\n\n#         current_UTM_coord = [float(current_UTM_coord[1:-1].split(\", \")[0]), float(current_UTM_coord[1:-1].split(\", \")[-1])]\n#         previous_UTM_coord = [float(previous_UTM_coord[1:-1].split(\", \")[0]), float(previous_UTM_coord[1:-1].split(\", \")[-1])]\n\n#         current_speed = 4 * np.linalg.norm(np.array(current_UTM_coord) - np.array(previous_UTM_coord))\n        \n#     return current_speed\n\n# def get_current_direction(hj_race_data, trakus_idx):\n#     if trakus_idx == 1 or trakus_idx == 2:\n#         current_angle = 0\n#     else:\n#         previous2_UTM_coord = hj_race_data[hj_race_data['trakus_index'] == trakus_idx - 2]['utm_coordinates'].reset_index(drop=True)[0]\n#         previous1_UTM_coord = hj_race_data[hj_race_data['trakus_index'] == trakus_idx - 1]['utm_coordinates'].reset_index(drop=True)[0]\n#         current_UTM_coord = hj_race_data[hj_race_data['trakus_index'] == trakus_idx]['utm_coordinates'].reset_index(drop=True)[0]\n\n#         current_UTM_coord = [float(current_UTM_coord[1:-1].split(\", \")[0]), float(current_UTM_coord[1:-1].split(\", \")[-1])]\n#         previous1_UTM_coord = [float(previous1_UTM_coord[1:-1].split(\", \")[0]), float(previous1_UTM_coord[1:-1].split(\", \")[-1])]\n#         previous2_UTM_coord = [float(previous2_UTM_coord[1:-1].split(\", \")[0]), float(previous2_UTM_coord[1:-1].split(\", \")[-1])]\n\n#         current_angle = 180 - math.degrees(math.atan2(current_UTM_coord[1]-previous1_UTM_coord[1], current_UTM_coord[0]-previous1_UTM_coord[0]) - math.atan2(previous2_UTM_coord[1]-previous1_UTM_coord[1], previous2_UTM_coord[0]-previous1_UTM_coord[0]))\n#         if current_angle < -180:\n#             current_angle += 360\n#         if current_angle > 180:\n#             current_angle -= 360\n#         if abs(current_angle) > 5:\n#             return np.nan\n#     return current_angle\n\n# def get_current_acceleration(current_speed, previous_speed):\n#     current_acceleration = (current_speed - previous_speed)/0.25\n#     return current_acceleration\n\n\n# '''Process dataset to get feature space for HORACE'''\n\n# dataset = pd.read_csv(\"../input/processed-dataset-first-stage/dataset.csv\")\n\n# dataset['program_number'] = dataset['program_number'].astype(str)\n# program_numbers = {'1': '01', '1  ': '01', '10': '10', '10 ': '10', '11': '11', '11 ': '11', '12': '12', '12 ': '12',\n#            '13': '13', '13 ': '13', '14': '14', '14 ': '14', '15': '15', '15 ': '15', '16': '16', '16 ': '16',\n#            '1A ': '01A', '1X ': '01X', '2': '02', '2  ': '02', '2B ': '02B', '3': '03', '3  ': '03', '3X ': '03X',\n#            '4': '04', '4  ': '04', '5': '05', '5  ': '05', '6': '06', '6  ': '06', '7': '07', '7  ': '07',\n#            '8': '08', '8  ': '08', '9': '09', '9  ': '09', '1A': '01A', '2B': '02B', '3X': '03X', '1X': '01X'}\n# dataset['program_number'] = dataset['program_number'].map(program_numbers)\n\n# processed_dataset = []\n\n# k = 1\n# store = []\n# for race, date in zip(dataset['race_number'], dataset['race_date']):\n    \n#     combination = f\"{race}|{date}\"\n#     if combination in store:\n#         continue\n#     store.append(combination)\n    \n#     print(f\"Processing race {k}.\")\n#     k += 1   \n\n#     try:\n#         race_data = dataset[(dataset['race_number'] == race) & (dataset['race_date'] == date)]\n\n#         number_of_participants = len(race_data['program_number'].unique())\n#         track_condition = race_data['track_condition'].reset_index(drop=True)[0]\n#         race_type = race_data['race_type'].reset_index(drop=True)[0]\n#         course_type = race_data['course_type'].reset_index(drop=True)[0]\n#         run_up_distance = race_data['run_up_distance'].reset_index(drop=True)[0]\n#         track_id = race_data['track_id'].reset_index(drop=True)[0]\n\n#         race_date = race_data['race_date'].reset_index(drop=True)[0]\n#         race_number = race_data['race_number'].reset_index(drop=True)[0]\n\n#         participants = {}\n\n#         for hj_pair in race_data['program_number'].unique():\n#             participants[hj_pair] = {\n#                 'best_position' : 99,\n#                 'worst_position' : 1,\n#                 'previous_position' : np.nan,\n#                 'max_speed' : 0,\n#                 'cumulative_speed' : 0,\n#                 'previous_speed' : 0,\n#                 'max_acceleration' : 0,\n#                 'cumulative_acceleration' : 0,\n#                 'previous_acceleration' : 0,\n#                 'previous_direction' : np.nan,\n#             }\n\n#         for trakus_idx in race_data['trakus_index'].unique():\n#             trakus_data = race_data[race_data['trakus_index'] == trakus_idx]\n\n#             trakus_data_byhj = []\n\n#             hj_pair_mapping = []\n#             hj_data_stored = {}\n#             for hj_pair in trakus_data['program_number']:\n#                 hj_pair_mapping.append(hj_pair)\n#                 hj_data = trakus_data[trakus_data['program_number'] == hj_pair]\n#                 hj_data_stored[hj_pair] = hj_data # store sub-dataframes for subsequent usage\n\n#                 best_position = participants[hj_pair]['best_position']\n#                 worst_position = participants[hj_pair]['worst_position']\n#                 max_speed = participants[hj_pair]['max_speed']\n#                 cumulative_speed = participants[hj_pair]['cumulative_speed']\n#                 previous_speed = participants[hj_pair]['previous_speed']\n#                 max_acceleration = participants[hj_pair]['max_acceleration']\n#                 cumulative_acceleration = participants[hj_pair]['cumulative_acceleration']\n#                 previous_acceleration = participants[hj_pair]['previous_acceleration']\n#                 previous_position = participants[hj_pair]['previous_position']\n#                 previous_direction = participants[hj_pair]['previous_direction']\n\n#     #             ahead = []\n#     #             behind = []\n#     #             lon_distances = hj_data['longitudinal_distances'].reset_index(drop=True)[0]\n#     #             lon_distances = lon_distances[1:-1].split(\", \")\n#     #             for lon_dis in lon_distances:\n#     #                 if lon_dis > 0:\n#     #                     ahead.append(lon_distances.index(lon_dis))\n#     #                 elif lon_dis < 0:\n#     #                     behind.append(lon_distances.index(lon_dis))\n\n#                 current_position = hj_data['position'].reset_index(drop=True)[0]\n#                 if current_position < best_position:\n#                     best_position = current_position\n#                 if current_position > worst_position:\n#                     worst_position = current_position\n\n\n#                 hj_race_data = race_data[race_data['program_number'] == hj_pair]\n\n#                 current_speed = get_current_speed(hj_race_data, trakus_idx)\n#                 current_direction = get_current_direction(hj_race_data, trakus_idx)\n#                 if current_speed > max_speed:\n#                     max_speed = current_speed\n#                 cumulative_speed += current_speed\n#                 average_speed = cumulative_speed / trakus_idx\n\n#                 current_acceleration = get_current_acceleration(current_speed, previous_speed)\n#                 if current_acceleration > max_acceleration:\n#                     max_acceleration = current_acceleration\n#                 cumulative_acceleration += current_acceleration\n#                 average_acceleration = cumulative_acceleration / trakus_idx\n\n#                 final_position = hj_data['position_at_finish'].reset_index(drop=True)[0]\n#                 weight_carried = hj_data['weight_carried'].reset_index(drop=True)[0]\n\n#                 participants[hj_pair] = {\n#                     'best_position' : best_position,\n#                     'worst_position' : worst_position,\n#                     'previous_position' : current_position,\n#                     'max_speed' : max_speed,\n#                     'cumulative_speed' : cumulative_speed,\n#                     'previous_speed' : current_speed,\n#                     'max_acceleration' : max_acceleration,\n#                     'cumulative_acceleration' : cumulative_acceleration,\n#                     'previous_acceleration' : current_acceleration,\n#                     'previous_direction' : current_direction,\n\n#                     'previous_acceleration2' : previous_acceleration,\n#                     'previous_direction2' : previous_direction,\n#                     'previous_speed2' : previous_speed,\n#                     'previous_position2' : previous_position,\n\n#                     'current_position' : current_position,\n#                     'current_speed' : current_speed,\n#                     'current_direction' : current_direction,\n#                     'current_acceleration' : current_acceleration,\n#                     'average_speed' : average_speed,\n#                     'average_acceleration' : average_acceleration,\n#                     'weight_carried' : weight_carried,\n\n#                     'final_position' : final_position,\n#                 }\n\n\n#                 trakus_data_byhj.append([race_date,race_number,hj_pair,trakus_idx,\n#                                         trakus_idx*0.25, \n#                                         current_position, best_position, worst_position, previous_position,\n#                                         run_up_distance,\n#                                         max_speed, current_speed, cumulative_speed, average_speed, previous_speed,\n#                                         current_direction, previous_direction,\n#                                         max_acceleration, current_acceleration, cumulative_acceleration, average_acceleration, previous_acceleration,\n#                                         number_of_participants, track_condition, race_type, course_type,\n#                                         weight_carried])\n\n#             j = 0\n#             for hj_pair in trakus_data['program_number']:\n#                 hj_data = hj_data_stored[hj_pair] # trakus_data[trakus_data['program_number'] == hj_pair]\n\n#                 lon_distances = hj_data['longitudinal_distances'].reset_index(drop=True)[0]\n#                 lon_distances = lon_distances[1:-1].split(\", \")\n\n#                 lat_distances = hj_data['lateral_distances'].reset_index(drop=True)[0]\n#                 lat_distances = lat_distances[1:-1].split(\", \")\n\n#                 if trakus_idx == 1:\n#                     lon_distances = [0.0 for lon_distance in lon_distances]\n#                     lat_distances = [0.0 for lat_distance in lat_distances]\n#                     remove_self = copy.copy(lon_distances)\n#                     lon_distances_updated = copy.copy(remove_self)\n#                     lon_distances_updated2 = copy.copy(lon_distances_updated)\n\n#                 else:\n#                     lon_distances = [float(lon_distance) for lon_distance in lon_distances]\n#                     lat_distances = [float(lat_distance) for lat_distance in lat_distances]\n#                     remove_self = copy.copy(lon_distances)\n#                     remove_self.remove(0.0)\n#                     lon_distances_updated = copy.copy(remove_self)\n#                     lon_distances_updated.remove(min(lon_distances_updated))\n#                     lon_distances_updated2 = copy.copy(lon_distances_updated)\n#                     lon_distances_updated2.remove(min(lon_distances_updated2))\n\n\n#                 closest_opponent_1CO_index = lon_distances.index(min(remove_self))\n#                 closest_opponent_1CO = hj_pair_mapping[closest_opponent_1CO_index]\n#                 distance_1CO = lon_distances[closest_opponent_1CO_index] # min(lon_distances)\n#                 lat_dis_1CO = lat_distances[closest_opponent_1CO_index]\n#                 best_position_1CO = participants[closest_opponent_1CO]['best_position']\n#                 worst_position_1CO = participants[closest_opponent_1CO]['worst_position']\n#                 max_speed_1CO = participants[closest_opponent_1CO]['max_speed']\n#                 cumulative_speed_1CO = participants[closest_opponent_1CO]['cumulative_speed']\n#                 previous_speed_1CO = participants[closest_opponent_1CO]['previous_speed2']\n#                 max_acceleration_1CO = participants[closest_opponent_1CO]['max_acceleration']\n#                 cumulative_acceleration_1CO = participants[closest_opponent_1CO]['cumulative_acceleration']\n#                 previous_acceleration_1CO = participants[closest_opponent_1CO]['previous_acceleration2']\n#                 previous_direction_1CO = participants[closest_opponent_1CO]['previous_direction2']\n#                 previous_position_1CO = participants[closest_opponent_1CO]['previous_position2']\n#                 current_position_1CO = participants[closest_opponent_1CO]['current_position']\n#                 current_speed_1CO = participants[closest_opponent_1CO]['current_speed']\n#                 current_direction_1CO = participants[closest_opponent_1CO]['current_direction']\n#                 current_acceleration_1CO = participants[closest_opponent_1CO]['current_acceleration']\n#                 average_speed_1CO = participants[closest_opponent_1CO]['average_speed']\n#                 average_acceleration_1CO = participants[closest_opponent_1CO]['average_acceleration']\n\n#                 weight_carried_1CO = participants[closest_opponent_1CO]['weight_carried']\n\n#                 closest_opponent_2CO_index = lon_distances.index(min(lon_distances_updated))\n#                 closest_opponent_2CO = hj_pair_mapping[closest_opponent_2CO_index]\n#                 distance_2CO = lon_distances[closest_opponent_2CO_index] # min(lon_distances)\n#                 lat_dis_2CO = lat_distances[closest_opponent_2CO_index]\n#                 best_position_2CO = participants[closest_opponent_2CO]['best_position']\n#                 worst_position_2CO = participants[closest_opponent_2CO]['worst_position']\n#                 max_speed_2CO = participants[closest_opponent_2CO]['max_speed']\n#                 cumulative_speed_2CO = participants[closest_opponent_2CO]['cumulative_speed']\n#                 previous_speed_2CO = participants[closest_opponent_2CO]['previous_speed2']\n#                 max_acceleration_2CO = participants[closest_opponent_2CO]['max_acceleration']\n#                 cumulative_acceleration_2CO = participants[closest_opponent_2CO]['cumulative_acceleration']\n#                 previous_acceleration_2CO = participants[closest_opponent_2CO]['previous_acceleration2']\n#                 previous_direction_2CO = participants[closest_opponent_2CO]['previous_direction2']\n#                 previous_position_2CO = participants[closest_opponent_2CO]['previous_position2']\n#                 current_position_2CO = participants[closest_opponent_2CO]['current_position']\n#                 current_speed_2CO = participants[closest_opponent_2CO]['current_speed']\n#                 current_direction_2CO = participants[closest_opponent_2CO]['current_direction']\n#                 current_acceleration_2CO = participants[closest_opponent_2CO]['current_acceleration']\n#                 average_speed_2CO = participants[closest_opponent_2CO]['average_speed']\n#                 average_acceleration_2CO = participants[closest_opponent_2CO]['average_acceleration']\n#                 weight_carried_2CO = participants[closest_opponent_2CO]['weight_carried']\n\n#                 closest_opponent_3CO_index = lon_distances.index(min(lon_distances_updated2))\n#                 closest_opponent_3CO = hj_pair_mapping[closest_opponent_3CO_index]\n#                 distance_3CO = lon_distances[closest_opponent_3CO_index] # min(lon_distances)\n#                 lat_dis_3CO = lat_distances[closest_opponent_3CO_index]\n#                 best_position_3CO = participants[closest_opponent_3CO]['best_position']\n#                 worst_position_3CO = participants[closest_opponent_3CO]['worst_position']\n#                 max_speed_3CO = participants[closest_opponent_3CO]['max_speed']\n#                 cumulative_speed_3CO = participants[closest_opponent_3CO]['cumulative_speed']\n#                 previous_speed_3CO = participants[closest_opponent_3CO]['previous_speed2']\n#                 max_acceleration_3CO = participants[closest_opponent_3CO]['max_acceleration']\n#                 cumulative_acceleration_3CO = participants[closest_opponent_3CO]['cumulative_acceleration']\n#                 previous_acceleration_3CO = participants[closest_opponent_3CO]['previous_acceleration2']\n#                 previous_direction_3CO = participants[closest_opponent_3CO]['previous_direction2']\n#                 previous_position_3CO = participants[closest_opponent_3CO]['previous_position2']\n#                 current_position_3CO = participants[closest_opponent_3CO]['current_position']\n#                 current_speed_3CO = participants[closest_opponent_3CO]['current_speed']\n#                 current_direction_3CO = participants[closest_opponent_3CO]['current_direction']\n#                 current_acceleration_3CO = participants[closest_opponent_3CO]['current_acceleration']\n#                 average_speed_3CO = participants[closest_opponent_3CO]['average_speed']\n#                 average_acceleration_3CO = participants[closest_opponent_3CO]['average_acceleration']\n#                 weight_carried_3CO = participants[closest_opponent_3CO]['weight_carried']\n\n\n#                 opponent_features = [distance_1CO, max_speed_1CO, current_speed_1CO, cumulative_speed_1CO, average_speed_1CO,previous_speed_1CO,\n#                                    max_acceleration_1CO, current_acceleration_1CO, cumulative_acceleration_1CO, average_acceleration_1CO,previous_acceleration_1CO,\n#                                    previous_position_1CO, previous_direction_1CO,\n#                                    current_position_1CO, best_position_1CO, worst_position_1CO,lat_dis_1CO,weight_carried_1CO,\n#                                    distance_2CO, max_speed_2CO, current_speed_2CO, cumulative_speed_2CO, average_speed_2CO, previous_speed_2CO,\n#                                    max_acceleration_2CO, current_acceleration_2CO, cumulative_acceleration_2CO, average_acceleration_2CO, previous_acceleration_2CO,\n#                                    previous_position_2CO, previous_direction_2CO,  \n#                                    current_position_2CO, best_position_2CO, worst_position_2CO,lat_dis_2CO,weight_carried_2CO,\n#                                    distance_3CO, max_speed_3CO, current_speed_3CO, cumulative_speed_3CO, average_speed_3CO,previous_speed_3CO,\n#                                    max_acceleration_3CO, current_acceleration_3CO, cumulative_acceleration_3CO, average_acceleration_3CO,previous_acceleration_3CO,\n#                                    previous_position_3CO, previous_direction_3CO,  \n#                                    current_position_3CO, best_position_3CO, worst_position_3CO,lat_dis_3CO,weight_carried_3CO,\n#                                     track_id]\n\n#                 for feat in opponent_features:\n#                     trakus_data_byhj[j].append(feat)\n#                 trakus_data_byhj[j].append(participants[hj_pair]['final_position'])\n\n#                 processed_dataset.append(trakus_data_byhj[j])\n\n#                 j += 1\n                \n#     except ValueError:\n#         print(\"ValueError Occurred; potential noise that may damage model training.\")\n#         continue\n\n# processed_dataset = pd.DataFrame(processed_dataset, columns=['race_date','race_number','program_number','trakus_index',\n#                                                             'seconds_passed',\n#                                                             'current_position','best_position','worst_position','previous_position',\n#                                                             'run_up_distance',\n#                                                             'max_speed','current_speed','cumulative_speed','average_speed','previous_speed',\n#                                                             'current_direction','previous_direction',\n#                                                             'max_acceleration','curent_acceleration','cumulative_acceleration','average_acceleration','previous_acceleration',\n#                                                             'number_of_participants','track_condition','race_type','course_type',\n#                                                             'weight_carried',\n#                                                            'distance_1CO', 'max_speed_1CO', 'current_speed_1CO', 'cumulative_speed_1CO', 'average_speed_1CO','previous_speed1CO',\n#                                                            'max_acceleration_1CO', 'current_acceleration_1CO', 'cumulative_acceleration_1CO', 'average_acceleration_1CO','previous_acceleration1CO',\n#                                                            'previous_position_1CO', 'previous_direction_1CO',  \n#                                                             'current_position_1CO', 'best_position_1CO', 'worst_position_1CO','lat_dis_1CO','weight_carried_1CO',\n#                                                            'distance_2CO', 'max_speed_2CO', 'current_speed_2CO', 'cumulative_speed_2CO', 'average_speed_2CO','previous_speed2CO',\n#                                                            'max_acceleration_2CO', 'current_acceleration_2CO', 'cumulative_acceleration_2CO', 'average_acceleration_2CO','previous_acceleration2CO',\n#                                                            'previous_position_2CO', 'previous_direction_2CO',  \n#                                                             'current_position_2CO', 'best_position_2CO', 'worst_position_2CO','lat_dis_2CO','weight_carried_2CO',\n#                                                            'distance_3CO', 'max_speed_3CO', 'current_speed_3CO', 'cumulative_speed_3CO', 'average_speed_3CO','previous_speed3CO',\n#                                                            'max_acceleration_3CO', 'current_acceleration_3CO', 'cumulative_acceleration_3CO', 'average_acceleration_3CO','previous_acceleration3CO',\n#                                                            'previous_position_3CO', 'previous_direction_3CO',  \n#                                                             'current_position_3CO', 'best_position_3CO', 'worst_position_3CO','lat_dis_3CO','weight_carried_3CO',\n#                                                            'track_id','final_position'])\n# processed_dataset = processed_dataset.sort_values(['race_date','race_number','program_number','trakus_index'], ascending=[True, True, True, True]).reset_index(drop=True)\n\n# processed_dataset.to_csv(\"./processed_dataset.csv\", index=False)\n\n# '''Process categorical variables; map response variable in the 0-1 range (and adapt similar features)'''\n\n\n# processed_dataset = pd.get_dummies(processed_dataset, \n#                                    columns=['track_condition','race_type','course_type'], \n#                                    prefix=['track_condition_','race_type_','course_type_'], \n#                                    drop_first=True)\n# final_positions = []\n# current_positions = []\n# best_positions = []\n# worst_positions = []\n# previous_positions = []\n# current_positions_1CO = []\n# best_positions_1CO = []\n# worst_positions_1CO = []\n# previous_positions_1CO = []\n# current_positions_2CO = []\n# best_positions_2CO = []\n# worst_positions_2CO = []\n# previous_positions_2CO = []\n# current_positions_3CO = []\n# best_positions_3CO = []\n# worst_positions_3CO = []\n# previous_positions_3CO = []\n\n# for index in range(len(processed_dataset.reset_index(drop=True)['final_position'])):\n#     number_of_participants = processed_dataset['number_of_participants'][index]\n#     scaling_factor = 1 / (number_of_participants - 1)\n    \n#     final_position = processed_dataset['final_position'][index]\n#     current_position = processed_dataset['current_position'][index]\n#     best_position = processed_dataset['best_position'][index]\n#     worst_position = processed_dataset['worst_position'][index]\n#     previous_position = processed_dataset['previous_position'][index]\n#     current_position_1CO = processed_dataset['current_position_1CO'][index]\n#     best_position_1CO = processed_dataset['best_position_1CO'][index]\n#     worst_position_1CO = processed_dataset['worst_position_1CO'][index]\n#     previous_position_1CO = processed_dataset['previous_position_1CO'][index]\n#     current_position_2CO = processed_dataset['current_position_2CO'][index]\n#     best_position_2CO = processed_dataset['best_position_2CO'][index]\n#     worst_position_2CO = processed_dataset['worst_position_2CO'][index]\n#     previous_position_2CO = processed_dataset['previous_position_2CO'][index]\n#     current_position_3CO = processed_dataset['current_position_3CO'][index]\n#     best_position_3CO = processed_dataset['best_position_3CO'][index]\n#     worst_position_3CO = processed_dataset['worst_position_3CO'][index]\n#     previous_position_3CO = processed_dataset['previous_position_3CO'][index]\n\n    \n#     final_positions.append(scaling_factor * (final_position - 1))\n#     current_positions.append(scaling_factor * (current_position - 1))\n#     best_positions.append(scaling_factor * (best_position - 1))\n#     worst_positions.append(scaling_factor * (worst_position - 1))\n#     previous_positions.append(scaling_factor * (previous_position - 1))\n#     current_positions_1CO.append(scaling_factor * (current_position_1CO - 1))\n#     best_positions_1CO.append(scaling_factor * (best_position_1CO - 1))\n#     worst_positions_1CO.append(scaling_factor * (worst_position_1CO - 1))\n#     previous_positions_1CO.append(scaling_factor * (previous_position_1CO - 1))\n#     current_positions_2CO.append(scaling_factor * (current_position_2CO - 1))\n#     best_positions_2CO.append(scaling_factor * (best_position_2CO - 1))\n#     worst_positions_2CO.append(scaling_factor * (worst_position_2CO - 1))\n#     previous_positions_2CO.append(scaling_factor * (previous_position_2CO - 1))\n#     current_positions_3CO.append(scaling_factor * (current_position_3CO - 1))\n#     best_positions_3CO.append(scaling_factor * (best_position_3CO - 1))\n#     worst_positions_3CO.append(scaling_factor * (worst_position_3CO - 1))\n#     previous_positions_3CO.append(scaling_factor * (previous_position_3CO - 1))\n\n# processed_dataset['final_position'] = final_positions\n# processed_dataset['current_position'] = current_positions\n# processed_dataset['best_position'] = best_positions\n# processed_dataset['worst_position'] = worst_positions\n# processed_dataset['previous_position'] = previous_positions\n# processed_dataset['current_position_1CO'] = current_positions_1CO \n# processed_dataset['best_position_1CO'] = best_positions_1CO \n# processed_dataset['worst_position_1CO'] = worst_positions_1CO \n# processed_dataset['previous_position_1CO'] = previous_positions_1CO \n# processed_dataset['current_position_2CO'] = current_positions_2CO \n# processed_dataset['best_position_2CO'] = best_positions_2CO \n# processed_dataset['worst_position_2CO'] = worst_positions_2CO \n# processed_dataset['previous_position_2CO'] = previous_positions_2CO \n# processed_dataset['current_position_3CO'] = current_positions_3CO \n# processed_dataset['best_position_3CO'] = best_positions_3CO \n# processed_dataset['worst_position_3CO'] = worst_positions_3CO \n# processed_dataset['previous_position_3CO'] = previous_positions_3CO \n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true},"execution_count":null,"outputs":[]}]}