Fast, pythonic access to MotoGP data — inspired by FastF1
Project description
PyMotoGP
Fast, pythonic access to MotoGP data — inspired by FastF1.
PyMotoGP gives you session-centric access to qualifying, sprint, and race data from the official PulseLive API and DORNA timing PDFs. Load a session, get a pandas DataFrame of laps, plot pace, run analytics, predict race outcomes.
import motogp
session = motogp.load(2024, 'qatar', 'Q2')
print(session.best_lap.rider_name) # 'Jorge MARTIN'
df = session.laps.to_dataframe() # full lap data, sectors, top speeds
session.plot.lap_times() # matplotlib figure
session.analysis.gap_to_pole() # gap analysis
Install
pip install pymotogp
Requires Python 3.10+. Core dependencies: requests, pandas, pdfplumber,
matplotlib.
Quickstart
Load a session
import motogp
# Year + event + session label. Event matches by name, country, or short code.
session = motogp.load(2024, 'cataluña', 'Q2')
Supported session labels: Q1, Q2, qualifying, FP1, FP2, practice,
sprint, warm-up, race.
Stay current with the season
motogp.get_event_schedule(2026) # calendar DataFrame with a `finished` flag
motogp.get_event_schedule() # None → current season
# Sync every finished GP of the season into the local cache — laps,
# classification, and Analysis PDFs. Safe to re-run after each race weekend;
# already-cached sessions are skipped.
report = motogp.update(2026)
# Or narrow it down:
motogp.update(2026, events=['assen', 'GER'], sessions=['Q2', 'SPR', 'RAC'])
update() returns a per-session report (riders, laps, status). Sessions from
a race weekend that just ended may show classification-only or empty until
DORNA publishes the Analysis PDF — rerun later to fill them in.
Automated updates (no manual syncing)
python -m motogp [year] runs the same sync from the command line, so any
scheduler can keep your data current. On macOS use the bundled launchd agent
(preferred over cron — it runs missed jobs when the Mac wakes):
cp scripts/com.pymotogp.update.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.pymotogp.update.plist
It syncs every Monday at 09:00 and logs to ~/.motogp/update.log. Edit the
plist to change the python path, repo location, or schedule. On Linux, the
cron equivalent is: 0 9 * * 1 cd /path/to/PyMotoGP && python3 -m motogp.
Prefer to keep your machine out of it entirely? The repo ships a scheduled
GitHub Actions workflow (.github/workflows/update-data.yml) that runs the
same sync on GitHub's runners every Monday 08:00 UTC and commits per-session
JSON exports to data/<year>/ (python -m motogp --export data). It can
also be triggered manually from the Actions tab.
Inspect laps
df = session.laps.to_dataframe()
# Columns: rider_name, lap_number, lap_time_ms, sector1_ms ... sector4_ms,
# top_speed, is_valid, is_cancelled, is_pit, is_best
session.best_lap # fastest valid lap
session.riders # list of rider names
session.classification # official position list
Plot
fig = session.plot.lap_times(riders=['Bagnaia', 'Martin'])
fig.savefig('out.png')
session.plot.pace_distribution() # boxplot per rider
session.plot.sector_comparison() # grouped bars
session.plot.compare('Bagnaia', 'Martin') # 2-panel comparison
All plotting methods return a matplotlib.figure.Figure. Cancelled laps,
pit laps, and outlaps (>1.03× that rider's own best) are filtered by default,
with a secondary cap at 1.10× the field best to handle riders with no flying lap.
Analyze
session.analysis.theoretical_best() # sum of best sectors per rider
session.analysis.gain_potential('Bagnaia') # ms left on the table
session.analysis.gap_to_pole()
session.analysis.sector_strength()
session.analysis.consistency_ranking()
Cross-season history
from motogp import HistoricalAnalyzer
hist = HistoricalAnalyzer()
hist.track_evolution('catalunya') # pole-time progression by year
hist.rider_form('Bagnaia', year=2024) # per-round qualifying form
hist.team_pace(year=2024) # aggregate by team
Predict race pace (transparent baseline)
from motogp import RacePaceEstimator
qual = motogp.load(2024, 'malaysia', 'Q2')
est = RacePaceEstimator()
est.predict(qual, n_laps=20)
The estimator uses a transparent linear model:
race_lap(n) = q_best + race_offset + degradation × (n − 1). All assumptions
are exposed as parameters. See "Honest limits" below for accuracy data.
Data sources
PyMotoGP uses a two-tier resolution strategy:
- Local cache (instant) — if you have a directory of pre-scraped JSON
files, set
MOTOGP_SCRAPER_OUTPUTto point at it. Hits return instantly. - PulseLive API (live) — falls back to the official MotoGP API:
api.motogp.pulselive.com/motogp/v1/. Discovers the session, downloads the official Analysis PDF, parses it withpdfplumber. PDFs are cached at~/.motogp_pdfs/.
No API key required. Be considerate — the library caches everything.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
MOTOGP_SCRAPER_OUTPUT |
~/.motogp/scraper_output |
Pre-scraped JSON cache |
MOTOGP_PDF_CACHE |
~/.motogp_pdfs |
DORNA Analysis PDF cache |
Honest limits
PyMotoGP includes a backtest pipeline so you can measure model accuracy instead of trusting it blindly.
from motogp.analysis import RacePaceValidator
v = RacePaceValidator()
df = v.validate_season(2024)
v.summarize(df)
2024 season backtest results (20 GP races validated):
| Metric | Value |
|---|---|
| Winner hit rate | 30% |
| Podium overlap (mean of 3) | 1.2 |
| Position MAE | 3.5 places |
| Kendall's tau | 0.24 |
Treat RacePaceEstimator.predict() as a directional baseline, not a
black-box predictor. The model assumes qualifying pace transfers linearly
to race pace with uniform degradation — which is wrong for ~70% of MotoGP
races because tire management, race craft, weather, and DNFs aren't
captured. Calibrate per-track with est.calibrate_from_race(qual, race)
once you have real race data.
Architecture
motogp/
├── core/ # Session, Lap, Sector, Rider data models
├── api/ # PulseLive client + DORNA PDF parser
├── plots/ # Matplotlib-based session plots
├── analysis/ # SessionAnalyzer, HistoricalAnalyzer,
│ # RacePaceEstimator, RacePaceValidator
Roadmap
- ✅ Phase 1 — PulseLive API wrapper, core data models, caching
- ✅ Phase 2 — Real data integration (scraper cache + PulseLive + PDFs)
- ✅ Phase 3 — Session/historical/race-pace analytics + validation
- 🚧 Phase 4 — Sphinx docs, VCR-cassette integration tests, PyPI release
Contributing
Issues and PRs welcome. Run tests with:
pip install -e ".[dev]"
pytest
License
Not affiliated with Dorna Sports or MotoGP™. All data is sourced from publicly available APIs and PDFs for personal and research use.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pymotogp-0.2.0.tar.gz.
File metadata
- Download URL: pymotogp-0.2.0.tar.gz
- Upload date:
- Size: 59.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
231d8bf47d0045b5dd76259d57ed0938a326564eed2cc2a67e11e0297a2e91e8
|
|
| MD5 |
ca973166bb9a73cc1b94017e704aee13
|
|
| BLAKE2b-256 |
556f6dabcab9a3bf02db1866d22a6121d81769cb9f67803f745d8ce1bebb15f5
|
Provenance
The following attestation bundles were made for pymotogp-0.2.0.tar.gz:
Publisher:
release.yml on tejred213/PyMotoGP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymotogp-0.2.0.tar.gz -
Subject digest:
231d8bf47d0045b5dd76259d57ed0938a326564eed2cc2a67e11e0297a2e91e8 - Sigstore transparency entry: 2148729644
- Sigstore integration time:
-
Permalink:
tejred213/PyMotoGP@3f40e5d8834676b252d288280218c5f71abbd849 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/tejred213
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f40e5d8834676b252d288280218c5f71abbd849 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pymotogp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pymotogp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 48.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f9509dcab7d8b70fa5842ca6d1d71813864924e26b3b1b66768471adcf043702
|
|
| MD5 |
0e665878f0a3e6988a1d1a037c582b9b
|
|
| BLAKE2b-256 |
fd6e6a74a679c2807d240ddc07f17e3834d89ceef3af0979cf698287a8f00ee8
|
Provenance
The following attestation bundles were made for pymotogp-0.2.0-py3-none-any.whl:
Publisher:
release.yml on tejred213/PyMotoGP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymotogp-0.2.0-py3-none-any.whl -
Subject digest:
f9509dcab7d8b70fa5842ca6d1d71813864924e26b3b1b66768471adcf043702 - Sigstore transparency entry: 2148729660
- Sigstore integration time:
-
Permalink:
tejred213/PyMotoGP@3f40e5d8834676b252d288280218c5f71abbd849 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/tejred213
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f40e5d8834676b252d288280218c5f71abbd849 -
Trigger Event:
push
-
Statement type: