From 0e747ef2a23271c7187461d72060386eec0f609f Mon Sep 17 00:00:00 2001
From: Lennard Geese <lennard.geese@sva.de>
Date: Fri, 4 Apr 2025 19:08:36 +0200
Subject: [PATCH] Implement counting of overtakes, so that position changes in
 in-laps (so also pitstops) are not counted

---
 DataAnalyser.py | 69 ++++++++++++++++++++++++++-----------------------
 DataChecker.py  |  2 ++
 DataImporter.py |  7 +----
 DataPlotter.py  | 10 +------
 README.md       |  2 +-
 Todos.md        |  8 +++---
 main.py         | 12 +--------
 7 files changed, 47 insertions(+), 63 deletions(-)

diff --git a/DataAnalyser.py b/DataAnalyser.py
index c03b8c8..6416145 100644
--- a/DataAnalyser.py
+++ b/DataAnalyser.py
@@ -1,7 +1,8 @@
 import math
-from typing import List
-
+import numpy as numpy
+import pandas as pandas
 from fastf1.core import Session, Lap, Laps
+from pandas.errors import IndexingError
 
 from DataHandler import DataHandler
 
@@ -12,14 +13,14 @@ class DataAnalyser(DataHandler):
 
     # ===== Overtakes =====
 
-    def analyseRacesForOvertakes(self, races: List[Session]):
-        overtakesInRaces: List[List[int]] = [[]]
+    def analyseRacesForOvertakes(self, races: list[Session]):
+        overtakesInRaces: list[list[int]] = [[]]
         for race in races:
             overtakesInRaces.append(self.analyseRaceForOvertakes(race))
         return overtakesInRaces
 
     def analyseRaceForOvertakes(self, race: Session):
-        overtakesInLaps: List[int] = self.countOvertakesPerLap(race)
+        overtakesInLaps: list[int] = self.countOvertakesPerLap(race)
         return overtakesInLaps
 
     def countOvertakesPerLap(self, race: Session):
@@ -39,18 +40,16 @@ class DataAnalyser(DataHandler):
         of laps the race had), all laps in the list will be either None objects or empty Panda Dataframes.
         '''
 
-        # TODO: Implement returning starting grid for lapNumber = 1
-        # TODO: Sidestep getting start position from lap, get from session results instead, for lap 1 SOL (start of lap)
-
-
-
         previousLaps: Laps = race.laps.pick_laps(lapNumber - 1)
         currentLaps: Laps = race.laps.pick_laps(lapNumber)
         out: int = self.numberOfDrivers  # weighting for drivers that did not complete the lap (they are "out" of the lap/race)
         weightedOrder: list[(Lap, int)] = [(None, out)] * 20  # data from start of lap + weighting for end of lap; initialize to add laps in lap's starting order
 
+        if self.activateDebugOvertakeAnalysis: print(f"Lap: {lapNumber}")
+
         # Put every driver's laps in a sortable array & apply weighting based on position at end of lap
         for driver in race.drivers:
+            if self.activateDebugOvertakeAnalysis: print(f"Driver: {driver}")
             driversPreviousLap: Laps = previousLaps.pick_drivers(
                 driver)  # should only get 1 lap, but data type shenanigans
             driversCurrentLap: Laps = currentLaps.pick_drivers(driver)
@@ -64,10 +63,12 @@ class DataAnalyser(DataHandler):
                 endPosition = int(driversCurrentLap['Position'].iloc[0])
 
             except ValueError:
-                if self.activateDebugOvertakeAnalysis: print(f"Could not fetch positions from lap; driver %d likely didn't finish lap %d or %d",
-                      driver, lapNumber, (lapNumber + 1))
+                if self.activateDebugOvertakeAnalysis:
+                    print(f"Could not fetch positions from lap; driver %d likely didn't finish lap %d or %d",
+                          driver, lapNumber, (lapNumber + 1))
             except IndexError:
-                if self.activateDebugOvertakeAnalysis: print("['Position'].iloc[0] was out of bounds; lap was likely empty because driver previously left the race")
+                if self.activateDebugOvertakeAnalysis:
+                    print("['Position'].iloc[0] was out of bounds; lap was likely empty because driver previously left the race")
 
             weightedOrder[startPosition - 1] = (driversCurrentLap, endPosition)
 
@@ -97,16 +98,19 @@ class DataAnalyser(DataHandler):
 
         return overtakes
 
-    def isAnInLap(self, lap: Lap):
-        # TODO: Implement :)
-        return False
-
-
     def getGridPositionFor(self, driverNumber: int, race: Session):
         sessionResults = race.results
         gridPosition: int = int(sessionResults['GridPosition'].loc[driverNumber])
         return gridPosition
 
+    def isAnInLap(self, lap: Lap):
+        try:
+            return not pandas.isnull(lap['PitInTime'].iloc[0])
+        except: # caused when lap is empty and possibly when lap is None
+            return True # like in-laps, empty laps should not be counted for overtakes
+
+
+
 
     def getGridPositions(self, race: Session):
         sessionResults = race.results
@@ -127,7 +131,7 @@ class DataAnalyser(DataHandler):
         return gridPositions
 
     def getRunningLapsPositions(self, race: Session):
-        runningLapsPositions: List[dict[str, int]] = []
+        runningLapsPositions: list[dict[str, int]] = []
         allRaceLaps = race.laps
 
         for raceLapIndex in range(race.total_laps):
@@ -163,8 +167,8 @@ class DataAnalyser(DataHandler):
 
     # ===== Weather =====
 
-    def analyseRacesForWeather(self, races: List[Session]):
-        weatherInRaces: List[List[bool]] = [[]] # isRaining per lap per race
+    def analyseRacesForWeather(self, races: list[Session]):
+        weatherInRaces: list[list[bool]] = [[]] # isRaining per lap per race
         for race in races:
             weatherInRace = self.analyseRaceForWeather(race)
             weatherInRaces.append(weatherInRace)
@@ -176,8 +180,8 @@ class DataAnalyser(DataHandler):
 
     # ===== Tire Changes =====
 
-    def getEarliestTireChanges(self, races: List[Session]):
-        earliestTireChanges: List[int] = [[]]  # isRaining per lap per race
+    def getEarliestTireChanges(self, races: list[Session]):
+        earliestTireChanges: list[int] = [[]]  # isRaining per lap per race
         for race in races:
             earliestTireChange = self.getEarliestTireChange(race)
             earliestTireChanges.append(earliestTireChange)
@@ -186,15 +190,15 @@ class DataAnalyser(DataHandler):
     # Returns -1 if no tire change occured
     def getEarliestTireChange(self, race: Session):
         earliestTireChangeLap: int = -1
-        compoundsPerLap: List[List[str]] = self.getCompoundsForRace(race)
+        compoundsPerLap: list[list[str]] = self.getCompoundsForRace(race)
         compoundsPerLap[0] = compoundsPerLap[1] # presume grid tires same as 1st lap; races are only picked if weather change after first 10 laps anyways, so its ok
         startingCompound: str = self.getPredominantCompound(compoundsPerLap[0])
         earliestTireChangeLap = self.getFirstLapWithOppositeCompound(compoundsPerLap, startingCompound)
 
         return earliestTireChangeLap
 
-    def getLatestTireChanges(self, races: List[Session]):
-        latestTireChanges: List[int] = [[]]  # isRaining per lap per race
+    def getLatestTireChanges(self, races: list[Session]):
+        latestTireChanges: list[int] = [[]]  # isRaining per lap per race
         for race in races:
             latestTireChange = self.getLatestTireChange(race)
             latestTireChanges.append(latestTireChange)
@@ -203,14 +207,14 @@ class DataAnalyser(DataHandler):
     # Returns -1 if no tire change occured
     def getLatestTireChange(self, race: Session):
         latestTireChangeLap: int = -1
-        compoundsPerLap: List[List[str]] = self.getCompoundsForRace(race)
+        compoundsPerLap: list[list[str]] = self.getCompoundsForRace(race)
         compoundsPerLap[0] = compoundsPerLap[1]  # presume grid tires same as 1st lap; races are only picked if weather change after first 10 laps anyways, so its ok
         startingCompound: str = self.getPredominantCompound(compoundsPerLap[0])
         latestTireChangeLap = self.getFirstLapWithoutCompound(compoundsPerLap, startingCompound)
 
         return latestTireChangeLap
 
-    def getFirstLapWithoutCompound(self, compoundsPerLap: List[List[str]], startingCompound: str):
+    def getFirstLapWithoutCompound(self, compoundsPerLap: list[list[str]], startingCompound: str):
         currentLap = 0
         filter = self.setFilter(startingCompound)
         for compoundsThisLap in compoundsPerLap:
@@ -224,16 +228,15 @@ class DataAnalyser(DataHandler):
         return -1 # no lap without compound found; all laps use same compound type
 
     def getCompoundsForRace(self, race: Session):
-        compoundsPerLap: List[List[str]] = [[]]
+        compoundsPerLap: list[list[str]] = [[]]
         allRaceLaps = race.laps
 
         for raceLapIndex in range(race.total_laps):
-            compoundsThisLap: List[str] = []
+            compoundsThisLap: list[str] = []
             for driver in race.drivers:
                 raceLap = allRaceLaps.pick_laps(raceLapIndex + 1)  # Lap 0 doesn't exist
                 raceLap = raceLap.pick_drivers(driver)
                 try:
-                    driverAbbreviation = raceLap['Driver'].iloc[0]
                     compound = raceLap['Compound'].iloc[0]
                     compoundsThisLap.append(compound)
                 except:  # triggered when not all drivers that took part reached lap, probably by crashing or being behind
@@ -242,7 +245,7 @@ class DataAnalyser(DataHandler):
 
         return compoundsPerLap
 
-    def getPredominantCompound(self, compoundsThisLap: List[str]):
+    def getPredominantCompound(self, compoundsThisLap: list[str]):
         slickCounter = 0
         interCounter = 0
         wetCounter = 0
@@ -256,7 +259,7 @@ class DataAnalyser(DataHandler):
         if wetCounter == mostUsed: return 'WET'
         return 'error'
 
-    def getFirstLapWithOppositeCompound(self, compoundsPerLap: List[List[str]], startingCompound: str):
+    def getFirstLapWithOppositeCompound(self, compoundsPerLap: list[list[str]], startingCompound: str):
         filter = self.setFilter(startingCompound)
         currentLap = 0
         for compoundsThisLap in compoundsPerLap:
diff --git a/DataChecker.py b/DataChecker.py
index 6ddbfa0..9b9da54 100644
--- a/DataChecker.py
+++ b/DataChecker.py
@@ -3,6 +3,7 @@ from DataHandler import DataHandler
 
 class DataChecker(DataHandler):
 
+    @DeprecationWarning
     def check_imported_race(self, race_data):
         for current_lap in range(len(race_data)):
             print(f"Lap {current_lap}")
@@ -14,6 +15,7 @@ class DataChecker(DataHandler):
             print()
         print(f"Number of laps run: {len(race_data) - 1}")
 
+    @DeprecationWarning
     def check_imported_races(self, races):
         for race in races:
             self.check_imported_race(race)
\ No newline at end of file
diff --git a/DataImporter.py b/DataImporter.py
index 40e7e2b..b2dc7f5 100644
--- a/DataImporter.py
+++ b/DataImporter.py
@@ -1,10 +1,5 @@
-import json
-import time
-
 import fastf1
-import requests
 from abc import ABC
-from typing import List
 
 from fastf1.core import Session
 
@@ -17,7 +12,7 @@ from DataHandler import DataHandler
 class DataImporter(DataHandler, ABC):
 
     def importRaceSessions(self, races):
-        sessions: List[Session] = []
+        sessions: list[Session] = []
         for race in races:
             sessions.append(self.importRaceSession(race))
         return sessions
diff --git a/DataPlotter.py b/DataPlotter.py
index 19bf93c..9b95e74 100644
--- a/DataPlotter.py
+++ b/DataPlotter.py
@@ -1,18 +1,10 @@
-import math
-from abc import ABC
-from typing import List
-
 import matplotlib.pyplot as plt
 import numpy as np
-
-
-from fastf1.core import Session, Lap
-
 from DataHandler import DataHandler
 
 
 class DataPlotter(DataHandler):
-    def plotOvertakesWithTireChangeWindow(self, overtakesPerLap: List[int], earliestTireChange: int, latestTireChange: int, raceName: str):
+    def plotOvertakesWithTireChangeWindow(self, overtakesPerLap: list[int], earliestTireChange: int, latestTireChange: int, raceName: str):
 
         overtakesPerLap.insert(0, 0) # Insert 0th lap, which cannot have overtakes
         laps: int = len(overtakesPerLap)
diff --git a/README.md b/README.md
index 08fdf44..bc6e6d8 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ On some READMEs, you may see small images that convey metadata, such as whether
 Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
 
 ## Installation
-Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
+Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
 
 ## Usage
 Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
diff --git a/Todos.md b/Todos.md
index edd1083..65c2d80 100644
--- a/Todos.md
+++ b/Todos.md
@@ -1,11 +1,11 @@
 # Todo (sorted by priority)
 - Fetch rain races via API, not Reddit
-- Adjust for pitstop discrepancies
+- Adjust for position changes caused by crashes (keep out of calculation similarly to PCs due to pitstops ?)
 - Include safety car periods & driver crashes
 - Automatically determine if race is DWR or SWR
 - Adjust for situations like Canada 2024 -> 2 drivers taking wets at the start does not constitute a weather change
   - Implement actual 1/2 of drivers changing compounds rule
-- Save data in CSV file
+- Cache data in CSV file for faster access / analysis
 - Adjust for finding multiple weather changes in a race, not just one
 - Read out number of drivers participating in session, rather than hardcoding number of drivers (since it might change from 20 to more (or less) in future)
 - Also read out direct weather data from API
@@ -17,4 +17,6 @@
 # Done
 - Automatically title graph by race name (no more hardcoding the graph name)
 - Migrate counting of overtakes functionality from integer-array based to list\[(Lap, int)] based so that in-laps 
-  can be accounted for
\ No newline at end of file
+  can be accounted for
+- Count overtakes in 1st lap after migration
+- Adjust for pitstop discrepancies
diff --git a/main.py b/main.py
index a74ceda..53850a4 100644
--- a/main.py
+++ b/main.py
@@ -1,19 +1,9 @@
-from typing import List
-
-import matplotlib.pyplot as plt
-
-import fastf1.plotting
-
-
 from DataAnalyser import DataAnalyser
 from DataChecker import DataChecker
 from DataImporter import DataImporter
 from DataPlotter import DataPlotter
 
 
-
-
-
 class Main:
 
     def main(self):
@@ -40,7 +30,7 @@ class Main:
 
 
             # Analyse
-            overtakesInRaces: List[int] = analyser.analyseRaceForOvertakes(raceSession)
+            overtakesInRaces: list[int] = analyser.analyseRaceForOvertakes(raceSession)
             print("Overtake analysis done")
             # weatherInRaces = analyser.analyseRaceForWeather(raceSession)
             earliestTireChange: int = analyser.getEarliestTireChange(raceSession) # first lap where someone switched from slicks to non slicks or vice versa, denoted by lap number
-- 
GitLab