Skip to main content
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:
  • Integer lap number
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_lap_numbers

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")

Transforming Lap Data

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

  1. 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()
  1. 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()
  1. 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()
  1. 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()
  1. 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.
Last modified on March 5, 2026