Ahora estamos listos para jugar con los datos para crear las visualizaciones.
Desafíos:
Para obtener los datos necesarios para las imágenes, mi primera intuición fue: mirar la columna de distancia acumulada para cada corredor, identificar cuándo completó una distancia de vuelta (1000, 2000, 3000, etc.) por cada uno de ellos y marcar las diferencias con el tiempo.
Este algoritmo parece simple y podría funcionar, pero tenía algunas limitaciones que necesitaba resolver:
- Las distancias de vuelta exactas a menudo se completan entre dos puntos de datos registrados. Para ser más preciso, tuve que hacer interpolación de los dos posición Y tiempo.
- En razón de diferencia de precisión máquinasPuede haber desalineaciones entre los ciclistas. Lo más común es cuando la notificación de vuelta de un corredor suena antes que la de otro, incluso si han estado juntos durante todo el recorrido. Para minimizar este problema, decidí Utilice el corredor de referencia para establecer marcas de posición para cada vuelta de la pista.. La diferencia de tiempo se calculará cuando otros corredores pasen estos puntos (incluso si su distancia acumulada está por delante o por detrás de la vuelta). Esto se acerca más a la realidad de las carreras: si alguien cruza un punto antes, está por delante (independientemente de la distancia acumulada de su dispositivo)
- Con el punto anterior viene otro problema: la latitud y longitud de una marca de referencia nunca pueden registrarse exactamente en los datos de otros corredores. Los vecinos más cercanos para encontrar el punto de datos más cercano en términos de posición.
- Finalmente, los vecinos más cercanos pueden generar puntos de datos erróneos si la pista cruza las mismas posiciones en diferentes momentos. Por lo tanto, la población en la que los vecinos más cercanos buscarán la mejor coincidencia debe ser reducido a un grupo más pequeño de candidatos. definí un tamaño de ventana de 20 puntos de datos alrededor de la distancia objetivo (distancia_cum).
Algoritmo
Teniendo en cuenta todas las limitaciones anteriores, el algoritmo debería ser el siguiente:
1. Elige la referencia y una distancia de vuelta (por defecto = 1 km)
2. Utilizando los datos de referencia, identifique la posición y el momento en que se realizó cada giro: las marcas de referencia.
3. Acceder a los datos de otros corredores e identificar los momentos en que cruzaron estas marcas de posición. Luego calcula la diferencia de tiempo entre los dos corredores que cruzan estas marcas. Finalmente, el delta de esta diferencia horaria para representar la evolución de la brecha.
Ejemplo de código
1. Elige la referencia y una distancia de vuelta (por defecto = 1 km)
- Juan será la referencia (juan_df) en los ejemplos.
- Los otros corredores serán Pedro (pedro_df ) y Jimena (jimena_df).
- La distancia del recorrido será de 1000 metros.
2. Crear interpolar_turns(): función que encuentra o interpola el punto exacto para cada ronda completada y lo devuelve en un nuevo marco de datos. La inferpolación se realiza con la función: interpolar_valor() eso También fue creado.
## Function: interpolate_value()Input:
- start: The starting value.
- end: The ending value.
- fraction: A value between 0 and 1 that represents the position between
the start and end values where the interpolation should occur.
Return:
- The interpolated value that lies between the start and end values
at the specified fraction.
def interpolate_value(start, end, fraction):
return start + (end - start) * fraction
## Function: interpolate_laps()Input:
- track_df: dataframe with track data.
- lap_distance: metres per lap (default 1000)
Return:
- track_laps: dataframe with lap metrics. As many rows as laps identified.
def interpolate_laps(track_df , lap_distance = 1000):
#### 1. Initialise track_laps with the first 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 = Total Distance / lap_distance
number_of_laps = track_df['distance_cum'].max()//lap_distance
#### 3. For each lap i from 1 to number_of_laps:
for i in range(1,int(number_of_laps+1),1):
# a. Calculate target_distance = i * lap_distance
target_distance = i*lap_distance
# b. Find first_crossing_index where track_df['distance_cum'] > target_distance
first_crossing_index = (track_df['distance_cum'] > target_distance).idxmax()
# c. If match is exactly 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 new row
new_row = pd.Series({
'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
}, name=f'lap_{i}')
# d. Add the new row to the dataframe that stores 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 remove timezone
track_laps['date_time'] = pd.to_datetime(track_laps['date_time'], format='%Y-%m-%d %H:%M:%S.%f%z')
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
La aplicación de la función de interpolación al marco de datos de referencia generará el siguiente marco de datos:
juan_laps = interpolate_laps(juan_df , lap_distance=1000)
Tenga en cuenta que al ser una carrera de 10 km, se identificaron 10 vueltas de 1.000 m (ver columna distancia_cum). La columna diferencia_segundos tiene tiempo por vuelta. El resto de las columnas (latitud, longitud, elevación Y fecha hora) marcar la posición y el tiempo de cada revolución de referencia como resultado de la interpolación.
3. Para calcular las diferencias horarias entre la referencia y los demás corredores creé la función desviación_de_referencia()
## Helper Functions:
- get_seconds(): Convert timedelta to total seconds
- format_timedelta(): Format timedelta as a string (e.g., "+01:23" or "-00:45")
# Convert timedelta to total seconds
def get_seconds(td):
# Convert to total 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 total seconds
total_seconds = td.total_seconds()
# Determine sign
sign = '+' if total_seconds >= 0 else '-'
# Take absolute value 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"{sign}{minutes:02d}:{seconds:02d}"
## Function: gap_to_reference()Input:
- laps_dict: dictionary containing the df_laps for all the runnners' names
- df_dict: dictionary containing the track_df for all the runnners' names
- reference_name: name of the reference
Return:
- matches: processed data with time differences.
def gap_to_reference(laps_dict, df_dict, reference_name):
#### 1. Get the reference's lap data from laps_dict
matches = laps_dict[reference_name][['latitude','longitude','date_time','distance_cum']]#### 2. For each racer (name) and their data (df) in df_dict:
for name, df in df_dict.items():
# If racer is the reference:
if name == reference_name:
# Set time difference to zero for all laps
for lap, row in matches.iterrows():
matches.loc[lap,f'seconds_to_reference_{reference_name}'] = 0
# If racer is not the reference:
if name != reference_name:
# a. For each lap find the nearest point in racer's data based on lat, lon.
for lap, row in matches.iterrows():
# Step 1: set the position and lap distance from the reference
target_coordinates = matches.loc[lap][['latitude', 'longitude']].values
target_distance = matches.loc[lap]['distance_cum']
# Step 2: find the datapoint that will be in the centre of the window
first_crossing_index = (df_dict[name]['distance_cum'] > target_distance).idxmax()
# Step 3: select the 20 candidate datapoints to look 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 nearest match using the coordinates
nn = NearestNeighbors(n_neighbors=1, metric='euclidean')
nn.fit(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 difference between racer and reference at this point
matches[f'time_to_ref_{name}'] = matches[f'nearest_timestamp_{name}'] - matches['date_time']
# c. Store time difference and other relevant data
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 data using helper functions
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 data with time differences
return matches
A continuación se muestra el código para implementar la lógica y almacenar los resultados en el marco de datos. correspondencias_gap_to_the_reference:
# Lap distance
lap_distance = 1000# Store the DataFrames in a dictionary
df_dict = {
'jimena': jimena_df,
'juan': juan_df,
'pedro': pedro_df,
}
# Store 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)
Las columnas del marco de datos resultante contienen información importante que se mostrará en los gráficos: