Guides
Race pace
Clean the laps, take the fuel out, and rank drivers on true pace, clean air against traffic, consistency and the ideal lap.
Raw race lap times mix pace with fuel load, traffic, safety cars and pit stops. lapbox.pace takes those out one at a time. Every example uses the 2025 Italian Grand Prix and continues from the one before.
Clean the laps, with an audit
LapCleaningPipeline drops laps unsuitable for analysis and reports how many each step removed:
import fastf1
from lapbox.pace import LapCleaningPipeline
session = fastf1.get_session(2025, "Monza", "R")
session.load(telemetry=False, weather=False, messages=False)
cleaning = LapCleaningPipeline().clean(session.laps)
print(cleaning.initial_rows, "->", cleaning.final_rows)
print(cleaning.removed)974 -> 916
{'duplicates': 0, 'missing_laptime': 0, 'pit_laps': 41, 'inaccurate': 17, 'deleted': 0, 'implausible_time': 0, 'outliers': 0}Each threshold is a field of LapCleaningConfig. The outlier step is off by default; turn it on with a robust z-score threshold:
from lapbox.pace import LapCleaningConfig
strict = LapCleaningPipeline(LapCleaningConfig(outlier_mad_threshold=3.5)).clean(session.laps)
print(strict.final_rows, strict.removed["outliers"])888 28
The ranking functions below do their own filtering, so they take session.laps directly. Clean first for TyreDegradationModel and for your own analysis.
A circuit’s normal pace
from lapbox.pace import representative_base_time
print(round(representative_base_time(cleaning.frame["LapTimeSeconds"]), 3))83.552
representative_base_time is the median of the laps within 107 % of the best, ignoring rain, safety-car and traffic laps. With fewer than 20 such laps it falls back to the plain median: a handful of laps is a worse estimate than a contaminated many.
Take the fuel out
fuel_correct normalises every timed, non-pit lap to end-of-race fuel, taking off 0.048 s for every lap still to run:
from lapbox.pace import fuel_correct
corrected = fuel_correct(session.laps)
print(corrected[corrected["Driver"] == "VER"].head(3).round(3).to_string(index=False))Driver LapNumber Compound raw corrected VER 1 MEDIUM 87.159 84.663 VER 2 MEDIUM 84.859 82.411 VER 3 MEDIUM 83.512 81.112
It returns lap 1 too; the rankings below leave it out. The race distance defaults to the highest lap number in the laps — pass total_laps= if the laps you pass don’t reach the end.
True pace
true_pace_ranking ranks each driver’s median over representative laps — from lap 2, no pit laps, within 107 % of their best — raw and fuel-corrected:
from lapbox.pace import true_pace_ranking
pace = true_pace_ranking(session.laps)
print(pace[["driver", "laps", "raw_median", "corrected_median", "gap", "rank_change"]].head(5).round(3).to_string(index=False))driver laps raw_median corrected_median gap rank_change VER 50 82.874 81.215 0.000 0 NOR 50 82.966 81.560 0.345 1 PIA 50 82.938 81.651 0.436 -1 LEC 50 83.102 81.803 0.588 0 RUS 50 83.140 81.892 0.677 0
A positive rank_change means a driver moves up once fuel is accounted for: their quick laps came on heavy fuel. Norris and Piastri swap here, and the corrected top five matches the finishing order — one race, which proves nothing about the method.
Clean air against traffic
A car within about two seconds of the one ahead loses time in its wake. clean_air_ranking splits each driver’s fuel-corrected laps by the gap to the car ahead at the line and compares the two medians:
from lapbox.pace import clean_air_ranking, clean_air_split
print(clean_air_ranking(session.laps)[["driver", "clean_laps", "traffic_laps", "delta"]].head(3).round(3).to_string(index=False))
law = clean_air_split(session.laps, "LAW")
print(f"clean {law.clean_pace:.3f} s, traffic {law.traffic_pace:.3f} s, led {law.led_laps} laps")driver clean_laps traffic_laps delta LAW 10 40 0.647 ALB 24 26 0.542 PIA 42 8 0.492 clean 82.109 s, traffic 82.756 s, led 0 laps
Lawson spent 40 of his 50 counted laps in traffic and was 0.647 s a lap slower there. The 2 s threshold (DIRTY_AIR_S) is a rule of thumb, not a measurement; change it with threshold=:
print(clean_air_split(session.laps, "LAW", threshold=1.0).traffic_laps)35
A driver needs three laps on each side to be split; otherwise clean_air_split returns None. Gaps only count cars on the same lap, so a lapped car in front is not traffic.
Consistency
from lapbox.pace import consistency_ranking, driver_consistency
print(consistency_ranking(session.laps).head(3).round(3).to_string(index=False))
ver = driver_consistency(session.laps, "VER")
print(ver.laps, round(ver.std, 3), [(lap, round(off, 3)) for lap, off in ver.series[:3]])driver laps best mean std cv ALO 20 83.757 84.539 0.485 0.574 OCO 49 82.892 83.977 0.664 0.791 STR 49 82.164 83.972 0.748 0.891 50 0.853 [(2, 3.856), (3, 2.509), (4, 2.259)]
The spread of representative laps (from lap 2, no pit laps, within 107 % of the best), most consistent first by coefficient of variation. series holds every counted lap as seconds off the driver’s best — ready to plot.
The ideal lap
ideal_lap adds up a driver’s best three sectors and compares them with their fastest lap; biggest_loss says where that lap left the most time:
from lapbox.pace import biggest_loss, ideal_lap
ideal = ideal_lap(session.laps, "VER")
print(f"ideal {ideal.ideal:.3f} s, fastest {ideal.fastest:.3f} s (lap {ideal.fastest_lap}), gain {ideal.gain:.3f} s")
worst = biggest_loss(ideal)
print(f"most to find in S{worst.sector}: {worst.loss_on_fastest:.3f} s (best on lap {worst.lap})")ideal 80.974 s, fastest 81.003 s (lap 52), gain 0.029 s most to find in S1: 0.029 s (best on lap 42)
The ideal is rarely achievable in one lap: the best sectors usually come from laps on different fuel and tyres. It returns None rather than a misleading number when a sector was never timed on a clean lap, or when the sum comes out slower than the fastest lap. It works on any session; in qualifying it is the classic “ultimate lap”.
Next: Tyre degradation.
These docs describe lapbox 0.1.0. Something wrong or missing? Open an issue.