Skip to content

Osaka Marathon 2025 Example

Load the FIT file into a WorkoutData object. WorkoutData is a subclass of pandas.DataFrame — it has all the same columns, indexing, and plotting behaviour, but adds higher-level methods for computing metrics like elevation gain, best intervals, and fastest splits directly on the object.

from pathlib import Path
import chironpy

osaka = chironpy.examples(path="18360138543_ACTIVITY_Osaka_Marathon_2025.fit")
data = chironpy.WorkoutData.from_file(osaka, resample=True, interpolate=True)
data.head()
latitude longitude distance unknown_87 heartrate cadence temperature fractional_cadence unknown_135 unknown_136 record_sequence unknown_90 session lap speed elevation time is_moving grade
datetime
2025-02-24 00:15:21+00:00 34.686225 135.520607 0.000 146.0 80.0 94.0 22.0 0.0 176.0 80.0 0.0 6.0 0.0 0.0 1.7640 23.40 0 True 0.000000
2025-02-24 00:15:22+00:00 34.686248 135.520601 2.572 116.8 80.8 93.6 22.0 0.1 176.0 80.8 0.2 6.0 0.0 0.0 1.8778 23.36 1 True -0.029455
2025-02-24 00:15:23+00:00 34.686270 135.520595 5.144 87.6 81.6 93.2 22.0 0.2 176.0 81.6 0.4 6.0 0.0 0.0 1.9916 23.32 2 True -0.029933
2025-02-24 00:15:24+00:00 34.686293 135.520589 7.716 58.4 82.4 92.8 22.0 0.3 176.0 82.4 0.6 6.0 0.0 0.0 2.1054 23.28 3 True -0.030411
2025-02-24 00:15:25+00:00 34.686316 135.520583 10.288 29.2 83.2 92.4 22.0 0.4 176.0 83.2 0.8 6.0 0.0 0.0 2.2192 23.24 4 True -0.030890

WorkoutData normalises data to a consistent set of standard columns regardless of the source file format. For example, enhanced_speed and enhanced_altitude from Garmin devices are normalised to speed and elevation. Any other additional columns present in the original file are included alongside the standard ones.

Plot data over workout duration

WorkoutData adds a time column — integer seconds since workout start — use it as the x-axis for duration-based plots.

data.plot(x="time", y="speed", title="Speed over time", xlabel="Duration (s)", ylabel="Speed (m/s)")
<Axes: title={'center': 'Speed over time'}, xlabel='Duration (s)', ylabel='Speed (m/s)'>
No description has been provided for this image
data.plot(x="time", y="heartrate", title="Heart Rate over Time", xlabel="Duration (s)", ylabel="Heart Rate (bpm)", color="red")
<Axes: title={'center': 'Heart Rate over Time'}, xlabel='Duration (s)', ylabel='Heart Rate (bpm)'>
No description has been provided for this image

Plot data over workout distance

Use the distance column (metres) as the x-axis.

data.plot(x="distance", y="speed", title="Speed over distance", xlabel="Distance (m)", ylabel="Speed (m/s)")
<Axes: title={'center': 'Speed over distance'}, xlabel='Distance (m)', ylabel='Speed (m/s)'>
No description has been provided for this image
data.plot(x="distance", y="cadence", title="Cadence over distance", xlabel="Distance (m)", ylabel="Cadence (rpm)", color="green")
<Axes: title={'center': 'Cadence over distance'}, xlabel='Distance (m)', ylabel='Cadence (rpm)'>
No description has been provided for this image

Mean max

mms = data["speed"].chironpy.mean_max()
mms
0 days 00:00:01    5.533000
0 days 00:00:02    5.528375
0 days 00:00:03    5.523750
0 days 00:00:04    5.519125
0 days 00:00:05    5.514500
                     ...   
0 days 02:27:10    4.660766
0 days 02:27:11    4.660490
0 days 02:27:12    4.660201
0 days 02:27:13    4.659899
0 days 02:27:14    4.659584
Name: mean_max_speed, Length: 8834, dtype: float64
mm = data.chironpy.mean_max(["speed", "heartrate"])
mm
mean_max_speed mean_max_heartrate
0 days 00:00:01 5.533000 202.000000
0 days 00:00:02 5.528375 201.900000
0 days 00:00:03 5.523750 201.800000
0 days 00:00:04 5.519125 201.725000
0 days 00:00:05 5.514500 201.660000
... ... ...
0 days 02:27:10 4.660766 183.844168
0 days 02:27:11 4.660490 183.832771
0 days 02:27:12 4.660201 183.821286
0 days 02:27:13 4.659899 183.809714
0 days 02:27:14 4.659584 183.798053

8834 rows × 2 columns

mm["mean_max_heartrate"].plot(
    title="Mean Max Heart Rate", xlabel="Duration (s)", ylabel="Heart Rate (bpm)", color="red"
)
<Axes: title={'center': 'Mean Max Heart Rate'}, xlabel='Duration (s)', ylabel='Heart Rate (bpm)'>
No description has been provided for this image

Performance Benchmarks

Best efforts (fastest time) and average heart rate over key race distances, computed using fastest_distance_intervals() and best_distance_intervals().

from IPython.display import Markdown

distances = [1000, 5000, 10000, 21100, 42200]
labels = ["1 km", "5 km", "10 km", "21.1 km", "42.2 km"]

fastest = data.fastest_distance_intervals(distances)
best_hr = data.best_distance_intervals(distances, "heartrate")

rows = []
for i, label in enumerate(labels):
    f = fastest[i]
    hr = best_hr[i]
    if f is None:
        rows.append(f"| {label} | — | — | — |")
    else:
        secs = int(f["value"])
        h, rem = divmod(secs, 3600)
        m, s = divmod(rem, 60)
        time_str = f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
        pace_secs = f["value"] / distances[i] * 1000  # sec/km
        pm, ps = divmod(int(pace_secs), 60)
        pace_str = f"{pm}:{ps:02d} /km"
        hr_str = f"{hr['value']:.0f} bpm" if hr else "—"
        rows.append(f"| {label} | {time_str} | {pace_str} | {hr_str} |")

table = "| Distance | Time | Pace | Heart Rate |\n"
table += "|----------|------|------|------------|\n"
table += "\n".join(rows)

Markdown(table)
Distance Time Pace Heart Rate
1 km 3:10 3:10 /km 194 bpm
5 km 16:47 3:21 /km 192 bpm
10 km 34:03 3:24 /km 191 bpm
21.1 km 1:12:31 3:26 /km 189 bpm
42.2 km 2:26:16 3:27 /km 184 bpm

Laps

> Work in progressWorkoutData does not yet have a dedicated .laps() method. The lap column is included from the FIT file (zero-indexed lap counter), but because the DataFrame is resampled and interpolated at 1 Hz, the column contains fractional values at lap boundaries. Round to integer before grouping.

Laps can be accessed by grouping on the lap column:

from IPython.display import Markdown
from chironpy.metrics.vert import elevation_gain as _elevation_gain

data["lap_int"] = data["lap"].round().astype(int)

laps = data.groupby("lap_int")

rows = []
for lap_num, group in laps:
    dist = group["distance"].max() - group["distance"].min()
    secs = int(group["time"].max() - group["time"].min())
    h, rem = divmod(secs, 3600)
    m, s = divmod(rem, 60)
    time_str = f"{h}:{m:02d}:{s:02d}" if h else f"{m}:{s:02d}"
    pace_secs = secs / dist * 1000 if dist &gt; 0 else None
    if pace_secs:
        pm, ps = divmod(int(pace_secs), 60)
        pace_str = f"{pm}:{ps:02d} /km"
    else:
        pace_str = "—"
    avg_hr = f"{group['heartrate'].mean():.0f} bpm" if "heartrate" in group else "—"
    avg_cad = f"{group['cadence'].mean():.0f} rpm" if "cadence" in group else "—"
    net_elev = group["elevation"].iloc[-1] - group["elevation"].iloc[0] if "elevation" in group.columns else None
    elev_str = f"{net_elev:+.0f} m" if net_elev is not None else "—"
    rows.append(f"| {int(lap_num) + 1} | {dist:.0f} m | {time_str} | {pace_str} | {avg_hr} | {avg_cad} | {elev_str} |")

table = "| Lap | Distance | Time | Avg Pace | Avg Heart Rate | Avg Cadence | Net Elevation |\n"
table += "|-----|----------|------|----------|----------------|-------------|---------------|\n"
table += "\n".join(rows)

Markdown(table)
Lap Distance Time Avg Pace Avg Heart Rate Avg Cadence Net Elevation
1 5046 m 17:22 3:26 /km 164 bpm 94 rpm -13 m
2 5038 m 17:15 3:25 /km 178 bpm 93 rpm -3 m
3 5051 m 17:23 3:26 /km 189 bpm 93 rpm -5 m
4 5034 m 17:32 3:28 /km 192 bpm 93 rpm +1 m
5 1093 m 3:48 3:28 /km 191 bpm 93 rpm +1 m
6 3900 m 13:28 3:27 /km 188 bpm 92 rpm +1 m
7 4983 m 17:10 3:26 /km 187 bpm 92 rpm +2 m
8 5005 m 17:19 3:27 /km 188 bpm 92 rpm -1 m
9 5000 m 17:45 3:33 /km 183 bpm 90 rpm -4 m
10 2239 m 8:03 3:35 /km 186 bpm 90 rpm +5 m