Guides
Comparing two laps
Match two laps by track position, read the time gap, find where time changes hands, and know when a pair can’t be compared.
Who was quicker, where, and by how much — the question every qualifying session asks. This guide compares Verstappen’s and Norris’s fastest laps from Silverstone 2025 qualifying, then a pair from China 2026 that lapbox refuses to compare, and why.
Compare two laps
compare takes two laps’ telemetry and returns a Comparison. Driver A’s lap sets the distance axis.
import fastf1
from lapbox.telemetry import compare
session = fastf1.get_session(2025, "Silverstone", "Q")
session.load(weather=False, messages=False)
ver = session.laps.pick_drivers("VER").pick_fastest().get_telemetry()
nor = session.laps.pick_drivers("NOR").pick_fastest().get_telemetry()
result = compare(ver, nor, driver_a="VER", driver_b="NOR")
print(result.matched, round(result.residual, 1))True 5.1
Read Comparison.matched first. It is True here: after alignment the two speed traces are 5.1 km/h RMS apart, well under the 15 km/h limit.
Why the laps are matched first
FastF1’s Distance is integrated from speed, so two laps of the same circuit disagree on their total length (by up to 606 m in one measured session) and can start from slightly different points. Comparing them at equal distances lines one car’s corner up against the other’s straight: over a season of LapBox comparisons, 14 % of driver pairs compared that way gave corner speeds more than 40 km/h apart — up to 181 km/h, which is impossible between two F1 cars.
So align puts lap B onto lap A’s basis first: it stretches B to A’s lap length, then slides it by the offset that best lines up the two speed traces, to a fraction of a metre. What’s left over is the residual, the RMS speed difference after alignment.
Some laps still can’t be matched: a telemetry fault can stretch one part of a lap, and no single shift undoes that. Above MAX_ALIGNMENT_RESIDUAL (15 km/h), matched is False. The threshold was measured on 250 driver pairs: it caught 32 of 33 physically impossible pairs and withheld 8 of 217 sound ones.
The time gap
delta_time gives the running gap at every point of the lap. Positive means driver A is ahead.
from lapbox.telemetry import delta_time
gap = delta_time(result.aligned)
print(f"at the line: {gap.iloc[-1]:+.3f} s")
print(f"VER's biggest lead: {gap.max():+.3f} s at {result.aligned['Distance'][gap.idxmax()]:.0f} m")at the line: +0.118 s VER's biggest lead: +0.324 s at 4986 m
The shape of each lap comes from integrating its speed; each lap is then scaled to its own measured lap time, so the gap ends exactly at the official one. In between it is as good as the alignment: on four qualifying sessions it was a median 29–42 ms off the official gap at the sector lines (at most 143 ms).
Where time changes hands: segments
Comparison.segments (segment_gaps) splits the lap at its speed peaks — from the end of one straight, through the corners after it, to the end of the next — where a few metres of misplacement cost almost nothing:
print(result.segments.round(3).to_string(index=False)) segment start_distance end_distance time_a time_b time_delta
1 0.084 344.149 4.186 4.249 0.063
2 344.149 962.299 9.501 9.495 -0.006
3 962.299 1778.724 13.451 13.560 0.109
4 1778.724 2956.708 18.425 18.351 -0.074
5 2956.708 4764.506 22.011 22.213 0.202
6 4764.506 5347.666 7.581 7.524 -0.058
7 5347.666 5825.858 9.736 9.619 -0.117Verstappen gained 0.202 s between 2,957 and 4,765 m and lost 0.117 s over the last segment; the seven time_delta values add up to the +0.118 s at the line. On six qualifying sessions, the largest gaining and losing segments were 65 ms from the same stretch rebuilt from the cars’ positions, against 113 ms for the curve’s largest rise and fall. A segment often holds several corners.
Minisectors
Comparison.minisectors (minisector_dominance) splits the lap into 21 equal-distance pieces and gives each to the driver who spent less time in it:
ms = result.minisectors
print(result.summary)
close = (ms["time_a"] - ms["time_b"]).abs() < 0.020
print(f"{close.sum()} of {len(ms)} minisectors decided by less than 20 ms"){'minisectors_a': 14, 'minisectors_b': 7, 'more_minisectors': 'VER', 'max_speed_a': 324.0, 'max_speed_b': 319.0}
4 of 21 minisectors decided by less than 20 msCorner speeds and speed delta
Find the corners on lap A with detect_corners, then read both drivers’ minimum speed through each with corner_speeds:
from lapbox.telemetry import corner_speeds, detect_corners
apexes = detect_corners(ver)["apex_distance"].tolist()
corners = corner_speeds(result.aligned, apexes)
print(corners.head(5).round(1).to_string(index=False)) corner distance speed_a speed_b
1 893.8 110.3 112.6
2 1032.4 89.0 88.5
3 2008.2 180.0 180.8
4 2161.6 116.0 120.2
5 3171.1 304.0 300.0Corners are found where the car slows down, so the numbering won’t match the circuit map (see Reading one lap). For any channel’s difference along the lap, use channel_delta:
from lapbox.telemetry import channel_delta
speed = channel_delta(result.aligned, "Speed")
print(f"VER up to {speed.max():.1f} km/h faster, NOR up to {-speed.min():.1f} km/h faster")VER up to 21.2 km/h faster, NOR up to 16.1 km/h faster
When a pair can’t be compared
Albon against Bottas, fastest laps of China 2026 qualifying:
china = fastf1.get_session(2026, "China", "Q")
china.load(weather=False, messages=False)
alb = china.laps.pick_drivers("ALB").pick_fastest().get_telemetry()
bot = china.laps.pick_drivers("BOT").pick_fastest().get_telemetry()
refused = compare(alb, bot, driver_a="ALB", driver_b="BOT")
print(refused.matched, round(refused.residual, 1))
print(refused.frozen_a.round(1).to_string(index=False))
print(refused.frozen_b.round(1).to_string(index=False))False 57.4
start_distance end_distance seconds speed sentinel
544.2 775.0 2.5 335.0 True
start_distance end_distance seconds speed sentinel
2.6 194.4 3.2 213.0 True
5189.4 5257.3 1.3 191.0 TrueBoth laps have frozen car data: the timing feed repeated one reading — speed, RPM, gear and throttle unchanged, with a throttle above 100 % — for seconds while the car drove on. Distance is integrated from that frozen speed, so the rest of each lap is misplaced and no alignment can match them. Comparison.frozen_a and Comparison.frozen_b say where (detect_frozen_stretches). On 2026 China, Suzuka and Monza qualifying, such a lap was behind every one of the 57 refused pairs.

refused.plot(channels=["Speed", "Throttle"]): the chart says why and leaves the gap out. The traces visibly disagree on where the corners are.Each lap on its own is still worth reading — its trace, its top speed — but nothing that compares the two point by point: no gap, speed delta, corner speeds, minisectors or segments.
Aligning without compare
compare is the usual entry point. For your own analysis on the matched laps, call align directly and check alignment_residual yourself:
from lapbox.telemetry import MAX_ALIGNMENT_RESIDUAL, align, alignment_residual
aligned = align(ver, nor, num_points=500)
print(list(aligned.columns))
print(f"{alignment_residual(aligned):.1f} km/h (limit {MAX_ALIGNMENT_RESIDUAL})")['Distance', 'Speed_a', 'Speed_b', 'Throttle_a', 'Throttle_b', 'Brake_a', 'Brake_b', 'RPM_a', 'RPM_b', 'nGear_a', 'nGear_b', 'DRS_a', 'DRS_b', 'Time_a', 'Time_b'] 5.1 km/h (limit 15.0)
Time_a and Time_b are each driver’s own clock at that point, added when both laps carry FastF1’s Time. delta_time, channel_delta, corner_speeds and segment_gaps all take this frame.
These docs describe lapbox 0.1.0. Something wrong or missing? Open an issue.