Skip to main content
The pitstops.py script fetches detailed pit stop information including lap number, stop duration, and timing for each driver’s pit stops.
Pit stop data is only available from the 2011 season onwards. The script automatically skips seasons before 2011.

Script Overview

Location: ~/workspace/source/pitstops.py The PitstopsFetcher class:
  1. Fetches pit stop data with pagination
  2. Validates that the season is 2011 or later
  3. Combines paginated results into a single file
  4. Handles races with no pit stops (e.g., safety car finishes)

API Endpoint with Pagination

url = f"https://api.jolpi.ca/ergast/f1/{season}/{round_num}/pitstops.json?limit={limit}&offset={offset}"

PitstopsFetcher Class

Initialization

class PitstopsFetcher:
    def __init__(self, base_dir=".", season=None, round_num=None):
        """
        Initialize the PitstopsFetcher for a specific season and round.

        Args:
            base_dir: Base directory where race data is stored
            season: Season year to fetch pitstops for
            round_num: Round number to fetch pitstops for
        """
        self.base_dir = Path(base_dir)
        self.season = season
        self.round_num = round_num
        self.base_url = "https://api.jolpi.ca/ergast/f1"

        # Rate limiting parameters
        self.burst_limit = 4  # 4 requests per second
        self.last_request_time = 0

Fetch Pitstops with Pagination

def fetch_pitstops_for_race(self):
    """
    Fetch all pitstops for the specified race using pagination

    Returns:
        Complete pitstops data for the race
    """
    limit = 100  # Maximum number of results per request
    offset = 0
    all_pitstops = []
    total_pitstops = None

    # Initial request
    url = f"{self.base_url}/{self.season}/{self.round_num}/pitstops.json?limit={limit}&offset={offset}"
    response_data = self.make_request(url)

    if not response_data:
        logger.error(
            f"Failed to fetch pitstops for {self.season} round {self.round_num}"
        )
        return None

    # Extract data from the response
    race_data = response_data["MRData"]
    total_pitstops = int(race_data["total"])

    # If there are no pitstops, return the empty response
    if total_pitstops == 0:
        logger.info(f"No pitstops found for {self.season} round {self.round_num}")
        return response_data

    # Add the first batch of pitstops
    if (
        "RaceTable" in race_data
        and "Races" in race_data["RaceTable"]
        and len(race_data["RaceTable"]["Races"]) > 0
    ):
        race = race_data["RaceTable"]["Races"][0]
        if "PitStops" in race:
            all_pitstops.extend(race["PitStops"])

    # Fetch remaining pitstops if needed
    while len(all_pitstops) < total_pitstops:
        offset += limit
        url = f"{self.base_url}/{self.season}/{self.round_num}/pitstops.json?limit={limit}&offset={offset}"
        response_data = self.make_request(url)

        if not response_data:
            logger.error(
                f"Failed to fetch pitstops at offset {offset} for {self.season} round {self.round_num}"
            )
            break

        race_data = response_data["MRData"]
        if (
            "RaceTable" in race_data
            and "Races" in race_data["RaceTable"]
            and len(race_data["RaceTable"]["Races"]) > 0
        ):
            race = race_data["RaceTable"]["Races"][0]
            if "PitStops" in race:
                all_pitstops.extend(race["PitStops"])

    # Reconstruct the complete response with all pitstops
    if (
        len(all_pitstops) > 0
        and "RaceTable" in response_data["MRData"]
        and "Races" in response_data["MRData"]["RaceTable"]
        and len(response_data["MRData"]["RaceTable"]["Races"]) > 0
    ):
        response_data["MRData"]["RaceTable"]["Races"][0]["PitStops"] = all_pitstops

    logger.info(
        f"Fetched {len(all_pitstops)} pitstops for {self.season} round {self.round_num}"
    )
    return response_data

Run Method with Season Validation

def run(self):
    """Run the pitstops fetcher for the specified season and round"""
    # Skip if season is before 2011 as pitstops data starts from 2011
    if self.season < 2011:
        logger.info(
            f"Skipping season {self.season} - pitstops data starts from 2011"
        )
        return

    race_info = self.get_race_info()
    if not race_info:
        logger.error(
            f"Could not find race information for {self.season} round {self.round_num}"
        )
        return

    race_name = race_info["raceName"]
    race_folder_name = self.get_race_folder_name(race_info)
    race_folder = self.base_dir / str(self.season) / race_folder_name

    # Create race folder if it doesn't exist
    if not race_folder.exists():
        logger.warning(f"Race folder {race_folder} does not exist, creating it")
        race_folder.mkdir(parents=True, exist_ok=True)

    pitstops_file = race_folder / "pitstops.json"

    logger.info(
        f"Fetching pitstops for {self.season} {race_name} (Round {self.round_num})"
    )
    pitstops_data = self.fetch_pitstops_for_race()

    if pitstops_data:
        with open(pitstops_file, "w") as f:
            json.dump(pitstops_data, f, indent=2)
        logger.info(f"Saved pitstops data to {pitstops_file}")
    else:
        logger.error(f"Failed to fetch pitstops for {self.season} {race_name}")

Output Structure

Stored at: {year}/{race-name}/pitstops.json
{
  "MRData": {
    "series": "f1",
    "url": "https://api.jolpi.ca/ergast/f1/2011/1/pitstops.json",
    "limit": "100",
    "offset": "0",
    "total": "45",
    "RaceTable": {
      "season": "2011",
      "round": "1",
      "Races": [
        {
          "season": "2011",
          "round": "1",
          "raceName": "Australian Grand Prix",
          "Circuit": {
            "circuitId": "albert_park",
            "circuitName": "Albert Park Grand Prix Circuit",
            "Location": {
              "lat": "-37.8497",
              "long": "144.968",
              "locality": "Melbourne",
              "country": "Australia"
            }
          },
          "date": "2011-03-27",
          "time": "06:00:00Z",
          "PitStops": [
            {
              "driverId": "alguersuari",
              "lap": "1",
              "stop": "1",
              "time": "17:05:23",
              "duration": "26.898"
            },
            {
              "driverId": "michael_schumacher",
              "lap": "1",
              "stop": "1",
              "time": "17:05:52",
              "duration": "25.021"
            },
            {
              "driverId": "webber",
              "lap": "11",
              "stop": "1",
              "time": "17:20:48",
              "duration": "23.426"
            }
          ]
        }
      ]
    }
  }
}

Pit Stop Data Fields

FieldDescription
driverIdDriver identifier
lapLap number when pit stop occurred
stopPit stop number for this driver (1st, 2nd, 3rd, etc.)
timeTime of day when stop occurred (HH:MM:SS)
durationDuration of pit stop in seconds
The duration field is particularly useful for analyzing pit crew performance and comparing stop times.

Usage Example

Single Race

from pitstops import PitstopsFetcher

fetcher = PitstopsFetcher(base_dir=".", season=2024, round_num=1)
fetcher.run()

Multiple Races

for round_num in range(1, 23):
    fetcher = PitstopsFetcher(base_dir=".", season=2024, round_num=round_num)
    fetcher.run()

With Season Validation

# This will be skipped automatically
fetcher = PitstopsFetcher(base_dir=".", season=2010, round_num=1)
fetcher.run()  # Logs: "Skipping season 2010 - pitstops data starts from 2011"

# This will fetch data
fetcher = PitstopsFetcher(base_dir=".", season=2011, round_num=1)
fetcher.run()  # Fetches pitstops data

Pagination Example

For a typical race:
  • 20 drivers × 2-3 stops each = 40-60 pit stops
  • Usually fits in a single 100-record request
  • High-strategy races may require multiple requests

Race Strategy Analysis

Pit stop data enables various analyses:

Pit Stop Statistics

import json

with open("2024/bahrain-grand-prix/pitstops.json") as f:
    data = json.load(f)

pitstops = data["MRData"]["RaceTable"]["Races"][0]["PitStops"]

# Find fastest pit stop
fastest = min(pitstops, key=lambda x: float(x["duration"]))
print(f"Fastest stop: {fastest['driverId']} - {fastest['duration']}s")

# Average stop duration
avg_duration = sum(float(p["duration"]) for p in pitstops) / len(pitstops)
print(f"Average stop duration: {avg_duration:.3f}s")

Strategy Comparison

# Count stops per driver
from collections import defaultdict

driver_stops = defaultdict(int)
for stop in pitstops:
    driver_stops[stop["driverId"]] += 1

for driver, count in sorted(driver_stops.items(), key=lambda x: x[1]):
    print(f"{driver}: {count} stops")

Data Availability Timeline

Season RangeAvailability
1950-2010No pit stop data
2011-presentComplete pit stop data
Attempting to fetch pit stop data for seasons before 2011 will return empty results. The script automatically detects and skips these seasons.

Logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S"
)

Lap Times

Compare with lap timing data

Race Results

Analyze how pit strategy affected results

Sprint Races

Fetch sprint race data (2021+)

Build docs developers (and LLMs) love