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 de cada corredor, identificar cuándo cada uno de ellos completó una distancia de vuelta (1000, 2000, 3000, etc.) y hacer las diferencias de marcas de tiempo.
Ese algoritmo parece simple y podría funcionar, pero tenía algunas limitaciones que necesitaba abordar:
- Las distancias de vuelta exactas se suelen completar entre dos puntos de datos registrados. Para ser más preciso, tuve que hacer interpolación de ambos posición y tiempo.
- Debido a Diferencia en la precisión de dispositivospuede haber desajustes entre corredores. Lo más típico es cuando la notificación de vuelta de un corredor suena antes que la de otro, incluso si han estado juntos durante toda la pista. Para minimizar esto, decidí Utilice el corredor de referencia para establecer las marcas de posición para cada vuelta en la pistaLa diferencia de tiempo se calculará cuando otros corredores crucen esas marcas (aunque su distancia acumulada esté por delante o por detrás de la vuelta). Esto es más cercano a la realidad de la carrera: si alguien cruza un punto antes, está por delante (independientemente de la distancia acumulada de su dispositivo).
- Con el punto anterior surge otro problema: la latitud y longitud de una marca de referencia puede que nunca queden registradas exactamente en los datos de los demás corredores. Vecinos más cercanos para encontrar el punto de datos más cercano en términos de posición.
- Por último, los vecinos más cercanos pueden proporcionar puntos de datos erróneos si la pista cruza las mismas posiciones en diferentes momentos del tiempo. 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 candidatosDefiní una 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. Elija la referencia y una distancia de vuelta (predeterminada = 1 km)
2. Utilizando los datos de referencia, identifica la posición y el momento en que se completó cada vuelta: las marcas de referencia.
3. Ve a los datos de los otros corredores e identifica los momentos en que cruzaron esas marcas de posición. Luego calcula la diferencia de tiempo entre ambos corredores al cruzar las marcas. Finalmente, calcula el delta de esta diferencia de tiempo para representar la evolución de la brecha.
Ejemplo de código
1. Elija la referencia y una distancia de vuelta (predeterminada = 1 km)
- Juan será el referente (juan_df) sobre los ejemplos.
- Los otros corredores serán Pedro (pedro_df ) y Jimena (jimena_df).
- La distancia de vuelta será de 1000 metros.
2. Crear interpolar_vueltas():función que encuentra o interpola el punto exacto de cada vuelta 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
Al aplicar la función de interpolación al marco de datos de referencia, se generará el siguiente marco de datos:
juan_laps = interpolate_laps(juan_df , lap_distance=1000)
Tenga en cuenta que, como era una carrera de 10 km, se han identificado 10 vueltas de 1000 m (consulte la columna distancia_cum). La columna segundos_diff tiene el tiempo por vuelta. El resto de las columnas (latitud, longitud, elevación y fecha y hora) marcar la posición y el tiempo de cada vuelta de la referencia como resultado de la interpolación.
3. Para calcular los intervalos de tiempo entre la referencia y los demás corredores creé la función brecha_a_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. coincide_con_la_brecha_de_referencia:
# 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 la información importante que se mostrará en los gráficos: