The lap operations module provides helper functions for working with lap data, including filtering, coercion, and extraction utilities.
Most users interact with laps through the high-level Session and Driver APIs. These utilities are for advanced use cases and internal operations.
Overview
Lap operations include:
- Coercing lap numbers and times to standard formats
- Extracting lap numbers from DataFrames
- Getting lap column names with fallback logic
- Filtering and transforming lap data
Lap Number Operations
_coerce_lap_number
Convert various lap number formats to integer.
def _coerce_lap_number(lap_value: Any) -> int
Parameters:
lap_value: Lap number in various formats (int, str, float)
Returns:
Raises:
ValueError: If lap cannot be converted to integer or is None
Example:
from tif1.lap_ops import _coerce_lap_number
# Integer (passthrough)
lap = _coerce_lap_number(19) # 19
# String
lap = _coerce_lap_number("19") # 19
# Float
lap = _coerce_lap_number(19.0) # 19
# Invalid
try:
lap = _coerce_lap_number("invalid")
except ValueError as e:
print(f"Error: {e}")
# Error: Invalid lap number: invalid
Extract all lap numbers from a DataFrame.
def _extract_lap_numbers(laps, lib: str) -> set[int]
Parameters:
laps: DataFrame with lap data containing LapNumber or lap column
lib: library (“pandas” or “polars”)
Returns:
- Set of unique lap numbers (for fast membership checks)
Example:
from tif1.lap_ops import _extract_lap_numbers
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Get all lap numbers
lap_numbers = _extract_lap_numbers(laps, session.lib)
print(f"Total laps: {len(lap_numbers)}")
# Total laps: 44
# Check if specific lap exists
if 19 in lap_numbers:
print("Lap 19 exists")
Lap Time Operations
_coerce_lap_time
Convert lap time to float seconds and reject NaN values.
def _coerce_lap_time(lap_time_value: Any) -> float
Parameters:
lap_time_value: Lap time in various formats (float seconds, string, etc.)
Returns:
- Float representing lap time in seconds
Raises:
ValueError: If lap time cannot be converted or is None/NaN
Example:
from tif1.lap_ops import _coerce_lap_time
# From seconds (float)
lap_time = _coerce_lap_time(83.456) # 83.456
# From string representation
lap_time = _coerce_lap_time("83.456") # 83.456
# Invalid (NaN)
import math
try:
lap_time = _coerce_lap_time(math.nan)
except ValueError as e:
print(f"Error: {e}")
# Error: Invalid lap time: nan
Column Operations
_get_lap_column
Get lap number column name with fallback logic.
def _get_lap_column(df, lib: str) -> str
Parameters:
df: DataFrame with lap data
lib: library (“pandas” or “polars”)
Returns:
- Column name string (“LapNumber” or “lap”)
Example:
from tif1.lap_ops import _get_lap_column
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Get the lap number column name
lap_col = _get_lap_column(laps, session.lib)
print(f"Lap column: {lap_col}")
# Lap column: LapNumber
# Use it to access lap numbers
lap_numbers = laps[lap_col]
```---
## Filtering Laps
### By Lap Number
```python
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
# Get a specific driver
laps = session.laps
ver_laps = laps[laps["Driver"] == "VER"]
# Single lap
lap_19 = ver_laps[ver_laps["LapNumber"] == 19]
# Range of laps
mid_race = ver_laps[(ver_laps["LapNumber"] >= 20) & (ver_laps["LapNumber"] <= 40)]
# First 10 laps
first_10 = ver_laps[ver_laps["LapNumber"] <= 10]
# Last 10 laps
max_lap = ver_laps["LapNumber"].max()
last_10 = ver_laps[ver_laps["LapNumber"] > max_lap - 10]
By Lap Time
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Qualifying")
session.load()
laps = session.laps
# Fastest laps (under 1:45)
fast_laps = laps[laps["LapTime"] < 105.0]
# Laps within 107% of fastest
fastest_time = laps["LapTime"].min()
within_107 = laps[laps["LapTime"] <= fastest_time * 1.07]
# Personal best laps
pb_laps = laps[laps["IsPersonalBest"] == True]
By Compound
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Soft tire laps
soft_laps = laps[laps["Compound"] == "SOFT"]
# Medium or hard tire laps
race_laps = laps[laps["Compound"].isin(["MEDIUM", "HARD"])]
# Fresh tire laps
fresh_laps = laps[laps["FreshTyre"] == True]
By Track Status
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Green flag laps only
green_laps = laps[laps["TrackStatus"] == "1"]
# Exclude yellow flag laps
clean_laps = laps[laps["TrackStatus"] != "2"]
# Safety car laps
sc_laps = laps[laps["TrackStatus"] == "4"]
By Stint
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Filter for a specific driver
ver_laps = laps[laps["Driver"] == "VER"]
# First stint
stint_1 = ver_laps[ver_laps["Stint"] == 1]
# Laps 5-10 of each stint
for stint_num in ver_laps["Stint"].unique():
stint_laps = ver_laps[ver_laps["Stint"] == stint_num]
stint_laps_5_10 = stint_laps[
(stint_laps["TyreLife"] >= 5) & (stint_laps["TyreLife"] <= 10)
]
print(f"Stint {stint_num}: {len(stint_laps_5_10)} laps")
Convert Lap Times
import tif1
import pandas as pd
session = tif1.get_session(2021, "Belgian Grand Prix", "Qualifying")
session.load()
laps = session.laps
# Convert to timedelta
laps["LapTimeDelta"] = pd.to_timedelta(laps["LapTime"], unit="s")
# Convert to formatted string
laps["LapTimeStr"] = laps["LapTime"].apply(
lambda x: f"{int(x//60)}:{x%60:06.3f}"
)
# Example: 83.456 → "1:23.456"
Calculate Deltas
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Qualifying")
session.load()
laps = session.laps
# Filter for a specific driver
ver_laps = laps[laps["Driver"] == "VER"].copy()
# Delta to fastest lap
fastest = ver_laps["LapTime"].min()
ver_laps["DeltaToFastest"] = ver_laps["LapTime"] - fastest
# Delta to previous lap
ver_laps["DeltaToPrevious"] = ver_laps["LapTime"].diff()
# Cumulative time
ver_laps["CumulativeTime"] = ver_laps["LapTime"].cumsum()
Aggregate by Stint
import tif1
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
laps = session.laps
# Filter for a specific driver
ver_laps = laps[laps["Driver"] == "VER"]
# Average lap time per stint
stint_avg = ver_laps.groupby("Stint")["LapTime"].mean()
# Fastest lap per stint
stint_fastest = ver_laps.groupby("Stint")["LapTime"].min()
# Stint length
stint_length = ver_laps.groupby("Stint").size()
# Compound used per stint
stint_compound = ver_laps.groupby("Stint")["Compound"].first()
``` ---
## Complete Examples
### Find Optimal Lap
```python
import tif1
def find_optimal_lap(session, driver_code):
"""Find the optimal lap (fastest on fresh tires under green flag)."""
laps = session.laps
driver_laps = laps[laps["Driver"] == driver_code]
# Filter for optimal conditions
optimal_laps = driver_laps[
(driver_laps["TrackStatus"] == "1") & # Green flag
(driver_laps["FreshTyre"] == True) & # Fresh tires
(driver_laps["Deleted"] == False) # Not deleted
]
if len(optimal_laps) == 0:
return None
# Get fastest
fastest_idx = optimal_laps["LapTime"].idxmin()
return optimal_laps.loc[fastest_idx]
session = tif1.get_session(2021, "Belgian Grand Prix", "Qualifying")
session.load()
optimal = find_optimal_lap(session, "VER")
if optimal is not None:
print(f"Optimal lap: {optimal['LapNumber']}")
print(f"Time: {optimal['LapTime']:.3f}s")
print(f"Compound: {optimal['Compound']}")
Analyze Tire Degradation
import tif1
import matplotlib.pyplot as plt
import numpy as np
def analyze_tire_deg(session, driver_code, stint_num):
"""Analyze tire degradation for a specific stint."""
laps = session.laps
driver_laps = laps[laps["Driver"] == driver_code]
# Filter for stint
stint_laps = driver_laps[
(driver_laps["Stint"] == stint_num) &
(driver_laps["TrackStatus"] == "1") & # Green flag only
(driver_laps["Deleted"] == False)
]
if len(stint_laps) == 0:
return None
# Calculate degradation
tire_life = stint_laps["TyreLife"].values
lap_times = stint_laps["LapTime"].values
# Linear fit
slope, intercept = np.polyfit(tire_life, lap_times, 1)
print(f"Degradation: {slope:.4f}s per lap")
print(f"Compound: {stint_laps['Compound'].iloc[0]}")
# Plot
plt.figure(figsize=(10, 6))
plt.scatter(tire_life, lap_times, label="Actual")
plt.plot(tire_life, slope * tire_life + intercept, 'r--', label="Trend")
plt.xlabel("Tire Life (laps)")
plt.ylabel("Lap Time (s)")
plt.title(f"{driver_code} - Stint {stint_num} Degradation")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
return slope
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
deg = analyze_tire_deg(session, "VER", 1)
Compare Lap Times
import tif1
def compare_drivers(session, driver1, driver2):
"""Compare lap times between two drivers."""
laps = session.laps
laps1 = laps[laps["Driver"] == driver1]
laps2 = laps[laps["Driver"] == driver2]
# Get common lap numbers
common_laps = set(laps1["LapNumber"]) & set(laps2["LapNumber"])
# Compare lap by lap
deltas = []
for lap_num in sorted(common_laps):
time1 = laps1[laps1["LapNumber"] == lap_num]["LapTime"].iloc[0]
time2 = laps2[laps2["LapNumber"] == lap_num]["LapTime"].iloc[0]
delta = time1 - time2
deltas.append((lap_num, delta))
# Summary
avg_delta = sum(d for _, d in deltas) / len(deltas)
print(f"{driver1} vs {driver2}")
print(f"Average delta: {avg_delta:+.3f}s")
print(f"{driver1} faster: {sum(1 for _, d in deltas if d < 0)} laps")
print(f"{driver2} faster: {sum(1 for _, d in deltas if d > 0)} laps")
return deltas
session = tif1.get_session(2021, "Belgian Grand Prix", "Race")
session.load()
deltas = compare_drivers(session, "VER", "HAM")
Best Practices
- Filter before operations: Reduce data size for faster processing.
# Good: Filter first
clean_laps = laps[laps["Deleted"] == False]
fastest = clean_laps["LapTime"].min()
# Less efficient: Operate on full dataset
fastest = laps[laps["Deleted"] == False]["LapTime"].min()
- Use vectorized operations: Avoid loops when possible.
# Good: Vectorized
laps["Delta"] = laps["LapTime"] - laps["LapTime"].min()
# Bad: Loop
for idx in laps.index:
laps.loc[idx, "Delta"] = laps.loc[idx, "LapTime"] - laps["LapTime"].min()
- Check for empty results: Always validate filtered data.
filtered = laps[laps["Compound"] == "SOFT"]
if len(filtered) == 0:
print("No soft tire laps found")
else:
fastest = filtered["LapTime"].min()
- Use appropriate data types: Convert lap times to timedelta for time operations.
import pandas as pd
laps["LapTimeDelta"] = pd.to_timedelta(laps["LapTime"], unit="s")
total_time = laps["LapTimeDelta"].sum()
- Handle missing data: Check for NaN values.
# Remove laps with missing times
valid_laps = laps[laps["LapTime"].notna()]
# Or fill with default
laps["LapTime"].fillna(999.999, inplace=True)
``` ---
## Summary
Lap operations provide:
- Type coercion for lap numbers and times
- Column extraction with fallback logic
- Filtering utilities for various criteria
- Transformation helpers for analysis
- Best practices for efficient lap data processing
Use these utilities for advanced lap data manipulation and analysis beyond the high-level Session APIs.