Applicazioni pratiche di machine learning/Previsioni su scommesse sportive

Indice del libro

CALCIOModifica

Caricamento librerieModifica

 library(dplyr)
 library(caret)

Parte 1: DatiModifica

Dal seguente link: https://www.football-data.co.uk/italym.php è possibile scaricare le probabilità di vincita, pareggio o vittoria in trasferta di vari bookmakers per le partite di calcio di serie A in Italia . In particolare si utilizzano per la previsione nelle scommesse sportive gli anni 2017,2018,2019,2020 .

Caricamento dati:

 I1_17_18 <- read.csv("I1_17_18.csv")
 I1_18_19 <- read.csv("I1_18_19.csv")
 I1_19_20 <- read.csv("I1_19_20.csv")

Creazione colonna "risultato", contenente gli 1,X,2 risultati della corrispondente partita, da aggiungere al dataframe degli anni 2017 e 2018:

 risultato <- rep(NA,nrow(I1_17_18))
 risultato[which(I1_17_18$FTR=="A")]="2"
 risultato[which(I1_17_18$FTR=="D")]="X"
 risultato[which(I1_17_18$FTR=="H")]="1"

 df1<-I1_17_18[,which(colnames(I1_17_18) %in% colnames(I1_19_20))]
 df1 <- cbind(df1,risultato)


Creazione colonna "risultato", contenente gli 1,X,2 risultati della corrispondente partita, da aggiungere al dataframe degli anni 2018 e 2019:

 risultato <- rep(NA,nrow(I1_18_19))
 risultato[which(I1_18_19$FTR=="A")]="2"
 risultato[which(I1_18_19$FTR=="D")]="X"
 risultato[which(I1_18_19$FTR=="H")]="1"

 df2<-I1_18_19[,which(colnames(I1_18_19) %in% colnames(I1_19_20))]
 df2 <- cbind(df2,risultato)


Creazione colonna "risultato", contenente gli 1,X,2 risultati della corrispondente partita, da aggiungere al dataframe degli anni 2019 e 2020:

 risultato <- rep(NA,nrow(I1_19_20))
 risultato[which(I1_19_20$FTR=="A")]="2"
 risultato[which(I1_19_20$FTR=="D")]="X"
 risultato[which(I1_19_20$FTR=="H")]="1"

 df3<-I1_19_20[,which(colnames(I1_19_20) %in% colnames(I1_18_19))]
 df3 <- cbind(df3,risultato)

Si scelgono per la previsione le seguenti variabili contenenti le probabilità di scommessa 1,X,2 sulle partite fatte dai bookmakers:

  • B365H = Bet365 probabilità di vittoria in casa
  • B365D = Bet365 probabilità di pareggio
  • B365A = Bet365 probabilità di vittoria in trasferta
  • BSH = probabilità di vittoria in casa Blue Square
  • BSD = probabilità di pareggio Blue Square
  • BSA = probabilità di vittoria in trasferta Blue Square
  • BWH = Bet&Win vittoria in casa
  • BWD = Bet&Win pareggio
  • BWA = Bet&Win probabilità di vincere in trasferta
  • GBH = probabilità di vittoria in casa di Gamebookers
  • GBD = Gamebookers probabilità di pareggio
  • GBA = Le probabilità di vincere in trasferta di Gamebookers
  • IWH = probabilità di vittoria in casa Interwetten
  • IWD = probabilità di pareggio Interwetten
  • IWA = Interwetten in trasferta
  • LBH = Probabilità di vittoria in casa di Ladbrokes
  • LBD = Ladbrokes probabilità di pareggio
  • LBA = Ladbrokes probabilità di vittoria in trasferta
  • PSH e PH = probabilità di vittoria in casa Pinnacle
  • PSD e PD = probabilità di pareggio di Pinnacle
  • PSA e PA = Pinnacle probabilità di vittoria in trasferta
  • SOH = probabilità di vittoria in casa Sporting Odds
  • SOD = probabilità di pareggio Sporting Odds
  • SOA = probabilità di vittoria in trasferta Sporting Odds
  • SBH = probabilità di vittoria in casa di Sportingbet
  • SBD = probabilità di pareggio di Sportingbet
  • SBA = probabilità di vittoria in trasferta Sportingbet
  • SJH = probabilità di vittoria in casa di Stan James
  • SJD = probabilità di pareggio di Stan James
  • SJA = probabilità di vittoria in trasferta di Stan James
  • SYH = probabilità di vittoria in casa di Stanleybet
  • SYD = probabilità di pareggio di Stanleybet
  • SYA = Probabilità di vittoria in trasferta di Stanleybet
  • VCH = VC probabilità di vincita in casa
  • VCD = VC probabilità di pareggio
  • VCA = VC probabilità di vittoria in trasferta
  • WHH = probabilità di vittoria in casa di William Hill
  • WHD = probabilità di pareggio di William Hill
  • WHA = Probabilità di vittoria in trasferta di William Hill


...e si uniscono i 3 dataframes contenenti le probabilità di vincita in casa, pareggio o trasferta negli anni 2017,2018,2019,2020 :

 df1<-rbind(df1,df2)
 df1<-rbind(df1,df3)

 df1 <- df1[,which(colnames(df1) %in%  c("Date","HomeTeam","AwayTeam","B365H","B365D","B365A","BWH","BWD","BWA","IWH","IWD","IWA","PSH","PSD","PSA","WHH","WHD","WHA","VCH","VCD","VCA","risultato" ))]

 head(df1)

Si eliminano dal dataframe df1 le righe contenenti gli eventuali valori mancanti nelle probabilità...

 df1 <- df1[-which(rowSums(is.na(df1))>0),]


Parte 2 : Domanda di ricercaModifica

Di prescelte partite future di serie A in Italia il risultato sarà 1,X o 2 tramite l'intelligenza artificiale?

Parte 3 : Modellizzazione e PrevisioneModifica

Si suddivide il dataset df1 in un training set costituito dal 90% delle partite e in un testing set costituito dal rimanente 10% di partite e come predictors si scelgono le probabilità indicate dai vari bookmakers escludendo data della partita e nomi delle squadre in casa e trasferta. La variabile da predire è "risultato" .

 n <-nrow(df1)
 training <- df1[1:round(n*0.9),c(4:22)]
 testing <- df1[round(n*0.9):n,c(4:22)]
    
 model <-  train( risultato ~ .,data=training, method="rf", verbose=FALSE )

 p1 <- predict(model,newdata = training)
 print(confusionMatrix(p1,training$risultato)) 

 p1 <- predict(model,newdata = testing)
 print(confusionMatrix(p1,testing$risultato))
Confusion Matrix and Statistics of training set:
         Reference
Prediction   1   2   X
        1  370   0   0
        2    0 272   0
        X    0   0 211
Overall Statistics
                                    
              Accuracy : 1          
                95% CI : (0.9957, 1)
   No Information Rate : 0.4338     
   P-Value [Acc > NIR] : < 2.2e-16  
                                    
                 Kappa : 1          
                                    
Mcnemar's Test P-Value : NA         
Statistics by Class:
                        Class: 1 Class: 2 Class: X
Sensitivity            1.0000   1.0000   1.0000
Specificity            1.0000   1.0000   1.0000
Pos Pred Value         1.0000   1.0000   1.0000
Neg Pred Value         1.0000   1.0000   1.0000
Prevalence             0.4338   0.3189   0.2474
Detection Rate         0.4338   0.3189   0.2474
Detection Prevalence   0.4338   0.3189   0.2474
Balanced Accuracy      1.0000   1.0000   1.0000


Confusion Matrix and Statistics of the testing set:
         Reference
Prediction  1  2  X
        1  27 12  9
        2   5 18  6
        X   5  7  7
Overall Statistics
                                         
              Accuracy : 0.5417          
                95% CI : (0.4369, 0.6438)
   No Information Rate : 0.3854          
   P-Value [Acc > NIR] : 0.001359        
                                         
                 Kappa : 0.29            
                                         
Mcnemar's Test P-Value : 0.250645        
Statistics by Class:
                    Class: 1 Class: 2 Class: X
Sensitivity            0.7297   0.4865  0.31818
Specificity            0.6441   0.8136  0.83784
Pos Pred Value         0.5625   0.6207  0.36842
Neg Pred Value         0.7917   0.7164  0.80519
Prevalence             0.3854   0.3854  0.22917
Detection Rate         0.2812   0.1875  0.07292
Detection Prevalence   0.5000   0.3021  0.19792
Balanced Accuracy      0.6869   0.6500  0.57801

Parte 4: ConclusioniModifica

Utilizzando quindi l'algoritmo Random Forest l'accuratezza della previsione sul training set è del 100%, mentre sul testing set è solo del 54,17% e quindi in pratica partecipare alle scommesse sportive utilizzando il machine learning è approssimativamente come giocare sul rosso e il nero alla Roulette...

TENNISModifica

Caricamento librerieModifica

library(dplyr)
library(ggplot2)
library(h2o)
library(caret)
library(stringr)

Parte 1 : DatiModifica

I dati scaricabili da qui : https://github.com/JeffSackmann/tennis_atp contengono variabili di tutte le partite di tennis ATP giocate dal 1968 in poi . Le variabili che si prendono in considerazione per la previsione del vincitore o del perdente sono :

  • best_of - il numero massimo di set giocati
  • draw_size - la dimensione del disegno
  • match_num - numero partita in un determinato torneo
  • round: il round del torneo a cui appartiene una partita
  • surface - superficie in cui si gioca la partita
  • tourney_id - id del torneo
  • tourney_level - livello del torneo

'G' = Grande Slam 'M' = Master 1000s 'A' = altri eventi a livello di tour 'C' = Sfidanti 'S' = Satelliti/ITF 'F' = finali del tour e altri eventi di fine stagione 'D' = Coppa Davis

  • loser_age - età del giocatore perdente
  • loser_entry - Come è entrato il giocatore perdente nei tornei?

WC - Carattere jolly D - Qualificazione LL - Perdente fortunato PR - Classifica protetta SE - Esenzione speciale ALT - Giocatore alternativo

  • loser_hand - mano del giocatore perdente, destra o sinistra
  • loser_ht - l'altezza del giocatore perdente
  • loser_id - id del giocatore perdente
  • loser_ioc - il paese di origine del giocatore perdente
  • loser_rank - ranking del giocatore perdente
  • loser_seed - semi del giocatore perdente
  • winner_age - età del giocatore vincente
  • winner_entry - Come è entrato il giocatore vincente nei tornei?

WC - Carattere jolly D - Qualificazione LL - Perdente fortunato PR - Classifica protetta SE - Esenzione speciale ALT - Giocatore alternativo

  • winner_hand - mano del giocatore vincente, destra o sinistra
  • winner_ht - l'altezza del giocatore vincente
  • winner_id - id del giocatore vincente
  • winner_ioc - il paese di origine del giocatore vincente
  • winner_rank - ranking del giocatore vincente
  • winner_seed - semi del giocatore vincente


Caricamento dati:

df <- read.csv("ATP.csv")

Parte 2 : Domanda di ricercaModifica

Chi sarà il vincitore o il perdente in una partita di tennis tramite l'intelligenza artificiale ?

Parte 3 : Modellizzazione e PrevisioneModifica

Si creano 2 nuove variabili: mese e anno:

df$year<- as.integer(str_sub(df$tourney_date, start = 1,end = 4))
df$month <-as.integer(str_sub(df$tourney_date, start = 5,end = 6))

Si scelgono le variabili dal dataset sui cui fare l'addestramento del modello:

df <- df[, c(
  'best_of',
  'draw_size',
  'loser_age',
  'loser_entry',
  'loser_hand',
  'loser_ht',
  'loser_id',
  'loser_ioc',
  'loser_rank',
  'loser_seed',
  'match_num',
  'round',
  'surface',
  'tourney_id',
  'tourney_level',
  'winner_age',
  'winner_entry',
  'winner_hand',
  'winner_ht',
  'winner_id',
  'winner_ioc',
  'winner_rank',
  'winner_seed',
  'year',
  'month'
)]

Si rinominano le variabili relative a loser e winner:

colnames(df)[3:10] <- c(
  "first_age",
  "first_entry",
  "first_hand",
  "first_ht",
  "first_id",
  "first_ioc",
  "first_rank",
  "first_seed"
)

colnames(df)[16:23] <- c(
  "second_age",
  "second_entry",
  "second_hand",
  "second_ht",
  "second_id",
  "second_ioc",
  "second_rank",
  "second_seed"
)

Si crea una copia del dataset df in df1 e si scambia il primo con il secondo giocatore:

df1 <- df
df1[, c(
  "first_age",
  "first_entry",
  "first_hand",
  "first_ht",
  "first_id",
  "first_ioc",
  "first_rank",
  "first_seed",
  "second_age",
  "second_entry",
  "second_hand",
  "second_ht",
  "second_id",
  "second_ioc",
  "second_rank",
  "second_seed"
)] <- df1[, c(
  "second_age",
  "second_entry",
  "second_hand",
  "second_ht",
  "second_id",
  "second_ioc",
  "second_rank",
  "second_seed",
  "first_age",
  "first_entry",
  "first_hand",
  "first_ht",
  "first_id",
  "first_ioc",
  "first_rank",
  "first_seed"
)]

Si crea la variabile label da predire . label assume il valore 0 nel dataset df in cui second player vince e assume il valore 1 nel dataset df1 in cui first player vince e si concatenano i 2 due dataset:

df$label=0
df1$label=1
df <- rbind(df,df1)

Si divide il dataset df in un training set fatto dal 75% delle osservazioni e su cui si addestra il modello ed il rimanente 25% costituisce il testing set su cui verrà testato il modello :

df$label <- as.factor(df$label)
trainIndex <- createDataPartition(df$label,p=0.75, list = FALSE)
training <- df[trainIndex,]
testing <- df[-trainIndex,]

Si inizializza la libreria h2o necessaria per automatizzare il machine learning:

h2o.init()
Connection successful!
R is connected to the H2O cluster: 
   H2O cluster uptime:         1 hours 20 minutes 
   H2O cluster timezone:       Europe/Rome 
   H2O data parsing timezone:  UTC 
   H2O cluster version:        3.38.0.1 
   H2O cluster version age:    1 month and 1 day  
   H2O cluster name:           H2O_started_from_R_gian_yty236 
   H2O cluster total nodes:    1 
   H2O cluster total memory:   1.06 GB 
   H2O cluster total cores:    2 
   H2O cluster allowed cores:  2 
   H2O cluster healthy:        TRUE 
   H2O Connection ip:          localhost 
   H2O Connection port:        54321 
   H2O Connection proxy:       NA 
   H2O Internal Security:      FALSE 
   R Version:                  R version 4.2.1 (2022-06-23) 


Si addestra il modello :

df_hf <- as.h2o(training)
y <- "label"
x <- names(training)[1:25]

aml <- h2o.automl(x = x, y = y,
                  training_frame = df_hf,
                  max_runtime_secs =300)


Si ottengono i seguenti modelli per cui il migliore risulta : StackedEnsemble_BestOfFamily_4_AutoML_3_20221020_193936 con un auc=74,21%

lb <- aml@leaderboard
lb
model_id       auc   logloss     aucpr mean_per_class_error      rmse       mse
1 StackedEnsemble_BestOfFamily_4_AutoML_3_20221020_193936 0.7421346 0.5959886 0.7396699            0.3577591 0.4533024 0.2054831
2    StackedEnsemble_AllModels_2_AutoML_3_20221020_193936 0.7420115 0.5958908 0.7398776            0.3588214 0.4532841 0.2054665
3    StackedEnsemble_AllModels_1_AutoML_3_20221020_193936 0.7419540 0.5958249 0.7400070            0.3604438 0.4532571 0.2054420
4 StackedEnsemble_BestOfFamily_2_AutoML_3_20221020_193936 0.7411001 0.5967041 0.7383371            0.3551206 0.4536377 0.2057871
5 StackedEnsemble_BestOfFamily_3_AutoML_3_20221020_193936 0.7407722 0.5971972 0.7378754            0.3517726 0.4538422 0.2059727
6                          GBM_2_AutoML_3_20221020_193936 0.7405898 0.5974683 0.7373835            0.3475135 0.4539269 0.2060496


Si prova il modello sul testing set ottenendo la matrice di confusione e un'accuracy del 64,07% :

test <- as.h2o(testing)
model <- aml@leader
p1 = h2o.predict(model, newdata=test)
confusionMatrix(df2$predict,testing$label)
Confusion Matrix and Statistics
          Reference
Prediction     0     1
         0 17099  5159
         1 25323 37263
                                        
              Accuracy : 0.6407         
                95% CI : (0.6375, 0.644)
   No Information Rate : 0.5            
   P-Value [Acc > NIR] : < 2.2e-16      
                 Kappa : 0.2815         
Mcnemar's Test P-Value : < 2.2e-16      
           Sensitivity : 0.4031         
           Specificity : 0.8784         
        Pos Pred Value : 0.7682         
        Neg Pred Value : 0.5954         
            Prevalence : 0.5000         
        Detection Rate : 0.2015         
  Detection Prevalence : 0.2623         
     Balanced Accuracy : 0.6407         

Parte 4 : Previsioni con PythonModifica

Di seguito si utilizza il codice contenuto in questo Notebook : https://www.kaggle.com/code/alexisbcook/pipelines rilasciato con licenza Apache 2.0 .

import pandas as pd
from sklearn.model_selection import train_test_split

# Caricamento dei dati
data = pd.read_csv('tennis.csv')

# Separo la variabile da predire label dai predictors
y = data.label
X = data.drop(['label'], axis=1)

# Divido i dati in training e validation subsets, il primo con il 75% delle partite e il secondo con il 25%
X_train_full, X_valid_full, y_train, y_valid = train_test_split(X, y, train_size=0.75, test_size=0.25,random_state=0)

# Seleziono le colonne con valori categoriali in numero minore di 10
categorical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].nunique() < 10 and 
                        X_train_full[cname].dtype == "object"]

# Seleziono le colonne numeriche
numerical_cols = [cname for cname in X_train_full.columns if X_train_full[cname].dtype in ['int64', 'float64']]

# Mantengo soltanto le colonne selezionate
my_cols = categorical_cols + numerical_cols
X_train = X_train_full[my_cols].copy()
X_valid = X_valid_full[my_cols].copy()

Le colonne numeriche e categoriche nel dataset che hanno valori mancanti NA vengono trasformate con valori approssimativi nei NA tramite SimpleImputer, mentre le variabili categoriche vengono trasformate in numeriche tramite OneHotEncoder :

from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder

# Preprocessing per i dati numerici
numerical_transformer = SimpleImputer(strategy='constant')

# Preprocessing per i dati categoriali
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='most_frequent')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))
])


preprocessor = ColumnTransformer(
    transformers=[
        ('num', numerical_transformer, numerical_cols),
        ('cat', categorical_transformer, categorical_cols)
    ])

Per la previsione utilizzo l'algoritmo XGBoost (Extreme Gradient Boosting) e addestro il modello sul training set dopo avere preprocessato i dati con il preprocessor precedentemente creato. Infine prevedo i dati sul validation set.

from xgboost import XGBClassifier

model = XGBClassifier(learning_rate=0.05)

my_pipeline = Pipeline(steps=[('preprocessor', preprocessor),
                              ('model', model)
                             ])

# addestro il modello 
my_pipeline.fit(X_train, y_train)

# ottengo le previsioni sul validation set
preds = my_pipeline.predict(X_valid)

Sul validation set ottengo un'accuracy del 68% :

from sklearn.metrics import classification_report
 
print(classification_report(y_valid, preds))
             precision    recall  f1-score   support
           0       0.68      0.67      0.68     42670
           1       0.67      0.68      0.67     42175
    accuracy                           0.68     84845
   macro avg       0.68      0.68      0.68     84845
weighted avg       0.68      0.68      0.68     84845

Parte 5: ConclusioniModifica

Rispetto al calcio, nel tennis si ottiene un'accuracy maggiore e quindi una previsione migliore ma non sufficiente per considerare le previsioni su scommesse sportive tramite machine learning un buon investimento.