├── f1predict ├── __init__.py ├── common │ ├── __init__.py │ ├── RaceData.py │ ├── Season.py │ ├── common.py │ ├── file_operations.py │ └── dataclean.py ├── quali │ ├── __init__.py │ ├── baseline │ │ ├── __init__.py │ │ ├── ResultsGetter.py │ │ ├── RandomModel.py │ │ └── PreviousQualiResultModel.py │ ├── utils.py │ ├── monteCarlo.py │ ├── LinearModel.py │ ├── dataclean.py │ └── DataProcessor.py └── race │ ├── __init__.py │ ├── baseline │ ├── __init__.py │ ├── RandomModel.py │ ├── ResultsGetter.py │ └── QualiOrderModel.py │ ├── dataclean.py │ ├── raceMonteCarlo.py │ ├── retirementBlame.py │ ├── utils.py │ ├── EloModel.py │ └── DataProcessor.py ├── scripts ├── __init__.py ├── datacleaner.py ├── datacleaner_race.py ├── datacleaner_quali.py ├── variancehyperparams.py ├── quali_model_trainer.py ├── grid_generate-predictions.py ├── generate-predictions.py └── generate-predictions-for-all.py ├── .gitattributes ├── data ├── Engines.csv ├── gridTest.json ├── qualiChanges.csv ├── TeamChanges.csv ├── newDrivers.json ├── optimalParams_lastBest.py ├── grid.txt ├── grid.json ├── futureRaces.json └── ConstructorEngines.csv ├── .gitignore ├── setup.py ├── dbUpdater.sh ├── requirements.txt ├── README.md ├── notebooks ├── RaceDataVisualisation.ipynb ├── QualiInsights.ipynb └── ConstructorDataExplorer.ipynb └── LICENSE /f1predict/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /f1predict/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /f1predict/quali/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /f1predict/race/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /f1predict/quali/baseline/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /f1predict/race/baseline/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /data/Engines.csv: -------------------------------------------------------------------------------- 1 | 1,Ferrari 2 | 2,BMW 3 | 3,Mercedes 4 | 4,Renault 5 | 5,Ford 6 | 6,Cosworth 7 | 7,Honda 8 | 8,Toyota 9 | 9,Ferrari_Old 10 | -------------------------------------------------------------------------------- /f1predict/common/RaceData.py: -------------------------------------------------------------------------------- 1 | class RaceData: 2 | def __init__(self, circuitId, round): 3 | self.circuitId = circuitId 4 | self.round = round -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | __pycache__ 3 | out 4 | dist 5 | build 6 | .DS_Store 7 | .vscode 8 | *.sql 9 | *.xml 10 | *.ods 11 | *.pickle 12 | *.egg-info 13 | config.py 14 | .ipynb_checkpoints 15 | .idea 16 | user_variables.txt 17 | -------------------------------------------------------------------------------- /data/gridTest.json: -------------------------------------------------------------------------------- 1 | [ 2 | 1, 3 | 822, 4 | 830, 5 | 844, 6 | 20, 7 | 848, 8 | 832, 9 | 846, 10 | 807, 11 | 840, 12 | 817, 13 | 839, 14 | 842, 15 | 826, 16 | 825, 17 | 154, 18 | 8, 19 | 847, 20 | 841, 21 | 849 22 | ] -------------------------------------------------------------------------------- /data/qualiChanges.csv: -------------------------------------------------------------------------------- 1 | Year,Race,Q3,Q2 2 | 2006,1,10,16 3 | 2007,1,10,16 4 | 2008,1,10,16 5 | 2008,5,10,15 6 | 2009,1,10,15 7 | 2010,1,10,17 8 | 2011,1,10,17 9 | 2012,1,10,17 10 | 2013,1,10,16 11 | 2014,1,10,16 12 | 2014,17,10,14 13 | 2014,19,8,13 14 | 2015,1,10,15 15 | 2016,1,10,16 16 | 2017,1,10,15 17 | 2018,1,10,15 18 | 2019,1,10,15 19 | -------------------------------------------------------------------------------- /data/TeamChanges.csv: -------------------------------------------------------------------------------- 1 | Year,New,Old 2 | 2005,9,19 3 | 2006,5,18 4 | 2006,2,15 5 | 2006,13,17 6 | 2006,14,13 7 | 2006,11,16 8 | 2007,12,14 9 | 2008,10,12 10 | 2009,23,11 11 | 2010,15,2 12 | 2010,131,23 13 | 2012,208,4 14 | 2012,207,205 15 | 2012,206,166 16 | 2015,209,206 17 | 2016,4,208 18 | 2019,211,10 19 | 2019,51,15 20 | 2020,213,5 21 | 2021,117,211 22 | 2021,214,4 -------------------------------------------------------------------------------- /data/newDrivers.json: -------------------------------------------------------------------------------- 1 | { 2 | "drivers": { 3 | "8": "", 4 | "841": "", 5 | "842": "", 6 | "852": 213, 7 | "20": 117, 8 | "844": "", 9 | "853": 210, 10 | "854": 210, 11 | "846": "", 12 | "832": 6, 13 | "1": "", 14 | "822": "", 15 | "815": 9, 16 | "840": 117, 17 | "4": 214, 18 | "830": "", 19 | "817": 1, 20 | "839": 214, 21 | "849": "", 22 | "847": 3 23 | } 24 | } -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | 4 | with open("README.md", "r") as fh: 5 | long_description = fh.read() 6 | 7 | 8 | setuptools.setup( 9 | name='F1Predict', 10 | version='1.0dev', 11 | author="Ville Kuosmanen", 12 | description="Tools for predicting Formula 1 Grand Prix results", 13 | long_description=long_description, 14 | url="https://github.com/villekuosmanen/F1Predict", 15 | packages=['f1predict', ], 16 | ) 17 | -------------------------------------------------------------------------------- /f1predict/common/Season.py: -------------------------------------------------------------------------------- 1 | class Season: 2 | def __init__(self): 3 | self.races = {} 4 | #constructorId, engineId 5 | self.constructorEngines = {} 6 | #New, old 7 | self.teamChanges = {} 8 | def addRace(self, raceId, raceData): 9 | self.races[raceId] = raceData 10 | def addConstructorEngine(self, constructorId, engineId): 11 | self.constructorEngines[constructorId] = engineId 12 | def addTeamChange(self, new, old): 13 | self.teamChanges[new] = old -------------------------------------------------------------------------------- /f1predict/common/common.py: -------------------------------------------------------------------------------- 1 | def getColor(constructor): 2 | return { 3 | "Mercedes": "#00D2BE", 4 | "Ferrari": "#DC0000", 5 | "Red Bull": "#0600EF", 6 | "Aston Martin": "#006F62", 7 | "Williams": "#005AFF", 8 | "Alpine F1 Team": "#0090FF", 9 | "AlphaTauri": "#2B4562", 10 | "Haas F1 Team": "#FFFFFF", 11 | "McLaren": "#FF9800", 12 | "Alfa Romeo": "#900000" 13 | }.get(constructor, "#000000") 14 | 15 | def getCurrentYear(): 16 | return 2021 -------------------------------------------------------------------------------- /data/optimalParams_lastBest.py: -------------------------------------------------------------------------------- 1 | #Optimal values when using last best time: 2 | 3 | cleaner.k_engine_change = 0.0185 4 | cleaner.k_const_change = 0.27 5 | cleaner.k_driver_change = 0.18 6 | cleaner.k_track_impact = 0.00 7 | cleaner.k_eng_impact = 1.05 8 | cleaner.k_const_impact = 0.55 9 | 10 | cleaner.k_rookie_pwr = 1.15 11 | #cleaner.k_rookie_variance = 5 12 | cleaner.k_race_regress_exp = 0.87 #TODO needs to change! 13 | #cleaner.k_variance_multiplier_end = 1.5 14 | 15 | cleaner.k_eng_regress = 1.02 16 | cleaner.k_const_regress = 0.68 17 | cleaner.k_driver_regress = 0.81 18 | -------------------------------------------------------------------------------- /data/grid.txt: -------------------------------------------------------------------------------- 1 | # A helper file for current driver IDs. May be out of date 2 | 3 | 4 | 1: Lewis Hamilton 5 | 822: Valtteri Bottas 6 | 7 | 830: Max Verstappen 8 | 815: Sergio Pérez 9 | 10 | 844: Charles Leclerc 11 | 832: Carlos Sainz 12 | 13 | 817: Daniel Ricciardo 14 | 846: Lando Norris 15 | 16 | 852: Yuki Tsunoda 17 | 842: Pierre Gasly 18 | 19 | 20: Sebastian Vettel 20 | 840: Lance Stroll 21 | 22 | 4: Fernando Alonso 23 | 839: Esteban Ocon 24 | 25 | 8: Kimi Räikkönen 26 | 841: Antonio Giovinazzi 27 | 28 | 853: Nikita Mazepin 29 | 854: Mick Schumacher 30 | 31 | 847: George Russell 32 | 849: Nicholas Latifi 33 | 34 | 35 | 807: Nico Hulkenberg 36 | 37 | -------------------------------------------------------------------------------- /f1predict/quali/baseline/ResultsGetter.py: -------------------------------------------------------------------------------- 1 | class ResultsGetter: 2 | '''Returns results of qualifyings without times''' 3 | 4 | def __init__(self, seasonsData, qualiResultsData): 5 | self.seasonsData = seasonsData 6 | self.qualiResultsData = qualiResultsData 7 | 8 | def constructQualiResults(self): 9 | results = [] 10 | 11 | for year, season in self.seasonsData.items(): # Read every season: 12 | racesAsList = list(season.races.items()) 13 | racesAsList.sort(key=lambda x: x[1].round) 14 | for raceId, data in racesAsList: 15 | # A single race 16 | if raceId in self.qualiResultsData: 17 | qresults = self.qualiResultsData[raceId] 18 | res = [x[0] for x in qresults] 19 | results.append(res) 20 | return results -------------------------------------------------------------------------------- /dbUpdater.sh: -------------------------------------------------------------------------------- 1 | # Delete old SQL files: 2 | rm f1db.sql 3 | rm f1db.sql.gz 4 | 5 | # Get most recent database dump from the internet and extract it 6 | wget http://ergast.com/downloads/f1db.sql.gz 7 | gunzip f1db.sql.gz 8 | 9 | # Run the database dump SQL file to the database 10 | # Note that this resets everything in the database 11 | db_username=$(grep -Po '(?<=^db_username=)\w*!*' 'user_variables.txt') 12 | db_password=$(grep -Po '(?<=^db_password=)\w*!*' 'user_variables.txt') 13 | db_database=$(grep -Po '(?<=^db_database=)\w*!*$' 'user_variables.txt') 14 | mysql -u $db_username -p$db_password -D $db_database < f1db.sql 15 | 16 | # Run data cleaning scripts 17 | python3 scripts/datacleaner.py 18 | python3 scripts/datacleaner_quali.py 19 | python3 scripts/datacleaner_race.py 20 | 21 | # Run scripts that generate predictions 22 | python3 scripts/quali_model_trainer.py 23 | python3 scripts/generate-predictions.py -------------------------------------------------------------------------------- /f1predict/race/baseline/RandomModel.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class RandomModel: 5 | '''Predicts race results randomly''' 6 | 7 | def __init__(self, seasonsData, raceResultsData): 8 | self.seasonsData = seasonsData 9 | self.raceResultsData = raceResultsData 10 | 11 | def constructPredictions(self): 12 | predictions = [] 13 | 14 | for year, season in self.seasonsData.items(): # Read every season: 15 | racesAsList = list(season.races.items()) 16 | racesAsList.sort(key=lambda x: x[1].round) 17 | for raceId, data in racesAsList: 18 | # A single race 19 | if raceId in self.raceResultsData: 20 | results = self.raceResultsData[raceId] 21 | driver_ids = [x['driverId'] for x in results] 22 | random.shuffle(driver_ids) 23 | predictions.append(driver_ids) 24 | return predictions -------------------------------------------------------------------------------- /f1predict/quali/baseline/RandomModel.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class RandomModel: 5 | '''Predicts qualifying results randomly''' 6 | 7 | def __init__(self, seasonsData, qualiResultsData): 8 | self.seasonsData = seasonsData 9 | self.qualiResultsData = qualiResultsData 10 | 11 | def constructPredictions(self): 12 | predictions = [] 13 | 14 | for year, season in self.seasonsData.items(): # Read every season: 15 | racesAsList = list(season.races.items()) 16 | racesAsList.sort(key=lambda x: x[1].round) 17 | for raceId, data in racesAsList: 18 | # A single race 19 | if raceId in self.qualiResultsData: 20 | qresults = self.qualiResultsData[raceId] 21 | driver_ids = [x[0] for x in qresults] 22 | random.shuffle(driver_ids) 23 | predictions.append(driver_ids) 24 | return predictions -------------------------------------------------------------------------------- /f1predict/race/baseline/ResultsGetter.py: -------------------------------------------------------------------------------- 1 | class ResultsGetter: 2 | '''Returns results of races without times''' 3 | 4 | def __init__(self, seasonsData, raceResultsData): 5 | self.seasonsData = seasonsData 6 | self.raceResultsData = raceResultsData 7 | 8 | def constructRaceResults(self): 9 | racesResults = [] 10 | 11 | for year, season in self.seasonsData.items(): # Read every season: 12 | racesAsList = list(season.races.items()) 13 | racesAsList.sort(key=lambda x: x[1].round) 14 | for raceId, data in racesAsList: 15 | # A single race 16 | if raceId in self.raceResultsData: 17 | results = self.raceResultsData[raceId] 18 | if results: 19 | results.sort(key=lambda x: (x['position'] is None, x['position'])) 20 | res = [x['driverId'] for x in results] 21 | racesResults.append(res) 22 | return racesResults -------------------------------------------------------------------------------- /f1predict/common/file_operations.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def getUserVariables(filename): 4 | user_vars = {} 5 | with open(filename) as f: 6 | for line in f: 7 | key, value = line.partition("=")[::2] 8 | user_vars[key.rstrip()] = value.rstrip() 9 | return user_vars 10 | 11 | def readNextRaceDetails(filename): 12 | with open(filename, 'r') as handle: 13 | futureRaces = json.load(handle) 14 | circuit = futureRaces[0]["circuitId"] 15 | circuitName = futureRaces[0]["name"] 16 | raceId = futureRaces[0]["raceId"] 17 | year = futureRaces[0]["year"] 18 | return circuit, circuitName, raceId, year 19 | 20 | def editIndexFile(filename, year, raceId, circuitName): 21 | with open(filename, 'r+') as handle: 22 | data = json.load(handle) 23 | data[str(year)][str(raceId)] = circuitName 24 | handle.seek(0) # <--- should reset file position to the beginning. 25 | json.dump(data, handle, indent=4) 26 | handle.truncate() 27 | 28 | def publishPredictions(filename, outFile): 29 | with open(filename, 'w') as fp: 30 | json.dump(outFile, fp) -------------------------------------------------------------------------------- /f1predict/race/dataclean.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | 4 | from f1predict.common.Season import Season 5 | from f1predict.common.RaceData import RaceData 6 | 7 | def addRaceSeasonData(cursor, seasonsData, raceResultsData, year): 8 | """Adds a Season Data object to the given map of seasons""" 9 | s = Season() 10 | 11 | sql = "SELECT `raceId`, `round`, `circuitId`, `name` FROM `races` WHERE `year`=%s" 12 | cursor.execute(sql, year) 13 | result = cursor.fetchall() 14 | 15 | for x in result: 16 | circuitId = x.get('circuitId') 17 | roundNo = x.get('round') 18 | 19 | raceData = RaceData(circuitId, roundNo) 20 | s.addRace(x.get('raceId'), raceData) 21 | addRaceResults(cursor, raceResultsData, x.get('raceId')) 22 | seasonsData[year] = s 23 | 24 | def addRaceResults(cursor, raceResultsData, raceId): 25 | """Adds race results data""" 26 | sql = "SELECT `driverId`, `constructorId`, `position`, `grid`, `status`.status as status \ 27 | FROM `results`, `status` WHERE `raceId`=%s AND `status`.statusId = `results`.statusId" 28 | cursor.execute(sql, raceId) 29 | result = cursor.fetchall() 30 | 31 | if result: 32 | result.sort(key=lambda result: (result['position'] is None, result['position'])) 33 | raceResultsData[raceId] = result -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.3.1 2 | autopep8==1.4.3 3 | backcall==0.1.0 4 | bleach==3.1.4 5 | cycler==0.10.0 6 | decorator==4.3.0 7 | defusedxml==0.5.0 8 | entrypoints==0.2.3 9 | ipykernel==5.1.0 10 | ipyparallel==6.2.3 11 | ipython==7.2.0 12 | ipython-genutils==0.2.0 13 | ipywidgets==7.4.2 14 | isort==4.3.21 15 | jedi==0.13.2 16 | Jinja2==2.11.2 17 | joblib==0.15.1 18 | jsonschema==2.6.0 19 | jupyter==1.0.0 20 | jupyter-client==5.2.4 21 | jupyter-console==6.0.0 22 | jupyter-core==4.4.0 23 | jupyterlab==0.35.4 24 | jupyterlab-server==0.2.0 25 | kiwisolver==1.0.1 26 | lazy-object-proxy==1.4.2 27 | MarkupSafe==1.1.0 28 | matplotlib==3.0.2 29 | mccabe==0.6.1 30 | mistune==0.8.4 31 | nbconvert==5.4.0 32 | nbformat==4.4.0 33 | notebook==5.7.8 34 | numpy==1.15.4 35 | pandas==0.23.4 36 | pandocfilters==1.4.2 37 | parso==0.3.1 38 | pexpect==4.6.0 39 | pickleshare==0.7.5 40 | prometheus-client==0.5.0 41 | prompt-toolkit==2.0.7 42 | ptyprocess==0.6.0 43 | pycodestyle==2.5.0 44 | Pygments==2.3.1 45 | pylint==2.4.2 46 | PyMySQL==0.9.3 47 | pyparsing==2.3.0 48 | python-dateutil==2.7.5 49 | pytz==2018.7 50 | pyzmq==17.1.2 51 | qtconsole==4.4.3 52 | rope==0.11.0 53 | scikit-surprise==1.1.0 54 | scipy==1.2.0 55 | Send2Trash==1.5.0 56 | six==1.12.0 57 | surprise==0.1 58 | terminado==0.8.1 59 | testpath==0.4.2 60 | tornado==5.1.1 61 | traitlets==4.3.2 62 | typed-ast==1.4.0 63 | wcwidth==0.1.7 64 | webencodings==0.5.1 65 | widgetsnbextension==3.4.2 66 | wrapt==1.11.2 67 | -------------------------------------------------------------------------------- /f1predict/race/baseline/QualiOrderModel.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class QualiOrderModel: 5 | '''Predicts race results to be equal to qualifying results''' 6 | 7 | def __init__(self, seasonsData, raceResultsData, qualiResultsData): 8 | self.seasonsData = seasonsData 9 | self.raceResultsData = raceResultsData 10 | self.qualiResultsData = qualiResultsData 11 | 12 | def constructPredictions(self): 13 | predictions = [] 14 | for year, season in self.seasonsData.items(): # Read every season: 15 | racesAsList = list(season.races.items()) 16 | racesAsList.sort(key=lambda x: x[1].round) 17 | for raceId, data in racesAsList: 18 | # A single race 19 | if raceId in self.raceResultsData: 20 | results = self.raceResultsData[raceId] 21 | if results: 22 | this_pred = [] 23 | no_quali = [] 24 | results.sort(key=lambda x: x['grid']) 25 | for x in results: 26 | if x['grid']: 27 | this_pred.append(x['driverId']) 28 | else: 29 | no_quali.append(x['driverId']) 30 | random.shuffle(no_quali) 31 | this_pred += no_quali 32 | predictions.append(this_pred) 33 | return predictions -------------------------------------------------------------------------------- /scripts/datacleaner.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | import pandas as pd 4 | import numpy as np 5 | import pickle 6 | 7 | from f1predict.common import dataclean 8 | from f1predict.common import file_operations 9 | 10 | #DriverId, name 11 | driversData = {} 12 | #ConstructorId, name 13 | constructorsData = {} 14 | #EngineId, name 15 | enginesData = {} 16 | 17 | USER_VARS = file_operations.getUserVariables("user_variables.txt") 18 | 19 | #Set up a database connection: 20 | connection = pymysql.connect(host='localhost', 21 | user=USER_VARS['db_username'], 22 | password=USER_VARS['db_password'], 23 | db=USER_VARS['db_database'], 24 | charset='utf8', 25 | cursorclass=pymysql.cursors.DictCursor) 26 | 27 | qualiChanges = pd.read_csv('data/qualiChanges.csv') 28 | try: 29 | with connection.cursor() as cursor: 30 | dataclean.getDriversData(cursor, driversData) 31 | dataclean.getConstructorData(cursor, constructorsData) 32 | dataclean.getEngineData(enginesData) 33 | finally: 34 | connection.close() 35 | 36 | with open('data/driversData.pickle', 'wb') as out: 37 | pickle.dump(driversData, out, protocol=pickle.HIGHEST_PROTOCOL) 38 | 39 | with open('data/constructorsData.pickle', 'wb') as out: 40 | pickle.dump(constructorsData, out, protocol=pickle.HIGHEST_PROTOCOL) 41 | 42 | with open('data/enginesData.pickle', 'wb') as out: 43 | pickle.dump(enginesData, out, protocol=pickle.HIGHEST_PROTOCOL) -------------------------------------------------------------------------------- /f1predict/quali/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import operator 3 | import numpy as np 4 | 5 | def overwriteQualiModelWithNewDrivers(qualiModel, filename): 6 | newDrivers = json.load(open(filename))["drivers"] 7 | newDrivers = {int(did): cid for did, cid in newDrivers.items()} 8 | for did, cid in newDrivers.items(): 9 | if did < 0 or did not in qualiModel.drivers: # Cases when driver doesn't exist in data 10 | qualiModel.addNewDriver(did, "__PLACEHOLDER__", cid) 11 | if not cid == "": 12 | # Data in newDrivers.json overwrites database 13 | qualiModel.drivers[did].constructor = qualiModel.constructors[int(cid)] 14 | 15 | def calculateOrder(qualiModel, driverIDs, circuit): 16 | entries = [] 17 | for did in driverIDs: 18 | entry = [ 19 | qualiModel.drivers[did].pwr, 20 | qualiModel.drivers[did].constructor.pwr, 21 | qualiModel.drivers[did].constructor.engine.pwr, 22 | qualiModel.drivers[did].trackpwr[circuit], 23 | qualiModel.drivers[did].constructor.trackpwr[circuit], 24 | qualiModel.drivers[did].constructor.engine.trackpwr[circuit], 25 | 1 26 | ] 27 | entries.append(entry) 28 | 29 | linearRegResults = [np.dot(x, qualiModel.theta) for x in entries] 30 | 31 | orderedResults = [] # [(did, prediction) ...] 32 | for index, did in enumerate(driverIDs): 33 | orderedResults.append((did, linearRegResults[index])) 34 | 35 | orderedResults.sort(key = operator.itemgetter(1)) 36 | return [a for (a, b) in orderedResults] -------------------------------------------------------------------------------- /scripts/datacleaner_race.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | import pandas as pd 4 | import numpy as np 5 | import pickle 6 | 7 | from f1predict.common import dataclean 8 | from f1predict.common import file_operations 9 | from f1predict.common import common 10 | from f1predict.race import dataclean as race_dataclean 11 | 12 | # Get season and race data 13 | raceSeasonsData = {} 14 | 15 | raceResultsData = {} 16 | 17 | #Read user variables: 18 | USER_VARS = file_operations.getUserVariables("user_variables.txt") 19 | 20 | #Set up a database connection: 21 | connection = pymysql.connect(host='localhost', 22 | user=USER_VARS['db_username'], 23 | password=USER_VARS['db_password'], 24 | db=USER_VARS['db_database'], 25 | charset='utf8', 26 | cursorclass=pymysql.cursors.DictCursor) 27 | 28 | try: 29 | with connection.cursor() as cursor: 30 | for year in range(2003, common.getCurrentYear() + 1): 31 | race_dataclean.addRaceSeasonData(cursor, raceSeasonsData, raceResultsData, year) 32 | dataclean.addEngineToConstructor(raceSeasonsData) 33 | dataclean.getTeamChangeData(raceSeasonsData) 34 | finally: 35 | connection.close() 36 | 37 | 38 | # Save to pickle file 39 | with open('data/raceSeasonsData.pickle', 'wb') as out: 40 | pickle.dump(raceSeasonsData, out, protocol=pickle.HIGHEST_PROTOCOL) 41 | 42 | with open('data/raceResultsData.pickle', 'wb') as out: 43 | pickle.dump(raceResultsData, out, protocol=pickle.HIGHEST_PROTOCOL) -------------------------------------------------------------------------------- /data/grid.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Lewis Hamilton", 4 | "id": 1 5 | }, 6 | { 7 | "name": "Valtteri Bottas", 8 | "id": 822 9 | }, 10 | { 11 | "name": "Max Verstappen", 12 | "id": 830 13 | }, 14 | { 15 | "name": "Sergio Pérez", 16 | "id": 815 17 | }, 18 | { 19 | "name": "Pierre Gasly", 20 | "id": 842 21 | }, 22 | { 23 | "name": "Lando Norris", 24 | "id": 846 25 | }, 26 | { 27 | "name": "Charles Leclerc", 28 | "id": 844 29 | }, 30 | { 31 | "name": "Esteban Ocon", 32 | "id": 839 33 | }, 34 | { 35 | "name": "Fernando Alonso", 36 | "id": 4 37 | }, 38 | { 39 | "name": "Sebastian Vettel", 40 | "id": 20 41 | }, 42 | { 43 | "name": "Daniel Ricciardo", 44 | "id": 817 45 | }, 46 | { 47 | "name": "Lance Stroll", 48 | "id": 840 49 | }, 50 | { 51 | "name": "Kimi Räikkönen", 52 | "id": 8 53 | }, 54 | { 55 | "name": "Antonio Giovinazzi", 56 | "id": 841 57 | }, 58 | { 59 | "name": "Carlos Sainz", 60 | "id": 832 61 | }, 62 | { 63 | "name": "Yuki Tsunoda", 64 | "id": 852 65 | }, 66 | { 67 | "name": "George Russell", 68 | "id": 847 69 | }, 70 | { 71 | "name": "Nicholas Latifi", 72 | "id": 849 73 | }, 74 | { 75 | "name": "Nikita Mazepin", 76 | "id": 853 77 | }, 78 | { 79 | "name": "Mick Schumacher", 80 | "id": 854 81 | } 82 | ] -------------------------------------------------------------------------------- /f1predict/quali/baseline/PreviousQualiResultModel.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | class PreviousQualiResultModel: 5 | '''Predicts qualifying results based on the results of previous qualifying''' 6 | 7 | def __init__(self, seasonsData, qualiResultsData): 8 | self.seasonsData = seasonsData 9 | self.qualiResultsData = qualiResultsData 10 | 11 | def constructPredictions(self): 12 | predictions = [] 13 | previousQualiResults = [] 14 | 15 | for year, season in self.seasonsData.items(): # Read every season: 16 | racesAsList = list(season.races.items()) 17 | racesAsList.sort(key=lambda x: x[1].round) 18 | for raceId, data in racesAsList: 19 | # A single race 20 | if raceId in self.qualiResultsData: 21 | qresults = self.qualiResultsData[raceId] 22 | driver_ids = [x[0] for x in qresults] 23 | 24 | prediction = [] 25 | for driver_id in previousQualiResults: 26 | if driver_id in driver_ids: 27 | prediction.append(driver_id) 28 | 29 | unseen_drivers = [] 30 | for driver_id in driver_ids: 31 | if driver_id not in prediction: 32 | unseen_drivers.append(driver_id) 33 | random.shuffle(unseen_drivers) 34 | prediction.extend(unseen_drivers) 35 | 36 | predictions.append(prediction) 37 | qresults.sort(key=lambda x: x[2]) 38 | previousQualiResults = [x[0] for x in qresults] 39 | return predictions -------------------------------------------------------------------------------- /data/futureRaces.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "raceId": 1062, 4 | "name": "Hungarian Grand Prix", 5 | "circuitId": 11, 6 | "year": 2021 7 | }, 8 | { 9 | "raceId": 1063, 10 | "name": "Belgian Grand Prix", 11 | "circuitId": 13, 12 | "year": 2021 13 | }, 14 | { 15 | "raceId": 1064, 16 | "name": "Dutch Grand Prix", 17 | "circuitId": 39, 18 | "year": 2021 19 | }, 20 | { 21 | "raceId": 1065, 22 | "name": "Italian Grand Prix", 23 | "circuitId": 14, 24 | "year": 2021 25 | }, 26 | { 27 | "raceId": 1066, 28 | "name": "Russian Grand Prix", 29 | "circuitId": 71, 30 | "year": 2021 31 | }, 32 | { 33 | "raceId": 1067, 34 | "name": "Turkish Grand Prix", 35 | "circuitId": 5, 36 | "year": 2021 37 | }, 38 | { 39 | "raceId": 1068, 40 | "name": "Japanese Grand Prix", 41 | "circuitId": 22, 42 | "year": 2021 43 | }, 44 | { 45 | "raceId": 1069, 46 | "name": "United States Grand Prix", 47 | "circuitId": 69, 48 | "year": 2021 49 | }, 50 | { 51 | "raceId": 1070, 52 | "name": "Mexico City Grand Prix", 53 | "circuitId": 32, 54 | "year": 2021 55 | }, 56 | { 57 | "raceId": 1071, 58 | "name": "Brazilian Grand Prix", 59 | "circuitId": 18, 60 | "year": 2021 61 | }, 62 | { 63 | "raceId": 1072, 64 | "name": "Saudi Arabian Grand Prix", 65 | "circuitId": 77, 66 | "year": 2021 67 | }, 68 | { 69 | "raceId": 1073, 70 | "name": "Abu Dhabi Grand Prix", 71 | "circuitId": 24, 72 | "year": 2021 73 | } 74 | ] -------------------------------------------------------------------------------- /scripts/datacleaner_quali.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | import pandas as pd 4 | import numpy as np 5 | import pickle 6 | 7 | from f1predict.common import dataclean 8 | from f1predict.common import file_operations 9 | from f1predict.common import common 10 | from f1predict.quali import dataclean as quali_dataclean 11 | 12 | #Create data classes 13 | #Year, Season 14 | seasonsData = {} 15 | #RaceId, List of tuples of (driverId, constructorId, time) 16 | qualiResultsData = {} 17 | 18 | USER_VARS = file_operations.getUserVariables("user_variables.txt") 19 | 20 | #Set up a database connection: 21 | connection = pymysql.connect(host='localhost', 22 | user=USER_VARS['db_username'], 23 | password=USER_VARS['db_password'], 24 | db=USER_VARS['db_database'], 25 | charset='utf8', 26 | cursorclass=pymysql.cursors.DictCursor) 27 | 28 | qualiChanges = pd.read_csv('data/qualiChanges.csv') 29 | print(qualiChanges) 30 | try: 31 | with connection.cursor() as cursor: 32 | total_mistakes = 0 33 | for x in range(2003, common.getCurrentYear() + 1): 34 | no_mistakes = quali_dataclean.addSeason(cursor, seasonsData, qualiResultsData, qualiChanges, x) 35 | total_mistakes += no_mistakes 36 | dataclean.addEngineToConstructor(seasonsData) 37 | dataclean.getTeamChangeData(seasonsData) 38 | # print(total_mistakes) 39 | finally: 40 | connection.close() 41 | 42 | with open('data/seasonsData.pickle', 'wb') as out: 43 | pickle.dump(seasonsData, out, protocol=pickle.HIGHEST_PROTOCOL) 44 | 45 | with open('data/qualiResultsData.pickle', 'wb') as out: 46 | pickle.dump(qualiResultsData, out, protocol=pickle.HIGHEST_PROTOCOL) -------------------------------------------------------------------------------- /f1predict/common/dataclean.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | import pandas as pd 4 | 5 | def getDriversData(cursor, driversData): 6 | """Gathers the wanted data from all drivers""" 7 | sql = "SELECT `driverId`, `forename`, `surname` FROM `drivers`" 8 | cursor.execute(sql) 9 | result = cursor.fetchall() 10 | for x in result: 11 | driversData[x.get('driverId')] = x.get('forename') + " " + x.get('surname') 12 | 13 | def getConstructorData(cursor, constructorsData): 14 | """Gathers the wanted data from all constructors""" 15 | sql = "SELECT `constructorId`, `name` FROM `constructors`" 16 | cursor.execute(sql) 17 | result = cursor.fetchall() 18 | for x in result: 19 | constructorsData[x.get('constructorId')] = x.get('name') 20 | 21 | def getEngineData(enginesData): 22 | """Gathers the wanted data from all engines""" 23 | df = pd.read_csv('data/Engines.csv', names=['engineId', 'name']) 24 | for row in df.itertuples(): 25 | #The index value is 0: meaning 1=id, 2=name 26 | enginesData[row[1]] = row[2] 27 | 28 | def addEngineToConstructor(seasonsData): 29 | """Constructs a table that shows what engine each constructor used in a given year""" 30 | df = pd.read_csv('data/ConstructorEngines.csv') 31 | for row in df.itertuples(): 32 | #The index value is 0: meaning 1=year, 2=teamId, 3=engineId 33 | seasonsData[row[1]].addConstructorEngine(row[2], row[3]) 34 | 35 | def getTeamChangeData(seasonsData): 36 | """Constructs a table that shows when a team changed its name and ID, therefore tying the two together""" 37 | df = pd.read_csv('data/TeamChanges.csv') 38 | for row in df.itertuples(): 39 | #The index value is 0: meaning 1=year, 2=newId, 3=oldId 40 | seasonsData[row[1]].addTeamChange(row[2], row[3]) 41 | 42 | 43 | -------------------------------------------------------------------------------- /scripts/variancehyperparams.py: -------------------------------------------------------------------------------- 1 | # DEPRECATED, DO NOT USE 2 | # This file will be updated in the future 3 | 4 | from python import * 5 | 6 | import pickle 7 | import numpy as np 8 | 9 | cleaner = None 10 | best_regress = None 11 | best_driver = None 12 | best_const = None 13 | best_engine = None 14 | for i in range(1): 15 | print("i=" + str(i)) 16 | variance_regress = 1.5 17 | for m in range(12): 18 | with open('out/trained_cleaner.pickle', 'rb') as handle: 19 | cleaner = pickle.load(handle) 20 | cleaner.k_variance_multiplier_end = variance_regress 21 | cleaner.k_rookie_variance = 1 22 | cleaner.k_driver_variance_change = 0.18 + 0.02*m 23 | cleaner.k_const_variance_change = 0.12 + 0.02*m 24 | cleaner.k_engine_variance_change = 0.06 + 0.01*m 25 | 26 | cleaner.constructDataset() 27 | overall_variance = (mean(cleaner.driver_variances) + mean(cleaner.const_variances) + mean(cleaner.engine_variances)) 28 | if best_regress is None or best_regress[1] > overall_variance: 29 | best_regress = (cleaner.k_variance_multiplier_end, overall_variance) 30 | if best_driver is None or best_driver[1] > mean(cleaner.driver_variances): 31 | best_driver = (cleaner.k_driver_variance_change, mean(cleaner.driver_variances)) 32 | if best_const is None or best_const[1] > mean(cleaner.const_variances): 33 | best_const = (cleaner.k_const_variance_change, mean(cleaner.const_variances)) 34 | if best_engine is None or best_engine[1] > mean(cleaner.engine_variances): 35 | best_engine = (cleaner.k_engine_variance_change, mean(cleaner.engine_variances)) 36 | 37 | print("Regress variance param= " + str(best_regress[0])) 38 | print("Driver variance param= " + str(best_driver[0])) 39 | print("Constructor variance param= " + str(best_const[0])) 40 | print("Engine variance param= " + str(best_engine[0])) -------------------------------------------------------------------------------- /f1predict/race/raceMonteCarlo.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | # TODO investigate if deviation should be different for drivers, constructors and engines 4 | RANDOM_STANDARD_DEVIATION = 220 5 | 6 | def simulateRace(raceModel, grid, circuit): 7 | # At the beginning, adjust the ranking of each participating driver, constructor or engine randomly up or down 8 | adjustedConstructors = set() 9 | adjustedEngines = set() 10 | for did in grid: 11 | raceModel.drivers[did].rating += random.normalvariate(0, RANDOM_STANDARD_DEVIATION) 12 | 13 | if raceModel.drivers[did].constructor not in adjustedConstructors: 14 | raceModel.drivers[did].constructor.rating += random.normalvariate(0, RANDOM_STANDARD_DEVIATION) 15 | adjustedConstructors.add(raceModel.drivers[did].constructor) 16 | 17 | if raceModel.drivers[did].constructor.engine not in adjustedEngines: 18 | raceModel.drivers[did].constructor.engine.rating += random.normalvariate(0, RANDOM_STANDARD_DEVIATION) 19 | adjustedEngines.add(raceModel.drivers[did].constructor.engine) 20 | 21 | # Adjust track alpha up or down randomly 22 | raceModel.tracks[circuit] *= random.normalvariate(1, 0.15) # TODO investigate what value works best 23 | 24 | gaElos = [] 25 | retiredDrivers = [] 26 | for gridPosition, did in enumerate(grid): 27 | gaElo = raceModel.getGaElo(did, gridPosition, circuit) 28 | 29 | # Simulate "major event", allows participant to have a major gain or loss 30 | randCeck = random.random() 31 | if randCeck < 0.20: 32 | gaElo += random.uniform(-2*RANDOM_STANDARD_DEVIATION, RANDOM_STANDARD_DEVIATION) 33 | 34 | # Simulate retirement 35 | randCeck = random.random() 36 | if randCeck < raceModel.getRetirementProbability(circuit, did): 37 | retiredDrivers.append(did) 38 | else: 39 | gaElos.append((did, gaElo)) 40 | gaElos.sort(key=lambda x: x[1], reverse=True) 41 | return [x[0] for x in gaElos], retiredDrivers -------------------------------------------------------------------------------- /scripts/quali_model_trainer.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import numpy as np 3 | 4 | from f1predict.quali.DataProcessor import DataProcessor 5 | 6 | def gradient(x, err): 7 | grad = -(1.0/len(x)) * err @ x 8 | return grad 9 | 10 | def squaredError(err): 11 | return err**2 12 | 13 | 14 | with open('data/seasonsData.pickle', 'rb') as handle: 15 | seasonsData = pickle.load(handle) 16 | 17 | with open('data/qualiResultsData.pickle', 'rb') as handle: 18 | qualiResultsData = pickle.load(handle) 19 | 20 | with open('data/driversData.pickle', 'rb') as handle: 21 | driversData = pickle.load(handle) 22 | 23 | with open('data/constructorsData.pickle', 'rb') as handle: 24 | constructorsData = pickle.load(handle) 25 | 26 | with open('data/enginesData.pickle', 'rb') as handle: 27 | enginesData = pickle.load(handle) 28 | 29 | processor = DataProcessor(seasonsData, qualiResultsData, driversData, constructorsData, enginesData) 30 | 31 | # Run gradient descent 32 | alpha = 0.18 33 | stop = 0.016 34 | processor.processDataset() 35 | model = processor.getModel() 36 | entries, errors, results = processor.getDataset() 37 | grad = gradient(entries, errors) 38 | 39 | i = 0 40 | while np.linalg.norm(grad) > stop and i < 40: 41 | # Move in the direction of the gradient 42 | # N.B. this is point-wise multiplication, not a dot product 43 | model.theta = model.theta - grad*alpha 44 | mae = np.array([abs(x) for x in errors]).mean() 45 | print(mae) 46 | 47 | processor.processDataset() 48 | entries, errors, results = processor.getDataset() 49 | grad = gradient(entries, errors) 50 | i += 1 51 | 52 | print("Gradient descent finished. MAE="+str(mae)) 53 | model = processor.getModel() 54 | print(model.theta) 55 | 56 | with open('out/driver_variances.pickle', 'wb+') as out: 57 | pickle.dump(model.driver_variances, out, protocol=pickle.HIGHEST_PROTOCOL) 58 | with open('out/const_variances.pickle', 'wb+') as out: 59 | pickle.dump(model.const_variances, out, protocol=pickle.HIGHEST_PROTOCOL) 60 | with open('out/engine_variances.pickle', 'wb+') as out: 61 | pickle.dump(model.engine_variances, out, protocol=pickle.HIGHEST_PROTOCOL) 62 | 63 | # Save model 64 | with open('out/trained_quali_processor.pickle', 'wb+') as out: 65 | pickle.dump(processor, out, protocol=pickle.HIGHEST_PROTOCOL) 66 | 67 | with open('out/trained_quali_model.pickle', 'wb+') as out: 68 | pickle.dump(model, out, protocol=pickle.HIGHEST_PROTOCOL) -------------------------------------------------------------------------------- /f1predict/race/retirementBlame.py: -------------------------------------------------------------------------------- 1 | def getRetirementBlame(status): 2 | if status == "Collision": 3 | return (1, 0, 0) 4 | elif status == "Accident": 5 | return (1, 0, 0) 6 | elif status == "Engine": 7 | return (0, 0, 1) 8 | elif status == "Gearbox": 9 | return (0, 1, 0) 10 | elif status == "Hydraulics": 11 | return (0, 1, 0) 12 | elif status == "Brakes": 13 | return (0, 1, 0) 14 | elif status == "Spun off": 15 | return (1, 0, 0) 16 | elif status == "Suspension": 17 | return (0, 1, 0) 18 | elif status == "Electrical": 19 | return (0, 0.5, 0.5) 20 | elif status == "Power Unit": 21 | return (0, 0, 1) 22 | elif status == "Collision damage": 23 | return (1, 0, 0) 24 | elif status == "Wheel": 25 | return (0, 1, 0) 26 | elif status == "Transmission": 27 | return (0, 1, 0) 28 | elif status == "Mechanical": 29 | return (0, 0.5, 0.5) 30 | elif status == "Puncture": 31 | return (0.667, 0.333, 0) 32 | elif status == "Driveshaft": 33 | return (0, 1, 0) 34 | elif status == "Oil leak": 35 | return (0, 0.5, 0.5) 36 | elif status == "Tyre": 37 | return (0, 1, 0) 38 | elif status == "Fuel pressure": 39 | return (0, 0.5, 0.5) 40 | elif status == "Clutch": 41 | return (0, 1, 0) 42 | elif status == "Electronics": 43 | return (0, 0.5, 0.5) 44 | elif status == "Power loss": 45 | return (0, 0, 1) 46 | elif status == "Overheating": 47 | return (0, 0.5, 0.5) 48 | elif status == "Throttle": 49 | return (0, 1, 0) 50 | elif status == "Wheel nut": 51 | return (0, 1, 0) 52 | elif status == "Exhaust": 53 | return (0, 1, 0) 54 | elif status == "Steering": 55 | return (0, 1, 0) 56 | elif status == "Fuel system": 57 | return (0, 0.5, 0.5) 58 | elif status == "Water leak": 59 | return (0, 1, 0) 60 | elif status == "Battery": 61 | return (0, 0, 1) 62 | elif status == "ERS": 63 | return (0, 0, 1) 64 | elif status == "Water pressure": 65 | return (0, 1, 0) 66 | elif status == "Rear wing": 67 | return (0.667, 0.333, 0) 68 | elif status == "Vibrations": 69 | return (0.667, 0.333, 0) 70 | elif status == "Technical": 71 | return (0, 0.5, 0.5) 72 | elif status == "Oil pressure": 73 | return (0, 0.5, 0.5) 74 | elif status == "Pneumatics": 75 | return (0, 1, 0) 76 | elif status == "Turbo": 77 | return (0, 0, 1) 78 | elif status == "Front wing": 79 | return (0.667, 0.333, 0) 80 | return (0.334, 0.333, 0.333) -------------------------------------------------------------------------------- /f1predict/race/utils.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import json 3 | 4 | from f1predict.common import common 5 | from f1predict.race.EloModel import EloDriver 6 | from f1predict.race.DataProcessor import DataProcessor 7 | 8 | ROOKIE_DRIVER_RATING = 1820 9 | 10 | def loadData(): 11 | with open('data/raceSeasonsData.pickle', 'rb') as handle: 12 | seasonsData = pickle.load(handle) 13 | with open('data/raceResultsData.pickle', 'rb') as handle: 14 | raceResultsData = pickle.load(handle) 15 | with open('data/driversData.pickle', 'rb') as handle: 16 | driversData = pickle.load(handle) 17 | with open('data/constructorsData.pickle', 'rb') as handle: 18 | constructorsData = pickle.load(handle) 19 | with open('data/enginesData.pickle', 'rb') as handle: 20 | enginesData = pickle.load(handle) 21 | return seasonsData, raceResultsData, driversData, constructorsData, enginesData 22 | 23 | 24 | def getDriverDetailsForOutFile(raceModel, driverIDs): 25 | driversToWrite = {} 26 | for did in driverIDs: 27 | driversToWrite[int(did)] = {} 28 | driversToWrite[int(did)]["name"] = raceModel.drivers[int(did)].name 29 | driversToWrite[int(did)]["constructor"] = raceModel.drivers[int( 30 | did)].constructor.name 31 | driversToWrite[int(did)]["color"] = common.getColor( 32 | raceModel.drivers[int(did)].constructor.name) 33 | return driversToWrite 34 | 35 | def generateModel(): 36 | seasonsData, raceResultsData, driversData, constructorsData, enginesData = loadData() 37 | processor = DataProcessor( 38 | seasonsData, raceResultsData, driversData, constructorsData, enginesData) 39 | processor.processDataset() 40 | return processor.getModel() 41 | 42 | def overwriteRaceModelWithNewDrivers(raceModel, filename): 43 | newDrivers = json.load(open(filename))["drivers"] 44 | newDrivers = {int(did): cid for did, cid in newDrivers.items()} 45 | for did, cid in newDrivers.items(): 46 | if did < 0 or did not in raceModel.drivers: # Cases when driver doesn't exist in data 47 | raceModel.drivers[did] = EloDriver( 48 | "__PLACEHOLDER__", raceModel.constructors[int(cid)]) 49 | raceModel.drivers[did].rating = ROOKIE_DRIVER_RATING 50 | if not cid == "": 51 | # Data in newDrivers.json overwrites database 52 | raceModel.drivers[did].constructor = raceModel.constructors[int( 53 | cid)] 54 | 55 | def calculateGaElos(raceModel, grid, circuit): 56 | gaElos = [] 57 | for gridPosition, did in enumerate(grid): 58 | gaElo = raceModel.getGaElo(did, gridPosition, circuit) 59 | gaElos.append((did, gaElo)) 60 | return gaElos -------------------------------------------------------------------------------- /scripts/grid_generate-predictions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import copy 3 | from progress.bar import ChargingBar 4 | 5 | from f1predict.common import file_operations 6 | from f1predict.race.raceMonteCarlo import simulateRace 7 | from f1predict.race import utils as race_utils 8 | 9 | USER_VARS = file_operations.getUserVariables("user_variables.txt") 10 | 11 | 12 | def generatePercentualPredictions(raceModel, grid, circuit): 13 | racePredictions = {did: {} for did in grid} 14 | bar = ChargingBar("Simulating", max=100) 15 | 16 | for i in range(10000): 17 | modelCopy = copy.deepcopy(raceModel) 18 | results, retiredDrivers = simulateRace(modelCopy, grid, circuit) 19 | for pos, did in enumerate(results): 20 | if pos not in racePredictions[did]: 21 | racePredictions[did][pos] = 0 22 | racePredictions[did][pos] += 1 23 | for did in retiredDrivers: 24 | if "ret" not in racePredictions[did]: 25 | racePredictions[did]["ret"] = 0 26 | racePredictions[did]["ret"] += 1 27 | if i % 99 == 0: 28 | bar.next() 29 | 30 | bar.finish() 31 | return racePredictions 32 | 33 | print("STAGE 1: Generating a race model") 34 | raceModel = race_utils.generateModel() 35 | race_utils.overwriteRaceModelWithNewDrivers(raceModel, 'data/newDrivers.json') 36 | 37 | print("STAGE 2: Reading local data") 38 | gridFile = json.load(open('data/grid.json')) 39 | grid = [x["id"] for x in gridFile] 40 | circuit, circuitName, raceId, year = file_operations.readNextRaceDetails( 41 | 'data/futureRaces.json') 42 | 43 | outFile = {} # The object where we write output 44 | outFile["name"] = circuitName 45 | outFile["year"] = year 46 | outFile["drivers"] = race_utils.getDriverDetailsForOutFile(raceModel, grid) 47 | 48 | raceModel.addNewCircuit(circuit) 49 | for did in grid: 50 | raceModel.addNewCircuitToParticipant(did, circuit) 51 | gaElos = race_utils.calculateGaElos(raceModel, grid, circuit) 52 | gaElos.sort(key=lambda x: x[1], reverse=True) 53 | outFile["order"] = [a for (a, b) in gaElos] 54 | 55 | print("STAGE 3: Generating predictions (this may take a while)") 56 | outFile["predictions"] = generatePercentualPredictions( 57 | raceModel, grid, circuit) 58 | 59 | # Publish predictions 60 | print("STAGE 4: Writing predictions to output files") 61 | indexFileName = '{}races/index.json'.format( 62 | USER_VARS['predictions_output_folder']) 63 | file_operations.editIndexFile(indexFileName, year, raceId, circuitName) 64 | 65 | predictionsFileName = '{}races/{}_afterQuali.json'.format( 66 | USER_VARS['predictions_output_folder'], str(raceId)) 67 | file_operations.publishPredictions(predictionsFileName, outFile) 68 | -------------------------------------------------------------------------------- /f1predict/quali/monteCarlo.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import random 3 | import numpy as np 4 | 5 | #driverIDs: key-value mapping of driver id and power score 6 | def predictQualiResults(circuitId, driverIDs, model): 7 | editableDriverIDs = copy.deepcopy(driverIDs) 8 | finalScores = [] 9 | scores = runQualifying(circuitId, editableDriverIDs, model) 10 | #Sort scores: 11 | scoreList = list(scores.items()) 12 | scoreList.sort(key=lambda x: x[1]) 13 | tempList = [] 14 | if len(scoreList) == 20: 15 | tempList = scoreList[15:] 16 | elif len(scoreList) == 22: 17 | tempList = scoreList[16:] 18 | elif len(scoreList) == 24: 19 | tempList = scoreList[17:] 20 | else: 21 | return scoreList 22 | for (did, score) in tempList: 23 | del editableDriverIDs[int(did)] 24 | finalScores = tempList + finalScores 25 | 26 | #Q2 27 | scores = runQualifying(circuitId, editableDriverIDs, model) 28 | #Sort scores: 29 | scoreList = list(scores.items()) 30 | scoreList.sort(key=lambda x: x[1]) 31 | tempList = scoreList[10:] 32 | for (did, score) in tempList: 33 | del editableDriverIDs[int(did)] 34 | finalScores = tempList + finalScores 35 | 36 | #Q3 37 | scores = runQualifying(circuitId, editableDriverIDs, model) 38 | #Sort scores: 39 | scoreList = list(scores.items()) 40 | scoreList.sort(key=lambda x: x[1]) 41 | finalScores = scoreList + finalScores 42 | 43 | return [x[0] for x in finalScores] 44 | 45 | def runQualifying(circuitId, driverIDs, model): 46 | scores = {} 47 | constructorRands = {} 48 | engineRands = {} 49 | for did in driverIDs.keys(): 50 | did = int(did) 51 | 52 | if model.drivers[did].constructor in constructorRands: 53 | constRand = constructorRands[model.drivers[did].constructor] 54 | else: 55 | constRand = random.normalvariate(0, model.drivers[did].constructor.variance) 56 | constructorRands[model.drivers[did].constructor] = constRand 57 | if model.drivers[did].constructor.engine in engineRands: 58 | engineRand = engineRands[model.drivers[did].constructor.engine] 59 | else: 60 | engineRand = random.normalvariate(0, model.drivers[did].constructor.engine.variance) 61 | engineRands[model.drivers[did].constructor.engine] = engineRand 62 | 63 | entry = [ 64 | model.drivers[did].pwr + random.normalvariate(0, model.drivers[did].variance) / 3, 65 | model.drivers[did].constructor.pwr + constRand / 3, 66 | model.drivers[did].constructor.engine.pwr + engineRand / 3, 67 | model.drivers[did].trackpwr[circuitId], 68 | model.drivers[did].constructor.trackpwr[circuitId], 69 | model.drivers[did].constructor.engine.trackpwr[circuitId], 70 | 1 71 | ] 72 | score = np.dot(entry, model.theta) 73 | mistakeOdds = random.random() 74 | if mistakeOdds < 0.031: #Experimentally validated! 75 | score += 4 76 | scores[did] = score 77 | return scores -------------------------------------------------------------------------------- /data/ConstructorEngines.csv: -------------------------------------------------------------------------------- 1 | Year,TeamID,EngineId 2 | 2003,6,1 3 | 2003,3,2 4 | 2003,1,3 5 | 2003,4,4 6 | 2003,15,1 7 | 2003,17,5 8 | 2003,19,6 9 | 2003,16,7 10 | 2003,18,6 11 | 2003,7,8 12 | 2004,6,1 13 | 2004,3,2 14 | 2004,1,3 15 | 2004,4,4 16 | 2004,16,7 17 | 2004,15,1 18 | 2004,19,6 19 | 2004,7,8 20 | 2004,17,5 21 | 2004,18,6 22 | 2005,6,1 23 | 2005,16,7 24 | 2005,4,4 25 | 2005,3,2 26 | 2005,1,3 27 | 2005,15,1 28 | 2005,9,6 29 | 2005,7,8 30 | 2005,17,8 31 | 2005,18,6 32 | 2006,4,4 33 | 2006,1,3 34 | 2006,6,1 35 | 2006,7,8 36 | 2006,3,6 37 | 2006,11,7 38 | 2006,9,1 39 | 2006,2,2 40 | 2006,13,8 41 | 2006,14,8 42 | 2006,5,2 43 | 2006,8,7 44 | 2007,1,3 45 | 2007,4,4 46 | 2007,6,1 47 | 2007,11,7 48 | 2007,2,2 49 | 2007,7,8 50 | 2007,9,4 51 | 2007,3,8 52 | 2007,5,9 53 | 2007,12,9 54 | 2007,8,7 55 | 2008,6,1 56 | 2008,2,2 57 | 2008,4,4 58 | 2008,3,8 59 | 2008,9,4 60 | 2008,7,8 61 | 2008,5,1 62 | 2008,11,7 63 | 2008,8,7 64 | 2008,10,1 65 | 2008,1,3 66 | 2009,1,3 67 | 2009,6,1 68 | 2009,2,2 69 | 2009,4,4 70 | 2009,7,8 71 | 2009,5,1 72 | 2009,9,4 73 | 2009,3,8 74 | 2009,10,3 75 | 2009,23,3 76 | 2010,1,3 77 | 2010,131,3 78 | 2010,9,4 79 | 2010,6,1 80 | 2010,3,6 81 | 2010,4,4 82 | 2010,10,3 83 | 2010,5,1 84 | 2010,205,6 85 | 2010,164,6 86 | 2010,15,1 87 | 2010,166,6 88 | 2011,1,3 89 | 2011,131,3 90 | 2011,9,4 91 | 2011,6,1 92 | 2011,3,6 93 | 2011,4,4 94 | 2011,10,3 95 | 2011,5,1 96 | 2011,205,4 97 | 2011,164,6 98 | 2011,15,1 99 | 2011,166,6 100 | 2012,1,3 101 | 2012,131,3 102 | 2012,9,4 103 | 2012,6,1 104 | 2012,3,4 105 | 2012,208,4 106 | 2012,10,3 107 | 2012,5,1 108 | 2012,207,4 109 | 2012,164,6 110 | 2012,15,1 111 | 2012,206,6 112 | 2013,1,3 113 | 2013,131,3 114 | 2013,9,4 115 | 2013,6,1 116 | 2013,3,4 117 | 2013,208,4 118 | 2013,10,3 119 | 2013,5,1 120 | 2013,207,4 121 | 2013,206,6 122 | 2013,15,1 123 | 2014,1,3 124 | 2014,131,3 125 | 2014,9,4 126 | 2014,6,1 127 | 2014,3,3 128 | 2014,208,4 129 | 2014,10,3 130 | 2014,5,4 131 | 2014,207,4 132 | 2014,206,1 133 | 2014,15,1 134 | 2015,1,7 135 | 2015,131,3 136 | 2015,9,4 137 | 2015,6,1 138 | 2015,3,3 139 | 2015,208,3 140 | 2015,10,3 141 | 2015,5,4 142 | 2015,15,1 143 | 2015,209,1 144 | 2016,1,7 145 | 2016,131,3 146 | 2016,9,4 147 | 2016,6,1 148 | 2016,3,3 149 | 2016,4,4 150 | 2016,10,3 151 | 2016,5,9 152 | 2016,15,1 153 | 2016,209,3 154 | 2016,210,1 155 | 2017,1,7 156 | 2017,131,3 157 | 2017,9,4 158 | 2017,6,1 159 | 2017,3,3 160 | 2017,4,4 161 | 2017,10,3 162 | 2017,5,4 163 | 2017,15,9 164 | 2017,210,1 165 | 2018,1,4 166 | 2018,131,3 167 | 2018,9,4 168 | 2018,6,1 169 | 2018,3,3 170 | 2018,4,4 171 | 2018,10,3 172 | 2018,5,7 173 | 2018,15,1 174 | 2018,210,1 175 | 2019,1,4 176 | 2019,131,3 177 | 2019,9,7 178 | 2019,6,1 179 | 2019,3,3 180 | 2019,4,4 181 | 2019,211,3 182 | 2019,5,7 183 | 2019,51,1 184 | 2019,210,1 185 | 2020,1,4 186 | 2020,131,3 187 | 2020,9,7 188 | 2020,6,1 189 | 2020,3,3 190 | 2020,4,4 191 | 2020,211,3 192 | 2020,213,7 193 | 2020,51,1 194 | 2020,210,1 195 | 2021,1,3 196 | 2021,131,3 197 | 2021,9,7 198 | 2021,6,1 199 | 2021,3,3 200 | 2021,214,4 201 | 2021,117,3 202 | 2021,213,7 203 | 2021,51,1 204 | 2021,210,1 205 | -------------------------------------------------------------------------------- /f1predict/quali/LinearModel.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class LinearModel: 4 | 5 | def __init__(self, k_rookie_pwr, k_rookie_variance): 6 | self.theta = [0, 0, 0, 0, 0, 0, 0] # Weights: Driv, cons, eng, track-specifics and intercept 7 | self.resetVariables() 8 | self.k_rookie_pwr = k_rookie_pwr 9 | self.k_rookie_variance = k_rookie_variance 10 | 11 | def resetVariables(self): 12 | self.drivers = {} # DriverId, Driver 13 | self.constructors = {} # ConstructorId, Constructor 14 | self.engines = {} # EngineId, Engine 15 | self.driver_variances = [] 16 | self.const_variances = [] 17 | self.engine_variances = [] 18 | 19 | def addNewDriver(self, did, name, cid): 20 | self.drivers[did] = Driver(name, self.constructors[cid]) 21 | self.drivers[did].pwr = self.k_rookie_pwr 22 | self.drivers[did].variance = self.k_rookie_variance 23 | 24 | def addNewCircuit(self, driverID, circuitId): 25 | if circuitId not in self.drivers[driverID].trackpwr: 26 | self.drivers[driverID].trackpwr[circuitId] = 0 27 | if circuitId not in self.drivers[driverID].constructor.trackpwr: 28 | self.drivers[driverID].constructor.trackpwr[circuitId] = 0 29 | if circuitId not in self.drivers[driverID].constructor.engine.trackpwr: 30 | self.drivers[driverID].constructor.engine.trackpwr[circuitId] = 0 31 | 32 | class Engine: 33 | """The model for a F1 engine. 34 | param name: Name of the engine manufacturer. 35 | param pwr: The power ranking of the engine that shows what kind of results is expected from drivers whose cars use it. 36 | param trackpwr: A dictionary of circuit ids and power rankings, predicting how well the engine does on that track on average.""" 37 | def __init__(self, name): 38 | self.name = name 39 | self.trackpwr = {} 40 | self.pwr = 0 41 | self.variance = 0.7 42 | 43 | class Constructor: 44 | """The model for a F1 constructor. 45 | param name: Name of the constructor. 46 | param pwr The power ranking of the constructor that shows what kind of results is expected from its drivers. 47 | param trackpwr: A dictionary of circuit ids and power rankings, predicting how well the constructor does on that track on average. 48 | param engine: The engine model object that this constructor currently uses. 49 | """ 50 | def __init__(self, name, engine): 51 | self.name = name 52 | self.trackpwr = {} 53 | self.pwr = 0 54 | self.engine = engine 55 | self.variance = 0.7 56 | 57 | class Driver: 58 | """The model for a F1 driver. 59 | param name: Name of the driver. 60 | param pwr: The power ranking of the driver that shows what kind of results is expected from them. 61 | param trackpwr: A dictionary of circuit ids and power rankings, predicting how well the driver does on that track on average. 62 | param constructor: The constructor model object that this driver currently drives for. 63 | """ 64 | def __init__(self, name, constructor): 65 | self.name = name 66 | self.trackpwr = {} 67 | self.constructor = constructor 68 | #Rookie/new driver: special value? 69 | self.pwr = 0 70 | self.variance = 0.7 71 | def changeConstructor(self, constructor): 72 | self.constructor = constructor 73 | #change power levels? -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # F1Predict 2 | 3 | A project for predicting F1 qualifying results using statistical analysis, machine learning and a Monte Carlo simulation. For visualising the predictions, see [F1PredictWeb](https://github.com/villekuosmanen/F1PredictWeb). 4 | 5 | The project uses the [Ergast F1 API](https://ergast.com/mrd/) for statistical data. 6 | 7 | ## Running the project 8 | 9 | In order to run the prediction generator you first need to set up a MySQL database where a local copy of the F1 data will be stored. You will also need Python 3 and Pip. These instructions should work in Linux and MacOS computers (unfortunately there may be problems with installing certain Python packages in Windows). 10 | 11 | The instructions are as follows: 12 | - Fork and clone F1Predict on GitHub 13 | - Install MySQL on your computer (this may already be installed) 14 | - Set up a local database for your MySQL program. It doesn't need to have any tables. This will be used for storing the F1 data used by the program. 15 | - Set up a username-password account for your F1 database 16 | - (Optional) Clone F1PredictWeb in the same parent folder as you cloned F1Predict. 17 | - Create a file `user-variables.txt` inside F1Predict (see example below). Change the values of each field to match the values in your local database. 18 | - Create a new Python virtual environment and activate it. `python3 -m venv `, and `. venv/bin/activate` 19 | - Run `pip install -r requirements.txt` to install dependencies 20 | - Install the local f1predict package. To do this, first build the local package by running `python3 setup.py sdist bdist_wheel` and then install the built package with `pip install -e .` 21 | - Run `sh dbUpdater.sh`. This adds most recent data to your local database and runs the program, generating predictions to the output folder specified in user-variables.txt 22 | 23 | ### Example `user_variables.txt` file: 24 | db_username=username 25 | db_password=password 26 | db_database=f1db 27 | predictions_output_folder=../F1PredictWeb/src/public/data/ 28 | 29 | The keys (left side of equation) must remain constant, but the values (right-hand side) can be changed to values you prefer. Bu using the example output folder, the predictions are automatically saved to F1ProjectWeb's data folder (if the repositories share the same root folder). 30 | 31 | ## Package structure 32 | 33 | The `f1predict` folder contains the models, and utility and common code for running them. It is structured in classes and runnable functions. This package is designed to be as self-contained as possible, and as such it needs to be locally installed in order to use the scripts and notebooks below. 34 | 35 | The `scripts` folder contains scripts, each corresponding to a particular workflow. The shell script `dbUpdater.sh` runs them to generate qualifying an (pre-quali) race predictions, as well as generating all models for most recent data. The script `runRaceModel.py` generates post-quali predictions for a particular grid. 36 | 37 | The `notebooks` folder contains Jupyter Notebooks, which are mainly used for evaluating the models, and investigating the data. 38 | 39 | ## How to contribute 40 | 41 | If you're interested in contributing to the project, feel free to open an issue about the feature or improvement you would like to add. If an issue for it already exists, comment under it to express your interest. If you don't have a specific issue in mind but would like to contribute, you can open a new issue saying you'd like to contribute 42 | 43 | ## Credits 44 | 45 | ### Qualifying model 46 | - Ville Kuosmanen 47 | 48 | ### Race model 49 | - Ville Kuosmanen 50 | - Raiyan Chowdhury 51 | - Philip Searcy 52 | 53 | ### Special thanks 54 | - [Ergast F1 API](https://ergast.com/mrd/) 55 | -------------------------------------------------------------------------------- /scripts/generate-predictions.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import json 3 | import copy 4 | from progress.bar import ChargingBar 5 | 6 | from f1predict.common import common 7 | from f1predict.common import file_operations 8 | from f1predict.quali import utils as quali_utils 9 | from f1predict.quali.monteCarlo import predictQualiResults 10 | from f1predict.race import utils as race_utils 11 | from f1predict.race.raceMonteCarlo import simulateRace 12 | 13 | USER_VARS = file_operations.getUserVariables("user_variables.txt") 14 | 15 | def generatePercentualPredictions(qualiModel, raceModel, driverIds, circuit): 16 | racePredictions = {did: {} for did in driverIds} 17 | qualiPredictions = {did: {} for did in driverIds} 18 | bar = ChargingBar("Simulating", max=100) 19 | 20 | for i in range(10000): 21 | grid = predictQualiResults(circuit, {did: None for did in driverIds}, qualiModel) 22 | for pos, did in enumerate(grid): 23 | if pos not in qualiPredictions[did]: 24 | qualiPredictions[did][pos] = 0 25 | qualiPredictions[did][pos] += 1 26 | 27 | # Generate predictions for races 28 | modelCopy = copy.deepcopy(raceModel) 29 | results, retiredDrivers = simulateRace(modelCopy, grid, circuit) 30 | for pos, did in enumerate(results): 31 | if pos not in racePredictions[did]: 32 | racePredictions[did][pos] = 0 33 | racePredictions[did][pos] += 1 34 | for did in retiredDrivers: 35 | if "ret" not in racePredictions[did]: 36 | racePredictions[did]["ret"] = 0 37 | racePredictions[did]["ret"] += 1 38 | if i % 99 == 0: 39 | bar.next() 40 | 41 | bar.finish() 42 | return qualiPredictions, racePredictions 43 | 44 | print("STAGE 1: Reading a pre-trained quali model from file") 45 | with open('out/trained_quali_model.pickle', 'rb') as handle: 46 | qualiModel = pickle.load(handle) 47 | quali_utils.overwriteQualiModelWithNewDrivers(qualiModel, 'data/newDrivers.json') 48 | 49 | print("STAGE 2: Generating a race model") 50 | raceModel = race_utils.generateModel() 51 | race_utils.overwriteRaceModelWithNewDrivers(raceModel, 'data/newDrivers.json') 52 | 53 | print("STAGE 3: Reading local data") 54 | newDrivers = json.load(open('data/newDrivers.json'))["drivers"] 55 | driverIDs = [int(did) for did, cid in newDrivers.items()] 56 | circuit, circuitName, raceId, year = file_operations.readNextRaceDetails( 57 | 'data/futureRaces.json') 58 | 59 | outFile = {} # The object where we write quali output 60 | outFile["name"] = circuitName 61 | outFile["year"] = year 62 | outFile["drivers"] = race_utils.getDriverDetailsForOutFile(qualiModel, driverIDs) 63 | raceOutFile = copy.deepcopy(outFile) # The object where we write race output 64 | 65 | raceModel.addNewCircuit(circuit) 66 | for did in driverIDs: 67 | qualiModel.addNewCircuit(did, circuit) 68 | raceModel.addNewCircuitToParticipant(did, circuit) 69 | predictedOrder = quali_utils.calculateOrder(qualiModel, driverIDs, circuit) 70 | outFile["order"] = predictedOrder 71 | 72 | gaElos = race_utils.calculateGaElos(raceModel, predictedOrder, circuit) 73 | gaElos.sort(key=lambda x: x[1], reverse=True) 74 | raceOutFile["order"] = [a for (a, b) in gaElos] 75 | 76 | print("STAGE 4: Generating predictions (this may take a while)") 77 | qualiPredictions, racePredictions = generatePercentualPredictions(qualiModel, raceModel, driverIDs, circuit) 78 | 79 | outFile["predictions"] = qualiPredictions 80 | raceOutFile["predictions"] = racePredictions 81 | 82 | print("STAGE 4: Writing predictions to output files") 83 | # Publish predictions: edit index file 84 | qualiIndexFileName = '{}/index.json'.format( 85 | USER_VARS['predictions_output_folder']) 86 | file_operations.editIndexFile(qualiIndexFileName, year, raceId, circuitName) 87 | 88 | raceIndexFileName = '{}races/index.json'.format( 89 | USER_VARS['predictions_output_folder']) 90 | file_operations.editIndexFile(raceIndexFileName, year, raceId, circuitName) 91 | 92 | # Publish predictions: write out file 93 | qualiPredictionsFileName = '{}/{}.json'.format( 94 | USER_VARS['predictions_output_folder'], str(raceId)) 95 | file_operations.publishPredictions(qualiPredictionsFileName, outFile) 96 | 97 | racePredictionsFileName = '{}races/{}.json'.format( 98 | USER_VARS['predictions_output_folder'], str(raceId)) 99 | file_operations.publishPredictions(racePredictionsFileName, raceOutFile) -------------------------------------------------------------------------------- /scripts/generate-predictions-for-all.py: -------------------------------------------------------------------------------- 1 | # DEPRECATED, DO NOT USE 2 | # This file will be updated in the future 3 | 4 | import operator 5 | import pickle 6 | import json 7 | 8 | from python import * 9 | 10 | # This file generates predictions for all upcoming qualifyings 11 | # It can be useful for checking how the predictions change based on track 12 | 13 | def getColor(constructor): 14 | return { 15 | "Mercedes": "#00D2BE", 16 | "Ferrari": "#C00000", 17 | "Red Bull": "#0600EF", 18 | "Racing Point": "#F596C8", 19 | "Williams": "#0082FA", 20 | "Renault": "#FFF500", 21 | "AlphaTauri": "#C8C8C8", 22 | "Haas F1 Team": "#787878", 23 | "McLaren": "#FF8700", 24 | "Alfa Romeo": "#960000" 25 | }.get(constructor, "#000000") 26 | 27 | #Read user variables: 28 | user_vars = {} 29 | with open("user_variables.txt") as f: 30 | for line in f: 31 | key, value = line.partition("=")[::2] 32 | user_vars[key.rstrip()] = value.rstrip() 33 | 34 | with open('out/trained_cleaner.pickle', 'rb') as handle: 35 | cleaner = pickle.load(handle) 36 | 37 | newDrivers = json.load(open('data/newDrivers.json'))["drivers"] 38 | newDrivers = {int(did): cid for did, cid in newDrivers.items()} 39 | 40 | outFile = {} # The object where we write output 41 | 42 | driversToWrite = {} 43 | for did, cid in newDrivers.items(): 44 | driversToWrite[int(did)] = {} 45 | if int(did) == -1: # Cases when driver doesn't exist in data 46 | driversToWrite[int(did)]["name"] = "__PLACEHOLDER__" 47 | cleaner.addNewDriver(int(did), "__PLACEHOLDER__", cid) 48 | else: 49 | driversToWrite[int(did)]["name"] = cleaner.drivers[int(did)].name 50 | if not cid == "": 51 | cleaner.drivers[int(did)].constructor = cleaner.constructors[int(cid)] # Data in newDrivers.json overwrites database 52 | driversToWrite[int(did)]["constructor"] = cleaner.drivers[int(did)].constructor.name 53 | driversToWrite[int(did)]["color"] = getColor(cleaner.drivers[int(did)].constructor.name) 54 | outFile["drivers"] = driversToWrite 55 | 56 | raceId = -1 57 | with open('data/futureRaces.json', 'r') as handle: 58 | futureRaces = json.load(handle) 59 | 60 | for entry in futureRaces: 61 | circuit = entry["circuitId"] 62 | circuitName = entry["name"] 63 | raceId = entry["raceId"] 64 | 65 | with open('../F1PredictWeb/src/public/data/index.json', 'r+') as handle: 66 | data = json.load(handle) 67 | data[str(entry["year"])][str(raceId)] = circuitName 68 | handle.seek(0) # <--- should reset file position to the beginning. 69 | json.dump(data, handle, indent=4) 70 | handle.truncate() 71 | 72 | outFile["name"] = circuitName 73 | outFile["year"] = entry["year"] 74 | 75 | predictedEntrants = [] 76 | 77 | for did, cid in newDrivers.items(): 78 | if circuit not in cleaner.drivers[did].trackpwr: 79 | cleaner.drivers[did].trackpwr[circuit] = 0 80 | if circuit not in cleaner.drivers[did].constructor.trackpwr: 81 | cleaner.drivers[did].constructor.trackpwr[circuit] = 0 82 | if circuit not in cleaner.drivers[did].constructor.engine.trackpwr: 83 | cleaner.drivers[did].constructor.engine.trackpwr[circuit] = 0 84 | 85 | entry = [ 86 | cleaner.drivers[did].pwr, 87 | cleaner.drivers[did].constructor.pwr, 88 | cleaner.drivers[did].constructor.engine.pwr, 89 | cleaner.drivers[did].trackpwr[circuit], 90 | cleaner.drivers[did].constructor.trackpwr[circuit], 91 | cleaner.drivers[did].constructor.engine.trackpwr[circuit], 92 | 1 93 | ] 94 | predictedEntrants.append(entry) 95 | 96 | linearRegResults = [np.dot(x, cleaner.theta) for x in predictedEntrants] 97 | 98 | driverResults = {} # {did: {position: amount}} 99 | orderedResults = [] # [(did, prediction) ...] 100 | for index, (did, cid) in enumerate(newDrivers.items()): 101 | driverResults[int(did)] = {} 102 | orderedResults.append((did, linearRegResults[index])) 103 | 104 | orderedResults.sort(key = operator.itemgetter(1)) 105 | outFile["order"] = [a for (a, b) in orderedResults] 106 | 107 | for i in range(10000): 108 | scoreList = predictQualiResults(circuit, newDrivers, cleaner) 109 | for i, drivRes in enumerate(scoreList): 110 | if i not in driverResults[drivRes[0]]: 111 | driverResults[drivRes[0]][i] = 0 112 | driverResults[drivRes[0]][i] += 1 113 | 114 | outFile["predictions"] = driverResults 115 | with open(user_vars['predictions_output_folder'] + str(raceId) + '.json', 'w') as fp: 116 | json.dump(outFile, fp) -------------------------------------------------------------------------------- /f1predict/quali/dataclean.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | import pymysql.cursors 3 | import json 4 | 5 | from f1predict.common.Season import Season 6 | from f1predict.common.RaceData import RaceData 7 | 8 | def compareQualiTimes(q1, q2, q3): 9 | """Returns the best time of the three""" 10 | if q3 is None or not q3: 11 | if q2 is None or not q2: 12 | if q1 is None or not q1: 13 | #Didn't take part 14 | return None 15 | else: 16 | return 60.0 + float(q1.split(":")[1]) 17 | else: 18 | #See which is greater 19 | return 60.0 + fasterTime(float((q1.split(":"))[1]), float((q2.split(":"))[1])) 20 | else: 21 | #See which is greater 22 | return 60.0 + fasterTime(float((q1.split(":"))[1]), fasterTime(float((q2.split(":"))[1]), float((q3.split(":"))[1]))) 23 | 24 | def fasterTime(previous, current): 25 | """Returns the faster time, or either of them if they are equal""" 26 | if previous is None: 27 | return current 28 | elif current is None: 29 | return previous 30 | 31 | if previous < current: 32 | return previous 33 | else: 34 | return current 35 | 36 | def slowerTime(previous, current): 37 | """Returns the slower time, or either of them if they are equal""" 38 | if previous is None: 39 | return current 40 | elif current is None: 41 | return previous 42 | 43 | if previous < current: 44 | return current 45 | else: 46 | return previous 47 | 48 | def addSeason(cursor, seasonsData, qualiResultsData, qualiChanges, year): 49 | """Adds a Season Data object to the given map of seasons""" 50 | s = Season() 51 | q3no = 0 52 | q2no = 0 53 | futureRaces = None # Used when a race has no data yet 54 | 55 | sql = "SELECT `raceId`, `round`, `circuitId`, `name` FROM `races` WHERE `year`=%s" 56 | cursor.execute(sql, year) 57 | result = cursor.fetchall() 58 | no_mistakes = 0 59 | 60 | for x in result: 61 | circuitId = x.get('circuitId') 62 | roundNo = x.get('round') 63 | 64 | #Inefficient to loop through every time, but doesn't matter here 65 | for index, row in qualiChanges.iterrows(): 66 | if int(row["Year"]) == year and int(row["Race"]) == roundNo: 67 | print("quali change") 68 | q3no = row["Q3"] 69 | q2no = row["Q2"] 70 | break 71 | 72 | raceData = RaceData(circuitId, roundNo) 73 | s.addRace(x.get('raceId'), raceData) 74 | res, mistakes = addQualiResults(cursor, qualiResultsData, q3no, q2no, x.get('raceId')) 75 | no_mistakes += mistakes 76 | if not res: 77 | # Fail: there were no quali results! Therefore add race to future races object 78 | if futureRaces is None: 79 | futureRaces = [] 80 | futureRaces.append({ 81 | "raceId": x.get('raceId'), 82 | "name": x.get('name'), 83 | "circuitId": x.get('circuitId'), 84 | "year": year 85 | }) 86 | if futureRaces is not None: 87 | with open('data/futureRaces.json', 'w') as fp: 88 | json.dump(futureRaces, fp) 89 | seasonsData[year] = s 90 | return no_mistakes 91 | 92 | def addQualiResults(cursor, qualiResultsData, q3no, q2no, raceId): 93 | """Adds quali results from a race. Each result has a separate object for each driver's performance 94 | 95 | Returns true if quali results were found, and false otherwise""" 96 | qs = [] 97 | mistakes = 0 98 | 99 | sql = "SELECT `driverId`, `constructorId`, `q1`, `q2`, `q3`, `position` FROM `qualifying` WHERE `raceId`=%s" 100 | cursor.execute(sql, raceId) 101 | result = cursor.fetchall() 102 | if result: 103 | result.sort(key=lambda result: result['position']) 104 | fastestTimeOfAll = None 105 | for index, x in enumerate(result): 106 | #Use q1, q2 and q3 to identify best time 107 | q1 = x.get('q1') 108 | q2 = x.get('q2') 109 | q3 = x.get('q3') 110 | bestTime = compareQualiTimes(q1, q2, q3) 111 | if (index < q3no and (q3 is None or not q3)) or (index < q2no and (q2 is None or not q2)) or not q1: 112 | #Driver didn't participate to a qualifying they got in. Can take several actions but now just ignore them 113 | print("Race " + str(raceId) + ", place " + str(index + 1) + " failed to set a time") 114 | mistakes += 1 115 | continue 116 | 117 | if (fastestTimeOfAll == None): 118 | fastestTimeOfAll = bestTime 119 | if bestTime is not None: 120 | #If is 'None', don't add to results at all! 121 | #lastBestTime = slowerTime(lastBestTime, bestTime) 122 | if bestTime < 1.07*fastestTimeOfAll: #Using a 107% rule 123 | qs.append( (x.get('driverId'), x.get('constructorId'), bestTime) ) #A tuple 124 | else: 125 | mistakes += 1 126 | else: 127 | mistakes += 1 128 | qualiResultsData[raceId] = qs 129 | return True, mistakes 130 | return False, mistakes -------------------------------------------------------------------------------- /f1predict/race/EloModel.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | GRID_ADJUSTMENT_COEFFICIENT = 22 4 | GA_ELO_INTERCEPT_COEFFICIENT = 0 5 | K_FACTOR = 4 6 | K_FACTOR_TRACK = 8 7 | BASE_RETIREMENT_PROBABILITY = 0.1 8 | 9 | FIRST_TRACK_RATING = 1820 10 | 11 | DRIVER_WEIGHTING = 0.12 12 | CONSTRUCTOR_WEIGHTING = 0.52 13 | ENGINE_WEIGHTING = 0.36 14 | TRACK_WEIGH_SHARE = 0.00 # NOTE: Does not improve performance, so it's turned off for now 15 | 16 | 17 | class EloDriver: 18 | def __init__(self, name, constructor): 19 | self.name = name 20 | self.constructor = constructor 21 | self.rating = 2200 # Default rating 22 | self.trackRatings = {} 23 | self.retirementProbability = BASE_RETIREMENT_PROBABILITY 24 | 25 | def changeConstructor(self, constructor): 26 | self.constructor = constructor 27 | 28 | 29 | class EloConstructor: 30 | def __init__(self, name, engine): 31 | self.name = name 32 | self.engine = engine 33 | self.rating = 2200 # Default rating 34 | self.trackRatings = {} 35 | self.retirementProbability = BASE_RETIREMENT_PROBABILITY 36 | 37 | 38 | class EloEngine: 39 | def __init__(self, name): 40 | self.name = name 41 | self.rating = 2200 # Default rating 42 | self.trackRatings = {} 43 | self.retirementProbability = BASE_RETIREMENT_PROBABILITY 44 | 45 | 46 | class EloModel: 47 | def __init__(self, drivers, constructors, engines, tracks): 48 | self.drivers = drivers 49 | self.constructors = constructors 50 | self.engines = engines 51 | self.tracks = tracks 52 | self.tracksRetirementFactor = {} 53 | self.overallRetirementProbability = BASE_RETIREMENT_PROBABILITY 54 | 55 | def getGaElo(self, driverId, gridPosition, trackId): 56 | gridAdjustment = self.tracks[trackId] * \ 57 | self.getGridAdjustment(gridPosition) 58 | 59 | return (self.getDriverRating(driverId, trackId) * DRIVER_WEIGHTING) + \ 60 | (self.getConstructorRating(driverId, trackId) * CONSTRUCTOR_WEIGHTING) + \ 61 | (self.getEngineRating(driverId, trackId) * ENGINE_WEIGHTING) + \ 62 | gridAdjustment + GA_ELO_INTERCEPT_COEFFICIENT 63 | 64 | def getGaEloWithTrackAlpha(self, driverId, gridPosition, trackId, alphaAdjustment): 65 | gridAdjustment = ( 66 | self.tracks[trackId] * alphaAdjustment) * self.getGridAdjustment(gridPosition) 67 | 68 | return (self.getDriverRating(driverId, trackId) * DRIVER_WEIGHTING) + \ 69 | (self.getConstructorRating(driverId, trackId) * CONSTRUCTOR_WEIGHTING) + \ 70 | (self.getEngineRating(driverId, trackId) * ENGINE_WEIGHTING) + \ 71 | gridAdjustment + GA_ELO_INTERCEPT_COEFFICIENT 72 | 73 | def getDriverRating(self, driverId, trackId): 74 | return self.drivers[driverId].rating * (1-TRACK_WEIGH_SHARE) + \ 75 | self.drivers[driverId].trackRatings[trackId] * TRACK_WEIGH_SHARE 76 | 77 | def getConstructorRating(self, driverId, trackId): 78 | return self.drivers[driverId].constructor.rating * (1-TRACK_WEIGH_SHARE) + \ 79 | self.drivers[driverId].constructor.trackRatings[trackId] * TRACK_WEIGH_SHARE 80 | 81 | def getEngineRating(self, driverId, trackId): 82 | return self.drivers[driverId].constructor.engine.rating * (1-TRACK_WEIGH_SHARE) + \ 83 | self.drivers[driverId].constructor.engine.trackRatings[trackId] * TRACK_WEIGH_SHARE 84 | 85 | def getRetirementProbability(self, trackId, driverID): 86 | return (self.overallRetirementProbability + 87 | self.tracksRetirementFactor[trackId] + 88 | self.drivers[driverID].retirementProbability + 89 | self.drivers[driverID].constructor.retirementProbability + 90 | self.drivers[driverID].constructor.engine.retirementProbability) / 5 91 | 92 | def getGridAdjustment(self, gridPosition): 93 | return (9.5 - gridPosition) * GRID_ADJUSTMENT_COEFFICIENT 94 | 95 | def getExpectedScore(self, a, b): 96 | '''Returns a's expected score against b. A float value between 0 and 1''' 97 | # This is the mathematical formula for calculating expected score in Elo ranking 98 | # See Wikipedia article for more details 99 | return 1 / (1 + 10 ** ((b - a) / 400)) 100 | 101 | def adjustEloRating(self, driverId, adjustment, trackId): 102 | self.drivers[driverId].rating += (adjustment * K_FACTOR) 103 | self.drivers[driverId].trackRatings[trackId] += (adjustment * K_FACTOR_TRACK) 104 | 105 | def adjustEloRatingConstructor(self, constructor, adjustment, trackId): 106 | constructor.rating += (adjustment * K_FACTOR) 107 | constructor.trackRatings[trackId] += (adjustment * K_FACTOR_TRACK) 108 | 109 | def adjustEloRatingEngine(self, engine, adjustment, trackId): 110 | engine.rating += (adjustment * K_FACTOR) 111 | engine.trackRatings[trackId] += (adjustment * K_FACTOR_TRACK) 112 | 113 | def adjustCircuitAplha(self, alphaAdjustment, trackId): 114 | self.tracks[trackId] *= alphaAdjustment 115 | 116 | def addNewCircuit(self, circuitId): 117 | if circuitId not in self.tracks: 118 | self.tracks[circuitId] = 1 119 | if circuitId not in self.tracksRetirementFactor: 120 | self.tracksRetirementFactor[circuitId] = BASE_RETIREMENT_PROBABILITY 121 | 122 | def addNewCircuitToParticipant(self, driverId, trackId): 123 | if trackId not in self.drivers[driverId].trackRatings: 124 | # TODO maybe change defaults 125 | self.drivers[driverId].trackRatings[trackId] = FIRST_TRACK_RATING 126 | if trackId not in self.drivers[driverId].constructor.trackRatings: 127 | # TODO maybe change defaults 128 | self.drivers[driverId].constructor.trackRatings[trackId] = FIRST_TRACK_RATING 129 | if trackId not in self.drivers[driverId].constructor.engine.trackRatings: 130 | # TODO maybe change defaults 131 | self.drivers[driverId].constructor.engine.trackRatings[trackId] = FIRST_TRACK_RATING -------------------------------------------------------------------------------- /notebooks/RaceDataVisualisation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# A Notebook for investigating F1 race data" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": {}, 14 | "outputs": [], 15 | "source": [ 16 | "import pymysql\n", 17 | "import pymysql.cursors\n", 18 | "import pandas as pd\n", 19 | "import numpy as np\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import statistics\n", 22 | "\n", 23 | "from f1predict.common import file_operations\n", 24 | "from f1predict.common import common" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "USER_VARS = file_operations.getUserVariables(\"../user_variables.txt\")\n", 34 | "\n", 35 | "#Set up a database connection:\n", 36 | "connection = pymysql.connect(host='localhost',\n", 37 | " user=USER_VARS['db_username'],\n", 38 | " password=USER_VARS['db_password'],\n", 39 | " db=USER_VARS['db_database'],\n", 40 | " charset='utf8',\n", 41 | " cursorclass=pymysql.cursors.DictCursor)" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 3, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "raceResultsByGridPosition = {}\n", 51 | "\n", 52 | "with connection.cursor() as cursor:\n", 53 | " for year in range(2003, common.getCurrentYear() + 1):\n", 54 | " sql = \"SELECT `raceId`, `round`, `circuitId`, `name` FROM `races` WHERE `year`=%s\"\n", 55 | " cursor.execute(sql, year)\n", 56 | " result = cursor.fetchall()\n", 57 | "\n", 58 | " for x in result:\n", 59 | " circuitId = x.get('circuitId') \n", 60 | " sql = \"SELECT `driverId`, `constructorId`, `position`, `grid` FROM `results` WHERE `raceId`=%s\"\n", 61 | " cursor.execute(sql, x.get('raceId'))\n", 62 | " result = cursor.fetchall()\n", 63 | "\n", 64 | " if result:\n", 65 | " result.sort(key=lambda result: (result['position'] is None, result['position']))\n", 66 | " for row in result:\n", 67 | " if row['position']:\n", 68 | " if row['grid'] not in raceResultsByGridPosition and row['grid'] > 0:\n", 69 | " raceResultsByGridPosition[row['grid']] = []\n", 70 | " if row['grid'] > 0:\n", 71 | " raceResultsByGridPosition[row['grid']].append(row['position'])" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 22, 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "name": "stdout", 81 | "output_type": "stream", 82 | "text": [ 83 | "Finished: 3245\n", 84 | "Collision: 285\n", 85 | "Accident: 220\n", 86 | "Engine: 174\n", 87 | "Gearbox: 101\n", 88 | "Hydraulics: 82\n", 89 | "Brakes: 74\n", 90 | "Spun off: 65\n", 91 | "Suspension: 64\n", 92 | "Retired: 61\n", 93 | "Electrical: 34\n", 94 | "Power Unit: 33\n", 95 | "Collision damage: 27\n", 96 | "Disqualified: 25\n", 97 | "Withdrew: 25\n", 98 | "Wheel: 24\n", 99 | "Transmission: 22\n", 100 | "Mechanical: 19\n", 101 | "Puncture: 16\n", 102 | "Driveshaft: 16\n", 103 | "Oil leak: 15\n", 104 | "Tyre: 14\n", 105 | "Fuel pressure: 14\n", 106 | "Clutch: 13\n", 107 | "Electronics: 10\n", 108 | "Power loss: 10\n", 109 | "Overheating: 9\n", 110 | "Throttle: 8\n", 111 | "Wheel nut: 8\n", 112 | "Exhaust: 8\n", 113 | "Steering: 7\n", 114 | "Fuel system: 6\n", 115 | "Water leak: 6\n", 116 | "Battery: 5\n", 117 | "Out of fuel: 5\n", 118 | "ERS: 5\n", 119 | "Water pressure: 5\n", 120 | "Rear wing: 5\n", 121 | "Did not qualify: 4\n", 122 | "Vibrations: 4\n", 123 | "Technical: 4\n", 124 | "Oil pressure: 4\n", 125 | "Pneumatics: 4\n", 126 | "Turbo: 4\n", 127 | "Front wing: 4\n", 128 | "Alternator: 3\n", 129 | "Radiator: 3\n", 130 | "Fuel pump: 2\n", 131 | "Track rod: 2\n", 132 | "Injured: 2\n", 133 | "Heat shield fire: 2\n", 134 | "Wheel rim: 2\n", 135 | "Excluded: 1\n", 136 | "Oil line: 1\n", 137 | "Tyre puncture: 1\n", 138 | "Fuel rig: 1\n", 139 | "Driver Seat: 1\n", 140 | "Seat: 1\n", 141 | "Launch control: 1\n", 142 | "Injury: 1\n", 143 | "Not classified: 1\n", 144 | "Spark plugs: 1\n", 145 | "Broken wing: 1\n", 146 | "Fuel: 1\n", 147 | "Damage: 1\n", 148 | "Differential: 1\n", 149 | "Debris: 1\n", 150 | "Handling: 1\n", 151 | "Drivetrain: 1\n", 152 | "Refuelling: 1\n", 153 | "Fire: 1\n", 154 | "Engine misfire: 1\n", 155 | "Engine fire: 1\n", 156 | "Brake duct: 1\n" 157 | ] 158 | } 159 | ], 160 | "source": [ 161 | "with connection.cursor() as cursor:\n", 162 | " sql = \"SELECT `status`.status, COUNT(*) AS `count` FROM `status`, `results`, `races` \\\n", 163 | " WHERE `status`.statusId = `results`.statusId AND `results`.raceId = `races`.raceId AND `races`.year >= 2003 \\\n", 164 | " GROUP BY `results`.statusId \\\n", 165 | " ORDER BY COUNT(*) DESC\"\n", 166 | " cursor.execute(sql)\n", 167 | " result = cursor.fetchall()\n", 168 | " for x in result:\n", 169 | " if \" Lap\" not in x.get('status'):\n", 170 | " print(\"{}: {}\".format(x.get('status'), x.get('count')))" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [] 179 | } 180 | ], 181 | "metadata": { 182 | "kernelspec": { 183 | "display_name": "Python 3", 184 | "language": "python", 185 | "name": "python3" 186 | }, 187 | "language_info": { 188 | "codemirror_mode": { 189 | "name": "ipython", 190 | "version": 3 191 | }, 192 | "file_extension": ".py", 193 | "mimetype": "text/x-python", 194 | "name": "python", 195 | "nbconvert_exporter": "python", 196 | "pygments_lexer": "ipython3", 197 | "version": "3.7.9" 198 | } 199 | }, 200 | "nbformat": 4, 201 | "nbformat_minor": 2 202 | } -------------------------------------------------------------------------------- /notebooks/QualiInsights.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import statistics\n", 10 | "import pickle\n", 11 | "import numpy as np\n", 12 | "import json\n", 13 | "import matplotlib.pyplot as plt\n", 14 | "%matplotlib inline\n", 15 | "\n", 16 | "from f1predict.quali.utils import overwriteQualiModelWithNewDrivers" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "with open('../out/trained_quali_model.pickle', 'rb') as handle:\n", 26 | " linearModel = pickle.load(handle)\n", 27 | " \n", 28 | "overwriteQualiModelWithNewDrivers(linearModel, '../data/newDrivers.json')" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 3, 34 | "metadata": {}, 35 | "outputs": [ 36 | { 37 | "name": "stdout", 38 | "output_type": "stream", 39 | "text": [ 40 | "Driver insights:\n", 41 | "Max Verstappen: -0.5229378469654977 (variance: 0.5346209590694327)\n", 42 | "Lando Norris: -0.3922902945136218 (variance: 0.6433739531203528)\n", 43 | "Charles Leclerc: -0.30802046420716733 (variance: 0.6268085266234709)\n", 44 | "Lewis Hamilton: -0.26851062915572005 (variance: 0.4630789863191164)\n", 45 | "Pierre Gasly: -0.24935325450741583 (variance: 0.6485573274943948)\n", 46 | "Carlos Sainz: -0.09281461568629032 (variance: 0.5337112225617733)\n", 47 | "Valtteri Bottas: -0.08518400357509061 (variance: 0.3701291346945996)\n", 48 | "George Russell: -0.04631248034875108 (variance: 0.7275743677674338)\n", 49 | "Antonio Giovinazzi: -0.04005367035985888 (variance: 0.46914218735707386)\n", 50 | "Sergio Pérez: -0.004422908699917095 (variance: 0.43983275094884766)\n", 51 | "Daniel Ricciardo: 0.09938973089436028 (variance: 0.6195699694881222)\n", 52 | "Fernando Alonso: 0.13533024910101238 (variance: 0.6087463789579572)\n", 53 | "Esteban Ocon: 0.14875338147944367 (variance: 0.6230396947592074)\n", 54 | "Lance Stroll: 0.20812424269395421 (variance: 0.5119923974375318)\n", 55 | "Sebastian Vettel: 0.32475886933214204 (variance: 0.5208037256870683)\n", 56 | "Kimi Räikkönen: 0.5488251477081849 (variance: 0.7121360185127983)\n", 57 | "Yuki Tsunoda: 0.6280794781030522 (variance: 1.2156215193618713)\n", 58 | "Mick Schumacher: 0.6906843613894031 (variance: 0.7054320805903171)\n", 59 | "Nicholas Latifi: 1.0718315188336773 (variance: 0.4521587802639808)\n", 60 | "Nikita Mazepin: 1.3665399868421189 (variance: 0.6244785737712946)\n" 61 | ] 62 | } 63 | ], 64 | "source": [ 65 | "newDrivers = json.load(open('../data/newDrivers.json'))[\"drivers\"]\n", 66 | "driverIDs = [int(did) for did, cid in newDrivers.items()]\n", 67 | "\n", 68 | "print(\"Driver insights:\")\n", 69 | "driversByScore = []\n", 70 | "for did in driverIDs:\n", 71 | " driversByScore.append((linearModel.drivers[did].name, linearModel.drivers[did].pwr, linearModel.drivers[did].variance))\n", 72 | "\n", 73 | "driversByScore.sort(key=lambda tup: tup[1])\n", 74 | "for name, score, variance in driversByScore:\n", 75 | " print(\"{}: {} (variance: {})\".format(name, score, variance))" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": 4, 81 | "metadata": {}, 82 | "outputs": [ 83 | { 84 | "name": "stdout", 85 | "output_type": "stream", 86 | "text": [ 87 | "Constructor insights:\n", 88 | "Mercedes: -0.5187421431248395 (variance: 0.4289483126807082)\n", 89 | "Red Bull: -0.508503559351721 (variance: 0.34494439039665925)\n", 90 | "Ferrari: -0.20617829021044995 (variance: 0.6265723887021709)\n", 91 | "AlphaTauri: -0.1707580573154822 (variance: 0.655642611998981)\n", 92 | "McLaren: -0.12117996092776 (variance: 0.32974827281431046)\n", 93 | "Aston Martin: 0.016902985509166833 (variance: 0.39160225416887906)\n", 94 | "Alpine F1 Team: 0.06938877800454854 (variance: 0.4155856924325455)\n", 95 | "Williams: 0.2858172071631062 (variance: 0.43269593082485647)\n", 96 | "Alfa Romeo: 0.29953177154107863 (variance: 0.5612453371305237)\n", 97 | "Haas F1 Team: 0.8955769636351937 (variance: 0.546164636698933)\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "print(\"Constructor insights:\")\n", 103 | "constructorsByScore = []\n", 104 | "constructors = set()\n", 105 | "for did in driverIDs:\n", 106 | " if linearModel.drivers[did].constructor.name not in constructors:\n", 107 | " constructorsByScore.append((linearModel.drivers[did].constructor.name, linearModel.drivers[did].constructor.pwr, linearModel.drivers[did].constructor.variance))\n", 108 | " constructors.add(linearModel.drivers[did].constructor.name)\n", 109 | "\n", 110 | "constructorsByScore.sort(key=lambda tup: tup[1])\n", 111 | "for name, score, variance in constructorsByScore:\n", 112 | " print(\"{}: {} (variance: {})\".format(name, score, variance))" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 5, 118 | "metadata": {}, 119 | "outputs": [ 120 | { 121 | "name": "stdout", 122 | "output_type": "stream", 123 | "text": [ 124 | "Engine insights:\n", 125 | "Mercedes: -0.22902804990666972 (variance: 0.4219459798007611)\n", 126 | "Renault: 0.02241112439939676 (variance: 0.4106290279103022)\n", 127 | "Ferrari: 0.2720449185672815 (variance: 0.6454561235694194)\n", 128 | "Honda: 0.376832615917477 (variance: 0.5782604858739203)\n" 129 | ] 130 | } 131 | ], 132 | "source": [ 133 | "print(\"Engine insights:\")\n", 134 | "enginesByScore = []\n", 135 | "engines = set()\n", 136 | "for did in driverIDs:\n", 137 | " if linearModel.drivers[did].constructor.engine.name not in engines:\n", 138 | " enginesByScore.append((linearModel.drivers[did].constructor.engine.name, linearModel.drivers[did].constructor.engine.pwr, linearModel.drivers[did].constructor.engine.variance))\n", 139 | " engines.add(linearModel.drivers[did].constructor.engine.name)\n", 140 | "\n", 141 | "enginesByScore.sort(key=lambda tup: tup[1])\n", 142 | "for name, score, variance in enginesByScore:\n", 143 | " print(\"{}: {} (variance: {})\".format(name, score, variance))" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [] 152 | } 153 | ], 154 | "metadata": { 155 | "kernelspec": { 156 | "display_name": "Python 3", 157 | "language": "python", 158 | "name": "python3" 159 | }, 160 | "language_info": { 161 | "codemirror_mode": { 162 | "name": "ipython", 163 | "version": 3 164 | }, 165 | "file_extension": ".py", 166 | "mimetype": "text/x-python", 167 | "name": "python", 168 | "nbconvert_exporter": "python", 169 | "pygments_lexer": "ipython3", 170 | "version": "3.7.9" 171 | } 172 | }, 173 | "nbformat": 4, 174 | "nbformat_minor": 2 175 | } 176 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /f1predict/quali/DataProcessor.py: -------------------------------------------------------------------------------- 1 | import random 2 | import pandas as pd 3 | import numpy as np 4 | from statistics import mean 5 | 6 | from f1predict.quali.LinearModel import LinearModel, Engine, Constructor, Driver 7 | 8 | 9 | class DataProcessor: 10 | 11 | def __init__(self, seasonsData, qualiResultsData, driversData, constructorsData, enginesData): 12 | self.seasonsData = seasonsData 13 | self.qualiResultsData = qualiResultsData 14 | self.driversData = driversData 15 | self.constructorsData = constructorsData 16 | self.enginesData = enginesData 17 | 18 | self._initialiseConstants() 19 | 20 | self.model = LinearModel( 21 | self.k_rookie_pwr, self.k_rookie_variance) 22 | self.predictions = None 23 | self.entries = None 24 | self.errors = None 25 | self.results = None 26 | 27 | def _initialiseConstants(self): 28 | # Various hyperparameters 29 | self.k_driver_change = 0.33 30 | self.k_const_change = 0.33 31 | self.k_engine_change = 0.20 32 | self.k_track_change_multiplier = 1 33 | self.k_rookie_pwr = 0.70 34 | 35 | self.k_rookie_variance = 1 36 | self.k_driver_variance_change = 0.20 37 | self.k_const_variance_change = 0.15 38 | self.k_engine_variance_change = 0.055 39 | self.k_variance_multiplier_end = 1.5 40 | self.k_driver_const_change_variance_multiplier = 1.2 41 | self.k_const_name_change_variace_multiplier = 1.5 42 | 43 | self.k_eng_regress = 0.9 44 | self.k_const_regress = 0.9 45 | self.k_driver_regress = 0.74 46 | 47 | # Processes the qualifying data the class was initialised with 48 | # After completion, the class contains the processed model, list of predictions, and list of entries, errors and results 49 | def processDataset(self): 50 | self.model.resetVariables() 51 | self.predictions = [] 52 | self.entries = [] 53 | self.errors = [] 54 | self.results = [] 55 | 56 | # Deviation variables 57 | globaldev = [None] * 20 58 | trackdev = {} 59 | 60 | for year, season in self.seasonsData.items(): 61 | self._updateModelsForYear(season) 62 | racesAsList = list(season.races.items()) 63 | racesAsList.sort(key=lambda x: x[1].round) 64 | 65 | for raceId, data in racesAsList: 66 | if raceId in self.qualiResultsData: 67 | qresults = self.qualiResultsData[raceId] 68 | self._addNewDriversAndConstructors(qresults, year) 69 | pwr_changes_driver = {} 70 | pwr_changes_constructor = {} 71 | pwr_changes_engine = {} 72 | 73 | # Scores and qresults need to be in the same, sequential order by score 74 | qresults.sort(key=lambda x: x[2]) 75 | scores = calculateScoresFromResults( 76 | qresults, data.circuitId, globaldev, trackdev) 77 | 78 | prediction = [] 79 | for index, (driverId, constId, time) in enumerate(qresults): 80 | self.model.addNewCircuit(driverId, data.circuitId) 81 | entry = self._buildEntry(driverId, data.circuitId) 82 | self.entries.append(entry) 83 | self.results.append(scores[index]) 84 | 85 | # Calculate predicted result 86 | y_hat = np.dot(entry, self.model.theta) 87 | prediction.append((driverId, y_hat)) 88 | 89 | # Calculate prediction error 90 | err = scores[index] - y_hat 91 | self.errors.append(err) 92 | 93 | # Calculate model adjustments 94 | pwr_changes_driver[driverId] = err 95 | if constId not in pwr_changes_constructor: 96 | pwr_changes_constructor[constId] = [] 97 | pwr_changes_constructor[constId].append(err) 98 | if self.model.constructors[constId].engine not in pwr_changes_engine: 99 | pwr_changes_engine[self.model.constructors[constId].engine] = [ 100 | ] 101 | pwr_changes_engine[self.model.constructors[constId].engine].append( 102 | err) 103 | 104 | random.shuffle(prediction) 105 | prediction.sort(key=lambda x: x[1]) 106 | self.predictions.append([x[0] for x in prediction]) 107 | # Adjust models 108 | self._updateModels( 109 | pwr_changes_driver, pwr_changes_constructor, pwr_changes_engine, data.circuitId) 110 | self._updateModelsAtEndOfYear(season) 111 | 112 | # Returns the generated LinearQualiModel from the last processing, or an empty model if the function was not called yet 113 | def getModel(self): 114 | return self.model 115 | 116 | # Returns a list of all generated predictions from the last processing 117 | # Throws an exception if called before processing a dataset 118 | def getPredictions(self): 119 | if self.predictions == None: 120 | raise AssertionError( 121 | "Predictions not generated yet! Call before calling me.") 122 | return self.predictions 123 | 124 | # Returns the entries, errors and results from the last processing 125 | # Throws an exception if called before processing a dataset 126 | def getDataset(self): 127 | if self.entries == None: 128 | raise AssertionError( 129 | "Dataset not generated yet! Call before calling me.") 130 | return np.array(self.entries), np.array(self.errors), np.array(self.results) 131 | 132 | def _buildEntry(self, driverId, circuitId): 133 | entry = [ 134 | self.model.drivers[driverId].pwr, 135 | self.model.drivers[driverId].constructor.pwr, 136 | self.model.drivers[driverId].constructor.engine.pwr, 137 | self.model.drivers[driverId].trackpwr[circuitId], 138 | self.model.drivers[driverId].constructor.trackpwr[circuitId], 139 | self.model.drivers[driverId].constructor.engine.trackpwr[circuitId], 140 | 1 # Intercept 141 | ] 142 | return entry 143 | 144 | def _updateModelsAtEndOfYear(self, season): 145 | # Delete old, unused constructors 146 | for new, old in season.teamChanges.items(): 147 | del self.model.constructors[old] 148 | 149 | # Regress all powers towards the mean 150 | for (engid, eng) in self.model.engines.items(): 151 | eng.pwr *= self.k_eng_regress 152 | eng.variance *= self.k_variance_multiplier_end 153 | for (constid, const) in self.model.constructors.items(): 154 | const.pwr *= self.k_const_regress 155 | const.variance *= self.k_variance_multiplier_end 156 | for (drivId, driver) in self.model.drivers.items(): 157 | driver.pwr *= self.k_driver_regress 158 | driver.variance *= self.k_variance_multiplier_end 159 | 160 | def _updateModelsForYear(self, season): 161 | '''Resolves team name changes''' 162 | # Updating list of engines and constructors: 163 | for new, old in season.teamChanges.items(): 164 | self.model.constructors[new] = self.model.constructors[old] 165 | self.model.constructors[new].name = self.constructorsData[new] 166 | self.model.constructors[new].variance *= self.k_const_name_change_variace_multiplier 167 | 168 | for cId, engineId in season.constructorEngines.items(): 169 | # Check that the constructor and engine exist 170 | if engineId not in self.model.engines: 171 | self.model.engines[engineId] = Engine( 172 | self.enginesData[engineId]) 173 | if cId not in self.model.constructors: 174 | self.model.constructors[cId] = Constructor( 175 | self.constructorsData[cId], None) 176 | # Assign it its engine 177 | self.model.constructors[cId].engine = self.model.engines[engineId] 178 | 179 | def _addNewDriversAndConstructors(self, qresults, year): 180 | for res in qresults: 181 | if res[0] not in self.model.drivers: 182 | self.model.drivers[res[0]] = Driver( 183 | self.driversData[res[0]], res[1]) 184 | if year > 2003: 185 | self.model.drivers[res[0]].pwr = self.k_rookie_pwr 186 | self.model.drivers[res[0] 187 | ].variance = self.k_rookie_variance 188 | if self.model.drivers[res[0]].constructor is not self.model.constructors[res[1]]: 189 | self.model.drivers[res[0] 190 | ].constructor = self.model.constructors[res[1]] 191 | self.model.drivers[res[0] 192 | ].variance *= self.k_driver_const_change_variance_multiplier 193 | 194 | def _updateModels(self, pwr_changes_driver, pwr_changes_constructor, pwr_changes_engine, circuitId): 195 | for did, err in pwr_changes_driver.items(): 196 | self.model.drivers[did].pwr += err * \ 197 | self.k_driver_change * self.model.drivers[did].variance 198 | self.model.drivers[did].trackpwr[circuitId] += err * \ 199 | self.k_driver_change * self.k_track_change_multiplier 200 | 201 | driv_var = abs(err) - self.model.drivers[did].variance 202 | self.model.drivers[did].variance += self.k_driver_variance_change * driv_var 203 | self.model.driver_variances.append(abs(driv_var)) 204 | 205 | for cid, err_list in pwr_changes_constructor.items(): 206 | self.model.constructors[cid].pwr += mean(err_list) * \ 207 | self.k_const_change * self.model.constructors[cid].variance 208 | self.model.constructors[cid].trackpwr[circuitId] += mean( 209 | err_list) * self.k_const_change * self.k_track_change_multiplier 210 | 211 | const_var = abs(mean(err_list)) - \ 212 | self.model.constructors[cid].variance 213 | self.model.constructors[cid].variance += self.k_const_variance_change * const_var 214 | self.model.const_variances.append(abs(const_var)) 215 | 216 | for engine, err_list in pwr_changes_engine.items(): 217 | engine.pwr += mean(err_list) * \ 218 | self.k_engine_change * engine.variance 219 | engine.trackpwr[circuitId] += mean(err_list) * \ 220 | self.k_engine_change * self.k_track_change_multiplier 221 | 222 | eng_var = abs(mean(err_list)) - engine.variance 223 | engine.variance += self.k_engine_variance_change * eng_var 224 | self.model.engine_variances.append(abs(eng_var)) 225 | 226 | 227 | def calculateScoresFromResults(qresults, circuitId, globaldev, trackdev): 228 | '''Return a list of standardised quali score values for the quali results.''' 229 | best = qresults[0][2] 230 | 231 | # Only the times. Maintains the same order as the original tuples, so the same index can be used 232 | times = [((x[2])*100/best) for x in qresults] 233 | median = np.median(times) 234 | dev = np.mean(np.abs(times - median)) 235 | 236 | # Standardised list 237 | stdList = [(x - median)/dev for x in times] 238 | # print(stdList) 239 | 240 | updateDevValues(dev, circuitId, globaldev, trackdev) 241 | return [x/(np.median(list(filter(None.__ne__, globaldev))) + np.median(list(filter(None.__ne__, trackdev[circuitId])))/2) 242 | for x in stdList] 243 | 244 | 245 | def updateDevValues(dev, circuitId, globaldev, trackdev): 246 | '''Updates the deviation values by popping the oldest value and inserting the newest to the front''' 247 | globaldev.pop() # Removes last item 248 | globaldev.insert(0, dev) 249 | 250 | if circuitId not in trackdev: 251 | trackdev[circuitId] = [None] * 6 252 | trackdev[circuitId].pop() 253 | trackdev[circuitId].insert(0, dev) 254 | -------------------------------------------------------------------------------- /f1predict/race/DataProcessor.py: -------------------------------------------------------------------------------- 1 | import statistics 2 | 3 | from f1predict.race.EloModel import EloModel, EloDriver, EloConstructor, EloEngine 4 | from f1predict.race.retirementBlame import getRetirementBlame 5 | 6 | RETIREMENT_PENALTY = -1.8 7 | FINISHING_BONUS = 0.1 8 | BASE_RETIREMENT_PROBABILITY = 0.1 9 | RETIREMENT_PROBABILITY_CHANGE_TRACK = 0.33 10 | RETIREMENT_PROBABILITY_CHANGE_DRIVER = 0.10 11 | ROOKIE_DRIVER_RATING = 1820 12 | 13 | class DataProcessor: 14 | def __init__(self, seasonsData, raceResultsData, driversData, constructorsData, enginesData): 15 | self.seasonsData = seasonsData 16 | self.raceResultsData = raceResultsData 17 | self.driversData = driversData 18 | self.constructorsData = constructorsData 19 | self.enginesData = enginesData 20 | self.model = None 21 | 22 | def processDataset(self): 23 | self.model = EloModel({}, {}, {}, {}) 24 | self.predictions = [] 25 | for year, season in self.seasonsData.items(): # Read every season: 26 | self._updateModelsForYear(season) 27 | racesAsList = list(season.races.items()) 28 | racesAsList.sort(key=lambda x: x[1].round) 29 | 30 | for raceId, data in racesAsList: 31 | if raceId in self.raceResultsData and self.raceResultsData[raceId]: 32 | results = self.raceResultsData[raceId] 33 | self._addNewDriversAndConstructors(results, year) 34 | self.model.addNewCircuit(data.circuitId) 35 | 36 | gaElos = {} 37 | classified = [] 38 | retired = [] 39 | for index, res in enumerate(results): 40 | self.model.addNewCircuitToParticipant(res["driverId"], data.circuitId) 41 | gaElos[res["driverId"]] = self.model.getGaElo( 42 | res["driverId"], res["grid"], data.circuitId) 43 | if res["position"] is None: 44 | retired.append((res["driverId"], res["status"])) 45 | else: 46 | classified.append(res["driverId"]) 47 | 48 | # Generate predictions: 49 | sortedGaElos = [(driverId, gaElo) for (driverId, gaElo) in gaElos.items()] 50 | sortedGaElos.sort(key=lambda x: x[1], reverse=True) 51 | if sortedGaElos: # TODO is this if-check necessary? 52 | self.predictions.append([x[0] for x in sortedGaElos]) 53 | 54 | # Adjust models based on race results 55 | eloAdjustments, alphaAdjustment = self._calculateTrackAlphaAdjustmentAndBestEloAdjustments( 56 | classified, results, data.circuitId) 57 | self._adjustEloRatings(classified, retired, eloAdjustments, data.circuitId) 58 | self._adjustRetirementFactors(retired, classified, data.circuitId) 59 | self.model.adjustCircuitAplha( 60 | alphaAdjustment, data.circuitId) 61 | 62 | 63 | # Returns the generated EloModel from the last processing, or an empty model if the function was not called yet 64 | def getModel(self): 65 | return self.model 66 | 67 | # Returns a list of all generated predictions from the last processing 68 | # Throws an exception if called before processing a dataset 69 | def getPredictions(self): 70 | if self.predictions == None: 71 | raise AssertionError( 72 | "Predictions not generated yet! Call before calling me.") 73 | return self.predictions 74 | 75 | def _updateModelsForYear(self, season): 76 | '''Resolves team name changes''' 77 | # Updating list of engines and constructors: 78 | for new, old in season.teamChanges.items(): 79 | self.model.constructors[new] = self.model.constructors[old] 80 | self.model.constructors[new].name = self.constructorsData[new] 81 | 82 | for cId, engineId in season.constructorEngines.items(): 83 | # Check that the constructor and engine exist 84 | if engineId not in self.model.engines: 85 | self.model.engines[engineId] = EloEngine( 86 | self.enginesData[engineId]) 87 | if cId not in self.model.constructors: 88 | self.model.constructors[cId] = EloConstructor( 89 | self.constructorsData[cId], None) 90 | # Assign it its engine 91 | self.model.constructors[cId].engine = self.model.engines[engineId] 92 | 93 | def _updateModelsAtEndOfYear(self, season): 94 | # Delete old, unused constructors 95 | for new, old in season.teamChanges.items(): 96 | del self.model.constructors[old] 97 | 98 | # Regress all powers towards the mean 99 | # TODO 100 | 101 | def _addNewDriversAndConstructors(self, resultsForRace, year): 102 | for res in resultsForRace: 103 | if res["driverId"] not in self.model.drivers: 104 | self.model.drivers[res["driverId"]] = EloDriver( 105 | self.driversData[res["driverId"]], res["constructorId"]) 106 | if year > 2003: 107 | self.model.drivers[res["driverId"] 108 | ].rating = ROOKIE_DRIVER_RATING 109 | if self.model.drivers[res["driverId"]].constructor is not self.model.constructors[res["constructorId"]]: 110 | self.model.drivers[res["driverId"] 111 | ].constructor = self.model.constructors[res["constructorId"]] 112 | 113 | def _calculateTrackAlphaAdjustmentAndBestEloAdjustments(self, driverIDs, resultsForRace, circuitId): 114 | eloAdjustments = () 115 | eloAdjustmentsSum = None 116 | bestAdjustment = 0 117 | adjustments = [0.9, 0.91, 0.92, 0.93, 0.94, 0.95, 0.96, 0.97, 0.98, 118 | 0.99, 1, 1.01, 1.02, 1.03, 1.04, 1.05, 1.06, 1.07, 1.08, 1.09, 1.1] 119 | for alphaAdjustment in adjustments: 120 | results = {} 121 | gaElos = {} 122 | for index, res in enumerate(resultsForRace): 123 | results[res["driverId"]] = res["position"] 124 | gaElos[res["driverId"]] = self.model.getGaEloWithTrackAlpha( 125 | res["driverId"], res["grid"], circuitId, alphaAdjustment) 126 | curEloAdjustments = self._calculateEloAdjustments(driverIDs, gaElos, results) 127 | curEloAdjustmentsSum = 0 128 | curEloAdjustmentsSum += statistics.mean(map(abs, curEloAdjustments[0].values())) 129 | curEloAdjustmentsSum += statistics.mean(map(abs, curEloAdjustments[1].values())) 130 | curEloAdjustmentsSum += statistics.mean(map(abs, curEloAdjustments[2].values())) 131 | 132 | if not eloAdjustmentsSum or curEloAdjustmentsSum < eloAdjustmentsSum: 133 | eloAdjustmentsSum = curEloAdjustmentsSum 134 | eloAdjustments = curEloAdjustments 135 | bestAdjustment = alphaAdjustment 136 | return eloAdjustments, bestAdjustment 137 | 138 | def _calculateEloAdjustments(self, driverIDs, gaElos, results): 139 | driverAdjustments = {} 140 | engineAdjustments = {} 141 | constructorAdjustments = {} 142 | for i in range(len(driverIDs)): 143 | for k in range(i+1, len(driverIDs)): 144 | if driverIDs[i] not in driverAdjustments: 145 | driverAdjustments[driverIDs[i]] = 0 146 | if driverIDs[k] not in driverAdjustments: 147 | driverAdjustments[driverIDs[k]] = 0 148 | 149 | if self.model.drivers[driverIDs[i]].constructor not in constructorAdjustments: 150 | constructorAdjustments[self.model.drivers[driverIDs[i]].constructor] = 0 151 | if self.model.drivers[driverIDs[k]].constructor not in constructorAdjustments: 152 | constructorAdjustments[self.model.drivers[driverIDs[k]].constructor] = 0 153 | 154 | if self.model.drivers[driverIDs[i]].constructor.engine not in engineAdjustments: 155 | engineAdjustments[self.model.drivers[driverIDs[i] 156 | ].constructor.engine] = 0 157 | if self.model.drivers[driverIDs[k]].constructor.engine not in engineAdjustments: 158 | engineAdjustments[self.model.drivers[driverIDs[k] 159 | ].constructor.engine] = 0 160 | 161 | headToHeadResult = 1 if results[driverIDs[i]] < results[driverIDs[k]] else 0 162 | expectedScore = self.model.getExpectedScore( 163 | gaElos[driverIDs[i]], gaElos[driverIDs[k]]) 164 | driverAdjustments[driverIDs[i]] += headToHeadResult - expectedScore 165 | driverAdjustments[driverIDs[k]] += expectedScore - headToHeadResult 166 | 167 | constructorAdjustments[self.model.drivers[driverIDs[i] 168 | ].constructor] += headToHeadResult - expectedScore 169 | constructorAdjustments[self.model.drivers[driverIDs[k] 170 | ].constructor] += expectedScore - headToHeadResult 171 | 172 | engineAdjustments[self.model.drivers[driverIDs[i] 173 | ].constructor.engine] += headToHeadResult - expectedScore 174 | engineAdjustments[self.model.drivers[driverIDs[k] 175 | ].constructor.engine] += expectedScore - headToHeadResult 176 | 177 | return (driverAdjustments, constructorAdjustments, engineAdjustments) 178 | 179 | def _adjustEloRatings(self, classified, retired, eloAdjustments, circuitId): 180 | for driverId in classified: 181 | self.model.adjustEloRating( 182 | driverId, eloAdjustments[0][driverId] + FINISHING_BONUS, circuitId) 183 | for (driverId, _) in retired: 184 | self.model.adjustEloRating( 185 | driverId, RETIREMENT_PENALTY, circuitId) 186 | 187 | for constructor in eloAdjustments[1]: 188 | self.model.adjustEloRatingConstructor( 189 | constructor, eloAdjustments[1][constructor], circuitId) 190 | 191 | for engine in eloAdjustments[2]: 192 | self.model.adjustEloRatingEngine( 193 | engine, eloAdjustments[2][engine], circuitId) 194 | 195 | def _adjustRetirementFactors(self, retired, classified, circuitID): 196 | const_retirements = {} 197 | eng_retirements = {} 198 | all_retirements = [] 199 | 200 | # Process drivers who were classified in the race 201 | for driverID in classified: 202 | if self.model.drivers[driverID].constructor not in const_retirements: 203 | const_retirements[self.model.drivers[driverID].constructor] = [] 204 | if self.model.drivers[driverID].constructor.engine not in eng_retirements: 205 | eng_retirements[self.model.drivers[driverID].constructor.engine] = [] 206 | 207 | all_retirements.append(0) 208 | self.model.drivers[driverID].retirementProbability *= 1-RETIREMENT_PROBABILITY_CHANGE_DRIVER 209 | const_retirements[self.model.drivers[driverID].constructor].append(0) 210 | eng_retirements[self.model.drivers[driverID].constructor.engine].append(0) 211 | 212 | # Process drivers who retired from the race 213 | for (driverID, retirementReason) in retired: 214 | if self.model.drivers[driverID].constructor not in const_retirements: 215 | const_retirements[self.model.drivers[driverID].constructor] = [] 216 | if self.model.drivers[driverID].constructor.engine not in eng_retirements: 217 | eng_retirements[self.model.drivers[driverID].constructor.engine] = [] 218 | 219 | all_retirements.append(1) 220 | blame = getRetirementBlame(retirementReason) 221 | self.model.drivers[driverID].retirementProbability = (3 * blame[0] * RETIREMENT_PROBABILITY_CHANGE_DRIVER) + \ 222 | (1-RETIREMENT_PROBABILITY_CHANGE_DRIVER) * self.model.drivers[driverID].retirementProbability 223 | const_retirements[self.model.drivers[driverID].constructor].append(blame[1]) 224 | eng_retirements[self.model.drivers[driverID].constructor.engine].append(blame[2]) 225 | 226 | # Adjust overall retirement factor 227 | self.model.overallRetirementProbability = statistics.mean(all_retirements) * \ 228 | RETIREMENT_PROBABILITY_CHANGE_DRIVER + (1-RETIREMENT_PROBABILITY_CHANGE_DRIVER) \ 229 | * self.model.overallRetirementProbability 230 | 231 | # Adjust track retirement factors 232 | if circuitID not in self.model.tracksRetirementFactor: 233 | self.model.tracksRetirementFactor[circuitID] = BASE_RETIREMENT_PROBABILITY 234 | oldValue = self.model.tracksRetirementFactor[circuitID] 235 | self.model.tracksRetirementFactor[circuitID] += (statistics.mean(all_retirements) - 236 | oldValue) * RETIREMENT_PROBABILITY_CHANGE_TRACK 237 | 238 | # Adjust constructor factors 239 | for constructor, blames in const_retirements.items(): 240 | newValue = statistics.mean(blames) 241 | constructor.retirementProbability = (3 * newValue * RETIREMENT_PROBABILITY_CHANGE_DRIVER) + \ 242 | (1-RETIREMENT_PROBABILITY_CHANGE_DRIVER) * constructor.retirementProbability 243 | 244 | # Adjust engine factors 245 | for engine, blames in eng_retirements.items(): 246 | newValue = statistics.mean(blames) 247 | engine.retirementProbability = (3 * newValue * RETIREMENT_PROBABILITY_CHANGE_DRIVER) + \ 248 | (1-RETIREMENT_PROBABILITY_CHANGE_DRIVER) * engine.retirementProbability 249 | -------------------------------------------------------------------------------- /notebooks/ConstructorDataExplorer.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Testing the type of data in the constructors table" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 3, 13 | "metadata": { 14 | "collapsed": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "import pymysql\n", 19 | "import pymysql.cursors\n", 20 | "import matplotlib.pyplot as plt\n", 21 | "import numpy as np\n", 22 | "import pickle\n", 23 | "\n", 24 | "from f1predict.common import common" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 4, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "output_type": "stream", 34 | "name": "stdout", 35 | "text": [ 36 | "1, Lewis Hamilton\n2, Nick Heidfeld\n3, Nico Rosberg\n4, Fernando Alonso\n5, Heikki Kovalainen\n6, Kazuki Nakajima\n7, Sébastien Bourdais\n8, Kimi Räikkönen\n9, Robert Kubica\n10, Timo Glock\n11, Takuma Sato\n12, Nelson Piquet Jr.\n13, Felipe Massa\n14, David Coulthard\n15, Jarno Trulli\n16, Adrian Sutil\n17, Mark Webber\n18, Jenson Button\n19, Anthony Davidson\n20, Sebastian Vettel\n21, Giancarlo Fisichella\n22, Rubens Barrichello\n23, Ralf Schumacher\n24, Vitantonio Liuzzi\n25, Alexander Wurz\n26, Scott Speed\n27, Christijan Albers\n28, Markus Winkelhock\n29, Sakon Yamamoto\n30, Michael Schumacher\n31, Juan Pablo Montoya\n32, Christian Klien\n33, Tiago Monteiro\n34, Yuji Ide\n35, Jacques Villeneuve\n36, Franck Montagny\n37, Pedro de la Rosa\n38, Robert Doornbos\n39, Narain Karthikeyan\n40, Patrick Friesacher\n41, Ricardo Zonta\n42, Antônio Pizzonia\n43, Cristiano da Matta\n44, Olivier Panis\n45, Giorgio Pantano\n46, Gianmaria Bruni\n47, Zsolt Baumgartner\n48, Marc Gené\n49, Heinz-Harald Frentzen\n50, Jos Verstappen\n51, Justin Wilson\n52, Ralph Firman\n53, Nicolas Kiesa\n54, Luciano Burti\n55, Jean Alesi\n56, Eddie Irvine\n57, Mika Häkkinen\n58, Tarso Marques\n59, Enrique Bernoldi\n60, Gastón Mazzacane\n61, Tomáš Enge\n62, Alex Yoong\n63, Mika Salo\n64, Pedro Diniz\n65, Johnny Herbert\n66, Allan McNish\n67, Sébastien Buemi\n68, Toranosuke Takagi\n69, Luca Badoer\n70, Alessandro Zanardi\n71, Damon Hill\n72, Stéphane Sarrazin\n73, Ricardo Rosset\n74, Esteban Tuero\n75, Shinji Nakano\n76, Jan Magnussen\n77, Gerhard Berger\n78, Nicola Larini\n79, Ukyo Katayama\n80, Vincenzo Sospiri\n81, Gianni Morbidelli\n82, Norberto Fontana\n83, Pedro Lamy\n84, Martin Brundle\n85, Andrea Montermini\n86, Giovanni Lavaggi\n87, Mark Blundell\n88, Aguri Suzuki\n89, Taki Inoue\n90, Roberto Moreno\n91, Karl Wendlinger\n92, Bertrand Gachot\n93, Domenico Schiattarella\n94, Pierluigi Martini\n95, Nigel Mansell\n96, Jean-Christophe Boullion\n97, Massimiliano Papis\n98, Jean-Denis Délétraz\n99, Gabriele Tarquini\n100, Érik Comas\n101, David Brabham\n102, Ayrton Senna\n103, Éric Bernard\n104, Christian Fittipaldi\n105, Michele Alboreto\n106, Olivier Beretta\n107, Roland Ratzenberger\n108, Paul Belmondo\n109, Jyrki Järvilehto\n110, Andrea de Cesaris\n111, Jean-Marc Gounon\n112, Philippe Alliot\n113, Philippe Adams\n114, Yannick Dalmas\n115, Hideki Noda\n116, Franck Lagorce\n117, Alain Prost\n118, Derek Warwick\n119, Riccardo Patrese\n120, Fabrizio Barbazza\n121, Michael Andretti\n122, Ivan Capelli\n123, Thierry Boutsen\n124, Marco Apicella\n125, Emanuele Naspetti\n126, Toshio Suzuki\n127, Maurício Gugelmin\n128, Eric van de Poele\n129, Olivier Grouillard\n130, Andrea Chiesa\n131, Stefano Modena\n132, Giovanna Amati\n133, Alex Caffi\n134, Enrico Bertaggia\n135, Perry McCarthy\n136, Jan Lammers\n137, Nelson Piquet\n138, Satoru Nakajima\n139, Emanuele Pirro\n140, Stefan Johansson\n141, Julian Bailey\n142, Pedro Chaves\n143, Michael Bartels\n144, Naoki Hattori\n145, Alessandro Nannini\n146, Bernd Schneider\n147, Paolo Barilla\n148, Gregor Foitek\n149, Claudio Langes\n150, Gary Brabham\n151, Martin Donnelly\n152, Bruno Giacomelli\n153, Jaime Alguersuari\n154, Romain Grosjean\n155, Kamui Kobayashi\n156, Jonathan Palmer\n157, Christian Danner\n158, Eddie Cheever\n159, Luis Pérez-Sala\n160, Piercarlo Ghinzani\n161, Volker Weidler\n162, Pierre-Henri Raphanel\n163, René Arnoux\n164, Joachim Winkelhock\n165, Oscar Larrauri\n166, Philippe Streiff\n167, Adrián Campos\n168, Jean-Louis Schlesser\n169, Pascal Fabre\n170, Teo Fabi\n171, Franco Forini\n172, Jacques Laffite\n173, Elio de Angelis\n174, Johnny Dumfries\n175, Patrick Tambay\n176, Marc Surer\n177, Keke Rosberg\n178, Alan Jones\n179, Huub Rothengatter\n180, Allen Berg\n181, Manfred Winkelhock\n182, Niki Lauda\n183, François Hesnault\n184, Mauro Baldi\n185, Stefan Bellof\n186, Kenny Acheson\n187, John Watson\n188, Johnny Cecotto\n189, Jo Gartner\n190, Corrado Fabi\n191, Mike Thackwell\n192, Chico Serra\n193, Danny Sullivan\n194, Eliseo Salazar\n195, Roberto Guerrero\n196, Raul Boesel\n197, Jean-Pierre Jarier\n198, Jacques Villeneuve Sr.\n199, Carlos Reutemann\n200, Jochen Mass\n201, Slim Borgudd\n202, Didier Pironi\n203, Gilles Villeneuve\n204, Riccardo Paletti\n205, Brian Henton\n206, Derek Daly\n207, Mario Andretti\n208, Emilio de Villota\n209, Geoff Lees\n210, Tommy Byrne\n211, Rupert Keegan\n212, Hector Rebaque\n213, Beppe Gabbiani\n214, Kevin Cogan\n215, Miguel Ángel Guerra\n216, Siegfried Stohr\n217, Ricardo Zunino\n218, Ricardo Londoño\n219, Jean-Pierre Jabouille\n220, Giorgio Francia\n221, Patrick Depailler\n222, Jody Scheckter\n223, Clay Regazzoni\n224, Emerson Fittipaldi\n225, Dave Kennedy\n226, Stephen South\n227, Tiff Needell\n228, Desiré Wilson\n229, Harald Ertl\n230, Vittorio Brambilla\n231, James Hunt\n232, Arturo Merzario\n233, Hans-Joachim Stuck\n234, Gianfranco Brancatelli\n235, Jacky Ickx\n236, Patrick Gaillard\n237, Alex Ribeiro\n238, Ronnie Peterson\n239, Brett Lunger\n240, Danny Ongais\n241, Lamberto Leoni\n242, Divina Galica\n243, Rolf Stommelen\n244, Alberto Colombo\n245, Tony Trimmer\n246, Hans Binder\n247, Michael Bleekemolen\n248, Carlo Franchi\n249, Bobby Rahal\n250, Carlos Pace\n251, Ian Scheckter\n252, Tom Pryce\n253, Ingo Hoffmann\n254, Renzo Zorzi\n255, Gunnar Nilsson\n256, Larry Perkins\n257, Boy Lunger\n258, Patrick Nève\n259, David Purley\n260, Conny Andersson\n261, Bernard de Dryver\n262, Jackie Oliver\n263, Mikko Kozarowitzky\n264, Andy Sutcliffe\n265, Guy Edwards\n266, Brian McGuire\n267, Vern Schuppan\n268, Hans Heyer\n269, Teddy Pilette\n270, Ian Ashley\n271, Loris Kessel\n272, Kunimitsu Takahashi\n273, Kazuyoshi Hoshino\n274, Noritake Takahara\n275, Lella Lombardi\n276, Bob Evans\n277, Michel Leclère\n278, Chris Amon\n279, Emilio Zapico\n280, Henri Pescarolo\n281, Jac Nelleman\n282, Damien Magee\n283, Mike Wilds\n284, Alessandro Pesenti-Rossi\n285, Otto Stuppacher\n286, Warwick Brown\n287, Masahiro Hasemi\n288, Mark Donohue\n289, Graham Hill\n290, Wilson Fittipaldi\n291, Guy Tunmer\n292, Eddie Keizan\n293, Dave Charlton\n294, Tony Brise\n295, Roelof Wunderink\n296, François Migault\n297, Torsten Palm\n298, Gijs van Lennep\n299, Hiroshi Fushida\n300, John Nicholson\n301, Dave Morgan\n302, Jim Crawford\n303, Jo Vonlanthen\n304, Denny Hulme\n305, Mike Hailwood\n306, Jean-Pierre Beltoise\n307, Howden Ganley\n308, Richard Robarts\n309, Peter Revson\n310, Paddy Driver\n311, Tom Belsø\n312, Brian Redman\n313, Rikky von Opel\n314, Tim Schenken\n315, Gérard Larrousse\n316, Leo Kinnunen\n317, Reine Wisell\n318, Bertil Roos\n319, José Dolhem\n320, Peter Gethin\n321, Derek Bell\n322, David Hobbs\n323, Dieter Quester\n324, Helmuth Koinigg\n325, Carlo Facetti\n326, Eppie Wietzes\n327, François Cevert\n328, Jackie Stewart\n329, Mike Beuttler\n330, Nanni Galli\n331, Luiz Bueno\n332, George Follmer\n333, Andrea de Adamich\n334, Jackie Pretorius\n335, Roger Williamson\n336, Graham McRae\n337, Helmut Marko\n338, David Walker\n339, Alex Soler-Roig\n340, John Love\n341, John Surtees\n342, Skip Barber\n343, Bill Brack\n344, Sam Posey\n345, Pedro Rodríguez\n346, Jo Siffert\n347, Jo Bonnier\n348, François Mazet\n349, Max Jean\n350, Vic Elford\n351, Silvio Moser\n352, George Eaton\n353, Pete Lovely\n354, Chris Craft\n355, John Cannon\n356, Jack Brabham\n357, John Miles\n358, Jochen Rindt\n359, Johnny Servoz-Gavin\n360, Bruce McLaren\n361, Piers Courage\n362, Peter de Klerk\n363, Ignazio Giunti\n364, Dan Gurney\n365, Hubert Hahne\n366, Gus Hutchison\n367, Peter Westbury\n368, Sam Tingle\n369, Basil van Rooyen\n370, Richard Attwood\n371, Al Pease\n372, John Cordts\n373, Jim Clark\n374, Mike Spence\n375, Ludovico Scarfiotti\n376, Lucien Bianchi\n377, Jo Schlesser\n378, Robin Widdows\n379, Kurt Ahrens\n380, Frank Gardner\n381, Bobby Unser\n382, Moisés Solana\n383, Bob Anderson\n384, Luki Botha\n385, Lorenzo Bandini\n386, Richie Ginther\n387, Mike Parkes\n388, Chris Irwin\n389, Guy Ligier\n390, Alan Rees\n391, Brian Hart\n392, Mike Fisher\n393, Tom Jones\n394, Giancarlo Baghetti\n395, Jonathan Williams\n396, Bob Bondurant\n397, Peter Arundell\n398, Vic Wilson\n399, John Taylor\n400, Chris Lawrence\n401, Trevor Taylor\n402, Giacomo Russo\n403, Phil Hill\n404, Innes Ireland\n405, Ronnie Bucknum\n406, Paul Hawkins\n407, David Prophet\n408, Tony Maggs\n409, Trevor Blokdyk\n410, Neville Lederle\n411, Doug Serrurier\n412, Brausch Niemann\n413, Ernie Pieterse\n414, Clive Puzey\n415, Ray Reed\n416, David Clapham\n417, Alex Blignaut\n418, Masten Gregory\n419, John Rhodes\n420, Ian Raby\n421, Alan Rollinson\n422, Brian Gubby\n423, Gerhard Mitter\n424, Roberto Bussinello\n425, Nino Vaccarella\n426, Giorgio Bassi\n427, Maurice Trintignant\n428, Bernard Collomb\n429, André Pilette\n430, Carel Godin de Beaufort\n431, Edgar Barth\n432, Mário de Araújo Cabral\n433, Walt Hansgen\n434, Hap Sharp\n435, Willy Mairesse\n436, John Campbell-Jones\n437, Ian Burgess\n438, Tony Settember\n439, Nasif Estéfano\n440, Jim Hall\n441, Tim Parnell\n442, Kurt Kuhnke\n443, Ernesto Brambilla\n444, Roberto Lippi\n445, Günther Seiffert\n446, Carlo Abate\n447, Gaetano Starrabba\n448, Peter Broeker\n449, Rodger Ward\n450, Ernie de Vos\n451, Frank Dochnal\n452, Thomas Monarch\n842, Pierre Gasly\n453, Jackie Lewis\n454, Ricardo Rodríguez\n455, Wolfgang Seidel\n456, Roy Salvadori\n457, Ben Pon\n458, Rob Slotemaker\n459, Tony Marsh\n460, Gerry Ashmore\n461, Heinz Schiller\n462, Colin Davis\n463, Jay Chamberlain\n464, Tony Shelly\n465, Keith Greene\n466, Heini Walter\n467, Ernesto Prinoth\n468, Roger Penske\n469, Rob Schroeder\n470, Timmy Mayer\n471, Bruce Johnstone\n472, Mike Harris\n473, Gary Hocking\n474, Syd van der Vyver\n475, Stirling Moss\n476, Wolfgang von Trips\n477, Cliff Allison\n478, Hans Herrmann\n479, Tony Brooks\n480, Michael May\n481, Henry Taylor\n482, Olivier Gendebien\n483, Giorgio Scarlatti\n484, Brian Naylor\n485, Juan Manuel Bordeu\n486, Jack Fairman\n487, Massimo Natili\n488, Peter Monteverdi\n489, Renato Pirocchi\n490, Geoff Duke\n491, Alfonso Thiele\n492, Menato Boffa\n493, Peter Ryan\n494, Lloyd Ruby\n495, Ken Miles\n496, Carlos Menditeguy\n497, Alberto Rodriguez Larreta\n498, José Froilán González\n499, Roberto Bonomi\n500, Gino Munaron\n501, Harry Schell\n502, Alan Stacey\n503, Ettore Chimeri\n504, Antonio Creus\n505, Chris Bristow\n506, Bruce Halford\n507, Chuck Daigh\n508, Lance Reventlow\n509, Jim Rathmann\n510, Paul Goldsmith\n511, Don Branson\n512, Johnny Thomson\n513, Eddie Johnson\n514, Bob Veith\n515, Bud Tingelstad\n516, Bob Christie\n517, Red Amick\n518, Duane Carter\n519, Bill Homeier\n520, Gene Hartley\n521, Chuck Stevenson\n522, Bobby Grim\n523, Shorty Templeman\n524, Jim Hurtubise\n525, Jimmy Bryan\n526, Troy Ruttman\n527, Eddie Sachs\n528, Don Freeland\n529, Tony Bettenhausen\n530, Wayne Weiler\n531, Anthony Foyt\n532, Eddie Russo\n533, Johnny Boyd\n534, Gene Force\n535, Jim McWithey\n536, Len Sutton\n537, Dick Rathmann\n538, Al Herman\n539, Dempsey Wilson\n540, Mike Taylor\n541, Ron Flockhart\n542, David Piper\n543, Giulio Cabianca\n544, Piero Drogo\n545, Fred Gamble\n546, Arthur Owen\n547, Horace Gould\n548, Bob Drake\n549, Ivor Bueb\n550, Alain de Changy\n551, Maria de Filippis\n552, Jean Lucienbonnet\n553, André Testut\n554, Jean Behra\n555, Paul Russo\n556, Jimmy Daywalt\n557, Chuck Arnold\n558, Al Keller\n559, Pat Flaherty\n560, Bill Cheesbourg\n561, Ray Crawford\n562, Jack Turner\n563, Chuck Weyant\n564, Jud Larson\n565, Mike Magill\n566, Carroll Shelby\n567, Fritz d'Orey\n568, Azdrubal Fontes\n569, Peter Ashdown\n570, Bill Moss\n571, Dennis Taylor\n572, Harry Blanchard\n573, Alessandro de Tomaso\n574, George Constantine\n575, Bob Said\n576, Phil Cade\n577, Luigi Musso\n578, Mike Hawthorn\n579, Juan Fangio\n580, Paco Godia\n581, Peter Collins\n582, Ken Kavanagh\n583, Gerino Gerini\n584, Bruce Kessler\n585, Paul Emery\n586, Luigi Piotti\n587, Bernie Ecclestone\n588, Luigi Taramazzo\n589, Louis Chiron\n590, Stuart Lewis-Evans\n591, George Amick\n592, Jimmy Reece\n593, Johnnie Parsons\n594, Johnnie Tolan\n595, Billy Garrett\n596, Ed Elisian\n597, Pat O'Connor\n598, Jerry Unser\n599, Art Bisch\n600, Christian Goethals\n601, Dick Gibson\n602, Robert La Caze\n603, André Guelfi\n604, François Picard\n605, Tom Bridger\n606, Alfonso de Portago\n607, Cesare Perdisa\n608, Eugenio Castellotti\n609, André Simon\n610, Les Leston\n611, Sam Hanks\n612, Andy Linden\n613, Marshall Teague\n614, Don Edmunds\n615, Fred Agabashian\n616, Elmer George\n617, Mike MacDowel\n618, Herbert MacKay-Fraser\n619, Bob Gerard\n620, Umberto Maglioli\n621, Paul England\n622, Chico Landi\n623, Alberto Uria\n624, Hernando da Silva Ramos\n625, Élie Bayol\n626, Robert Manzon\n627, Louis Rosier\n628, Bob Sweikert\n629, Cliff Griffith\n630, Duke Dinsmore\n631, Keith Andrews\n632, Paul Frère\n633, Luigi Villoresi\n634, Piero Scotti\n635, Colin Chapman\n636, Desmond Titterington\n637, Archie Scott Brown\n638, Ottorino Volonterio\n639, André Milhoux\n640, Toulo de Graffenried\n641, Piero Taruffi\n642, Nino Farina\n643, Roberto Mieres\n644, Sergio Mantovani\n645, Clemar Bucci\n646, Jesús Iglesias\n647, Alberto Ascari\n648, Karl Kling\n649, Pablo Birger\n650, Jacques Pollet\n651, Lance Macklin\n652, Ted Whiteaway\n653, Jimmy Davies\n654, Walt Faulkner\n655, Cal Niday\n656, Art Cross\n657, Bill Vukovich\n658, Jack McGrath\n659, Jerry Hoyt\n660, Johnny Claes\n661, Peter Walker\n662, Mike Sparken\n663, Ken Wharton\n664, Kenneth McAlpine\n665, Leslie Marr\n666, Tony Rolt\n667, John Fitch\n668, Jean Lucas\n669, Prince Bira\n670, Onofre Marimón\n671, Roger Loyer\n672, Jorge Daponte\n673, Mike Nazaruk\n674, Larry Crockett\n675, Manny Ayulo\n676, Frank Armi\n677, Travis Webb\n678, Len Duncan\n679, Ernie McCoy\n680, Jacques Swaters\n681, Georges Berger\n682, Don Beauman\n683, Leslie Thorne\n684, Bill Whitehouse\n685, John Riseley-Prichard\n686, Reg Parnell\n687, Peter Whitehead\n688, Eric Brandon\n689, Alan Brown\n690, Rodney Nuckey\n691, Hermann Lang\n692, Theo Helfrich\n693, Fred Wacker\n694, Giovanni de Riu\n695, Oscar Gálvez\n696, John Barber\n697, Felice Bonetto\n698, Adolfo Cruz\n699, Duke Nalon\n700, Carl Scarborough\n701, Bill Holland\n702, Bob Scott\n703, Arthur Legat\n704, Yves Cabantous\n705, Tony Crook\n706, Jimmy Stewart\n707, Ian Stewart\n708, Duncan Hamilton\n709, Ernst Klodwig\n710, Rudolf Krause\n711, Oswald Karch\n712, Willi Heeks\n713, Theo Fitzau\n714, Kurt Adolff\n715, Günther Bechem\n716, Erwin Bauer\n717, Hans von Stuck\n718, Ernst Loof\n719, Albert Scherrer\n720, Max de Terra\n721, Peter Hirt\n722, Piero Carini\n723, Rudi Fischer\n724, Toni Ulmen\n725, George Abecassis\n726, George Connor\n727, Jim Rigsby\n728, Joe James\n729, Bill Schindler\n730, George Fonder\n731, Henry Banks\n732, Johnny McDowell\n733, Chet Miller\n734, Bobby Ball\n735, Charles de Tornaco\n736, Roger Laurent\n737, Robert O'Brien\n738, Tony Gaze\n739, Robin Montgomerie-Charrington\n740, Franco Comotti\n741, Philippe Étancelin\n742, Dennis Poore\n743, Eric Thompson\n744, Ken Downing\n745, Graham Whitehead\n746, Gino Bianco\n747, David Murray\n748, Eitel Cantoni\n749, Bill Aston\n750, Adolf Brudes\n751, Fritz Riess\n752, Helmut Niedermayr\n753, Hans Klenk\n754, Marcel Balsa\n755, Rudolf Schoeller\n756, Paul Pietsch\n757, Josef Peters\n758, Dries van der Lof\n759, Jan Flinterman\n760, Piero Dusio\n761, Alberto Crespo\n762, Franco Rol\n763, Consalvo Sanesi\n764, Guy Mairesse\n765, Henri Louveau\n766, Lee Wallard\n767, Carl Forberg\n768, Mauri Rose\n769, Bill Mackey\n770, Cecil Green\n771, Walt Brown\n772, Mack Hellings\n773, Pierre Levegh\n774, Eugène Chaboud\n775, Aldo Gordini\n776, Joe Kelly\n777, Philip Fotheringham-Parker\n778, Brian Shawe Taylor\n779, John James\n780, Toni Branca\n781, Ken Richardson\n782, Juan Jover\n783, Georges Grignard\n784, David Hampshire\n785, Geoff Crossley\n786, Luigi Fagioli\n787, Cuth Harrison\n788, Joe Fry\n789, Eugène Martin\n790, Leslie Johnson\n791, Clemente Biondetti\n792, Alfredo Pián\n793, Raymond Sommer\n794, Joie Chitwood\n795, Myron Fohr\n796, Walt Ader\n797, Jackie Holmes\n798, Bayliss Levrett\n799, Jimmy Jackson\n800, Nello Pagani\n801, Charles Pozzi\n802, Dorino Serafini\n803, Bill Cantrell\n804, Johnny Mantz\n805, Danny Kladis\n806, Óscar González\n807, Nico Hülkenberg\n808, Vitaly Petrov\n810, Lucas di Grassi\n811, Bruno Senna\n812, Karun Chandhok\n813, Pastor Maldonado\n814, Paul di Resta\n815, Sergio Pérez\n816, Jérôme d'Ambrosio\n817, Daniel Ricciardo\n818, Jean-Éric Vergne\n819, Charles Pic\n820, Max Chilton\n821, Esteban Gutiérrez\n822, Valtteri Bottas\n823, Giedo van der Garde\n824, Jules Bianchi\n825, Kevin Magnussen\n826, Daniil Kvyat\n827, André Lotterer\n828, Marcus Ericsson\n829, Will Stevens\n830, Max Verstappen\n831, Felipe Nasr\n832, Carlos Sainz\n833, Roberto Merhi\n834, Alexander Rossi\n835, Jolyon Palmer\n836, Pascal Wehrlein\n837, Rio Haryanto\n838, Stoffel Vandoorne\n839, Esteban Ocon\n840, Lance Stroll\n841, Antonio Giovinazzi\n843, Brendon Hartley\n844, Charles Leclerc\n845, Sergey Sirotkin\n846, Lando Norris\n847, George Russell\n848, Alexander Albon\n849, Nicholas Latifi\n850, Pietro Fittipaldi\n851, Jack Aitken\n852, Yuki Tsunoda\n853, Nikita Mazepin\n854, Mick Schumacher\n\n1, McLaren\n2, BMW Sauber\n3, Williams\n4, Renault\n5, Toro Rosso\n6, Ferrari\n7, Toyota\n8, Super Aguri\n9, Red Bull\n10, Force India\n11, Honda\n12, Spyker\n13, MF1\n14, Spyker MF1\n15, Sauber\n16, BAR\n17, Jordan\n18, Minardi\n19, Jaguar\n20, Prost\n21, Arrows\n22, Benetton\n23, Brawn\n24, Stewart\n25, Tyrrell\n26, Lola\n27, Ligier\n28, Forti\n29, Footwork\n30, Pacific\n31, Simtek\n32, Team Lotus\n33, Larrousse\n34, Brabham\n35, Dallara\n36, Fondmetal\n37, March\n38, Andrea Moda\n39, AGS\n40, Lambo\n41, Leyton House\n42, Coloni\n44, Euro Brun\n45, Osella\n46, Onyx\n47, Life\n48, Rial\n49, Zakspeed\n50, RAM\n51, Alfa Romeo\n52, Spirit\n53, Toleman\n54, ATS\n55, Theodore\n56, Fittipaldi\n57, Ensign\n58, Shadow\n59, Wolf\n60, Merzario\n61, Kauhsen\n62, Rebaque\n63, Surtees\n64, Hesketh\n65, Martini\n66, BRM\n67, Penske\n68, LEC\n69, McGuire\n70, Boro\n71, Apollon\n72, Kojima\n73, Parnelli\n74, Maki\n75, Embassy Hill\n76, Lyncar\n77, Trojan\n78, Amon\n79, Token\n80, Iso Marlboro\n81, Tecno\n82, Matra\n83, Politoys\n84, Connew\n85, Bellasi\n86, De Tomaso\n87, Cooper\n88, Eagle\n89, LDS\n90, Protos\n91, Shannon\n92, Scirocco\n93, RE\n94, BRP\n95, Porsche\n96, Derrington\n97, Gilby\n98, Stebro\n99, Emeryson\n100, ENB\n101, JBW\n102, Ferguson\n103, MBM\n104, Behra-Porsche\n105, Maserati\n106, Scarab\n107, Watson\n108, Epperly\n109, Phillips\n110, Lesovsky\n111, Trevis\n112, Meskowski\n113, Kurtis Kraft\n114, Kuzma\n115, Christensen\n116, Ewing\n117, Aston Martin\n118, Vanwall\n119, Moore\n120, Dunn\n121, Elder\n122, Sutton\n123, Fry\n124, Tec-Mec\n125, Connaught\n126, Alta\n127, OSCA\n128, Gordini\n129, Stevens\n130, Bugatti\n131, Mercedes\n132, Lancia\n133, HWM\n134, Schroeder\n135, Pawl\n136, Pankratz\n137, Arzani-Volpini\n138, Nichels\n139, Bromme\n140, Klenk\n141, Simca\n142, Turner\n143, Del Roy\n144, Veritas\n145, BMW\n146, EMW\n147, AFM\n148, Frazer Nash\n149, Sherman\n150, Deidt\n151, ERA\n152, Aston Butterworth\n153, Cisitalia\n154, Talbot-Lago\n155, Hall\n156, Marchese\n157, Langley\n158, Rae\n159, Olson\n160, Wetteroth\n161, Adams\n162, Snowberger\n163, Milano\n164, HRT\n167, Cooper-Maserati\n166, Virgin\n168, Cooper-OSCA\n169, Cooper-Borgward\n170, Cooper-Climax\n171, Cooper-Castellotti\n172, Lotus-Climax\n173, Lotus-Maserati\n174, De Tomaso-Osca\n175, De Tomaso-Alfa Romeo\n176, Lotus-BRM\n177, Lotus-Borgward\n178, Cooper-Alfa Romeo\n179, De Tomaso-Ferrari\n180, Lotus-Ford\n181, Brabham-BRM\n182, Brabham-Ford\n183, Brabham-Climax\n184, LDS-Climax\n185, LDS-Alfa Romeo\n186, Cooper-Ford\n187, McLaren-Ford\n188, McLaren-Serenissima\n189, Eagle-Climax\n190, Eagle-Weslake\n191, Brabham-Repco\n192, Cooper-Ferrari\n193, Cooper-ATS\n194, McLaren-BRM\n195, Cooper-BRM\n196, Matra-Ford\n197, BRM-Ford\n198, McLaren-Alfa Romeo\n199, March-Alfa Romeo\n200, March-Ford\n201, Lotus-Pratt & Whitney\n202, Shadow-Ford\n203, Shadow-Matra\n204, Brabham-Alfa Romeo\n205, Lotus\n206, Marussia\n207, Caterham\n208, Lotus F1\n209, Manor Marussia\n210, Haas F1 Team\n211, Racing Point\n213, AlphaTauri\n214, Alpine F1 Team\n\n" 37 | ] 38 | } 39 | ], 40 | "source": [ 41 | "#Set up a database connection:\n", 42 | "connection = pymysql.connect(host='localhost',\n", 43 | " user='root',\n", 44 | " password='default',\n", 45 | " db='f1db',\n", 46 | " charset='utf8',\n", 47 | " cursorclass=pymysql.cursors.DictCursor)\n", 48 | "\n", 49 | "try:\n", 50 | " with connection.cursor() as cursor:\n", 51 | " sql = \"SELECT `driverId`, `forename`, `surname` FROM `drivers`\"\n", 52 | " cursor.execute(sql)\n", 53 | " result = cursor.fetchall()\n", 54 | " for x in result:\n", 55 | " print(str(x.get('driverId')) + \", \" + x.get('forename') + \" \" + x.get('surname'))\n", 56 | " print()\n", 57 | " \n", 58 | " sql = \"SELECT `constructorId`, `name` FROM `constructors`\"\n", 59 | " cursor.execute(sql)\n", 60 | " result = cursor.fetchall()\n", 61 | " for x in result:\n", 62 | " print(str(x.get('constructorId')) + \", \" + x.get('name'))\n", 63 | " print()\n", 64 | " \n", 65 | " sql = \"SELECT `raceId`, `circuitId`, `name` FROM `races` where `year`=\" + str(common.getCurrentYear() + 1)\n", 66 | " cursor.execute(sql)\n", 67 | " results = cursor.fetchall()\n", 68 | " for x in results:\n", 69 | " print(str(x.get('raceId')) + \", \" + str(x.get('circuitId')) + \", \" + x.get('name'))\n", 70 | "finally:\n", 71 | " connection.close()" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 8, 77 | "metadata": { 78 | "collapsed": true 79 | }, 80 | "outputs": [ 81 | { 82 | "name": "stdout", 83 | "output_type": "stream", 84 | "text": [ 85 | "0.41707484380848764\n", 86 | "0.2778752321086688\n", 87 | "1.2527192334238295\n" 88 | ] 89 | }, 90 | { 91 | "data": { 92 | "image/png": "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\n", 93 | "text/plain": [ 94 | "
" 95 | ] 96 | }, 97 | "metadata": { 98 | "needs_background": "light" 99 | }, 100 | "output_type": "display_data" 101 | }, 102 | { 103 | "data": { 104 | "image/png": "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\n", 105 | "text/plain": [ 106 | "
" 107 | ] 108 | }, 109 | "metadata": { 110 | "needs_background": "light" 111 | }, 112 | "output_type": "display_data" 113 | }, 114 | { 115 | "data": { 116 | "image/png": "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\n", 117 | "text/plain": [ 118 | "
" 119 | ] 120 | }, 121 | "metadata": { 122 | "needs_background": "light" 123 | }, 124 | "output_type": "display_data" 125 | } 126 | ], 127 | "source": [ 128 | "with open('../out/driver_variances.pickle', 'rb') as handle:\n", 129 | " driver_variances = pickle.load(handle)\n", 130 | "with open('../out/const_variances.pickle', 'rb') as handle:\n", 131 | " const_variances = pickle.load(handle)\n", 132 | "with open('../out/engine_variances.pickle', 'rb') as handle:\n", 133 | " engine_variances = pickle.load(handle)\n", 134 | "\n", 135 | "plt.subplots()\n", 136 | "plt.hist(driver_variances, density=True, bins=30)\n", 137 | "print(np.std(driver_variances))\n", 138 | "plt.subplots()\n", 139 | "plt.hist(const_variances, density=True, bins=30)\n", 140 | "print(np.std(const_variances))\n", 141 | "plt.subplots()\n", 142 | "plt.hist(engine_variances, density=True, bins=30)\n", 143 | "print(np.std(engine_variances))\n" 144 | ] 145 | }, 146 | { 147 | "cell_type": "code", 148 | "execution_count": null, 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [] 152 | } 153 | ], 154 | "metadata": { 155 | "kernelspec": { 156 | "display_name": "Python 3", 157 | "language": "python", 158 | "name": "python3" 159 | }, 160 | "language_info": { 161 | "codemirror_mode": { 162 | "name": "ipython", 163 | "version": 3 164 | }, 165 | "file_extension": ".py", 166 | "mimetype": "text/x-python", 167 | "name": "python", 168 | "nbconvert_exporter": "python", 169 | "pygments_lexer": "ipython3", 170 | "version": "3.7.9-final" 171 | } 172 | }, 173 | "nbformat": 4, 174 | "nbformat_minor": 2 175 | } --------------------------------------------------------------------------------