Мне стало интересно проанализировать данные о своих тренировках за последние несколько лет, и я понял, что обычного функционала приложений типа Garmin Connect или бесплатной версии Strava будет недостаточно. В этой статье я расскажу как получить свои персональные данные о тренировках из устройств Garmin и разместить их в реляционной базе данных с помощью библиотек python.
Что такое FIT файл и что он может содержать
Если вы используете носимые устройства (фитнес-браслеты, часы, смартфоны, велокомпьютеры) для записи своих активностей или тренировок, то скорее всего информация будет сохранена в FIT файле активности – наиболее распространенный формат, используемый в фитнес-приложениях для обмена детальными данными о совершенных тренировках.
Для каждой отдельной активности файл включает в себя набор обязательных и дополнительных сообщений. Я разобрался с обязательными: они включают в себя записи о дате и времени тренировки, ее типе спорта, данные о кругах, GPS-трек, данные с сенсоров (может быть датчик пульса, датчик каденса/скорости, мощемер и прочее). Подробнее о структуре каждого сообщения ниже в таблице.
Название сообщения |
Описание и содержание сообщения |
File Id |
Содержит данные о производителе устройства сбора данных и название устройства |
Activity |
Включает данные о дате и времени активности, общее время, количестве сессий |
Session |
Обобщенная информация об активности (средняя и максимальная скорость, общее время, общее расстояние, общий набор высоты, вид и подвид спорта, средняя и максимальная частота пульса и другие) |
Lap |
Отображает обобщенные сведения о кругах или интервалах в рамках одной активности (номер круга, общее расстояние круга, максимальная скорость круга и другие). Может быть несколько кругов для одной активности |
Record |
Содержит данные со всех сенсоров в момент времени (координаты, номер круга, абсолютная высота на местности, дата и время, значение пульса, каденса, скорости, мощности, температуры воздуха и другие) |
Как получить доступ к данным
Все персональные данные о тренировках, собранные с помощью устройств Garmin (в моем случае это несколько версий часов и велокомпьютер), можно получить по запросу с официального сайта Garmin. После запроса в течение 48 часов вам на почту должна быть прислана ссылка на загрузку пакета данных.
В скачанном архиве наиболее ценные файлы с точки зрения данных об активностях будут находиться в папке DI_CONNECT\DI-Connect-Fitness-Uploaded-Files\UploadedFiles_0-_Part1. Каждый файл содержит информацию об одной совершенной активности.
Структура данных
На базе PostgreSQL я создал пять новых таблиц, которые будут соответствовать сообщениям из FIT файла — File Id, Activity, Session, Lap, Record. Таблица Activity будет содержать в себе ключ (activity_id) к каждой уникальной активности, все остальные таблицы будут содержать этот ключ как foreign key. При парсинге файла заполнение таблиц будет начинаться именно с сообщения Activity. Сгенерированная в PostgreSQL ERD схема базы данных представлена ниже.
Описание кода
Для декодирования, чтения и загрузки и проверке данных об активностях из FIT файлов в таблицы PostgreSQL я использовал Jupiter Notebook и несколько библиотек python, в том числе os, pandas, psycopg2, fitdecode и matplotlib.
Код для каждого файла из указанной выше папки проходит следующие шаги:
-
Извлечение уникального номера активности и идентификатора пользователя из названия FIT файла
-
Чтение, декодирование и запись данных в датафрейм для каждого из пяти сообщений (File Id, Activity, Session, Lap, Record)
-
Загрузка датафреймов в соответствующую таблицу PostgreSQL
Я подготовил схему данных для каждой из таблиц, чтобы код искал только те поля данных, которые мне будут необходимы из каждого отдельного сообщения. Например, из сообщения Activity мне нужны следующие данные:
activity = ['timestamp', 'total_timer_time', 'local_timestamp', 'num_sessions', 'type', 'event', 'event_type', 'event_group']
Извлечение кода активности и идентификатора пользователя
Названия файлов могут варьироваться по содержанию, но всегда содержат идентификатор пользователя и номер активности как два первых параметра, разделенных символом ‘_’. Для извлечения этих параметров использовал простую функцию:
def get_user_activity_details(file): filename = os.path.basename(file) user_id, activity_id = filename.split('_')[0], filename.split('_')[1] if '.' in activity_id: activity_id = activity_id.split('.')[0] return user_id, activity_id
Чтение, декодирование и запись данных в датафрейм
Для парсинга FIT файла была использована специальная библиотека fitdecode. Здесь можно найти исходную документацию. Установка библиотеки через PyPI:
pip install fitdecode
Библиотека позволяет создать объект FitReader, который считывает FIT файл и получает доступ к каждому сообщению (или фрейму) в этом файле. Каждый фрейм включает в себя FitHeader, FitDefinitionMessage, FitDataMessage, FitCRC. Нас будет интересовать только FitDataMessage, так как он содержит в себе данные об активности. В этой статье описано подробнее.
Пример извлечения данных из сообщения Activity в датафрейм ниже:
def get_fit_other_data(col, frame: fitdecode.records.FitDataMessage) -> Optional[Dict[str, Union[float, int, str, datetime]]]: data: Dict[str, Union[float, int, str, datetime]] = {} for field in col: if frame.has_field(field): data[field] = frame.get_value(field) return data def get_dataframes(fname: str) -> Tuple[pd.DataFrame]: activity_data = [] with fitdecode.FitReader(fname) as fit_file: for frame in fit_file: if isinstance(frame, fitdecode.records.FitDataMessage): if frame.name == 'activity': activity_data.append(get_fit_other_data(activity, frame)) activity_df = pd.DataFrame(activity_data, columns = activity) df['activity_id'] = activity_id if activity_df.empty: activity_df = activity_df.append({'activity_id':activity_id}, ignore_index=True) return activity_df
Загрузка датафрейма в таблицу базы данных
Для подключения и загрузки данных в PostgreSQL я использовал стандартную библиотеку psycopg2. Пример загрузки данных из датафрейма Activity в соответствующую таблицу ниже:
def load_dataframe_to_postgres(df, tabl): if not df.empty: df = df.fillna(0) cursor = conn.cursor() if tabl == 'activity': df = df.astype({'activity_id': 'int64','timestamp': 'datetime64[ns, UTC]', 'total_timer_time': 'float64', 'local_timestamp': 'datetime64[ns]', 'num_sessions': 'int64', 'type': 'object', 'event': 'object', 'event_type': 'object', 'event_group': 'object'}) for index, row in df.iterrows(): cursor.execute("""insert into activity(activity_id, timestamp, total_timer_time, local_timestamp, num_sessions, type, event, event_type, event_group) values (%s, %s, %s, %s, %s, %s, %s, %s, %s)""", [row.activity_id, row.timestamp, row.total_timer_time, row.local_timestamp, row.num_sessions, row.type, row.event, row.event_type, row.event_group]) conn.commit() cursor.close()
Проверка полученных данных
Для проверки адекватности полученных данных я попробовал визуализировать количество минут, потраченных на вид активности по месяцам за весь доступный период времени (с апреля 2014 по февраль 2022).
Мне было достаточно обобщенных данных из таблицы Session – я использовал поля с данными о дате активности, ее продолжительности и виде (timestamp, total_timer_time, sport). Итоговые данные сгруппированы по месяцам и виду активности.
# get session data summary with sport split conn = psycopg2.connect(host="localhost", database="garmin_data", user="postgres", password="afande") df = pd.read_sql_query("""select to_char(timestamp, 'YYYY-MM') as stamp, sum(total_timer_time / 60) as minutes_spent, sport from session group by to_char(timestamp, 'YYYY-MM'), sport having sum(total_timer_time / 60) > 0 order by to_char(timestamp, 'YYYY-MM') desc""", conn)
Дополнительно был создан датафрейм со всеми месяцами для их отображения на графике даже при нулевом значении (когда не было ни одной завершенной активности). Для этого были определены самый ранний и самый поздний месяцы, которые есть в таблице. После создан range с шагом в месяц и этими месяцами как граничными значениями.
# get min and max dates from the dataframe min_date = datetime.strptime(min(df.stamp), '%Y-%m') max_date = datetime.strptime(max(df.stamp), '%Y-%m') n_max_date = max_date + pd.DateOffset(months=1) # create a table with all months from min to max date data = pd.DataFrame() data['Dates'] = pd.date_range(start=min_date, end=n_max_date, freq='M') data['Dates'] = data['Dates'].dt.strftime('%Y-%m') # merge datasets df_main = pd.merge(data, df, left_on='Dates', right_on='stamp', how='left', indicator=True) df_main = df_main[['Dates', 'minutes_spent','sport']] df_main = df_main.fillna(0)
Сводная таблица была использована для построения графика stacked bar из библиотеки matplotlib. На график дополнительно добавлена разграфка по годам в виде вертикальных разделительных линий.
# pivot table df_pivot = pd.pivot_table(df_main, index='Dates', columns='sport', values='minutes_spent').reset_index() df_pivot = df_pivot.fillna(0) df_pivot = df_pivot[['Dates', 'cross_country_skiing', 'cycling', 'running', 'swimming', 'walking']] # create stacked bar chart for monthly sports df_pivot.plot(x='Dates', kind='bar', stacked=True, color=['r', 'y', 'g', 'b', 'k']) # labels for x & y axis plt.xlabel('Months', fontsize=20) plt.ylabel('Minutes Spent', fontsize=20) plt.legend(loc='upper left', fontsize=20) for num in [69, 57, 45, 33, 21, 9]: plt.axvline(linewidth=2, x=num, linestyle=':', color = 'grey') # title of plot plt.title('Minutes spent by Sport', fontsize=20) plt.rcParams['figure.figsize'] = [24, 10]
Итоговый график правдоподобно отображает количество минут потраченных на виды активностей – например максимум минут на велосипеде в апреле-мае 2019 года соответствует велотуру по Скандинавии, отсутствие велотренировок в августе-сентябре 2021 года из-за травмы и их замены на занятия по плаванию и пешими прогулками, появление лыжных тренировок с декабря 2021 года соответствует обкатке новой пары лыж.
Итоги
Всего я получил доступ к 5,300 FIT файлам, среди которых только один оказался битым и не выдавал доступ к данным. Полный код для загрузки данных из всех главных сообщений FIT файла в таблицы PostgreSQL DB можно найти здесь.
Данный метод позволяет получить доступ ко всем историческим данным одновременно и проводить дальнейший анализ в удобной среде.
Стоит отметить, что существует способ получения данных через Activity API, он больше ориентирован для разработчиков приложений, которым необходим постоянный доступ к обновляемым данным.
ссылка на оригинал статьи https://habr.com/ru/post/658675/
Добавить комментарий