Анализ Данных Виртуального Велоспорта

В предыдущей статье Я рассказал о том, как получить персональные данные о тренировках из набора FIT-файлов, которые создаются при использовании носимых устройств (фитнес-браслетов, часов, смартфонов, велокомпьютеров).

После дальнейшего анализа своей деятельности я решил сосредоточиться на виртуальных тренировках на велосипеде по нескольким причинам:

  • наличие более сотни записей данного вида тренировок, выполненных за последние два года на тренажере одного и того же типа с использованием одних и тех же датчиков (измерителя мощности, датчика частоты вращения педалей и пульсометра) и одного и того же приложения
  • тренировки проводились в зимний сезон в помещении при относительно одинаковых условиях, то есть исключается фактор разных погодных условий (температура воздуха, сила ветра, влажность)
  • тренировки проводились регулярно примерно в одно и то же время суток и дни недели
Относительная сопоставимость условий обучения позволяет сравнивать имеющиеся данные между собой.

Стоит отметить, что уровень точности датчиков устойчивости пока не претендует на абсолютную значимость результатов анализа, а сам анализ интересен лишь с любительской точки зрения.



Что такое виртуальная тренировка на велосипеде?

Виртуальные тренировки на велосипеде — это замена или дополнение тренировок на велосипеде в помещении, проводимых на велотренажерах.

Минимальный набор оборудования для начала виртуальной тренировки на велосипеде включает в себя сам велосипед и тренажер, пульсометр, датчик частоты вращения педалей, измеритель мощности и приложение (на компьютере, смартфоне или приставке Smart TV), транслирующее структурирует тренировки и записывает данные со всех датчиков.

Виртуальность можно дополнить прорисованным в приложении маршрутом и соревнованиями с другими велосипедистами.

Подробнее о том, как проходят виртуальные тренировки на велосипеде, вы можете посмотреть.

Здесь .

Ключевые показатели тренировок на виртуальном велосипеде включают в себя:

  • власть (мощность, Вт) – сила, приложенная к педалям, умноженная на скорость.

    Измеряется в ваттах и показывает эффективность работы, которую вы выполняете при вращении педалей.

  • ритм (каденс, об/мин) - частота вращения педалей или другими словами количество оборотов педали велосипедиста, совершаемых за одну минуту.

  • пульс (сердцебиение, удары в минуту) – частота сердечных сокращений или другими словами количество ударов сердца в минуту.

Данные со всех датчиков записываются в определенный момент времени (каждую секунду).

Если рассматривать файл FIT, то упомянутые данные для каждой тренировки хранятся в сообщении Record, обобщенные данные хранятся в Session.

Как построить график тренировок

Чтобы получить данные о конкретной тренировке из базы данных, которую я ранее развернул, я использую запрос, указывающий уникальный номер тренировки.

(идентификатор активности =124863703316) в таблицу записей:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

select * from record where activity_id =124863703316 order by timestamp asc



Анализ данных виртуального велоспорта

Структура таблицы рекордов с данными одной тренировки Добавлю, что другие показатели - координаты, высота, скорость, расстояние - в структурированной виртуальной тренировке на велосипеде они выводятся из ключевых (мощность, частота вращения педалей и частота сердечных сокращений), поэтому исключаются из дальнейшего анализа.

Для построения обычного графика показателей обучения я использовал подключение к базе данных PostgreSQL через модуль психокопг2 , панды для работы с набором данных и 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 , что не очень удобно для общего графика тренировок.

Обозначим начало тренировки как нулевую секунду; все последующие записи будут пересчитаны относительно начала.

Давайте добавим новый столбец сек в набор данных:

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()

В результате мы получаем следующий график:

Анализ данных виртуального велоспорта

График пульса, мощности и частоты шагов за одну тренировку На этом графике хорошо видно, что показатели могут меняться довольно резко (секунды падают и растут), что может быть связано с потерей сигнала датчика или остановкой педалирования.

Для визуально приятного отображения графика обучения вы можете добавить сглаживание линий с помощью функции сплайна.

Метод описан Здесь .

Я добавил функцию в код сплайн с тремя параметрами — значениями по оси 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")

На рисунке ниже показано сравнение двух вариантов:

Анализ данных виртуального велоспорта

Сравнение двух графиков: слева с исходными данными, справа с помощью сплайн-функции

Какие виды тренировок на велосипеде существуют?

Регулярное планирование велосипедных тренировок представляет собой достаточно сложный процесс и может быть направлено на достижение различных целей: конкретного старта, повышения выносливости на длинных дистанциях, общей максимизации эффективности тренировочного времени и т. д. Самый популярный метод планирования основан на объединении различных зон интенсивности в рамках одной структурированной тренировки и/или в рамках еженедельного/ежемесячного плана тренировок, в зависимости от ваших целей.

Деление на зоны интенсивности чаще всего происходит по признаку функциональный пороговая мощность ( Функциональная пороговая мощность, FTP ) – средняя максимальная мощность при езде на велосипеде в течение часа.

Персональное значение FTP велосипедиста можно определить с помощью FTP-тест .

Одна из наиболее распространенных моделей предполагает разделение интенсивности на семь зон в соответствии с физиологической реакцией организма спортсмена:

Анализ данных виртуального велоспорта

Семь зон интенсивности езды на велосипеде.

Источник: https://www.highnorth.co.uk/articles/cycling-training-zones Дано подробное описание каждой из семи зон.

Здесь.

Фактически, одна тренировка может сочетать в себе несколько типов, и к одному типу тренировку можно отнести лишь условно.



Как идентифицировать похожие тренировки

Всю совокупность выполненных тренировок я постарался разделить на несколько типов с учетом продолжительности тренировки, соотношения времени пребывания в зонах интенсивности, соотношения времени пребывания на низком и среднем каденсе.

Далее я расскажу вам, как я получил каждый из показателей.



Продолжительность обучения

Продолжительность тренировки можно узнать прямо из таблицы Сессия - это столбец total_timer_time , где время обучения записано в секундах.

При дальнейшем сравнении времени тренировки с показателями в других единицах измерения целесообразно нормировать эти данные.

Для готового кадра данных нормализацию можно выполнить следующим образом, где time_n – столбец с нормализованными данными, время - исходные данные:

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 – два.

Результаты теста я внес в отдельную таблицу Тест в вашей базе данных.



Анализ данных виртуального велоспорта

Таблица испытаний с пороговыми значениями мощности для каждого теста FTP Далее для каждой тренировки из таблицы Сессия (общая информация о тренировке хранится здесь) Я добавил значение последнего FTP-теста, на основании которого выставляются зоны интенсивности для всех последующих тренировок.

Я также исключил все тренировки перед первыми тестами сезона, поскольку использование значений прошлого сезона нецелесообразно (за лето форма могла измениться).

В запросе я использовал перекрестное соединение И ранжировать() выше :

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

Для получения подробных данных обучения я сделал запрос к таблице Записывать (ранее мы также сделали к нему запрос на построение одной тренировки), исключая тренировки в каждом сезоне перед первым тестом:

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()

Окончательный фрейм данных выглядит так:

Анализ данных виртуального велоспорта

Тренировки с рассчитанным соотношением времени, затрачиваемого на зоны интенсивности (зона_1 - зона_7)

Соотношение времени, проведенного при низком и высоком темпе

Используя метод расчета зон интенсивности, аналогичный расчет можно провести и для частоты шагов.

Я использовал следующую функцию для расчета трех условных зон частоты вращения педалей.

(низкий, нормальный или средний и высокий) :

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()

Когда мы объединяем все данные в одну таблицу, мы получаем фрейм данных, который выглядит следующим образом:

Анализ данных виртуального велоспорта

Тренировки с рассчитанным соотношением времени, проведенного по зонам интенсивности (зона_1 - зона_7), зонам каденции (cadence_zone_0-cadence_zone_3) и ее продолжительности в нормированном виде (time_n)

Использование контролируемой классификации

В итоговой таблице я получил список 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, который включен в модуль sklearn. Доступны четкие инструкции по использованию.

Здесь .

Для построения модели в качестве входных данных я использовал различные комбинации столбцов из заранее подготовленной таблицы.

На мой взгляд, лучшая комбинация для моего набора данных выглядит так:

X=df_train[['time_n', 'zone_1_2', 'zone_3', 'zone_4_6', 'cadence_zone_1']]

Я суммировал значения первая и вторая зоны интенсивности , поскольку они почти всегда соответствуют низкому уровню, аналогично для четвертый, пятый и шестой – потому что они соответствуют высокому уровню.

Седьмой Зону я исключил, так как для некоторых тренировок она отображается только один раз (1 процент и меньше).

Для каденции я взял только зону с низкими значениями, так как значение средней и высокой зон будет напрямую зависеть.

В качестве вывода я использовал 37 значений, соответствующих типу обучения, который я перечислил ранее.

Тестирование модели показало ее высокую точность (точность: 1,0), но, скорее всего, она меньше, исходя из небольшого набора данных.

Используя эту модель, я классифицировал все остальные тренировки на четыре типа.

По итогам классификации я получил 5 пороговых силовых тренировок, 39 темповых, 14 выносливых, 44 восстановительных.



Сравнение похожих тренировок

Для аналогичных тренировок я попробовал объединить их профили мощности на одном графике, поскольку они представляют собой исходную структурированную задачу.

Теоретически я должен был увидеть похожие профили для одного и того же типа обучения.

Для визуализации нескольких профилей одновременно я использовал опцию подсюжет из библиотеки 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-тесты).

Три из них были вручную идентифицированы как эталонные; модель правильно определила оставшиеся два.



Анализ данных виртуального велоспорта

Сравнение профилей тренировочной мощности с кодом 3: верхний ряд — эталонные тренировки, нижний ряд — определены с помощью контролируемой модели классификации.

Остальные виды обучения определены относительно точно.

Сравнение обучения внутри выявленных типов будет продолжено в последующем анализе.



Анализ данных виртуального велоспорта

Сравнение профилей тренировочной мощности с кодом 2: верхний ряд — эталонные тренировки, нижний ряд — определены с помощью контролируемой модели классификации.



Полученные результаты

Это моя первая попытка проанализировать данные, полученные из файлов FIT. По сути, мне удалось воспроизвести отображение базового набора показателей, которые в дальнейшем можно использовать для расчета эффективности тренировок.

В нынешнем виде это, конечно, ничего не добавляет к существующему функционалу фитнес-приложений (таких как Garmin Connect или Strava), но это первый шаг к независимости от их интерфейса.

Я попытался классифицировать завершенные тренировки, чтобы в дальнейшем сравнивать результативность аналогичных тренировок, а именно сравнивать реакцию организма на подобную нагрузку через пульс.

Я надеюсь поделиться этим анализом в будущих статьях.

Ссылка на гитхаб .

Теги: #python #postgresql #garmin #matplotlib #cycling #sklearn #zwift #fit #fit

Вместе с данным постом часто просматривают: