Race pace

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:

Python
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)
Output
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:

Python
from lapbox.pace import LapCleaningConfig

strict = LapCleaningPipeline(LapCleaningConfig(outlier_mad_threshold=3.5)).clean(session.laps)
print(strict.final_rows, strict.removed["outliers"])
Output
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

Python
from lapbox.pace import representative_base_time

print(round(representative_base_time(cleaning.frame["LapTimeSeconds"]), 3))
Output
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:

Python
from lapbox.pace import fuel_correct

corrected = fuel_correct(session.laps)
print(corrected[corrected["Driver"] == "VER"].head(3).round(3).to_string(index=False))
Output
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:

Python
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))
Output
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:

Python
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")
Output
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=:

Python
print(clean_air_split(session.laps, "LAW", threshold=1.0).traffic_laps)
Output
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

Python
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]])
Output
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:

Python
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})")
Output
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.