{"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 numpy as np \nimport pandas as pd\nimport math\n\nimport plotly as py\nimport plotly.graph_objs as go\nfrom plotly.subplots import make_subplots\nfrom plotly.offline import init_notebook_mode\ninit_notebook_mode(connected=True)\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport matplotlib.lines as lines\n%matplotlib inline\n\nimport os\nimport imageio\nfrom IPython.display import Image, display_html\n\nfrom tqdm import tqdm\ntqdm.pandas()\n\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import roc_auc_score, roc_curve, auc\nimport xgboost as xgb\nimport lightgbm as lgb\nimport optuna\n\nimport datetime\nfrom shapely.geometry import Point, Polygon, LineString\n!pip install utm\nimport utm\n\npd.set_option('display.max_columns', None)\nimport itertools\nimport warnings\nwarnings.filterwarnings(\"ignore\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_kg_hide-input":true,"_kg_hide-output":true,"jupyter":{"source_hidden":true,"outputs_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:18:46.294958Z","iopub.execute_input":"2022-11-10T13:18:46.295641Z","iopub.status.idle":"2022-11-10T13:19:06.899561Z","shell.execute_reply.started":"2022-11-10T13:18:46.295495Z","shell.execute_reply":"2022-11-10T13:19:06.897990Z"},"collapsed":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"![](https://s.hdnux.com/photos/01/17/53/13/20886266/4/rawImage.jpg)","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Introduction</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe most pressing issue in horse racing, from my point of view, is the determination of the expected position of the participant. Of course, mathematics and statistics don't see the future, but they can predict the likely outcome, based on which there can be drawn important conclusions.\n</p>\n<p style=\"text-align:justify;\">\nThe main points of this work are:\n<ol>\n    <li>Evaluating the compliance between the expected finishing position by odds and actual finishing position.</li>\n    <li>Evaluating the difference of results during the race between the expected winners and the actual ones, as well as between the underdogs and the actual losers.</li>\n    <li>Creating a model that predicts the participant's finish position and the probability of victory during the race.</li>\n    <li>Determining the importance of the factors on which the expected position depends and their change during the race.</li>\n</ol>\n</p>\n<p style=\"text-align:justify;\">\nThe main idea of this work is that in determining the position of participants in future races, not only the final results of previous races are important, but also intermediate results: the time of holding certain positions, the deviation of speed from other participants, the distance to the leader, horse health indicators and many others. There were used only data provided by the organizers.\n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Data preprocessing</b></div>","metadata":{}},{"cell_type":"markdown","source":"Preprocessing of data on tracking the movement of horses includes:\n<ul>\n    <li>Recovering and deleting missing data.</li>\n    <li>Determinating of passed distance, speed, acceleration.</li>\n    <li>Determinating speed deviation from the average speed.</li>\n    <li>Removing unnecessary information about the race after the loser overcomes 100% of the distance.</li>\n    <li>Determining where the horse is running (straight or turn).</li>\n    <li>Determining the positions of horses during the race.</li>\n    <li>Correction of anomalies (unreal high speed or acceleration).</li>\n    <li>Determining the distance to the leader.</li>\n</ul>\n<p style=\"text-align:justify;\">\nYou can get acquainted with the code for preprocessing in the code block below. The preprocessed race tracking data you can find <a href=\"https://www.kaggle.com/datasets/dmitryuarov/derby-full-data\">here</a>.\n</p>\n<div class=\"alert alert-block alert-info\">\n<b>Important notes</b> \n<p style=\"text-align:justify;\">\n<ul>\n    <li>To determine the distance was used Universal Transverse Mercator (UTM) coordinate system. <a href=\"https://www.usgs.gov/faqs/what-does-term-utm-mean-utm-better-or-more-accurate-latitudelongitude\">[1]</a></li>\n    <li>Completing 100% of the distance includes a run-up and means crossing the finish line.</li>\n    <li>The speed is calculated in <i>mph</i> and the distance in <i>feet</i>.</li>\n</ul>        \n</div>","metadata":{}},{"cell_type":"code","source":"# If you want to run this code, then keep in mind that calculating \n# the position during the race takes the longest amount of time. \n# In total, it will take ~9 hours to process the data.\n\n#####################\n# FIND MISSIND DATA #\n#####################\n\nraces_info = pd.read_csv('../input/big-data-derby-2022/nyra_race_table.csv')\ndf = pd.read_csv('../input/big-data-derby-2022/nyra_tracking_table.csv')\n\n# Create ID of each race\ndef race_id(df):\n    df['race_id'] = df['track_id'] + '_' + \\\n                    df['race_date'].astype('str') + '_' + \\\n                    df['race_number'].astype('str')\n    df.sort_values(['race_date', 'race_number'], inplace=True)\n    df['race_id'] = df['race_id'].map({rid: i for i, rid in enumerate(\n        df['race_id'].unique().tolist())})\n    return df\n\n# Time transformations\ndef preprocessing_1(df):\n    df['race_date'] = pd.to_datetime(df['race_date'])\n    df['time_sec'] = df['trakus_index'] * 0.25\n    df = race_id(df)\n    df['race_number'] = df['race_number'].astype(np.int8)\n    df['time_sec'] = df['time_sec'].astype(np.float32)\n    df.drop('trakus_index', axis=1, inplace=True) \n    return df\n\n# Create an integer ID of horse\nle = LabelEncoder()\ndf['program_number'] = le.fit_transform(df['program_number'].astype('str'))\ndf['race_number'] = df['race_number'].astype(np.int8)\n\ndf = preprocessing_1(df)\nraces_info = race_id(races_info)\nraces_info['dist_feet'] = races_info['distance_id'] / 100 * 660\nraces_info['dist_furlong'] = races_info['dist_feet'] / 660\nraces_info.drop('distance_id', axis=1, inplace=True)\n\n# Determining the rationality of data recovery\ndef dpdr():\n    if mv <= 0.1:\n        if race not in htr:\n            htr[race] = set()\n            htr[race].add(horse)\n        else:\n            htr[race].add(horse)\n    else:\n        if race not in htd:\n            htd[race] = set()\n            htd[race].add(horse)\n        else:\n            htd[race].add(horse)\n\n# Find missing data\nhorses_o, miss_vals_o, horses_i, miss_vals_i = [], [], [], []\nmiss_time_i, miss_time_o, htr, htd = [], [], {}, {}\nfor race in races_info['race_id']:\n    miss_o, miss_i, count_o, count_i = [], [], 0, 0\n    info = df[df['race_id']==race]\n    time = info['time_sec'].unique()\n    \n    for horse in info['program_number'].unique():\n        h_info = info[info['program_number']==horse]\n        if len(h_info) != len(time):\n            count_o += 1\n            miss_time_o.append((set(time) - set(h_info['time_sec'])))\n            mv = round(1-(len(h_info)/len(time)), 3)\n            miss_o.append(mv)\n            dpdr()     \n        if h_info['latitude'].nunique() != len(h_info):\n            count_i += 1\n            miss_time_i.append(h_info.iloc[h_info.duplicated(\n                subset=['latitude']).values]['time_sec'].tolist())\n            mv = round(1-(h_info['latitude'].nunique()/len(h_info)), 3)\n            miss_i.append(mv)\n            dpdr()\n            \n    horses_o.append(count_o), miss_vals_o.append(miss_o)\n    horses_i.append(count_i), miss_vals_i.append(miss_i)\n    \nfor race in set(htd.keys()) & set(htr.keys()):\n    hrs = htr[race].copy()\n    for horse in hrs:\n        if horse in htd[race]:\n            htr[race].remove(horse)\n            if htr[race] == set():\n                del htr[race]\n    \nraces_info['missing_data_o'], races_info['missing_vals_o'] = horses_o, miss_vals_o\nraces_info['missing_data_i'], races_info['missing_vals_i'] = horses_i, miss_vals_i\nraces_info['race_o'] = np.where(races_info['missing_data_o'] != 0, True, False)\nraces_info['race_i'] = np.where(races_info['missing_data_i'] != 0, True, False)\n\n# Table with missing information\ncols = ['Missing type', 'Number of races',\n        'Mean percentage of missing values',\n        'Median time when data missed']\nmiss_df = pd.DataFrame(columns=cols)\nmiss_df[cols[0]] = ['Explicitly', 'Implicitly']\nmiss_df[cols[1]] = [str(races_info[\"race_\"+mt].sum()) + ' (' \\\n                    + str(round(races_info[\"race_\"+mt].sum() / 2000 * 100, 1)) + \\\n                    '%)' for mt in ['o', 'i']] \nmiss_df[cols[2]] = [str(np.mean(list(itertools.chain(*[i for i in \\\n                    races_info[races_info[\"missing_data_\"+mt]!=0] \\\n                    [\"missing_vals_\"+mt]]))).round(3)*100)+'%' \\\n                    for mt in ['o', 'i']]\nmiss_df[cols[3]] = [str(np.median(list(itertools.chain(*mt)))) + \\\n                    ' sec.' for mt in [miss_time_o, miss_time_i]]\nmiss_df = miss_df.style.hide_index().set_caption(\"Information about missing data\") \\\n    .set_table_styles([\n    {'selector': 'th.col_heading', 'props': 'font-size: 10pt; text-align: center; font-weight: bold; border-collapse: collapse'},\n    {'selector': 'td', 'props': 'text-align: center; font-size: 10pt; border: 1px solid black !important'},\n    {'selector': 'caption', 'props': 'font-size: 14pt; font-weight: bold; color: #303030'},\n    {'selector': 'th:not(.index_name)', 'props': 'background-color: #34675c; color: white;'}\n], overwrite=False).set_properties(**{'background-color': '#fafafa'})\n\nraces_info.drop(races_info.columns[-6:], axis=1, inplace=True)\n\n#################################################################\n#################################################################\n#################################################################\n\n#################\n# PREPROCESSING #\n#################\n\nnot_turns = {'AQU': Polygon([(40.6738, -73.82686), (40.66982, -73.82866),\n                             (40.67087, -73.83258), (40.67493, -73.83072)]),\n             'SAR': Polygon([(43.0712, -73.76502), (43.06919, -73.77014),\n                             (43.07192, -73.77222), (43.07403, -73.76698)]),\n             'BEL': Polygon([(40.71263, -73.72016), (40.71438, -73.7267),\n                             (40.71762, -73.72503), (40.71587, -73.7184)])}\n\nspecial_starts = {'AQU': Polygon([(40.67011, -73.82953), (40.66988, -73.82846),\n                                  (40.66653, -73.82996), (40.66674, -73.83099)]),\n                  'SAR': Polygon([(43.06993, -73.77064), (43.06914, -73.77011),\n                                  (43.06842, -73.77184), (43.06884, -73.77214)]),\n                  'BEL': Polygon([(40.71515, -73.71866), (40.71628, -73.7183),\n                                  (40.71537, -73.71508), (40.71462, -73.71545)])}\n\ntrack_name = dict(zip(races_info['race_id'], races_info['track_id']))\n\ndef is_turn(row, not_turn, ss):\n    if ss == True:\n        if row['time_sec'] < 20:\n            return True\n        else:\n            return Point(row['latitude'], row['longitude']).within(not_turn)\n    else:\n        return Point(row['latitude'], row['longitude']).within(not_turn)\n\ndef turn(df, track):\n    fp = tuple(df[['latitude', 'longitude']].values[0])\n    ss = False\n    if Point(fp).within(special_starts[track]) == True:\n        ss = True\n    df['not_turn'] = df[['latitude', 'longitude', 'time_sec']]\\\n        .apply(lambda row: is_turn(row, not_turns[track], ss), axis=1)\n    return df\n\ndef get_distance(row):\n    return np.linalg.norm(np.array(row['utm_lat_lon']) - \\\n                          np.array(row['utm_lat_lon_lag'])) * 3.28084\n\ndef get_distance2(row):\n    try:\n        return np.linalg.norm(np.array(row['utm_lat_lon']) - \\\n                              np.array(row['lead_position'])) * 3.28084\n    except:\n        return 0\n\ndef dist_and_speed(df):\n    df = df.groupby('program_number') \\\n        .apply(lambda x: x.sort_values('time_sec')).reset_index(drop=True)\n    for col in ['latitude', 'longitude']: \n        df[f\"{col}_lag\"] = df.groupby('program_number')[col].shift(1)\n    df['utm_lat_lon'] = np.array(utm.from_latlon(df['latitude'].values, \n                                                 df['longitude'].values)[:2]) \\\n                                                 .T.tolist()\n    df['utm_lat_lon_lag'] = df.groupby('program_number')['utm_lat_lon'].shift(1)\n    df['dist_0.25sec'] = df[['utm_lat_lon', 'utm_lat_lon_lag']] \\\n        .apply(lambda row: get_distance(row), axis=1)\n    df.fillna(0, inplace=True)\n        \n    df['passed_dist'] = df.groupby('program_number')['dist_0.25sec'].cumsum()\n    df['speed_mph'] = df['dist_0.25sec'] / 5280 * 4 * 60 * 60\n    df['acceleration'] = df.groupby('program_number')['speed_mph'].diff()\n    df.fillna(0, inplace=True)\n    return df\n\ndef find_miss_intervals(m_time):\n    fp, m_intrvls = m_time[0], [] \n    if len(m_time) == 1:\n        m_intrvls.append([m_time[0]])\n    else: \n        for i in range(1, len(m_time)):\n            if m_time[i] - m_time[i-1] == 0.25 and i != len(m_time)-1:\n                continue\n            elif m_time[i] - m_time[i-1] != 0.25:\n                m_intrvls.append(m_time[m_time.index(fp):i])\n                fp = m_time[i]\n            else:\n                m_intrvls.append(m_time[m_time.index(fp):i+1])\n    return m_intrvls\n\ndef no_miss(df, race):\n    if race in htd.keys():\n        df = df.loc[~df['program_number'].isin(list(htd[race]))]\n    if race in htr.keys():\n        time = set(df['time_sec'].unique())\n        df = df.loc[df.iloc[~df.duplicated(subset=['latitude']).values].index]\n        nh = df[~df['program_number'].isin(htr[df['race_id'].values[0]])]\n        for horse in htr[df['race_id'].values[0]]:\n            h_info = df[df['program_number']==horse] \n            df = df[df['program_number']!=horse] \n            m_time = sorted(list(time - set(h_info['time_sec'])))\n            lat_lon = {mt: [] for mt in m_time} \n            m_intrvls = find_miss_intervals(m_time)\n            \n            for m_intrvl in m_intrvls:\n                fp, lp = m_intrvl[0]-0.25, m_intrvl[-1]+0.25\n                utm_fp = h_info[h_info['time_sec']==fp]['utm_lat_lon'].tolist()\n                if len(h_info[h_info['time_sec']==lp]['utm_lat_lon']) == 0:\n                    utm_lp = nh[nh['time_sec']==lp-0.25]['utm_lat_lon'].tolist()\n                    dist_fp_lp = np.linalg.norm(np.array(utm_lp[0]) - \\\n                                                np.array(utm_fp[0]))\n                    utm_prefp = h_info[h_info['time_sec']==fp-0.25]['utm_lat_lon'].tolist()\n                    dist_to_pass = np.linalg.norm(np.array(utm_prefp[0]) - \\\n                                                  np.array(utm_fp[0])) \n                    if dist_to_pass == 0:\n                        dist_to_pass = 0.1\n                else:\n                    utm_lp = h_info[h_info['time_sec']==lp]['utm_lat_lon'].tolist()\n                    dist_to_pass = np.linalg.norm(np.array(utm_lp[0]) - \\\n                                                  np.array(utm_fp[0])) / \\\n                                                  (len(m_intrvl)+1)\n                utm_crds = utm_fp\n                utm_crds += utm_lp  \n                miss_intrvl = LineString([(utm_crds[0][0], utm_crds[0][1]),\n                                          (utm_crds[1][0], utm_crds[1][1])])\n                dist = 0\n                for mt in m_intrvl:\n                    dist += dist_to_pass\n                    new_point = miss_intrvl.interpolate(dist)\n                    for i in [0, 1]:\n                        lat_lon[mt].append(utm.to_latlon(new_point.x, \n                                                         new_point.y, 18, 'T')[i])\n                lat_lon2 = {key: val for key, val in lat_lon.items() if val != []}\n                rec_df = pd.DataFrame({'latitude': [x[0] for x in lat_lon2.values()],\n                                       'longitude': [x[1] for x in lat_lon2.values()],\n                                       'time_sec': list(lat_lon2.keys())})\n                rec_df['program_number'] = horse\n                h_info = pd.concat([h_info, rec_df], axis=0).ffill()\n                lat_lon = {key: val for key, val in lat_lon.items() if val == []}\n            h_info = dist_and_speed(h_info)\n            df = pd.concat([df, h_info], axis=0)\n    return df\n\ndef del_excess_data(df): \n    dist = sum(races_info[races_info['race_id']==df['race_id'].values[0]] \\\n               [['dist_feet', 'run_up_distance']].values[0])\n    df['completed_dist'] = round(df['passed_dist'] / dist * 100, 1)\n    fin_momemts = []\n    for horse in df['program_number'].unique():\n        fm = df[(df['program_number']==horse) &\n                (df['completed_dist']>=100)]['time_sec'].min()\n        if np.isnan(fm):\n            return df\n        else:\n            fin_momemts.append(fm)\n    df = df[df['time_sec'] <= max(fin_momemts)]\n    return df\n\ndef get_position(df):\n    places = pd.DataFrame(columns=['time_sec', \n                                   'program_number', \n                                   'position'])\n    t, pn, pl = [], [], []\n    for sec in sorted(df['time_sec'].unique().tolist()[1:]):\n        s_info = df[df['time_sec']==sec]\n        for horse in s_info['program_number'].tolist():\n            h_info = s_info[s_info['program_number']==horse]\n            vec = LineString([(h_info['longitude_lag'].values[0], \n                               h_info['latitude_lag'].values[0]),\n                              (h_info['longitude'].values[0], \n                               h_info['latitude'].values[0])])\n            \n            x1 = vec.parallel_offset(0.01, 'left').boundary[1]\n            x2 = vec.parallel_offset(0.001, 'right').boundary[0]\n            top1, top2 = LineString([x1, x2]), LineString([x2, x1])\n            x3 = top1.parallel_offset(0.01, 'right').boundary[0]\n            x4 = top2.parallel_offset(0.01, 'left').boundary[1]\n            behind_area = Polygon([(x1.x, x1.y), (x2.x, x2.y),\n                                   (x3.x, x3.y), (x4.x, x4.y)])\n            \n            rivals, h_behind = s_info['program_number'].tolist(), 0\n            rivals.remove(horse)\n            for rival in rivals:\n                rival = s_info[s_info['program_number']==rival]\n                r_lon = rival['longitude'].values[0]\n                r_lat = rival['latitude'].values[0]\n                if Point(r_lon, r_lat).within(behind_area) == True:\n                    h_behind += 1\n                        \n            t.append(sec)\n            pn.append(horse)\n            pl.append(h_info['num_horses'].values[0] - h_behind)\n\n    places['time_sec'], places['program_number'], places['position'] = t, pn, pl\n    df = df.merge(places, on=['time_sec', 'program_number'], how='left')\n    return df\n\ndef no_anomalies(df):\n    speed = df['speed_mph']\n    speed = np.where(speed > 50, np.median(speed), speed)\n    df['speed_mph'] = speed\n    acc = df['acceleration']\n    acc = np.where(acc > 15, 15, acc)\n    acc = np.where(acc < -15, -15, acc)\n    df['acceleration'] = acc\n    return df\n\ndef speed_diff_from_mean(df):\n    mean_speed = df.groupby(['time_sec'])['speed_mph'].mean() \\\n                    .reset_index().rename({'speed_mph': 'gen_avg_speed'}, axis=1)\n    df = df.merge(mean_speed, on='time_sec', how='left')\n    df['speed_diff_avg_num'] = df['speed_mph'] - df['gen_avg_speed']\n    df['speed_diff_avg_%'] = (df['speed_mph']/df['gen_avg_speed']*100)-100\n    return df\n\ndef dist_to_lead(df):\n    lead_position = df[df['position']==1].sort_values('time_sec') \\\n        .drop_duplicates(subset=['time_sec'])[['time_sec', 'utm_lat_lon']] \\\n        .rename({'utm_lat_lon': 'lead_position'}, axis=1)\n    df = df.merge(lead_position, on='time_sec', how='left')\n    df['dist_to_lead'] = df[['utm_lat_lon', 'lead_position']] \\\n        .apply(lambda row: get_distance2(row), axis=1)\n    df = df.drop('lead_position', axis=1).fillna(0)\n    return df\n    \ndef preprocessing_2(df):\n    new_df = pd.DataFrame()\n    for race in tqdm(df['race_id'].unique()):\n        info = df[df['race_id']==race]\n        info = dist_and_speed(info)\n        info = no_miss(info, race)\n        info = turn(info, track=track_name[race])\n        info = del_excess_data(info)\n        \n        info['num_horses'] = info['program_number'].nunique()\n        \n        info = get_position(info)\n        info = no_anomalies(info)\n        info = speed_diff_from_mean(info)\n        info = dist_to_lead(info)\n        \n        info.drop(['utm_lat_lon', 'utm_lat_lon_lag',\n                   'dist_0.25sec', 'race_number'], axis=1, inplace=True) \n        new_df = pd.concat([new_df, info], ignore_index=True)\n        \n    new_df.drop(['latitude_lag', 'longitude_lag',\n                 'track_id', 'race_date'], axis=1, inplace=True)  \n    new_df.fillna(0, inplace=True)\n    new_df['race_id'] = new_df['race_id'].astype(np.int16)\n    new_df['position'] = new_df['position'].astype(np.int16)\n    return new_df\n\n#df = preprocessing_2(df)","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:19:06.904326Z","iopub.execute_input":"2022-11-10T13:19:06.905329Z","iopub.status.idle":"2022-11-10T13:20:26.908756Z","shell.execute_reply.started":"2022-11-10T13:19:06.905272Z","shell.execute_reply":"2022-11-10T13:20:26.907267Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Problem of missing data</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nDuring the initial data analysis there was found a considerable amount of missing data that needs to be recovered or deleted. There was found 2 types of missing data:\n<ul>\n  <li><b>Explicitly</b> - the absence of data at a certain time moment.</li>\n  <li><b>Implicitly</b> - the absolute immobility of the horse, that is, longitude and latitude data remain unchanged throughout the certain time interval.</li>\n</ul>\nLet's take a look:\n</p>","metadata":{}},{"cell_type":"code","source":"miss_df","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:20:26.910485Z","iopub.execute_input":"2022-11-10T13:20:26.910907Z","iopub.status.idle":"2022-11-10T13:20:26.931200Z","shell.execute_reply.started":"2022-11-10T13:20:26.910852Z","shell.execute_reply":"2022-11-10T13:20:26.929071Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nIt is necessary to recover data and the main reason for this is correct determination of the position of each horse during the race, as well as to get rid of anomalies in the movement of horses (rapid acceleration and unreal high speed). Having considered the distribution of time moments of implicitly missing values, there was found an interesting moment that more than <b>95%</b> of observations were missed in the first <b>5</b> seconds when the run-up occurs. This deprives us of important information about the start of the race, however, a small number of missing values allows us to recover them. For the purpose of rational data recovery, it was decided to recover the data of those horses that missed no more than 10% of observations. The rest will be deleted. Thus, data will be recovered in <b>301</b> races. To recover the movement of horses, it was decided to use interpolation. \n\nLet's take a look how it works:\n</p>","metadata":{}},{"cell_type":"code","source":"exp = df[df['race_id']==18]\nexp = dist_and_speed(exp)\nexp['utm_lat_lon'] = np.array(utm.from_latlon(exp['latitude'].values, \n                                              exp['longitude'].values)[:2]) \\\n                                              .T.tolist()\ncolors = ['#'+x for x in ['e8e8e8', 'e8e8e8', '1e56c7', 'e8e8e8',\n                          'e8e8e8', 'e8e8e8']]\nlim = exp[exp['time_sec']<=2.5]\nfilenames = []\n\nmt = [0.5, 0.75, 1, 1.25, 1.5, 1.75]\nrec_time = {9: mt[:1], 10: mt[:2], 11: mt[:3], \n            12: mt[:4], 13: mt[:5], 14: mt[:6]}\nmt_for_plot = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75]\nfor i, sec in enumerate([0.25]+mt+[2]*8+[0.25]+mt+[2]):\n    data = exp[exp['time_sec']==sec]\n    fig = plt.figure(figsize = (10, 10))\n    if i > 7 and i < 15:\n        ndata = exp[((exp['time_sec']==0.25)|(exp['time_sec']==2))\n                    &(exp['program_number']==12)]\n        sns.lineplot(data=ndata, x=\"longitude\", y=\"latitude\", \n                     color='#3d3d3d', linestyle='--')\n        plt.annotate('Missing interval', xy=(-73.830355, 40.666860), \n                     xytext=(-73.830400, 40.666900),\n                     arrowprops=dict(facecolor='steelblue', arrowstyle=\"->\", \n                                     connectionstyle=\"arc3,rad = 0.22\", \n                                     color='#3d3d3d'), \n                     fontsize = 13, ha='center')\n        ndata = exp[(exp['time_sec']==0.25)&(exp['program_number']==12)]\n        sns.scatterplot(data=ndata, x=\"longitude\", y=\"latitude\", color='#1e56c7', s=150, \n                        linewidth=0.5, edgecolor=\"black\", alpha=1, zorder=10)\n        \n    if i == 8:\n        exp = no_miss(exp, 18)\n    if i > 8 and i < 15:\n        ndata = exp[(exp['time_sec'].isin(rec_time[i]))&(exp['program_number']==12)]\n        sns.scatterplot(data = ndata, x=\"longitude\", y=\"latitude\", color='#1e56c7', s=150, \n                        linewidth=0.5, edgecolor=\"black\", alpha=1, zorder=10)\n        plt.figtext(0.58, 0.285, 'Passed distance at \\nmissing moment time', size=11, ha='center')\n        plt.figtext(0.8, 0.3, 'Total distance of \\nmissing interval \\n/ ', size=11, ha='center')\n        plt.figtext(0.8, 0.26, 'Number of \\nmissing moments + 1', size=11, ha='center')\n        plt.figtext(0.7, 0.295, '=', size=15, ha='center')\n        \n    a = sns.scatterplot(data=data, x=\"longitude\", y=\"latitude\", \n                        hue=\"program_number\", palette=colors, s=150, \n                        linewidth=0.5, edgecolor=\"black\", alpha=1, zorder=10)\n    \n    plt.xlabel('')\n    plt.ylabel('')\n    a.set_xlim([min(lim['longitude'])-0.00005, max(lim['longitude'])+0.00005])\n    a.set_ylim([min(lim['latitude'])-0.00003, max(lim['latitude'])+0.00005])\n    plt.xticks([])\n    plt.yticks([])\n    plt.legend(title='Horse №', frameon=False, \n               title_fontsize=12, bbox_to_anchor=(1.018, 1.02))\n    for j in ['right', 'top', 'bottom', 'left']:\n        a.spines[j].set_visible(False)\n        \n    plt.figtext(0.14, 0.86, 'Date: 2019-01-05', size=15, ha='left')\n    plt.figtext(0.14, 0.835, 'Track: AQU', size=15, ha='left')\n    plt.figtext(0.14, 0.81, 'Race №: 2', size=15, ha='left')\n    plt.figtext(0.14, 0.785, f'Time: {sec}', size=15, ha='left')\n    \n    if i < 8:\n        plt.figtext(0.55, 0.855, 'REAL DATA', size=20, ha='center')\n    if i > 14:\n        plt.figtext(0.55, 0.855, 'RECOVERED DATA', size=20, ha='center')\n    \n    filename = f'{i}_{sec}.png'\n    filenames.append(filename)\n    plt.savefig(filename)\n    plt.close()\n    \nimages = list(map(lambda filename: imageio.imread(filename), filenames))\nimageio.mimsave('gif1.gif', images, duration=1)\n\nfor filename in set(filenames):\n    os.remove(filename)\n    \nImage(open('gif1.gif', 'rb').read())","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:20:26.935061Z","iopub.execute_input":"2022-11-10T13:20:26.935628Z","iopub.status.idle":"2022-11-10T13:20:37.618156Z","shell.execute_reply.started":"2022-11-10T13:20:26.935578Z","shell.execute_reply":"2022-11-10T13:20:37.617153Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Determining the position during the race</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nDetermining the position of the horse during the race is the main task for further analysis. I created not the fastest way to determine the position, but accurate and understandable for any person. A perpendicular line is built to the vector of the direction of movement in a quarter of a second, then two more perpendicular lines are built to the first perpendicular line in the opposite direction of the direction of movement, as a result of which a polygon is formed. The position is determined by subtracting from the total number of participants the number of participants inside the polygon. \n</p>","metadata":{}},{"cell_type":"code","source":"data = exp[exp['time_sec'].isin([10, 10.25])]\nfilenames = []\n\nfor i, sec in enumerate([10]+[10.25]*8):\n    fig = plt.figure(figsize = (10, 10))\n    ndata = data[data['time_sec']==sec]\n    \n    if i < 4:\n        colors = ['#'+x for x in ['e8e8e8', 'e8e8e8', \n                                  '1e56c7', 'e8e8e8',\n                                  'e8e8e8', 'e8e8e8']]\n    else:\n        colors = ['#'+x for x in ['c71c1c', '34675c', \n                                  '1e56c7', '34675c',\n                                  'c71c1c', 'c71c1c']]\n    a = sns.scatterplot(data=ndata, x=\"longitude\", y=\"latitude\", \n                        hue=\"program_number\", palette=colors, s=150, \n                        linewidth=0.5, edgecolor=\"black\", alpha=1, zorder=10)\n    if i > 1:\n        ndata = data[data['program_number']==12]\n        sns.lineplot(data=ndata, x=\"longitude\", y=\"latitude\", \n                     color='#3d3d3d', linestyle='--')\n\n        plt.annotate('Direction vector', xy=(-73.829795, 40.668087), \n                     xytext=(-73.829828, 40.668042),\n                     arrowprops=dict(facecolor='steelblue', arrowstyle=\"->\", \n                                     color='#3d3d3d'),\n                     fontsize=12)\n    if i > 2:\n        sns.lineplot(x=np.array([-73.8298013, -73.829806]), \n                     y=np.array([40.668085, 40.66808765]), \n                     color='black', linestyle='-')\n        sns.lineplot(x=np.array([-73.829806, -73.82980355]), \n                     y=np.array([40.66808765, 40.6680933]), \n                     color='black', linestyle='-')\n        \n        x = np.array([-73.829950, -73.8297037])\n        y = np.array([40.66818, 40.668035])\n        sns.lineplot(x=x, y=y, color='black', linestyle='-')\n    if i > 3:\n        plt.fill_between(x, y, color='#f7f7f7')\n        plt.figtext(0.5, 0.2,\n                    'Position = number of participants - number of participants behind',\n                    size=13, ha='center')\n        \n    plt.xlabel('')\n    plt.ylabel('')\n    a.set_xlim([min(data['longitude'])-0.00003, max(data['longitude'])+0.00003])\n    a.set_ylim([min(data['latitude'])-0.00003, max(data['latitude'])+0.00003])\n    plt.xticks([])\n    plt.yticks([])\n    a.get_legend().set_visible(False)\n    \n    for j in ['right', 'top', 'bottom', 'left']:\n        a.spines[j].set_visible(False)\n    \n    filename = f'{i}_{sec}.png'\n    filenames.append(filename)\n    plt.savefig(filename)\n    plt.close()\n    \nimages = list(map(lambda filename: imageio.imread(filename), filenames))\nimageio.mimsave('gif2.gif', images, duration=1)\n\nfor filename in set(filenames):\n    os.remove(filename)\n    \nImage(open('gif2.gif', 'rb').read())","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:20:37.619722Z","iopub.execute_input":"2022-11-10T13:20:37.620300Z","iopub.status.idle":"2022-11-10T13:20:41.190725Z","shell.execute_reply.started":"2022-11-10T13:20:37.620262Z","shell.execute_reply":"2022-11-10T13:20:41.189480Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Viewers expectations</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nTo begin with, it's interesting to evaluate how often the expected by odds winners and underdogs compliance with the actual results. The expected position by viewers is quite easy to calculate based on the distribution of their bets.\n</p>","metadata":{}},{"cell_type":"code","source":"full_df = pd.read_csv('../input/derby-full-data/derby_tracking_full.csv') # Main file (tracking info)\n\ncols = ['track_id', 'race_date', 'race_number', \n        'program_number', 'weight_carried', 'jockey', \n        'odds', 'final_place']\njok_info = pd.read_csv('../input/big-data-derby-2022/nyra_start_table.csv', names=cols)\n\njok_info = race_id(jok_info)\njok_info['program_number'] = le.transform(jok_info['program_number'].astype('str'))\njok_info['race_date'] = pd.to_datetime(jok_info['race_date'])\njok_info.drop(['track_id', 'race_number'], axis=1, inplace=True)\ncols = jok_info.columns.tolist()\njok_info = jok_info[cols[-1:] + cols[:-1]]\n\n# Expected finish place\njok_info['expected_finish_place'] = jok_info.groupby('race_id')['odds'] \\\n                                                .rank(method='min').astype(np.int16)\n# Reality equals expectations\ndef ree(row):\n    return row['final_place'] == row['expected_finish_place']\njok_info['REE'] = jok_info[['final_place', 'expected_finish_place']] \\\n    .apply(lambda row: ree(row), axis=1)\n\n# Count of horses and dist\njok_info = jok_info.merge(full_df[['race_id', 'num_horses']].drop_duplicates(),\n                          on='race_id', how='left') \\\n                   .merge(races_info[['race_id', 'dist_furlong']], \n                          on='race_id', how='left')\n\n# Favorite or underdog\ndef fav_und(row, col):\n    if row[col]==1:\n        return '1st'\n    elif row[col]==2:\n        return '2nd'\n    elif row[col]==row['num_horses']-1:\n        return 'pre-last'\n    elif row[col]==row['num_horses']:\n        return 'last'\n    else:\n        return 'middle'\n    \njok_info['fav_und'] = jok_info[['expected_finish_place', 'num_horses']]\\\n        .apply(lambda row: fav_und(row, 'expected_finish_place'), axis=1)\n    \njok_info['act_pos'] = jok_info[['final_place', 'num_horses']]\\\n        .apply(lambda row: fav_und(row, 'final_place'), axis=1)\n\n# ##############################################\n\nstat = jok_info.groupby('fav_und', as_index=False) \\\n               .agg({'REE': 'sum'}).drop([3]).reset_index(drop=True)\nb, c = stat.iloc[2].copy(), stat.iloc[3].copy()\nstat.iloc[2], stat.iloc[3] = c, b\n\nstat['REE_rating'] = round(stat['REE'] / 2000 * 100, 1)\n\nfig = go.Figure()\nfig.add_trace(go.Bar(x=stat['REE_rating'], y=stat['fav_und'],\n                     marker=dict(color='#34675c',\n                     line=dict(width=0.5, color='black')),\n                     hovertemplate='<b>%{x}</b>'+ \"<extra></extra>\", \n                     showlegend=False,\n                     orientation='h'))\n\nfig.update_layout(width=800, height=350, plot_bgcolor='white',\n                  title=dict(text=\"<b>Compliance of the expected and actual finish position in all races</b>\",\n                         font=dict(size=22), x=0.097),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=14, font_family=\"Calibri\", bgcolor=\"white\"),\n                  barmode='group', bargap=0.2, bargroupgap=0.3)\n\nfig.update_xaxes(showline=False, tickfont_size=14, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=6, ticksuffix='%',\n                 tickvals=[0,10,20,30,40],)\n\nfig.update_yaxes(showline=True, linecolor='#f5f2f2', linewidth=2, \n                 tickfont_size=14, ticks=\"outside\", tickcolor='white', \n                 ticklen=10, autorange=\"reversed\")\n\nfig.show()\n\n# ##############################################\n\ncompl_furlong = pd.DataFrame(columns=['Expected position',\n                                      '4.5-5.5', '6-6.5',\n                                      '7-8', '8.5<'])\ncompl_horses = pd.DataFrame(columns=['Expected position',\n                                     '4-5', '6-7',\n                                     '8-9', '10<'])\ncompl_furlong['Expected position'] = ['1st', '2nd', 'pre-last', 'last']\ncompl_horses['Expected position'] = ['1st', '2nd', 'pre-last', 'last']\nfurlongs = [[4.5, 5.5], [6, 6.5], [7, 8], [8.5, 20]]\nhorses = [[4, 5], [6, 7], [8, 9], [10, 14]]\n\n\nfor compl, filtr, col in zip([compl_furlong, compl_horses],\n                             [furlongs, horses],\n                             ['dist_furlong', 'num_horses']):\n    res_1, res_2, res_3, res_4 = [], [], [], []\n    for position in ['1st', '2nd', 'pre-last', 'last']:\n        for fil, res in zip(filtr, [res_1, res_2, res_3, res_4]):\n            info = jok_info[jok_info[col].between(fil[0], fil[1])]\n            l = info['race_id'].nunique()\n            info = info[(info['fav_und']==position)&\n                        (info['expected_finish_place']==info['final_place'])]\n            res.append(round(len(info)/l*100,2))\n    \n    if col == 'dist_furlong':\n        compl['4.5-5.5'], compl['6-6.5'] = res_1, res_2\n        compl['7-8'], compl['8.5<'] = res_3, res_4\n    else:\n        compl['4-5'], compl['6-7'] = res_1, res_2\n        compl['8-9'], compl['10<'] = res_3, res_4\n\nstat = compl_furlong.merge(compl_horses, on='Expected position', how='left')\n\ncols = ['Expected position']\ncols += [('Length of the distance, furlong', col) for col in stat.columns[1:5]]\ncols += [('Number of participants', col) for col in stat.columns[5:]]\nstat.columns = cols\nstat.set_index('Expected position', inplace=True)\nstat.columns = pd.MultiIndex.from_tuples(stat.columns)\nstat.reset_index(inplace=True)\n\ndef stat_style(df):\n    for col in df.columns:\n        df[col] = df[col].astype('str')\n    \n    df = df.style.hide_index().set_caption('Сompliance by length of the distance and number of participants')\\\n        .set_table_styles([\n        {'selector': 'th.col_heading', 'props': 'font-size: 12pt; text-align: center; font-weight: bold; border-collapse: collapse'},\n        {'selector': 'th.col_heading.level1', 'props': 'font-size: 11pt; border-bottom: 2px solid #525252'},\n        {'selector': 'td', 'props': 'text-align: center; font-size: 11pt; border: 1px solid black !important'},\n        {'selector': 'caption', 'props': 'font-size: 14pt; font-weight: bold; color: #303030'},\n        {'selector': 'th:not(.index_name)', 'props': 'background-color: #34675c; color: white;'}\n        ], overwrite=False).set_properties(subset=['Expected position'], **{'font-weight': 'bold'}) \\\n        .set_properties(**{'background-color': '#fafafa'})\n    \n    df = df.set_table_styles({('Number of participants', '4-5'): \n                              [{'selector': 'th', 'props': 'border-left: 2px solid #525252 !important'},\n                               {'selector': 'td', 'props': 'border-left: 2px solid #525252 !important'}]}, overwrite=False)\n    df = df.set_table_styles({('Length of the distance, furlong', '4.5-5.5'): \n                              [{'selector': 'th', 'props': 'border-left: 2px solid #525252 !important'},\n                               {'selector': 'td', 'props': 'border-left: 2px solid #525252 !important'}]}, overwrite=False)\n    return df\nstat = stat_style(stat)\ndisplay(stat)","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:20:41.192545Z","iopub.execute_input":"2022-11-10T13:20:41.192915Z","iopub.status.idle":"2022-11-10T13:21:07.922296Z","shell.execute_reply.started":"2022-11-10T13:20:41.192881Z","shell.execute_reply":"2022-11-10T13:21:07.920832Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nViewers are more likely to guess the winners at short and long distances than at medium distances. The more participants in the race, the less chance to guess the winner. They are <b>5%</b> more likely to correctly identify the current underdog at long distances than at short ones. Also, they correctly identify the underdog in almost half of the races, where the number of participants is no more than 5.\n</p>","metadata":{}},{"cell_type":"code","source":"jok_info = jok_info.merge(races_info[['race_id', 'track_condition', 'race_type']], \n                          on='race_id', how='left')\n\ncompl_tc = pd.DataFrame(columns=['Expected position', 'MY ', 'GD ', 'FT ', \n                                 'SY ', 'FM ', 'YL ', 'SF '])\ncompl_tc['Expected position'] = ['1st', '2nd', 'pre-last', 'last']\ntc = ['MY ', 'GD ', 'FT ', 'SY ', 'FM ', 'YL ', 'SF ']\n\nr1, r2, r3, r4, r5, r6, r7 = [], [], [], [], [], [], []\nfor position in ['1st', '2nd', 'pre-last', 'last']:\n    for t, res in zip(tc, [r1, r2, r3, r4, r5, r6, r7]):\n        info = jok_info[jok_info['track_condition']==t]\n        l = info['race_id'].nunique()\n        info = info[(info['fav_und']==position)&\n                    (info['expected_finish_place']==info['final_place'])]\n        res.append(round(len(info)/l*100,2))\nfor col, res in zip(compl_tc.columns[1:], [r1, r2, r3, r4, r5, r6, r7]):\n    compl_tc[col] = res\n    \n###################################################\n\ncompl_rt = pd.DataFrame(columns=['Expected position', 'AOC', 'MCL', 'ALW', 'STK', \n                                 'CLM', 'MSW', 'STR', 'SOC', \n                                 'SHP', 'WCL', 'SST', 'WMC'])\ncompl_rt['Expected position'] = ['1st', '2nd', 'pre-last', 'last']\nrt = ['AOC', 'MCL', 'ALW', 'STK', \n      'CLM', 'MSW', 'STR', 'SOC', \n      'SHP', 'WCL', 'SST', 'WMC']\n\nr1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12 = [], [], [], [], [], [], [], [], [], [], [], []\nfor position in ['1st', '2nd', 'pre-last', 'last']:\n    for t, res in zip(rt, [r1, r2, r3, r4, r5, r6, \n                           r7, r8, r9, r10, r11, r12]):\n        info = jok_info[jok_info['race_type']==t]\n        l = info['race_id'].nunique()\n        info = info[(info['fav_und']==position)&\n                    (info['expected_finish_place']==info['final_place'])]\n        try:\n            res.append(round(len(info)/l*100,2))\n        except:\n            res.append(0)\nfor col, res in zip(compl_rt.columns[1:], [r1, r2, r3, r4, r5, r6, \n                           r7, r8, r9, r10, r11, r12]):\n    compl_rt[col] = res\n    \n#####################################################\n    \ndef style_df(df, title, size):\n    for col in df.columns:\n        df[col] = df[col].astype('str')\n    \n    df = df.style.hide_index().set_caption(title)\\\n        .set_table_styles([\n            {'selector': 'th.col_heading', 'props': 'font-size: 12pt; text-align: center; font-weight: bold; border-collapse: collapse'},\n            {'selector': 'td', 'props': 'text-align: center; font-size: 11pt; border: 1px solid black !important'},\n            {'selector': 'caption', 'props': 'font-size: 14pt; font-weight: bold; color: #303030'},\n            {'selector': 'th:not(.index_name)', 'props': 'background-color: #34675c; color: white;'}\n        ], overwrite=False).set_properties(subset=['Expected position'], **{'font-weight': 'bold'}) \\\n        .set_properties(**{'background-color': '#fafafa'}).set_table_attributes(\"style='display:inline'\")\n    return df\ndisplay(style_df(compl_rt, 'Compliance by type of race', 10))\nprint()\ndisplay(style_df(compl_tc, 'Compliance by track condition', 11))","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:21:07.926220Z","iopub.execute_input":"2022-11-10T13:21:07.926617Z","iopub.status.idle":"2022-11-10T13:21:08.273545Z","shell.execute_reply.started":"2022-11-10T13:21:07.926581Z","shell.execute_reply":"2022-11-10T13:21:08.271940Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe data contains information about only 40 races with types WMC, SHP, WCL, SST and SOC, so it is useless to make any conclusions about them. Most often, viewers guess the winner in races with \"Starter Allowance\" type - <b>46.75%</b>. The least often, they guess the winner in races with \"Maiden claiming\" type - <b>35.38%.</b> This difference is easy to explain: \"Maiden claiming\" races are for horses that have never won a race. Therefore, it is harder for viewers to predict the outcome. In <b>40.15%</b> of \"Maiden Special Weight\" races, the underdog justifies it's position. \"Maiden special weight\" races feature top quality horses that are expected to quickly break their maidens and move on to more prestigious competitions. This means that it is easier for viewers to single out the worst among promising horses. Most often, viewers manage to guess the winner in races with a sloppy track condition. This is quite strange, given the increased complexity for all participants.\n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Hold time of position</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nEveryone is interested in the final result, but let's see what part of the time the participants hold the expected and actual position depending on the stage of the distance (the first 30% of the distance, 30-70% and the final 30%) and what was their finish position:\n</p>","metadata":{}},{"cell_type":"code","source":"def positions(df, position):\n    if position == 'expected_finish_place':\n        suff = '_efp'\n    else:\n        suff = '_act'\n    info = pd.DataFrame(columns=['race_id', 'program_number', \n                                 'compliance_0_30'+suff, 'compliance_30_70'+suff, \n                                 'compliance_70_100'+suff])\n    r, h, res30, res70, res100 = [], [], [], [], []\n    for i in df['race_id'].unique():\n        r_info = df[(df['race_id']==i)&(df['completed_dist']<=100)]\n        for horse in r_info['program_number'].unique():\n            h_info = r_info[r_info['program_number']==horse]\n            place = jok_info[(jok_info['race_id']==i)&\n                             (jok_info['program_number']==horse)] \\\n                             [position].values[0]\n            r.append(i)\n            h.append(horse)\n            for c_dist, list_ in zip({30: 30, 70: 40, 100: 30}.items(), \n                                     [res30, res70, res100]):\n                h_info2 = h_info[(h_info['completed_dist']>(c_dist[0]-c_dist[1]))&\n                                 (h_info['completed_dist']<=c_dist[0])]\n                try:\n                    list_.append(int(len(h_info2[(h_info2['position']==place)]) \\\n                                     /len(h_info2)*100))\n                except:\n                    list_.append(np.nan)\n    for col, list_ in zip(info.columns.tolist(), [r, h, res30, res70, res100]):\n        info[col] = list_\n    return info\n\npos_df = positions(full_df, 'expected_finish_place')\njok_info = jok_info.merge(pos_df, on=['race_id', 'program_number'], how='left')\npos_df = positions(full_df, 'final_place')\njok_info = jok_info.merge(pos_df, on=['race_id', 'program_number'], how='left')\n\njok_info['compliance_total_efp'] = jok_info['compliance_0_30_efp']*0.3 + \\\n                                   jok_info['compliance_30_70_efp']*0.4 + \\\n                                   jok_info['compliance_70_100_efp']*0.3\n\ndef positions2(df, act_efp, suff):\n    stat = df.groupby(act_efp, as_index=False) \\\n        .agg({'compliance_0_30'+suff: 'mean',\n              'compliance_30_70'+suff: 'mean',\n              'compliance_70_100'+suff: 'mean'}).drop([3]).reset_index(drop=True)\n    for col in stat.columns[1:]:\n        stat[col] = stat[col].round(1)\n    b, c = stat.iloc[2].copy(), stat.iloc[3].copy()\n    stat.iloc[2], stat.iloc[3] = c, b\n    return stat\n\nstat_efp = positions2(jok_info, 'fav_und', '_efp')\nstat_act = positions2(jok_info, 'act_pos', '_act')\n\ncolors = ['#'+x for x in ['34675c', 'b7b8b6', 'ebc91e']]\nfig = make_subplots(rows=2, cols=1, \n                    vertical_spacing=0.1,\n                    subplot_titles=('<b>Expected</b>',  \n                                    '<b>Actual</b>'))\n    \ncols = ['compliance_0_30', 'compliance_30_70', 'compliance_70_100']\nnames = {col: name for col, name in zip(cols, ['0-30', '30-70', '70-100'])}\ndfs = [stat_efp, stat_act]\nx_ = {1: 'fav_und', 2: 'act_pos'}\n\nfor info in zip(dfs, ['_efp', '_act'], [1,2]):\n    for col in cols:\n        fig.add_trace(go.Scatter(x=info[0][x_[info[2]]], \n                                 y=info[0][col+info[1]], \n                                 name=names[col],\n                                 mode='markers',\n                                 marker=dict(color=colors[cols.index(col)],\n                                             line=dict(width=0.5, color='black'),\n                                             size=10),\n                                 legendgroup=col, \n                                 hovertemplate='<b>%{y}</b>'+ \"<extra></extra>\",\n                                 showlegend=(True if info[2]==1 else False)),\n                      row=info[2], col=1) \n        \nfig.update_layout(width=800, height=700, plot_bgcolor='white',\n                  title=dict(text=\"<b>Average time of compliance the position during the race<br>to the expected and actual finish position</b>\",\n                             font=dict(size=22), x=0.83, xanchor='right'),\n                  legend=dict(title=dict(text=' <b>Distance, %</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=14, font_family=\"Calibri\"), \n                  hovermode=\"x\")\n\nfig.update_xaxes(showticklabels=False, showline=False, showgrid=True, gridwidth=2, gridcolor='#f5f2f2', row=1, col=1)\nfig.update_xaxes(showline=False, showgrid=True, gridwidth=2, gridcolor='#f5f2f2', tickfont_size=14, row=2, col=1)\nfig.update_yaxes(title_text='<b>Time</b>', showline=False, showgrid=False, tickfont_size=14, \n                 ticksuffix='%', ticks=\"outside\", tickson=\"boundaries\", ticklen=3)\n\nfig.layout.annotations[0].update(x=0.05, font_size=18)\nfig.layout.annotations[1].update(x=0.04, font_size=18)\n        \nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:21:08.275537Z","iopub.execute_input":"2022-11-10T13:21:08.275984Z","iopub.status.idle":"2022-11-10T13:24:35.099835Z","shell.execute_reply.started":"2022-11-10T13:21:08.275949Z","shell.execute_reply":"2022-11-10T13:24:35.099011Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe last <b>30%</b> of the distance becomes decisive - on average, <b>55%</b> of the time leaders and losers holds it's actual finish position. The first 70% of the distance leaders and losers hold less than a quarter of the time of their actual position. But looking at the graph, there is definitely no clear conclusion, why so? It is not so easy to take and hold a good expected position, especially with a large number of competitors. In addition, the goal of jockeys is not to hold the expected position, but to win. \n</p>\n<p style=\"text-align:justify;\">\nSo, it is important to evaluate the actual finishing position depending on how long the expected leaders remain in the TOP 3, and underdogs in the LAST 3. For this purpose, were considered races with 6 or more participants:\n</p>","metadata":{}},{"cell_type":"markdown","source":"<div class=\"alert alert-block alert-info\">\n<b>Important:</b> time parts (1/3, 2/3) indicate the time for expected leaders in the TOP 3 and for underdogs in the LAST 3.\n</div>","metadata":{}},{"cell_type":"code","source":"num_horses = full_df.groupby('race_id')['num_horses'].mean().reset_index()\nraces = num_horses[num_horses['num_horses']>5]['race_id']\n\ndef positions3(df):\n    info = pd.DataFrame(columns=['race_id', 'program_number', 'position_efp',\n                                 'position_act', 'compliance_total'])\n    r, h, pos, pos_act, res = [], [], [], [], []\n\n    for i in races:\n        r_info = df[(df['race_id']==i)&(df['completed_dist']<=100)]\n        lp = r_info['num_horses'].values[0]\n        positions = {1: '1st', 2: '2nd', lp-1: 'pre_last', lp: 'last'}\n        for p in positions.keys():\n            try:\n                horse = jok_info[(jok_info['race_id']==i)&\n                                 (jok_info['expected_finish_place']==p)] \\\n                ['program_number'].values[0]\n            except:\n                continue\n            r.append(i), h.append(horse), pos.append(positions[p])\n            pos_act.append(jok_info[(jok_info['race_id']==i)&\n                          (jok_info['program_number']==horse)] \\\n                          ['final_place'].values[0])\n            h_info = r_info[r_info['program_number']==horse]\n            try:\n                if p < 3:\n                    res.append(len(h_info[h_info['position']<=3])/\n                               len(h_info)*100)\n                else:\n                    res.append(len(h_info[h_info['position']>=lp-2])/\n                               len(h_info)*100)\n            except:\n                res.append(0)\n            \n    for col, list_ in zip(info.columns.tolist(), [r, h, pos, \n                                                  pos_act, res]):\n        info[col] = list_\n    return info\n\npos_stats = positions3(full_df)\n\ndef positions4(df, col):\n    r_over_top3 = df[df['index']>3]['position_act'].sum()\n    df = df[df['index']<4].reset_index(drop=True)\n    df.loc[len(df)] = [4, r_over_top3]\n    df[col] = round(df['position_act']/\n                    df['position_act'].sum()*100,1)\n    df = df.drop('position_act', axis=1) \\\n           .rename({'index': 'Actual place'}, axis=1)\n    return df\n\nfirst_stat = pd.DataFrame({'Actual place': [1,2,3,4]})\nsecond_stat = first_stat.copy()\npre_last_stat = first_stat.copy()\nlast_stat = first_stat.copy()\n\ndef positions5(df, exp_pos):\n    for time, col in zip([33.33, 66.66, 100], ['<1/3', '1/3-2/3', '2/3<']):\n        df = df.merge(\n        positions4(pos_stats[(pos_stats['position_efp']==exp_pos)&\n                             (pos_stats['compliance_total']<=time)&\n                             (pos_stats['compliance_total']>time-33.33)] \\\n                   ['position_act'].value_counts().reset_index(), col),\n        on='Actual place', how='left'\n        )\n    df['Actual place'] = df['Actual place'].map({1:'1st', 2:'2nd',\n                                                 3:'3rd', 4:'4th<'})\n    return df\n\ndef stat_style(df, fav_undr, pos):\n    for col in df.columns:\n        df[col] = df[col].astype('str')\n    \n    df = df.style.hide_index().set_caption(f\"Expected {pos}\")\\\n        .set_table_styles([\n            {'selector': 'th.col_heading', 'props': 'font-size: 12pt; text-align: center; font-weight: bold; border-collapse: collapse'},\n            {'selector': 'td', 'props': 'text-align: center; font-size: 11pt; border: 1px solid black !important'},\n            {'selector': 'caption', 'props': 'font-size: 14pt; font-weight: bold; color: #303030'},\n            {'selector': 'th:not(.index_name)', 'props': 'background-color: #34675c; color: white;'}\n        ], overwrite=False).set_properties(subset=['Actual place'], **{'font-weight': 'bold'}) \\\n        .set_properties(**{'background-color': '#fafafa'}).set_table_attributes(\"style='display:inline'\")\n    return df\n\nfirst_stat = stat_style(positions5(first_stat, '1st'), 'TOP-3', '1st')\nsecond_stat = stat_style(positions5(second_stat, '2nd'), 'TOP-3', '2nd')\npre_last_stat = stat_style(positions5(pre_last_stat, 'pre_last'), 'LAST-3', 'pre-last')\nlast_stat = stat_style(positions5(last_stat, 'last'), 'LAST-3', 'last')\n\nspace = \"\\xa0\" * 45\ndisplay_html(first_stat._repr_html_()+space+second_stat._repr_html_(), raw=True)","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:24:35.101523Z","iopub.execute_input":"2022-11-10T13:24:35.102403Z","iopub.status.idle":"2022-11-10T13:25:17.333842Z","shell.execute_reply.started":"2022-11-10T13:24:35.102367Z","shell.execute_reply":"2022-11-10T13:25:17.332576Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nOf course, there is the fact that the higher the horse hold a position in the TOP 3, the higher the compliance of the expected and actual finish position. The results are quite obvious, but even long-term hold of leading positions doesn't guarantee leading positions at the finish. The expected leaders of the race, holding the leading positions for more than 2/3 time of the race, finished in fourth position and worse in <b>15.8%</b> (<i>expected 1st</i>) and <b>27.7%</b> (<i>expected 2nd</i>) of such races.\n\nIt is also worth considering similar statistics for underdogs:\n</p>","metadata":{}},{"cell_type":"code","source":"display_html(pre_last_stat._repr_html_()+space+last_stat._repr_html_(), raw=True)","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:25:17.338666Z","iopub.execute_input":"2022-11-10T13:25:17.339002Z","iopub.status.idle":"2022-11-10T13:25:17.365167Z","shell.execute_reply.started":"2022-11-10T13:25:17.338972Z","shell.execute_reply":"2022-11-10T13:25:17.363780Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nMost of all attention should be paid to the fact that the expected underdogs, who spend almost all the time of the race in the last positions, get into the TOP-3 in <b>~10%</b> of the races!\n</p>\n<p style=\"text-align:justify;\">\nHow does this happen and what does it depend on? During the race, the change in position determines the difference in speed from the average speed of competitors. Speed changes can occur for a variety of reasons, which, unfortunately, it is not available to us to determine for all races in general. In case of slowing down, it may be a jockey's mistake, sudden deterioration in the health or fatigue of the horse, slowing down of competitors running ahead, etc. \n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>The underdog that was underestimated</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nBefore I start analyzing all races in general, I want to demonstrate an example of a race where the underdog Joey R. Martinez, running in the last position throughout the race, took first place at the very last moment, overtaking the expected winner Reylu Gutierrez, who held the leading position <b>92.5%</b> of the race! At the finish straight, the distance to the leader was <b>60</b> feet, there was 25% of the distance left, but Martinez snatched the victory contrary to any expectations.\n</p>","metadata":{}},{"cell_type":"code","source":"exp = full_df[full_df['race_id']==32]\nexp['text_scatter'] = 'Distance (feet)</b>: ' + exp['passed_dist'].round(2).astype('str') + '<br>' + \\\n    '<b>Speed (mph)</b>: ' + exp['speed_mph'].round(2).astype('str') + \\\n    '<br>' +'<b>Position</b>: ' + exp['position'].astype('str')\n\ntime = sorted(exp['time_sec'].unique().tolist())\n\nnames = jok_info[jok_info['race_id']==32][['program_number', 'jockey']]\nnames = dict(zip(names['program_number'], names['jockey']))\ncolors = {h: '#'+x for h, x in zip(names.keys(), ['c71c1c', 'e8e8e8', \n                                                  'e8e8e8', '34675c',\n                                                  'e8e8e8', 'e8e8e8'])}\n\nh_ids = exp['program_number'].unique().tolist()\nfig_dict = {\"data\": [], \"layout\": {},\"frames\": []}\n\n# Layout\nlayout = go.Layout(updatemenus=[{\"buttons\": [\n            {\n                \"args\": [None, {\"frame\": {\"duration\": 50, \"redraw\": False},\n                                \"fromcurrent\": True, \"transition\": {\"duration\": 1,\n                                                                    \"easing\": \"quadratic-in-out\"}}],\n                \"label\": \"Play\", \"method\": \"animate\"\n            },\n            {\n                \"args\": [[None], {\"frame\": {\"duration\": 0, \"redraw\": False},\n                                  \"mode\": \"immediate\",\n                                  \"transition\": {\"duration\": 0}}],\n                \"label\": \"Pause\", \"method\": \"animate\"\n            }\n        ],\n        \"direction\": \"left\", \"pad\": {\"r\": 10, \"t\": 50},\n        \"showactive\": False, \"type\": \"buttons\",\n        \"x\": 0.1, \"xanchor\": \"right\",\n        \"y\": -0.05, \"yanchor\": \"top\"\n    }]\n)\n\nfig_dict[\"layout\"] = layout\n\n# Slider\nsliders_dict = {\n    \"active\": 0,\n    \"yanchor\": \"top\",\n    \"xanchor\": \"left\",\n    \"currentvalue\": {\n        \"font\": {\"size\": 17},\n        \"prefix\": \"seconds from start: \",\n        \"visible\": True,\n        \"xanchor\": \"right\"\n    },\n    \"transition\": {\"duration\": 50, \"easing\": \"bounce-in-out\"},\n    \"pad\": {\"b\": 10, \"t\": 50},\n    \"len\": 0.9,\n    \"x\": 0.1,\n    \"y\": 0,\n    \"steps\": []\n}\n\n# Data\nfor horse in h_ids:\n    info = exp.query(\"program_number == @horse\").iloc[[0]]\n    fig_dict[\"data\"].append(go.Scatter(x=info['longitude'], y=info['latitude'], name=names[horse],\n                                       mode='markers', \n                                       marker=dict(size=8, opacity=0.8, color=colors[horse],\n                                                   line=dict(width=0.5, color='black')),\n                                       text=info['text_scatter'], hovertemplate=\"<b>%{text}</b>\"))\n    \n# Frames\nfor i in time:\n    frame = {\"data\": [], \"name\": str(i), \"layout\": {}}\n    for horse in h_ids:\n        info = exp.query(\"time_sec == @i & program_number == @horse\")\n        frame[\"data\"].append(go.Scatter(x=info['longitude'], y=info['latitude'], name=names[horse],\n                                        mode='markers', \n                                        marker=dict(size=8, opacity=0.8, color=colors[horse],\n                                                   line=dict(width=0.5, color='black')),\n                                        text=info['text_scatter'], hovertemplate=\"<b>%{text}</b>\"))\n    \n    info = exp.query(\"time_sec == @i\")\n    frame[\"layout\"][\"xaxis\"] = {\"range\": [info['longitude'].min()-0.0005, info['longitude'].max()+0.0005]}\n    frame[\"layout\"][\"yaxis\"] = {\"range\": [info['latitude'].min()-0.0005, info['latitude'].max()+0.0005]}\n    fig_dict[\"frames\"].append(frame)\n    \n    slider_step = {\"args\": [\n        [i],\n        {\"frame\": {\"duration\": 10, \"redraw\": False},\n         \"mode\": \"immediate\",\n         \"transition\": {\"duration\": 10}}\n    ],\n        \"label\": i,\n        \"method\": \"animate\",\n        }\n    sliders_dict[\"steps\"].append(slider_step)\n        \nfig_dict[\"layout\"][\"sliders\"] = [sliders_dict]    \n\nfig = go.Figure(fig_dict)\n\ninfo = exp.query(\"time_sec == 0.25\")\nfig.update_layout(width=800, height=800, plot_bgcolor='white', \n                   xaxis={\"range\": [info['longitude'].min()-0.0005, info['longitude'].max()+0.0005]}, \n                   yaxis={\"range\": [info['latitude'].min()-0.0005, info['latitude'].max()+0.0005]},\n                   title={\"text\": \"<b>Track: AQU | Date: 2019-01-06 | Race: №7</b>\", \"font\": {\"size\": 22}, \"x\": 0.06}, \n                   legend={\"title_text\": \"                <b>Jockey</b>\", \"title\": {\"font\": {\"size\": 16}},\n                           \"bordercolor\": \"#707070\", \"borderwidth\": 0.7, \"bgcolor\": '#f7f7f7', 'y':0.5},\n                   font_family=\"Calibri\", font_color=\"#303030\",\n                   hoverlabel=dict(font_size=12,font_family=\"Calibri\"))\n\nfig.update_yaxes(title_text='latitude', showline=False, linecolor='#f5f2f2', \n                 showgrid=True, gridwidth=1, gridcolor='#f5f2f2',\n                 linewidth=1, tickfont_size=10)\n\nfig.update_xaxes(title_text='longitude', showline=False, linecolor='#f5f2f2', \n                 showgrid=True, gridwidth=1, gridcolor='#f5f2f2',\n                 linewidth=1, tickfont_size=10)\n                  \nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:25:17.367029Z","iopub.execute_input":"2022-11-10T13:25:17.367686Z","iopub.status.idle":"2022-11-10T13:25:25.149979Z","shell.execute_reply.started":"2022-11-10T13:25:17.367651Z","shell.execute_reply":"2022-11-10T13:25:25.148894Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nNo statistical model could have predicted such an outcome even during the race. Of course, this is an exception to the rules, but this race serves as an example of the fact that the <b>actual finish position depends not only on the horse's own position during the race</b>. It is important to take into account the speed of competitors. \n</p>  \n<p style=\"text-align:justify;\">\nThis race is also an excellent example for analyzing the speed of the expected winner and underdog in comparison with the average speed of all participants:\n</p>","metadata":{}},{"cell_type":"code","source":"exp_plot = exp[(exp['time_sec']>0.25)&(exp['program_number']==0)]\n\nfig = make_subplots(rows=3, cols=1, \n                    vertical_spacing=0.05,\n                    subplot_titles=('<b>Speed</b>',\n                                    '<b>Deviation from the average speed</b>',\n                                    \"<b>Distance to the leader</b>\"))\nfig.add_trace(go.Scatter(x=exp_plot['time_sec'], y=exp_plot['gen_avg_speed'],\n                         name='average speed', mode='lines',\n                         line=dict(color='#344d90', width=1, dash='dot'),\n                         hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n              row=1, col=1)\n\nfor horse, name, color in zip([0, 14], \n                              ['Joey R. Martinez', 'Reylu Gutierrez'], \n                              ['#c71c1c', '#34675c']):\n    exp_plot = exp[(exp['time_sec']>0.25)&(exp['program_number']==horse)]\n    fig.add_trace(go.Scatter(x=exp_plot['time_sec'], y=exp_plot['speed_mph'],\n                             name=name, mode='lines',\n                             line=dict(color=color, width=1),\n                             legendgroup=name, \n                             showlegend=True,\n                             hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                  row=1, col=1)\n    \n    exp_plot = exp[(exp['time_sec']>1)&(exp['program_number']==horse)]\n    fig.add_trace(go.Scatter(x=exp_plot['time_sec'], y=exp_plot['speed_diff_avg_%'],\n                             name=name, mode='lines',\n                             line=dict(color=color, width=1),\n                             legendgroup=name, \n                             showlegend=False,\n                             hovertemplate='%{y:.2f}%'+\"<extra></extra>\"),\n                  row=2, col=1)\n    \n    exp_plot = exp[(exp['time_sec']>0.25)&(exp['program_number']==horse)]\n    fig.add_trace(go.Scatter(x=exp_plot['time_sec'], y=exp_plot['dist_to_lead'], mode='lines',\n                             line=dict(color=color, width=3),\n                             legendgroup=name,\n                             showlegend=False,\n                             hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                  row=3, col=1)\n    \nfig.add_hline(y=0, line=dict(dash=\"dot\", width=0.5, color='#344d90'), row=2, col=1)\n\nfig.update_layout(width=800, height=1200, plot_bgcolor='white',\n                  title=dict(text=\"<b>Analysis of the expected winner and underdog</b>\",\n                         font=dict(size=22), x=0.81, xanchor='right'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  legend=dict(bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  hoverlabel=dict(font_size=12, font_family=\"Calibri\"),\n                  hovermode=\"x\")\n\nfig.update_xaxes(showline=False, showticklabels=False, row=1, col=1)\nfig.update_xaxes(showline=False, showticklabels=False, row=2, col=1)\nfig.update_xaxes(title_text='<b>Time</b>', showline=False, tickfont_size=12,\n                 ticks=\"outside\", tickson=\"boundaries\", ticklen=3, row=3, col=1)\n\nfig.update_yaxes(title_text='<b>mph</b>', showline=False, tickfont_size=12, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=3, row=1, col=1)\nfig.update_yaxes(showline=False, tickfont_size=12, ticksuffix='%', \n                 ticks=\"outside\", tickson=\"boundaries\", ticklen=3, row=2, col=1)\nfig.update_yaxes(title_text='<b>feet</b>', showline=False, tickfont_size=12, \n                 ticks=\"outside\", tickson=\"boundaries\", ticklen=3, row=3, col=1)\n\nfig.layout.annotations[0].update(x=0.04, font_size=18)\nfig.layout.annotations[1].update(x=0.22, font_size=18)\nfig.layout.annotations[2].update(x=0.14, font_size=18)\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:25:25.151824Z","iopub.execute_input":"2022-11-10T13:25:25.152156Z","iopub.status.idle":"2022-11-10T13:25:25.309228Z","shell.execute_reply.started":"2022-11-10T13:25:25.152126Z","shell.execute_reply":"2022-11-10T13:25:25.308019Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nWe can see that the distance between Martinez and Gutierrez steadily increased throughout the race, but at 48 seconds, as soon as Martinez reached the finish straight, he abruptly shortened the distance, overtaking everyone and taking first place. There may be many reasons for this outcome, but from my point of view, Martinez, holding on to the last position, retained a lot of strength of his horse to the finish straight, thanks to which he was able to overtake tired competitors. It is also worth noting that there were <b>4</b> more similar situations in his career in 2019. We'll leave this race for now, but we'll come back to it later.\n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Speed and distance to the leader</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nOur task is not to thoroughly study one race, but to try to find a common solution. Let's look at all the races depending on their distance. The distance of all races is different and each takes a different amount of time, so it is incorrect to take into account the speed during each quarter of a second. Thus, the analysis will be performed based on each 1% of the completed distance. Many analysts consider the speed of each participant individually, but it is more interesting for me to look at the difference in speed between winners and losers.\n</p>","metadata":{}},{"cell_type":"code","source":"def percent_races(df):\n    pr = pd.DataFrame()\n    for race in df['race_id'].unique():\n        exp = df[df['race_id']==race]\n        exp['completed_dist_round'] = exp['completed_dist'].apply(lambda x: math.ceil(x))\n        info = exp.groupby(['completed_dist_round', 'program_number']) \\\n                  .agg({'speed_mph': 'mean', 'dist_to_lead': 'mean'}).reset_index() \\\n                  .rename({'completed_dist_round': 'dist'}, axis=1)\n        info = info[info['dist'].between(1,100)]\n        ms = exp.groupby('completed_dist_round') \\\n                ['gen_avg_speed'].mean().reset_index() \\\n                .rename({'completed_dist_round': 'dist'}, axis=1)\n        info = info.merge(ms, on='dist', how='left')\n        info['race_id'] = exp['race_id'].unique()[0]\n        info['sdm_num'] = info['speed_mph'] - info['gen_avg_speed']\n        info['sdm_%'] = round((info['speed_mph'] /\n                               info['gen_avg_speed']*100)-100, 2)\n        pr = pd.concat([pr, info], axis=0, ignore_index=True)\n    return pr\n\npr = percent_races(full_df)\npr = pr.merge(jok_info[['race_id', 'program_number', 'final_place',\n                        'expected_finish_place', 'dist_furlong', \n                        'num_horses', 'fav_und', 'act_pos']], \n              on=['race_id', 'program_number'], how='left')\npr = pr[pr['dist_furlong'].isin(sorted(pr['dist_furlong'].unique().tolist())[2:9])]\n\n#############################################################\n\nfig = make_subplots(rows=4, cols=1, \n                    vertical_spacing=0.05,\n                    subplot_titles=('<b>6 furlongs</b>',\n                                    '<b>7 furlongs</b>',\n                                    '<b>8 furlongs</b>',\n                                    '<b>9 furlongs</b>'))\n\nfor i, furlong in enumerate([6,7,8,9]):\n    info = pr[pr['dist_furlong']==furlong]\n    for line, color in zip(['1st', 'last'], ['#34675c', '#c71c1c']):\n        for col, name, width in zip(['fav_und', 'act_pos'], \n                                    ['expected ', 'actual '],\n                                    [1, 3]):\n            info_2 = info[info[col]==line]\n            fig.add_trace(go.Scatter(x=sorted(info_2['dist'].unique().tolist()), \n                                     y=info_2.groupby('dist')['sdm_%'].mean(), \n                                     name=name+line, mode='lines', legendgroup=name+line,\n                                     line=dict(color=color, width=width, \n                                               dash=('solid' if width==3 else 'dot')),\n                                     showlegend=(True if i==0 else False),\n                                     hovertemplate='%{y:.2f}%'+\"<extra></extra>\"),\n                          row=i+1, col=1)\n        \nfor i in [1,2,3,4]:\n    fig.add_hline(y=0, line=dict(dash=\"dot\", width=0.5, color='#344d90'), \n                  row=i, col=1)\n\nfig.update_layout(width=800, height=1200, plot_bgcolor='white',\n                  title=dict(text=\"<b>Deviation of speed of winners and losers from<br>average speed depending on the race distance</b>\",\n                         font=dict(size=22), x=0.83, xanchor='right'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  legend=dict(title=dict(text='       <b>Participant</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  hoverlabel=dict(font_size=12, font_family=\"Calibri\"),\n                  hovermode=\"x\")\n\nfor i in [1,2,3]:\n    fig.update_xaxes(showline=False, showticklabels=False, row=i, col=1)\nfig.update_xaxes(title_text='<b>Completed distance, %</b>', showline=False, \n                 tickfont_size=12, ticks=\"outside\", tickson=\"boundaries\", \n                 ticklen=3, tickvals=[10,20,30,40,50,60,70,80,90,100], row=4, col=1)\nfig.update_yaxes(showline=False, tickfont_size=12, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=3, ticksuffix='%')\n\nfor i, x in zip([0,1,2,3], [0.06, 0.06, 0.06, 0.06]):\n    fig.layout.annotations[i].update(x=x, font_size=17)\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:25:25.311014Z","iopub.execute_input":"2022-11-10T13:25:25.311367Z","iopub.status.idle":"2022-11-10T13:26:33.200607Z","shell.execute_reply.started":"2022-11-10T13:25:25.311336Z","shell.execute_reply":"2022-11-10T13:26:33.199301Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nRegarding races at middle and long distances (7-9 furlongs), horses keep approximately the same speed for more than half of the distance. The speed deviation from the average does not differ by more than <b>1%</b> on average: <b>50%</b> of distance for 7 furlongs, <b>55%</b> for 8 furlongs and <b>60%</b> for 9 furlongs. This is quite understandable - there is no need to frazzle horses at the beginning or in the middle of the race, when you can stick to the average speed. All participants try to leave the explosive power of horses to the end of the race. With increasing distance, endurance becomes more important than explosive power. But it is also important to note that before the participants begin to hold the average speed, the leaders start with a considerable deviation from the average speed of the other participants. As for short-distance races, all is the opposite - the participants start the fight earlier and explosive power is already becoming more important.\n</p>\n<p style=\"text-align:justify;\">\nIt is also necessary to consider the difference in speed on turns:\n</p>","metadata":{}},{"cell_type":"code","source":"full_df = full_df.merge(jok_info[['race_id', 'program_number', 'fav_und', \n                                  'act_pos', 'dist_furlong']], \n                        on=['race_id', 'program_number'], how='left')\nfig = go.Figure()\nfor fur in [6,7,8,9]:\n    info = full_df[(full_df['not_turn']==False)&\n                   (full_df['dist_furlong']==fur)]\n    for pos, color in zip(['1st', 'last'], ['#34675c', '#c71c1c']):\n        fig.add_trace(go.Scatter(x=np.array(fur), \n                                 y=np.array(info[info['act_pos']==pos]['speed_diff_avg_%'].mean()), \n                                 name='Actual '+pos,\n                                 mode='markers',\n                                 marker=dict(color=color,\n                                             line=dict(width=0.5, color='black'),\n                                             size=10),\n                                 hovertemplate='<b>%{y:.2f}%</b>'+ \"<extra></extra>\",\n                                 showlegend=(True if fur==6 else False)))\n        fig.add_trace(go.Scatter(x=np.array(fur), \n                                 y=np.array(info[info['fav_und']==pos]['speed_diff_avg_%'].mean()), \n                                 name='Expected '+pos,\n                                 mode='markers',\n                                 marker=dict(color=color,\n                                             line=dict(width=0.5, color='black'),\n                                             size=5),\n                                 hovertemplate='<b>%{y:.2f}%</b>'+ \"<extra></extra>\",\n                                 showlegend=(True if fur==6 else False)))\nfig.add_hline(y=0, line=dict(dash=\"dot\", width=0.5, color='#344d90'))\n        \nfig.update_layout(width=800, height=700, plot_bgcolor='white',\n                  title=dict(text=\"<b>Speed deviation from average speed on turns<br>depending on the length of the distance</b>\",\n                             font=dict(size=22), x=0.1, xanchor='left'),\n                  legend=dict(title=dict(text='       <b>Participant</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=14, font_family=\"Calibri\"), \n                  hovermode=\"x\")\n\nfig.update_xaxes(title_text='<b>Distance, fur.</b>', showline=False, showgrid=True, \n                 gridwidth=2, gridcolor='#f5f2f2', tickfont_size=14, tickvals=[6,7,8,9])\nfig.update_yaxes(title_text='<b>Deviation</b>', showline=False, showgrid=False, tickfont_size=14, \n                 ticksuffix='%', ticks=\"outside\", tickson=\"boundaries\", ticklen=3)\n        \nfig.show()\n    ","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:26:33.202521Z","iopub.execute_input":"2022-11-10T13:26:33.203181Z","iopub.status.idle":"2022-11-10T13:26:37.131422Z","shell.execute_reply.started":"2022-11-10T13:26:33.203129Z","shell.execute_reply":"2022-11-10T13:26:37.130498Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe results are quite obvious, but I propose to consider in more detail how the distance to the leader changes during the race:\n</p>","metadata":{}},{"cell_type":"code","source":"fig = make_subplots(rows=4, cols=1, \n                    vertical_spacing=0.05,\n                    subplot_titles=('<b>6 furlongs</b>',\n                                    '<b>7 furlongs</b>',\n                                    '<b>8 furlongs</b>',\n                                    '<b>9 furlongs</b>'))\n\nfor i, furlong in enumerate([6,7,8,9]):\n    info = pr[pr['dist_furlong']==furlong]\n    fig.add_trace(go.Scatter(x=sorted(info['dist'].unique().tolist()), \n                             y=info.groupby('dist')['dist_to_lead'].mean(), \n                             name='average dist', mode='lines', \n                             legendgroup='average dist',\n                             line=dict(color='#344d90', width=1, dash='dot'),\n                             showlegend=(True if i==0 else False),\n                             hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                  row=i+1, col=1)\n    for line, color in zip(['1st', 'last'], ['#34675c', '#c71c1c']):\n        for col, name, width in zip(['fav_und', 'act_pos'], \n                                    ['expected ', 'actual '],\n                                    [1, 3]):\n            \n            info_2 = info[info[col]==line]\n            fig.add_trace(go.Scatter(x=sorted(info_2['dist'].unique().tolist()), \n                                     y=info_2.groupby('dist')['dist_to_lead'].mean(), \n                                     name=name+line, mode='lines', legendgroup=name+line,\n                                     line=dict(color=color, width=width, \n                                               dash=('solid' if width==3 else 'dot')),\n                                     showlegend=(True if i==0 else False),\n                                     hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                          row=i+1, col=1)\n\nfig.update_layout(width=800, height=1200, plot_bgcolor='white',\n                  title=dict(text=\"<b>Average distance to the leader of winners and<br>losers depending on the race distance</b>\",\n                         font=dict(size=22), x=0.83, xanchor='right'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  legend=dict(title=dict(text='       <b>Participant</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  hoverlabel=dict(font_size=12, font_family=\"Calibri\"),\n                  hovermode=\"x\")\n\nfor i in [1,2,3,4]:\n    fig.add_hline(y=0, line=dict(width=0.5, color='#b5b5b5'), \n                  row=i, col=1)\n    \nfor i in [1,2,3]:\n    fig.update_xaxes(showline=False, showticklabels=False, row=i, col=1)\nfig.update_xaxes(title_text='<b>Completed distance, %</b>', showline=False, \n                 tickfont_size=12, ticks=\"outside\", tickson=\"boundaries\", \n                 ticklen=3, tickvals=[10,20,30,40,50,60,70,80,90,100], row=4, col=1)\nfig.update_yaxes(showline=False, linecolor='#f5f2f2', linewidth=1, tickfont_size=12, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=3)\nfor i, x in zip([0,1,2,3], [0.06, 0.06, 0.06, 0.06]):\n    fig.layout.annotations[i].update(x=x, font_size=17)\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:26:37.132940Z","iopub.execute_input":"2022-11-10T13:26:37.133653Z","iopub.status.idle":"2022-11-10T13:26:37.838599Z","shell.execute_reply.started":"2022-11-10T13:26:37.133617Z","shell.execute_reply":"2022-11-10T13:26:37.837536Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe results of the change in the average distance to the leader during the race also confirms that the culmination of middle and long races occurs closer to the finish.  In the first half of the race, the distance of the actual leaders to the leading position does not exceed <b>16-23.5</b> feet, depending on the race distance. Difference between the leaders and losers to leading position is no more than <b>9-17</b> feet for 7-9 furlongs distances, but with 10% remaining until the end of the race, the difference is more than <b>120</b> feet! In addition, it is also interesting that a small difference in the results of speed and distance to the leader between the expected and actual leaders and losers persists for quite a long time from the start.\n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Overtakings</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nBased on the graphs describing the change in speed and distance to the leader, there could be a misconception that in races with distances from 7 furlongs and above, from the start to the middle of the distance, there is almost no change in positions. This is partly true, especially if we compare the distance interval of 20-70% and last 30%. However, the change of positions occurs throughout the race.\n</p>","metadata":{}},{"cell_type":"code","source":"def overtaking_stat(df):\n    res = pd.DataFrame()\n    for race in full_df['race_id'].unique():\n        exp = full_df[full_df['race_id']==race]\n        exp = exp[exp['time_sec']>=1]\n        exp['completed_dist_round'] = exp['completed_dist'] \\\n                                             .apply(lambda x: math.ceil(x))\n        ot_stat = pd.DataFrame()\n        time = sorted(exp['time_sec'].unique().tolist())[1:]\n        overtaking = []\n        for sec in time:\n            positions_aft = exp[exp['time_sec']==sec]['position'].tolist()\n            positions_bef = exp[exp['time_sec']==sec-0.25]['position'].tolist()\n            if positions_aft == positions_bef:\n                overtaking.append(False)\n            else:\n                overtaking.append(True)\n        \n        ot_stat['time_sec'] = time\n        ot_stat['overtaking'] = overtaking\n\n        winner = jok_info[(jok_info['race_id']==race)&\n                          (jok_info['final_place']==1)] \\\n                         ['program_number'].values[0]\n        dist = exp[(exp['program_number']==winner)&\n                   (exp['completed_dist_round']<=100)] \\\n                  [['time_sec', 'completed_dist_round']]\n        ot_stat = ot_stat.merge(dist, on='time_sec', how='inner')\n\n        ot_stat = ot_stat.groupby('completed_dist_round') \\\n                             ['overtaking'].sum().reset_index()\n        ot_stat['race_id'] = exp['race_id'].values[0]\n        \n        res = pd.concat([res, ot_stat], ignore_index=True)\n    return res\n\not_stat = overtaking_stat(full_df)\not_stat = ot_stat.merge(races_info[['race_id', 'dist_furlong', \n                                    'track_condition']], \n                        on='race_id', how='left')\not_stat = ot_stat.merge(jok_info[['race_id', 'num_horses']].drop_duplicates(),\n                        on='race_id', how='left')\not_stat.rename({'completed_dist_round': 'dist'}, axis=1, inplace=True)\n\n###########################################\n\nfig = make_subplots(rows=4, cols=1, \n                    vertical_spacing=0.05,\n                    subplot_titles=('<b>4-5 participants</b>',\n                                    '<b>6-7 participants</b>',\n                                    '<b>8-9 participants</b>',\n                                    '<b>10-11 participants</b>'))\n\nfor i, p in enumerate([4,6,8,10]):\n    info = ot_stat[(ot_stat['num_horses'].between(p, p+1))&(ot_stat['dist_furlong']==9)]\n    x = sorted(info['dist'].unique().tolist())\n    y = info.groupby('dist')['overtaking'].mean().reset_index()\n    ma = y['overtaking'].rolling(5).mean()\n    \n    fig.add_trace(go.Scatter(x=x, y=y['overtaking'], \n                             name='Every 1% of distance', mode='lines', \n                             line=dict(color='#344d90', width=1),\n                             showlegend=(True if i==0 else False),\n                             legendgroup='Every 1% of distance',\n                             hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                  row=i+1, col=1)\n    fig.add_trace(go.Scatter(x=x, y=ma, \n                             name='Moving average (5%)',\n                             line=dict(color='#c71c1c', width=4, dash='dot'),\n                             showlegend=(True if i==0 else False),\n                             legendgroup='Moving average (5%)',\n                             hovertemplate='%{y:.2f}'+\"<extra></extra>\"),\n                  row=i+1, col=1)\n\nfig.update_layout(width=800, height=1200, plot_bgcolor='white',\n                  title=dict(text=\"<b>Average number of overtakings at distances of 9 furlongs<br>depending on the number of participants</b>\",\n                         font=dict(size=22), x=0.78, y=0.97, xanchor='right'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  legend=dict(title=dict(text='               <b>Indicator</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  hoverlabel=dict(font_size=12, font_family=\"Calibri\"),\n                  hovermode=\"x\")\n\nfor i in [1,2,3,4]:\n    fig.add_hline(y=0, line=dict(width=0.5, color='#b5b5b5'), \n                  row=i, col=1)\n    \nfor i in [1,2,3]:\n    fig.update_xaxes(showline=False, showticklabels=False, row=i, col=1)\nfig.update_xaxes(title_text='<b>Completed distance, %</b>', showline=False, \n                 tickfont_size=12, ticks=\"outside\", tickson=\"boundaries\", \n                 ticklen=3, tickvals=[10,20,30,40,50,60,70,80,90,100], row=4, col=1)\nfig.update_yaxes(showline=False, linecolor='#f5f2f2', linewidth=1, tickfont_size=12, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=3)\nfor i, x in zip([0,1,2,3], [0.12, 0.12, 0.12, 0.13]):\n    fig.layout.annotations[i].update(x=x, font_size=17)\n\nfig.show()\n\n####################################################\n\not_tab_stat = pd.DataFrame()\nhorses = [4,6,8,10]\not_tab_stat['Participants'] = [str(x)+'-'+str(x+1) for x in horses]\nfor furlong in [6,7,8,9]:\n    res = []\n    for n_horse in horses:\n        info = ot_stat[(ot_stat['num_horses'].between(n_horse, n_horse+1))&\n                       (ot_stat['dist_furlong']==furlong)]\n        r70 = info[info['dist'].between(70,100)]['overtaking'].mean()\n        r20 = info[info['dist'].between(20,69)]['overtaking'].mean()\n        res.append(round(r70/r20*100-100,2))\n    ot_tab_stat[f'{furlong} fur.'] = res\n    \ndef stat_style(df):\n    for col in df.columns:\n        df[col] = df[col].astype('str')\n    \n    df = df.style.hide_index().set_caption('Increasing the frequency of overtaking in the last 30% of the distance')\\\n        .set_table_styles([\n            {'selector': 'th.col_heading', 'props': 'font-size: 12pt; text-align: center; font-weight: bold; border-collapse: collapse'},\n            {'selector': 'td', 'props': 'text-align: center; font-size: 11pt; border: 1px solid black !important'},\n            {'selector': 'caption', 'props': 'font-size: 14pt; font-weight: bold; color: #303030'},\n            {'selector': 'th:not(.index_name)', 'props': 'background-color: #34675c; color: white;'}\n        ], overwrite=False).set_properties(subset=['Participants'], **{'font-weight': 'bold'}) \\\n        .set_properties(**{'background-color': '#fafafa'}).set_table_attributes(\"style='display:inline'\")\n    return df\n\ndisplay(stat_style(ot_tab_stat))","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:37:04.177488Z","iopub.execute_input":"2022-11-10T13:37:04.178214Z","iopub.status.idle":"2022-11-10T13:47:28.465566Z","shell.execute_reply.started":"2022-11-10T13:37:04.178168Z","shell.execute_reply":"2022-11-10T13:47:28.464742Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nTry to change the interval of the completed distance (20-100%) and you will see the difference in the number of overtaking. From the above results, the number of overtaking in the last 30% of the distance in all races increases by <b>~28%</b>. Interesting that the change of positions with 6-9 participants throughout the race is quite stable. The analysis of speed, distance to the leader and the number of overtaking tells us that the last ~30% of the distance are the most important. \n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Probability of victory and expected position</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe whole analysis above rested on the fact that the most important and decisive stage of the race is the final one, where the participant, who holds the last positions throughout the race, can become the winner. But <b>how valuable is the information throughout the race and what does the expected position depend on?</b> To answer this question, it will take something more than statistics and graphs - machine learning algorithm!\n</p>\n<p style=\"text-align:justify;\">\nFrom the point of view of machine learning, calculating the expected finish position is a multi-class classification task, but there is one big nuance: the number of participants in each race is different. A participant whose expected position is the third among 3-4 participants is a bad expectation, but among 9 or more participants is already a good one. Therefore, I decided to use binary classification, where the expected position of all participants will depend on their chances of winning in the ranking order. There will be used XGB algorithm with AUC metric. What is AUC and how to understand it's results will be explained immediately after learning the algorithm.\n</p>\n<p style=\"text-align:justify;\">\nInformation that will be useful for learning the algorithm:\n<ul>\n  <li>Average deviation of speed from average speed of all participants, underdog and expected winner by odds</li>\n  <li>Average and last observed distance to the leader</li>\n  <li>Mode of position</li>\n  <li>Last observed position</li>\n  <li>Odds</li>\n  <li>Weight carried</li>\n  <li>Distance of race</li>\n  <li>Number of participants</li>\n</ul>    \n</p>\n<p style=\"text-align:justify;\">\nThe model will be trained at intervals every 10% of the distance, which will allow for a better evaluation of the impact of various factors on predictions throughout the race. In addition, the model will not use the individual speed of the participant, but only his speed deviation from the speed of other participants. You may have noticed that information about the type of race and the condition of the track was not added to the model. This information, as well as the length of the distance and the number of participants, has no effect on improving the quality of predictions. You will see the results below:\n</p>","metadata":{}},{"cell_type":"code","source":"#################\n# PREPROCESSING #\n#################\n\njok_info.drop(jok_info.columns[-7:].tolist(), axis=1, inplace=True)\n\n###########\n# Statistics on previous games of 2019 didn't show big improvement, so I decided not to take it into account.\n###########\n\n# lej = LabelEncoder()\n# jok_info['jockey_id'] = lej.fit_transform(jok_info['jockey'])\n#\n# def jockey_position_time(df):\n#     res_df = pd.DataFrame(columns=['race_id', 'program_number', \n#                                    'time_in_top3', 'time_top1', \n#                                    'time_last'])\n#     rid, pn, top3, top1, last = [], [], [], [], []\n#     for race in df['race_id'].unique():\n#         info = df[df['race_id']==race]\n#         for horse in info['program_number'].unique():\n#             h_info = info[info['program_number']==horse]\n#             rid.append(race)\n#             pn.append(horse)\n#             top3.append(len(h_info[h_info['position']<4])/len(h_info))\n#             top1.append(len(h_info[h_info['position']==1])/len(h_info))\n#             last.append(len(h_info[h_info['position']==h_info['num_horses'].values[0]])/len(h_info))\n#     for list_, col in zip([rid, pn, top3, top1, last],\n#                           res_df.columns):\n#         res_df[col] = list_\n#     return res_df\n\n# jpt = jockey_position_time(full_df)\n\n################################################################\n\n# jok_stat = jok_info.groupby(['jockey', 'race_id'])['final_place'].count() \\\n#                     .reset_index().rename({'final_place': 'races'}, axis=1)\n# wins = jok_info[jok_info['final_place']==1].groupby(['jockey', \n#                                                      'race_id'])['final_place'].count() \\\n#                                            .reset_index().rename({'final_place': \n#                                                                   'wins'}, axis=1)\n# top3 = jok_info[jok_info['final_place']<4].groupby(['jockey', \n#                                                     'race_id'])['final_place'].count() \\\n#                                           .reset_index().rename({'final_place': \n#                                                                  'top3'}, axis=1)\n# last_place = jok_info[jok_info['act_pos']=='last'].groupby(['jockey', \n#                                                             'race_id'])['final_place'].count() \\\n#                                                   .reset_index().rename({'final_place': \n#                                                                          'last'}, axis=1)\n\n# jok_stat = jok_stat.merge(wins, on=['jockey', 'race_id'], how='left') \\\n#                    .merge(top3, on=['jockey', 'race_id'], how='left') \\\n#                    .merge(last_place, on=['jockey', 'race_id'], how='left').fillna(0)\n# jok_stat.sort_values(['jockey', 'race_id'], inplace=True)\n# jok_stat = jok_stat.merge(jok_info[['race_id', 'jockey', 'program_number']], \n#                           on=['race_id', 'jockey'], how='left')\n# jok_stat = jok_stat.merge(jpt, on=['program_number', 'race_id'], how='left').drop('program_number', axis=1)\n\n# cum_stat = jok_stat.groupby('jockey').cumsum()\n# cols = ['wins', 'top3',  'last', \n#         'time_in_top3', 'time_top1', 'time_last']\n# for col in cols:\n#     jok_stat[col] = cum_stat[col] / cum_stat['races']\n# jok_stat['races'] = cum_stat['races']\n\n# for col in cols+['races']:\n#     jok_stat[col] = jok_stat.groupby('jockey')[col].shift(1).fillna(0)\n    \n# def comparison_participants(df):\n#     new_df = pd.DataFrame()\n#     for race in df['race_id'].unique():\n#         info = df[df['race_id']==race].groupby('race_id').mean() \\\n#                                       .reset_index().drop('jockey', axis=1)\n#         info.rename({col: 'avg_'+col for col in info.columns.tolist()[1:]}, \n#                     axis=1, inplace=True)\n#         new_df = pd.concat([new_df, info], axis=0, ignore_index=True)\n#     cols = df.columns[-7:]\n#     df = df.merge(new_df, on='race_id', how='left')\n#     for col in cols:\n#         df[f'diff_{col}'] = df[col] - df[f'avg_{col}']\n#         df.drop([f'avg_{col}', col], axis=1, inplace=True)\n#     return df\n# jok_stat = comparison_participants(jok_stat)\n\n# jok_info = jok_info.merge(jok_stat, on=['jockey', 'race_id'], how='left')\n# jok_info['jockey'] = lej.inverse_transform(jok_info['jockey'])\n\n########################################\n\ndef find_winner_loser(info, joks, race_id, pos, col_name):\n    horse = joks[joks['fav_und']==pos]['program_number'].values[0]\n    return info[info['program_number']==horse] \\\n                    .groupby('completed_dist_round')['speed_mph'].mean() \\\n                    .reset_index().rename({'speed_mph': col_name}, axis=1)\n\ndef ml_preprocessing(df):\n    new_df = pd.DataFrame(columns=['race_id', 'program_number', 'avg_sdw', \n                                   'avg_sdl', 'avg_sda', 'avg_dtl', \n                                   'l_dtl', 'mode_pos', 'l_pos', \n                                   'odds', 'j_wt', 'r_dist', 'num_horses'])\n    rid, pn, avg_sdw, avg_sdl, avg_sda = [], [], [], [], []\n    avg_dtl, l_dtl, mode_pos, l_pos, exp_pos, odds = [], [], [], [], [], []\n    j_wt, r_dist, num_horses = [], [], []\n    \n    jinfo = jok_info[~jok_info['race_id'].isin(list(htd.keys())+[652, 820])]\n    # In races 652 and 820 there are participants who didn't reach the finish line\n    \n    for race in tqdm(jinfo['race_id'].unique()):\n        info = df[df['race_id']==race]\n        joks = jinfo[jinfo['race_id']==race]\n        \n        winner_speed = find_winner_loser(info, joks, race, '1st', 'w_speed')\n        try: \n            loser_speed = find_winner_loser(info, joks, race, 'last', 'l_speed')\n        except: # In some races there are several underdogs at once\n            loser_speed = find_winner_loser(info, joks, race, 'pre-last', 'l_speed')\n        avg_speed = info.groupby('completed_dist_round')['speed_mph'].mean()\\\n                        .reset_index().rename({'speed_mph': 'avg_speed'}, axis=1)\n        \n        w_carried = dict(zip(joks['program_number'], joks['weight_carried']))\n        odds_stat = dict(zip(joks['program_number'], joks['odds']))\n        \n        nh = info['num_horses'].values[0]\n        num_horses += [nh]*nh\n        r_dist += [joks['dist_furlong'].values[0]]*nh\n        rid += [joks['race_id'].values[0]]*nh\n        \n        positions = [[], []] # List with mode of position and last position for min-max scaling\n        \n        for horse in info['program_number'].unique():\n            all_h_info = info[info['program_number']==horse]\n            ## --- ##\n            h_info = all_h_info.groupby('completed_dist_round') \\\n                               .agg({'speed_mph': 'mean',\n                                     'dist_to_lead': 'mean'}).reset_index()\n            h_info = h_info.merge(winner_speed, on='completed_dist_round', how='left') \\\n                           .merge(loser_speed, on='completed_dist_round', how='left') \\\n                           .merge(avg_speed, on='completed_dist_round', how='left')\n            \n            cols = ['sdw', 'sdl', 'sda']\n            for col1, col2 in zip(h_info.columns[-3:], cols):\n                h_info[col2] = round(h_info['speed_mph']/h_info[col1]*100-100, 2)\n                \n            for list_, col in zip([avg_sdw, avg_sdl, avg_sda],\n                                  ['sdw', 'sdl', 'sda']):\n                list_.append(h_info[col].mean())\n            ## --- ##    \n            avg_dtl.append(all_h_info['dist_to_lead'].mean())\n            l_dtl.append(all_h_info['dist_to_lead'].values[-1])\n            j_wt.append(w_carried[horse])\n            odds.append(odds_stat[horse])\n            pn.append(horse)\n            \n            positions[0].append(all_h_info['position'].mode().values[0])\n            positions[1].append(all_h_info['position'].values[-1])\n            \n        for i, list_ in enumerate([mode_pos, l_pos]): # min-max scaling\n            pos = positions[i].copy()\n            list_ += [(x-min(pos))/(max(pos)-min(pos)) for x in pos]\n    \n    for list_, col in zip([rid, pn, avg_sdw, avg_sdl, avg_sda, \n                           avg_dtl, l_dtl, mode_pos, \n                           l_pos, odds, j_wt, r_dist, num_horses],\n                          new_df.columns):\n        new_df[col] = list_\n    return new_df\n\ndf1, df2, df3, df4, df5, df6, df7, df8, df9 = 0,0,0,0,0,0,0,0,0\n\ndef create_ml_df(dist):\n    df = full_df[full_df['completed_dist'].between(dist-10, dist)]\n    df['completed_dist_round'] = df['completed_dist'].apply(lambda x: math.ceil(x))\n    df = df[df['completed_dist_round']>0]\n    df = ml_preprocessing(df)\n    df = df.merge(jok_info[['race_id', 'program_number', 'final_place']], \n                  on=['race_id', 'program_number'], how='left')\n    arr = df['final_place']\n    arr = np.where(arr > 1, 0, 1)\n    df['final_place'] = arr\n    return df\n\ndf1 = create_ml_df(10)\ndf2 = create_ml_df(20)\ndf3 = create_ml_df(30)\ndf4 = create_ml_df(40)\ndf5 = create_ml_df(50)\ndf6 = create_ml_df(60)\ndf7 = create_ml_df(70)\ndf8 = create_ml_df(80)\ndf9 = create_ml_df(90)\n\ny = df1['final_place']\n\nFOLDS = 5\nseed = 228\n\n############\n# TRAINING #\n############\n\nbest_xgb_params = {\n            'max_depth': 6,\n            'learning_rate': 0.02,\n            'subsample': 0.6,\n            'colsample_bytree': 0.8,\n            'min_child_weight': 44,\n            'gamma': 4,\n            'lambda': 44,\n    \n            'seed': seed,\n            'objective': 'binary:logistic',\n            'eval_metric': 'auc'\n    } # Found with Optuna\n\nall_predictions, pred_results = [], []\n\ndef ml_train(ml_df, dist):\n    global jok_info\n    \n    X = ml_df.drop(['race_id', 'program_number', 'final_place'], axis=1)\n    importances, scores, predictions = [], [], pd.DataFrame()\n    \n    print(f'Start learning for {dist-10}-{dist}% of race data...')\n    k = StratifiedKFold(n_splits=FOLDS, random_state=seed, shuffle=True)\n    for fold, (train_idx, val_idx) in enumerate(k.split(X, y)):\n        dtrain = xgb.DMatrix(X.iloc[train_idx], label=y.iloc[train_idx])\n        dvalid = xgb.DMatrix(X.iloc[val_idx], label=y.iloc[val_idx])\n        watchlist = [(dtrain, 'train'), (dvalid, 'eval')]\n    \n        model = xgb.train(best_xgb_params, dtrain=dtrain, num_boost_round=3000,\n                          evals=watchlist, early_stopping_rounds=300, \n                          verbose_eval=False, maximize=True)\n    \n        imp = model.get_score(importance_type='weight')\n        imp_stat = pd.DataFrame({'feature': imp.keys(), \n                                 f'importance_{fold}': imp.values()})\n        importances.append(imp_stat)\n                \n        val_preds = model.predict(dvalid, iteration_range=(0, model.best_ntree_limit))\n        results = X.iloc[val_idx]\n        results['preds'] = val_preds\n        predictions = pd.concat([predictions, results[['preds']]], axis=0)\n        val_score = roc_auc_score(y.iloc[val_idx], val_preds)\n        scores.append(val_score)\n        print(f'--- FOLD {fold+1} AUC score - {round(val_score, 6)} | best iteration - {model.best_ntree_limit} ---')\n        \n    imp = importances[0].copy()\n    for i in range(1, FOLDS): \n        imp = imp.merge(importances[i], on='feature', how='left')\n    imp['importance'] = imp.iloc[:,1:].mean(axis=1)\n    imp = imp.sort_values('importance',ascending=False)\n    imp['imp%'] = imp['importance'] / imp['importance'].sum() * 100\n        \n    predictions = predictions.sort_index().rename({'preds': f'preds{dist}%'}, axis=1)\n    ml_df = ml_df.merge(predictions, left_index=True, right_index=True)\n    jok_info = jok_info.merge(ml_df[['race_id', 'program_number', f'preds{dist}%']], \n                              on=['race_id', 'program_number'], how='left')\n    \n    all_predictions.append(predictions[f'preds{dist}%'].tolist())\n    pred_results.append(round(np.mean(scores),3))\n    print('*'*45)\n    print(f'Final CV AUC score - {np.mean(scores)}\\n')\n    \n    return imp\n\nimp1 = ml_train(df1, 10)\nimp2 = ml_train(df2, 20)\nimp3 = ml_train(df3, 30)\nimp4 = ml_train(df4, 40)\nimp5 = ml_train(df5, 50)\nimp6 = ml_train(df6, 60)\nimp7 = ml_train(df7, 70)\nimp8 = ml_train(df8, 80)\nimp9 = ml_train(df9, 90)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"jupyter":{"source_hidden":true,"outputs_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T13:47:28.467083Z","iopub.execute_input":"2022-11-10T13:47:28.468154Z","iopub.status.idle":"2022-11-10T14:28:35.960431Z","shell.execute_reply.started":"2022-11-10T13:47:28.468090Z","shell.execute_reply":"2022-11-10T14:28:35.959101Z"},"collapsed":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig=go.Figure()\ncolors = ['#'+x for x in ['c71c1c', '344d90', '344d90', \n                          '344d90', '344d90', '344d90', \n                          '344d90', '344d90', '34675c']]\nfig.add_trace(go.Scatter(x=np.linspace(0,1,11), y=np.linspace(0,1,11), \n                         name='Random Chance', mode='lines', showlegend=False,\n                         line=dict(color=\"black\", width=1, dash=\"dot\")))\n    \nfor i, dist, res in zip([0,1,2,3,4,5,6,7,8],\n                        [10,20,30,40,50,60,70,80,90],\n                        pred_results):\n    pred = all_predictions[i]\n    fpr, tpr, _ = roc_curve(y, pred)\n    roc_auc = auc(fpr, tpr)\n    name = (f'{dist-10}-{dist}% | AUC - {res}' if i != 0 else f'  {dist-10}-{dist}% | AUC - {res}')\n    fig.add_trace(go.Scatter(x=fpr, y=tpr, line=dict(color=colors[i], width=(3 if i in [0,8] else 1)), \n                             hovertemplate=False,\n                             visible=(\"legendonly\" if i not in [0,4,8] else True),\n                             name=name))\n\nfig.update_layout(width=800, height=700, plot_bgcolor='white',\n                  title=dict(text=\"<b>AUC results of determination of the winner<br>depending on completed distance</b>\",\n                         font=dict(size=22), x=0.1, xanchor='left'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  legend=dict(title=dict(text='      <b>Distance | Result</b>', font=dict(size=15)),\n                              bordercolor=\"#707070\", borderwidth=0.7, \n                              bgcolor='#f7f7f7', y=0.5),\n                  hoverlabel=dict(font_size=12, font_family=\"Calibri\"),\n                  hovermode=False)\n\nfig.update_xaxes(title_text='<b>False Positive Rate</b>', showline=True, \n                 linecolor='black', linewidth=1, tickfont_size=12, \n                 ticks=\"outside\", tickson=\"boundaries\", ticklen=3, range = [0,1.05])\nfig.update_yaxes(title_text='<b>True Positive Rate</b>', showline=True, \n                 linecolor='black', linewidth=1, tickfont_size=12, \n                 ticks=\"outside\", tickson=\"boundaries\", ticklen=3, range = [0,1.05])\n\nfig.add_annotation(x=0.5, y=0.48, text=\"Random chance\", \n                   font=dict(size=12), showarrow=False, textangle=-45)\n    \nfig.show()\n\n###################################################\n\nfeatures = {'odds': 'Odds',\n 'avg_sda': 'Average speed deviation from average speed of participants',\n 'avg_dtl': 'Average distance to the leader',\n 'avg_sdl': 'Average speed deviation from speed of underdog',\n 'l_dtl': 'Last observed distance to the leader',\n 'avg_sdw': 'Average speed deviation from speed of expected winner',\n 'l_pos': 'Last observed position',\n 'mode_pos': 'Mode of position',\n 'r_dist': 'Race distance',\n 'j_wt': 'Weight carried',\n 'num_horses': 'Number of participants'}\n\nbuttons=[]\nfig = go.Figure()\n\nfor i, imp, label in zip([0,1,2,3,4,5,6,7,8], \n                         [imp1, imp2, imp3, \n                          imp4, imp5, imp6, \n                          imp7, imp8, imp9],\n                         ['0-10%', '10-20%', '20-30%',\n                          '30-40%', '40-50%', '50-60%',\n                          '60-70%', '70-80%', '80-90%',]):\n    fig.add_trace(go.Bar(x=imp['imp%'], y=imp['feature'],\n                         marker=dict(color='#34675c',\n                         line=dict(width=0.5, color='black')),\n                         hovertemplate='<b>%{x:.2f}%</b>'+ \"<extra></extra>\", \n                         showlegend=False,\n                         visible=(True if i==0 else False),\n                         orientation='h'))\n    \n    visibility=[False]*9\n    visibility[i]=True\n    button=dict(label=label,\n                method=\"update\",\n                args=[{\"visible\": visibility}])\n    buttons.append(button)\n\nfig.update_layout(width=800, height=600, plot_bgcolor='white',\n                  title=dict(text=\"<b>The importance of information that<br>can determine the winner</b>\",\n                         font=dict(size=22), x=0.395, xanchor='left'),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=14, font_family=\"Calibri\", bgcolor=\"white\"),\n                  barmode='group', bargap=0.2, bargroupgap=0.3,\n                  updatemenus=[dict(active=0, type=\"dropdown\",\n                                    buttons=buttons, xanchor='left',\n                                    yanchor='bottom', y=0.905, x=0.97)])\n\nfig.update_xaxes(showline=False, tickfont_size=12, ticks=\"outside\", \n                 tickson=\"boundaries\", ticklen=6, ticksuffix='%')\n\nfig.update_yaxes(showline=True, linecolor='#f5f2f2', linewidth=2, \n                 tickfont_size=12, ticks=\"outside\", tickcolor='white', \n                 ticklen=10, autorange=\"reversed\",\n                 tickvals=list(features.keys()),\n                 ticktext=list(features.values()))\n\nfig.add_annotation(x=1.135, y=1.03, text=\"<b>Distance</b>\", \n                   xref=\"paper\", yref=\"paper\",\n                   font=dict(size=14), showarrow=False)\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T14:28:35.962234Z","iopub.execute_input":"2022-11-10T14:28:35.963149Z","iopub.status.idle":"2022-11-10T14:28:36.239068Z","shell.execute_reply.started":"2022-11-10T14:28:35.963084Z","shell.execute_reply":"2022-11-10T14:28:36.237942Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nThe better the classifier separates two classes (winner and loser) - the larger the AUC (area under curve). It may seem that \"Wow, having data on the start of the race, it is already possible to determine the possible winner with not bad accuracy\". No, it's not. A lot depends on odds. But do the odds determine the expected position all the time of the race? No! After 60% of the distance, odds are no longer the dominant factor. The most important factors in determining the expected position throughout the race are average speed deviation from average speed of participants and distance to the leader. It is also interesting that in the first 40% of the distance, much depends on how much the speed of the participant differs from the speed of the underdog.\n</p>\n<p style=\"text-align:justify;\">\nBut why XGB? Not LGB or CatBoost? In fact, the difference between the values predicted by them is small (if we talk about expected position, not probability of winning). I also tested LGB using categorical features (track condition, race type), but there were no drastic changes in the results. Why 10%? Not 1% or 5%? To increase the frequency of receipt of the expected results, you can choose any distance interval, but if you choose an interval of 1%, then you will have to train 100 models. Why is this particular approach to model development chosen? This model is just an example, the beginning of what can be improved with an increase in the amount of data.\n</p>\n<p style=\"text-align:justify;\">\nLet's return to the analysis and take a look at the race we are already familiar with, which I told you about earlier, and see how much the expected position of all participants changes:\n</p>\n<div class=\"alert alert-block alert-info\">\n<b>Important:</b> The expected position is calculated after the last participant has completed 10% of the distance. Information is updated every 10% of the distance. When you hover the cursor over the markers on the chart, you can see detailed information.\n</div>","metadata":{}},{"cell_type":"code","source":"exp = full_df[full_df['race_id']==32]\nexp['completed_dist_round'] = exp['completed_dist'].apply(lambda x: math.ceil(x))\nexp = exp.merge(jok_info[jok_info['race_id']==32][['program_number', 'final_place']],\n                on='program_number', how='left')\n\n# Supercomplex connection of predictions with tracking data. The deadline of the competition was close :)\ncomp_dist = exp.groupby('completed_dist_round')['time_sec'].max().reset_index()\ncomp_dist = comp_dist[comp_dist['completed_dist_round']%10==0]\ncomp_dist['show'] = comp_dist['time_sec']/0.25\ncomp_dist['show_rows'] = comp_dist['show'].diff()\npreds_show = list(comp_dist.drop(0)['show_rows'])\npreds_show[0] = 35\npreds_show[-1] = 30\n\nfind_jok_preds = {x: x*10 for x in [1,2,3,4,5,6,7,8,9]}\nrace = jok_info[jok_info['race_id']==32]\npreds = []\nfor horse in exp['program_number'].unique():\n    info = race[race['program_number']==horse]\n    for i, show_row in enumerate(preds_show):\n        if i == 0:\n            for _ in range(int(show_row)):\n                preds.append(0.0)\n        else:\n            pred = info[f'preds{find_jok_preds[i]}%'].values[0]\n            for _ in range(int(show_row)):\n                preds.append(pred)\nexp['preds_win'] = preds\nexp['preds_win'] = round(exp['preds_win']*100,2)\n                \ndef expected_position(df):\n    res = pd.DataFrame()\n    for sec in df['time_sec'].unique():\n        info = df[df['time_sec']==sec]\n        info['exp_pos'] = info['preds_win'].rank(method='min', ascending=False)\n        res = pd.concat([res, info[['program_number', 'time_sec', 'exp_pos']]], ignore_index=True)\n    df = df.merge(res, on=['program_number', 'time_sec'], how='left')\n    return df\n\nexp = expected_position(exp)\n\nexp['text_scatter'] = 'Completed distance</b>: ' + exp['completed_dist'].astype('str') + '%' + '<br>' + \\\n    '<b>Position</b>: ' + exp['position'].astype('str') + '<br>' + '<br>' + \\\n    '<b>Expected finish position</b>: ' + exp['exp_pos'].astype('int').astype('str') + '<br>' + \\\n    '<b>Chances to win</b>: ' + exp['preds_win'].astype('str') + '%' + '<br>' + \\\n    '<b>Actual finish position</b>: ' + exp['final_place'].astype('str')\n\ntime = sorted(exp['time_sec'].unique().tolist())\n\nnames = jok_info[jok_info['race_id']==32][['program_number', 'jockey']]\nnames = dict(zip(names['program_number'], names['jockey']))\ncolors = {h: '#'+x for h, x in zip(names.keys(), ['c71c1c', '344d90', \n                                                  'ebc91e', '34675c',\n                                                  '5e2929', '8e2fd6'])}\n\nh_ids = exp['program_number'].unique().tolist()\nfig_dict = {\"data\": [], \"layout\": {},\"frames\": []}\n\n# Layout\nlayout = go.Layout(updatemenus=[{\"buttons\": [\n            {\n                \"args\": [None, {\"frame\": {\"duration\": 50, \"redraw\": False},\n                                \"fromcurrent\": True, \"transition\": {\"duration\": 1,\n                                                                    \"easing\": \"quadratic-in-out\"}}],\n                \"label\": \"Play\", \"method\": \"animate\"\n            },\n            {\n                \"args\": [[None], {\"frame\": {\"duration\": 0, \"redraw\": False},\n                                  \"mode\": \"immediate\",\n                                  \"transition\": {\"duration\": 0}}],\n                \"label\": \"Pause\", \"method\": \"animate\"\n            }\n        ],\n        \"direction\": \"left\", \"pad\": {\"r\": 10, \"t\": 50},\n        \"showactive\": False, \"type\": \"buttons\",\n        \"x\": 0.1, \"xanchor\": \"right\",\n        \"y\": -0.05, \"yanchor\": \"top\"\n    }]\n)\n\nfig_dict[\"layout\"] = layout\n\n# Slider\nsliders_dict = {\n    \"active\": 0,\n    \"yanchor\": \"top\",\n    \"xanchor\": \"left\",\n    \"currentvalue\": {\n        \"font\": {\"size\": 17},\n        \"prefix\": \"seconds from start: \",\n        \"visible\": True,\n        \"xanchor\": \"right\"\n    },\n    \"transition\": {\"duration\": 50, \"easing\": \"bounce-in-out\"},\n    \"pad\": {\"b\": 10, \"t\": 50},\n    \"len\": 0.9,\n    \"x\": 0.1,\n    \"y\": 0,\n    \"steps\": []\n}\n\n# Data\nfor horse in h_ids:\n    info = exp[exp['program_number']==horse].iloc[[0]]\n    fig_dict[\"data\"].append(go.Scatter(x=info['longitude'], y=info['latitude'], name=names[horse],\n                                       mode='markers', \n                                       marker=dict(size=8, opacity=0.8, color=colors[horse],\n                                                   line=dict(width=0.5, color='black')),\n                                       text=info['text_scatter'], hovertemplate=\"<b>%{text}</b>\"))\n    \n# Frames\nfor i in time:\n    frame = {\"data\": [], \"name\": str(i), \"layout\": {}}\n    for horse in h_ids:\n        info = exp[(exp['time_sec']==i)&(exp['program_number']==horse)]\n        frame[\"data\"].append(go.Scatter(x=info['longitude'], y=info['latitude'], name=names[horse],\n                                        mode='markers', \n                                        marker=dict(size=8, opacity=0.8, color=colors[horse],\n                                                   line=dict(width=0.5, color='black')),\n                                        text=info['text_scatter'], hovertemplate=\"<b>%{text}</b>\"))\n    \n    info = exp[exp['time_sec']==i]\n    frame[\"layout\"][\"xaxis\"] = {\"range\": [info['longitude'].min()-0.0005, info['longitude'].max()+0.0005]}\n    frame[\"layout\"][\"yaxis\"] = {\"range\": [info['latitude'].min()-0.0005, info['latitude'].max()+0.0005]}\n    fig_dict[\"frames\"].append(frame)\n    \n    slider_step = {\"args\": [\n        [i],\n        {\"frame\": {\"duration\": 10, \"redraw\": False},\n         \"mode\": \"immediate\",\n         \"transition\": {\"duration\": 10}}\n    ],\n        \"label\": i,\n        \"method\": \"animate\",\n        }\n    sliders_dict[\"steps\"].append(slider_step)\n        \nfig_dict[\"layout\"][\"sliders\"] = [sliders_dict]    \n\nfig = go.Figure(fig_dict)\n\ninfo = exp.query(\"time_sec == 0.25\")\nfig.update_layout(width=800, height=800, plot_bgcolor='white', \n                   xaxis={\"range\": [info['longitude'].min()-0.0005, info['longitude'].max()+0.0005]}, \n                   yaxis={\"range\": [info['latitude'].min()-0.0005, info['latitude'].max()+0.0005]},\n                   title={\"text\": \"<b>Track: AQU | Date: 2019-01-06 | Race: №7</b>\", \"font\": {\"size\": 22}, \"x\": 0.06}, \n                   legend={\"title_text\": \"                <b>Jockey</b>\", \"title\": {\"font\": {\"size\": 16}},\n                           \"bordercolor\": \"#707070\", \"borderwidth\": 0.7, \"bgcolor\": '#f7f7f7', 'y':0.5},\n                   font_family=\"Calibri\", font_color=\"#303030\",\n                   hoverlabel=dict(font_size=13, font_family=\"Calibri\"))\n\nfig.update_yaxes(title_text='latitude', showline=False, linecolor='#f5f2f2', \n                 showgrid=True, gridwidth=1, gridcolor='#f5f2f2',\n                 linewidth=1, tickfont_size=10)\n\nfig.update_xaxes(title_text='longitude', showline=False, linecolor='#f5f2f2', \n                 showgrid=True, gridwidth=1, gridcolor='#f5f2f2',\n                 linewidth=1, tickfont_size=10)\n                  \nfig.show()\n\n###############################\n\nj_preds = jok_info[jok_info['race_id']==32][['jockey']+jok_info.columns[-9:].tolist()].set_index('jockey').T.reset_index()\nj_preds['index'] = j_preds['index'].apply(lambda x: x[-3:-1])\nfor jockey in j_preds.columns[1:]:\n    j_preds[jockey] = round(j_preds[jockey]*100,4)\n    \nj_exp_pos = j_preds[j_preds.columns[1:]].rank(axis=1, ascending=False, method='min') \\\n            .merge(j_preds[['index']], left_index=True, right_index=True)\nfor col in j_exp_pos.columns:\n    j_exp_pos[col] = j_exp_pos[col].astype('int')\n    \n###############################\ncolors = {h: '#'+x for h, x in zip(j_preds.columns[1:], \n                                   ['c71c1c', '344d90', \n                                    'ebc91e', '34675c',\n                                    '5e2929', '8e2fd6'])}\n\nfig = make_subplots(rows=2, cols=1, \n                    vertical_spacing=0.2,\n                    subplot_titles=('<b>Probability of winning</b>',\n                                    '<b>Expected position</b>'))\n\nfor jockey in j_preds.columns[1:]:\n    fig.add_trace(go.Scatter(x=j_preds['index'], y=j_preds[jockey],\n                             name=jockey,\n                             marker=dict(size=8, color=colors[jockey],  \n                                         line=dict(width=0.5, color='black')),\n                             line=dict(width=3, color=colors[jockey]),\n                             legendgroup=jockey,\n                             hovertemplate='<b>%{y:.2f}%</b>', \n                             visible=('legendonly' if jockey not in ['Joey R. Martinez',\n                                                                     'Junior Alvarado',\n                                                                     'Reylu Gutierrez'] else True),\n                             showlegend=True), row=1, col=1)\n    \nfor jockey in j_exp_pos.columns[:-1]:\n    fig.add_trace(go.Scatter(x=j_exp_pos['index'], y=j_exp_pos[jockey],\n                             name=jockey,\n                             marker=dict(size=8, color=colors[jockey],  \n                                         line=dict(width=0.5, color='black')),\n                             line=dict(width=3, color=colors[jockey]),\n                             legendgroup=jockey,\n                             hovertemplate='<b>%{y}</b>',\n                             visible=('legendonly' if jockey not in ['Joey R. Martinez',\n                                                                     'Junior Alvarado',\n                                                                     'Reylu Gutierrez'] else True),\n                             showlegend=False), row=2, col=1)\n    \nfig.add_hline(y=0, line=dict(width=0.5, color='#b5b5b5'), row=1, col=1)\n\nfig.update_layout(width=800, height=900, plot_bgcolor='white',\n                  title=dict(text=\"<b>Model predictions</b>\",\n                         font=dict(size=22), x=0.104, y=0.95),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=13),\n                  barmode='group', bargap=0.2, bargroupgap=0.3, \n                  hovermode='x',\n                  legend={\"title_text\": \"                <b>Jockey</b>\", \"title\": {\"font\": {\"size\": 16}},\n                           \"bordercolor\": \"#707070\", \"borderwidth\": 0.7, \"bgcolor\": '#f7f7f7', 'y':0.5},)\n\nfig.update_xaxes(title_text='<b>Completed distance, %</b>', tickfont_size=13, \n                 tickvals=[10,20,30,40,50,60,70,80,90], ticks=\"outside\", tickcolor='black', \n                 ticklen=3, tickson=\"boundaries\", row=2, col=1)\nfig.update_xaxes(showline=False, showticklabels=False, row=1, col=1)\n\nfig.update_yaxes(title_text='<b>Probability of winning, %</b>', showline=False,\n                 tickfont_size=13, ticks=\"outside\", tickcolor='black', \n                 ticklen=3, tickson=\"boundaries\", row=1, col=1)\n\nfig.update_yaxes(title_text='<b>Expected position</b>', showline=False,\n                 tickfont_size=13, ticks=\"outside\", tickcolor='black', \n                 ticklen=3, tickson=\"boundaries\", autorange='reversed', row=2, col=1)\n\nfig.layout.annotations[0].update(x=0.16, font_size=18, font_family='Calibri')\nfig.layout.annotations[1].update(x=0.13, font_size=18, font_family='Calibri')\n\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T14:28:41.175260Z","iopub.execute_input":"2022-11-10T14:28:41.175622Z","iopub.status.idle":"2022-11-10T14:28:46.110925Z","shell.execute_reply.started":"2022-11-10T14:28:41.175592Z","shell.execute_reply":"2022-11-10T14:28:46.109937Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nWatching this race has become even more interesting. As mentioned earlier, no model could have predicted such an outcome of the race. When Joey R. Martinez reached the finish straight, the probability of his victory was <b>0.51%</b>! And even with data on 90% of the race and the fact of strong acceleration, his probability of winning was increase up to <b>10%</b>. The first time I paid attention only to Martinez and Gutierrez, but now I want to pay attention also to Junior Alvarado with the 2nd expected position, who held this position throughout the race and finished <b>5th</b>! As for the other participants, from the beginning to the end of the race, the probability of their victory was ~4%.\n</p>\n<p style=\"text-align:justify;\">\nThis race is of sincere interest. But our goal is in the overall evaluate, so let's take a look at the results of all the races at once.\n</p>","metadata":{}},{"cell_type":"code","source":"def all_expected_position(df):\n    res = pd.DataFrame()\n    for race_id in df['race_id'].unique():\n        race = jok_info[jok_info['race_id']==race_id]\n        race = race[['race_id', 'program_number', 'fav_und', \n                     'act_pos', 'final_place']+race.columns[-9:].tolist()]\n        for col in race.columns[-9:]:\n            race[col] = race[col].rank(method='min', ascending=False)\n            race[col] = race['final_place'] == race[col]\n        res = pd.concat([res, race], ignore_index=True)\n    return res\n\nfinal = all_expected_position(jok_info.query('`preds10%` == `preds10%`'))\n\ndef pos_stat(df, position):\n    res_df = pd.DataFrame()\n    dist, res = [10,20,30,40,50,60,70,80,90], []\n    for d in dist:\n        res.append(len(df[(df['act_pos']==position)&\n                          (df[f'preds{d}%']==1)])/df['race_id'].nunique() * 100)\n    res_df['dist'], res_df[f'true_pred_{position}'] = dist, res\n    return res_df\n\ndef all_pos_stat(df):\n    new_df = pd.DataFrame(columns=['dist'])\n    new_df['dist'] = [10,20,30,40,50,60,70,80,90]\n    for pos in ['1st', '2nd', 'pre-last', 'last']:\n        new_df = new_df.merge(pos_stat(df, pos))\n    return new_df \n        \nraces_true = all_pos_stat(final)  \n\nfig = go.Figure()\nfor pos, color in zip(['1st', '2nd', 'pre-last', 'last'],\n                      ['34675c', '344d90', 'ebc91e', 'c71c1c']):\n    fig.add_trace(go.Scatter(x=races_true['dist'], y=races_true[f'true_pred_{pos}'],\n                             name=pos,\n                             marker=dict(size=8, color='#'+color,  \n                                         line=dict(width=0.5, color='black')),\n                             line=dict(width=3, color='#'+color),\n                             hovertemplate='<b>%{y:.2f}%</b>',\n                             visible=('legendonly' if pos not in ['1st',\n                                                                  'last'] else True),\n                             showlegend=True))\n    \nfig.add_hline(y=0, line=dict(width=0.5, color='#b5b5b5'), row=1, col=1)\n\nfig.update_layout(width=800, height=600, plot_bgcolor='white',\n                  title=dict(text=\"<b>Accuracy of predictions in all races</b>\",\n                         font=dict(size=22), x=0.104, y=0.95),\n                  font_family=\"Calibri\", font_color=\"#303030\",\n                  hoverlabel=dict(font_size=13),\n                  hovermode='x',\n                  legend={\"title_text\": \"<b>Finish place</b>\", \"title\": {\"font\": {\"size\": 16}},\n                           \"bordercolor\": \"#707070\", \"borderwidth\": 0.7, \"bgcolor\": '#f7f7f7', 'y':0.5})\n\nfig.update_xaxes(title_text='<b>Completed distance, %</b>', tickfont_size=13, \n                 tickvals=[10,20,30,40,50,60,70,80,90], ticks=\"outside\", tickcolor='black', \n                 ticklen=3, tickson=\"boundaries\")\n\nfig.update_yaxes(title_text='<b>Accuracy, %</b>', showline=False,\n                 tickfont_size=13, ticks=\"outside\", tickcolor='black', \n                 ticklen=3, tickson=\"boundaries\")\nfig.show()","metadata":{"_kg_hide-input":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2022-11-10T14:28:46.112558Z","iopub.execute_input":"2022-11-10T14:28:46.112867Z","iopub.status.idle":"2022-11-10T14:29:05.789173Z","shell.execute_reply.started":"2022-11-10T14:28:46.112840Z","shell.execute_reply":"2022-11-10T14:29:05.787575Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nEven with data on the completion of 80-90% of the distance, the model is wrong in determining the winner in <b>22%</b> of races. This indicates a high unpredictability in some races even before the finish line. In <b>21.6%</b> of the races, the prediction of the winner and in <b>10.8%</b> of races, the prediction of the loser was correct throughout the race! In <b>13.2%</b> of the races, the winners became the participants who did not have a dominant probability of winning throughout the race and also there were <b>23</b> races, where underdogs with low chances of winning during the race became winners, including the race that was considered in this work.\n</p>\n<p style=\"text-align:justify;\">\nIn general, the results can not be called impressive and half of the distance, the winner is essentially determined by odds, but don't forget about the availability of only data of the movement of participants. \n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>Conclusion. How to dispel the fog of the future?</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\nWith only physical movement data available, the true causes of deceleration and acceleration can only be based on assumptions. To find the best overall solution for all races, more data is needed. It's not just about quantity, but also about completely new information.\n</p>    \n<p style=\"text-align:justify;\">\nLet's start with <b>the model of determining the expected position before the start of the race</b>. Of course, it is possible to build a model based on the statistics of victories, defeats, taking the top 3, etc., but this does not carry anything innovative. It is important to take into account not only the final race results of each jockey and horse, but also the intermediate results of previous races: time in the leading and last positions, speed depending on the speed of competitors and types of tracks, indicators of the health of the horse, training results, the number of mistakes made by jockeys and the optimality of decisions made, etc. \n</p>   \n<p style=\"text-align:justify;\">\nIf we talk about <b>the model for determining the probability of winning during the race</b>, then the greatest attention should be paid to the horse's health indicators. It is impossible to predict a jockey's mistake, but a sharp deterioration in the state of health or a decline in the strength of the horse is quite possible. I am convinced that the time is coming when there will be added a specialist to the team, who will monitor the horse's health indicators and inform the jockey about the optimality of decisions during the race.\n</p>\n<p style=\"text-align:justify;\">\nWe had data from 2019 and are already on the eve of 2023. I am sure that in three years there has been collected more data that will allow horse racing to reach a new level!\n</p>","metadata":{}},{"cell_type":"markdown","source":"# <div style=\"background-color:#34675c;text-align:center;color:white;font-size:150%;font-family:Calibri;border-radius:10px\"> <b>References</b></div>","metadata":{}},{"cell_type":"markdown","source":"<p style=\"text-align:justify;\">\n<ol>\n    <li><a href=\"https://www.usgs.gov/faqs/what-does-term-utm-mean-utm-better-or-more-accurate-latitudelongitude\"><b>What does the term UTM mean? Is UTM better or more accurate than latitude/longitude?</b></a></li>\n    <li><a href=\"https://www.liveabout.com/understanding-the-types-and-classes-of-horse-races-1880414\"><b>Understanding the Types and Classes of Horse Races</b></a></li>\n    <li><a href=\"https://edge.twinspires.com/racing/an-introduction-to-track-conditions-and-all-the-related-lingo/\"><b>An introduction to track conditions</b></a></li>\n    <li><a href=\"https://training.arioneo.com/en/blog-racing-horse-racing-speed-analysis/\"><b>Horse racing speed analysis: what should you analyse?</b></a></li>\n    <li><a href=\"https://training.arioneo.com/en/blog-how-can-data-predict-the-performance-of-your-racehorses/\"><b>Is training data predictive of race performance?</b></a>\n    <li><a href=\"https://training.arioneo.com/en/blog-exertional-rhabdomyolysis-in-horses/\"><b>Exertional rhabdomyolysis in horses</b></a></li>\n</ol>\n</p>","metadata":{}}]}