Caso e classifiche

Calcio, Python e data science

di

thumbnail

12

lug

2017

L’anno prossimo scattano i Mondiali. Un piccolo esempio di data science condotto via Python su gironi a quattro squadre.

Riproduciamo tradotto in versione italiana, con il permesso dell’autore del nostro libro Data Science con Python, un articolo pubblicato originalmente su LinkedIn con il titolo Copa America through a Python Pryzm.

Le manifestazioni calcistiche internazionali, come i Mondiali FIFA o la Copa America (il torneo autonomo tra squadre nazionali più longevo al mondo), si distinguono per due caratteristiche: ogni giorno è pieno di partite e i risultati sono sempre aperti alla sorpresa.

Il data scientist si chiede però: le sorprese sono tutte uguali? O ci sono risultati più inattesi di altri? Il tifoso preparato conosce già le risposte. Chi invece si interessa al calcio solo durante i grandi eventi potrebbe cercarle tramite la programmazione.

Lavoriamo sulla prima fase di questi tornei, che tipicamente vede un numero di squadre multiplo di quattro suddivise in gironi, appunto, da quattro (chiamiamo N il numero di squadre per girone: N=4). In ciascun girone ogni squadra incontra le altre tre, per un totale di N(N-1)/2=6 partite. La vittoria vale tre punti, il pareggio uno, la sconfitta zero. La prima o le prime classificate passano alla fase seguente del torneo, che però va oltre lo scopo di questo esercizio.

Pari opportunità

Se gli esiti di ciascun incontro fossero equiprobabili (vittoria, pareggio, sconfitta con uguali probabilità di verificarsi), qualunque sequenza di sei risultati in un gruppo avrebbe uguali probabilità di verificarsi. (Ci sono 3^6=729 sequenze possibili). Per ogni sequenza di risultati possiamo calcolare la classifica finale del gruppo. Se tutte le sei partite finissero pari, per esempio, ogni squadra terminerebbe con 3 (1+1+1) punti. C’è solo questo modo per ottenere una classifica con tutte le squadre in parità.

Se una squadra vince sempre, una vince due incontri, un’altra se ne aggiudica uno e l’ultima squadra finisce sempre sconfitta, i punteggi della classifica saranno 9, 6, 3 e 0. Dato che potrebbe essere in testa una qualunque delle quattro squadre, il secondo posto potrebbe essere aggiudicato a una qualsiasi delle altre tre e ognuna delle ultime due potrebbe arrivare terza, esistono 4*3*2=24 sequenze di risultati che portano a una classifica 9-6-3-0. In altre parole, questa classifica è 24 volte più portata a verificarsi di 3-3-3-3, che come abbiamo visto è raggiungibile con una sola sequenza di risultati. Se questo rapporto non venisse rispettato, significherebbe che i risultati non sono affatto equiprobabili e qualche squadra ha più probabilità delle altre di pareggiare, vincere o perdere.

Calcoliamo la probabilità di ogni possibile punteggio e proviamo a confrontarlo con quanto è accaduto, per esempio, durante l’ultima Copa América. Dobbiamo contare quante sequenze delle 729 contribuiscono a ciascuna classifica. Possiamo farlo a mano o, molto meglio, con un programma. Io preferisco usare Python, ma è questione di gusti.

Data Science con Python

La scienza dei dati è applicabile anche ai tornei di calcio.

 

Funzioni importanti

Per cominciare, importiamo moduli utili allo scopo e definiamo alcune costanti, possibilmente autoesplicative:

import itertools, numpy as np, pandas as pd
N = 4; VITTORIA = 3; SCONFITTA = 0; PAREGGIO = 1

 

La funzione itertools.product(x) considera raccolte di dati e ne calcola il prodotto cartesiano. Per esempio, product((0,1), (0,1), (0,1)) restituisce [(0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), …]. Nel nostro caso, ciascuna raccolta consiste in un array di possibili esiti di incontro (codificati come tuple a due elementi di punti guadagnati da ciascuna squadra per ciascun risultato) e ci sono 6 raccolte interne dentro la raccolta esterna, una per incontro.

outcomes = itertools.product(*((N * (N - 1) // 2) * ([(SCONFITTA, VITTORIA),
(PAREGGIO, PAREGGIO), (VITTORIA, SCONFITTA)], )))

 

Il segno di moltiplicazione in mezzo (l’asterisco, in Python) effettua una moltiplicazione numerica. L’ultimo asterisco replica sei volte la raccolta interna di esiti delle partite. Il primo asterisco spacchetta l’elenco degli argomenti, per cui la funzione product() vede sei argomenti invece di uno. (Si aspetta che ciascuna raccolta sia un argomento separato). Una raccolta di esiti somiglia a questa:

((0, 3), (0, 3), (3, 0), (3, 0), (1, 1), (0, 3))

 

Qui è successo che il Team 1 (T1) ha perso contro T2 e T3, per vincere contro T4; T2 ha sconfitto T3 e ha pareggiato con T4; T3 ha perso contro T4. Ci serve ora una funzione che converta una raccolta di esiti nei punteggi della classifica finale.

Useremo NumPy, un potente motore numerico per Python, per creare una matrice N per N (4×4). Gli esiti o_seq vengono convertiti in un array NumPy di punteggi per la prima e la seconda squadra di ciascuna partita:

array([[0, 0, 3, 3, 1, 0], [3, 3, 0, 0, 1, 3]])

 

I punteggi vengono incollati nelle parti triangolari superiore e inferiore della matrice, usando l’indicizzazione smart di NumPy e l’assegnazione simultanea di Python. Calcoliamo infine le somme di riga o di colonna, le ordiniamo per scoprire le squadre migliori e peggiori, per convertire il risultato in una tupla immutabile:

def outcomes2points(o_seq):
        u = np.zeros([N, N], dtype=int)
        u[np.triu_indices(N, 1)], u[np.tril_indices(N, -1)] = np.array(o_seq).T
        return tuple(sorted(u.sum(axis=0)))

 

La funzione viene applicata a ogni possibile raccolta di risultati. Alcune tuple finiscono per avere, naturalmente, gli stessi valori.

points = [outcomes2points(outcome) for outcome in outcomes]

 

Analisi dei dati

Useremo pandas per calcolare le frequenze delle tuple: convertiamo l’elenco di punti in una serie di pandas, contiamo le righe univoche, ordiniamo e normalizziamo le frequenze. Pandas può contare righe univoche solo se sono immutabili, il motivo per cui prima le abbiamo convertite prima in tuple.

probs = pd.Series(points, name="p").value_counts().sort_values() / len(points)

 

Ecco il prodotto dell’analisi in cima e in fondo alle serie di pandas:

(3, 3, 3, 3) 0.001372
(0, 0, 9, 9) 0.002743
(0, 2, 5, 9) 0.005487
(2, 3, 3, 7) 0.005487
(1, 1, 6, 9) 0.005487
Name: p, dtype: float64

 

(1, 3, 6, 7) 0.038409
(1, 2, 5, 7) 0.054870
(2, 4, 4, 5) 0.054870
(3, 4, 4, 6) 0.054870
(1, 4, 4, 7) 0.076818
Name: p, dtype: float64

 

La classifica di gruppo meno probabile è 3-3-3-3 (ma lo sapevamo già!) e la più probabile è 1-4-4-7 (quattro vittorie e due pareggi).

Ho usato le classifiche dei gironi della Copa América 2016, organizzati in un frame di dati autodocumentante:

final = pd.DataFrame({"group" : list("ABCD"), "points" : [tuple(sorted(p)) for p in
((6, 6, 4, 1), (7, 5, 4, 0), (7, 7, 3, 0), (9, 6, 3, 0))]})

 

(Volevo essere sicuro che le classifiche finali fossero ordinate nello stesso ordine delle chiavi delle serie). Ora possiamo confrontare i risultati reali con la loro probabilità teorica di verificarsi:

actual = final.merge(probs[final["points"]].reset_index(),
on="points").set_index("group").sort_values("p")

 

E vedere le risposte.

             Punti          p
Gruppo 
C           (0, 3, 7, 7)    0.005487
B           (0, 4, 5, 7)    0.010974
D           (0, 3, 6, 9)    0.027435
A           (1, 4, 6, 6)    0.032922
 

Caso o calcio?

La classifica del Gruppo C è la più improbabile (p~0.5%). Messico, Venezuela, Uruguay e Jamaica hanno ottenuto risultati molto diversi da quelli che avrebbe dato una generazione casuale di esiti.

L’anno prossimo è tempo di Mondiali 2018. I gironi saranno otto, il doppio di quelli esaminati qui. Ma la modalità di analisi sarà identica, se qualcuno vorrà togliersi la curiosità: risultati vicini al puro caso, o veramente dei divari tecnici e agonistici? L’esercizio viene lasciato al lettore, con tutto il tempo di impratichirsi prima del calcio di inizio del 14 giugno!




Dmitry Zinoviev (@aqsaqal) vanta un dottorato di ricerca in Informatica e un master in Fisica. I suoi ambiti di ricerca includono la creazione di modelli simulati al computer, l’analisi delle reti sociali e l’informatica umanistica. Insegna alla Suffolk University di Boston dal 2001.

In Rete: https://github.com/dzinoviev

Letto 1.592 volte | Tag: , , ,

Lascia il tuo commento