---
title: "Lap Operations"
icon: "stopwatch"
description: "Utilities for working with lap data, filtering, and transformations"
keywords:
  - "tif1"
  - "Formula 1"
  - "F1 data"
  - "documentation"
---

The lap operations module provides helper functions for working with lap data, including filtering, coercion, and extraction utilities.

<Note>
  Most users interact with laps through the high-level `Session` and `Driver` APIs. These utilities are for advanced use cases and internal operations.
</Note>

---

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

```python
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:**
```python
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.

```python
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:**
```python
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.

```python
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:**
```python
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.

```python
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:**
```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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.

```python
# 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()
```

2. **Use vectorized operations**: Avoid loops when possible.

```python
# 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()
```

3. **Check for empty results**: Always validate filtered data.

```python
filtered = laps[laps["Compound"] == "SOFT"]
if len(filtered) == 0:
    print("No soft tire laps found")
else:
    fastest = filtered["LapTime"].min()
```

4. **Use appropriate data types**: Convert lap times to timedelta for time operations.

```python
import pandas as pd

laps["LapTimeDelta"] = pd.to_timedelta(laps["LapTime"], unit="s")
total_time = laps["LapTimeDelta"].sum()
```

5. **Handle missing data**: Check for NaN values.

```python
# 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.
