В предыдущей статье я рассказал о том, как получить данные о персональных тренировках из набора FIT-файлов, которые создаются при использовании носимых устройств (фитнес-браслеты, часы, смартфоны, велокомпьютеры).
При дальнейшем анализе моих активностей я решил сфокусироваться на виртуальных велотренировках по нескольким причинам:
-
наличие более сотни записей с этого вида тренировок, совершенных за последние два года на одном виде станка с использованием одних и тех же сенсоров (мощемер, датчик каденса и пульсометр) и одного и того же приложения
-
тренировки проводились в зимний сезон в помещении при относительно одинаковых условиях, то есть исключается фактор разных метеоусловий (температура воздуха, сила ветра, влажность)
-
тренировки проводились регулярно приблизительно в одно и то же время суток и дни недели
Относительная сопоставимость условий проведения тренировок позволяет сравнивать между собой имеющиеся данные.
Стоит отметить, что уровень точности сенсоров их стабильности все же не претендует на абсолютную значимость результатов анализа, и сам анализ интересен исключительно с любительской точки зрения.
Что такое виртуальная велотренировка
Виртуальная велотренировка является заменой или дополнением к велотренировкам в помещении, которые проходят на велотренажерах.
Минимальный набор оборудования для начала виртуальных велотренировок включает в себя сам велосипед и станок, пульсометр, датчик каденса, мощемер и приложение (на компьютере, смартфоне или смарт-приставке телевизора), которое транслирует структурированную тренировку и записывает данные со всех датчиков. Виртуальность может дополняться прорисованным в приложении маршрутом, соревнованиями с другими велосипедистами.
Детально о том как проходит виртуальная велотренировка можно посмотреть здесь.
К ключевым показателям виртуальной велотренировки относятся:
-
мощность (power, watts) – прилагаемая на педали сила, умноженная на скорость. Измеряется в ваттах и демонстрирует эффективность той работы, которую вы выполняете, крутя педали
-
каденс (cadence, rpm) – частота педалирования или по-другому количество оборотов педалей велосипедиста, сделанных за одну минуту
-
пульс (heart rate, bpm) – частота сердечных сокращений или по-другому количество ударов сердца в минуту.
Данные со всех датчиков регистрируются в момент времени (каждую секунду). Если рассматривать FIT-файл, то упомянутые данные для каждой тренировки хранятся в сообщении Record, обощенные данные — в Session.
Как построить график тренировки
Для извлечения данных о конкретной тренировке из развернутой мною ранее базы данных, я использую запрос с указанием уникального номера тренировки (activity_id =124863703316) в таблицу Record:
select * from record where activity_id =124863703316 order by timestamp asc
Добавлю, что другие показатели — координаты, отметка высоты, скорость, расстояние — в структурированных виртуальных велотренировках являются производными от ключевых (мощность, каденс и пульс), поэтому исключены из дальнейшего анализа.
Для построения привычного графика показателей тренировки я использовал подключение к базе данных PostgreSQL через модуль psycopg2, pandas для работы с датасетом и matplotlib для визуализации.
В первом шаге подключаемся к данным:
import psycopg2 import pandas as pd activity_id = 124863703316 conn = psycopg2.connect(host="localhost", database="garmin_data", user="postgres", password="afande") df = pd.read_sql_query("""select timestamp, heart_rate, cadence, power from record where activity_id ={} order by timestamp asc""".format(activity_id), conn)
Для каждой записи мы имеем указание на конкретный момент времени в виде 2022-02-15 16:18:16+00:00, что не очень удобно для общего графика тренировки. Обозначим старт тренировки как нулевую секунду, все последующие записи будут пересчитаны относительно старта. Добавим новую колонку sec к датасету:
df['sec'] = (df['timestamp']-min(df['timestamp'])).dt.total_seconds()/60
На графике предполагается отображение пульса, мощности и каденса одновременно, то есть необходимо иметь три шкалы показателей. Я нашел пример построения графика с тремя осями Y на одном графике здесь.
import matplotlib.pyplot as plt fig, ax = plt.subplots() fig.subplots_adjust(right=0.75) plt.title(str(min(df['timestamp']).date()) + " / Activity - " +str(activity_id)) twin1 = ax.twinx() twin2 = ax.twinx() twin2.spines.right.set_position(("axes", 1.1)) p1, = ax.plot(df.sec, df.heart_rate,"r-", label="HR") p2, = twin1.plot(df.sec, df.power, "b-", label="Power") p3, = twin2.plot(df.sec, df.cadence, "g-", label="Cadence") ax.set_xlim(0, 90) ax.set_ylim(0, 200) twin1.set_ylim(0, 400) twin2.set_ylim(0, 120) ax.set_xlabel("Time, min") ax.set_ylabel("HR, bpm") twin1.set_ylabel("Power, watts") twin2.set_ylabel("Cadence, bpm") ax.yaxis.label.set_color(p1.get_color()) twin1.yaxis.label.set_color(p2.get_color()) twin2.yaxis.label.set_color(p3.get_color()) tkw = dict(size=4, width=1.5) ax.tick_params(axis='y', colors=p1.get_color(), **tkw) twin1.tick_params(axis='y', colors=p2.get_color(), **tkw) twin2.tick_params(axis='y', colors=p3.get_color(), **tkw) ax.tick_params(axis='x', **tkw) ax.legend(handles=[p1, p2, p3]) plt.rcParams['figure.figsize'] = [10, 5] plt.show()
В итоге мы получаем следующий график:
На этом графике хорошо видно, что показатели могут довольно резко изменяться (секундные падения и взлеты), что может быть обусловлено потерей сигнала сенсора или остановкой педалирования.
Для визуально приятного отображения графика тренировки можно добавить сглаживание линий через функцию сплайна. Способ описан здесь.
Я добавил в код функцию spline с тремя параметрами – значения по оси X, значения по оси Y и параметр n, отвечающий за уровень сглаживания конечной линии (чем он меньше, тем более сглаженной получится линия):
from scipy.interpolate import make_interp_spline import numpy as np def spline(x, y, n): do_spline = make_interp_spline(x,y) x_ = np.linspace(x.min(), x.max(), n) y_ = do_spline(x_) return x_, y_
Заменим код для линий на графике на следующий:
p1, = ax.plot(spline(df.sec, df.heart_rate, 150)[0], spline(df.sec, df.heart_rate, 150)[1],"r-", label="HR") p2, = twin1.plot(spline(df.sec, df.power, 150)[0], spline(df.sec, df.power, 150)[1], "b-", label="Power") p3, = twin2.plot(spline(df.sec, df.cadence, 150)[0], spline(df.sec, df.cadence, 150)[1], "g-", label="Cadence")
На рисунке ниже показано сопотавление двух вариантов:
Какие бывают велотренировки
Регулярное планирование велотренировок довольно сложный процесс и может быть нацелено на достижения различных целей: конкретный старт, повышение выносливости на длинных дистанциях, общей максимизации эффективности тренировочного времени и прочее.
Наиболее популярный метод планирования построен на комбинировании различных зон интенсивности в рамках одной структурированной тренировки и/или в пределах недельного/месячного плана тренировок в зависимости от целей.
Разделение на зоны интенсивности чаще всего происходит на основе функциональной пороговой мощности (Functional Threshold Power, FTP) – средняя максимальная мощность при езде на велосипеде в течение часа. Персональное значение FTP велосипедиста можно определить с помощью FTP-теста.
Одна из наиболее распространенных моделей предполагает деление на семь зон интенсивности в соответствии с физиологическим ответом организма спортсмена:
Подробное описание каждой из семи зон приведено здесь.
Фактически отдельная тренировка может сочетать в себе несколько типов, и отнести тренировку к одному типу можно только условно.
Как определить похожие тренировки
Я попытался разделить весь набор завершенных тренировок на несколько типов, учитывая продолжительность тренировки, соотношение проведенного времени в зонах интенсивности, соотношение проведенного времени при низком и среднем каденсе. Дальше я расскажу как я получил каждый из показателей.
Продолжительность тренировки
Продолжительность тренировки можно получить напрямую из таблицы Session – это колонка total_timer_time, где время тренировки записано в секундах.
При дальнейшем сопоставлении времени тренировок с показателями в других единицах измерения целесообразно нормализовать эти данные. Для готового датафрейма нормализация может быть сделана следующим образом, где time_n – колонка с нормализованными данными, time – исходные данные:
data['time_n'] = data.apply(lambda row: round((row['time']-min(data['time']))/(max(data['time'])-min(data['time']))*100.00)
Соотношение проведенного времени в зонах интенсивности
Выставленные зоны интенсивности в процессе тренировок меняются и зависят от последнего результата FTP-теста. В сезоне 2020-2021 я провел три аналогичных теста, в сезоне 2021-2022 – два. Результаты тестов я занес в отдельную таблицу Test в своей базе данных.
Далее для каждой тренировки из таблицы Session (здесь хранятся общие сведения о тренировке) я добавил значение последнего FTP-теста, на основе которого выставляются зоны интенсивности для всех последующих тренировок. Также я исключил все тренировки до первого теста в сезоне, так как использовать значение из прошлого сезона не правдоподобно (за лето форма могла измениться). В запросе я использовал cross join и rank() over:
select * from (select session.activity_id, session.timestamp, session.avg_heart_rate, session.avg_power, session.total_timer_time/3600 as time, test.power_threshold, test.timestamp as test_timestamp, rank() over (partition by session.activity_id order by session.activity_id, test.timestamp desc) as ftp_rank from session cross join test where sub_sport = 'virtual_activity' and avg_heart_rate > 90 and session.timestamp > '2020-12-26' and session.timestamp >= test.timestamp and record.activity_id not in (109983788203, 110771005101, 111376595537, 111494782478) order by session.activity_id) a where a.ftp_rank = 1
Для детальных данных тренировок я сделал запрос к таблице Record (к ней же мы делали запрос для построения графика одной тренировки ранее), исключив тренировки в каждом сезоне до первого теста:
select record.record_id, record.activity_id, record.timestamp, record.heart_rate, record.cadence, record.power from record join session on record.activity_id = session.activity_id where record.timestamp >= '2020-12-26' and session.sub_sport = 'virtual_activity' and record.power > 0 and record.activity_id not in (109983788203, 110771005101, 111376595537, 111494782478) order by timestamp asc
При объединении таблиц я получил детальные данные для каждой тренировки вместе с пороговой мощностью. Теперь можно рассчитать соотношение времени, проведенного в разных зонах интенсивности.
Добавим новую колонку в объединенную таблицу, где будет рассчитан процент от пороговой мощности для каждой записи (каждой секунды):
record_data['percent_power'] = round(record_data['power']/record_data['power_threshold']*100.00)
Для дальнейших расчетов я добавил следующую функцию на основе приведенной ранее таблицы семи зон интенсивности:
def zone(row): if row['percent_power'] <= 55: val = 1 elif row['percent_power'] > 55 and row['percent_power'] <=75: val = 2 elif row['percent_power'] > 75 and row['percent_power'] <=90: val = 3 elif row['percent_power'] > 90 and row['percent_power'] <=105: val = 4 elif row['percent_power'] > 105 and row['percent_power'] <=120: val = 5 elif row['percent_power'] > 120 and row['percent_power'] <=130: val = 6 elif row['percent_power'] > 130: val = 7 else: val = 0 return val
Расчет номера зоны для каждой строчки в датафрейме упрощается до вида:
record_data['zone'] = record_data.apply(zone, axis=1)
Далее агрегируем данные для того, чтобы получить соотношение времени потраченного в каждой зоне интенсивности для отдельной тренировки:
record_pivot = pd.pivot_table(record_data, index = ['activity_id', 'zone'], values = ['percent_power'], aggfunc='count') record_pivot = record_pivot.reset_index() record_pivot['record_count'] = record_pivot.groupby('activity_id')['percent_power'].transform('sum') record_pivot['percent_zone'] = round(record_pivot.percent_power/record_pivot.record_count*100.00) record_pivot = record_pivot[['activity_id', 'zone', 'percent_zone']] record_pivot['zone_desc'] = record_pivot.apply(lambda row: 'zone_'+str(int(row['zone'])), axis=1) training_zone = pd.pivot_table(record_pivot, index=['activity_id'], values='percent_zone', columns='zone_desc') training_zone = training_zone.reset_index()
Итоговый датафрейм имеет следующий вид:
Соотношение проведенного времени при низком и высоком каденсе
Используя метод расчета для зон интенсивности, можно сделать аналогичный расчет для каденса. Я использовал следующую функцию для расчета трех условных зон каденса (низкий, обычный или средний и высокий):
def cadence(row): if row['cadence'] >= 45 and row['cadence'] < 65: val = 1 elif row['cadence'] >=85 and row['cadence'] <= 95: val = 2 elif row['cadence'] > 95: val = 3 else: val = 0 return val
Аналогично рассчитаем зоны каденса для каждой секунды тренировок:
cadence_pivot = pd.pivot_table(record_data, index = ['activity_id', 'cadence_zone'], values = ['cadence'], aggfunc='count') cadence_pivot = cadence_pivot.reset_index() cadence_pivot['record_count'] = cadence_pivot.groupby('activity_id')['cadence'].transform('sum') cadence_pivot['cadence'] = round(cadence_pivot.cadence/cadence_pivot.record_count*100.00) cadence_pivot = cadence_pivot[['activity_id', 'cadence_zone', 'cadence']] cadence_pivot['cadence_zone_desc'] = cadence_pivot.apply(lambda row: 'cadence_zone_'+str(int(row['cadence_zone'])), axis=1) cadence_zone = pd.pivot_table(cadence_pivot, index=['activity_id'], values='cadence', columns='cadence_zone_desc') cadence_zone = cadence_zone.reset_index()
При объединении всех данных в одну таблицу мы получаем датафрейм следующего вида:
Использование контролируемой классификации
В итоговой таблице я получил список из 102 тренировок за два последние сезона (2020-2021 и 2021-2022). Учитывая размер выборки можно проклассифицировать тренировки вручную, но я решил попробовать метод контролируемой классификации.
Сразу скажу, что рациональность этого действия сводится к минимуму, но мне было интересно как этот метод сможет сработать в данном случае.
Метод контролируемой классификации (или обучение с учителем) использует заранее подготовленные образцы или эталоны для классификации данных или точного прогнозирования результатов. Подробнее о методах обучения здесь.
Подготовка тренировок-эталонов
Для 37 из 102 тренировок я проставил наиболее близкий тип тренировки, сопоставляя время и зоны интенсивности для каждой из них. Примеры всех типов тренировок-эталонов и их описания приведены в таблице:
Код эталона |
Название |
Продолжительность |
Зоны интенсивности |
Зоны каденса |
3 |
Условно пороговая тренировка / FTP-тест |
короткая (меньше часа) |
значительная часть времени (50% и более) проведено в зонах высокой интенсивности |
большая часть времени проведена на среднем и высоком каденсе |
2 |
Условно тренировка темпо |
средняя (1,5-2 часа) |
большая часть времени (более 70%) проведена в зонах низкой и средней интенсивности |
большая часть времени проведена на среднем каденсе или на низком и среднем каденсе |
1 |
Условно тренировка на выносливость |
длительная (больше 1,5 часов) |
большая часть времени (более 70%) проведена в зонах низкой интенсивности |
большая часть времени проведена на среднем каденсе |
0 |
Условно восстановительная тренировка |
короткая (до 1,5 часов) |
Большая часть времени (более 70%) проведена в зонах низкой интенсивности |
большая часть времени проведена на среднем каденсе |
Random Forests
Я использовал наиболее распространенный и простой в исполнении алгоритм Random Forests, который включен в модуль sklearn. Понятная инструкция для его применения доступна здесь.
Для построения модели в качестве входных данных я использовал различные комбинации колонок из подготовленной ранее таблицы. Наиболее удачная комбинация на мой взгляд для моего набора данных выглядит следующим образом:
X=df_train[['time_n', 'zone_1_2', 'zone_3', 'zone_4_6', 'cadence_zone_1']]
Я суммировал значения первой и второй зон интенсивности, так как они почти всегда соответствуют низкому уровню, аналогично для четвертой, пятой и шестой – так как они соответствуют высокому уровню. Седьмую зону я исключил, так как она единично представлена для некоторых тренировок (1 и меньше процента). Для каденса я взял только зону с низкими значениями, так как значение средней и высокой зоны будет зависимо напрямую.
В качестве выходных данных я использовал 37 значений, соотносящихся с типом тренировок, расставленных ранее.
Тестирование модели показало ее высокую точность (Accuracy: 1.0), но скорее всего она меньше, исходя из маленького набора данных.
Применив эту модель, я проклассифицировал все остальные тренировки на четыре типа. В результате классификации я получил 5 тренировок на пороговой мощности, 39 темпо, 14 на выносливость, 44 на восстановление.
Сравнение похожих тренировок
Для похожих тренировок я попробовал совместить на одном графике их профили мощности, поскольку они отображают исходное структурированное задание. В теории я должен был увидеть схожие профили для одного типа тренировок.
Для визуализации нескольких профилей одновременно я использовал опцию subplot из библиотеки matplotlib.
# import libraries import psycopg2 import pandas as pd import matplotlib.pyplot as plt from datetime import datetime # connect to the dataset df = pd.read_csv('power_zones_data_rf_comparison.csv', index_col=0) df = df[['activity_id', 'training_type']] training_3 = df.loc[df['training_type']==3] list_3 = training_3['activity_id'].to_list() conn = psycopg2.connect(host="localhost", database="garmin_data", user="postgres", password="afande") # generate a chart for each training from the list_3 for i in range(5): activity_id = list_3[i] df = pd.read_sql_query("""select timestamp, heart_rate, cadence, power from record where activity_id ={} order by timestamp asc""".format(activity_id), conn) df['sec'] = (df['timestamp']-min(df['timestamp'])).dt.total_seconds()/60 plt.subplot(2,3,i+1) plt.plot(df.sec, df.power) plt.title(min(df['timestamp']).date()) plt.xlim(0, 60) plt.ylim(0,450) plt.xlabel('time, min') plt.ylabel('power, watts') plt.rcParams['figure.figsize'] = [20, 10] plt.show()
Наиболее точно были определены пороговые тренировки (или FTP-тесты). Три из них были определены вручную как эталонные, модель правильно определила оставшиеся две.
Относительно точно были определены тренировки других типов. Сопоставление тренировок в рамках выделенных типов будет продолжено в последующем анализе.
Итоги
Это моя первая попытка проанализировать данные, полученные из FIT-файлов. По сути я смог воспроизвести отображение базового набора показателей, которые могут в дальшейшем быть использованы для расчетов эффективности тренировок. В данном виде это конечно ничего не добавляет к существующей функциональности фитнес-приложений (такие как Garmin Connect или Strava), но это первый шаг к независимости от их интерфейса.
Я попробовал произвести классификацию завершенных тренировок для дальнейшего сопоставления показателей похожих тренировок, а именно для сравнения ответа организма на похожую нагрузку через пульс. Надеюсь поделиться этим анализом в следующих статьях.
ссылка на оригинал статьи https://habr.com/ru/post/661067/
Добавить комментарий