Reglas de asociación (y detección de anomalías) con futbolistas usando R y estadísticas de FIFA

Aprovecho la cuarentena provocada por la pandemia del coronavirus para adaptar para esta web un trabajo que realicé para la asignatura Minería de datos: Aprendizaje no supervisado y detección de anomalías del Máster de Ciencia de Datos de la Universidad de Granada.

Las reglas de asociación son una técnica de aprendizaje no supervisado usada para extraer información relevante a partir de grandes conjuntos de datos. Sin entrar mucho en detalle, cada regla de asociación tiene vinculadas distintas medidas numéricas para determinar la relevancia de la regla. Su punto fuerte es la interpretabilidad, que cada vez tiene más importancia en el mundo del machine learning.

Un ejemplo de regla de asociación es \(A \Rightarrow B\), donde \(A\) y \(B\) son conjuntos de items (itemsets) disjuntos. \(A\) y \(B\) son llamados, respectivamente, el antecedente y el consecuente de la regla.

Un ejemplo aplicado al mundo del fútbol podría ser:

\[ \text{Si un jugador es defensa} \Rightarrow \text{Su tiro es malo} \]
Esto no quiere decir que TODOS los defensas tiren mal, sino que en general cuando un jugador es defensa su tiro es malo. También podría leerse como “los defensas suelen tirar mal”.

1. Introducción al conjunto de datos

FIFA Ultimate Team es un modo de juego online de la serie de videojuegos de fútbol FIFA, al que juegan más de 10 millones de usuarios anualmente. En este modo de juego, los jugadores reales de fútbol se representan como cartas (o cromos) que pueden ser vendidos o comprados a cambio de dinero virtual. Las cartas virtuales de los jugadores tienen estadísticas asociadas (velocidad, pase, defensa, …) para hacer que sean parecidas a los jugadores reales.

Ejemplo de equipo de FIFA Ultimate Team.

Futbin es una página web que permite conocer las estadísticas de los jugadores, así como su precio actual.

Estadísticas y precios de Soldado en Futbin.

El conjunto de datos que se analiza a continuación está obtenido mediante web scraping en Futbin y contiene todos los jugadores que tienen precio distinto de cero, no son porteros y tienen una puntuación media mayor o igual a 75. El web scraping se realizó el 20 de enero de 2020.

Tanto el conjunto de datos como el preprocesamiento realizado se omiten en esta publicación, aunque están disponibles en GitHub.

2. Carga de paquetes

library(dplyr)      # Para usar pipes (%>%), select, filter, ...
library(arules)     # Para trabajar con reglas de asociación
library(arulesViz)  # Para visualizar reglas de asociación

3. Descripción del conjunto de datos

El conjunto de datos contiene información sobre 1841 jugadores de fútbol, en 16 variables. No hay ningún dato faltante.

# Número de filas y columnas
dim(futbin)
## [1] 1841   16

# Comprobación de datos faltantes
sum(is.na(futbin))
## [1] 0

Se describe a continuación el significado de cada variable:

  • name: Nombre del jugador.

  • rating: Puntuación global del jugador (de 75 a 99).

  • skills: Número de filigranas (de 1 a 5). Cuanto mayor es este número, más filigranas podrá hacer el jugador.

  • weak_foot: Número de pie malo (de 1 a 5). Cuanto mayor es este número, mejor jugará el jugador con su pie malo.

  • pac, sho, pas, dri, def, phy: Estadísticas de velocidad, disparo, pase, regate, defensa y físico del jugador (de 1 a 99).

  • hei: Altura del jugador, en centímetros.

  • popularity: Popularidad del jugador, según el voto (positivo o negativo) realizado por la comunidad de Futbin.

  • ps: Precio del jugador en la plataforma de PlayStation 4.

  • atacante, mediocentro, defensa: Indican la posición del jugador. Toman los valores “si” o “no”. Estas variables dummy se introducen para poder encontrar reglas de asociación con elementos negados (p.ej. defensa = no).

Se muestra la cabecera del conjunto de datos.

head(futbin, 10)
##                 name rating skills weak_foot pac sho pas dri def phy hei
## 1       Lionel Messi     94      4         4  87  92  92  96  39  66 170
## 2  Cristiano Ronaldo     93      5         4  90  93  82  89  35  78 187
## 3          Neymar Jr     92      5         5  91  85  87  95  32  58 175
## 4    Kevin De Bruyne     91      4         5  76  86  92  87  61  78 181
## 5        Eden Hazard     91      4         4  91  83  86  94  35  66 175
## 6      Mohamed Salah     90      4         3  93  86  81  89  45  74 175
## 7    Virgil van Dijk     90      2         3  77  60  70  72  90  86 193
## 8        Luka Modric     90      4         4  74  76  89  90  72  66 172
## 9  Giorgio Chiellini     89      2         3  68  46  58  62  90  82 187
## 10     Sergio Agüero     89      4         4  80  90  77  88  33  74 173
##    popularity     ps atacante mediocentro defensa
## 1        5267 958000       si          no      no
## 2        3393 920000       si          no      no
## 3        4662 762000       si          no      no
## 4        2493 189000       no          si      no
## 5        2164 199000       si          no      no
## 6        2909 150000       si          no      no
## 7        3627 490000       no          no      si
## 8         827  56000       no          si      no
## 9         326  41500       no          no      si
## 10       3293  78000       si          no      no

Eliminamos del conjunto el nombre de los jugadores, guardándolo previamente en una variable.

nombres_jugadores <- futbin$name
futbin <- futbin %>% select(-name)

4. Reglas de asociación

4.1. Preparación

Como vamos a usar el método a priori, las variables continuas deben convertirse a categóricas (factores), especificando los valores de corte. Otros métodos como MOPNAR realizan de manera autónoma los cortes que considera más oportunos en las variables numéricas.

# Puntuación global
futbin$rating <- ordered(cut(futbin$rating,
                            unique(quantile(futbin$rating)),
                            include.lowest = TRUE))
# Velocidad (análogo para disparo, pase, regate, defensa y físico)
futbin$pac <- ordered(cut(futbin$pac,
                          c(0, 65, 75, 80, 100), 
                          labels = c("Muy bajo", "Bajo", "Alto", "Muy alto"),
                          include.lowest = TRUE))
# Altura
futbin$hei <- ordered(cut(futbin$hei, c(0, 177, 185, 205),
                          labels = c("Bajo", "Alto", "Muy alto"),
                          include.lowest = TRUE))
# Popularidad
futbin$popularity <- ordered(cut(futbin$popularity,
                                 quantile(futbin$popularity),
                                 include.lowest = TRUE))
# Precio
futbin$ps <- ordered(cut(futbin$ps,
                         quantile(futbin$ps),
                         include.lowest = TRUE))
# Pie malo
futbin$weak_foot <- factor(futbin$weak_foot,
                          levels = c("1", "2", "3", "4", "5"),
                          ordered = TRUE)
# Filigranas
futbin$skills    <- factor(futbin$skills,
                          levels = c("1", "2", "3", "4", "5"),
                          ordered = TRUE)

Se convierte el conjunto de datos a tipo transacciones.

futbin_transacciones <- as(futbin, "transactions")

Se muestran gráficamente los items frecuentes (itemsets sólo de tamaño 1) con soporte mayor o igual del 30%.

itemFrequencyPlot(futbin_transacciones, support = 0.3, cex.names = 0.8)

4.2. Extracción de reglas con método a priori

Se realiza el método a priori de reglas de asociación, con 10% como mínimo de soporte de un itemset.

ifutbin_transacciones <- apriori(futbin_transacciones,
                                parameter = list(support = 0.1,
                                                 target = "frequent"),
                                control = list(verbose = FALSE))

# Se ordenan las reglas por el valor del soporte
ifutbin_transacciones <- sort(ifutbin_transacciones, by = "support") 

# Itemsets frecuentes
barplot(table(size(ifutbin_transacciones)),
        xlab = "Tamaño de itemset", ylab = "Frecuencia",
        main = "Tamaños de itemsets en los itemsets frecuentes")

Se calculan los itemsets maximales y cerrados:

# Itemsets maximales
imaxfutbin_transacciones <- ifutbin_transacciones[is.maximal(ifutbin_transacciones)]

# Itemsets cerrados
iclofutbin_transacciones <- ifutbin_transacciones[is.closed(ifutbin_transacciones)]

Se muestran en un gráfico el número de itemsets frecuentes, cerrados y maximales:

barplot(c(Frecuentes = length(ifutbin_transacciones),
          Cerrados = length(iclofutbin_transacciones),
          Maximales = length(imaxfutbin_transacciones)),
        ylab = "Frecuencia", xlab = "Tipo de itemsets")

Extracción de reglas: se exigen al menos dos elementos de la regla (antecedente y consecuente), un soporte mínimo del 10% y una confianza del 80%.

# Extracción de reglas
rules <- apriori(futbin_transacciones,
                 parameter = list(support = 0.1, confidence = 0.8, minlen = 2),
                 control = list(verbose = FALSE))

# Se ordenan las reglas por confianza
rulesSorted <- sort(rules, by = "confidence")

Se eliminan las reglas que son redundantes, esto es, reglas que están incluidas dentro de otras (antecedente de una regla incluida en el antecedente de otra regla).

# Matriz con todas las reglas como nombres de filas y columnas.
# is.subset comprueba para cada regla qué elementos son subconjuntos
# de todas las reglas una a una
subsetMatrix <- is.subset(rulesSorted, rulesSorted)

# Se filtran ahora los que han salido contenidas en 2 o más reglas
# porque como mínimo, cada regla es un subconjunto de sí misma
redundant <- colSums(subsetMatrix, na.rm = TRUE) >= 2 

# Se eliminan las reglas redundantes
rulesPruned <- rulesSorted[!redundant] 

Se eliminan ahora las reglas con confianza 1, que son fruto de que las variables atacante, mediocentro y defensa sean excluyentes.

# Se muestran las reglas con confianza 1
subset(rulesPruned, subset = confidence == 1) %>% inspect
##     lhs                 rhs              support   confidence coverage 
## [1] {atacante=si}    => {mediocentro=no} 0.2368278 1          0.2368278
## [2] {atacante=si}    => {defensa=no}     0.2368278 1          0.2368278
## [3] {defensa=si}     => {mediocentro=no} 0.3096143 1          0.3096143
## [4] {defensa=si}     => {atacante=no}    0.3096143 1          0.3096143
## [5] {mediocentro=si} => {defensa=no}     0.4535578 1          0.4535578
## [6] {mediocentro=si} => {atacante=no}    0.4535578 1          0.4535578
##     lift     count
## [1] 1.830020 436  
## [2] 1.448466 436  
## [3] 1.830020 570  
## [4] 1.310320 570  
## [5] 1.448466 835  
## [6] 1.310320 835

# Se eliminan esas reglas
reglas_seleccionadas <- subset(rulesPruned, subset = confidence < 1)

Se seleccionan sólo aquellas reglas que tienen lift > 1, lo que implica una dependencia positiva entre antecedente y consecuente.

reglas_seleccionadas <- subset(reglas_seleccionadas, subset = lift > 1)

Se pueden examinar, por ejemplo, aquellas reglas en las que la altura de los jugadores aparece como antecedente.

reglas_altura <- subset(reglas_seleccionadas, subset = lhs %pin% "hei")

# Se muestra la regla con mayor confianza
inspect(head(reglas_altura, 1)) 
##     lhs           rhs          support   confidence coverage  lift     count
## [1] {hei=Bajo} => {defensa=no} 0.2466051 0.8239564  0.2992939 1.193473 454

Se podría concluir que los jugadores bajos no suelen ser defensas.

4.3. Selección de reglas relevantes

Se seleccionará un conjunto pequeño de reglas que puedan ser relevantes, en el sentido de que aporten información que no es obvia.

# Añadimos a las reglas varias medidas de interés:
mInteres <- interestMeasure(reglas_seleccionadas,
                            measure = c("gini", "chiSquared"),
                            transactions=futbin_transacciones)
quality(reglas_seleccionadas) <- cbind(quality(reglas_seleccionadas), mInteres)

Se buscan reglas con valores de soporte comprendidos entre 20% y 40% aproximadamente. Esto hará que se encuentren reglas que no son muy generales, y son lo suficientemente poco frecuentes como para que sean relevantes. Se seleccionan las 5 reglas de asociación que tienen mejor medida de interés gini.

subset(reglas_seleccionadas, subset = support > .2 & support < .4) %>%
  head(by = "gini", n = 5) %>%
  inspect
##     lhs               rhs              support   confidence coverage  lift    
## [1] {defensa=si}   => {sho=Muy bajo}   0.2819120 0.9105263  0.3096143 2.253063
## [2] {atacante=si}  => {def=Muy bajo}   0.2357414 0.9954128  0.2368278 2.184213
## [3] {pas=Muy bajo} => {mediocentro=no} 0.2346551 0.9250535  0.2536665 1.692866
## [4] {sho=Bajo}     => {defensa=no}     0.3623031 0.9315642  0.3889191 1.349339
## [5] {skills=4}     => {defensa=no}     0.2819120 0.9351351  0.3014666 1.354511
##     count gini       chiSquared
## [1] 519   0.23000828 879.2154  
## [2] 434   0.18076588 670.8387  
## [3] 432   0.09744229 361.9048  
## [4] 667   0.07404027 318.8445  
## [5] 519   0.05170417 222.6571

Por interpretar algunas reglas, se podría concluir que los defensas tienen disparo muy bajo (regla 1), los atacantes defienden muy mal (regla 2), y los jugadores con pase muy bajo no suelen ser centrocampistas (regla 3).

4.4. Visualización de las reglas de asociación

Se visualizan ahora las 10 reglas más relevantes encontradas, usando distintos métodos de visualización del paquete arulesViz.

# Tipo grafo
reglas_seleccionadas %>%
  head(by = "gini", n = 10) %>%
  plot(method = "graph")

# Tipo matriz (Antecedente por columnas, consecuente por filas)
reglas_seleccionadas %>%
  head(by = "gini", n = 10) %>%
  plot(method = "grouped")

# Tipo coordenadas paralelas
reglas_seleccionadas %>%
  head(by = "gini", n = 10) %>%
  plot(method = "paracoord", reorder = TRUE)

4.5. Detección de anomalías

Se mostrarán las reglas con mayor confianza para luego examinar los jugadores que no las cumplen, que podrían ser considerados excepciones o anomalías.

subset(reglas_seleccionadas, subset = support > .15 & support < .4) %>%
  head(by = "confidence", n = 8) %>%
  inspect
##     lhs               rhs            support   confidence coverage  lift    
## [1] {def=Bajo}     => {atacante=no}  0.2721347 0.9980080  0.2726779 1.307710
## [2] {def=Alto}     => {atacante=no}  0.2069527 0.9973822  0.2074959 1.306890
## [3] {atacante=si}  => {def=Muy bajo} 0.2357414 0.9954128  0.2368278 2.184213
## [4] {sho=Alto}     => {defensa=no}   0.1553504 0.9930556  0.1564367 1.438407
## [5] {skills=2}     => {atacante=no}  0.2015209 0.9686684  0.2080391 1.269266
## [6] {phy=Muy bajo} => {defensa=no}   0.1933732 0.9621622  0.2009777 1.393659
## [7] {dri=Muy alto} => {defensa=no}   0.1591526 0.9482201  0.1678436 1.373464
## [8] {skills=2}     => {sho=Muy bajo} 0.1950027 0.9373368  0.2080391 2.319405
##     count gini       chiSquared
## [1] 501   0.04135059 210.5961  
## [2] 381   0.02872427 146.2910  
## [3] 434   0.18076588 670.8387  
## [4] 286   0.03397735 146.3189  
## [5] 371   0.02218600 112.9919  
## [6] 356   0.03715719 160.0125  
## [7] 293   0.02681708 115.4842  
## [8] 359   0.14937075 570.9754
# Se recuperan los nombres de los jugadores
futbin_w_names <- cbind(name = nombres_jugadores, futbin)

¿Hay atacantes que saben defender? (Reglas 1, 2 y 3)

futbin_w_names %>% filter(def == "Bajo" & atacante != "no") %>%
  select(name)
##                   name
## 1 Kevin-Prince Boateng

futbin_w_names %>% filter(def == "Alto" & atacante != "no") %>%
  select(name)
##           name
## 1 Marcos Acuña

futbin_w_names %>% filter(atacante == "si" & def != "Muy bajo") %>%
  select(name)
##                   name
## 1         Marcos Acuña
## 2 Kevin-Prince Boateng

Hay dos excepciones: Kevin-Prince Boateng y Acuña, dos atacantes que tienen buenas estadísticas defensivas. De hecho, aunque Boateng aparezca como delantero, durante gran parte de su carrera ha jugado de centrocampista, como demuestra el hecho de que desde FIFA 10 hasta FIFA 19 siempre ha aparecido como centrocampista. El caso de Acuña es similar: aunque en el juego aparece como extremo izquierdo, suele jugar de interior izquierdo, carrilero o incluso lateral.

¿Hay defensas con buen disparo? (Regla 4)

futbin_w_names %>%
  filter(defensa == "si" & sho == "Alto") %>%
  select(name) 
##                  name
## 1 Alessandro Florenzi
## 2           Vieirinha

Florenzi (70 goles en 391 partidos) y Vierinha (51 goles en 413 partidos) son de hecho los dos defensas con mejor tiro en el videojuego, y sus estadísticas reales de goles son excelentes para tratarse de defensas.

¿Hay delanteros con pocas filigranas? (Regla 5)

futbin_w_names %>%
  filter(atacante == "si" & skills == "2") %>%
  select(name, hei, pac) 
##                   name      hei      pac
## 1           Edin Džeko Muy alto Muy bajo
## 2          Diego Costa Muy alto     Bajo
## 3         Luuk de Jong Muy alto Muy bajo
## 4             Bas Dost Muy alto Muy bajo
## 5          Troy Deeney     Alto Muy bajo
## 6         Glenn Murray     Alto Muy bajo
## 7           Chris Wood Muy alto     Bajo
## 8  Klaas-Jan Huntelaar Muy alto Muy bajo
## 9    Christian Benteke Muy alto Muy bajo
## 10      Charlie Austin Muy alto Muy bajo
## 11        Andy Carroll Muy alto Muy bajo
## 12       Stefano Okaka Muy alto Muy bajo

Son delanteros toscos, y en su mayoría muy altos, superando algunos los 190 centímetros.

¿Hay jugadores con físico muy bajo que sean defensas? (Regla 6)

futbin_w_names %>%
  filter(phy == "Muy bajo" & defensa == "si") %>%
  select(name) %>%
  t %>%
  as.vector
##  [1] "Aarón Martín"    "Emerson"         "Danilo"          "Vieirinha"      
##  [5] "Júnior Caiçara"  "Phil Jagielka"   "Diogo Viana"     "Raúl Navas"     
##  [9] "Leighton Baines" "Álex Moreno"     "Scott Dann"      "Yuto Nagatomo"  
## [13] "Zaldúa"          "Martín"

Son jugadores en su mayor parte laterales, que no necesitan una gran corpulencia para defender.

¿Hay defensas que regatean muy bien? (Regla 7)

futbin_w_names %>%
  filter(dri == "Muy alto" & defensa == "si") %>%
  select(name) %>%
  t %>%
  as.vector
##  [1] "Jordi Alba"      "Joshua Kimmich"  "Marcelo"         "Carvajal"       
##  [5] "João Cancelo"    "Alex Telles"     "Grimaldo"        "Ricardo Pereira"
##  [9] "Nélson Semedo"   "Ismaily"         "Bernat"          "Kwadwo Asamoah" 
## [13] "Rubén Peña"      "Mitchell Weiser" "Youcef Atal"     "Vieirinha"

La mayoría son laterales de primer nivel mundial: Jordi Alba, Marcelo, …

¿Hay jugadores con pocas filigranas que no tiren muy mal? (Regla 8)

futbin_w_names %>%
  filter(skills == "2" & sho != "Muy bajo") %>%
  select(name) %>%
  t %>%
  as.vector
##  [1] "Casemiro"            "Blaise Matuidi"      "Edin Džeko"         
##  [4] "Sami Khedira"        "Diego Costa"         "Luuk de Jong"       
##  [7] "Bas Dost"            "Lukasz Piszczek"     "Luka Milivojevic"   
## [10] "Fabian Schär"        "Andreas Samaris"     "Leander Dendoncker" 
## [13] "Manu García"         "Troy Deeney"         "Mikel San José"     
## [16] "Glenn Murray"        "Chris Wood"          "Klaas-Jan Huntelaar"
## [19] "Marco Höger"         "Oliver Norwood"      "Christian Benteke"  
## [22] "Charlie Austin"      "Andy Carroll"        "Stefano Okaka"

Son jugadores torpes con el balón pero con buen tiro, algunos por ser delanteros rematadores (Diego Costa, Andy Carroll) y otros por ser centrocampistas o defensas con llegada desde fuera del área, o grandes cabeceadores (Casemiro, Khedira, San José).