Visualising Strava Race Evaluation. Two New Graphs That Examine Runners on… | by Juan Hernanz | Aug, 2024

[ad_1]

We’re prepared now to play with the information to create the visualisations.

Challenges:

To acquire the information wanted for the visuals my first instinct was: have a look at the cumulative distance column for each runner, establish when a lap distance was accomplished (1000, 2000, 3000, and so on.) by every of them and do the variations of timestamps.

That algorithm appears to be like easy, and may work, but it surely had some limitations that I wanted to deal with:

  1. Actual lap distances are sometimes accomplished in between two knowledge factors registered. To be extra correct I needed to do interpolation of each place and time.
  2. Because of distinction within the precision of units, there could be misalignments throughout runners. The commonest is when a runner’s lap notification beeps earlier than one other one even when they’ve been collectively the entire monitor. To minimise this I made a decision to use the reference runner to set the place marks for each lap within the monitor. The time distinction might be calculated when different runners cross these marks (despite the fact that their cumulative distance is forward or behind the lap). That is extra near the fact of the race: if somebody crosses a degree earlier than, they’re forward (regardless the cumulative distance of their gadget)
  3. With the earlier level comes one other downside: the latitude and longitude of a reference mark may by no means be precisely registered on the opposite runners’ knowledge. I used Nearest Neighbours to search out the closest datapoint by way of place.
  4. Lastly, Nearest Neighbours may carry mistaken datapoints if the monitor crosses the identical positions at completely different moments in time. So the inhabitants the place the Nearest Neighbours will search for the very best match must be lowered to a smaller group of candidates. I outlined a window measurement of 20 datapoints across the goal distance (distance_cum).

Algorithm

With all of the earlier limitations in thoughts, the algorithm ought to be as follows:

1. Select the reference and a lap distance (default= 1km)

2. Utilizing the reference knowledge, establish the place and the second each lap was accomplished: the reference marks.

3. Go to different runner’s knowledge and establish the moments they crossed these place marks. Then calculate the distinction in time of each runners crossing the marks. Lastly the delta of this time distinction to characterize the evolution of the hole.

Code Instance

1. Select the reference and a lap distance (default= 1km)

  • Juan would be the reference (juan_df) on the examples.
  • The opposite runners might be Pedro (pedro_df ) and Jimena (jimena_df).
  • Lap distance might be 1000 metres

2. Create interpolate_laps(): perform that finds or interpolates the precise level for every accomplished lap and return it in a brand new dataframe. The inferpolation is completed with the perform: interpolate_value() that was additionally created.

## Operate: interpolate_value()

Enter:
- begin: The beginning worth.
- finish: The ending worth.
- fraction: A price between 0 and 1 that represents the place between
the beginning and finish values the place the interpolation ought to happen.
Return:
- The interpolated worth that lies between the begin and finish values
on the specified fraction.

def interpolate_value(begin, finish, fraction):
return begin + (finish - begin) * fraction
## Operate: interpolate_laps()

Enter:
- track_df: dataframe with monitor knowledge.
- lap_distance: metres per lap (default 1000)
Return:
- track_laps: dataframe with lap metrics. As many rows as laps recognized.

def interpolate_laps(track_df , lap_distance = 1000):
#### 1. Initialise track_laps with the primary row of track_df
track_laps = track_df.loc[0][['latitude','longitude','elevation','date_time','distance_cum']].copy()

# Set distance_cum = 0
track_laps[['distance_cum']] = 0

# Transpose dataframe
track_laps = pd.DataFrame(track_laps)
track_laps = track_laps.transpose()

#### 2. Calculate number_of_laps = Whole Distance / lap_distance
number_of_laps = track_df['distance_cum'].max()//lap_distance

#### 3. For every lap i from 1 to number_of_laps:
for i in vary(1,int(number_of_laps+1),1):

# a. Calculate target_distance = i * lap_distance
target_distance = i*lap_distance

# b. Discover first_crossing_index the place track_df['distance_cum'] > target_distance
first_crossing_index = (track_df['distance_cum'] > target_distance).idxmax()

# c. If match is precisely the lap distance, copy that row
if (track_df.loc[first_crossing_index]['distance_cum'] == target_distance):
new_row = track_df.loc[first_crossing_index][['latitude','longitude','elevation','date_time','distance_cum']]

# Else: Create new_row with interpolated values, copy that row.
else:

fraction = (target_distance - track_df.loc[first_crossing_index-1, 'distance_cum']) / (track_df.loc[first_crossing_index, 'distance_cum'] - track_df.loc[first_crossing_index-1, 'distance_cum'])

# Create the brand new row
new_row = pd.Collection({
'latitude': interpolate_value(track_df.loc[first_crossing_index-1, 'latitude'], track_df.loc[first_crossing_index, 'latitude'], fraction),
'longitude': interpolate_value(track_df.loc[first_crossing_index-1, 'longitude'], track_df.loc[first_crossing_index, 'longitude'], fraction),
'elevation': interpolate_value(track_df.loc[first_crossing_index-1, 'elevation'], track_df.loc[first_crossing_index, 'elevation'], fraction),
'date_time': track_df.loc[first_crossing_index-1, 'date_time'] + (track_df.loc[first_crossing_index, 'date_time'] - track_df.loc[first_crossing_index-1, 'date_time']) * fraction,
'distance_cum': target_distance
}, title=f'lap_{i}')

# d. Add the brand new row to the dataframe that shops the laps
new_row_df = pd.DataFrame(new_row)
new_row_df = new_row_df.transpose()

track_laps = pd.concat([track_laps,new_row_df])

#### 4. Convert date_time to datetime format and take away timezone
track_laps['date_time'] = pd.to_datetime(track_laps['date_time'], format='%Y-%m-%d %H:%M:%S.%fpercentz')
track_laps['date_time'] = track_laps['date_time'].dt.tz_localize(None)

#### 5. Calculate seconds_diff between consecutive rows in track_laps
track_laps['seconds_diff'] = track_laps['date_time'].diff()

return track_laps

Making use of the interpolate perform to the reference dataframe will generate the next dataframe:

juan_laps = interpolate_laps(juan_df , lap_distance=1000)
Dataframe with the lap metrics on account of interpolation. Picture by Writer.

Be aware because it was a 10k race, 10 laps of 1000m has been recognized (see column distance_cum). The column seconds_diff has the time per lap. The remainder of the columns (latitude, longitude, elevation and date_time) mark the place and time for every lap of the reference as the results of interpolation.

3. To calculate the time gaps between the reference and the opposite runners I created the perform gap_to_reference()

## Helper Capabilities:
- get_seconds(): Convert timedelta to whole seconds
- format_timedelta(): Format timedelta as a string (e.g., "+01:23" or "-00:45")
# Convert timedelta to whole seconds
def get_seconds(td):
# Convert to whole seconds
total_seconds = td.total_seconds()

return total_seconds

# Format timedelta as a string (e.g., "+01:23" or "-00:45")
def format_timedelta(td):
# Convert to whole seconds
total_seconds = td.total_seconds()

# Decide signal
signal = '+' if total_seconds >= 0 else '-'

# Take absolute worth for calculation
total_seconds = abs(total_seconds)

# Calculate minutes and remaining seconds
minutes = int(total_seconds // 60)
seconds = int(total_seconds % 60)

# Format the string
return f"{signal}{minutes:02d}:{seconds:02d}"

## Operate: gap_to_reference()

Enter:
- laps_dict: dictionary containing the df_laps for all of the runnners' names
- df_dict: dictionary containing the track_df for all of the runnners' names
- reference_name: title of the reference
Return:
- matches: processed knowledge with time variations.


def gap_to_reference(laps_dict, df_dict, reference_name):
#### 1. Get the reference's lap knowledge from laps_dict
matches = laps_dict[reference_name][['latitude','longitude','date_time','distance_cum']]

#### 2. For every racer (title) and their knowledge (df) in df_dict:
for title, df in df_dict.gadgets():

# If racer is the reference:
if title == reference_name:

# Set time distinction to zero for all laps
for lap, row in matches.iterrows():
matches.loc[lap,f'seconds_to_reference_{reference_name}'] = 0

# If racer shouldn't be the reference:
if title != reference_name:

# a. For every lap discover the closest level in racer's knowledge based mostly on lat, lon.
for lap, row in matches.iterrows():

# Step 1: set the place and lap distance from the reference
target_coordinates = matches.loc[lap][['latitude', 'longitude']].values
target_distance = matches.loc[lap]['distance_cum']

# Step 2: discover the datapoint that might be within the centre of the window
first_crossing_index = (df_dict[name]['distance_cum'] > target_distance).idxmax()

# Step 3: choose the 20 candidate datapoints to search for the match
window_size = 20
window_sample = df_dict[name].loc[first_crossing_index-(window_size//2):first_crossing_index+(window_size//2)]
candidates = window_sample[['latitude', 'longitude']].values

# Step 4: get the closest match utilizing the coordinates
nn = NearestNeighbors(n_neighbors=1, metric='euclidean')
nn.match(candidates)
distance, indice = nn.kneighbors([target_coordinates])

nearest_timestamp = window_sample.iloc[indice.flatten()]['date_time'].values
nearest_distance_cum = window_sample.iloc[indice.flatten()]['distance_cum'].values
euclidean_distance = distance

matches.loc[lap,f'nearest_timestamp_{name}'] = nearest_timestamp[0]
matches.loc[lap,f'nearest_distance_cum_{name}'] = nearest_distance_cum[0]
matches.loc[lap,f'euclidean_distance_{name}'] = euclidean_distance

# b. Calculate time distinction between racer and reference at this level
matches[f'time_to_ref_{name}'] = matches[f'nearest_timestamp_{name}'] - matches['date_time']

# c. Retailer time distinction and different related knowledge
matches[f'time_to_ref_diff_{name}'] = matches[f'time_to_ref_{name}'].diff()
matches[f'time_to_ref_diff_{name}'] = matches[f'time_to_ref_diff_{name}'].fillna(pd.Timedelta(seconds=0))

# d. Format knowledge utilizing helper capabilities
matches[f'lap_difference_seconds_{name}'] = matches[f'time_to_ref_diff_{name}'].apply(get_seconds)
matches[f'lap_difference_formatted_{name}'] = matches[f'time_to_ref_diff_{name}'].apply(format_timedelta)

matches[f'seconds_to_reference_{name}'] = matches[f'time_to_ref_{name}'].apply(get_seconds)
matches[f'time_to_reference_formatted_{name}'] = matches[f'time_to_ref_{name}'].apply(format_timedelta)

#### 3. Return processed knowledge with time variations
return matches

Beneath the code to implement the logic and retailer outcomes on the dataframe matches_gap_to_reference:

# Lap distance
lap_distance = 1000

# Retailer the DataFrames in a dictionary
df_dict = {
'jimena': jimena_df,
'juan': juan_df,
'pedro': pedro_df,
}

# Retailer the Lap DataFrames in a dictionary
laps_dict = {
'jimena': interpolate_laps(jimena_df , lap_distance),
'juan': interpolate_laps(juan_df , lap_distance),
'pedro': interpolate_laps(pedro_df , lap_distance)
}

# Calculate gaps to reference
reference_name = 'juan'
matches_gap_to_reference = gap_to_reference(laps_dict, df_dict, reference_name)

The columns of the ensuing dataframe include the necessary info that might be displayed on the graphs:

[ad_2]
Juan Hernanz
2024-08-06 23:51:26
Source hyperlink:https://towardsdatascience.com/visualising-strava-race-analysis-bdabe0b67c02?source=rss—-7f60cf5620c9—4

Similar Articles

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular