{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"**Investigation into the rating feature on the football probability prediction competition**\n\nOne of the things we can do is understand the 'rating' feature better. The documentation supplied simply says:\n> The rating features are calculated by Octosport. Ratings are meant to give information on the team's relative strength for a given match. For instance, we would expect that a team with a rating of 10.5 beats a team with a rating of 2.3.\n\nThis is not very detailed. First, we'll assume that Octosport don't create a 'rating' for no reason and that it has some predictive power. Second, we'll assume that it will be possible to infer roughly what the 'rating' means. A more numerical understanding of the value is desired and although there is not a detailed explanation of how rating is calculated, we do have historical data where the rating data is given along with the subsequent results.\n\nFor this inital investigation we'll just consider the i=1 history. It's likely that going beyond that will  include duplicate histories into the data.\n\nIf we assume that the 'rating' has some predictive power (an implicit assumption is that the rating was assigned before the game) then we can see for example how a difference in rating results in a different probability of a home win.","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19"}},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport matplotlib.pyplot as plt\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\ntrain = pd.read_csv(\"/kaggle/input/football-match-probability-prediction/train.csv\")\ntest = pd.read_csv(\"/kaggle/input/football-match-probability-prediction/test.csv\")","metadata":{"execution":{"iopub.status.busy":"2022-04-11T17:27:55.338508Z","iopub.execute_input":"2022-04-11T17:27:55.338856Z","iopub.status.idle":"2022-04-11T17:28:04.479511Z","shell.execute_reply.started":"2022-04-11T17:27:55.338751Z","shell.execute_reply":"2022-04-11T17:28:04.478576Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"To start we will extract just the history ratings and goals; from these be can calculate the 'match rating' which is the difference between the two team ratings and the 'home_result' which is 1 for a home win and 0 otherwise.\n\nUpdate: Following feedback it has been highlighted that team ratings are calculated on a match-by-match basis making generalisation for prediction inappropriate. However, potentially the mean rating will give an indication of the strength of the team. To facilitate this two functions that calculate the mean rating have been added.","metadata":{}},{"cell_type":"code","source":"# function to calculate mean home rating. A start parameter is required as the initial investigation\n# uses the historical information only making in inappropriate to use the '1' suffixed data entries.\ndef meanRateHome(df, strt):\n    HTR = 'home_team_history_rating_'\n    HomeRateHist = []\n    for i in range(strt, 11):\n        HomeRateHist.append(df[HTR+str(i)])\n    return np.nanmean(HomeRateHist)\n        \ndef meanRateAway(df, strt):\n    ATR = 'away_team_history_rating_'\n    AwayRateHist = []\n    for i in range(strt, 11):        \n        AwayRateHist.append(df[ATR+str(i)])\n    return np.nanmean(AwayRateHist)\n            \nratingFrame = train.loc[:, ('id', 'target', 'home_team_history_goal_1', 'home_team_history_opponent_goal_1')].dropna()\nratingFrame['home_mean_rating'] = train.dropna().apply(meanRateHome, args=[1], axis=1)\nratingFrame['away_mean_rating'] = train.dropna().apply(meanRateAway, args=[1], axis=1)\nratingFrame['match_rating'] = ratingFrame['home_mean_rating']-ratingFrame['away_mean_rating']\nratingFrame['home_result'] = 0\n# comment out following line for specific match calculation (and uncomment following line)\nratingFrame['home_result'][ratingFrame['target']=='home'] = 1\n# comment out following line for prediction (and uncomment preceding line)\n#ratingFrame['home_result'][train['home_team_history_goal_1']>train['home_team_history_opponent_goal_1']] = 1\nratingFrame = ratingFrame.dropna()","metadata":{"execution":{"iopub.status.busy":"2022-04-11T17:28:12.0579Z","iopub.execute_input":"2022-04-11T17:28:12.058687Z","iopub.status.idle":"2022-04-11T17:28:25.291852Z","shell.execute_reply.started":"2022-04-11T17:28:12.058639Z","shell.execute_reply":"2022-04-11T17:28:25.291104Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Comparing home with rate with rating**\n\nTo calcuate the distribution of 'home_result' vs 'match_rating' we'll bootstrap by taking multiple random samples from the data and then qcut the sample into equally sized bins.","metadata":{}},{"cell_type":"code","source":"bins = 15\nnoIts = 5\nhomeRatingResults = np.empty([2, bins*noIts])\nfor i in range(noIts):\n    dataSample = ratingFrame.sample(frac=0.33, random_state=np.random.randint(0, 100))\n    # split data into bins of equal size\n    dataSample['Hbins'] = pd.qcut(dataSample['match_rating'], bins, duplicates='drop')\n    binList = dataSample['Hbins'].unique() #create list of bins\n    winRate = [] #initialise before loop\n    rateBinMid = []\n    for j, binInt in enumerate(binList):\n        rateBinMid.append(binInt.mid) #  get mid point of interval\n        temp = dataSample.loc[dataSample['Hbins']==binInt] #slice data based on bin\n        winRate.append(temp['home_result'].sum()/temp['home_result'].count()) #calculate occurence of event\n        homeRatingResults[:, i*bins+j] = [rateBinMid[j], winRate[j]]\nfig, ax = plt.subplots()\nax.plot(homeRatingResults[0,:], homeRatingResults[1,:], 'o')","metadata":{"execution":{"iopub.status.busy":"2022-04-11T17:28:28.423716Z","iopub.execute_input":"2022-04-11T17:28:28.424208Z","iopub.status.idle":"2022-04-11T17:28:28.789422Z","shell.execute_reply.started":"2022-04-11T17:28:28.42417Z","shell.execute_reply":"2022-04-11T17:28:28.788385Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Fit the output**\n\nThese results indicate that match rating does seem to have some predictive power, furthermore, it appears the win probability is some sort of sigmoid function. Let's fit it...","metadata":{}},{"cell_type":"code","source":"from scipy.optimize import curve_fit\nfrom scipy.special import expit\nsigmoid = lambda rating, a, b, c, d : a*expit(b*rating-c)+d\npopt, pcov = curve_fit(sigmoid, homeRatingResults[0,:], homeRatingResults[1,:], [1, 1, 1, 0.1])\nfig, ax = plt.subplots()\nax.plot(homeRatingResults[0,:], homeRatingResults[1,:], 'o', np.arange(-20, 20), sigmoid(np.arange(-20, 20), popt[0], popt[1], popt[2], popt[3]))\nhomeFit = lambda rating : popt[0]*expit(popt[1]*rating-popt[2])+popt[3]","metadata":{"execution":{"iopub.status.busy":"2022-04-11T17:28:36.135822Z","iopub.execute_input":"2022-04-11T17:28:36.136742Z","iopub.status.idle":"2022-04-11T17:28:36.908121Z","shell.execute_reply.started":"2022-04-11T17:28:36.136699Z","shell.execute_reply":"2022-04-11T17:28:36.907107Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Extend to draws and away results**\n\nNow let's do similar excercise for draws and away results. Need to change the fitting functions as they are not the same shape.","metadata":{}},{"cell_type":"code","source":"ratingFrame['draw_result'] = 0\nratingFrame['away_result'] = 0\n# comment out following 2 lines for rating-match pairing (and uncomment following 2 lines)\nratingFrame['draw_result'][ratingFrame['target']=='draw'] = 1\nratingFrame['away_result'][ratingFrame['target']=='away'] = 1\n# comment out following 2 lines for prediction (and uncomment preceding 2 lines)\n#ratingFrame['draw_result'][train['home_team_history_goal_1']==train['home_team_history_opponent_goal_1']] = 1\n#ratingFrame['away_result'][train['home_team_history_goal_1']<train['home_team_history_opponent_goal_1']] = 1\nbins = 15\nnoIts = 5\ndrawRatingResults = np.empty([2, bins*noIts])\nawayRatingResults = np.empty([2, bins*noIts])\nfor i in range(noIts):\n    dataSample = ratingFrame.sample(frac=0.33, random_state=np.random.randint(0, 100))\n    # split data into bins of equal size\n    dataSample['Hbins'] = pd.qcut(dataSample['match_rating'], bins, duplicates='drop')\n    binList = dataSample['Hbins'].unique() #create list of bins\n    drawRate = [] #initialise before loop\n    awayRate = []\n    rateBinMid = []\n    for j, binInt in enumerate(binList):\n        rateBinMid.append(binInt.mid) #  get mid point of interval\n        temp = dataSample.loc[dataSample['Hbins']==binInt] #slice data based on bin\n        drawRate.append(temp['draw_result'].sum()/temp['draw_result'].count()) #calculate occurence of event\n        awayRate.append(temp['away_result'].sum()/temp['away_result'].count()) #calculate occurence of event\n        drawRatingResults[:, i*bins+j] = [rateBinMid[j], drawRate[j]]\n        awayRatingResults[:, i*bins+j] = [rateBinMid[j], awayRate[j]]\n\n        negSigmoid = lambda rating, a, b, c, d : -a*expit(b*rating-c)+d\ngauss = lambda rating, a, b, c, d : a*np.exp(-(rating-b)**2/(2*c**2))+d\n# remove draw extrema\n#drawPopt, pcov = curve_fit(gauss, drawRatingResults[0,:][(drawRatingResults[0,:]>-5)&(drawRatingResults[0,:]<5)], drawRatingResults[1,:][(drawRatingResults[0,:]>-5)&(drawRatingResults[0,:]<5)], [0.2, 0, 5, 0.1])\ndrawPopt, pcov = curve_fit(gauss, drawRatingResults[0,:], drawRatingResults[1,:], [0.2, 0, 5, 0.1])\ndrawFit = lambda rating : drawPopt[0]*np.exp(-(rating-drawPopt[1])**2/(2*drawPopt[2]**2))+drawPopt[3]\nfig, ax = plt.subplots()\nax.plot(drawRatingResults[0,:], drawRatingResults[1,:], 'o', np.arange(-20, 20), drawFit(np.arange(-20, 20)))\nawayPopt, pcov = curve_fit(negSigmoid, awayRatingResults[0,:],awayRatingResults[1,:], [1, 1, 1, 3])\nawayFit = lambda rating : -awayPopt[0]*expit(awayPopt[1]*rating-awayPopt[2])+awayPopt[3]\nfig, ax = plt.subplots()\nax.plot(awayRatingResults[0,:], awayRatingResults[1,:], 'o', np.arange(-20, 20), awayFit(np.arange(-20, 20)))","metadata":{"execution":{"iopub.status.busy":"2022-04-11T17:45:28.681295Z","iopub.execute_input":"2022-04-11T17:45:28.681585Z","iopub.status.idle":"2022-04-11T17:45:29.237187Z","shell.execute_reply.started":"2022-04-11T17:45:28.681549Z","shell.execute_reply":"2022-04-11T17:45:29.236221Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Use functions to make predictions based on match rating**\n\nNext simply use team ratings and inputs into the functions that have been fitted.","metadata":{}},{"cell_type":"code","source":"prediction = test.loc[:, ('id', 'home_team_history_rating_1', 'away_team_history_rating_1')]\nprediction['home_mean_rating'] = test.dropna().apply(meanRateHome, args=[1], axis=1)\nprediction['away_mean_rating'] = test.dropna().apply(meanRateAway, args=[1], axis=1)\nprediction['match_rating'] = prediction['home_mean_rating']-prediction['away_mean_rating']\n#prediction['match_rating'] = prediction['home_team_history_rating_1']-prediction['away_team_history_rating_1']\nprediction['match_rating'] = prediction['match_rating'].fillna(0)\nprediction['home'] = prediction['match_rating'].apply(homeFit)\nprediction['draw'] = prediction['match_rating'].apply(drawFit)\nprediction['away'] = prediction['match_rating'].apply(awayFit)","metadata":{"execution":{"iopub.status.busy":"2022-03-18T13:20:50.700909Z","iopub.execute_input":"2022-03-18T13:20:50.701612Z","iopub.status.idle":"2022-03-18T13:20:52.251579Z","shell.execute_reply.started":"2022-03-18T13:20:50.701551Z","shell.execute_reply":"2022-03-18T13:20:52.25081Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Results**\n\nApplying this gives a score of 1.05556, which is better than the default (all probabilites = 0.33) of 1.09861 and considering the low number of features used a decent improvement.\n","metadata":{}},{"cell_type":"code","source":"prediction[['id', 'home', 'draw', 'away']].to_csv('submission.csv', index=False)","metadata":{},"execution_count":null,"outputs":[]}]}