Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

English | Русский

F1 Race Pace & Tyre Degradation — Hungarian GP 2026

Lap-level analysis of race pace and tyre degradation using FastF1. Single race, laps only, no telemetry.

Lap time vs tyre life

Conclusion

Ranking drivers by median clean-lap time gives a different order than ranking by fastest lap: GAS is 6th on his single best lap (82.871 s) but 11th on median pace (86.599 s), a 3.7 s spread against 2.0 s for the race-pace leader NOR. Fitting LapTimeSec ~ TyreLife per stint yields median slopes of +0.042 s/lap (HARD), +0.028 (MEDIUM) and +0.009 (SOFT) — the reverse of the expected order. This is not a calculation error: the slope measures tyre wear minus fuel burn, and since compounds are not run at comparable fuel loads, stint lengths or race phases, a single race cannot separate the two effects.

Data

  • Source: FastF1 3.8.3, 2026 Hungarian Grand Prix, Race
  • 22 drivers, 1431 raw laps, 1231 after cleaning

Defining a clean lap

Each filter is a separate step with a row count printed after it, so a filter removing an unexpected number of rows is visible immediately. This caught a real bug during development: a positional slice (clean[1:]) that was meant to drop the standing-start lap removed exactly one row instead of twenty-one.

Step Rule Laps left
raw 1431
has lap time LapTime not null 1429
known compound Compound != 'None' 1404
no pit laps PitInTime and PitOutTime both null 1314
no first lap LapNumber != 1 1293
green flag TrackStatus == '1' 1231
within 107% lap time <= 1.07 × driver median 1231

Notes:

  • Compound == 'None' is a literal string, not a null, and it coincides exactly with the 25 rows where TyreLife is missing. One filter handles both.
  • TrackStatus concatenates codes when the status changes mid-lap, producing values like '12' or '2671'. Matching the exact string '1' keeps only laps run entirely under green. A lap where a VSC starts halfway is dropped whole — a deliberate trade of sample size for consistency.
  • No safety car in this race: code 4 never appears. Codes 6 and 7 (VSC) account for most of the 84 non-green laps.
  • The standing start is not flagged as a pit out, so LapNumber != 1 is needed as its own step.
  • The 107% cut removed zero laps. After the earlier filters the widest deviation from a driver's own median was +4.4%, so the threshold was already satisfied. The step is kept because it would bind on a race with more disruption.
  • FastF1's own IsAccurate flag marks 1295 laps. This set is stricter (1231), as expected: IsAccurate does not exclude VSC laps.

Race pace

groupby('Driver')['LapTimeSec'].agg(['min', 'median', 'count']), sorted by median. The count column matters: BOT (12 laps) and PER (19 laps) retired early and their medians are not comparable to drivers with ~60 laps. They are excluded from any interpretation.

A per-compound pivot (pivot_table over Driver × Compound) shows inconsistent signs — SOFT is faster than HARD for GAS (−2.0 s) but slower than MEDIUM for ALO (+1.9 s). This is fuel load, not rubber: the same car is roughly a second slower on a full tank, so whoever ran a compound late in the race gets a flattering median. The pivot is reported to motivate the per-stint approach below, not as a measurement of compound pace.

Degradation

Two linear fits:

  1. Pooled — one line per compound across all laps: +0.004 / +0.022 / +0.048 s/lap for SOFT / MEDIUM / HARD.
  2. Per stint — one line per (Driver, Stint) with at least 6 laps, then the median across the 65 stints: +0.009 / +0.028 / +0.042.

The per-stint version is the one to trust. Pooling mixes drivers whose baseline pace differs by up to 5 s, so the pooled line partly fits the gap between cars rather than the wear within a stint. The scatter plot shows this directly: at any fixed tyre age the vertical spread is 6–10 s, while the effect being measured is under 1 s across a whole stint.

Refitting on DeltaToMedian (lap time minus that driver's median) returns identical slopes, confirming that removing a per-group constant shifts the intercept only.

Why HARD looks worse than SOFT

  • Stint context is not random. HARD stints average 21 laps (max 43), SOFT 17. The 18 laps run past 35 laps of tyre life all belong to two midfield drivers (BOR, LIN) on a one-stop, whose own medians are 87.1 and 86.7 s. Their pace, not their rubber, lifts the HARD slope.
  • SOFT is noisy. Slope std is 0.117 s/lap against a median of 0.009 — an order of magnitude larger than the effect, ranging from −0.31 to +0.25 across stints. HARD is far more consistent (std 0.044, median 0.042). Raising the minimum stint length from 6 to 10 laps drops two SOFT stints and brings the std to 0.087, still nine times the median, so the spread is a property of the compound rather than an artefact of short samples.
  • Fuel burn cannot be the explanation. It is close to constant per lap, so it is subtracted from all three compounds alike: it lowers every slope by the same amount and leaves their order intact. It explains why nine of the 65 stints come out negative, not why HARD sits above SOFT.

SQL

The cleaned laps are exported to PostgreSQL (table laps) and the aggregation steps are reimplemented as queries in queries.sql.

The pandas and SQL idioms map onto each other directly:

pandas PostgreSQL
groupby('Driver')['LapTimeSec'].agg(...) GROUP BY driver with aggregates
groupby('Driver')[...].transform('median') CTE with percentile_cont + JOIN (Q3)
groupby('Driver')[...].transform('mean') avg(...) OVER (PARTITION BY driver) (Q7)
pivot_table(index=, columns=, aggfunc='median') GROUP BY + percentile_cont(...) FILTER (WHERE ...)

The middle rows are the ones worth internalising. transform and a window function do the same thing: aggregate within a group, then broadcast the result back onto every row instead of collapsing the group. PARTITION BY is the groupby, OVER is the transform — but only for plain aggregates. PostgreSQL refuses OVER on ordered-set aggregates (percentile_cont, percentile_disc, mode), so a median broadcast has to be assembled by hand from a CTE and a join, while a mean broadcast is a one-liner.

Limitations

  • No fuel correction. Every slope is wear minus fuel burn, i.e. a lower bound on true degradation.
  • No track temperature. Merging session.weather_data by nearest timestamp (pd.merge_asof) would control for this; out of scope here.
  • Compounds are relative labels, not fixed rubber. Pirelli allocates three of the C0–C6 range per event, so "SOFT" here is not comparable to "SOFT" at another track or in another season.
  • Linear fit only. A stint's wear curve is typically flat then steep, so a straight line understates late-stint loss by construction.
  • One race, and one where overtaking is rare. Nothing here generalises to other circuits.

Reproducing

Requires Python 3.10 or newer (FastF1 3.8).

# macOS / Linux
python3 -m venv .venv
source .venv/bin/activate

# Windows
py -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt

export F1_DB_URL=postgresql://user@localhost:5432/f1
python analysis.py
psql "$F1_DB_URL" -f queries.sql

The URL carries no +psycopg2 dialect prefix: SQLAlchemy defaults to psycopg2 for postgresql://, while libpq — which psql uses — does not understand the prefixed form, so one variable serves both. If the server requires password authentication, put the credentials in ~/.pgpass (mode 600) rather than in the URL; psycopg2 goes through libpq and picks them up too.

The first run downloads session data (~1 min); later runs read from cache/, which the script creates on startup — Cache.enable_cache raises NotADirectoryError if the directory is missing.

If F1_DB_URL is unset the script skips the database and writes clean_laps.csv instead, so the analysis still runs end to end without PostgreSQL.

Files

File Purpose
analysis.py Full pipeline: load, clean, aggregate, fit, export
queries.sql The same aggregations in PostgreSQL, plus window functions
degradation.png Lap time vs tyre life by compound
requirements.txt Pinned dependencies

About

Race pace and tyre degradation analysis of the 2026 Hungarian GP — pandas, PostgreSQL, FastF1

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages