-
Notifications
You must be signed in to change notification settings - Fork 26
/
Copy pathparse_fit.py
97 lines (75 loc) · 3.87 KB
/
parse_fit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""Some functions for parsing a FIT file (specifically, a FIT file
generated by a Garmin vívoactive 3) and creating a Pandas DataFrame
with the data.
"""
from datetime import datetime, timedelta
from typing import Dict, Union, Optional,Tuple
import pandas as pd
import fitdecode
# The names of the columns we will use in our points DataFrame. For the data we will be getting
# from the FIT data, we use the same name as the field names to make it easier to parse the data.
POINTS_COLUMN_NAMES = ['latitude', 'longitude', 'lap', 'altitude', 'timestamp', 'heart_rate', 'cadence', 'speed']
# The names of the columns we will use in our laps DataFrame.
LAPS_COLUMN_NAMES = ['number', 'start_time', 'total_distance', 'total_elapsed_time',
'max_speed', 'max_heart_rate', 'avg_heart_rate']
def get_fit_lap_data(frame: fitdecode.records.FitDataMessage) -> Dict[str, Union[float, datetime, timedelta, int]]:
"""Extract some data from a FIT frame representing a lap and return
it as a dict.
"""
data: Dict[str, Union[float, datetime, timedelta, int]] = {}
for field in LAPS_COLUMN_NAMES[1:]: # Exclude 'number' (lap number) because we don't get that
# from the data but rather count it ourselves
if frame.has_field(field):
data[field] = frame.get_value(field)
return data
def get_fit_point_data(frame: fitdecode.records.FitDataMessage) -> Optional[Dict[str, Union[float, int, str, datetime]]]:
"""Extract some data from an FIT frame representing a track point
and return it as a dict.
"""
data: Dict[str, Union[float, int, str, datetime]] = {}
if not (frame.has_field('position_lat') and frame.has_field('position_long')):
# Frame does not have any latitude or longitude data. We will ignore these frames in order to keep things
# simple, as we did when parsing the TCX file.
return None
else:
data['latitude'] = frame.get_value('position_lat') / ((2**32) / 360)
data['longitude'] = frame.get_value('position_long') / ((2**32) / 360)
for field in POINTS_COLUMN_NAMES[3:]:
if frame.has_field(field):
data[field] = frame.get_value(field)
return data
def get_dataframes(fname: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
"""Takes the path to a FIT file (as a string) and returns two Pandas
DataFrames: one containing data about the laps, and one containing
data about the individual points.
"""
points_data = []
laps_data = []
lap_no = 1
with fitdecode.FitReader(fname) as fit_file:
for frame in fit_file:
if isinstance(frame, fitdecode.records.FitDataMessage):
if frame.name == 'record':
single_point_data = get_fit_point_data(frame)
if single_point_data is not None:
single_point_data['lap'] = lap_no
points_data.append(single_point_data)
elif frame.name == 'lap':
single_lap_data = get_fit_lap_data(frame)
single_lap_data['number'] = lap_no
laps_data.append(single_lap_data)
lap_no += 1
# Create DataFrames from the data we have collected. If any information is missing from a particular lap or track
# point, it will show up as a null value or "NaN" in the DataFrame.
laps_df = pd.DataFrame(laps_data, columns=LAPS_COLUMN_NAMES)
laps_df.set_index('number', inplace=True)
points_df = pd.DataFrame(points_data, columns=POINTS_COLUMN_NAMES)
return laps_df, points_df
if __name__ == '__main__':
from sys import argv
fname = argv[1] # Path to FIT file to be given as first argument to script
laps_df, points_df = get_dataframes(fname)
print('LAPS:')
print(laps_df)
print('\nPOINTS:')
print(points_df)