Skip to content
Snippets Groups Projects
Commit e77919cd authored by Lennard Geese's avatar Lennard Geese
Browse files

T

parents
No related branches found
No related tags found
1 merge request!1Merge into main
Showing with 529 additions and 0 deletions
# Default ignored files
/shelf/
/workspace.xml
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (F1-OvertakeAnalyser-Python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (F1-OvertakeAnalyser-Python)" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/F1-OvertakeAnalyser-Python.iml" filepath="$PROJECT_DIR$/.idea/F1-OvertakeAnalyser-Python.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
import math
from typing import List
from fastf1.core import Session, Lap
from DataHandler import DataHandler
class DataAnalyser(DataHandler):
# ===== Overtakes =====
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):
# Collect grid positions
allLapPositions: List[dict[str, int]] = []
gridPositions: dict[str, int] = self.getGridPositions(race)
allLapPositions.append(gridPositions)
runningLapsPositions: List[dict[str, int]] = self.getRunningLapsPositions(race)
for runningLapPositions in runningLapsPositions:
allLapPositions.append(runningLapPositions)
# Count position changes between all lap pairs
overtakesInLaps: List[int] = self.countOvertakesInLaps(allLapPositions)
return overtakesInLaps
def getGridPositions(self, race: Session):
sessionResults = race.results
gridPositions: dict[str, int] = {} # driverId & lapNumber
for i in range(self.numberOfDrivers):
driverPosition = sessionResults['GridPosition'].iloc[i]
driverAbbreviation = sessionResults['Abbreviation'].iloc[i]
gridPositions[driverAbbreviation] = driverPosition
if self.activateDebugOvertakeAnalysis:
print(f"\nLap: 0")
for position in range(len(gridPositions)):
gridPositions.values()
driverAtPosition = list(gridPositions.keys())[list(gridPositions.values()).index(position + 1)] # get dictionary keys (driverIds) by values (current race position)
print(f"P{position + 1}: {driverAtPosition}")
return gridPositions
def getRunningLapsPositions(self, race: Session):
runningLapsPositions: List[dict[str, int]] = []
allRaceLaps = race.laps
for raceLapIndex in range(race.total_laps):
runningLapPositions: dict[str, int] = {} # driverId & lapNumber
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]
driverPosition = raceLap['Position'].iloc[0]
runningLapPositions[driverAbbreviation] = driverPosition
except: # triggered when not all drivers that took part reached lap, probably by crashing or being behind
x = 0 # do nothing
runningLapsPositions.append(runningLapPositions)
# debug
if self.activateDebugOvertakeAnalysis:
for raceLapIndex in range(race.total_laps):
print(f"\nLap: {raceLapIndex + 1}")
runningLapPositions = runningLapsPositions[raceLapIndex]
for position in range(len(runningLapPositions)):
runningLapPositions.values()
driverAtPosition = list(runningLapPositions.keys())[list(runningLapPositions.values()).index(position + 1)]
print(f"P{position + 1}: {driverAtPosition}")
return runningLapsPositions
def countOvertakesInLaps(self, allLapPositions: List[dict[str, int]]):
overtakesInLaps: List[int] = []
for i in range(len(allLapPositions) - 1):
overtakesInLap: int = self.countOvertakesInLap(allLapPositions[i], allLapPositions[i + 1])
overtakesInLaps.append(overtakesInLap)
if self.activateDebugOvertakeAnalysis: print(f"Overtakes in lap {i+1}: {overtakesInLap}")
return overtakesInLaps
def countOvertakesInLap(self, startOrder: dict[str, int], endOrder: dict[str, int]):
out: int = self.numberOfDrivers
orderToSort: List[int] = [self.numberOfDrivers] * self.numberOfDrivers
for driver in startOrder:
if driver not in endOrder: # add missing drivers for sorting/counting purposes
endOrder[driver] = out # heaviest weighting to drop driver to bottom, as they do irl when crashing or similar
if math.isnan(startOrder[driver]): startOrder[driver] = out
if math.isnan(endOrder[driver]): endOrder[driver] = out
driverStartPosition: int = int(startOrder[driver])
driverEndPosition: int = int(endOrder[driver])
orderToSort[driverStartPosition - 1] = driverEndPosition
if self.activateDebugOvertakeAnalysis:
#print("Weighting:")
for i in range(len(orderToSort)):
x=0
#print(orderToSort[i])
overtakes: int = 0
index: int = 0
while index < len(orderToSort) - 1:
if orderToSort[index] > orderToSort[index + 1]:
temp: int = orderToSort[index]
orderToSort[index] = orderToSort[index + 1]
orderToSort[index + 1] = temp
overtakes += 1
index = -1
index += 1
return overtakes
# ===== Weather =====
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)
return weatherInRaces
def analyseRaceForWeather(self, race: Session):
x = 0
# ===== Tire Changes =====
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)
return earliestTireChanges
# Returns -1 if no tire change occured
def getEarliestTireChange(self, race: Session):
earliestTireChangeLap: int = -1
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
for race in races:
latestTireChange = self.getLatestTireChange(race)
latestTireChanges.append(latestTireChange)
return latestTireChanges
# Returns -1 if no tire change occured
def getLatestTireChange(self, race: Session):
latestTireChangeLap: int = -1
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):
currentLap = 0
filter = self.setFilter(startingCompound)
for compoundsThisLap in compoundsPerLap:
noStartingCompoundsLeft = True
for compound in compoundsThisLap:
if compound in filter:
noStartingCompoundsLeft = False
if noStartingCompoundsLeft: return currentLap
currentLap += 1
def getCompoundsForRace(self, race: Session):
compoundsPerLap: List[List[str]] = [[]]
allRaceLaps = race.laps
for raceLapIndex in range(race.total_laps):
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
x = 0 # do nothing
compoundsPerLap.append(compoundsThisLap)
return compoundsPerLap
def getPredominantCompound(self, compoundsThisLap: List[str]):
slickCounter = 0
interCounter = 0
wetCounter = 0
for compound in compoundsThisLap:
if compound in self.slickCompounds: slickCounter += 1
if compound == 'INTERMEDIATE': interCounter += 1
if compound == 'WET': wetCounter += 1
mostUsed = max(slickCounter, interCounter, wetCounter)
if slickCounter == mostUsed: return 'SLICK'
if interCounter == mostUsed: return 'INTERMEDIATE'
if wetCounter == mostUsed: return 'WET'
return 'error'
def getFirstLapWithOppositeCompound(self, compoundsPerLap: List[List[str]], startingCompound: str):
filter = self.setFilter(startingCompound)
currentLap = 0
for compoundsThisLap in compoundsPerLap:
for compound in compoundsThisLap:
if compound not in filter:
return currentLap
currentLap += 1
def setFilter(self, startingCompound: str):
if startingCompound == 'SLICK': return self.slickCompounds
return startingCompound
# ===== Crashes =====
def analyseRacesForCrashes(self, races):
x = 0
# ===== Events =====
def analyseRacesForEvents(self, races):
x = 0
from DataHandler import DataHandler
class DataChecker(DataHandler):
def check_imported_race(self, race_data):
for current_lap in range(len(race_data)):
print(f"Lap {current_lap}")
for position in range(1, len(race_data[0]) + 1):
driver_at_position = race_data[current_lap][position - 1]
if driver_at_position == self.invalidDriverId:
break # skip entries without drivers
print(f"P{position}: {race_data[current_lap][position - 1]}")
print()
print(f"Number of laps run: {len(race_data) - 1}")
def check_imported_races(self, races):
for race in races:
self.check_imported_race(race)
\ No newline at end of file
from abc import ABC, abstractmethod
class DataHandler(ABC):
def __init__(self):
self.numberOfDrivers = 20
self.invalidDriverId = "NO_DRIVER"
self.enableTestingMode = False # only partially import data to speed up testing, because HTTP 429 limits import speed
self.activateDebugOvertakeAnalysis = False
self.slickCompounds = ('SOFT', 'MEDIUM', 'HARD')
\ No newline at end of file
import json
import time
import fastf1
import requests
from abc import ABC
from typing import List
from fastf1.core import Session
from DataHandler import DataHandler
class DataImporter(DataHandler, ABC):
def importRaceSessions(self, races):
sessions: List[Session] = []
for race in races:
sessions.append(self.importRaceSession(race))
return sessions
def importRaceSession(self, race):
season, raceIndex = race
session = fastf1.get_session(season, raceIndex, 'R')
session.load(laps=True, weather=True, messages=False, telemetry=False)
return session
\ No newline at end of file
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):
overtakesPerLap.insert(0, 0) # Insert 0th lap, which cannot have overtakes
laps: int = len(overtakesPerLap)
figure, axis = plt.subplots(figsize=(8.0, 4.9))
axis.plot(overtakesPerLap)
axis.set_xlabel('Lap')
axis.set_ylabel('Position changes in lap')
major_ticks_laps = np.arange(0, laps, 5)
major_ticks_overtakes = np.arange(0, max(overtakesPerLap) + 1, 5)
minor_ticks_laps = np.arange(0, laps, 1)
minor_ticks_overtakes = np.arange(0, max(overtakesPerLap) + 1, 1)
axis.set_xticks(major_ticks_laps)
axis.set_xticks(minor_ticks_laps, minor=True)
axis.set_yticks(major_ticks_overtakes)
axis.set_yticks(minor_ticks_overtakes, minor=True)
# And a corresponding grid
axis.grid(which='both')
# Or if you want different settings for the grids:
axis.grid(which='minor', alpha=0.2)
axis.grid(which='major', alpha=0.5)
axis.legend(bbox_to_anchor=(1.0, 1.02))
plt.tight_layout()
plt.show()
\ No newline at end of file
File added
File added
File added
File added
File added
main.py 0 → 100644
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):
# contains pairs of season & race number to identify specific races
race_indexes = [
[2022, 4] # Imola 2022 (DWR)
# [2024, 7] # Imola 2024 (SWR)
# [2024, 9] # Canada 2024 (DWR)
#
# TODO: add more races
]
importer = DataImporter()
analyser = DataAnalyser()
plotter = DataPlotter()
for race_index in race_indexes:
# Import
raceSession = importer.importRaceSession(race_index)
print("Import done")
# Analyse
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
print("First tire change done")
latestTireChange: int = analyser.getLatestTireChange(raceSession)
print("Last tire change done")
plotter.plotOvertakesWithTireChangeWindow(overtakesInRaces, earliestTireChange, latestTireChange)
print("Plot done")
print(f"\n\n===== Data for race {race_index[0]}/{race_index[1]} =====")
print("Lap\tOvertakes")
currentLap = 0
for overtakesInLap in overtakesInRaces:
print(f"{currentLap}\t{overtakesInLap}")
currentLap += 1
print(f"First tire compound: lap {earliestTireChange}")
print(f"Last tire compound: lap {latestTireChange}")
print(f"Weather change window is therefore: laps {earliestTireChange-1} - {latestTireChange+1}")
def fastF1Example(self):
# Load FastF1's dark color scheme
fastf1.plotting.setup_mpl(mpl_timedelta_support=False, misc_mpl_mods=False,
color_scheme='fastf1')
session = fastf1.get_session(2024, 7, 'R')
session.load(laps=True, weather=True)
figure, axis = plt.subplots(figsize=(8.0, 4.9))
for driver in session.drivers:
driverLaps = session.laps.pick_drivers(driver)
driverAbbreviation = driverLaps['Driver'].iloc[0]
style = fastf1.plotting.get_driver_style(identifier=driverAbbreviation,
style=['color', 'linestyle'],
session=session)
axis.plot(driverLaps['LapNumber'], driverLaps['Position'],
label=driverAbbreviation, **style)
axis.set_ylim([20.5, 0.5])
axis.set_yticks([1, 5, 10, 15, 20])
axis.set_xlabel('Lap')
axis.set_ylabel('Position')
axis.legend(bbox_to_anchor=(1.0, 1.02))
plt.tight_layout()
plt.show()
def oldJavaTransfer(self):
# contains pairs of season & race number to identify specific races
race_indexes = [
[2024, 7], # Imola 2024
[2022, 4] # Imola 2022
# TODO: add more races
]
importer = DataImporter()
checker = DataChecker()
analyser = DataAnalyser()
# plotter = DataPlotter() # Commented as it's not used
races = importer.import_laps_for_races(race_indexes)
checker.check_imported_races(races)
overtakes_in_races = analyser.count_overtakes_in_races(races)
print(f"Data for race {race_indexes[0][1]}/{race_indexes[0][0]}")
for current_lap in range(len(overtakes_in_races[0])):
print(f"Overtakes in lap {current_lap + 1}: {overtakes_in_races[0][current_lap]}")
return overtakes_in_races
if __name__ == '__main__':
app = Main()
# Call the get_overtakes_in_races method
app.main()
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment