Оживляем данные Strava: от парсинга GPX до интерактивной карты на Python и JS

от автора

Привет, Хабр! Меня зовут Александр, я разработчик и, как многие в IT, стараюсь уравновешивать сидячую работу спортом — в моем случае, это велосипед и бег. И, как многие спортсмены-любители, я пользуюсь Strava.

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

Это показалось мне… неправильным. Данные — мои. Устройство — мое. Почему за анализ моих же цифр я должен платить? Этот вопрос и стал отправной точкой для моего pet-проекта, который перерос в нечто большее — Peakline.

В этой статье я хочу провести вас «под капот» моего проекта и показать на реальных фрагментах кода, как с помощью Python, щепотки NumPy и капли JavaScript можно построить собственный мощный инструмент для анализа спортивных данных. Это история не только про код, но и про философию открытых данных и желание сделать профессиональные инструменты доступными для всех.

Шаг 1: «Вскрываем» GPX-файл. Работа с сырыми данными

Любой анализ начинается с данных. В Strava они удобно разложены по полочкам через API. Но что, если у вас есть только GPX-файл, например, выгруженный с велокомпьютера или часов? Настоящий инструмент должен уметь работать и с такими «сырыми» данными.

Под капотом Peakline использует стандартную библиотеку xml.etree.ElementTree, чтобы пройтись по файлу и извлечь самое ценное: координаты, высоту и время для каждой точки трека. Выглядит это примерно так:

# Фрагмент из функции analyze_local_gpx в Peakline import xml.etree.ElementTree as ET from datetime import datetime  # ...  # Namespace для GPX-файлов, чтобы парсер нас понял ns = {'gpx': 'http://www.topografix.com/GPX/1/1'}  # root - это корень нашего распарсенного XML-документа track_points = [] # Ищем все теги <trkpt> — это и есть точки нашего маршрута for point in root.findall('.//gpx:trkpt', ns):     lat = float(point.get('lat'))     lon = float(point.get('lon'))          # Время и высота могут отсутствовать, обрабатываем это     time_elem = point.find('gpx:time', ns)     elevation_elem = point.find('gpx:ele', ns)          track_point = {         'lat': lat,         'lon': lon,         'time': datetime.fromisoformat(time_elem.text.replace('Z', '+00:00')) if time_elem is not None else None,         'elevation': float(elevation_elem.text) if elevation_elem is not None else None     }     track_points.append(track_point)     

На выходе мы получаем чистый список словарей track_points — нашу «цифровую нефть», готовую к дальнейшей обработке. Уже на этом этапе Peakline может рассчитать общую дистанцию, набор высоты и время тренировки. Пользователю не нужно ничего делать — только загрузить файл, а сервис сам покажет ему базовую статистику, еще до того, как тренировка попадет в Strava.

Шаг 1.5: Не делаем лишней работы. Магия кэширования

Когда пользователь открывает свою тренировку, он хочет видеть результат мгновенно. Повторный полный анализ одной и той же активности — это долго для пользователя и расточительно для ресурсов сервера (и API-лимитов Strava).

Поэтому в Peakline встроена простая, но эффективная система кэширования на Redis. Прежде чем запускать весь конвейер анализа, система проверяет: а мы не анализировали это раньше?

# Фрагмент из analyze_activity в Peakline  # Пытаемся достать результат из кэша по ID активности и ID владельца cached_result = get_cached_analysis_for_owner(activity_id, user_id)  if cached_result:     logger.info(f"Cache HIT for activity {activity_id}.")     return cached_result # Отдаем результат из кэша, не дергая API  # Если в кэше нет - запускаем полный анализ... logger.info(f"Cache MISS for activity {activity_id}. Re-analyzing.") # ... (запускаем полный анализ) ... # ... (и сохраняем результат в кэш) ...     

Этот простой if экономит тысячи вызовов API и делает повторные загрузки страниц молниеносными. Это маленький штрих, который отличает pet-проект «для себя» от сервиса, готового к реальным пользователям.

Шаг 2: Мусор на входе — мусор на выходе. Почему важна очистка данных

Казалось бы, что может быть проще, чем рассчитать скорость? Но любой, кто смотрел на свой GPS-трек в городе или в густом лесу, знает: он «скачет». Эти скачки создают «шум» в данных, из-за которого моментальная скорость может подпрыгивать до нереалистичных значений. Если просто усреднить эти «грязные» данные, мы получим неверную картину тренировки.

К счастью, Strava предоставляет уже обработанный поток данных — velocity_smooth, где эти выбросы сглажены. И Peakline всегда предпочитает работать именно с ним, чтобы обеспечить точность расчетов. Вот как, например, рассчитывается средняя скорость с учетом только времени движения:

# Фрагмент из summary_calculator.py в Peakline import numpy as np  # ...  # streams - словарь с потоками данных от Strava API # speed_data_ms — это наш 'velocity_smooth' в м/с # moving_data — это список булевых значений (True/False),  # который говорит, двигался ли пользователь в данный момент speed_data_ms = streams['velocity_smooth']['data'] moving_data = streams['moving']['data']  # 1. Отфильтровываем моменты, когда пользователь стоял на месте # 2. Переводим скорость из м/с в км/ч (* 3.6) speed_data_kmh_moving = [     s * 3.6 for i, s in enumerate(speed_data_ms)      if i < len(moving_data) and moving_data[i] ]  # Если было движение, считаем среднее и максимальное значение if speed_data_kmh_moving:     # Используем NumPy для быстрых и точных расчетов     arr = np.array(speed_data_kmh_moving)     avg_speed = round(float(np.mean(arr)), 1)     max_speed = round(float(np.max(arr)), 1)     

Обратите внимание на две ключевые детали. Во-первых, мы используем массив moving_data, чтобы исключить из расчета остановки на светофорах или передышки — нас интересует только средняя скорость в движении. Во-вторых, для самих вычислений мы используем NumPy. Эта библиотека — золотой стандарт для научных вычислений в Python, она работает невероятно быстро и эффективно.

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

Шаг 3: Строим свой «завод по аналитике». Обходим платные ограничения

А вот мы и подошли к самому интересному. К той самой причине, почему я вообще начал делать Peakline. У меня есть датчик мощности, я знаю свой FTP (Functional Threshold Power — грубо говоря, максимальная мощность, которую я могу поддерживать в течение часа), но чтобы увидеть детальный анализ по зонам в Strava, я должен платить.

Я решил исправить эту «несправедливость». Peakline рассчитывает зоны мощности для любого пользователя, у которого есть данные с мощемера. Для этого достаточно один раз указать свой FTP в настройках. Под капотом запускается вот такая функция:

# Фрагмент из power_calculator.py в Peakline  def calculate_power_zones_manually(streams, ftp):     """Рассчитывает время в зонах мощности вручную на основе FTP."""          power_data = streams['watts'].get('data', [])     time_data = streams['time'].get('data', [])      if not power_data or not time_data:         return None      # Классические 7 зон мощности по доктору Эндрю Коггану     power_zones_definitions = [         ("Z1 (Восстановление)", 0, ftp * 0.55),         ("Z2 (Выносливость)", ftp * 0.55, ftp * 0.75),         ("Z3 (Темп)", ftp * 0.75, ftp * 0.90),         ("Z4 (Порог)", ftp * 0.90, ftp * 1.05),         ("Z5 (VO2 Max)", ftp * 1.05, ftp * 1.20),         ("Z6 (Анаэробная)", ftp * 1.20, ftp * 1.50),         ("Z7 (Нейромышечная)", ftp * 1.50, 9999), # 9999 - условная "бесконечность"     ]          # calculate_time_in_zones — это наша внутренняя функция-счетчик.     # Она проходит по всему треку и считает, сколько секунд     # было проведено в каждой из заданных зон.     time_in_zones_dict = calculate_time_in_zones(         power_data,          time_data,          power_zones_definitions     )      return {         "labels": list(time_in_zones_dict.keys()),         "values": list(time_in_zones_dict.values())     }     

Всего одна функция — и платная фича Strava становится доступной для всех пользователей Peakline. Мы просто берем поток данных о мощности, пользовательский FTP и на выходе получаем наглядный результат: сколько времени атлет провел в каждой зоне.

Именно в этом и заключается философия моего проекта: дать мощные аналитические инструменты в руки обычных спортсменов-любителей. И для этого не нужно быть гуру программирования — достаточно немного Python, NumPy и желания сделать лучше.

Шаг 4: Оживляем графики. Интерактивность — ключ к пониманию

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

На первый взгляд, это просто красивые диаграммы. Но настоящая магия начинается, когда графики становятся интерактивными.

Связка «График-Карта»: Где именно я так страдал?

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

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

Под капотом это работает благодаря обработчику onHover в Chart.js и паре строк кода, которые связывают данные графика с объектом карты из Leaflet.js:

// Упрощенный фрагмент из activity_renderer.js в Peakline  // Внутри настроек графика Chart.js onHover: (event, chartElement) => {     // chartElement - это то, на что мы навели курсор     if (mapInstance && trackMarker && streams.latlng) {         if (chartElement.length > 0) {             // Получаем индекс точки данных (например, 512-я секунда трека)             const dataIndex = chartElement[0].index;             // По этому индексу находим GPS-координаты             const coords = streams.latlng.data[dataIndex];                          if (coords) {                 // И просто двигаем маркер на карте в эту точку                 trackMarker.setLatLng(coords);                 trackMarker.addTo(mapInstance);             }         } else {             // Убрали курсор с графика - убрали маркер с карты             trackMarker.remove();         }     } }     

Зум: Фокус на главном

Вторая важная интерактивная возможность — это зум. Вся 2-часовая тренировка — это хорошо, но что если вас интересует конкретный 5-минутный интервал? Нет проблем. Просто выделите нужную область на графике мышкой, и он приблизится, показывая только этот фрагмент в деталях. Это стандартная функция плагина chartjs-plugin-zoom, но она кардинально меняет пользовательский опыт.

Контекст — это всё

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

Но и это не всё. Помните, мы говорили про контекст? Почему в прошлый вторник при той же мощности ехалось легче, чем сегодня? Возможно, дело в погоде. И Peakline пытается дать ответ на этот вопрос, подтягивая температуру и направление ветра для каждой тренировки. В планах — реализовать полноценный «Weather Impact» отчет, который будет показывать, как погода в цифрах повлияла на ваш результат.


Шаг 5: Что дальше? От анализатора к «умному тренеру»

Все, что я описал выше — это уже работающий функционал Peakline. Но самое интересное еще впереди. Моя главная цель — превратить проект из инструмента, который отвечает на вопрос «что было?», в персонального помощника, который отвечает на вопрос «а что дальше?».

В планах — внедрение элементов машинного обучения, чтобы Peakline мог:

  • Предсказывать вашу производительность. Анализируя тренды, сервис мог бы подсказывать: «Судя по последним тренировкам, вы вышли на плато. Попробуйте добавить одну высокоинтенсивную тренировку в неделю. Вероятно, вы будете готовы к рекорду на вашем любимом сегменте через ~3 недели».

  • Оценивать уровень усталости и готовности. На основе динамики пульса, мощности и других метрик рассчитывать «индекс восстановления», помогая избежать перетренированности.

  • Генерировать простые тренировочные планы. Пользователь ставит цель («пробежать 10 км быстрее 50 минут»), а Peakline предлагает базовый план, адаптированный под его текущий уровень.

Это амбициозные цели, но именно они превращают Peakline из простого pet-проекта в настоящего «умного тренера» в кармане каждого атлета.

Заключение: Путь важнее цели

Создание Peakline началось с простого желания — получить больше от своих данных. Этот путь провел меня от парсинга XML-файлов до погружения в тонкости физиологии спорта и обратно к коду на Python.

Я хотел поделиться этой историей, чтобы показать: вам не нужна огромная команда или бюджет, чтобы создать что-то по-настоящему полезное. Иногда достаточно любопытства, открытых данных и мощи современных инструментов вроде Python, NumPy и FastAPI.

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

Проект активно развивается, и лучший источник идей — это отзывы реальных пользователей. Поэтому буду счастлив услышать ваше мнение, идеи или сообщения об ошибках в комментариях к этой статье или в нашем небольшом, но уютном Telegram-сообществе.

Советую опробовать самому, буду рад фидбеку и советам — проект бесплатный и народный. Peakline: Strava Segment- & Trainingsanalyse.

Спасибо, что дочитали, и удачных вам тренировок.


ссылка на оригинал статьи https://habr.com/ru/articles/925262/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *