lapbox.telemetry

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 compare returns: 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 align put 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_b from 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 % Brake channel.
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.
Zone
A contiguous stretch of a lap between two distances. The zone functions return their rows as a DataFrame; Zone is the record behind each row.
ANALYSIS_CHANNELS
The channels resample and align carry 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

from lapbox.telemetry import compare
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

Comparison

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.

Example
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 line

Comparisondataclass

from lapbox.telemetry import Comparison
class Comparison

What 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_residual of aligned: 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_stretches of lap A: where its car data froze.
frozen_b: pd.DataFrame
The same for lap B.

Comparison.matchedproperty

property
Comparison.matched -> bool

Whether 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

method
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

from lapbox.telemetry import align
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_CHANNELS both 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 (Time or TimeSeconds): 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

from lapbox.telemetry import alignment_residual
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 (or Comparison.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

from lapbox.telemetry import MAX_ALIGNMENT_RESIDUAL
MAX_ALIGNMENT_RESIDUAL = 15.0

The 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

from lapbox.telemetry import delta_time
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 (or Comparison.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

from lapbox.telemetry import channel_delta
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 (or Comparison.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

from lapbox.telemetry import corner_speeds
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 (or Comparison.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 no Speed columns.

minisector_dominancefunction

from lapbox.telemetry import minisector_dominance
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

from lapbox.telemetry import segment_gaps
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 (or Comparison.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 to delta_time at 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

from lapbox.telemetry import resample
resample(tel, num_points=1000, channels=None)

Interpolate telemetry channels onto a uniform distance grid.

Parameters

tel: pd.DataFrame
Telemetry with a Distance column.
num_points: int = 1000
Evenly spaced samples across the lap.
channels: tuple[str, ...] | None = None
Channels to interpolate; defaults to those of ANALYSIS_CHANNELS present. Any numeric column works, e.g. ("Speed", "X", "Y") for gforces.

Returns

pd.DataFrame with Distance and one column per channel.

Raises

  • ValueError: no Distance column, or fewer than two distinct distances.

detect_cornersfunction

from lapbox.telemetry import detect_corners
detect_corners(tel, *, prominence=12.0, min_separation=60.0)

Corner apexes, found as prominent local speed minima.

Parameters

tel: pd.DataFrame
Telemetry with Distance and Speed.
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

from lapbox.telemetry import corner_analysis
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

from lapbox.telemetry import detect_braking_zones
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, Brake and ideally Speed.
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: no Distance column.

detect_full_throttle_zonesfunction

from lapbox.telemetry import detect_full_throttle_zones
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: no Distance column.

gforcesfunction

from lapbox.telemetry import gforces
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; 0 turns 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

from lapbox.telemetry import channel_summary
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: no Distance column.

detect_frozen_stretchesfunction

from lapbox.telemetry import detect_frozen_stretches
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 (Time or TimeSeconds), Distance, Speed, RPM, nGear and Throttle. Car samples are the rows with Source == "car"; without Source every 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

from lapbox.telemetry import TelemetryAnalyzer
TelemetryAnalyzer(*, corner_prominence=12.0)

Everything above for one lap in one call.

Parameters

corner_prominence: float = 12.0
Passed to detect_corners as prominence.

TelemetryAnalyzer.analyzemethod

method
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

LapAnalysis

LapAnalysisdataclass

from lapbox.telemetry import LapAnalysis
class LapAnalysis

What 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

from lapbox.telemetry import Zone
class Zone

A 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

Zone.lengthproperty

property
Zone.length -> float

end_distance - start_distance (m).

ANALYSIS_CHANNELSconstant

from lapbox.telemetry import ANALYSIS_CHANNELS
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.