API reference
lapbox.telemetry
One lap on its own, and two laps matched by track position.
Every function takes FastF1 telemetry as it comes from lap.get_telemetry(), never modifies it, and returns new pandas or NumPy objects. Read Comparing two laps for why two laps are matched before they are compared.
Summary
compare- The full comparison of two laps in one call: alignment, minisectors, segments, a summary, the alignment residual and whether the laps could be matched.
Comparison- What
comparereturns: the aligned laps and everything derived from them. align- Resample both laps onto one distance grid, matched by track position.
alignment_residual- The RMS speed difference (km/h) between two aligned laps: how well
alignput them on the same piece of track. MAX_ALIGNMENT_RESIDUAL- The residual (km/h RMS) above which two laps count as not matched.
delta_time- The cumulative time gap (seconds) between two aligned laps along the lap.
channel_delta- The per-sample difference
channel_a - channel_bfrom an aligned frame. corner_speeds- Each driver’s minimum speed through every corner.
minisector_dominance- Split the lap into equal-distance minisectors and name the driver who took less time in each.
segment_gaps- Split the lap at its speed peaks and return each driver’s time between them: where time changes hands.
resample- Interpolate telemetry channels onto a uniform distance grid.
detect_corners- Corner apexes, found as prominent local speed minima.
corner_analysis- Entry, minimum and exit speed per corner, and a speed class.
detect_braking_zones- Braking zones: contiguous stretches with the brake applied. Takes a boolean or a 0–100 %
Brakechannel. detect_full_throttle_zones- Full-throttle stretches:
Throttle >= threshold. gforces- Longitudinal and lateral acceleration (g) along a lap, from speed and the racing line — no time channel needed.
channel_summary- Summary statistics across a lap’s channels.
detect_frozen_stretches- Where the timing feed’s car data froze: one reading repeated while the car moved on.
TelemetryAnalyzer- Everything above for one lap in one call.
LapAnalysis- What
TelemetryAnalyzer.analyzereturns. Zone- A contiguous stretch of a lap between two distances. The zone functions return their rows as a DataFrame;
Zoneis the record behind each row. ANALYSIS_CHANNELS- The channels
resampleandaligncarry by default: numeric and interpolatable over distance.
Two laps
Two drivers’ laps put on the same points of track, then compared. Check Comparison.matched before reading anything that compares them sample for sample.
comparefunction
compare(tel_a, tel_b, *, driver_a, driver_b, num_points=1000, num_minisectors=21)The full comparison of two laps in one call: alignment, minisectors, segments, a summary, the alignment residual and whether the laps could be matched.
Parameters
- tel_a: pd.DataFrame
- Driver A’s lap. The comparison’s distance axis is A’s.
- tel_b: pd.DataFrame
- Driver B’s lap.
- driver_a: str
- Label for driver A, used in the minisector table and the summary.
- driver_b: str
- Label for driver B.
- num_points: int = 1000
- Points on the grid of
aligned. - num_minisectors: int = 21
- Equal-distance minisectors the lap is split into.
Returns
num_points sets the grid of aligned only. The minisectors are decided on their own 4,000-point alignment, so a coarse display grid doesn’t make them noisier. The segments use aligned: on 2023 Bahrain qualifying the largest gaining and losing segments were the same at 500 and 4,000 points in 42 and 38 of 45 pairs, their times a median 5 ms apart.
summary["more_minisectors"] is a count, not who was quicker: in 2023 Bahrain qualifying LEC won 12 of 21 minisectors and VER took pole by 0.292 s. For who was quicker, read delta_time at the line.
result = compare(ver, nor, driver_a="VER", driver_b="NOR")
if result.matched:
print(f"{delta_time(result.aligned).iloc[-1]:+.3f} s") # +0.118 s: VER ahead at the lineComparisondataclass
class ComparisonWhat compare returns: the aligned laps and everything derived from them.
Fields
- driver_a: str
- Driver A’s label.
- driver_b: str
- Driver B’s label.
- aligned: pd.DataFrame
- Both laps on one distance grid, from
align. - minisectors: pd.DataFrame
- From
minisector_dominance. - summary: dict[str, float | str]
minisectors_a,minisectors_b,more_minisectors(the driver who won more of them; a tie goes to A) and, with speed,max_speed_a,max_speed_b.- residual: float
alignment_residualofaligned: km/h RMS between the two speed traces after alignment.- segments: pd.DataFrame
- From
segment_gaps: each driver’s time between the lap’s speed peaks. - frozen_a: pd.DataFrame
detect_frozen_stretchesof lap A: where its car data froze.- frozen_b: pd.DataFrame
- The same for lap B.
Comparison.matchedproperty
Comparison.matched -> boolWhether the two laps could be put on the same points of track: residual <= MAX_ALIGNMENT_RESIDUAL. When False, the time delta, speed delta, corner speeds, minisectors and segments describe the mismatch, not the drivers; don’t show them. Each lap’s own trace is unaffected. True means the traces line up; it doesn’t by itself make every derived number accurate. When False and a lap has frozen stretches, those are the cause: every refusal measured on 2026 qualifying had one.
Comparison.plotmethod
Comparison.plot(**kwargs)The time gap and both laps’ channels as a Plotly figure: lapbox.plot.comparison(self, **kwargs). Needs pip install "lapbox[plot]".
Returns
plotly.graph_objects.Figure
alignfunction
align(tel_a, tel_b, *, num_points=1000, channels=None, suffixes=('_a', '_b'))Resample both laps onto one distance grid, matched by track position.
Parameters
- tel_a: pd.DataFrame
- Lap A. It keeps its own distance basis, so apex distances taken from it stay valid against the result.
- tel_b: pd.DataFrame
- Lap B, put onto A’s basis.
- num_points: int = 1000
- Points on the shared grid.
- channels: tuple[str, ...] | None = None
- Channels to carry; defaults to those of
ANALYSIS_CHANNELSboth laps have. - suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
pd.DataFrame
Columns
- Distance: float
- A’s distance (m).
- {channel}_a, {channel}_b: float
- Each channel for both laps at the same point of track.
- Time_a, Time_b: float
- Only when both laps carry their timing (
TimeorTimeSeconds): each driver’s own clock at that point, in seconds. A place before B’s line belongs to the end of B’s lap, one lap time earlier.
Distance is integrated from speed, so two laps of the same circuit disagree on total length (up to 606 m in one measured session) and can begin from slightly different points. Sampling them at equal distances compared a corner on one lap with a straight on the other: over a season, 14 % of driver pairs gave corner speeds more than 40 km/h apart, up to 181 km/h.
So B is stretched to A’s lap length, then slid by the circular offset that best matches the two speed traces. The offset is found on its own 10,000-point grid and refined to a fraction of a sample, so it doesn’t depend on num_points.
alignment_residualfunction
alignment_residual(aligned, suffixes=('_a', '_b'))The RMS speed difference (km/h) between two aligned laps: how well align put them on the same piece of track.
Parameters
- aligned: pd.DataFrame
- Two laps from
align(orComparison.aligned). - suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
float: km/h RMS, or inf when there is no speed to compare.
Compare it against MAX_ALIGNMENT_RESIDUAL. Above it, anything that compares the laps sample for sample describes a mismatch rather than a performance difference.
MAX_ALIGNMENT_RESIDUALconstant
MAX_ALIGNMENT_RESIDUAL = 15.0The residual (km/h RMS) above which two laps count as not matched.
Measured on 250 driver pairs (September 2026): sound comparisons sat at a median residual of 7.6 km/h, 90 % below 12.2; corrupted laps at a median of 34.7. The 15 km/h cut caught 32 of 33 impossible pairs and withheld 8 of 217 sound ones. Re-checked for 0.1.0 on the ten fastest laps of 2023 Bahrain, 2025 Silverstone and 2026 China qualifying: pairs without frozen car data sat at a median of 4.2–7.3 km/h, none above 12.8, none withheld.
delta_timefunction
delta_time(aligned, suffixes=('_a', '_b'))The cumulative time gap (seconds) between two aligned laps along the lap.
Parameters
- aligned: pd.DataFrame
- Two laps from
align(orComparison.aligned). - suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
pd.Series named time_delta: time_b - time_a at every point. Positive means driver A is ahead there. It starts at 0.
It integrates dt = dx / v for each driver over the shared grid (speeds clamped to a 1 km/h floor). When the laps carry their clocks (the Time_a / Time_b columns align adds), each lap is scaled to add up to its measured lap time, so the gap ends exactly at the lap-time gap.
On 2023 Bahrain, 2025 Monza, 2025 Silverstone and 2026 Melbourne qualifying (45 pairs each) it was a median 29–42 ms (at most 143 ms) off the official gap at the sector lines. Integrating speed alone ended a median 92–113 ms from the official gap. FastF1’s own utils.delta_time is deprecated and documented as “not actually very accurate”; this one takes the name on purpose.
channel_deltafunction
channel_delta(aligned, channel, suffixes=('_a', '_b'))The per-sample difference channel_a - channel_b from an aligned frame.
Parameters
- aligned: pd.DataFrame
- Two laps from
align(orComparison.aligned). - channel: str
- A channel both laps carry:
Speed,Throttle,Brake, … - suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
pd.Series
Raises
KeyError: the aligned frame has no such channel.
corner_speedsfunction
corner_speeds(aligned, apex_distances, *, window=40.0, suffixes=('_a', '_b'))Each driver’s minimum speed through every corner.
Parameters
- aligned: pd.DataFrame
- Two laps from
align(orComparison.aligned). - apex_distances: list[float]
- Apexes on A’s lap, e.g.
detect_corners(tel_a)["apex_distance"]. - window: float = 40.0
- Metres either side of each apex to take the minimum over, so small apex-position differences don’t skew it.
- suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
pd.DataFrame, one row per corner; apexes whose window falls outside the grid are skipped.
Columns
- corner: int
- 1-based.
- distance: float
- The apex (m).
- speed_a: float
- A’s minimum speed (km/h).
- speed_b: float
- B’s minimum speed (km/h).
Raises
KeyError: the aligned frame has noSpeedcolumns.
minisector_dominancefunction
minisector_dominance(tel_a, tel_b, *, driver_a, driver_b, num_minisectors=21)Split the lap into equal-distance minisectors and name the driver who took less time in each.
Parameters
- tel_a: pd.DataFrame
- Driver A’s lap.
- tel_b: pd.DataFrame
- Driver B’s lap.
- driver_a: str
- Label for A.
- driver_b: str
- Label for B.
- num_minisectors: int = 21
- How many minisectors.
Returns
pd.DataFrame, one row per minisector.
Columns
- minisector: int
- 1-based.
- start_distance: float
- m
- end_distance: float
- m
- mean_speed_a: float
- For reference only: speed doesn’t decide a minisector.
- mean_speed_b: float
- For reference only.
- time_a: float
- A’s time in the minisector (s).
- time_b: float
- B’s time in the minisector (s).
- fastest: str
- The driver with less time; a tie goes to A.
The laps are aligned on their own 4,000-point grid and times are built as delta_time builds the gap, so the minisector times add up to the gap at the line.
Many minisectors are close: on 2023 Bahrain qualifying 29 % of 945 were decided by less than 20 ms. Read a margin of a few hundredths as a tie.
segment_gapsfunction
segment_gaps(aligned, *, prominence=20.0, suffixes=('_a', '_b'))Split the lap at its speed peaks and return each driver’s time between them: where time changes hands.
Parameters
- aligned: pd.DataFrame
- Two laps from
align(orComparison.aligned). - prominence: float = 20.0
- How far (km/h) a peak of the two laps’ mean speed must stand above its surroundings. Peaks of 10 to 50 km/h gave the same result in testing.
- suffixes: tuple[str, str] = ('_a', '_b')
- The column suffixes of the two laps in the aligned frame.
Returns
pd.DataFrame, one row per segment.
Columns
- segment: int
- 1-based.
- start_distance: float
- m
- end_distance: float
- m
- time_a: float
- A’s time in the segment (s).
- time_b: float
- B’s time in the segment (s).
- time_delta: float
time_b - time_a: positive means A gained time there. The segments add up todelta_timeat the line.
A few metres of misplacement cost the most where the car is slowest, so the gap curve can swing around a slow corner by tenths that aren’t real. Segments end at speed peaks, where misplacement costs almost nothing: on six qualifying sessions (246 matched pairs) 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.
One lap
Analysis of a single lap’s telemetry.
resamplefunction
resample(tel, num_points=1000, channels=None)Interpolate telemetry channels onto a uniform distance grid.
Parameters
- tel: pd.DataFrame
- Telemetry with a
Distancecolumn. - num_points: int = 1000
- Evenly spaced samples across the lap.
- channels: tuple[str, ...] | None = None
- Channels to interpolate; defaults to those of
ANALYSIS_CHANNELSpresent. Any numeric column works, e.g.("Speed", "X", "Y")forgforces.
Returns
pd.DataFrame with Distance and one column per channel.
Raises
ValueError: noDistancecolumn, or fewer than two distinct distances.
detect_cornersfunction
detect_corners(tel, *, prominence=12.0, min_separation=60.0)Corner apexes, found as prominent local speed minima.
Parameters
- tel: pd.DataFrame
- Telemetry with
DistanceandSpeed. - prominence: float = 12.0
- Minimum speed drop (km/h) into a minimum from the lower of its two surrounding ridges. Filters out ripples on a straight.
- min_separation: float = 60.0
- Minimum spacing between apexes (m); the slower minimum wins inside it.
Returns
pd.DataFrame
Columns
- apex_distance: float
- m
- apex_speed: float
- km/h
Prominence has no speed ceiling, so a 300 km/h kink between two 320 km/h straights counts as much as a hairpin. The count won’t match the circuit’s official corner numbering — flat-out kinks have no speed minimum — but the same corners are found on every lap of a circuit, which is what comparisons need.
corner_analysisfunction
corner_analysis(distance, speed_kmh, apex_distances, *, approach_m=80.0, exit_m=80.0, apex_window_m=25.0)Entry, minimum and exit speed per corner, and a speed class.
Parameters
- distance: array-like
- The lap’s distance (m), ideally a uniform grid from
resample. - speed_kmh: array-like
- Speed on the same grid.
- apex_distances: list[float]
- Apexes, e.g. from
detect_corners. - approach_m: float = 80.0
- Entry speed is read this far before the apex.
- exit_m: float = 80.0
- Exit speed is read this far after it.
- apex_window_m: float = 25.0
- The corner speed is the minimum within this distance of the apex.
Returns
list[dict], one per corner in distance order, with corner, apex_distance, entry_speed, min_speed, exit_speed (rounded to 0.1) and speed_class: slow under 130 km/h, medium 130–210, fast above 210. An empty list for fewer than three points.
detect_braking_zonesfunction
detect_braking_zones(tel, *, min_length=0.0)Braking zones: contiguous stretches with the brake applied. Takes a boolean or a 0–100 % Brake channel.
Parameters
- tel: pd.DataFrame
- Telemetry with
Distance,Brakeand ideallySpeed. - min_length: float = 0.0
- Drop zones shorter than this (m).
Returns
pd.DataFrame, one row per zone; empty without a Brake column.
Columns
- start_distance: float
- Where the zone starts (m).
- end_distance: float
- Where it ends (m).
- length: float
end_distance - start_distance(m).- entry_speed: float
- Speed at the zone’s first sample (km/h).
- min_speed: float
- Lowest speed in the zone (km/h).
- max_speed: float
- Highest speed in the zone (km/h).
Raises
ValueError: noDistancecolumn.
detect_full_throttle_zonesfunction
detect_full_throttle_zones(tel, *, threshold=99.0, min_length=0.0)Full-throttle stretches: Throttle >= threshold.
Parameters
- tel: pd.DataFrame
- One lap’s telemetry as
lap.get_telemetry()returns it, with FastF1’s column names (Distance,Speed,Throttle, …). Extra columns are ignored. - threshold: float = 99.0
- Throttle (%) that counts as full.
- min_length: float = 0.0
- Drop zones shorter than this (m).
Returns
pd.DataFrame with the same columns as detect_braking_zones; empty without a Throttle column.
Columns
- start_distance: float
- Where the zone starts (m).
- end_distance: float
- Where it ends (m).
- length: float
end_distance - start_distance(m).- entry_speed: float
- Speed at the zone’s first sample (km/h).
- min_speed: float
- Lowest speed in the zone (km/h).
- max_speed: float
- Highest speed in the zone (km/h).
Raises
ValueError: noDistancecolumn.
gforcesfunction
gforces(distance, speed_kmh, x, y, *, smooth_window=5)Longitudinal and lateral acceleration (g) along a lap, from speed and the racing line — no time channel needed.
Parameters
- distance: array-like
- Distance (m) on a shared, ideally uniform grid.
- speed_kmh: array-like
- Speed on that grid.
- x: array-like
- Position X on that grid (any unit: heading is scale-free).
- y: array-like
- Position Y on that grid.
- smooth_window: int = 5
- Samples of light smoothing on speed and heading before differentiating;
0turns it off.
Returns
tuple[np.ndarray, np.ndarray]: (a_long, a_lat) in g. a_long is positive accelerating, negative braking. Zeros when there are fewer than three points or the lengths differ.
a_long = v · dv/ds; a_lat = v² · dθ/ds, where θ is the heading of the racing line.
Don’t read the lateral maximum. On 1,008 clean qualifying laps (2023–2026) the single highest lateral reading was above 6 g on 554, up to 105 g, usually at the same place on every lap of a session: the position feed bends the line. The 95th percentile of |a_lat| stayed at 2.3–5.3 g and ranked drivers far more consistently (median Spearman 0.66 against 0.24 for the peak). a_long comes from the car’s speed and is physical.
channel_summaryfunction
channel_summary(tel)Summary statistics across a lap’s channels.
Parameters
- tel: pd.DataFrame
- One lap’s telemetry as
lap.get_telemetry()returns it, with FastF1’s column names (Distance,Speed,Throttle, …). Extra columns are ignored.
Returns
dict[str, float]: lap_distance, and for the channels present max_speed, mean_speed, min_speed, max_rpm, full_throttle_pct (share of samples at ≥ 99 % throttle), braking_pct, max_gear.
Raises
ValueError: noDistancecolumn.
detect_frozen_stretchesfunction
detect_frozen_stretches(tel, *, min_seconds=1.0, sentinel_seconds=0.5)Where the timing feed’s car data froze: one reading repeated while the car moved on.
Parameters
- tel: pd.DataFrame
- One lap’s telemetry with its clock (
TimeorTimeSeconds),Distance,Speed,RPM,nGearandThrottle. Car samples are the rows withSource == "car"; withoutSourceevery row counts. - min_seconds: float = 1.0
- A stretch counts when Speed, RPM, gear and throttle all repeat for this long…
- sentinel_seconds: float = 0.5
- …or for this long with a throttle above 100, which is no real value.
Returns
pd.DataFrame, one row per stretch; empty when nothing froze or the columns to judge it are missing.
Columns
- start_distance: float
- m
- end_distance: float
- m
- seconds: float
- How long the reading was held.
- speed: float
- The held speed (km/h).
- sentinel: bool
- Whether the throttle read above 100.
Distance is integrated from the frozen speed, so the rest of the lap is misplaced: the five laps behind the 2026 China and Suzuka refusals came out between 380 m short and 85 m long. On 2026 China, Suzuka and Monza qualifying every one of the 57 pairs compare refused had such a lap, and no other pair was refused; on 183 laps from 2023, 2025 and three other 2026 sessions nothing was flagged.
TelemetryAnalyzerclass
TelemetryAnalyzer(*, corner_prominence=12.0)Everything above for one lap in one call.
Parameters
- corner_prominence: float = 12.0
- Passed to
detect_cornersasprominence.
TelemetryAnalyzer.analyzemethod
TelemetryAnalyzer.analyze(tel, *, driver='')Summary, braking zones, full-throttle zones and corners of one lap.
Parameters
- tel: pd.DataFrame
- One lap’s telemetry as
lap.get_telemetry()returns it, with FastF1’s column names (Distance,Speed,Throttle, …). Extra columns are ignored. - driver: str = ''
- A label carried on the result.
Returns
LapAnalysisdataclass
class LapAnalysisWhat TelemetryAnalyzer.analyze returns.
Fields
- driver: str
- The label passed in.
- summary: dict[str, float]
channel_summary.- braking_zones: pd.DataFrame
detect_braking_zones.- full_throttle_zones: pd.DataFrame
detect_full_throttle_zones.- corners: pd.DataFrame
detect_corners.
Zonedataclass
class ZoneA contiguous stretch of a lap between two distances. The zone functions return their rows as a DataFrame; Zone is the record behind each row.
Fields
- start_distance: float
- m
- end_distance: float
- m
- entry_speed: float
- km/h
- min_speed: float
- km/h
- max_speed: float
- km/h
ANALYSIS_CHANNELSconstant
ANALYSIS_CHANNELS = ('Speed', 'Throttle', 'Brake', 'RPM', 'nGear', 'DRS')The channels resample and align carry by default: numeric and interpolatable over distance.
These docs describe lapbox 0.1.0. Something wrong or missing? Open an issue.