Pokémons Legendarios
Se trata de un listado de todos los pokémons desde la primera generación hasta la sexta. Se encuentra disponible en kaggle en la siguiente ubicación: https://www.kaggle.com/abcsds/pokemon.
Se trata de entender mejor cómo son los pokémons, tener datos para ver qué diferencias hay entre generaciones, entre tipologías, e incluso ser capaces de tener datos que nos ayuden a crear el mejor escuadrón de pokémons entre jugadores serios.
En nuestro caso, vamos a tratar de resolver una pregunta sencilla: ¿Podríamos predecir los pokémons legendarios por sus stats? En la práctica, crearemos un clasificador que nos ayude a distinguir entre los pokémons legendarios y no legendarios.
Veamos primero un resumen de estos campos
summary(input_data)
## ID Pokémon Name Type 1 Type 2
## Min. : 1.0 Length:800 Length:800 Length:800
## 1st Qu.:184.8 Class :character Class :character Class :character
## Median :364.5 Mode :character Mode :character Mode :character
## Mean :362.8
## 3rd Qu.:539.2
## Max. :721.0
## Total HP Attack Defense
## Min. :180.0 Min. : 1.00 Min. : 5 Min. : 5.00
## 1st Qu.:330.0 1st Qu.: 50.00 1st Qu.: 55 1st Qu.: 50.00
## Median :450.0 Median : 65.00 Median : 75 Median : 70.00
## Mean :435.1 Mean : 69.26 Mean : 79 Mean : 73.84
## 3rd Qu.:515.0 3rd Qu.: 80.00 3rd Qu.:100 3rd Qu.: 90.00
## Max. :780.0 Max. :255.00 Max. :190 Max. :230.00
## Sp. Atk Sp. Def Speed Generation
## Min. : 10.00 Min. : 20.0 Min. : 5.00 Min. :1.000
## 1st Qu.: 49.75 1st Qu.: 50.0 1st Qu.: 45.00 1st Qu.:2.000
## Median : 65.00 Median : 70.0 Median : 65.00 Median :3.000
## Mean : 72.82 Mean : 71.9 Mean : 68.28 Mean :3.324
## 3rd Qu.: 95.00 3rd Qu.: 90.0 3rd Qu.: 90.00 3rd Qu.:5.000
## Max. :194.00 Max. :230.0 Max. :180.00 Max. :6.000
## Legendary
## Mode :logical
## FALSE:735
## TRUE :65
##
##
##
Lo cual nos indica ya alguna cosa interesante, como por ejemplo que de las 800 líneas, hay 65 correspondientes a pokémons legendarios… ¿Qué hace que un pokémon sea o no legendario?
Otra cosa que llama la atención es en el primer campo, el identificador, vemos que nos puede dar problemas a la hora de invocarlo porque la almohadilla tiene otros usos. Así que vamos a renombrarlo. Vemos además que tenemos un total de 721 pokémons diferentes. Las repeticiones vemos que se tratan de, por ejemplo, las “mega evoluciones”, que no están identificadas en el set de datos y vamos a tratar de discernir también.
Vamos a ver además si en estos campos tenemos valores nulos que deban ser tratados.
na_count <- sapply(input_data, function(y) sum(length(which(is.na(y)))))
data.table::data.table("Nombre del campo" = names(na_count),"#NAs" = na_count)
## Nombre del campo #NAs
## 1: ID Pokémon 0
## 2: Name 0
## 3: Type 1 0
## 4: Type 2 386
## 5: Total 0
## 6: HP 0
## 7: Attack 0
## 8: Defense 0
## 9: Sp. Atk 0
## 10: Sp. Def 0
## 11: Speed 0
## 12: Generation 0
## 13: Legendary 0
Vemos que el único que tiene valores nulos es el de la tipología del pokémon. Pero en este caso son valores reales, porque no todos los pokémons tienen dos tipologías. Por simplicidad sólo tendremos en cuenta el primer tipo, porque es el que está documentado como el tipo principal.
Para evitar duplicados en pokémons con características diferentes (por ejemplo, megaevoluciones, pokémons de diferentes tamaños con stats diferenciadas…), vamos a coger sólo la primera línea de cada uno de los pokémons con ID repetida.
### Como los datos ya vienen ordenados convenientemente, vamos a hacer un loop sobre cada fila y comprobaremos si la ID es la misma que el anterior, en cuyo caso lo marcaremos para borrar
transformed_data <- input_data
transformed_data$`Type 2` <- NULL ## quitamos la columna
transformed_data$RemoveRow <- FALSE
for(i_row in seq(nrow(transformed_data))){
if(i_row < 2) next
if(transformed_data[i_row,]$`ID Pokémon` == transformed_data[i_row-1,]$`ID Pokémon`)
transformed_data[i_row,]$RemoveRow <- TRUE
}
transformed_data <- subset(transformed_data, !transformed_data$RemoveRow)
transformed_data$RemoveRow <- NULL
paste0("Numero de filas: ", nrow(transformed_data),", Numero de Pokémons: ", length(unique(transformed_data$`ID Pokémon`)))
## [1] "Numero de filas: 721, Numero de Pokémons: 721"
Vemos que tras esta limpieza, tenemos efectivamente una tabla con tantas filas como pokémons listados.
Como discutíamos en la sección anterior, el único campo con valores nulos es el de Type 2, ya que se trata de la tipología secundaria del pokémon, no lo tendremos en cuenta.
Vamos a crear un boxplot para buscar los outliers entre los campos cuantitativos del set de datos.
quanti_fields <- c("HP", "Attack","Defense","Sp. Atk","Sp. Def", "Speed")
boxplot(transformed_data$HP, transformed_data$Attack, transformed_data$Defense, transformed_data$`Sp. Atk`,transformed_data$`Sp. Def`, transformed_data$Speed,
names = quanti_fields,
pars=list(outcol="red"))
Vemos que tras hacer un boxplot, unos cuantos puntos que quedan fuera. Vamos a echar un vistazo a qué puntos podrían ser:
head(transformed_data[order(transformed_data$HP,decreasing = T),])
## # A tibble: 6 x 12
## `ID Pokémon` Name `Type 1` Total HP Attack Defense `Sp. Atk` `Sp. Def`
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 242 Blis… Normal 540 255 10 10 75 135
## 2 113 Chan… Normal 450 250 5 5 35 105
## 3 202 Wobb… Psychic 405 190 33 58 33 58
## 4 321 Wail… Water 500 170 90 45 90 45
## 5 594 Alom… Water 470 165 75 80 40 45
## 6 143 Snor… Normal 540 160 110 65 65 110
## # … with 3 more variables: Speed <dbl>, Generation <dbl>, Legendary <lgl>
Vemos que estos pokémons con muchísima defensa, a la vez tienen el resto de stats bastante bajas… ¿Es posible que sobre el total haya menos outliers?
boxplot(transformed_data$Total, main = "Total")
Podemos verlo más en detalle en un histograma:
ggplot2::ggplot(transformed_data, aes(x=Total)) +
geom_histogram(binwidth=20, colour="black")
Aunque sea sólo por curiosidad, veamos cuáles son esos valores tan altos de puntuación total:
head(transformed_data[order(transformed_data$Total,decreasing = T),])
## # A tibble: 6 x 12
## `ID Pokémon` Name `Type 1` Total HP Attack Defense `Sp. Atk` `Sp. Def`
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 493 Arce… Normal 720 120 120 120 120 120
## 2 150 Mewt… Psychic 680 106 110 90 154 90
## 3 249 Lugia Psychic 680 106 90 130 90 154
## 4 250 Ho-oh Fire 680 106 130 90 110 154
## 5 384 Rayq… Dragon 680 105 150 90 150 90
## 6 483 Dial… Steel 680 100 120 120 150 100
## # … with 3 more variables: Speed <dbl>, Generation <dbl>, Legendary <lgl>
Efectivamente, ¡parece que los pokémons legendarios son los valores más limite entre los pokémons! Al menos desde lo alto.
¿Y qué pasa con los valores más bajos?
head(transformed_data[order(transformed_data$Total,decreasing = F),])
## # A tibble: 6 x 12
## `ID Pokémon` Name `Type 1` Total HP Attack Defense `Sp. Atk` `Sp. Def`
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 191 Sunk… Grass 180 30 30 30 30 30
## 2 298 Azur… Normal 190 50 20 40 20 40
## 3 401 Kric… Bug 194 37 25 41 25 41
## 4 10 Cate… Bug 195 45 30 35 20 20
## 5 13 Weed… Bug 195 40 35 30 20 20
## 6 265 Wurm… Bug 195 45 45 35 20 30
## # … with 3 more variables: Speed <dbl>, Generation <dbl>, Legendary <lgl>
Parece que los pokémon de tipo “hierba” y “bicho” son de los que tienen peores stats. No parece tampoco que pueda ser incorrecto, al final son los primeros pokémons que nos encontramos en el juego, pero rara vez se utilizan en muy grandes niveles por algo.
Por tanot, en resumen, no identificamos valores extremos que deban ser tratados porque los pokémons se han diseñado para que tengan stats diferenciados uno a uno.
Vamos a tener en cuenta para este apartado que el objetivo es esutdiar las diferencias entre la población de pokémons legendarios y no legendarios.
Vamos a aplicar el test de Shappiro Wilk sobre las variables cuantitativas.
Probémoslo primero para el total:
cat("Legendary Pokémons:")
## Legendary Pokémons:
legendary_data <- subset(transformed_data, transformed_data$Legendary == T)
shapiro.test(legendary_data$Total)
##
## Shapiro-Wilk normality test
##
## data: legendary_data$Total
## W = 0.75685, p-value = 2.424e-07
cat("Non Legendary Pokémons:")
## Non Legendary Pokémons:
non_legendary_data <- subset(transformed_data, transformed_data$Legendary == F)
shapiro.test(non_legendary_data$Total)
##
## Shapiro-Wilk normality test
##
## data: non_legendary_data$Total
## W = 0.96314, p-value = 5.483e-12
Debería tener un p-value > 0.05, que no es el caso.
Miremos ahora para el resto de variables cuantitativas:
pvalue_shapirotest <- function(df){
shapiro.test(df)$p.value
}
cat("Legendary Pokémons:\n")
## Legendary Pokémons:
apply(legendary_data[,quanti_fields], 2, pvalue_shapirotest)
## HP Attack Defense Sp. Atk Sp. Def Speed
## 0.078407400 0.322302083 0.006845207 0.091960782 0.018099079 0.012586851
cat("\nNon Legendary Pokémons:\n")
##
## Non Legendary Pokémons:
apply(non_legendary_data[,quanti_fields], 2, pvalue_shapirotest)
## HP Attack Defense Sp. Atk Sp. Def Speed
## 1.345819e-21 5.942148e-05 1.244077e-16 4.669263e-09 6.968816e-14 3.746209e-06
Vemos que variable por variable, para los pokémons legendarios quizá sí que podríamos asumir que se distribuye normalmente. No así para los pokémons no legendarios.
Aunque quizá no tenga mucho sentido realizar el test sin que cumpla la normalidad, lo realizaremos por propósitos didácticos. Realizaremos el test de Bartlet.
Probémoslo primero para el total:
bartlett.test(transformed_data$Total, g = paste0(transformed_data$Legendary))
##
## Bartlett test of homogeneity of variances
##
## data: transformed_data$Total and paste0(transformed_data$Legendary)
## Bartlett's K-squared = 33.842, df = 1, p-value = 5.979e-09
Como era de esperar, los datos no pasan el test de homocedasticidad.
Como los datos no muestran ser homogéneos en las varianzas ni se distribuyen normalmente, vamos a tener que hacer análisis más robustos. HAn sido barajadas pruebas estadísticas no paramétricas equivalentes a, por ejemplo, ANOVA, como el test de KRuskall-Wallis, pero estos tests siguen esperando que nuestros datos estén distribuidos con una función de densidad parecida, que sólo con ver las diferencias en el test de normalidad ya vemos que no.
Por ello vamos a evitar utilizar tests de hipótesis y regresiones, que parece que no sean adecuadas para nuestros datos.
Vamos a ver si reduciendo las dimensiones, podemos gráficamente distinguir a los pokémons legendarios del resto. Vamos a probar con un análisis de componentes princiupales
quanti_data <- transformed_data[,c(quanti_fields)]
poke_pca <- prcomp(quanti_data)
summary(poke_pca)
## Importance of components:
## PC1 PC2 PC3 PC4 PC5 PC6
## Standard deviation 45.1232 29.7676 25.8174 22.6353 18.83015 14.58333
## Proportion of Variance 0.4361 0.1898 0.1428 0.1098 0.07595 0.04556
## Cumulative Proportion 0.4361 0.6260 0.7687 0.8785 0.95444 1.00000
Vemos que con las dos primeras componentes ya podemos explicar un 62% de la varianza de los datos.
Echemos un vistazo a ver qué tal podemos distinguir entre pokémons legendarios y no legendarios:
# ggplot(df_pca,aes(x=PC1,y=PC2,color=Legendary )) + geom_point()
ggplot(transformed_data,aes(x=Total,y=Attack,color=Legendary )) + geom_point()
Vemos que con la primera componente tenemos una muy buena capacidad de predicción. ¿Cuál sería la matriz de confusión?
transformed_data$LegendaryMeth1a <- FALSE
transformed_data[poke_pca$x[,1] < -60,]$LegendaryMeth1a <- TRUE
table(transformed_data$Legendary, transformed_data$LegendaryMeth1a)
##
## FALSE TRUE
## FALSE 659 16
## TRUE 0 46
Antes hemos visto que entre los pokémons con un valor de stats total mayor, parecía que podía haber más legendarios. ¿Por qué no lo comparamos?
ggplot2::ggplot(transformed_data, aes(x=Total, color = Legendary)) +
geom_histogram(aes(color = Legendary, fill = Legendary),
position = "identity", binwidth = 25, alpha = 0.4) +
scale_color_manual(values = c("#00AFBB", "#E7B800")) +
scale_fill_manual(values = c("#00AFBB", "#E7B800"))
Por el método utilizando el total, habríamos tenido:
transformed_data$LegendaryMeth1b <- FALSE
transformed_data[transformed_data$Total > 575,]$LegendaryMeth1b <- TRUE
table(transformed_data$Legendary, transformed_data$LegendaryMeth1b)
##
## FALSE TRUE
## FALSE 660 15
## TRUE 0 46
Es decir, estaríamos cogiendo 15 falsos positivos vs 16.
Aquí además estamos viendo un objetivo secundario cumplido: ¿Podríamos identificar estos 15 pokémons que parecen legendarios pero no lo son? Seguro que son más fáciles de encontrar en el juego, pero siguen siendo muy buenos.
subset(transformed_data, transformed_data$LegendaryMeth1b == TRUE & transformed_data$Legendary == FALSE)
## # A tibble: 15 x 14
## `ID Pokémon` Name `Type 1` Total HP Attack Defense `Sp. Atk` `Sp. Def`
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 149 Drag… Dragon 600 91 134 95 100 100
## 2 151 Mew Psychic 600 100 100 100 100 100
## 3 248 Tyra… Rock 600 100 134 110 95 100
## 4 251 Cele… Psychic 600 100 100 100 100 100
## 5 289 Slak… Normal 670 150 160 100 95 65
## 6 373 Sala… Dragon 600 95 135 80 110 80
## 7 376 Meta… Steel 600 80 135 130 95 90
## 8 445 Garc… Dragon 600 108 130 95 80 85
## 9 488 Cres… Psychic 600 120 70 120 75 130
## 10 490 Mana… Water 600 100 100 100 100 100
## 11 635 Hydr… Dark 600 92 105 90 125 90
## 12 647 Keld… Water 580 91 72 90 129 90
## 13 648 Melo… Normal 600 100 77 77 128 128
## 14 649 Gene… Bug 600 71 120 95 120 95
## 15 706 Good… Dragon 600 90 100 70 110 150
## # … with 5 more variables: Speed <dbl>, Generation <dbl>, Legendary <lgl>,
## # LegendaryMeth1a <lgl>, LegendaryMeth1b <lgl>
¿No era Mew un pokémon legendario? Aparentemente, aunque Mewtwo (su clon) sí que es legendario, no es así Mew que se trata de un pokémon míticov https://bulbapedia.bulbagarden.net/wiki/Mythical_Pok%C3%A9mon, al igual que otros pokémons de esta lista: Celebi y Manaphy. Son descritos de forma semejante a los legendarios, como pokémons rarísimos y muy poderosos.
Mirando los datos del listado de míticos, aparentemente hay otros pokémons que son a la vez míticos y legendarios como Volcanion, pero está claro que es un factor de confusión. Pero no lo sería para la mayoría de estos pokémons.
Vamos a mirar si hay alguna diferencia entre las correlaciones entre variables de los pokémons legendarios o no legendarios.
Primero miremos los pokémons legendarios:
matriz <- cor(subset(quanti_data,transformed_data$Legendary == T))
m <- melt(matriz)
m$value_lab<-sprintf('%.2f',m$value)
ggplot(m, aes(Var2, Var1, fill = value, label=value_lab),color='blue') +
geom_tile() +
geom_text() +
xlab('')+
ylab('')+
theme_minimal()
y comparemos con los no legendarios:
matriz <- cor(subset(quanti_data,transformed_data$Legendary == F))
m <- melt(matriz)
m$value_lab<-sprintf('%.2f',m$value)
ggplot(m, aes(Var2, Var1, fill = value, label=value_lab),color='blue') +
geom_tile() +
geom_text() +
xlab('')+
ylab('')+
theme_minimal()
¡Es muy interesante! Los pokémons legendarios parece que nos obligan mucho más a tomar concesiones: Si quieres un pokémon con buena salud, deberás comprometer tu defensa en un legendario, pero con los no legendarios no. Una razón para que esto ocurra, es porque al tener en general stats muy altos nos obligan a comprometer o un pokémon con muy buena defensa y ataque podría resultar demasiada ventaja competitiva. Comprobémoslo entre los pokémons con unos stats totales más altos (>550):
matriz <- cor(subset(quanti_data,transformed_data$Legendary == F & transformed_data$Total > 550))
m <- melt(matriz)
m$value_lab<-sprintf('%.2f',m$value)
ggplot(m, aes(Var2, Var1, fill = value, label=value_lab),color='blue') +
geom_tile() +
geom_text() +
xlab('')+
ylab('')+
theme_minimal()
Resulta interesante, la suposición de que el valor de la suma total de stats podría ser un factor a tener en cuenta al mirar la matriz de correlación era acertada. Una vez filtrado por magnitudes semejantes, vemos que no sólo la primera idea de que los pokémons legendarios eran más exigentes a la hora de pedirnos concesiones era errónea… sino que resulta opuesta. Parece que los pokémons legendarios cumplen por ahora dos características:
Con toda esta información, añadimos a las variables cualitativas tres nuevos campos: 1. La suma del total 2. Defensa/Defensa Especial 3. Ataque/Ataque Especial
Parece que estas tres variables deberían darnos buen resultado para hacer un clasificador numérico.
quanti_data$Total <- transformed_data$Total
quanti_data$Legendary <- transformed_data$Legendary
quanti_data$ATAvsSp <- quanti_data$Attack / quanti_data$`Sp. Atk`
quanti_data$DEFvsSp <- quanti_data$Defense / quanti_data$`Sp. Def`
list_of_pokemons <- transformed_data$Name
Ahora vamos a hacer un sencillo clasificador bayesiano.
Primero, vamos a partir el set de datos en uno de entrenamiento y uno de test
set.seed(123123) #para asegurarnos la reproducibilidad
smp_size <- floor(0.70 * nrow(quanti_data))
train_ind <- sample(seq_len(nrow(quanti_data)), size = smp_size)
train <- quanti_data[train_ind, ]
test <- quanti_data[-train_ind, ]
fit <- rpart(Legendary ~ ., data= train, method = "class")
fancyRpartPlot(fit,sub = "")
Y comprobamos la matriz de confusión
table(test$Legendary,predict(fit,newdata = test, type = "class"))
##
## FALSE TRUE
## FALSE 202 2
## TRUE 1 12
Vemos que el clasificador a la hora de hacer el ajuste, no ha escogido las dos nuevas features que hemos creado. Por propósito didáctico, vamos a forzarle a que lo utilize a ver si el resultado es mucho peor.
Primero veamos una gráfica de dispersión, a ver qué aspecto tiene con dos de esas variables:
ggplot(quanti_data,aes(x=Total,y=ATAvsSp,color=Legendary )) + geom_point()
Veamos ahora qué tal funciona el clasificador
set.seed(123123) #para asegurarnos la reproducibilidad
smp_size <- floor(0.70 * nrow(quanti_data))
train_ind <- sample(seq_len(nrow(quanti_data)), size = smp_size)
train <- quanti_data[train_ind, c("Total","ATAvsSp","DEFvsSp","Legendary") ]
test <- quanti_data[-train_ind, c("Total","ATAvsSp","DEFvsSp","Legendary")]
fit <- rpart(Legendary ~ ., data= train, method = "class")
fancyRpartPlot(fit,sub = "")
table(test$Legendary,predict(fit,newdata = test, type = "class"))
##
## FALSE TRUE
## FALSE 201 3
## TRUE 1 12
Vemos que el performance es prácticamente igual. Si complicáramos algo más el clasificador, es posible que uno de estos ratios entre ataque y ataque especial ya bastara además del total para hacer un buen clasificador, que además al tener menos pasos en el árbol podríamos considerarlo como mejor para escalar.
¿Hemos obtenido el resultado buscado? En mi opinión sí, aunque siempre se podría realizar un análisis más en profundidad, comprobar distintos clasificadores, por ejemplo. Hemos aprendido qué hace que un pokémon sea legendario… No solo tienen muy buenas stats en general, sino que te fuerzan menos a tomar compromisos. Es decir. ¿que hace que un pokémon sea legendario? Su potencia y su versatilidad.