Exercice 1¶
Commençons par récupérer le jeu de données, puis regardons ce qu'il contient.
Note : toute l'analyse ci-dessous peut être réalisée dans un autre langage (Python, Perl, ...). À vous de choisir ! Il y aurait un travail de recherche à effectuer ceci dit pour obtenir les équivalents de chaque commande (ou si c'est impossible, des contournements / autres façons de procéder).
data <- read.csv("../data/Pokemon.csv")
dim(data)
head(data)
- 1072
- 13
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
1 | 1 | Bulbasaur | Grass | Poison | 318 | 45 | 49 | 49 | 65 | 65 | 45 | 1 | FALSE |
2 | 2 | Ivysaur | Grass | Poison | 405 | 60 | 62 | 63 | 80 | 80 | 60 | 1 | FALSE |
3 | 3 | Venusaur | Grass | Poison | 525 | 80 | 82 | 83 | 100 | 100 | 80 | 1 | FALSE |
4 | 3 | Mega Venusaur | Grass | Poison | 625 | 80 | 100 | 123 | 122 | 120 | 80 | 1 | FALSE |
5 | 3 | Gigantamax Venusaur | Grass | Poison | 525 | 80 | 82 | 83 | 100 | 100 | 80 | 1 | FALSE |
6 | 4 | Charmander | Fire | 309 | 39 | 52 | 43 | 60 | 50 | 65 | 1 | FALSE |
Il paraît clair que les colonnes "name", "type1", "type2" et "legendary" ne peuvent pas être utilisées, n'étant pas numériques. Elles peuvent être intéressantes ceci dit pour la visualisation ultérieure (variables supplémentaires, comme avec l'ACP/AFC). Ensuite, la génération d'un pokemon semble peu pertinente car elle définit en quelque sorte déjà un clustering : génération 1 = un groupe, génération 2 = un groupe etc. Finalement, la colonne "number" correspond probablement à un identifiant, et doit être ignorée. Vérifions cela rapidement :
length(unique(data$number))
levels(as.factor(data$generation)) #on peut aussi écrire simplement unique(data$generation)
- '0'
- '1'
- '2'
- '3'
- '4'
- '5'
- '6'
- '7'
- '8'
Bon, "number" n'est pas identifiant sur ce jeu de données. On le voit en fait dans l'extrait ci-dessus : Venusaur. Cette exemple semble indiquer que les numéros en double correspondent à des variantes d'un même pokemon. Vérifions.
# Recherche des numéros en double:
dbl <- unlist(lapply(unique(data$number), function(n) { if (sum(data$number == n) >= 2) return(n); return(NULL) }))
# Note: astuce, NULL dans une liste est ignoré par unlist(). Il y a plein d'autres façons de faire hein.
# Note2: attention length(data$number == n) renverrait 1072 ; il faut utiliser sum() ici.
dbl
- 3
- 6
- 9
- 12
- 15
- 18
- 19
- 20
- 25
- 26
- 27
- 28
- 37
- 38
- 50
- 51
- 52
- 53
- 65
- 68
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 83
- 88
- 89
- 94
- 99
- 103
- 105
- 110
- 115
- 122
- 127
- 130
- 131
- 133
- 142
- 143
- 144
- 145
- 146
- 150
- 181
- 199
- 208
- 212
- 214
- 222
- 229
- 248
- 254
- 257
- 260
- 263
- 264
- 282
- 302
- 303
- 306
- 308
- 310
- 319
- 323
- 334
- 354
- 359
- 362
- 373
- 376
- 380
- 381
- 382
- 383
- 384
- 386
- 413
- 428
- 445
- 448
- 460
- 475
- 479
- 487
- 492
- 531
- 554
- 555
- 562
- 569
- 618
- 641
- 642
- 645
- 646
- 647
- 648
- 658
- 678
- 681
- 710
- 711
- 718
- 719
- 720
- 741
- 745
- 746
- 774
- 800
- 809
- 812
- 815
- 818
- 823
- 826
- 834
- 839
- 841
- 842
- 844
- 849
- 851
- 858
- 861
- 869
- 875
- 876
- 877
- 879
- 884
- 888
- 889
- 890
- 892
- 893
- 898
data[data$number == 27,]
data[data$number == 445,]
data[data$number == 844,]
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
41 | 27 | Sandshrew | Ground | 300 | 50 | 75 | 85 | 20 | 30 | 40 | 1 | FALSE | |
42 | 27 | Alolan Sandshrew | Ice | Steel | 300 | 50 | 75 | 90 | 10 | 35 | 40 | 7 | FALSE |
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
539 | 445 | Garchomp | Dragon | Ground | 600 | 108 | 130 | 95 | 80 | 85 | 102 | 4 | FALSE |
540 | 445 | Mega Garchomp | Dragon | Ground | 700 | 108 | 170 | 115 | 120 | 95 | 92 | 4 | FALSE |
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
997 | 844 | Sandaconda | Ground | 510 | 72 | 107 | 125 | 65 | 70 | 71 | 8 | FALSE | |
998 | 844 | Gigantamax Sandaconda | Ground | 510 | 72 | 107 | 125 | 65 | 70 | 71 | 8 | FALSE |
On ne va pas tous les faire : la conclusion est claire. Notons que les deux formes du pokemon Sandshrew n'ont pas les mêmes types, et que les caractéristiques de Sandaconda sont exactement les mêmes que celles de sa forme "Gigantamax". Après vérification il n'y a pas d'erreur : https://www.pokebip.com/pokedex/pokemon/dunaconda-gigamax. Bizarre tout de même. Monde étrange que celui des Pokemons...
Je ne sais pas vous, mais je suis curieux de savoir parmi tous ceux-là lesquels sont légendaires. Regardons.
dim(data[data$legendary,]) #Awai, 118 quand-même...
head(data[data$legendary,])
- 118
- 13
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
195 | 144 | Articuno | Ice | Flying | 580 | 90 | 85 | 100 | 95 | 125 | 85 | 1 | TRUE |
196 | 144 | Galarian Articuno | Psychic | Flying | 580 | 90 | 85 | 85 | 125 | 100 | 95 | 8 | TRUE |
197 | 145 | Zapdos | Electric | Flying | 580 | 90 | 90 | 85 | 125 | 90 | 100 | 1 | TRUE |
198 | 145 | Galarian Zapdos | Fighting | Flying | 580 | 90 | 125 | 90 | 85 | 90 | 100 | 8 | TRUE |
199 | 146 | Moltres | Fire | Flying | 580 | 90 | 100 | 90 | 125 | 85 | 90 | 1 | TRUE |
200 | 146 | Galarian Moltres | Dark | Flying | 580 | 90 | 85 | 90 | 100 | 125 | 90 | 8 | TRUE |
Ma curiosité n'étant pas encore satisfaite, demandons à Poképédia : "Un Pokémon légendaire est un Pokémon particulier, associé généralement à un mythe concernant la création et l'organisation du monde, et dont la rareté n'a d'égale que sa puissance au combat. L'obtention d'un Pokémon légendaire se fait donc en effectuant une quête particulière ou par le biais d'une distribution lors d'événements organisés par Nintendo."
# Pokemon légendaire = plus puissant ?
options(repr.plot.width=15, repr.plot.height=10)
hist((1:nrow(data))[ data[order(data$total, decreasing=TRUE), "legendary"] ], xlab="Rang", ylab="Compte", main="")
# Répartition des pokemons légendaires dans la liste par puissances décroissantes:
# Conclusion = oui, plutôt.
Il est temps de revenir à nos m... pokemons. De ce qui précède on conclut que les colonnes à retenir seraient "total, hp, attack, defense, sp_attack, sp_defense, speed". "total" est cependant redondant = somme des autres colonnes. Il risque d'aplanir les différences dans une certaine mesure, deux pokemons différents mais ayant le même total se verraient rapprochés artificiellement. On conserve donc finalement 6 colonnes.
data_clust <- subset(data, select=c("hp", "attack", "defense", "sp_attack", "sp_defense", "speed"))
rownames(data_clust) <- data$name #utile pour les graphes
Clustering hiérarchique¶
distances <- dist(data_clust)
h <- hclust(distances, method="ward.D") #distance de Ward: en général bons résultats
plot(h, labels=substr(data$name, 1, 5)) #substr(x, 1, 5): 5 premiers caractères (très moche sinon)
Le bas du dendrogramme est illisible, normal avec tant d'individus. La hauteur avant la dernière fusion est importante, indiquant qu'il faut garder au moins deux groupes. On peut regarder aussi le résultat pour 3, 4 ou 5 groupes par exemple (choix arbitraire). Je ne trace ici que $K=2$ et $K=3$ pour ne pas surcharger : on comparera à PAM et kmeans.
library(factoextra)
cl2_h <- cutree(h, 2)
fviz_cluster(list(data=data_clust, cluster=cl2_h))
Loading required package: ggplot2 Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
Sans surprise, on retrouve un groupe de pokemons plutôt puissant à droite, et un autre plus modeste à gauche. Vérifions cette affirmation:
par(mfrow=c(1,2))
hist(data[cl2_h==1, "total"], main="Groupe 1 Ã gauche")
hist(data[cl2_h==2, "total"], main="Groupe 2 Ã droite")
cl3_h <- cutree(h, 3)
fviz_cluster(list(data=data_clust, cluster=cl3_h))
Le cluster de droite se subdivise en moitiés haute et basse : comparons les deux individus extrêmes pour tenter de comprendre ce second axe ACP.
data[which(data$name %in% c("Shuckle", "Deoxys Attack Forme")),]
number | name | type1 | type2 | total | hp | attack | defense | sp_attack | sp_defense | speed | generation | legendary | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <chr> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <int> | <lgl> | |
273 | 213 | Shuckle | Bug | Rock | 505 | 20 | 10 | 230 | 10 | 230 | 5 | 2 | FALSE |
475 | 386 | Deoxys Attack Forme | Psychic | 600 | 50 | 180 | 20 | 180 | 20 | 150 | 3 | TRUE |
Donc plus on est haut (resp. bas) sur le second axe plus on a de points de défense (resp. attaque).
PAM, kmeans¶
Ces deux algorithmes procédant de manière similaire, je les regroupe dans une section.
library(cluster)
cl2_p <- pam(data_clust, 2)
fviz_cluster(cl2_p)
cl2_k <- kmeans(data_clust, 2)
fviz_cluster(cl2_k, data=data_clust)
cl3_p <- pam(data_clust, 3)
fviz_cluster(cl3_p)
cl3_k <- kmeans(data_clust, 3)
fviz_cluster(cl3_k, data=data_clust)