{"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":"# Big Data Derby - A Performance Proxy 🏇\n\nSince the competition was launched, the lack of race results left many of us struggling with measuring horse performance across all races and tracks.\n\nHere is my attempt at defining a performance proxy, which seems to be negatively, moderately correlate with each individual horse racing odds as one would expect.\n\nPlease feel free to use some of these insights; however also note this notebook is meant to be a competition submission, and is under development.\n\n## Imports and Data Loading\n","metadata":{}},{"cell_type":"code","source":"# Import libs\nimport numpy as np\nimport pandas as pd\nimport pymc3 as pm\nimport geopy.distance as dist\nimport matplotlib.pyplot as plt\npd.options.mode.chained_assignment = None\n\n# Load CSV files\nrc_tbl = pd.read_csv(\"../input/big-data-derby-2022/nyra_race_table.csv\")\ntr_tbl = pd.read_csv(\"../input/big-data-derby-2022/nyra_tracking_table.csv\")\n# Handle start table w/ missing header\nst_tbl = pd.read_csv('../input/big-data-derby-2022/nyra_start_table.csv', header=None)\n\ncols = [\n    \"track_id\",\n    \"race_date\",\n    \"race_number\",\n    \"program_number\",\n    \"weight_carried\",\n    \"jockey\",\n    \"odds\",\n    \"position_at_finish\"\n]\nst_tbl.rename({i: col for i, col in enumerate(cols)}, axis=1, inplace=True)","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2022-09-25T18:25:14.735392Z","iopub.execute_input":"2022-09-25T18:25:14.736108Z","iopub.status.idle":"2022-09-25T18:25:18.846776Z","shell.execute_reply.started":"2022-09-25T18:25:14.736069Z","shell.execute_reply":"2022-09-25T18:25:18.845853Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Horse Speed Profiles\n\n### Calculation\n\nThe first step is, for every competing horse, determining the distance in meters between every two successive positions and divide it by 0.25s - the spanning period indicated by the organizing team - thus yielding speed as meters per second, $ms^{-1}$.","metadata":{}},{"cell_type":"code","source":"%%time\n# Sort by trakus indices\ntr_tbl = tr_tbl.sort_values(\"trakus_index\")\n\n# Extract latlong of next trakus index, drop NAs\ntr_gb = tr_tbl.groupby([\"track_id\", \"race_date\", \"race_number\", \"program_number\"])\ntr_tbl[\"lat_next\"] = tr_gb[\"latitude\"].shift(-1)\ntr_tbl[\"long_next\"] = tr_gb[\"longitude\"].shift(-1)\ntr_tbl = tr_tbl.dropna(subset=[\"lat_next\", \"long_next\"])\n\n# Compute m/s per point (numpy)\ntr_tbl[\"speed\"] = np.apply_along_axis(\n    func1d=lambda x: dist.geodesic((x[0], x[1]), (x[2], x[3])).m / .25,\n    axis=1,\n    arr=tr_tbl[[\"latitude\", \"longitude\", \"lat_next\", \"long_next\"]]\n    )","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:05:07.200232Z","iopub.execute_input":"2022-09-20T15:05:07.200652Z","iopub.status.idle":"2022-09-20T15:23:36.108370Z","shell.execute_reply.started":"2022-09-20T15:05:07.200617Z","shell.execute_reply":"2022-09-20T15:23:36.107287Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This descriptor will still require further processing - at the very least we should *i)* ensure speed values fall within a reasonable range and *ii)* utilize convolution to induce local smoothness in the resulting profiles. Let us tackle that next.\n\n### Processing\n\nHorse speed is obviously contrained by certain physical limits, and we can leverage this to identify and exclude outliers. Let us have a look into the left and right tails of the speed distribution.","metadata":{}},{"cell_type":"code","source":"# Check tails of speed distribution\ntr_tbl[\"speed\"].sort_values()","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:24:09.150673Z","iopub.execute_input":"2022-09-20T15:24:09.151074Z","iopub.status.idle":"2022-09-20T15:24:10.358080Z","shell.execute_reply.started":"2022-09-20T15:24:09.151041Z","shell.execute_reply":"2022-09-20T15:24:10.356971Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"While a horse may be stationary and speed is certainly non-negative, no horse can possibly run over 300 m/s so we need to discard these observations. Let us see if setting an upper limit of 30 m/s provides a more realistic distribution. Note that offending values are set to NA, which we will handle next when utilizing a convolutional operator.","metadata":{}},{"cell_type":"code","source":"# Limit observed speed to < 30 m/s\ntr_tbl[\"speed_adj\"] = np.where(tr_tbl[\"speed\"] < 30, tr_tbl[\"speed\"], np.nan)\n\n# Plot dist speed\nplt.figure(figsize=(8, 4))\nplt.hist(tr_tbl[\"speed_adj\"])\nplt.xlabel(\"Horse speed [m/s]\")\nplt.ylabel(\"Frequency\")\n\nplt.tight_layout()","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:24:11.781823Z","iopub.execute_input":"2022-09-20T15:24:11.782741Z","iopub.status.idle":"2022-09-20T15:24:12.189626Z","shell.execute_reply.started":"2022-09-20T15:24:11.782704Z","shell.execute_reply":"2022-09-20T15:24:12.188542Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"That seems to be the case, as speed does not extend much beyond 25 m/s. Now let us work on a convolutional filter to induce local smoothness after forward-filling missing values (in due time I will test more sophisticated imputation strategies).\n\nFor inducing local smoothness, I propose using a kernel of size five with uniform weights. This is based on my own experimentation, one might rightfully argue we could use a kernel of size three instead.","metadata":{}},{"cell_type":"code","source":"# Apply forward-filling NAs\ntr_tbl[\"speed_adj\"] = tr_tbl[\"speed_adj\"].ffill()\n\n# Refresh grouped track table\ntr_gb = tr_tbl.groupby([\"track_id\", \"race_date\", \"race_number\", \"program_number\"])\n\n# Create conv kernel\nkernel = [.2, .2, .2, .2, .2]\n# Return new df with final adjustments\nspeed_tbl = tr_gb[\"speed_adj\"].apply(\n    lambda x: np.convolve(x, kernel, \"valid\")\n).rename(\"speed\").reset_index()","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:24:13.661071Z","iopub.execute_input":"2022-09-20T15:24:13.661905Z","iopub.status.idle":"2022-09-20T15:24:17.136661Z","shell.execute_reply.started":"2022-09-20T15:24:13.661866Z","shell.execute_reply":"2022-09-20T15:24:17.134859Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Visualize Speed Profiles\n\nLet us examine the adjusted speed profiles of the horses competing on `race_number` 1, on January 1st 2019, in Aqueduct.","metadata":{}},{"cell_type":"code","source":"# Check example race\ntrack = speed_tbl[\"track_id\"] == \"AQU\"\ndate = speed_tbl['race_date'] == \"2019-01-01\"\nrace_n = speed_tbl[\"race_number\"] == 1\n\nrace = speed_tbl.loc[track & date & race_n, :]\n\n# Plot speed profile from example race\nplt.figure(figsize=(8, 4))\nfor horse in race[\"speed\"]:\n    plt.plot(horse)\n# Labels\nplt.xlabel(\"Trakus pair no.\")\nplt.ylabel(\"Horse speed [m/s]\")\n\nplt.tight_layout()","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:24:19.915000Z","iopub.execute_input":"2022-09-20T15:24:19.915517Z","iopub.status.idle":"2022-09-20T15:24:20.189345Z","shell.execute_reply.started":"2022-09-20T15:24:19.915473Z","shell.execute_reply":"2022-09-20T15:24:20.188449Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"As noted in various other notebooks, we can easily see when the race begins and ends; throughout we can also observed that speed is maximum early on, then decays slowly towards the end of the race. Interestingly there are regions in the above profiles that correlate well among horses, perhaps suggesting track segments that might be easier or harder, for example a slight upward inclination.\n\n## Validate Performance Proxy\n\nIdeally, if we want to consider horse speed as a performance proxy, we would do well relating it to - among other variables - the winning odds, since these inherently carry the expected performance and should weigh in history and various other relevant factors.\n\nHowever, we need to determine a single value from each speed profile associated with each horse; also, we need to make performance comparable across race types, since some will incur longer or shorter distances. To solve these two problems I decided to normalize the sum of the speed values - essentially the area under the profile curve - by mean-centering and unit-variance scaling. In addition, I will discard the first and last 20 trakus pairs since these largely reflect the very beginning and end of the races, respectively.\n\nLet us do that quickly and check if there is any meaningful relationship between this performance metric (Z-scores) and the log-odds.","metadata":{}},{"cell_type":"code","source":"# Add start table to speed\nspeed_tbl = speed_tbl.merge(st_tbl,\n    on=[\"track_id\", \"race_date\", \"race_number\", \"program_number\"],\n    how=\"left\")\n\n# Compute performance as the sum of the pointwise speed values per horse\nspeed_tbl[\"performance\"] = speed_tbl[\"speed\"].apply(sum)\n\n# Create race-specific Z scores so that performance can be compared across\nspeed_gb = speed_tbl.groupby([\"track_id\", \"race_date\", \"race_number\"])\nspeed_tbl[\"performance_z\"] = speed_gb[\"performance\"].transform(\n    lambda x: (x - x.mean()) / x.std()\n)\n\n# Plot Z scores against log of the odds of winning\nplt.scatter(speed_tbl[\"performance_z\"], np.log(speed_tbl[\"odds\"] + 1), alpha=.1, s=1)\nplt.xlabel(\"Performance Z\")\nplt.ylabel(\"Log-Winning-Odds\")\n\n# Print correlation\ncorr = np.corrcoef(speed_tbl[\"performance_z\"], np.log(speed_tbl[\"odds\"] + 1))\nprint(f\"Correlation: {corr}\")","metadata":{"execution":{"iopub.status.busy":"2022-09-20T15:24:25.434606Z","iopub.execute_input":"2022-09-20T15:24:25.435813Z","iopub.status.idle":"2022-09-20T15:24:25.676097Z","shell.execute_reply.started":"2022-09-20T15:24:25.435741Z","shell.execute_reply":"2022-09-20T15:24:25.674411Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"As expected, we see a (modest) negative correlation between performance and the log-odds - the worse the relative performance of a horse in a given race, the lower its winning odds. This is quite encouraging considering we did not even look at the effective race finish line.\n\n## Horse Acceleration Profiles\n\nTBD","metadata":{}},{"cell_type":"code","source":"# Extract acceleration by repeating shift(-1) process\n#gb = track_tbl.groupby([\"track_id\", \"race_date\", \"race_number\", \"program_number\"])\n#track_tbl[\"acc\"] = (track_tbl[\"speed\"] - gb[\"speed\"].shift(1)) / .25\n#track_tbl = track_tbl.dropna(subset=[\"acc\"])","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Final Remarks\n\nPlease give this notebook a 👍 if you liked it. As mentioned, this is work under development! Good luck everyone and thanks for the great discussions 🙂\n\n... STAY TUNED ...","metadata":{}}]}