Comparing two laps

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.

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

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

Python
print(result.segments.round(3).to_string(index=False))
Output
 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.117

Verstappen 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:

Python
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")
Output
{'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 ms

Corner speeds and speed delta

Find the corners on lap A with detect_corners, then read both drivers’ minimum speed through each with corner_speeds:

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

Corners 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:

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

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

Both 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.

ALB vs BOT at China 2026 qualifying: speed and throttle for both laps, with the reason the pair was not matched in the title and no time gap drawn.
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:

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