Привет, Хабр! Меня зовут Александр, я разработчик и, как многие в 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.
Спасибо, что дочитали, и удачных вам тренировок.
Комментарии (4)
10011001010010010
05.07.2025 15:16У меня не Страва, а SportUp, но он тоже выдаёт gpx. Должно читаться вашей софтиной.
cyberscoper Автор
05.07.2025 15:16Дело в том что всё устроено так что, Аккаунт стравы (в нем как раз хранятся тренировки) и туда же загружаются gpx, у меня не альтернатива Стравы или что то подобного плана.
Рекомендую для полной картины прочитать в моем профиле самый первый пост, там уже станет куда понятнее)
nikolz
Увы
ОШИБКА 403
Запрос не может быть удовлетворён.
Дистрибутив Amazon CloudFront настроен на блокировку доступа из вашей страны. В данный момент мы не можем подключиться к серверу этого приложения или веб-сайта. Возможно, слишком большой трафик или ошибка в конфигурации. Повторите попытку позже или свяжитесь с владельцем приложения или веб-сайта.Если вы предоставляете клиентам контент через CloudFront, вы можете найти инструкции по устранению неполадок и предотвращению этой ошибки в документации CloudFront.
cyberscoper Автор
https://habr.com/ru/news/655317
Напоминаю что страва с 2022 года не доступна на территории России и Беларуси