{"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":"code","source":"# Import necessary packages\nimport pandas as pd\nimport numpy as np\nimport plotly.express as px\n\nfrom IPython.display import Image\n\n# Import utility scripts\nimport bdd_gpshelper as gh\nimport bdd_distancemeasures as dm","metadata":{"_uuid":"3897d009-4215-4f87-b185-db00d52024ce","_cell_guid":"4feea227-707e-45bb-b1db-3affd6e34b19","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2022-10-13T11:58:17.728376Z","iopub.execute_input":"2022-10-13T11:58:17.728686Z","iopub.status.idle":"2022-10-13T11:58:17.736004Z","shell.execute_reply.started":"2022-10-13T11:58:17.728664Z","shell.execute_reply":"2022-10-13T11:58:17.734777Z"},"_kg_hide-input":true,"_kg_hide-output":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Introduction\n\nThe aim of this notebook is to show how I preprocessed the data used for the initialisation of the reinforcement learning environment. I deliberately preprocessed all the data so that each reset command of the reinforcement learning environment could happen quickly. All functions used are derived from the bdd_gpshelper (gh) and bdd_distancemeasures (dm) utility scripts. I copy the function into the cell where it is used for ease of reference.","metadata":{}},{"cell_type":"markdown","source":"# Import and filter data\n\nFirst, import the data and ensure the column headers are correct. Thank you to Dmitry Uarov for the code used in this cell. I reduce to data to a single race on Belmont Stakes Day to limit computation time for this example notebook.","metadata":{}},{"cell_type":"code","source":"data = pd.read_csv('../input/big-data-derby-2022/nyra_2019_complete.csv', header=None)\n\ncols = ['track_id', 'race_date', 'race_number', 'program_number', 'trakus_index', 'latitude', 'longitude', 'distance_id', 'course_type', 'track_condition', 'run_up_distance', 'race_type', 'purse', 'post_time', 'weight_carried', 'jockey', 'odds', 'final_place']\ndata.rename({i: col for i, col in enumerate(cols)}, axis=1, inplace=True)\n\nrace_data = data.loc[(data['track_id'] == 'BEL') & (data['race_date'] == '2019-06-08') & (data['course_type'] == 'D') & (data['race_number'] == 8)].copy()","metadata":{"execution":{"iopub.status.busy":"2022-10-13T11:43:48.947636Z","iopub.execute_input":"2022-10-13T11:43:48.947959Z","iopub.status.idle":"2022-10-13T11:43:56.239119Z","shell.execute_reply.started":"2022-10-13T11:43:48.947936Z","shell.execute_reply":"2022-10-13T11:43:56.237688Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Clean program numbers\n\nIn order to ensure there is no confusion with respect to program numbers and strings/integers being misrepresented, the gh.clean_program_number() function changes all program numbers to a 3 number string value (e.g. 1 becomes '001', 12 becomes '012' etc). This allows races with 1X ('01X') or 2A ('02A') horse numbers to be considered without any exceptions.","metadata":{}},{"cell_type":"code","source":"# Function\n# Standardise program numbers\ndef clean_program_number(prog_num):\n    \n    # data.program_number.unique() gives all the different number identities\n    # p_num_dict provides mapping to the 3 number string value\n    p_num_dict = {'1  ': '001', 1: '001', '1A ': '01A', '1X ': '01X',\n                  '2  ': '002', 2: '002', '2B ': '02B',\n                  '3  ': '003', 3: '003', '3X ': '03X',\n                  '4  ': '004', 4: '004',\n                  '5  ': '005', 5: '005',\n                  '6  ': '006', 6: '006',\n                  '7  ': '007', 7: '007',\n                  '8  ': '008', 8: '008',\n                  '9  ': '009', 9: '009',\n                  '10 ': '010', 10: '010',\n                  '11 ': '011', 11: '011',\n                  '12 ': '012', 12: '012',\n                  '13 ': '013', 13: '013',\n                  '14 ': '014', 14: '014',\n                  '15 ': '015', 15: '015',\n                  '16 ': '016', 16: '016'}\n    \n    return prog_num.map(p_num_dict).astype('str')\n\n\n# Usage\nrace_data['program_number'] = gh.clean_program_number(race_data['program_number'])","metadata":{"execution":{"iopub.status.busy":"2022-10-13T11:46:54.427068Z","iopub.execute_input":"2022-10-13T11:46:54.427408Z","iopub.status.idle":"2022-10-13T11:46:54.434733Z","shell.execute_reply.started":"2022-10-13T11:46:54.427383Z","shell.execute_reply":"2022-10-13T11:46:54.433292Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Create unique Race IDs\n\nAlthough not strictly required for this example, there is a function to create unique race IDs so that the data can be obtained easier. These IDs take the columns provided (in this case race date and race number) and use them to create a unique race ID for each race within the dataset. I sort by race number, program number and trakus index from preference more than anything else.","metadata":{}},{"cell_type":"code","source":"# Function\n# Generate race IDs for each race\ndef make_raceid(df):\n    \n    # Create string ID\n    str_id = df.apply(lambda x: '_'.join(map(str, x)), axis=1)\n    \n    # Return as factored number\n    return pd.factorize(str_id)[0]\n\n\n# Usage\nrace_data = race_data.sort_values(by=['race_number', 'program_number', 'trakus_index'])\nrace_data['race_id'] = gh.make_raceid(race_data[['race_date', 'race_number']])","metadata":{"execution":{"iopub.status.busy":"2022-10-13T11:53:44.845084Z","iopub.execute_input":"2022-10-13T11:53:44.845411Z","iopub.status.idle":"2022-10-13T11:53:44.884913Z","shell.execute_reply.started":"2022-10-13T11:53:44.845387Z","shell.execute_reply":"2022-10-13T11:53:44.884229Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Generating x and y coordinates\n\nHere, x and y coordinates are generated. I have added the data for this to belmontlatlong.csv, which in some cases saves the data to 1 decimal place less. I don't know why so I've added all values in raw format to my functions. In the csv file, home_outer_rail_1 provides the latitude, longitude for the coordinates 0,0 in our xy plot. home_outer_rail_2 provides the latitude, longitude for the coordinates 1150,0. They refer to the left and right ends of the straight white line which runs along the rail closest to the stands in the image below. home_straight_entrance and finish_line are used later on, but provide the latitude, longitude of the two pins in the image.\n\nThe function works by using pythagorean equations to estimate the location of the horse based on the known locations on the outer rail. By knowing their locations and the distance between them, the distance between the horse and both points can be used to identify its location. After calculating each x and y coordinate, I shift the values upwards twice - once to give the previous coordinates (0.25s previously), and again to provide the coordinates 0.5 seconds previously. I also calculate the movement vector, for the x and y coordinate movements. These steps make future calculations easier.","metadata":{}},{"cell_type":"code","source":"Image(filename=\"../input/belmontgoogleearth/Belmont Park Earth.png\")","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Function\n# Convert latitude,longitude to x,y coordinates\ndef xy_coordinates(xy, latitude, longitude):\n    \n    '''\n    Use the known HS1 and HS2 co-ordinates to get an xy map of the course.\n    HS1 (0,0) extends left of the home straight, HS2 extends right of the track\n    so all co-ordinates lie within the HS1, HS2 range along the x-axis.\n    '''\n    \n    # Set HS1 and HS2\n    HS1 = (40.71475945171949, -73.7291971926194)\n    HS2 = (40.71140369439631, -73.71632313180034)\n    \n    # Set latitude/longitude as tuple.\n    # Float ensures it is not a string value\n    location = (float(latitude), float(longitude))\n    \n    # Get distances\n    HS1_HS2 = geopy.distance.geodesic(HS1, HS2).m\n    HS1_loc = geopy.distance.geodesic(HS1, location).m\n    HS2_loc = geopy.distance.geodesic(HS2, location).m\n    \n    # Identify x and y co-ordinates\n    loc_x = ((HS1_HS2**2) + (HS1_loc**2) - (HS2_loc**2)) / (2*HS1_HS2)\n    loc_y = abs((HS1_loc**2) - (loc_x**2)) ** 0.5\n    \n    # Return co-ordinate\n    if xy == 'x':\n        return loc_x\n    elif xy == 'y':\n        return loc_y\n    else:\n        raise ValueError('xy should hold value \"x\" or \"y\"')\n\n\n# Usage\nrace_data['x'] = race_data.apply(lambda row: gh.xy_coordinates('x', row['latitude'], row['longitude']), axis=1)\nrace_data['y'] = race_data.apply(lambda row: gh.xy_coordinates('y', row['latitude'], row['longitude']), axis=1)\nrace_data['x1'] = race_data.groupby(['program_number']).x.shift(1)\nrace_data['y1'] = race_data.groupby(['program_number']).y.shift(1)\nrace_data['x2'] = race_data.groupby(['program_number']).x.shift(2)\nrace_data['y2'] = race_data.groupby(['program_number']).y.shift(2)\nrace_data['x_x1'] = race_data['x'] - race_data['x1']\nrace_data['y_y1'] = race_data['y'] - race_data['y1']","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:08:51.110300Z","iopub.execute_input":"2022-10-13T12:08:51.110639Z","iopub.status.idle":"2022-10-13T12:08:55.562252Z","shell.execute_reply.started":"2022-10-13T12:08:51.110616Z","shell.execute_reply":"2022-10-13T12:08:55.560934Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Identify whether horse has finished race\n\nNow, I input the home straight coordinates and finish line coordinates to the xy_coordinates() function to get their x and y values. Again, these can be taken directly from the csv file if desired. The finish line coordinate is used to decide whether a horse has crossed the finish line at any given trakus index. \n\nNote - this will result in some strange values if horses cross the finish line twice in any race. This isn't a problem for me as I eventually only consider the last 1000m or so but is something to consider if modelling the full race in the future.","metadata":{}},{"cell_type":"code","source":"finish_coordinates = (gh.xy_coordinates('x', 40.71330475501588, -73.72260194071144), \n                      gh.xy_coordinates('y', 40.71330475501588, -73.72260194071144))\n\nhome_coordinates = (gh.xy_coordinates('x', 40.71405567333758, -73.7264990114551), \n                    gh.xy_coordinates('y', 40.71405567333758, -73.7264990114551))\n\nrace_data['is_finished'] = np.where((race_data['x'] > finish_coordinates[0]) & (race_data['y'] < finish_coordinates[1]+5), 1, 0)","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:33:42.747397Z","iopub.execute_input":"2022-10-13T12:33:42.747742Z","iopub.status.idle":"2022-10-13T12:33:42.758356Z","shell.execute_reply.started":"2022-10-13T12:33:42.747718Z","shell.execute_reply":"2022-10-13T12:33:42.757614Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Getting rail coordinates\n\nNow, I get the coordinates of the rails at each time point. To do this, I calculate the orthogonal vector from all horses' previous movement vector and take the value that is closest to the identified central point. Again, when the race being assessed is longer than one circuit, it works better if this function is performed when the dataset is reduced in size. finish_coordinates(1) is used to identify the y value of the central location, home_coordinates(0) is used to identify if the horses are on a turn, in which case home_coordinates(0) is used as the x-value of the central location.\n","metadata":{}},{"cell_type":"code","source":"# Function\n# Get rail coordinates for race\ndef get_rail_coordinates(race_data, home_x, finish_y):\n    \n    # Set up loop information\n    x_coords = []\n    y_coords = []\n    centre_x_list = []\n    indexes = sorted(race_data.trakus_index.unique())\n    for idx in indexes[1:]:\n    \n        # Get data for trakus index\n        line_data = race_data.loc[race_data['trakus_index'] == idx].copy()\n    \n        # Set x_mean and y_mean values for comparison\n        if line_data['x'].min() < home_x:\n            x_mean = home_x\n            centre_x_list.append(x_mean)\n        else:\n            x_mean = line_data['x'].mean()\n            centre_x_list.append(x_mean)\n        y_mean = (420-finish_y)/2\n    \n        # Take orthogonal projection of horse's current position to identify \n        # co-ordinates of possible rail position\n        # Dividing vec norm by 8 works visually but could be changed when/wherever needed\n        line_data['vec_norm'] = np.linalg.norm(line_data[['x_x1', 'y_y1']].values) / 8\n            \n        line_data['perp_x'] = line_data['y_y1']/line_data['vec_norm']\n        line_data['perp_y'] = -line_data['x_x1']/line_data['vec_norm']\n    \n        # Co-ordinates are x-perp_x, y-perp_y\n        line_data['rail_x'] = line_data['x'] - line_data['perp_x']\n        line_data['rail_y'] = line_data['y'] - line_data['perp_y']\n    \n        # Calculate distance to centre point\n        line_data['centre_dist'] = np.linalg.norm(line_data[['rail_x', 'rail_y']].values - np.array([x_mean, y_mean]))\n        \n        # Take minimum distance and add to co-ordinates\n        x_coords.append(line_data.loc[line_data['centre_dist'].idxmin()]['rail_x'])\n        y_coords.append(line_data.loc[line_data['centre_dist'].idxmin()]['rail_y'])\n\n    return x_coords, y_coords, centre_x_list\n\n\n# Usage\nrail_x, rail_y, cx_list = gh.get_rail_coordinates(race_data, home_coordinates[0], finish_coordinates[1])","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:33:49.432096Z","iopub.execute_input":"2022-10-13T12:33:49.432431Z","iopub.status.idle":"2022-10-13T12:33:50.837467Z","shell.execute_reply.started":"2022-10-13T12:33:49.432407Z","shell.execute_reply":"2022-10-13T12:33:50.836602Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Rail measures\n\nHaving calculated the rail locations, we can now calculate the distance between the horse and rail and the angle the horse is travelling in relation to the rail. I use length of the vector from horse position to nearest rail coordinate for the rail distance, and I use the atan2 function for the direction of travel (-ve indicates moving towards the inside rail, +ve moves away from it)","metadata":{}},{"cell_type":"code","source":"# Function\n# Get distance to rail\ndef rail_dist(x, y, rail_x, rail_y):\n    \n    dist = np.linalg.norm(np.array([x,y]).T - np.array([rail_x, rail_y]).T, axis=1).min()\n    \n    return dist\n\n# Usage\nrace_data['rail_dist'] = race_data.apply(lambda row: dm.rail_dist(row['x'], row['y'], rail_x, rail_y), axis=1)\n\n\n# Function\n# Get angle of travel relative to rail\ndef rail_angle(x, y, x1, y1, rail_x, rail_y):\n    \n    rail_idx = closest_rail('xy', x, y, rail_x, rail_y)\n    \n    vx = np.array([x-x1, y-y1])\n    try:\n        vr = np.array([rail_x[rail_idx+1]-rail_x[rail_idx-1], rail_y[rail_idx+1]-rail_y[rail_idx-1]])\n    except IndexError:\n        if rail_idx == 0:\n            vr = np.array([rail_x[rail_idx+1]-rail_x[rail_idx], rail_y[rail_idx+1]-rail_y[rail_idx]])\n        else:\n            vr = np.array([rail_x[rail_idx]-rail_x[rail_idx-1], rail_y[rail_idx]-rail_y[rail_idx-1]])\n    \n    angle = math.atan2(vr[1], vr[0]) - math.atan2(vx[1], vx[0])\n    \n    # Keep angle less than 180 degrees so directions work\n    if angle > np.pi:\n        angle -= 2*np.pi\n    elif angle < -np.pi:\n        angle += 2*np.pi\n        \n    return angle\n\n# Usage\nrace_data['rail_angle'] = race_data.apply(lambda row: dm.rail_angle(row['x'], row['y'], row['x1'], row['y1'], rail_x, rail_y), axis=1)","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:40:16.779451Z","iopub.execute_input":"2022-10-13T12:40:16.779778Z","iopub.status.idle":"2022-10-13T12:40:17.276838Z","shell.execute_reply.started":"2022-10-13T12:40:16.779755Z","shell.execute_reply":"2022-10-13T12:40:17.275331Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Speed and acceleration\n\nSpeed and acceleration are calculated fairly easily as shown. Speed is just the length of the vector between the last two time steps divided by 0.25s (distance/time). I again shift the previous speed down one row as it allows acceleration to be calculated much more easily as the difference in speed divided by the time.","metadata":{}},{"cell_type":"code","source":"race_data['speed_ms'] = np.linalg.norm(race_data[['x', 'y']].values - race_data[['x1', 'y1']].values, axis=1)/0.25\nrace_data['speed_ms1'] = race_data.groupby(['program_number']).speed_ms.shift(1)\nrace_data['accel'] = (race_data['speed_ms'] - race_data['speed_ms1'])/0.25","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:43:33.772679Z","iopub.execute_input":"2022-10-13T12:43:33.773145Z","iopub.status.idle":"2022-10-13T12:43:33.782628Z","shell.execute_reply.started":"2022-10-13T12:43:33.773121Z","shell.execute_reply":"2022-10-13T12:43:33.781813Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Closest rival information\n\nIn preparation for use within the reinforcement learning environment, I calculate details regarding the five closest opponents. I identify the relative x and y coordinates of the horses and their speed relative to the subject horse. The x and y coordinates are obtained via simple subtraction from the subject horse's coordinates. Similarly, the speed is subtracted from the subject horse's speed.\n\n","metadata":{}},{"cell_type":"code","source":"# Function\n# Get xy coordinates of closest horses\ndef closest_horse_xy(xy, race_data, how_close, trakus, prog_num, x, y):\n\n    # Get data for trakus_id and remove horse being assessed\n    close_frame = race_data.loc[race_data['trakus_index'] == trakus].copy()\n    close_frame = close_frame.loc[close_frame['program_number'] != prog_num]\n        \n    # Get array of opponent locations\n    close_opps = close_frame[['x', 'y']].values\n    \n    # Get idx of opponent, then get x or y difference\n    if close_frame['program_number'].size > how_close-1:\n        opp_dist = np.linalg.norm(np.array([x,y]).T - close_opps, axis=1)\n        opp_idx = np.argpartition(opp_dist, how_close-1)[how_close-1]\n        \n        if xy == 'x':\n            return close_opps[opp_idx][0] - x\n        elif xy == 'y':\n            return close_opps[opp_idx][1] - y\n    \n    else:\n        return np.nan\n\n\n# Usage\nrace_data['close1_x'] = race_data.apply(lambda row: dm.closest_horse_xy('x', race_data, 1, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close2_x'] = race_data.apply(lambda row: dm.closest_horse_xy('x', race_data, 2, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close3_x'] = race_data.apply(lambda row: dm.closest_horse_xy('x', race_data, 3, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close4_x'] = race_data.apply(lambda row: dm.closest_horse_xy('x', race_data, 4, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close5_x'] = race_data.apply(lambda row: dm.closest_horse_xy('x', race_data, 5, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\n\nrace_data['close1_y'] = race_data.apply(lambda row: dm.closest_horse_xy('y', race_data, 1, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close2_y'] = race_data.apply(lambda row: dm.closest_horse_xy('y', race_data, 2, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close3_y'] = race_data.apply(lambda row: dm.closest_horse_xy('y', race_data, 3, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close4_y'] = race_data.apply(lambda row: dm.closest_horse_xy('y', race_data, 4, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\nrace_data['close5_y'] = race_data.apply(lambda row: dm.closest_horse_xy('y', race_data, 5, row['trakus_index'], row['program_number'], row['x'], row['y']), axis=1)\n\n\n# Function\n# Get relative speed of closest horses\ndef closest_horse_speed(race_data, how_close, trakus, prog_num, x, y, speed):\n    \n    # Get data for trakus_id and remove horse being assessed\n    close_frame = race_data.loc[race_data['trakus_index'] == trakus].copy()\n    close_frame = close_frame.loc[close_frame['program_number'] != prog_num]\n        \n    # Get array of opponent locations\n    close_opps = close_frame[['x', 'y']].values\n\n    # Get distance to relevant opponent\n    if close_frame['program_number'].size > how_close-1:\n        opp_dist = np.linalg.norm(np.array([x,y]).T - close_opps, axis=1)\n        opp_idx = np.argpartition(opp_dist, how_close-1)[how_close-1]  \n    \n        opp_speed = close_frame.loc[(close_frame['x'] == close_opps[opp_idx][0]) & (close_frame['y'] == close_opps[opp_idx][1])]['speed_ms'].item()\n    \n    else:\n        return np.nan\n        \n    return speed - opp_speed\n\n# Usage\nrace_data['close1_speed'] = race_data.apply(lambda row: dm.closest_horse_speed(race_data, 1, row['trakus_index'], row['program_number'], row['x'], row['y'], row['speed_ms']), axis=1)\nrace_data['close2_speed'] = race_data.apply(lambda row: dm.closest_horse_speed(race_data, 2, row['trakus_index'], row['program_number'], row['x'], row['y'], row['speed_ms']), axis=1)\nrace_data['close3_speed'] = race_data.apply(lambda row: dm.closest_horse_speed(race_data, 3, row['trakus_index'], row['program_number'], row['x'], row['y'], row['speed_ms']), axis=1)\nrace_data['close4_speed'] = race_data.apply(lambda row: dm.closest_horse_speed(race_data, 4, row['trakus_index'], row['program_number'], row['x'], row['y'], row['speed_ms']), axis=1)\nrace_data['close5_speed'] = race_data.apply(lambda row: dm.closest_horse_speed(race_data, 5, row['trakus_index'], row['program_number'], row['x'], row['y'], row['speed_ms']), axis=1)\n","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:50:03.323447Z","iopub.execute_input":"2022-10-13T12:50:03.324417Z","iopub.status.idle":"2022-10-13T12:51:11.924377Z","shell.execute_reply.started":"2022-10-13T12:50:03.324360Z","shell.execute_reply":"2022-10-13T12:51:11.923403Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Calculating distance remaining\n\nNow, I reduce the size of the data set to 242 frames prior to the first horse crossing the finish line. This is 240 frames (1 minute of racing) for the reinforcement learning model to run and 2 frames for its initialisation. The distance remaining function assumes that a horse will stay at its current distance from the rail throughout the rest of the race, which has an impact on how far it has left to travel on the bends, but not the straight. If the horse has crossed the finish line, its distance remaining is immediately set to 0. ","metadata":{}},{"cell_type":"code","source":"# Function\n# Reduce data to number of frames specified\ndef reduce_frames(race_data, finish_line, frames=242, drop_finished=False):\n    \n    # Get frames where finish line has been crossed\n    finished_frames = race_data.groupby(['trakus_index', 'program_number']).filter(lambda group: (group.x.min() > finish_line[0]) & (group.y.mean() < 100))\n    reduced_race_data = race_data.loc[race_data['trakus_index'] >= finished_frames.trakus_index.min()-frames].copy()\n    \n    # If selected, drop frames where horse has crossed finish line\n    if drop_finished:\n        reduced_race_data = reduced_race_data.drop(finished_frames.index)\n    \n    return reduced_race_data\n\n# Usage\nrace_data = gh.reduce_frames(race_data, finish_coordinates)\n","metadata":{"execution":{"iopub.status.busy":"2022-10-13T12:58:09.493023Z","iopub.execute_input":"2022-10-13T12:58:09.493338Z","iopub.status.idle":"2022-10-13T12:58:13.495986Z","shell.execute_reply.started":"2022-10-13T12:58:09.493312Z","shell.execute_reply":"2022-10-13T12:58:13.494632Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Function\n# Quite complicated here - several helper functions are defined.\n# The distance_remaining function at the bottom ties everything together and\n# returns the final value.\n\n# Get the index of the closest rail coordinate\ndef closest_rail(coord, x, y, rail_x, rail_y):\n    \n    # Is closeness based on x coordinate only, or x and y?\n    if coord == 'x':\n        idx = np.linalg.norm(np.array([x]).T - np.array([rail_x]).T, axis=1).argmin()\n    else:\n        idx = np.linalg.norm(np.array([x,y]).T - np.array([rail_x, rail_y]).T, axis=1).argmin()\n    \n    return idx\n\n# Check which part of the track the horse is on\ndef boolean_backhomecurve(point, back_x, home_x, centre_y):\n    \n    is_back = False\n    is_home = False\n    is_curve = False\n    \n    if (point[0] > back_x) & (point[1] > centre_y):\n        is_back = True\n    elif (point[0] > home_x) & (point[1] < centre_y):\n        is_home = True\n    else:\n        is_curve = True\n        \n    return is_back, is_home, is_curve\n\n# Move to the next closest rail point\ndef move_closest_rail(x, y, x1, y1, dist_to_rail,\n                      is_back, is_home, is_curve,\n                      rail_coords, closest_rail_idx, \n                      centre_x, centre_y):\n    \n    if is_back or is_home:\n        \n        # If we intersect the closest rail idx, take the distance to the next\n        # rail_idx and put the x coordinate of rail_idx and our y-coordinate\n        # as the next location. Add 1 to our closest_rail_idx counter\n        if boolean_intersection((x, y), (x1, y1), \n                                (rail_coords[closest_rail_idx][0]*2, rail_coords[closest_rail_idx][1]*2), \n                                (centre_x, centre_y)):\n            \n            try:\n                next_point_dist = abs(rail_coords[closest_rail_idx+1][0] - x)\n                next_point_pos = (rail_coords[closest_rail_idx+1][0], y)\n                closest_rail_idx +=1\n            except IndexError:\n                next_point_dist = abs(rail_coords[closest_rail_idx][0] - x)\n                next_point_pos = (rail_coords[closest_rail_idx][0], y)\n                \n        # Otherwise, find distance to the current closest_rail_idx, make that\n        # x coordinate with our y coordinate the new position, and keep the\n        # closest_rail_idx the same\n        else:\n            next_point_dist = abs(rail_coords[closest_rail_idx][0] - x)\n            next_point_pos = (rail_coords[closest_rail_idx][0], y)\n    \n    # If we are on the curve, we need to do a little bit more linear algebra \n    # to find the next position. We assume movement at the same distance from \n    # the central mean (i.e. a straight line movement), rather than moving \n    # inwards towards the rail or outwards away from it.\n    else:\n        \n        # If we intersect the closest_rail_idx, move to the next one.\n        if boolean_intersection((x, y), (x1, y1), \n                                (rail_coords[closest_rail_idx][0]*2, rail_coords[closest_rail_idx][1]*2), \n                                (centre_x, centre_y)):\n            \n            # Get the vector between the rail and centre, take its norm\n            vec = np.array([rail_coords[closest_rail_idx+1][0], rail_coords[closest_rail_idx+1][1]]) - np.array([centre_x, centre_y])\n            vec_length = np.linalg.norm(vec)\n            \n            # Scale up the vector so it's more representative of the horse's\n            # position with respect to the rail\n            vec_new = (vec/vec_length) * (vec_length+dist_to_rail)            \n            \n            # Calculate the horse's new position by adding the centre_x, \n            # centre_y coordinates. Take the length of the new position minus\n            # the old position to get the distance travelled and increment the\n            # closest_rail_idx counter\n            next_point_pos = vec_new + np.array([centre_x, centre_y])\n            next_point_dist = np.linalg.norm(next_point_pos - np.array([x,y]))\n            closest_rail_idx += 1\n        \n        # If we don't find distance to closest_rail_idx.\n        else:\n            \n            vec = np.array([rail_coords[closest_rail_idx][0], rail_coords[closest_rail_idx][1]]) - np.array([centre_x, centre_y])\n            vec_length = np.linalg.norm(vec)\n            \n            vec_new = (vec/vec_length) * (vec_length+dist_to_rail)            \n\n            next_point_pos = vec_new + np.array([centre_x, centre_y])\n            next_point_dist = np.linalg.norm(next_point_pos - np.array([x,y]))\n            \n    # Return\n    return next_point_pos, next_point_dist, closest_rail_idx\n\n# Calculate back straight distance\ndef back_straight_dist(current_position,\n                       rail_coords, back_straight_idx):\n    \n    # Calculate distance, position and rail_idx\n    back_straight_distance = abs(rail_coords[back_straight_idx][0] - current_position[0])\n    next_point_pos = (rail_coords[back_straight_idx][0], current_position[1])\n    closest_rail_idx = back_straight_idx\n    \n    return back_straight_distance, next_point_pos, closest_rail_idx\n\n# Calculate the curve distance\ndef curve_dist(current_position, dist_to_rail,\n               rail_coords, rail_coords_home_idx,\n               closest_rail_idx, centre_x, centre_y):\n    \n    # Get array of points and initialise centre coordinates.\n    # Centre x is the same for all curve positions so doesn't need to be\n    # changed in this function\n    curve_array = rail_coords[closest_rail_idx: rail_coords_home_idx+2]\n    centre_coords = np.array([centre_x, centre_y])\n    curve_distance = 0\n    \n    # We start from the [1] index because the [0] index is our current position\n    # (more or less)\n    for p in curve_array[1:]:\n        \n        # Get the vector between rail and centre\n        vec = np.array(p - centre_coords)\n        \n        # Find the vectors length then scale it to extend to the distance beyond\n        # the rail\n        vec_length = np.linalg.norm(vec)\n        vec_new = (vec/vec_length) * (vec_length+dist_to_rail)\n        \n        # Calculate next point position using this extended distance\n        next_point_pos = vec_new + centre_coords\n        \n        # Get the distance to the next point, add it to our curve distance\n        # ticker, then change next_point_pos to our current position for the\n        # next iteration of the loop.\n        next_point_dist = np.linalg.norm(next_point_pos - current_position)\n        curve_distance += next_point_dist\n        current_position = next_point_pos\n        \n    # Return rail_coords_home_idx as our current closest rail index\n    return curve_distance, current_position, rail_coords_home_idx\n\n# Calculate the home straight distance\ndef home_straight_dist(current_position, finish_coordinates):\n    \n    # Calculate distance, position and rail_idx\n    home_straight_distance = abs(finish_coordinates[0] - current_position[0])\n    \n    return home_straight_distance\n\n# Calculate distance remaining based on all functions above\ndef distance_remaining(row_x, row_y, row_x1, row_y1, row_raildist,\n                       rail_x, rail_y, cx_list,\n                       home_coordinates, finish_coordinates, y_ref):\n    \n    # Create a numpy array of rail co-ordinates and current position\n    rail_coords = np.array([rail_x, rail_y]).T\n    current_position = np.array([row_x, row_y])\n    \n    # Get reference central y point\n    centre_y = (y_ref-finish_coordinates[1])/2\n    \n    # Get important reference indices\n    # Filter rail coordinates to back straight only. Last index is closest to\n    # the (home_coordinates[0], centre_y) central point.\n    rails_back = rail_coords[rail_coords[:,1] > centre_y]\n    back_rail_idx = closest_rail('x', home_coordinates[0], centre_y, rails_back[:, 0], rails_back[:,1])\n    # Same for the home straight index, but data split to be the lower half of\n    # data\n    rails_home = rail_coords[rail_coords[:,1] < centre_y]\n    home_rail_idx = closest_rail('x', home_coordinates[0], centre_y, rails_home[:, 0], rails_back[:,1])\n    \n    # Back and home straight indices within the rail_coords array\n    rail_coords_home_idx = np.where(rail_coords == rails_home[home_rail_idx])[0][0]\n    rail_coords_back_idx = np.where(rail_coords == rails_back[back_rail_idx])[0][0]\n\n    # Home and back straight x values\n    back_straight_x = rails_back[back_rail_idx, 0]\n    home_straight_x = rails_home[home_rail_idx, 0]\n    \n    # Figure out where we are on the track\n    is_back_straight, is_home_straight, is_curve = boolean_backhomecurve(current_position, \n                                                                         back_straight_x, home_straight_x, \n                                                                         centre_y)\n    \n    # Now, we want to find the closest rail location to our position\n    closest_rail_idx = closest_rail('xy', row_x, row_y, rail_x, rail_y)\n    \n    # Calculate initial centre x as this point within the cx_list\n    centre_x = cx_list[closest_rail_idx]\n    \n    # Move directly to the next closest_rail_idx location. Calculate the\n    # distance we have moved to get there, which is our first calculation of\n    # the distance remaining\n    current_position, dist_remaining, closest_rail_idx = move_closest_rail(row_x, row_y, row_x1, row_y1,\n                                                                           row_raildist,\n                                                                           is_back_straight, is_home_straight, is_curve,\n                                                                           rail_coords, closest_rail_idx,\n                                                                           centre_x, centre_y)\n\n    # Check where we are on the track and update centre_x\n    is_back_straight, is_home_straight, is_curve = boolean_backhomecurve(current_position, \n                                                                         back_straight_x, home_straight_x, \n                                                                         centre_y)\n    centre_x = cx_list[closest_rail_idx]\n    \n    # For debugging\n    back_straight_distance = 0\n    curve_distance = 0\n    home_straight_distance = 0\n    \n    # Calculate distances based on where we are on the track\n    # Back straight distance calculated for debugging purposes, change indicator\n    # variables to ensure we can add on the curve distance\n    if is_back_straight:\n        back_straight_distance, current_position, closest_rail_idx = back_straight_dist(current_position, \n                                                                                        rail_coords, rail_coords_back_idx)\n        \n        dist_remaining += back_straight_distance\n        is_back_straight, is_home_straight, is_curve = boolean_backhomecurve(current_position,\n                                                                              back_straight_x, home_straight_x,\n                                                                              centre_y)\n        centre_x = cx_list[closest_rail_idx]\n        \n    # Curve distances\n    if is_curve:\n        curve_distance, current_position, closest_rail_idx = curve_dist(current_position, row_raildist,\n                                                                        rail_coords, rail_coords_home_idx, closest_rail_idx,\n                                                                        centre_x, centre_y)\n        dist_remaining += curve_distance\n        is_back_straight, is_home_straight, is_curve = boolean_backhomecurve(current_position,\n                                                                              back_straight_x, home_straight_x,\n                                                                              centre_y)\n        \n    # Home straight distance\n    if is_home_straight:\n        home_straight_distance = home_straight_dist(current_position, finish_coordinates)\n        dist_remaining += home_straight_distance\n    \n    return dist_remaining\n\n\n# Usage\nrace_data['dist_remaining'] = race_data.apply(lambda row: dm.distance_remaining(row['x'], row['y'], row['x1'], row['y1'], row['rail_dist'],\n                                                                                rail_x, rail_y, cx_list, \n                                                                                home_coordinates, finish_coordinates, 420), axis=1)\nrace_data['dist_remaining'] = np.where(race_data['is_finished'] == 1, 0, race_data['dist_remaining'])","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Calculate energy expenditure\n\nTo ensure that horses can't just sprint indefinitely, I calculate energy expenditure for every 0.25s frame. I assume constant acceleration and a mass of 500kg. Within the reinforcement learning model, if a horse uses up all of their energy, they are heavily penalised. \n\nUsing energy expenditure is by no means perfect as those horses who don't get a clear run are not be limited by their energy expenditure, but in the absence of other data it provides a starting point. Realistically, stride length and/or heart rate data could be useful for this aspect in the future.","metadata":{}},{"cell_type":"code","source":"# Function\n# Calculate energy expenditure\ndef energy_expenditure(v0, a0, t=0.25, mass=500):\n\n    return ((mass/2) * ((a0*t) + v0) * ((a0*t) + v0))/4\n\n\n# Usage\nrace_data['energy_j'] = race_data.apply(lambda row: dm.energy_expenditure(row['speed_ms1'], row['accel'], 0.25, 500), axis=1)","metadata":{"execution":{"iopub.status.busy":"2022-10-13T13:01:16.149443Z","iopub.execute_input":"2022-10-13T13:01:16.150737Z","iopub.status.idle":"2022-10-13T13:01:16.201687Z","shell.execute_reply.started":"2022-10-13T13:01:16.150692Z","shell.execute_reply":"2022-10-13T13:01:16.200957Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Turning angle\n\nFinally, I calculate the turning angle the horse used to reach its current location (x0,y0) relative to its previous movement vector (x1,y1 - x2,y2). This was used to help understand realistic movements that could be completed by the horses within the reinforcement learning algorithm.","metadata":{}},{"cell_type":"code","source":"# Function\n# Calculate turning angle\ndef turning_angle(x, y, x1, y1, x2, y2):\n    \n    # Calculate vectors of movement\n    v0x = x-x1\n    v0y = y-y1\n    v1x = x1-x2\n    v1y = y1-y2\n    \n    # Calculate angle relative to previous movement vector\n    angle = math.atan2(v1y, v1x) - math.atan2(v0y, v0x)\n    \n    # Keep angle less than 180 degrees so directions work\n    if angle > np.pi:\n        angle -= 2*np.pi\n    elif angle < -np.pi:\n        angle += 2*np.pi\n    \n    return angle\n\n# Usage\nrace_data['turn_angle'] = race_data.apply(lambda row: gh.turning_angle(row['x'], row['y'], row['x1'], row['y1'], row['x2'], row['y2']), axis=1)","metadata":{"execution":{"iopub.status.busy":"2022-10-13T13:04:18.714902Z","iopub.execute_input":"2022-10-13T13:04:18.715215Z","iopub.status.idle":"2022-10-13T13:04:18.813677Z","shell.execute_reply.started":"2022-10-13T13:04:18.715192Z","shell.execute_reply":"2022-10-13T13:04:18.812343Z"},"trusted":true},"execution_count":null,"outputs":[]}]}