Applicazioni pratiche di machine learning/Previsioni su scommesse sportive
CALCIO
modificaCaricamento librerie
modifica library(dplyr)
library(caret)
Parte 1: Dati
modificaDal 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 ricerca
modificaDi prescelte partite future di serie A in Italia il risultato sarà 1,X o 2 tramite l'intelligenza artificiale?
Parte 3 : Modellizzazione e Previsione
modificaSi 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: Conclusioni
modificaUtilizzando 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...
TENNIS
modificaCaricamento librerie
modificalibrary(dplyr)
library(ggplot2)
library(h2o)
library(caret)
library(stringr)
Parte 1 : Dati
modificaI 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 ricerca
modificaChi sarà il vincitore o il perdente in una partita di tennis tramite l'intelligenza artificiale ?
Parte 3 : Modellizzazione e Previsione
modificaSi 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 Python
modificaDi 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: Conclusioni
modificaRispetto 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.
FORMULA 1 (Gran Premi)
modificaParte 1: Dati
modificaDal seguente link: http://ergast.com/mrd/ è possibile scaricare i dataset dei Gran Premi di Formula 1 con dati dal 1950 al 2023 e predire tramite algoritmi di machine learning il pilota vincente con un'Accuracy del 99% sul testing set.
Caricamento dati:
import pandas as pd
# Tabella riassuntiva dei risultati:
result_df = pd.read_csv('results.csv')
# Stato del pilota:
stats_df = pd.read_csv('status.csv')
# Piloti :
drivers_df = pd.read_csv('drivers.csv')
# Gran Premi:
races_df = pd.read_csv('races.csv')
# Case automobilistiche :
constructor_df = pd.read_csv('constructors.csv')
driver_standings_df = pd.read_csv('driver_standings.csv')
# unisco i datasets in un singolo dataset df :
con1 = pd.merge(result_df, races_df, on ='raceId')
con2 = pd.merge(con1, drivers_df, on = 'driverId')
con3 = pd.merge(con2, driver_standings_df, on = 'driverId')
con4 = pd.merge(con3, constructor_df, on ='constructorId')
df = pd.merge(con4, stats_df, on ='statusId')
df.head()
# Le variabili sono le seguenti:
df.info()
Come si vede, ci sono 3.475.793 righe e 37 colonne nel dataset df :
<class 'pandas.core.frame.DataFrame'> Int64Index: 3475793 entries, 0 to 3475792 Data columns (total 37 columns): # Column Dtype --- ------ ----- 0 resultId int64 1 raceId_x int64 2 driverId int64 3 constructorId int64 4 number_x object 5 grid int64 6 position_x object 7 positionText_x object 8 positionOrder int64 9 points_x float64 10 laps int64 11 time object 12 milliseconds object 13 fastestLap object 14 rank object 15 fastestLapTime object 16 fastestLapSpeed object 17 statusId int64 18 driverRef object 19 number_y object 20 code object 21 forename object 22 surname object 23 dob object 24 nationality_x object 25 url_x object 26 driverStandingsId int64 27 raceId_y int64 28 points_y float64 29 position_y int64 30 positionText_y object 31 wins int64 32 constructorRef object 33 name object 34 nationality_y object 35 url_y object 36 status object dtypes: float64(2), int64(12), object(23) memory usage: 1007.7+ MB
# cancello alcune variabili :
df = df.drop(['url_y','url_x','position_x','fastestLapTime','positionText_x','driverRef',
'constructorRef','nationality_y','positionText_y','raceId_y','points_y'],1)
# Creo la variabile driver_name col nome e cognome del pilota, che verrà prevista dall'algoritmo:
df['driver_name'] = df['forename']+' '+df['surname']
df = df.drop(['forename','surname'],1)
# converto alcune variabili di tipo object in variabili numeriche :
l = ['number_x','milliseconds','fastestLap','rank','fastestLapSpeed','number_y']
for i in l:
df[i] = pd.to_numeric(df[i],errors='coerce')
Considero soltanto i piloti in stato finished, cioè quelli che hanno percorso almeno il 90% dei giri del Gran Premio, quindi su 60 giri ne hanno fatti almeno 54. In tal modo riduco il dataset a 1.320.987 righe:
df = df[df['status'] == 'Finished']
df.shape
(1320987, 42)
Parte 2: Previsioni
modificafrom sklearn.model_selection import train_test_split
# Separo la variabile da predire driver_name dai predictors
y = df.driver_name
X = df.drop(['driver_name'], axis=1)
# Divido i dati in training e validation subsets, il primo con il 70% delle partite e il secondo con il 30%
X_train_full, X_valid_full, y_train, y_valid = train_test_split(X, y, test_size=0.3,
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 LabelEncoder :
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
label_encoder = LabelEncoder()
for i in categorical_cols:
X_train[i]=label_encoder.fit_transform(X_train[i])
X_valid[i] = label_encoder.fit_transform(X_valid[i])
my_imputer = SimpleImputer()
X_train[numerical_cols] = my_imputer.fit_transform(X_train[numerical_cols])
X_valid[numerical_cols] = my_imputer.transform(X_valid[numerical_cols])
Per la previsione utilizzo l'algoritmo KNeighbors (il più semplice degli algoritmi di machine learning) e addestro il modello sul training set. Infine prevedo i dati sul validation set:
from sklearn.neighbors import KNeighborsClassifier
model= KNeighborsClassifier()
model.fit(X_train,y_train)
ypred = model.predict(X_valid)
Sul validation set ottengo un'accuracy del 99% :
from sklearn.metrics import classification_report
print(classification_report(y_valid, preds))
accuracy 0.99 396297 macro avg 0.99 0.98 0.98 396297 weighted avg 0.99 0.99 0.99 396297
Parte 3: Conclusioni
modificaSebbene l'accuracy previsionale sia del 99%, il modello non prevede eventuali incidenti, guasti o cattive condizioni climatiche quindi in realtà l'accuratezza è minore...