[Sports] Fifa et analyse de données

Après un été chargé en sports, l’automne et la Ligue 1 reprennent peu à peu leurs droits. C’est l’occasion de détailler un sujet d’analyse de données élaboré pour un cours à l’ENSAE. Il s’agit d’analyser des données qualitatives (caractéristiques physiques, tactiques et aptitudes relatives à certains aspects techniques du jeu) décrivant les joueurs du championnat de France de football. Le but final est de déterminer “statistiquement” à quel poste faire jouer Mathieu Valbuena 🙂 On utilise le langage R et l’excellent package d’analyse de données FactoMineR.

Les données

Comme indiqué dans l’énoncé du TD, il n’est pas nécessaire de bien connaître le football pour pouvoir suivre cet article. Seule une notion de l’emplacement des joueurs sur le terrain en fonction de leur poste (correspondant à la colonne “position” du dataset) est souhaitable. Voici un petit schéma pour aider les moins avertis :

disposition_terrain

Les données sont issues du jeu vidéo Fifa 15 (les connaisseurs auront remarqué que les données datent donc d’il y a déjà deux saisons, il peut donc y avoir quelques différences avec les effectifs actuels !), qui donne de nombreuses statistiques pour chaque joueur, incluant une évaluation de leurs capacités. Les données de Fifa sont quantitatives (par exemple chaque capacité est notée sur 100) mais pour cet article on les a rendues catégorielles sur 4 positions : 1. Faible / 2. Moyen / 3. Fort / 4. Très fort. On verra l’intérêt d’avoir procédé ainsi un peu plus loin !

Préparation des données

Commençons par charger les données. Notez l’utilisation de l’option stringsAsFactors=TRUE (plus d’explications sur ce fameux paramètre stringsAsFactors ici). Eh oui, une fois n’est pas coutume, FactoMineR utilise des facteurs pour effectuer l’analyse de données !

> champFrance <- read.csv2("td3_donnees.csv", stringsAsFactors=TRUE)
> champFrance <- as.data.frame(apply(champFrance, 2, factor))

La deuxième ligne sert à transformer les colonnes de type int créés par read.csv2 en factors.

FactoMineR utilise le paramètre “row.names” des data.frame de R pour l’affichage sur les graphes. On va donc indiquer qu’il faut utiliser la colonne “nom” en tant que row.names pour faciliter la lecture :

> row.names(champFrance) <- champFrance$nom
> champFrance$nom <- NULL

Voilà à quoi ressemble désormais notre data.frame (seules les premières lignes sont affichées) :

> head(champFrance)
                      pied position championnat age taille general
Florian Thauvin     Gauche      MDR      Ligue1   1      3       4
Layvin Kurzawa      Gauche       AG      Ligue1   1      3       4
Anthony Martial      Droit       BU      Ligue1   1      3       4
Clinton N'Jie        Droit       BU      Ligue1   1      2       3
Marco Verratti       Droit       MC      Ligue1   1      1       4
Alexandre Lacazette  Droit       BU      Ligue1   2      2       4

Analyse des données

Nous avons affaire à un tableau de variables catégorielles : la méthode adaptée est l’Analyse des Correspondances Multiples, qui est implémentée dans FactoMineR par la méthode MCA. Pour le moment on exclut de l’analyse les variables “position”, “championnat” et “âge” (que l’on traite comme variables supplémentaires) :

> library(FactoMineR)
> acm <- MCA(champFrance, quali.sup=c(2,3,4))

Trois graphes apparaissent dans la sortie : la projection sur les deux premiers axes factoriels des catégories et des individus, ainsi que le graphe des variables. A ce stade, seul le second nous intéresse :

2_nuages_points_2
Projection des individus sur les deux premiers axes factoriels

Avant même d’essayer d’aller plus loin dans l’analyse, quelque chose doit nous sauter aux yeux : il y a clairement deux nuages de points ! Or nos méthodes d’analyse de données supposent que le nuage qu’on analyse est homogène. Il va donc falloir se restreindre à l’analyse de l’un des deux nuages que l’on observe sur ce graphe.

Pour identifier à quels individus le nuage de droite correspond, on peut utiliser les variables supplémentaires (points verts). On observe que la projection de la position goal (“G”) correspond bien au nuage. En regardant de plus près les noms des individus concernés, on confirme que ce sont tous des gardiens de but.

On va se concentrer pour le reste de l’article sur les joueurs de champ. On en profite également pour retirer les colonnes ne concernant que les capacités de gardien, qui ne sont pas importantes pour les joueurs de champ et ne peuvent que bruiter notre analyse :

> champFrance_nogoals <- champFrance[champFrance$position!="G",-c(31:35)]
> acm_nogoals <- MCA(champFrance_nogoals, quali.sup=c(2,3,4))

Et l’on vérifie bien dans la sortie graphique que l’on a un nuage de points homogène.

Interprétation

On commence par réduire notre analyse à un certain nombre d’axes factoriels. Ma méthode favorite est la “règle du coude” : sur le graphe des valeurs propres, on va observer un décrochement (le “coude”) suivi d’une décroissance régulière. On sélectionnera ensuite un nombre d’axes correspondant au nombre de valeurs propres précédant le décrochement :

> barplot(acm_nogoals$eig$eigenvalue)

 

barplot
Éboulis des valeurs propres

Ici, on peut choisir par exemple 3 axes (mais on pourrait justifier aussi de retenir 4 axes). Passons maintenant à l’interprétation, en commençant par les graphes des projections sur les deux premiers axes retenus pour l’étude.

> plot.MCA(acm_nogoals, invisible = c("ind","quali.sup"))
axes_1_2_modalites
Projection des modalités sur les axes factoriels 1 et 2 (cliquer pour agrandir)

On peut par exemple lire sur ce graphe le nom des modalités possédant les plus fortes coordonnées sur les axes 1 et 2 et commencer ainsi l’interprétation. Mais avec un tel de nombre de modalités, la lecture directe sur le graphe n’est pas si aisée. On peut également obtenir un résultat dans la sortie texte spécifique de FactoMineR, dimdesc (seule une partie de la sortie est donnée ici) :

> dimdesc(acm_nogoals)
$`Dim 1`$category
                         Estimate       p.value
finition_1            0.700971584 1.479410e-130
volees_1              0.732349045 8.416993e-125
tirs_lointains_1      0.776647500 4.137268e-111
tacle_glisse_3        0.591937236 1.575750e-106
effets_1              0.740271243  1.731238e-87
[...]
finition_4           -0.578170467  7.661923e-82
puissance_tir_4      -0.719591411  2.936483e-86
controle_balle_4     -0.874377431 5.088935e-104
dribbles_4           -0.820552850 1.795628e-117

Les modalités les plus caractéristiques de l’axe 1 sont, à droite, un niveau faible dans les capacités offensives (finition, volées, tirs lointains), et de l’autre un niveau très fort dans ces même capacités. L’interprétation naturelle est donc que l’axe 1 discrimine selon les capacités offensives (les meilleurs attaquants à gauche, les moins bons à droite). On procède de même pour l’axe 2, et on observe le même phénomène, mais avec les capacités défensives : en haut on trouvera les meilleurs défenseurs, et en bas les moins bons défenseurs.

Les variables supplémentaires peuvent aussi aider à l’interprétation, et vont confirmer notre interprétation, notamment la variable de position :

> plot.MCA(acm_nogoals, invisible = c("ind","var"))
var_sup_axes_1_2
Projection des variables supplémentaires sur les axes factoriels 1 et 2 (cliquer pour agrandir)

On trouve bien à gauche du graphe les les postes offensifs (BU, AIG, AID) et en haut les postes défensifs (DC, AD, AG).

Une conséquence de cette interprétation est que l’on risque de trouver les joueurs de meilleur niveau organisés le long de la seconde bissectrice, avec les meilleurs joueurs dans le quadrant en haut à gauche, et les plus faibles dans le quadrant en bas à droite. Il y a beaucoup de moyens de le vérifier, mais on va se contenter de regarder dans le graphe des modalités l’emplacement des observations de la variable “général”, qui résume le niveau d’un joueur. Comme on s’y attend, on trouve “général_4” dans en haut à gauche et “général_1” dans le quadrant en bas à droite. On peut observer aussi le placement des variables supplémentaires “Ligue 1” et “Ligue 2” pour s’en convaincre 🙂

A ce stade, il y a déjà plein de choses intéressantes à relever ! Parmi celles qui m’amusent le plus :

  • Les ailiers gauches semblent avoir un meilleur niveau que les ailiers droits (si un spécialiste du foot voulait bien m’en expliquer la raison ce serait top !)
  • L’âge n’est pas explicatif du niveau du joueur, sauf pour les plus jeunes qui ont un niveau plus faible
  • Les joueurs les plus âgés ont des rôles plus défensifs.

N’oublions pas de nous occuper de l’axe 3 :

> plot.MCA(acm_nogoals, invisible = c("ind","var"), axes=c(2,3))
axes_2_3
Modalités projetées sur les axes 2 et 3

Les modalités les plus caractéristiques de ce troisième axe sont les faiblesses techniques : les joueurs les moins techniques sont sur les extrémités de l’axe, et les joueurs les plus techniques au centre. On le confirme sur le graphe des variables supplémentaires : les buteurs et défenseurs centraux sont en effet moins réputés pour leurs capacités techniques, tandis que tous les postes de milieux se retrouvent au centre de l’axe :

sup_axes_2_3
Variables supplémentaires sur les axes 2 et 3 (cliquer pour agrandir)

C’est l’intérêt d’avoir rendu ces variables catégorielles. Si l’on avait conservé le caractère quantitatif des données originelles de Fifa et effectué une ACP, les projections de chaque caractéristique sur chaque axe auraient été ordonnées par niveau, contrairement à ce qui se passe sur l’axe 3. Et après tout, discriminer les joueurs suivant leur niveau technique ne reflète pas forcément toute la richesse du football : à certains postes, on a besoin de techniciens, mais à d’autres, on préférera des qualités physiques !

Mathieu Valbuena

On va maintenant ajouter les données d’un nouvel entrant dans le championnat de France : Mathieu Valbuna (oui je vous avais prévenu, les données commencent à dater un peu :p) et le comparer aux autres joueurs en utilisant notre analyse.

> columns_valbuena <- c("Droit","AID","Ligue1",3,1
 ,4,4,3,4,3,4,4,4,4,4,3,4,4,3,3,1,3,2,1,3,4,3,1,1,1)
> champFrance_nogoals["Mathieu Valbuena",] <- columns_valbuena

> acm_valbuena <- MCA(champFrance_nogoals, quali.sup=c(2,3,4), ind.sup=912)
> plot.MCA(acm_valbuena, invisible = c("var","ind"), col.quali.sup = "red", col.ind.sup="darkblue")
> plot.MCA(acm_valbuena, invisible = c("var","ind"), col.quali.sup = "red", col.ind.sup="darkblue", axes=c(2,3))

Les deux dernières lignes permettent de représenter Mathieu Valbuena sur les axes 1 et 2, puis 2 et 3 :

Axes factoriels 1 et 2 avec Mathieu Valbuena en point supplémentaire (cliquer pour agrandir)
Axes factoriels 1 et 2 avec Mathieu Valbuena en point supplémentaire (cliquer pour agrandir)
Axes factoriels 2 et 3 avec Mathieu Valbuena en point supplémentaire (cliquer pour agrandir)
Axes factoriels 2 et 3 avec Mathieu Valbuena en point supplémentaire (cliquer pour agrandir)

Résultat de notre analyse : Mathieu Valbuena a plutôt un profil offensif (gauche de l’axe 1), mais possède un bon niveau général (sa projection sur la deuxième bissectrice est assez élevée). Il possède également de bonnes aptitudes techniques (centre de l’axe 3). Enfin, ses qualités semblent plutôt bien convenir aux postes de milieu offensif (MOC) ou milieu gauche (MG). Avec quelques lignes de code, on peut trouver les joueurs du championnat dont le profil est le plus proche de celui de Valbuena :

> acm_valbuena_distance <- MCA(champFrance_nogoals[,-c(3,4)], quali.sup=c(2), ind.sup=912, ncp = 79)
> distancesValbuena <- as.data.frame(acm_valbuena_distance$ind$coord)
> distancesValbuena[912, ] <- acm_valbuena_distance$ind.sup$coord

> euclidianDistance <- function(x,y) {
 
 return( dist(rbind(x, y)) )
 
}

> distancesValbuena$distance_valbuena <- apply(distancesValbuena, 1, euclidianDistance, y=acm_valbuena_distance$ind.sup$coord)
> distancesValbuena <- distancesValbuena[order(distancesValbuena$distance_valbuena),]

# On regarde les profils des 5 individus les plus proches
> nomsProchesValbuena <- c("Mathieu Valbuena", row.names(distancesValbuena[2:6,]))

Et l’on obtient : Ladislas Douniama, Frédéric Sammaritano, Florian Thauvin, N’Golo Kanté et Wissam Ben Yedder.

Il y aurait plein d’autres choses à dire sur ce jeu de données mais je préfère arrêter là cet article déjà bien long 😉 Pour finir, gardez à l’esprit que cette analyse n’est pas vraiment sérieuse et sert surtout à présenter un exemple sympathique pour la découverte de FactoMineR et de l’ADD.

 

[Sports] What the splines model for UEFA Euro 2016 got right and wrong

UEFA Euro 2016 is over! After France’s heartbreaking loss to Portugal in the Final, it’s now time to assess the performance of our “splines model“. On the main page of the project you can now find the initial predictions we made before the start on our competition. I also added a link to the archives of the odds we updated after each day (EDIT: I realize I made a mistake with a match that was played on Day 2, I’ll correct this asap – results should not be altered much though.)

screenshot Euro 2016
Screenshot predictions Euro 2016

What went well (Portugal, Hungary, Sweden)

Let’s begin with our new European champions: Portugal. They were our 5th favorite, with an estimated 8.3% chance of winning the title. To everyone’s surprise (including ours to be honest 😉 ), they finished 3rd in group F. However, the odds of this happening were estimated at 20%, so we can hardly say the splines model was completely stunned by this outcome! In fact, except for the initial draw against Iceland, we had all calls correct for Portugal games!

Hungary were described by some as the weakest team of the tournament, so by extension as the weakest team of group F. But they won it! Our model didn’t agree with those pundits, estimating the chances of advancing to the second round for Gábor Király‘s teammates at almost 3 out of 4.

Sweden certainly had one of the best players in the world with Zlatan Ibrahimovic. But our model was never a fan of their squad, and they did end up at the last place in group E. Similarly, Ukraine was often referred to as a potential second-rounder but ended up at the last place (losing all their games), which was the most likely outcome according to the splines model.

What went wrong (Iceland, Austria, England)

Austria were seen by the splines model as outsiders for this competition (4.7% of becoming champs – for instance, Italy’s chances were estimated at 4.2%). We evaluated their chances of advancing to the second round to be greater than 70%. They ended up at the last place of Group F with a single point.

On the contrary, Iceland were seen as one of the weakest teams of the competition and a clear favorite for last place in Group F. Eventually, they were astonishingly successful! On their way to the quarter-finals, they eliminated England. Our model gave England a good 85% probability to win the match. But, surprising as it was, this alone does not prove our model was not reliable (more on upsets on the next paragraph). Yet we can’t consider the projections for the Three Lions other than a failure, because they also ended up second in group B when we thought they would easily win the group.

Spain lost in round of 16 to Italy and in the group phase to Croatia. The estimated probabilities for these events were 40% and 16%.

Hard to say

We almsot included Turkey in the previous paragraph: after all, we gave them the same chances as Italy for winning the tournament, and we estimated their odds of advancing to the round of 16 to more than 70%, yet they failed. In addition, their level was described by experts as rather poor. But paradoxically, the splines model had all calls correct for Turkey games! What doomed them was the 3-0 loss against the defending champions, Spain. With a final goal average of -2 and 3 points, they couldn’t reach the second round as one of the four best thirds.

Wales unexpectedly beat Belgium, one of our favorites, in quarter-finals. But is this a sign of a bad model or bad luck? Upsets happen, and they’re not necessarily a sign that a team’s strength was incorrectly estimated.

Home field advantage

Our model stood out from others (examples here, here or here) on predictions for France. As a matter of fact, it valued much less home field advantage than the other models. But France didn’t win the Euro! Similarly, nearly all models predicted a Brazil victory in World Cup 2014, mostly because of home field advantage… and we all know what happened!

To us, it is unclear whether home field advantage during Euro or the World Cup can compare to home field advantage for a friendly match or a qualifier. I hope someone studies this particular point in the future!

Conclusion

We had a lot of fun building this model and it helped us enjoy the competition! I hope you guys enjoyed it too!

[Sports] L’adversaire des bleus en 8èmes

Après la première place du groupe acquise par l’équipe de France, Baptiste Desprez de Sport24 se demandait aujourd’hui quel est l’adversaire le plus probable pour les Bleus en huitièmes.

Ça tombe bien, on dispose d’un modèle capable de calculer des probabilités pour les matches de l’Euro. Je vous laisse lire l’article de Sport24 si vous voulez comprendre toutes les subtilités concoctées par l’UEFA pour ce premier Euro à 24. Nous, on va se contenter de faire tourner le modèle pour obtenir les probabilités. On obtient (avec arrondis) :

Irlande du Nord : 72% ; République d’Irlande : 14% ; Allemagne : 8% ; Belgique : 4% ; Pologne : 2%

probas_huitiemes

Voilà, il est extrêmement probable que le prochain adversaire de l’équipe de France se nomme “Irlande” 🙂 . Curieusement, la probabilité de rencontrer l’Allemagne est bien plus forte que de rencontrer la Pologne, alors même que le modèle donne une forte probabilité pour que l’Allemagne termine première de son groupe devant la Pologne… C’est complexe un tableau de l’Euro ! On va quand même croiser les doigts pour ne pas croiser la route de Müller et cie aussi tôt dans le tableau !

Il est également amusant de constater que, bien que ce soit possible, un huitième contre une équipe du groupe D (Tchéquie, Turquie ou Croatie) est hautement improbable (<0.2% de chances d’après les simulations). Il semblerait que les configurations permettant à ces équipes de se qualifier en tant que meilleurs troisièmes sont incompatibles avec les configurations les envoyant en huitième contre la France. Si un opérateur vous proposait ce pari, je ne saurais trop vous conseiller de l’éviter 😉

[Sampling] Talk at INSPS – Avignon

I’m in the beautiful city of Avignon for the 3rd ISNPS conference, which is held in the extroardinary Palace of the Popes Convention center. I’ve been invited by Ricardo Cao to give a talk wednesday morning during on sampling methods for big graphs.

[Sports] UEFA Euro 2016 predictions – Comments

Last week we published the results of our prediction model for UEFA Euro 2016. Here are a few comments.

This Euro is undecided

Our model gives fairly close probabilities of winning. To us, this suggests that the competition is fairly open and that no team is a clear favorite before the competition starts (we really hope to see that change after a few matches!)

One could object that this merely shows that our model is unable to predict an outcome with adequate confidence, so we ran the simulations after injecting an artificially low variance in our model, and results for the top teams turned out to be very similar: no clear favorite emerged.

Historically, the European Championship has always been somewhat undecided. Whereas only 8 different teams ever became World Champions (20 editions of the World Cup were held), 9 different teams have already won a European Championship in just 14 editions! In some cases, complete underdogs eventually won the title (for example Denmark in 1992 or Greece in 2004).

France is one of the favorites

The home team is every other’s model favorite! Check out Goldman Sachs’ or a model built by Austrian researchers based on bookmakers’ odds. Clearly, the home advantage is key here, although recently France has proven able to score quite a number of goals, which is an important feature of our model. The model’s favorite is Belgium (who are, the start of the competition, 2nd in the Fifa rankings), but Germany, Spain and England are very close.

Interestingly, even if our model selects France as one of its favorite, it predicts that the group phase won’t be as easy as it seems. For example, the most likely scenario for the opening match is a draw. The probability of reaching the second round is high (86%), but it’s only the fifth highest of values (which might be surprising if you consider that France’s first round opponents are really far behind in the Fifa/Elo rankings). This is very different from other models and bookies, who make France a very heavy favorite to end up at the first place of the group.

As a supporter of France, this reminds us a few (good) memories. In 2000, France only finished second of its group to the Netherlands (and still won the competition), while in the 2006 World Cup, France barely qualified among relatively weak teams, and still managed to reach the final.

Zlatan may not be enough

Altough Sweden, partly thanks to its legendary striker Zlatan Ibrahimovic, is generally said to be a fairly good team, it has the lowest probability of reaching the round of 16 (24.1%), slightly behind Iceland and Albania. In fact, Sweden was very unlucky during the draw and ended up in the so-called “group of death” along with two top tier teams (Belgium and Italy), and an outsider (Ireland) that our model predicts not so bad.

Are Switzerland and Hungary undervalued?

The biggest difference between our model and the bookies’ odds (or the other models) is the relatively high probability we put on Switzerland’s win (6% for us, 1% for Goldman Sachs for instance). It’s hard to really say why our model predicts they’ll fare so well, but we’re definitely impatient to see if this checks out 🙂

Our model also says Hungary is generally under-estimated: to it, the heirs of the “Magikus Magyarok” might very well fight for second place in Group F, while in most predictions they finish dead last.

EDIT: a previous version of this article presented France as the model’s favorite, which was the consequence of a bug that occurred for the second-round probabilities. It is now corrected. Other conclusions are unchanged.

[Sports] UEFA Euro 2016 Predictions – Model

Today we’re launching our own predictions for UEFA Euro 2016 that starts next week.

A model for football: state of the art

There are many ways to build a model to predict football results. The Elo ranking system is commonly used. As it name indicates, it relies on a ranking of the international football teams, either the official FIFA ratings (which are widely known as poorly predictive of a team’s strength) or a custom made Elo ranking. A few Elo rankings are available on the Internet, so one possibility was to use one of these to compute probabilities for each game (via a very simple analytical formula). But we wanted to do something different.

When FiveThirtyEight created a nice viz of their own showing odds for the men’s and women’s world cups, their model was based on ESPN’s Soccer Power Index (SPI). The principle of the SPI is quite simple: compute expected scored and against goals for each team under the assumption it plays against an “average” football squad. Then run a logistic regression to predict the outcome of any two teams, based on their expected performance against the “average” team. SPI takes an impressive amount of relevant parameters into account (including player performance), and has generally proven itself reliable (although FiveThirtyEight’s predictions always seemed a tad overconfident to me!).

Our very own model

For our model, we liked the principle of the SPI very much, but we wanted to try our own little variation. So we kept the core feature of the SPI: computing the expected goals scored and against for each team, but we chose to directly plug these results into our simulations (i.e without the logistic regression). Of course, due to lack of time and resources, our model will be way less sophisticated than ESPN’s (there was no way we could include player performance for example), but still the results might be worth analyzing!

So, for each one of the 24 teams competing, we’re trying to predict the quantitative variable that is the number of goals scored (and against) for each game. Of course, we’re going to use all our knowledge of machine learning to achieve this 😉 Our training data is composed of the 1795 international games played by the 24 teams that qualified for UEFA Euro 2016 between 2008 and 2016 (excluding the Olympics, which are too peculiar in football to be relevant).

We dispose of 1795 observations, for each of which we know: the location of the game, the teams that played, the final score and the type of the game (friendly, world cup qualifier, etc.). We matched each team to its Fifa ranking at the closest date available, and determine which team had home court advantage (if any).

Then we ran the simplest of regression models: a linear regression on year, team (as a categorical variable), dummy variable indicating if team plays at home or away, type of match and FIFA rankings of both teams. Before even thinking of using this model for simulations, we have to look at how it performs. And a lot of think indicates that it is too unsophisticated. The most telling example might be the prediction of large number of goals. Let’s plot the number of goals scored vs. the FIFA ranking of the opposing team.

linear_model
Number of goals scored with respect to strength of the opponent. Black points: observed ; red points: modeled (linear regression).

You can see on the right side of the plot that it’s not that rare that a large number of goals is scored, especially when playing a very weak team. However, the linear model is unable to predict more than 4 goals scored in a game. This can be a huge problem for simulations as ties are broken by number of goals scored at Euro.

The idea is thus to combine several linear models to get a more sensible prediction. This can be done using regression splines, for which the parameters are chosen using cross-validation.

model_splines_2
Number of goals scored with respect to strength of the opponent. Black points: observed ; green points: modeled (regression splines).

On number of ways, this model is much more satisfying than the first one. Regarding the large values of number of goals scored, the above plots show that our model is now able to predict them 🙂

Simulations and results

Our model gives us expected values for number of goals scored and against, as well as a model variance. We then simulate the number of goals with normal error around the expected value. We do this 10000 times for each match and finally get Monte-Carlo probabilities for the outcome of each group phase match, as well as odds for each team to end at each place in its group and to qualify to each round of the knockout phase.

The results can be found here, and I will post another article later to comment them (which really is the fun part after all!).

[Sampling] Coucher pour réussir ?

Je commence ma semaine en tombant sur un article de LCI qui indique qu’une enquête OpinionWay estime qu’un jeune sur cinq serait prêt à “coucher” pour réussir en entreprise (note : l’info a été en fait reprise dans beaucoup de quotidiens ce lundi matin). On note au passage la magnifique illustration qui montre cinq jeunes salariés … je suppose qu’il faut se demander lequel parmi les cinq est sur le point de coucher pour réussir ?

Essayons d’avoir un regard critique sur ce chiffre annoncé. Souvenons-nous que lorsque l’on fait un sondage, on choisit de n’interroger qu’un petit nombre d’individus, à qui on pose les questions qui nous intéressent. Bien entendu, ce serait trop coûteux d’interroger 66 millions de français au sujet de leur intention de coucher pour réussir (c’est l’intérêt du sondage), mais on paye tout de même un prix : la statistique obtenue aura une certaine imprécision, aussi appelée erreur d’échantillonnage. Essayons d’estimer cette “imprécision”.

Les informations données sur le sondage effectué montrent que l’échantillon a été construit “de façon représentative” (si vous voulez savoir ce que je pense de ce terme, vous pouvez lire cet article) par la méthode des quotas. Cela signifie que les proportions de jeunes salariés de 18-24 ans dans la population et dans l’échantillon sont censées être égales. D’après des statistiques diffusées par Pôle emploi, les 18-24 ans représentent environ 9% du nombre de salariés total. Les 18% de jeunes salariés prêts à coucher pour réussir sont donc dans l’échantillon au nombre de :

\begin{align*}
0.09 \cdot 1060 \approx 95.4
\end{align*}

 

Cette “statistique” ne repose donc que sur les réponses de 95 personnes, et non pas la totalité de l’échantillon (notez qu’il est probable que je surestime le nombre de jeunes salariés de l’échantillon car celui-ci semble construit sur le total de la population et non sur le total de salariés). On peut utiliser cette valeur pour calculer un intervalle de confiance pour la statistique, donné par la formule :

\begin{align*}
\hat{IC} &= \left[ 0.18 \pm 2\cdot \sqrt{\dfrac{0.18 \cdot (1-0.18)}{95}} \right] \\
&\approx [0.10 ; 0.26]
\end{align*}

 

Comme prévu, l’intervalle de confiance est très large : la valeur estimée à 18% est comprise entre 10% et 26% au seuil de confiance de 95%, soit entre “un jeune sur dix” et “un jeune sur quatre”. Et il s’agit uniquement de l’erreur d’échantillonnage ! Les sondages en général sont sujets à beaucoup d’autres sources d’erreurs (voir par exemple le dernier chapitre de ce cours pour plus de précisions). Par exemple, pour cette enquête, le questionnaire était rempli par les individus échantillonnés sur une page web. Le questionnaire n’est pas diffusé ici, mais imaginez que cette question soit la 198ème d’une série de 200, pourrait-on accorder une grande importance aux réponses données par ces 17 individus à cette question ? La formulation de la question peut également influer sur la réponse données par les individus interrogés.

Prenant tout cela en compte, on peut réécrire la version “statistiquement honnête” de l’article de LCI :

lci_9_mai

Finalement c’est peut-être mieux que je ne sois pas journaliste 😉

[Dataviz] Odonymie et couleur politique

Les noms de rue peuvent être parfois un sujet politique sensible, comme l’a montré une actualité récente. L’odonymie (ou étude des noms des voies de communication) a déjà donné lieu à quelques dataviz sympathiques, comme par exemple sur le blog datamix, le site PatronyMap ou sur Slate. Disposant d’une base de données de noms de rues en France Métropolitaine, nous avons cherché à notre tour à illustrer le lien entre odonymes et politique par une petite dataviz.

Accéder à la dataviz
Cliquez pour accéder à la dataviz

On dispose de la liste des noms de rues pour 1470 grandes villes, que l’on classe en deux catégories “Droite” et “Gauche” suivant la couleur politique de leur mairie en 2012 (738 communes à gauche et 732 à droite). On entraîne ensuite un modèle de classification naïve bayésienne sur le TF-IDF constitué par cette liste d’odonymes, que l’on optimise classiquement par validation croisée sur sa qualité prédictive. Etant donnée une liste de noms de voies, le classificateur choisi permet d’identifier correctement la couleur politique de la ville dans environ 70% des cas. Ce chiffre plus élevé que ce à quoi nous nous attendions montre que l’influence de la politique sur les noms de rues (ou tout du moins la corrélation entre les deux) est réelle. Enfin, prédire correctement la couleur politique d’une ville à partir d’un ou ou plusieurs noms de rues n’est que peu intéressant en soi : le modèle entraîné vaut surtout pour son pouvoir explicatif (modèle de régression). C’est pourquoi on utilise les probabilités calculées par le classificateur pour construire une “typicité d’un nom de ville de droite/gauche”, qui est indiquée par la jauge dans notre dataviz.

En faisant tourner le modèle sur notre base de données, les valeurs des probabilités (typicités) obtenues vont de 0.25 à 0.75 (en prenant la convention “0 = gauche” et “1 = droite”), avec plus de 99% des valeurs entre 0.3 et 0.7. Notre jauge est recalibrée en conséquence avec un minimum à 0.3 et un maximum à 0.7, de manière à pouvoir observer correctement les valeurs.

En faisant quelques tests, on s’aperçoit d’une typicité bien plus marquée à gauche qu’à droite (le recall de notre modèle est d’ailleurs supérieur à 90% pour les villes de gauche). A première vue, les odonymes les plus typiques de la droite semblent très “traditionnels” (par exemple Rue de l’industrie ou Rue des Fleurs) alors que les odonymes les plus typiques de la gauche mettent plutôt en avant des personnalités (rue Jean Jaurès, rue Salvador Allende, etc.). Partant de ce constat, on aurait très envie de pouvoir tester si le fait de renommer une rue est plus le fait de la gauche que de la droite, mais on ne dispose pas de données sur les renommages qui permettraient de le faire. Peut-être une prochaine fois ?

Certaines disparités régionales transparaissent également (testez par exemple rue des Alpes vs. rue des Pyrénées ou rue Eric Tabarly vs. rue des Cigognes), et curieusement le type de voie est parfois très influent (“quai” penche à gauche alors que “traverse” penche à droite). Les noms issus du communisme quant à eux penchent certes à gauche, mais peut-être pas autant que l’on aurait pu imaginer (avenue de l’Union Soviétique, avenue Karl Marx). Il faut se souvenir que notre modèle prend en compte la couleur politique de 2016, et que l’orientation politique de certaines villes ont pu changer depuis le moment où ces rues ont été baptisées. Enfin, gardez à l’esprit que cette dataviz n’est que l’illustration d’un modèle et qu’a fortiori rien de ce qui peut être indiqué n’a de valeur sociologique ou politique. N’hésitez pas à mener vos propres tests, et à nous envoyer/tweeter vos trouvailles les plus intéressantes !

[Sampling] Combien de salons de coiffure ont un jeu de mots dans leur nom ? (Deuxième partie)

Suite de notre première partie. Nous avions utilisé une méthode de sondage pour déterminer le nombre de salons de coiffure dont le nom est un jeu de mots. Dans cette seconde partie, nous allons essayer d’utiliser une méthode d’apprentissage pour estimer ce nombre. L’idée sera d’entraîner un modèle à reconnaître si une enseigne de coiffure présente un jeu de mots ou non. C’est parti !

Jeu d’entraînement

Il faut commencer par constituer un “jeu d’entraînement” (ou training set), qui comportera des noms avec et sans jeu de mots, de manière à ce que le modèle choisi puisse construire une règle de classification. Ce jeu d’entraînement, nous allons devoir le constituer à la main. Comme je n’ai pas envie de passer mon dimanche entier à classer des noms de salons de coiffure suivant qu’ils contiennent un jeu de mots ou non (je serais obligé de mentir si on me demandait ce que j’ai fait de mon week-end à la machine à café lundi matin) je choisis de me limiter à 200 enseignes, que je vais tirer aléatoirement dans la base.

Petite remarque supplémentaire : si je tirais ces noms avec une probabilité uniforme (comme on l’a d’abord fait en première partie), ma base comporterait environ 10 enseignes avec jeu de mots contre 190 enseignes sans (puisque, d’après la première partie, le taux de salons de coiffure avec un jeu de mot vaut environ 5%). Avec simplement 10 noms comportement un jeu de mot dans notre base de références, faire classer efficacement les noms de salons par un modèle ne serait pas chose aisée… Je choisis donc d’utiliser à nouveau un tirage stratifié (comme en première partie), de manière à équilibrer les données sur lesquelles le modèle va être entraîné.

Avant de passer à la suite, je réserve 50 noms parmi mes données d’entraînement que j’utiliserai uniquement pour tester la qualité de mon modèle. Le modèle sera donc entraîné sur 150 noms de salons, parmi lesquels environ 50% présentent un jeu de mot (je nomme ces données “jeu de développement”).

Une nécessaire sous-estimation

Il va s’agir de choisir un classificateur d’apprentissage qui présente de bonnes performances sur des données issues de Natural Language Processing (NLP). Le but est que notre modèle soit capable de reconnaître un jeu de mots similaire à ceux qui sont contenus dans le jeu de développement. Des noms similaires à une enseigne contenue dans le jeu de test devraient être correctement classés si les différences sont mineures (quelques lettres, l’ordre, une préposition en plus ou en moins, etc.). Par exemple, si le jeu de développement contient “FAUT TIFF HAIR”, il est bien possible que “FAUTIF HAIR” ou “FAUT TIF HAIRS” soient correctement classés. A fortiori, tous les noms strictement identiques à ceux du jeu de développement seront correctement classés. Par contre, on ne peut pas raisonnablement s’attendre à ce que le modèle soit capable de reconnaître un jeu de mots très différent de ceux qui seront contenus dans cette base de données.

Finalement, il faut s’attendre à ce que cette façon de procéder aboutisse à une sous-estimation du nombre de salons de coiffure avec jeu de mots. Pour le vérifier, on pourra comparer avec l’intervalle de confiance établi en première partie.

Le choix du modèle

J’utilise l’excellent librairie python scikit-learn, que j’utilise pour tester différents types de classificateurs. Bien souvent en machine learning, il s’agit de tester rigoureusement différents modèles et différents choix de paramètres comparativement les uns aux autres. Dans notre cas, je cherche simplement un classificateur suffisamment performant pour aboutir à une conclusion à peu près robuste. Je me contente donc de quelques essais à la main pour effectuer mon choix de modèle. J’effectue ensuite un petit grid search pour tester différents choix de paramètres. Dans le cas du choix de modèle comme du grid search, j’utilise une validation croisée pour choisir le meilleur modèle. Mon classificateur final est donc :

vectorizer = TfidfVectorizer(ngram_range=(1, 3), analyzer='char',
use_idf=False, stop_words=["SARL", "SAS", "SA"])

clf = Pipeline([
('vec', vectorizer),
('clf', SGDClassifier(loss="hinge", penalty="l2")),
])

clf.fit(docs_train, y_train)

Je peux enfin tester la performance de mon modèle sur le jeu de test de 50 noms que j’ai mis de côté :

Classé “avec jeu de mots” Classé “sans jeu de mots”
Sans jeu de mots 17 7
Avec jeu de mots 11 15

Par ailleurs, le jeu de test contenait 26 noms avec jeu de mots. L’estimation issue du modèle donne 22 noms avec jeu de mots, ce qui est cohérent avec le fait qu’on s’attend à obtenir une sous-estimation.

Petits tests à la main

Avant de faire tourner le classificateur sur l’ensemble de la base, j’effectue quelques petits tests à la main, avec des noms d’enseigne inventés. Comme prévu “CREA TIFFS”, “FAUT TIFF HAIR” et “FAUX TIFF HAIR”, proches de certains noms du jeu de développement, sont bien reconnus comme présentant un jeu de mot . “HAIR DRESSER” et “COIFFURE JEAN MICHEL” sont également correctement classés, en tant que noms ne présentant pas de jeu de mots. Je teste ensuite “LA CHAMBRE A HAIR”, “VOLT HAIR” et “LE SAVOIR F HAIR” (merci au tumblr lolcoiffeurs pour les idées !), sans grand espoir car ces jeux de mots me semblent trop éloignés de ceux présents dans le jeu d’entraînement. Et pourtant, les trois sont bel et bien reconnus comme jeux de mots ! Mon dernier test, “DE MECHE AVEC VOUS”, n’est lui pas correctement reconnu. Cela ne m’étonne pas outre mesure, car le nom de salon qui s’en approchait le plus dans mon jeu d’entraînement était “MECH EN LOOK”, qui ne contenait même pas le mot “MECHE” en entier. Finalement, je suis plutôt agréablement surpris des performances du modèle (comme souvent en learning , même si dans notre cas, le modèle et son ambition sont très modestes).

Les résultats

En exécutant notre classificateur sur toute la base des noms de salons, on obtient une valeur d’estimation de :

916, soit 3% de coiffeurs (contre environ 5% par la méthode par sondage)

présentant un jeu de mots dans leur enseigne. Ce chiffre correspond à l’estimation basse obtenue par sondage en partie 1. Ceci est cohérent avec notre remarque faite plus haut : ce chiffre obtenu par apprentissage est une sous-estimation du nombre total.

Dernière remarque : ce modèle est valable uniquement pour les enseignes de salons de coiffure. On pourrait appliquer une méthode similaire pour déterminer le nombre de jeu de mots parmi les enseignes d’un autre secteur (les boulangeries par exemple), en changeant le training set et en ajustant le modèle. Mais trouver une méthode générale pour reconnaître ce qui constitue ou non un jeu de mots en français serait une autre paire de manches !

Note 1 : L’image-titre est issue d’une note de Boulet, qui possède un blog bd très sympa que je vous encourage à aller voir !

Note 2 : en échangeant à propos de cet article, on m’a fait remarquer que notre définition du jeu de mot n’incluait pas par exemple le cas d’un jeu de mot “pour initié” sur le quartier dans lequel est situé la boutique ou sur le nom des entrepreneurs. Je précise donc que notre définition recouvre plus ou moins les jeux de mots “compréhensibles par tous” 😉

[Sports] Best/Worst NBA matchups ever

Earlier this month, the Philadelphia 76ers grabbed their first (and for now, only) win of the season by beating the Lakers. That night, checking for the menu on the NBA League Pass, I quickly elected not to watch this pretty unappealing matchup. Though I couldn’t help thinking that it is a shame for franchises that have both won NBA titles and see legends of the game wear their jerseys. During many seasons, a Lakers-Sixers game meant fun, excitement and most important of all excellent basketball. And I started wondering what were the best and worst matchups throughout NBA history.

Data and model

My criterion to evaluate these matchups will be the mean level of the two teams during each season. In fact, by “good” matchup I mean a game that feature two excellent ball clubs, making it an evening every NBA fan awaits impatiently as soon as the season calendar is made available. On the contrary, a “bad” matchup is a game whose only stake will be to determine draft pick orders. My criterion does not predict the actual interest of watching these games: a confrontation between two top teams might very well be pretty boring if the star players are having a bad night (or if the coach decides to bench them). Also, a contest between two mediocre teams might very well finally be a three-OT thriller with players showing excellent basketball skills. In fact, my criterion only holds from an historical perspective (or in the very unlikely case that you have to choose between the replay of several games without knowing when these games were played 🙂 ).

The level of each team is estimated using the excellent FiveThirtyEight NBA Elo rankings. Then, to rank the 435 possible matchups between the 30 NBA franchises, I will average the mean level of every two teams for all years between 1977 and 2015 (I chose to limit the analysis to after the NBA-ABA merger of 1976 so as to avoid dealing with defunct franchises). You can found the R codes I used on my GitHub page.

The best matchups

Of course, our method values regularity, so it’s no surprise we find at the top matchups between the teams that have been able to maintain a high level of competitivity throughout the years. In fact, the best matchup ever is “Lakers – Spurs”, two teams that have missed the playoffs only respectively 5 and 4 years since the 1976-1977 season! “Celtics – Lakers” comes in 6th: basketball fans won’t be suprised to find this legendary rivalry ranked up high. It might even have been higher if I had taken seasons prior to the merger into account. The first 10 matchups are:

1. “Lakers – Spurs”
2. “Lakers – Suns”
3. “Lakers – Trailblazers”
4. “Lakers – Thunder”
5. “Suns – Spurs”
6. “Celtics – Lakers”
7. “Spurs – Trailblazers”
8. “Spurs – Thunder”
9. “Rockets – Lakers”
10. “Spurs – Heat”

The worst matchups

The worst matchup ever is “Timberwolves vs. Hornets”. Thinking about the last few years, I have to admit that these games were clearly not among my favorites. Poor Michael Jordan’s Hornets trust the last 7 places on the ranking, thanks to the inglorious Bobcats run.

Among the most infamous matchups ever are:

425. “Clippers – Timberwolves”
426. “Clippers – Nets”
427. “Raptors – Hornets”
428. “Wizards – Timberwolves”

434. “Kings – Hornets”
435. “Timberwolves – Hornets”

I really hope the owners of these franchises are able to turn the tide and put their teams back up in the rankings soon!