Sette e Mezzo: analisi probabilità e strategie con Python¶
Vogliamo studiare quali sono le probabilità che si hanno di vincere a sette e mezzo e quale può essere una probabile strategia di gioco ottimale. Abbiamo un set di carte, fatto cosi:
$$ [\frac12,1,2,3,4,5,6,7] $$
la cui cardinalità di ogni elemento nel set è la seguente:
$$ [12,4,4,4,4,4,4,4] $$
infatti abbiamo esattamente 12 mezzi e per tutto il resto dato che ci sono 4 semi, avremo cardinalità 4.
import numpy as np
carte_nap = np.array([0.5] * 12 + [1] * 4 + [2] * 4 + [3] * 4 + [4] * 4 + [5] * 4 + [6] * 4 + [7] * 4) # ricorda: qui l'operatore + concatena le liste che creo
#sto creando delle liste, [elementi_lista] * numero_liste_da_generare
print(carte_nap)
[0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 1. 1. 1. 1. 2. 2. 2. 2. 3. 3. 3. 3. 4. 4. 4. 4. 5. 5. 5. 5. 6. 6. 6. 6. 7. 7. 7. 7. ]
adesso però dobbiamo mischiare le carte.Scrivo un algoritmo che fa questo: genera un array da 40 zeri, zero lo uso come placeholder, per sapere che in quella posizione ancora non inserisco una carta. Poi itero l'array creato prima, carte_nap, e scorrendolo, dal primo elemento, scelgo una posizione random nell'array nuovo, dove non c'è 0 e ci scrivo il valore di quella carta su cui sto iterando. A livello di complessità temporale non è affatto buono come algoritmo perchè quando il coefficiente di carico dell'array mescolato inizia ad alzarsi troppo(cioè si riempie l'array), devo generare tanti numeri casuali fino a "beccare" quello libero. Ma visto che sono solo 40 carte direi che possiamo ignorare l'ottimizzazione e procedere con questo algoritmo
#algoritmo di mescolazione carte
arr = np.array([0.0]*40) #genero un array da 40 zeri
for i in range(0,40):
trovato = False
rd_pos = 0
while(trovato == False):
rd_pos = np.random.randint(0,40)
if(arr[rd_pos] == 0):
arr[rd_pos] = carte_nap[i]
trovato=True
print(arr)
#in alternativa ovviamente ci sono dei metodi come shuffle della libreria random
[5. 5. 0.5 0.5 6. 2. 4. 0.5 1. 7. 0.5 0.5 7. 4. 7. 5. 0.5 3. 0.5 4. 0.5 0.5 1. 3. 4. 6. 1. 0.5 2. 2. 0.5 1. 3. 5. 6. 7. 2. 0.5 6. 3. ]
Adesso vediamo se non abbiamo fatto errori nell'algoritmo e controlliamo se effettivamente ci sono le stesse carte: contiamo quante carte per ogni valore ci sono. Usiamo la funzione count_nonzero() di numpy. Ricorda che con arr == VALORE, non confronti solo il primo elemento, ma confronti ogni elemento dell'array con 2 in modo vettoriale. Non è come in C, dove arr rappresenta il puntatore al primo elemento dell'array nello stack
print("Mezzi"+":"+str(np.count_nonzero(arr == 0.5)))
for i in range(1,8): #8 perchè ricordiamoci che la range(x,y) arriva a y-1
print("Carta " + str(i) + ":"+str(np.count_nonzero(arr == float(i))) )
Mezzi:12 Carta 1:4 Carta 2:4 Carta 3:4 Carta 4:4 Carta 5:4 Carta 6:4 Carta 7:4
questo era un controllo aggiuntivo e come si può vedere ha avuto esito positivo. Adesso che abbiamo un mazzo ben mescolato iniziamo a studiare le probabilità.Potremmo generare un DataFrame che ha su ogni riga una giocata e se il giocatore ha "sforato" o meno. Sulla base di questo andremo a costruire dei modelli che predicono l'andamento di una giocata. Dopo di che vedremo il MSE(Mean Square Error), Confusion Matrix, F1 score e altri criteri per vedere quanto il modello è affidabile.
Ci conviene poi splittare il dataset in uno di training e uno di test.
import numpy as np
import pandas as pd
class Giocata:
def __init__(self):
self.mazzo = arr.tolist() # Converte da numpy array a lista
self.initial_card = self.mazzo.pop(np.random.randint(0, len(self.mazzo))) # Estrazione senza reinserimento
self.initial_card_banco = self.mazzo.pop(np.random.randint(0, len(self.mazzo))) # Estrazione senza reinserimento
self.esito = ""
a1, num1 = self.gioca(self.initial_card)
a2, num2 = self.gioca(self.initial_card_banco)
if a1 > 7.5 and a2 > 7.5:
self.esito = "sconfitta" # Vince il banco se entrambi sballano
elif a1 > 7.5:
self.esito = "sballato"
elif a2 > 7.5 and a1 <= 7.5:
self.esito = "vittoria"
elif a1 > a2 and a1 <= 7.5:
self.esito = "vittoria"
elif a1 < a2 and a2 <= 7.5:
self.esito = "sconfitta"
elif a1 == a2:
self.esito = "sconfitta" # Il banco vince in caso di pareggio
self.amount = a1
self.num_card = num1
def gioca(self, amount):
num_of_cards = 0
while(amount <= 7.5):
random_choice = np.random.randint(0, 2) # 0 -> stare, 1 -> chiedere carta
if random_choice == 1: # Se decidiamo di chiedere una carta
new_card = self.mazzo.pop(np.random.randint(0, len(self.mazzo))) # Estrazione senza reinserimento
amount += new_card
num_of_cards += 1
if(amount > 7.5):
break
else: # Se decidiamo di stare, ci fermiamo
break
return amount, num_of_cards # Ritorno l'importo finale e il numero di carte estratte
#simuliamo 10 giocate
for i in range(1,10):
g = Giocata()
print(g.esito+" "+str( g.amount )+" num card:"+str(g.num_card))
#adesso siamo pronti a generare il dataframe
vittoria 2.5 num card:3 sballato 10.5 num card:2 sconfitta 3.5 num card:1 vittoria 7.0 num card:0 sconfitta 5.0 num card:0 sconfitta 8.0 num card:1 sconfitta 2.0 num card:0 sconfitta 5.0 num card:1 vittoria 7.5 num card:2
Adesso voglio fare una prova, per esercizio voglio vedere che la probabilità di ciascuna carta sia uguale, quindi voglio fare n estrazioni e vedere quante volte ogni carta è uscita. So che sono tutte indipendenti, quindi posso semplicemente creare un array con ogni valore di carta e per ogni valore carta vedo quante volte è uscito, senza considerare il fatto che nel sette e mezzo non c'è reinserimento(ogni volta che esce una carta viene rimossa dal mazzo):
import random
# Inizializzo il dizionario con le carte valide
valori_carte = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0}
# Definisco valori e pesi in base alla composizione reale del mazzo
valori = [0] * 12 + [1, 2, 3, 4, 5, 6, 7] * 4
# Simulo N estrazioni
N = 1000000
estrazioni = random.choices(valori, k=N)
# Aggiorno il conteggio
for e in estrazioni:
valori_carte[e] += 1
# Mostro il risultato finale
print(valori_carte)
{0: 299705, 1: 100163, 2: 100011, 3: 100118, 4: 100043, 5: 99966, 6: 100521, 7: 99473}
import matplotlib.pyplot as plt
plt.bar(valori_carte.keys(),valori_carte.values(),color="black")
<BarContainer object of 8 artists>
ovviamente, è venuto fuori che ogni carta esce lo stesso numero di volte(tranne le mezze che ovviamente sono in numero maggiore). Qui ogni estrazione è equiprobabile, indipendente e identicamente distribuita perchè c'è reinserimento. Qui spesso le persone potrebbero pensare una cosa, errata, cioè che se una carta non esce da tanto allora ha più probabilità di uscire per la "legge dei grandi numeri". La Legge Debole dei Grandi Numeri(Weak Law of Large Numbers, WLLN) afferma in realtà il contrario, cioè che la media campionaria (qiundi di un campione della popolazione) converge in probabilità al valore atteso(expected value) della variabile aleatoria. Vediamo bene cosa significa: posso considerare ogni estrazione di una certa carta k come una variabile aleatoria di Bernoulli, quindi una variabile che vale 1 con probabilità p(successo) e vale 0 con probabilità 1-p. Ogni estrazione non dipende dalla precedente,perchè il fatto che una certa carta k sia uscita o meno in passato non influenza la probabilità che esca nella prossima estrazione, come potevamo infatti aspettarci. Infatti il valore atteso E[X] della bernoulli è proprio la probabilità p, che è qui 1/8 con le dovute semplificazioni rispetto al fatto che ci siano più mezze nel gioco reale. Dimostriamo adesso che vale la WLLN
n_estrazioni = 10000
valori = np.random.choice([0.5, 1, 2, 3, 4, 5, 6, 7], size=n_estrazioni)
# Frequenze cumulative
frequenze = [np.sum(valori[:i] == 7) / i for i in range(1, n_estrazioni + 1)]
#sta tracciando la frequenza campionaria dell'uscita della carta 7
# Plot
plt.plot(frequenze, label='Frequenza della carta 7')
plt.axhline(1/8, color='red', linestyle='--', label='Probabilità teorica (12.5%)')
plt.title('Convergenza alla probabilità teorica')
plt.xlabel('Numero di estrazioni')
plt.ylabel('Frequenza')
plt.legend()
plt.show()
#attenzione: qui stiamo considerando 8 carte, non consideriamo il fatto che ci siano più mezze. Serve solo a dimostrare che c'è indipendenza tra i lanci
# e che quindi ogni carta ha la stessa probabilità di uscire
Tuttavia non abbiamo considerato che nel gioco ogni volta che esce una carta, viene rimossa dal mazzo, quindi se esce una carta, ad esempio un 5, la probabilità che la prossima sia anche un 5 diminuisce, perchè lo spazio campionario è stato condizionato da quell'evento, quindi ci sarà un 5 in meno nel mazzo. Vediamo allora come cambiano le probabilità sapendo questa cosa.
# Eseguiamo molte simulazioni
simulazioni = 1000000 # Numero di simulazioni
esiti = []
amounts = []
num_cards = []
for _ in range(simulazioni):
g = Giocata()
esiti.append(g.esito)
amounts.append(g.amount)
num_cards.append(g.num_card)
# Creiamo un DataFrame per analizzare i dati
df = pd.DataFrame({
'Esito': esiti,
'Amount': amounts,
'Numero_Carte': num_cards
})
# Mostriamo alcune statistiche
print(df['Esito'].value_counts()) # Conta quante vittorie, sconfitte, sballati
print("Media carte in più chieste:",df['Numero_Carte'].mean()) # Media delle carte utilizzate
totale_perdita = df[df["Esito"]=="sballato"].shape[0] +df[df["Esito"]=="sconfitta"].shape[0]
plt.figure(figsize=(10, 6))
plt.bar(['Vittoria',"Sconfitte Totali"],[df[df["Esito"]=="vittoria"].shape[0],totale_perdita], color=["green","red"])
# Plot dei risultati
# Frequenza degli esiti
plt.figure(figsize=(10, 6))
df['Esito'].value_counts().plot(kind='bar', color=['green', 'red', 'blue'])
plt.title('Distribuzione degli esiti delle giocate')
plt.xlabel('Esito')
plt.ylabel('Frequenza')
plt.xticks(rotation=0)
plt.show()
# Distribuzione degli importi finali
plt.figure(figsize=(10, 6))
plt.hist(df['Amount'], bins=30, color='purple', edgecolor='black')
plt.title('Distribuzione del valore cumulato delle carte a fine giocata')
plt.xlabel('Importo finale')
plt.ylabel('Frequenza')
plt.show()
# Distribuzione del numero di carte utilizzate
plt.figure(figsize=(10, 6))
plt.hist(df['Numero_Carte'], bins=10, color='orange', edgecolor='black')
plt.title('Distribuzione del numero di carte utilizzate')
plt.xlabel('Numero di carte')
plt.ylabel('Frequenza')
plt.show()
Esito vittoria 444502 sconfitta 365117 sballato 190381 Name: count, dtype: int64 Media carte in più chieste: 0.74605
Adesso vediamo come utilizzare una regressione lineare per predire l'andamento di una giocata.Ovviamente non è un modello preciso e neanche il più adatto per questo scopo, in realtà era necessario un modello di classificazione, ma per semplificare abbiamo usato una regressione lineare facendo predire un numero che, almeno in teoria, più è vicino a 1 e più probabilità di vincere si ha.Il tutto è a solo scopo di esercizio. Sono state fatte diverse semplificazioni.
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.metrics import accuracy_score
# Simulazioni già eseguite (assumiamo che 'df' sia già disponibile)
# Codifica dell'esito in numerico
df['Esito_numerico'] = df['Esito'].map({'vittoria': 1, 'sconfitta': 0, 'sballato': -1})
# Variabili indipendenti (feature)
X = df[['Amount', 'Numero_Carte']] # Puoi aggiungere altre variabili se necessario
# Variabile dipendente (target)
y = df['Esito_numerico']
# Divisione in dati di addestramento e test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
# Creazione del modello di regressione lineare
model = LinearRegression()
# Allenamento del modello
model.fit(X_train, y_train)
# Predizioni sui dati di test
y_pred = model.predict(X_test)
# Calcolo dell'errore medio quadratico (MSE)
mse = mean_squared_error(y_test, y_pred)
print("Errore medio quadratico (MSE):", mse)
# Visualizzazione dei coefficienti del modello
print("Coefficienti del modello:", model.coef_)
print("Intercetta:", model.intercept_)
# Predizioni su tutte le simulazioni
df['predizione'] = model.predict(X)
# Calcola l'accuratezza
print("Percentuale Predizioni giuste con predizione>0.1: "+str((df[(df["Esito"] == "vittoria") & (df["predizione"] > 0.2)].shape[0] / df[df["predizione"]>0.2].shape[0] )*100) + "%" )
# df[df["predizione"]>0.7].shape[0]
# Analizziamo le prime 5 predizioni
pd.set_option('display.max_rows', 100)#togliamo il limite di visualizzazione righe e lo mettiamo a 100
print(df[['Amount', 'Numero_Carte', 'Esito', 'predizione']].head(10))
Errore medio quadratico (MSE): 0.43654303303503633 Coefficienti del modello: [-0.07789824 -0.12903786] Intercetta: 0.7531272934110613 Percentuale Predizioni giuste con predizione>0.1: 54.319150822499665% Amount Numero_Carte Esito predizione 0 9.0 2 sballato -0.206033 1 4.0 0 sconfitta 0.441534 2 6.0 0 vittoria 0.285738 3 0.5 0 vittoria 0.714178 4 14.0 1 sballato -0.466486 5 3.5 1 sconfitta 0.351446 6 2.0 0 vittoria 0.597331 7 12.0 1 sballato -0.310689 8 1.0 0 vittoria 0.675229 9 1.5 1 sconfitta 0.507242
Ricordiamoci che l'intercetta in una regressione lineare è il valore di y quando tutte le variabili indipendenti (le x) sono uguali a zero. In altre parole, rappresenta il punto in cui la retta di regressione incrocia l'asse delle ordinate (y).
L'Errore Quadratico Medio (MSE), o Mean Squared Error, è una misura di valutazione della performance di un modello di regressione. Esso calcola la media degli errori al quadrato tra i valori predetti dal modello e i valori reali.
La formula del MSE è:
$$ MSE = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 $$
Dove:
- $y_i$ è il valore reale (osservato) per l'osservazione $i$,
- $\hat{y}_i$ è il valore predetto per l'osservazione $i$,
- $n$ è il numero totale di osservazioni.
Cosa rappresenta il MSE?¶
- MSE basso: Indica che il modello sta facendo buone previsioni, in quanto gli errori tra i valori predetti e quelli reali sono piccoli.
- MSE alto: Indica che il modello sta facendo errori più grandi nelle previsioni, quindi potrebbe non essere molto preciso.
Perché si usa l'errore quadratico?¶
L'errore viene elevato al quadrato per penalizzare maggiormente gli errori più grandi. In altre parole, gli errori più significativi influenzano molto di più il valore del MSE rispetto a quelli minori. Questo aiuta a evitare che piccole discrepanze vengano ignorate quando si ottimizza il modello.
Proviamo ad utilizzare un modello di regressione logistica:
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.metrics import f1_score
# Codifica dell'esito in numerico
df['Esito_numerico'] = df['Esito'].map({'vittoria': 1, 'sconfitta': 0, 'sballato': -1})
# Variabili indipendenti (feature)
X = df[['Amount', 'Numero_Carte']] # Puoi aggiungere altre variabili se necessario
# Variabile dipendente (target)
y = df['Esito_numerico']
# Divisione in dati di addestramento e test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
# Creazione del modello di regressione logistica
model = LogisticRegression()
# Allenamento del modello
model.fit(X_train, y_train)
# Predizioni sui dati di test
y_pred = model.predict(X_test)
# Calcoliamo l'accuratezza
accuracy = accuracy_score(y_test, y_pred)
print("Accuratezza della regressione logistica:", accuracy)
# Matrice di confusione
cm = confusion_matrix(y_test, y_pred)
print("Matrice di confusione:")
print(cm)
# Calcola l'F1 Score
f1 = f1_score(y_test, y_pred, average='weighted') # Puoi usare 'macro' o 'weighted' per bilanciare le classi
print("Punteggio F1:", f1)
#Significato dell'argomento average:
# • 'macro': Calcola l'F1 Score per ciascuna classe e poi fa la media. Non considera l'imbalanciamento delle classi.
# • 'weighted': Calcola l'F1 Score per ciascuna classe e poi fa la media ponderata, pesando le classi in base al loro numero. È utile se le classi sono sbilanciate.
# • 'micro': Calcola globalmente il punteggio F1 considerando il numero totale di veri positivi, falsi positivi e falsi negativi.
# Analizziamo le prime 10 predizioni
df['predizione_logistica'] = model.predict(X)
print(df[['Amount', 'Numero_Carte', 'Esito', 'predizione_logistica']].head(10))
Accuratezza della regressione logistica: 0.67336 Matrice di confusione: [[15707 0 3487] [ 5014 18303 13119] [ 67 10977 33326]] Punteggio F1: 0.667372678610514 Amount Numero_Carte Esito predizione_logistica 0 9.0 2 sballato -1 1 4.0 0 sconfitta 1 2 6.0 0 vittoria 1 3 0.5 0 vittoria 0 4 14.0 1 sballato -1 5 3.5 1 sconfitta 1 6 2.0 0 vittoria 1 7 12.0 1 sballato -1 8 1.0 0 vittoria 0 9 1.5 1 sconfitta 0
Matrice di Confusione: Cos'è e Come si Interpreta¶
La matrice di confusione è uno strumento utile per valutare le prestazioni di un modello di classificazione. Mostra il numero di predizioni corrette e errate suddivise per ciascuna classe, facilitando l'interpretazione delle performance del modello.
Cos'è la matrice di confusione?¶
La matrice di confusione è una tabella che descrive il comportamento del modello, con le righe che rappresentano le classi reali e le colonne che rappresentano le classi predette. La matrice di confusione ha generalmente la seguente struttura:
Predetto Positivo (1) | Predetto Negativo (0) | |
---|---|---|
Reale Positivo (1) | True Positive (TP) | False Negative (FN) |
Reale Negativo (0) | False Positive (FP) | True Negative (TN) |
Dove:
- True Positive (TP): il numero di veri positivi, ovvero i casi in cui il modello ha correttamente predetto la classe positiva.
- False Positive (FP): il numero di falsi positivi, cioè i casi in cui il modello ha erroneamente predetto la classe positiva quando in realtà era negativa.
- True Negative (TN): il numero di veri negativi, ovvero i casi in cui il modello ha correttamente predetto la classe negativa.
- False Negative (FN): il numero di falsi negativi, cioè i casi in cui il modello ha erroneamente predetto la classe negativa quando in realtà era positiva.
Come si usa la matrice di confusione?¶
La matrice di confusione viene utilizzata per calcolare diverse metriche di performance che ci aiutano a capire come si comporta il modello. Ecco alcune delle principali metriche calcolabili dalla matrice di confusione:
Accuratezza (Accuracy): La precisione complessiva del modello, cioè la percentuale di previsioni corrette. Si calcola come: $$ \text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN} $$
Precisione (Precision): La precisione misura la percentuale di predizioni positive corrette sul totale delle predizioni positive effettuate dal modello. Si calcola come: $$ \text{Precision} = \frac{TP}{TP + FP} $$
Recall (Sensibilità o True Positive Rate): Il recall misura la percentuale di veri positivi identificati dal modello rispetto al totale dei veri positivi presenti nel dataset. Si calcola come: $$ \text{Recall} = \frac{TP}{TP + FN} $$
F1-Score: L'F1-Score è la media armonica tra precisione e recall e viene usato per bilanciare entrambe le misure, specialmente quando il dataset è sbilanciato. Si calcola come: $$ F1 = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}} $$
Come interpretare la matrice di confusione?¶
- True Positives (TP): Più alto è questo valore, migliore è il modello nell'identificare correttamente la classe positiva.
- False Positives (FP): Un alto numero di FP indica che il modello tende a etichettare come positiva una classe che è effettivamente negativa. Questo può essere problematico se il modello è usato in contesti dove le false allarmate sono dannose (ad esempio, nel caso di diagnosi mediche).
- True Negatives (TN): Più alto è questo valore, migliore è il modello nel classificare correttamente la classe negativa.
- False Negatives (FN): Un alto numero di FN significa che il modello sta trascurando esempi positivi reali, il che potrebbe essere problematico se una falsa negazione ha un grande impatto (ad esempio, in situazioni di rilevamento di frodi).
Cos'è il punteggio F1?¶
Il punteggio F1 è una misura di valutazione delle prestazioni di un modello di classificazione, particolarmente utile quando si lavora con classi sbilanciate (ad esempio, quando una classe è molto meno frequente dell'altra). È la media armonica tra precisione e recall ed è usato per bilanciare i due aspetti, in quanto si vuole ottenere una buona performance sia nel classificare correttamente gli esempi positivi (precisione) sia nell'identificare tutti i veri esempi positivi (recall).
Cos'è il punteggio F1?¶
- Precisione (Precision): La precisione misura la percentuale di vere predizioni positive rispetto a tutte le predizioni positive fatte dal modello. È calcolata come:
$$ \text{Precision} = \frac{TP}{TP + FP} $$
Dove:
(TP) = True Positives (veri positivi)
(FP) = False Positives (falsi positivi)
Recall (o Sensibilità): Il recall misura la percentuale di veri positivi rispetto a tutti gli esempi positivi effettivi nel dataset. È calcolato come:
$$ \text{Recall} = \frac{TP}{TP + FN} $$
Dove:
(TP) = True Positives (veri positivi)
(FN) = False Negatives (falsi negativi)
F1 Score: Il punteggio F1 è la media armonica della precisione e del recall ed è definito come:
$$ F1 = 2 \times \frac{\text{Precision} \times \text{Recall}}{\text{Precision} + \text{Recall}} $$
Un punteggio F1 elevato indica un buon equilibrio tra precisione e recall.
Quando usare il punteggio F1 e come si interpreta?¶
Quando hai un dataset sbilanciato (ad esempio, un numero molto maggiore di esempi di una classe rispetto all'altra), l'accuratezza potrebbe non essere il miglior indicatore delle prestazioni del modello. In questi casi, il punteggio F1 ti aiuta a capire meglio come il modello sta gestendo le classi minoritarie.
Un punteggio F1 alto indica che il modello sta classificando correttamente sia le osservazioni positive che quelle negative.
Quando F1 è prossimo a 1 vuol dire che il modello sta facendo un buon lavoro e quindi le previsioni sono corrette.Se l'F1 Score è basso (ad esempio sotto 0.5), potrebbe significare che il modello sta facendo molte predizioni sbagliate, magari a causa di un'alta quantità di falsi positivi o falsi negativi
Come funziona il nostro modello¶
In pratica noi diamo in input lo stato attuale della giocata(l'amount, cioè il totale realizzato e il numero di carte chieste) e il modello ci dirà se quella giocata è vincente o meno.Adesso vediamo se conviene chiedere una carta dato il numero di carte chieste e il valore cumulato:
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
# Variabili indipendenti (feature)
X = df[['Amount', 'Numero_Carte']]
# Variabile dipendente (target)
y = df['Esito_numerico']
# Divisione in dati di addestramento e test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1, random_state=42)
# Creazione del modello di regressione logistica
model = LogisticRegression()
# Allenamento del modello
model.fit(X_train, y_train)
# Predizioni sui dati di test
y_pred = model.predict(X_test)
y_proba = model.predict_proba(X_test)[:, 1] # Probabilità di vittoria
# Accuracy del modello
accuracy = accuracy_score(y_test, y_pred)
print("Accuratezza:", accuracy)
# Confusion Matrix
cm = confusion_matrix(y_test, y_pred)
print("Matrice di confusione:\n", cm)
# Classification Report
print(classification_report(y_test, y_pred))
# Aggiunta delle predizioni nel dataframe
df['predizione'] = model.predict(X)
df['probabilità_vittoria'] = model.predict_proba(X)[:, 1]
# Suggerimento decisionale: conviene chiedere carta se probabilità > 0.5
df['conviene_chiedere'] = (df['probabilità_vittoria'] > 0.5).astype(int)
# Visualizziamo le prime righe
print(df[['Amount', 'Numero_Carte', 'Esito_numerico', 'probabilità_vittoria', 'conviene_chiedere']].head(10))
Accuratezza: 0.67401 Matrice di confusione: [[15554 0 3556] [ 5141 18535 12782] [ 75 11045 33312]] precision recall f1-score support -1 0.75 0.81 0.78 19110 0 0.63 0.51 0.56 36458 1 0.67 0.75 0.71 44432 accuracy 0.67 100000 macro avg 0.68 0.69 0.68 100000 weighted avg 0.67 0.67 0.67 100000 Amount Numero_Carte Esito_numerico probabilità_vittoria \ 0 10.5 2 -1 0.063713 1 1.0 0 0 0.523429 2 2.0 2 1 0.604134 3 1.0 1 1 0.582138 4 11.5 3 -1 0.024278 5 8.0 1 -1 0.249737 6 5.0 0 1 0.376649 7 7.0 0 1 0.288231 8 7.0 0 1 0.288231 9 7.5 1 1 0.287026 conviene_chiedere 0 0 1 1 2 1 3 1 4 0 5 0 6 0 7 0 8 0 9 0
L'accuratezza in un modello di classificazione è il rapporto delle previsioni corrette sulle previsioni totali, ci dice quindi quanto bene il modello fa le sue predizioni.Adesso vediamo graficamente i risultati:
import pandas as pd
# Definiamo il range per la linea di decisione
x_min, x_max = X['Amount'].min() - 1, X['Amount'].max() + 1
y_min, y_max = X['Numero_Carte'].min() - 1, X['Numero_Carte'].max() + 1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
# Convertiamo in DataFrame con nomi coerenti
grid = pd.DataFrame(np.c_[xx.ravel(), yy.ravel()], columns=['Amount', 'Numero_Carte'])
# Calcolo della probabilità su tutta la griglia
Z = model.predict_proba(grid)[:, 1]
Z = Z.reshape(xx.shape)
# Plot decision boundary
plt.figure(figsize=(10, 6))
sns.scatterplot(x=X['Amount'], y=X['Numero_Carte'], hue=y, palette='coolwarm', edgecolor='k')
plt.contourf(xx, yy, Z, alpha=0.3, cmap='coolwarm')
plt.colorbar(label="Probabilità di vittoria")
plt.xlabel('Amount')
plt.ylabel('Numero_Carte')
plt.title('Decision Boundary - Regressione Logistica')
plt.show()
soglia = 0.5
decisione = (Z > soglia).astype(int)
plt.figure(figsize=(10, 6))
# Scatterplot dei dati reali
sns.scatterplot(x=X['Amount'], y=X['Numero_Carte'], hue=y, palette='coolwarm', edgecolor='k')
# Contour per mostrare le aree di decisione
plt.contourf(xx, yy, decisione, alpha=0.3, cmap='coolwarm')
plt.colorbar(label="Decisione (1 = chiedere carta, 0 = non chiedere)")
plt.xlabel('Amount')
plt.ylabel('Numero_Carte')
plt.title(f'Decisione in base alla soglia {soglia}')
plt.show()
Vediamo adesso qual è il numero ottimale di carte da chiedere:
from sklearn.ensemble import RandomForestClassifier
X = df[['Amount', 'Esito_numerico', 'probabilità_vittoria']]
y = df['Numero_Carte']
model = RandomForestClassifier()
model.fit(X, y)
# Visualizzazione
import matplotlib.pyplot as plt
import seaborn as sns
# Facciamo le predizioni su tutto il dataset
df['predizione'] = model.predict(X)
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df, x='Amount', y='probabilità_vittoria', hue='predizione', palette='viridis')
plt.title("Strategia di richiesta carte in base alla probabilità di vittoria e all'importo")
plt.xlabel("Importo della giocata")
plt.ylabel("Probabilità di vittoria")
plt.show()
come ci aspettavamo, dopo 4 non conviene più chiedere carta, infatti la probabilità di vittoria decresce. Ci sono parti del grafico che potremmo eliminare, ad esempio potremmo limitarlo fino a 7 per renderlo più significativo.
Un'altra cosa che potevamo fare anche prima di iniziare con lo sviluppo del modello, era vedere la correlazione lineare che c'era tra le variabili del dataset, anche se qui non era così utile, ma per esercizio lo vediamo comunque. C'è una funzione di pandas che misura il coefficiente di correlazione lineare(definito quindi come rapporto della covarianza su deviazione standard) tra tutte le sue variabili e una in particolare che ci interessa predire. In pratica serve a vedere quanto una certa variabile è influenzata dalle altre:
#prima però dobbiamo convertire tutte le colonne in numeri(non posso calcolare la correlazione lineare
#di una stringa con una variabile numerica)
#df['Esito_numerico'] = df['Esito'].map({'vittoria': 1, 'sconfitta': 0, 'sballato': -1})
#commento la riga perchè già lo avevamo fatto in precedenza e abbiamo la colonna.
#allora includiamo solo le colonne numeriche:
df_numeric = df.select_dtypes(include=['number'])
# Calcolare la matrice di correlazione
corr_matrix = df_numeric.corr()
# Visualizzare la matrice di correlazione come heatmap
plt.figure(figsize=(8, 6))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f', linewidths=0.5)
plt.title('Matrice di Correlazione')
plt.show()
quindi questa matrice è utile a capire quali sono le variabili che influenzano la feature che ci interessa predire.Andrebbe vista prima di creare il modello, per cercare di capire quali feature tenere e quali rimuovere, in modo da evitare overfitting del modello, cioè il modello si adatta troppo al rumore, cattura quindi troppi outlayers dei dati e la predizione non è più accurata.
L'analisi di questa matrice è parte della EDA,Exploratory Data Analysis, una fase cruciale nel processo di analisi dei dati, che precede la costruzione di modelli predittivi. L'EDA si concentra sull'esplorazione, la comprensione e la visualizzazione del dataset, prima di applicare modelli statistici o di machine learning. Lo scopo è acquisire una conoscenza approfondita dei dati per identificare patterns, tendenze, outlier e relazioni tra le variabili.
La pipeline di data science è l'intero processo di lavoro con i dati, che generalmente include:
Raccolta dei dati: acquisizione dei dati da diverse fonti (database, file, API, sensori):
• Pre-processing dei dati: gestione dei valori mancanti, normalizzazione, trasformazione, e codifica delle variabili categoriche.
• Esplorazione dei dati (EDA): esplorazione visiva e statistica per comprendere il dataset
• Modellazione: applicazione di tecniche di machine learning (supervisionato, non supervisionato) per fare previsioni o estrarre pattern.
• Valutazione del modello: misurazione della performance tramite metriche appropriate (es. accuracy, precision, recall, F1-score). • Deployment: implementazione del modello in produzione per fare previsioni su dati reali.
Un altro esercizio che possiamo fare è la gestione dei valori nulli. In questo dataset non c'è questo problema, per cui ne prenderemo un altro per fare un esempio. Di solito possiamo scegliere se rimpiazzare questi valori nulli con la media o con degli zeri o un altro valore qualsiasi che riteniamo opportuno.
#esempio di dataset con valori null:
import pandas as pd
import numpy as np
# Creazione di un DataFrame di esempio. A,B,C saranno le colonne del dataframe
data = {
'A': [1, 2, np.nan, 4, 5],
'B': [5, np.nan, 3, 2, 1],
'C': [np.nan, 3, 2, 4, 5]
}
df = pd.DataFrame(data)
print("DataFrame originale:")
print(df)
DataFrame originale: A B C 0 1.0 5.0 NaN 1 2.0 NaN 3.0 2 NaN 3.0 2.0 3 4.0 2.0 4.0 4 5.0 1.0 5.0
# Calcolo della media per ogni colonna
mean_values = df.mean()
# Sostituzione dei valori NaN con la media della colonna corrispondente
df_filled = df.fillna(mean_values)
#oppure potevamo farlo colonna per colonna(stesso risultato):
#for col in df.columns:
# df[col] = df[col].fillna(df[col].mean())
print("\nDataFrame dopo aver riempito i valori mancanti con la media:")
print(df_filled)
DataFrame dopo aver riempito i valori mancanti con la media: A B C 0 1.0 5.00 3.5 1 2.0 2.75 3.0 2 3.0 3.00 2.0 3 4.0 2.00 4.0 4 5.0 1.00 5.0
Quindi come possiamo vedere non ci sono più valori nulli, che sono stati rimpiazzati con la media di quella colonna.
Vediamo adesso un altro concetto, la normalizzazione, molto importante.
Perché la normalizzazione è importante:
Scala uniforme: Alcuni algoritmi, come la regressione lineare, le reti neurali e le k-nearest neighbors (KNN), sono sensibili alla scala delle variabili. Se le variabili hanno scale molto diverse (ad esempio, una variabile che va da 0 a 1 e un'altra da 1000 a 10000), l'algoritmo potrebbe dare maggiore importanza a quella con la scala più ampia, distorcendo i risultati.
Convergenza più rapida: Nei modelli di apprendimento come le reti neurali, la normalizzazione dei dati può migliorare la velocità di convergenza durante l'allenamento. I dati scalati in modo uniforme rendono l'ottimizzazione (ad esempio, il gradiente) più stabile e veloce.
Migliore performance: Alcuni algoritmi come la SVM (Support Vector Machine) o K-means dipendono dalla distanza tra i punti. La normalizzazione è essenziale in questi casi per evitare che variabili con scale diverse influenzino troppo il calcolo della distanza.
Ci sono diversi tipi di normalizzazione, ecco quali vediamo:
• Min-Max Scaling: Normalizza i dati in un intervallo definito, solitamente [0, 1].
$$ X_{\text{norm}} = \frac{X - \text{min}(X)}{\text{max}(X) - \text{min}(X)} $$
• X è il valore originale della variabile.
• min(X) è il valore minimo della variabile.
• max(X) è il valore massimo della variabile.
• Standardizzazione (Z-score): Trasforma i dati in modo che abbiano media 0 e deviazione standard 1, quindi diventa una Gaussiana Standard.
$$ Z = \frac{X - \mu}{\sigma} $$ dove:
• X è il valore originale.
• μ è la media della variabile.
• σ è la deviazione standard della variabile.
In Python ci sono varie librerie per la standardizzazione, noi useremo MinMaxScaler e StandardScaler della libreria sklearn(sci-kit learn).
Partiamo dal MinMaxScaler:
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
# Creazione di un DataFrame di esempio
data = {
'A': [1, 2, 3, 4, 5],
'B': [10, 20, 30, 40, 50]
}
df = pd.DataFrame(data)
# Inizializzazione del MinMaxScaler
scaler = MinMaxScaler()
# Applicazione della normalizzazione
df_normalized = scaler.fit_transform(df)
# Creazione di un DataFrame con i valori normalizzati
df_normalized = pd.DataFrame(df_normalized, columns=df.columns)
print(df_normalized)
A B 0 0.00 0.00 1 0.25 0.25 2 0.50 0.50 3 0.75 0.75 4 1.00 1.00
Adesso vediamo come cambia usando l'altro metodo di standardizzazione, che ci darà quindi una distribuzione con media 0 e varianza 1.
from sklearn.preprocessing import StandardScaler
# Creazione di un DataFrame di esempio
data = {
'A': [1, 2, 3, 4, 5],
'B': [10, 20, 30, 40, 50]
}
df = pd.DataFrame(data)
# Inizializzazione dello StandardScaler
scaler = StandardScaler()
# Applicazione della standardizzazione
df_standardized = scaler.fit_transform(df)
# Creazione di un DataFrame con i valori standardizzati
df_standardized = pd.DataFrame(df_standardized, columns=df.columns)
print(df_standardized)
A B 0 -1.414214 -1.414214 1 -0.707107 -0.707107 2 0.000000 0.000000 3 0.707107 0.707107 4 1.414214 1.414214