diff --git a/DataAnalyser.py b/DataAnalyser.py index ee480342fc4693c8576ab3b72fa1505833eecfe3..db90be62c06ed7ce267adf14b628664baa8db586 100644 --- a/DataAnalyser.py +++ b/DataAnalyser.py @@ -1,22 +1,27 @@ import pandas as pandas from fastf1.core import Session, Lap, Laps -from DataHandler import DataHandler +from DataHandler import DataHandler, SessionIdentifier class DataAnalyser(DataHandler): + ''' + Analyses sessions by extrapolating existing or new data from them. + Any method of this class must be given a Session object or a list thereof. If the method does not require such an + object, it should not be part of this class. + ''' # ===== Overtakes ===== - def analyseRacesForOvertakes(self, races: list[Session]): + def getOvertakesPerLapForRaces(self, races: list[Session]): overtakesInRaces: list[list[int]] = [[]] for race in races: - overtakesInRaces.append(self.analyseRaceForOvertakes(race)) + overtakesInRaces.append(self.getOvertakesPerLapForRace(race)) return overtakesInRaces - def analyseRaceForOvertakes(self, race: Session): + def getOvertakesPerLapForRace(self, race: Session): if not race.session_info["Type"] == "Race": raise ValueError(f"Session must be a race session, not a {race.session_info["Type"]} session") overtakesInLaps: list[int] = self.countOvertakesPerLap(race) @@ -112,16 +117,12 @@ class DataAnalyser(DataHandler): # ===== 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): + def filterForWetSessions(self, sessions: list[Session]): x = 0 - return 0 + + + + return sessions diff --git a/DataHandler.py b/DataHandler.py index 8c0d92a2297dedbeef157d5177118761aae2ba1a..dcb288ec6148368c50118ffc14ba7fd8cd20e024 100644 --- a/DataHandler.py +++ b/DataHandler.py @@ -8,6 +8,7 @@ class DataHandler(ABC): self.activateDebugOvertakeAnalysis = False self.slickCompounds = ('SOFT', 'MEDIUM', 'HARD') self.countOutPitstops = True + self.firstFastF1Year: int = 2018 class SessionIdentifier: year: int diff --git a/DataImporter.py b/DataImporter.py index 44f888df75411993c1545652c0e2996646e0a908..7fe764778dcfd92983bf2f5916bc38c436426ece 100644 --- a/DataImporter.py +++ b/DataImporter.py @@ -4,33 +4,69 @@ from abc import ABC from fastf1.core import Session from fastf1.events import EventSchedule, Event +from DataAnalyser import DataAnalyser from DataHandler import DataHandler, SessionIdentifier class DataImporter(DataHandler, ABC): + ''' + Imports/loads sessions as specified by SessionIdentifiers and/or certain criteria such as rain. + + Any method of this class must be given a SessionIdentifier, a time or a time period by which to select + sessions. If the method does not require one of these, it should not be part of this class. + ''' + def importAllEventsFromYear(self, year: int): races: list[Event] = [] - schedule: EventSchedule = fastf1.get_event_schedule(year, include_testing = False) - for raceIndex in schedule['RoundNumber']: races.append(schedule.get_event_by_round(raceIndex)) - return races - def importRaceWeather(self): - x = 0 + def importSessionWeather(self, sessionIdentifier: SessionIdentifier): + ''' + Import a session containing only weather data. + :param sessionIdentifier: Session to import. Note that only races after 2017 can import weather, as session data + before 2018 is provided by the Ergast/Jolpica API, which does not contain weather data. + :return: Session object containing only weather data and basic session information. + ''' + return self.importSession(sessionIdentifier, laps = False, telemetry = False, weather = True, messages = False) - - def importSessions(self, sessionIdentifiers: list[SessionIdentifier], laps = True, telemetry = False, weather = True, messages = False): + def importSessions(self, sessionIdentifiers: list[SessionIdentifier], laps = True, telemetry = False, weather = False, messages = False): sessions: list[Session] = [] for sessionIdentifier in sessionIdentifiers: sessions.append(self.importSession(sessionIdentifier, laps, weather, messages, telemetry)) return sessions + def importSession(self, sessionIdentifier: SessionIdentifier, laps = True, telemetry = False, weather = False, messages = False): + if weather is True and sessionIdentifier.year < self.firstFastF1Year: + raise ValueError(f"Cannot fetch weather data for races before {self.firstFastF1Year} due to API limitations") - def importSession(self, sessionIdentifier: SessionIdentifier, laps = True, telemetry = False, weather = True, messages = False): session = fastf1.get_session(sessionIdentifier.year, sessionIdentifier.event, sessionIdentifier.sessionType) session.load(laps = laps, telemetry = telemetry, weather = weather, messages = messages) - return session \ No newline at end of file + return session + + def getRainRacesSince(self, firstYear: int): + rainRaces: list[Session] = [] + for firstYear in range(firstYear, 2025): # FIXME: Add automatic upper bound for year so that it can at most be set to current year or year of latest f1 race (e.g. shortly after new years eve) + wetWeatherRacesInYear: list[Session] = self.getRainRacesIn(firstYear) + for wetWeatherRace in wetWeatherRacesInYear: + rainRaces.append(wetWeatherRace) + return rainRaces + + def getRainRacesIn(self, year: int): + events: list[Event] = self.importAllEventsFromYear(year) + races: list[Session] = [] + for event in events: + raceIdentifier: SessionIdentifier = SessionIdentifier(event.year, event["RoundNumber"], "R") + try: + raceSession: Session = self.importSessionWeather(raceIdentifier) + races.append(raceSession) + except ValueError: # Needed as long as weather data for races before 2018 hasn't been supplemented + print(f"Weather for {event["EventName"]} {event.year} could not be determined") + + analyser: DataAnalyser = DataAnalyser() + rainRaces: list[Session] = analyser.filterForWetSessions(races) + + return rainRaces \ No newline at end of file diff --git a/Todos.md b/Todos.md index 71eb5f58c10a7999ce1729f825f029ea43021763..1231d7f76bb3cc6951cef8fcaea14c5af872e160 100644 --- a/Todos.md +++ b/Todos.md @@ -10,4 +10,8 @@ - [ ] 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 \ No newline at end of file +- [ ] Also read out direct weather data from API +- [ ] Extrapolate weather conditions from lap times by difference between dry & wet laptimes in order to retrieve + weather / track conditions for races before 2018, as Ergast / Jolpica APIs do not provide this data + - Alternatively, get weather data from a dedicated weather API for the location & date/time the race was carried out +- \ No newline at end of file diff --git a/main.py b/main.py index e28a4a8c9375cf4de0c5cb5a6b6a50ab511b5267..8e54a3a58ada0e6ba75575e21baccf314069175b 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from fastf1.core import Session from fastf1.events import Event from DataAnalyser import DataAnalyser @@ -17,7 +18,9 @@ class Main: def main(self): - self.listWetWeatherRacesSince(2015) + dataHandler: Main.DataHandlingPackage = Main.DataHandlingPackage() + rainRaces: list[Session] = dataHandler.importer.getRainRacesSince(2015) # FIXME: Change to all races _since_ 2015 + racesToAnalyse = [ SessionIdentifier(2022, "Imola", "R"), # Imola 2022 (DWR) @@ -27,14 +30,6 @@ class Main: ] self.overtakeAnalysis(racesToAnalyse) - def listWetWeatherRacesSince(self, firstYear: int): - dataHandler: Main.DataHandlingPackage = self.DataHandlingPackage() - events: list[Event] = [] - for year in range(firstYear, 2025): # FIXME: Add automatic upper bound for year - events.append(dataHandler.importer.importAllEventsFromYear(year)) - for event in events: - x = 0 - # TODO: filter out dry races def overtakeAnalysis(self, raceSessionIdentifiers: list[SessionIdentifier]): dataHandler: Main.DataHandlingPackage = self.DataHandlingPackage() @@ -49,7 +44,7 @@ class Main: # Analyse - overtakesInRaces: list[int] = dataHandler.analyser.analyseRaceForOvertakes(raceSession) + overtakesInRaces: list[int] = dataHandler.analyser.getOvertakesPerLapForRace(raceSession) print("Overtake analysis done") # weatherInRaces = analyser.analyseRaceForWeather(raceSession) earliestTireChange: int = dataHandler.analyser.getEarliestTireChange(raceSession) # first lap where someone switched from slicks to non slicks or vice versa, denoted by lap number @@ -78,6 +73,4 @@ class Main: if __name__ == '__main__': app = Main() - - # Call the get_overtakes_in_races method app.main() \ No newline at end of file