Le site arthur.bebou.netlib.re - retour accueil
git clone git://bebou.netlib.re/arthur.bebou
Log | Files | Refs |
index.sh (36909B)
1 #! page 2 title: De la datascience modeste dans votre terminal 3 author: Arthur Pons 4 description: Explorons comment faire de la datascience modeste via des outils simples accessibles en ligne de commande 5 publication: 2023-10-18 6 sectionmd: main 7 8 9 ## Avant propos, pour aller plus loin 10 11 Pour voir des exemples plus avancés vous pouvez jeter un coup d'oeil à cette 12 [tentative de reproduction] d'un [article analysant des données à propos d'un 13 tournoi de starcraft]. 14 15 ## Introduction 16 17 L'objectif de cet article est de démonter qu'il est possible de traiter 18 des données scientifiques en ligne de commande, via des outils 19 traditionnellement disponibles sur les distributions linux et/ou des outils 20 relativement simples se manipulant bien dans un terminal. On ne fera rien 21 de très complexe parce que 22 23 1. Je ne sais pas le faire, je ne suis pas data-scientist 24 2. Les outils que nous verrons sont volontairement assez simples 25 26 Nous allons traiter des fichiers TSV[^1] parce que j'ai constaté que 27 de très nombreux projets scientifiques font du traitement de données 28 tabulaires. Si l'on parvient à faire des choses intéressantes avec 29 on répond donc à de nombreux besoins. Il est fréquent que les chercheureuses 30 dégainent des outils très complexes, capables de faire bien plus que ce que 31 l'on va voir, pour faire de opérations très simples. L'idée est ici 32 de rendre plus tangible ce qu'il est possible de faire sans déployer ces 33 outils pour éviter de vider son bol de céréales avec une [cuillère de deux 34 mètres - 524Ko]. 35 36 Je vais introduire beaucoup de commandes sans les expliquer en profondeur. 37 Nombreuses d'entre elles ne seront utilisées que d'une seule façon ou dans un 38 seul contexte alors qu'elles regorgent d'options. Je vous incite fortement à 39 être curieux et curieuses et à lire les manuels des ces commandes en tapant 40 `man commande`, `commande --help` ou encore `info commande` pour en apprendre 41 plus. Quand j'écris "cette commande sert à/fait cela", il est très rare qu'elle 42 ne serve *qu'à* cela. 43 44 Presque tout ce que je vais faire peut ête reproduit d'autres manières, 45 certaines possiblement meilleures. C'est le propre des interfaces en ligne de 46 commande UNIX. Si vous voulez proposer des alternatives n'hésitez pas à 47 contacter le collectif par mail ou en présentiel, je modifierai l'article avec 48 plaisir. 49 50 Finalement, beaucoup de tabulations seront utilisés dans le code présenté dans 51 cet article. Malheureusement elles ressemblent à des espaces dans les blocs de 52 code, je vais réfléchir à un moyen d'y remédier. En attendant vous pouvez 53 lire cet article en markdown dans vim en faisant `curl 54 http://katzele.netlib.re/articles/datascience-cli/index.md | vim -` et faire 55 apparaître les tabulations avec la commande vim `set listchars=tab:\ \ \|`. 56 Pour comprendre ce que cela fait vous pouvez consulter l'aide `:help listchars`. 57 58 ## C'est parti 59 60 ### CSV vs TSV 61 62 Prenons ce fichier (de 500Ko) pour exemple de jeu de donnée : 63 https://raw.githubusercontent.com/datasets/population/main/data/population.csv 64 65 Je vais d'abord le convertir en TSV. Les raisons pour lesquelles je préfère le 66 TSV sont résumées dans [cet article d'une équipe d'eBay]. 67 68 Un moyen de convertir notre fichier CSV en TSV serait de simplement remplacer 69 toutes les occurrences de `,` par des tabulations. L'outil `tr`[^2] permet de 70 traduire un ensemble de caractères en un autre. Le début de `population.csv` 71 contient : 72 73 Country Name,Country Code,Year,Value 74 Aruba,ABW,1960,54608 75 Aruba,ABW,1961,55811 76 Aruba,ABW,1962,56682 77 Aruba,ABW,1963,57475 78 Aruba,ABW,1964,58178 79 Aruba,ABW,1965,58782 80 Aruba,ABW,1966,59291 81 Aruba,ABW,1967,59522 82 Aruba,ABW,1968,59471 83 84 Si l'on fait `tr ',' '\t' < population.csv > population.tsv'[^3] : 85 86 Country Name Country Code Year Value 87 Aruba ABW 1960 54608 88 Aruba ABW 1961 55811 89 Aruba ABW 1962 56682 90 Aruba ABW 1963 57475 91 Aruba ABW 1964 58178 92 Aruba ABW 1965 58782 93 Aruba ABW 1966 59291 94 Aruba ABW 1967 59522 95 Aruba ABW 1968 59471 96 97 on obtient bien un fichier TSV. Ici la tâche a été très simple puisque nos 98 données ne contiennent jamais de virgules ni aucun échappement en particulier. 99 Résoudre cette tâche à coup sûr est moins triviale qu'il n'y paraît. J'opte 100 donc généralement d'utiliser [csv2tsv] des outils [tsv-utils]. 101 102 csv2tsv population.csv > population.tsv 103 104 ### Compter des entrées 105 106 Première question, combien y a-t-il d'entrée dans notre fichier / base de 107 donnée ? `wc` est un outil permettant de compter les lignes d'un fichier : 108 109 wc -l population.tsv 110 16401 population.tsv 111 112 Si on ne passe pas le fichier en argument mais directement dans stdin `wc` n'affiche 113 pas le nom du fichier en sortie 114 115 wc -l < population.tsv 116 16401 117 118 Si vous avez un jeu de donnée contenant pleins de TSV et que vous voulez exécuter `wc` sur plusieurs de ces fichiers : 119 120 wc -l population.tsv population.tsv 121 16401 population.tsv 122 16401 population.tsv 123 32802 total 124 125 `wc` vous fera gentiment un affichage avec le total et le sous total par fichier. 126 127 Admettons que nous voulions maintenant regarder à l'intérieur du fichier. Il est souvent intéressant de n'afficher que 128 le début ou la fin pour éviter d'être submergé·e par un mur de texte. `head` et `tail` servent à cela : 129 130 head population.tsv 131 Country Name Country Code Year Value 132 Aruba ABW 1960 54608 133 Aruba ABW 1961 55811 134 Aruba ABW 1962 56682 135 Aruba ABW 1963 57475 136 Aruba ABW 1964 58178 137 Aruba ABW 1965 58782 138 Aruba ABW 1966 59291 139 Aruba ABW 1967 59522 140 Aruba ABW 1968 59471 141 142 Si jamais vous voulez un affichage où les colonnes sont alignées vous pouvez 143 utiliser `column` en lui disant que le séparateur est la tabulation[^4] : 144 145 head population.tsv | column -ts ' ' 146 Country Name Country Code Year Value 147 Aruba ABW 1960 54608 148 Aruba ABW 1961 55811 149 Aruba ABW 1962 56682 150 Aruba ABW 1963 57475 151 Aruba ABW 1964 58178 152 Aruba ABW 1965 58782 153 Aruba ABW 1966 59291 154 Aruba ABW 1967 59522 155 Aruba ABW 1968 59471 156 157 Sinon [tsv-utils] fournit son propre outil [tsv-pretty] qui semble faire la 158 même chose mais probablement plus/mieux (j'ai pas vérifié) : 159 160 head population.tsv | tsv-pretty 161 Country Name Country Code Year Value 162 Aruba ABW 1960 54608 163 Aruba ABW 1961 55811 164 Aruba ABW 1962 56682 165 Aruba ABW 1963 57475 166 Aruba ABW 1964 58178 167 Aruba ABW 1965 58782 168 Aruba ABW 1966 59291 169 Aruba ABW 1967 59522 170 Aruba ABW 1968 59471 171 172 ### Lister des valeurs 173 174 Disons que l'on souhaite obtenir la liste de tous les pays présents dans ce jeu 175 de données. D'abord restreignons nous à la colonne qui nous intéresse, la 176 première, avec `cut` : 177 178 head population.tsv | cut -f1 179 Country Name 180 Aruba 181 Aruba 182 Aruba 183 Aruba 184 Aruba 185 Aruba 186 Aruba 187 Aruba 188 Aruba 189 190 Pas besoin de dire à `cut` quel est le délimiteur, la tabulation est celui par 191 défaut. On lui dit à l'aide de l'argument `-f` quelle colonne on veut (la 192 première). Notre jeux de donnée affiche l'évolution des populations des pays 193 par années, on se retrouve donc avec pleins de duplicas. Pour résoudre ce souci 194 on peut utiliser `uniq`. En le faisant sur la totalité du fichier cette fois -ci : 195 196 cut -f1 population.tsv | uniq 197 Country Name 198 Aruba 199 Africa Eastern and Southern 200 Afghanistan 201 Africa Western and Central 202 Angola 203 Albania 204 Andorra 205 Arab World 206 United Arab Emirates 207 Argentina 208 ... 209 210 On peut aussi savoir rapidement combien de pays sont concernés en combinant avec `wc` : 211 212 cut -f1 population.tsv | uniq | wc -l 213 266 214 215 Ce qui semble vraiment beaucoup. Effectivement notre jeux de donnée contient 216 des regroupements de pays tels que "Europe & Central Asia". Cette capacité à combiner 217 des commandes est l'une des propriétés vraiment puissantes de l'utilisation du pipe (|) 218 dans le traitement de données. D'ailleurs c'est tellement puissant que R l'a reproduit 219 tel quel dans son langage. On remarque que le pays "United Arab Emirates" apparaît avant 220 le pays "Argentina". C'est l'occasion de voir comment trier les données. On utilise la 221 commande `sort` : 222 223 cut -f1 population.tsv | uniq | sort 224 Afghanistan 225 Africa Eastern and Southern 226 Africa Western and Central 227 Albania 228 Algeria 229 American Samoa 230 Andorra 231 Angola 232 Antigua and Barbuda 233 Arab World 234 Argentina 235 ... 236 237 On voit que l'entrée des Emirats Arabes Unis apparaît désormais plus loin. 238 239 ### Filtrer le fichier 240 241 Revenons à nos données. Admettons que l'on ne veuille retenir que l'années 242 2002. On va filtrer le fichier avec `grep` : 243 244 grep "2002" population.tsv | tsv-pretty 245 Aruba ABW 2002 91781 246 Africa Eastern and Southern AFE 2002 422741118 247 Afghanistan AFG 2002 21000256 248 Africa Western and Central AFW 1996 242200260 249 Africa Western and Central AFW 2002 284952322 250 Angola AGO 2002 17516139 251 252 oups petit souci, on constate que pour le regroupement "Africa Western and 253 Central" deux années on été retenues. Pourquoi ? Parce qu'en 1996 la population 254 de cette région s'élevait à 242**2002**260 personnes. `grep` ne sait pas ce 255 qu'est une colonne. Il y a plusieurs solutions à cela : 256 257 1. En utilisant `grep`, on fait non plus une recherche sur `2002` mais ` 258 2002 ` (tab2002tab) 259 260 Puisqu'il n'y a pas de tabulation dans les données : 261 262 grep " 2002 " population.tsv | tsv-pretty 263 Aruba ABW 2002 91781 264 Africa Eastern and Southern AFE 2002 422741118 265 Afghanistan AFG 2002 21000256 266 Africa Western and Central AFW 2002 284952322 267 Angola AGO 2002 17516139 268 Albania ALB 2002 3051010 269 270 2. On utilise un autre outil qui "comprend" les colonnes. 271 272 Par exemple `awk` (je ne rentre pas dans les détails pour le moment) : `awk -F' 273 ' '$3=="2002"' population.tsv`. `$3` désigne la troisième colonne, on vérifie 274 son égalité avec "2002". Par défaut si le test est vrai la ligne est affichée 275 dans `awk`. Un autre exemple serait, toujours dans la suite [tsv-utils], l'outil 276 [tsv-filter], `tsv-filter -H --eq 3:2002 population.tsv`. 277 278 Maintenant que l'on a les données pour l'année 2002, comment savoir, par exemple 279 quel est le pays / région la plus peuplée ? On ressort `sort` de notre besace. 280 281 grep " 2002 " population.tsv | sort -t' ' -nk4 282 Tuvalu TUV 2002 9609 283 Nauru NRU 2002 10351 284 Palau PLW 2002 19851 285 Turks and Caicos Islands TCA 2002 20598 286 British Virgin Islands VGB 2002 21288 287 Gibraltar GIB 2002 27892 288 San Marino SMR 2002 27969 289 Sint Maarten (Dutch part) SXM 2002 30777 290 291 Avec `-t` on dit à `sort` quel est le délimiteur (par défaut c'est n'importe 292 quelle transition d'un caractère non "blanc" vers un caractère "blanc") puis 293 avec `-n` que l'on veut trier des chiffres (par défaut du texte) puis avec 294 `-k4` que l'on veut trier sur le 4ème champ (la population). Avec un `-r` 295 on trie par ordre décroisant (par défaut par ordre croissant comme on le 296 constate au dessus). 297 298 grep " 2002 " population.tsv | sort -t' ' -nrk4 299 World WLD 2002 6308092739 300 IDA & IBRD total IBT 2002 5243235123 301 Low & middle income LMY 2002 5167898454 302 Middle income MIC 2002 4744040472 303 ... 304 305 ### Afficher une courbe 306 307 Nous souhaitons maintenant regarder l'évolution de la population d'un pays 308 donné et tenter de la visualiser. Prenons la France pour exemple. Nous allons 309 trier avec `grep` puis utiliser [gnuplot] pour le graph. J'aime bien 310 gnuplot parce que malgré ses commandes et sa logique parfois un peu 311 confuse[^5], c'est un logiciel qui s'utilise bien en ligne de commande. 312 313 grep "France" population.tsv | cut -f3,4 | gnuplot -p -e "plot '-'" 314 315 On a utilisé la `cut` pour retenir cette fois plusieurs colonnes, celle des 316 années et celle de la population en listant leurs numéros séparés par des 317 virgules. Le résultat donne ceci : 318 319 ![Evolution de la population de la France de 1960 à 2021](population-france.png) 320 321 J'espère que vous appréciez la puissance de la combinaison d'outils simples en 322 ligne de commande pour des traitements modestes comme celui-ci. En quelques 323 caractères et en installant uniquement gnuplot[^6] on peut filtrer un gros 324 fichier texte et grapher des valeurs numériques en cinq centièmes de secondes 325 (sur ma machine). 326 327 Les paramètres par défaut de gnuplot pour un script aussi simple (`plot '-'`) 328 donnent des résultats insatisfaisant si l'on souhaitait publier quelque chose de 329 sérieux puisqu'il n'y a pas de titre, la légende ne fait pas sens, les axes ne 330 sont pas labellisés, la population est exprimée en notation scientifique. 331 Gnuplot permet de modifier tout cela mais ce n'est pas l'objet de cet article. 332 Je vous renvoie vers la documentation. Bonne chance. Pour l'anecdote les sorties 333 de gnuplot sont nombreuses (png, svg, pdf, jpg, html etc) mais l'une d'entre 334 elle est plus inattendue que les autres, c'est la sortie ascii. Il faut exécuter 335 la commande `set term dumb`, on obtient alors : 336 337 grep "France" population.tsv | cut -f3,4 | gnuplot -p -e "set term dumb;plot '-'" 338 7e+07 +----------------------------------------------------------------+ 339 | + + + + + + | 340 | AAAAA A | 341 | AAAAA | 342 6.5e+07 |-+ AAA +-| 343 | AAA | 344 | AA | 345 | AAA | 346 6e+07 |-+ AAAA +-| 347 | AAAA | 348 | AAAA | 349 | AAAAA | 350 5.5e+07 |-+ AAAA +-| 351 | AAAA | 352 | AAA | 353 | AAA | 354 5e+07 |-+ AA +-| 355 | AAA | 356 |AA | 357 | + + + + + + | 358 4.5e+07 +----------------------------------------------------------------+ 359 1960 1970 1980 1990 2000 2010 2020 2030 360 361 362 Voilà, marrant. 363 364 ### Faire un filtre un peu plus avancé 365 366 Nous voulons dorénavant savoir combien il existe de pays (et de régions puisque 367 notre jeu de donée ne fait pas la différence) de plus de 50M d'habitant·e·s en 368 2020 : 369 370 grep " 2020 " population.tsv | cut -f4 | grep -E "^([5-9][0-9]{7}|[1-9][0-9]{8,})$" | wc -l 371 73 372 373 On filtre sur 2020, on ne conserve que la colonne de la population et on 374 filtre sur les nombre supérieurs ou égaux à 50M pour finalement compter le 375 nombre de ligne en résultat. J'introduis ici quelque chose d'assez complexe 376 mais incroyablement puissant, les expressions régulières. Je ne vais pas 377 les expliquer en détail mais celle que nous avons utilisé se comprend comme 378 ça : 379 380 * `[5-9][0-9]{7}` = 5,6,7,8 ou 9 suivi de n'importe quels sept chiffres (autrement dit tous les nombres entre 50M et 99 999 999) 381 * `[1-9][0-9]{8,}` = 1,2,3,4,5,6,7,8 ou 9 suivi de n'importe quels huit chiffres (autrement dit 100M et au delà) 382 383 Mettre les deux entre parenthèses séparées par un pipe `|` veut dire l'un ou 384 l'autre. Le `^` veut dire "début de la ligne", le `$` veut dire la fin, c'est 385 pour garantir de parser le nombre en entier. Je vous recommande très fortement 386 d'apprendre à utiliser les regex. Un bon bouquin serait [Mastering Regular 387 Expressions] que je peux vous prêter en français (dans une vieille édition) ou 388 que vous pouvez trouver sur internet en cherchant bien. Si on estime cela 389 utile/nécessaire on peut aussi utiliser `tsv-filter --ge 4:50000000` qui 390 est un peu moins ésotérique. Si l'on voulait conserver la colonne des pays, 391 pour grapher leurs populations par exemple, il faudrait modifier la regex 392 pour lui dire de ne s'intéresser qu'à la dernière colonne ce qui permet 393 de sauter le `cut` qui retire l'info que l'on veut : 394 395 grep " 2020 " population.tsv | 396 grep -E "([5-9][0-9]{7}|[1-9][0-9]{8,})$" | 397 cut -f1,4 | 398 sort -t' ' -nk2 > data 399 gnuplot -p -e "set style fill solid; 400 set xtics rotate by 90 right; 401 set datafile separator ' '; 402 set xtics font ',5'; 403 plot 'data' using 2:xticlabels(1) with histogram notitle" 404 405 Pour une raison qui m'est encore inconnue gnuplot ne veut pas générer la 406 totalité de cet histogramme. Je vais chercher pourquoi, en attendant je vous 407 met le résultat bugué. 408 409 > Je crois avoir trouvé. Il se trouve que lorsque gnuplot lit dans stdin il 410 > s'arrête lorsqu'il voit un `e` sur une nouvelle ligne. Ici le pays qui 411 > suivait l'europe centrale était l'Egypte, commençant donc par un `e`. Je 412 > trouve cela surprenant que le fonctionnement par défaut ne soit pas de 413 > s'arrêter à un `e` *seul* comme pour un here-doc mais soit. Pour y remédier 414 > je dépose donc les données dans un fichier. Le code au dessus a été corrigé. 415 416 ![Histograme de la population des pays et régions avec plus de 50M d'habitant·e·s en 2020](histo-popu-2020.png) 417 418 ### Des sommes, des moyennes, des écarts type etc 419 420 Calculons maintenant des valeurs statistiques. Par exemple, quel serait la 421 somme de la population de la France, l'Allemagne et l'Italie en 1973 ? 422 423 grep -E "(France|Germany|Italy).+ 1973 " population.tsv 424 Germany DEU 1973 78936666 425 France FRA 1973 53053660 426 Italy ITA 1973 54751406 427 428 grep -E "(France|Germany|Italy).+ 1973 " population.tsv | 429 cut -f4 | 430 paste -s -d'+' 431 78936666+53053660+54751406 432 433 grep -E "(France|Germany|Italy).+ 1973 " population.tsv | 434 cut -f4 | 435 paste -s -d'+' | 436 bc -l 437 186741732 438 439 On utilise ici `paste` pour coller les lignes de nos données une à une avec le 440 délimiteur `+` ce qui créé une expression que `bc` (une calculette en ligne de 441 commande) peut comprendre. Si l'on voulait faire ce traitement pour toutes les 442 années : 443 444 cut -f3 population.tsv | tail -n+2 | sort -u | 445 xargs -d'\n' -n1 sh -c 'printf "$1 "; 446 grep -E "(France|Germany|Italy).+ $1" population.tsv | 447 cut -f4 | 448 paste -s -d"+" | 449 bc -l 450 ' -- | 451 gnuplot -p -e "plot '-'" 452 453 ![Somme des populations de l'Allemagne, l'Italie et la France avec le temps](sum-pop-fr-de-it.png) 454 455 Je reconnais que ça commence à être un peu velu. Sur la première ligne on 456 récupère la liste des années. Le `tail` permet d'enlever la première ligne 457 (l'entête) et le `sort -u` de retirer les doublons. Ensuite on utilise `xargs` 458 pour appeler la commande qui nous donnait la somme des populations pour une 459 année donnée successivement sur toutes les années que la première ligne nous 460 donne. Il faut bien sûr remplacer le `1973` par un `$1` qui est une variable 461 qui sera remplacée par l'année. Il y a beaucoup plus à expliquer à propos 462 d'`xargs` mais je vais en rester là. 463 464 Il est à noter que cet exemple est relativement peu efficace. La commande 465 termine en deux dixièmes de secondes sur ma machine, ce qui me semble tout à 466 fait respectable, mais sur un gros fichier ce temps d'exécution aurait explosé. 467 De plus l'écriture de la commande n'est pas aisée. Si vous rencontrez des limites 468 rédactionnelles ou de performance dans ce genre de cas c'est peut-être que vous 469 devriez utiliser d'autres outils. Par exemple, en utilisant la suite [tsv-utils] : 470 471 tsv-filter --regex 1:"France|Germany|Italy" population.tsv | 472 tsv-summarize --sum 4 --group-by 3 | 473 gnuplot -p -e "plot '-'" 474 475 `tsv-filter` ne garde que les lignes dont la première colonne (**1**:"France...) 476 match la regex (**--regex** 1:...) qui match soit la France soit l'Allemagne soit 477 l'Italie (1:"**France|Germany|Italy**". Ensuite `tsv-summarize` fait une somme de 478 la quatrième colonne (`--sum 4`) en groupant par années (`--group-by 3`). 479 480 Il faut reconnaître que c'est un peu plus facile à écrire et à lire. C'est 481 aussi dramatiquement plus efficace puisque la commande termine en 14 centièmes 482 de secondes soit presque 20 fois moins que celle avec xargs. Cela pour deux 483 raisons principales. Premièrement les [tsv-utils] ont été bien 484 optimisés pour faire de la lecture de fichiers `tsv` et `tsv` uniquement 485 là où les autres outils sont souvent plus généralistes et ne peuvent donc 486 pas se permettre certaines optimisations. Deuxièmement l'algorithme interne 487 de `tsv-summarize` permettant de faire une opération en groupant sur une 488 autre variable est terriblement plus efficace que de bêtement refiltrer 489 sur chacune des années avec un `grep` etc. Cette dernière technique 490 ouvre un nouveau processus à chaque année ce qui constitue un overhead 491 significatif. Si vous avec besoin de plus de performance et où s'il est 492 vraiment important d'exprimer ce que vous voulez plus simplement alors 493 [tsv-utils] pourrait vous être utile[^7]. 494 495 `tsv-summarize` sait aussi calculer des statistiques assez classiques telles que 496 la moyenne, la médiane, l'écart type etc. Les tsv-utils savent aussi adresser 497 les colonnes par leurs noms si un entête existe. A chaque fois que l'on a 498 utilisé des chiffres on aurait pu utiliser le nom de la colonne. 499 500 ### Et des jointures ? 501 502 Et si l'on veut faire des jointures ? Admettons que notre fichier soit séparé 503 en deux, d'un côté nous avons les codes des pays, les années et les valeurs, de 504 l'autre nous avons la correspondance entre les codes des pays et leur noms 505 complets. 506 507 head correspondance.tsv sans-pays.tsv 508 ==> correspondance.tsv <== 509 Country Name Country Code 510 Aruba ABW 511 Africa Eastern and Southern AFE 512 Afghanistan AFG 513 Africa Western and Central AFW 514 Angola AGO 515 Albania ALB 516 Andorra AND 517 Arab World ARB 518 United Arab Emirates ARE 519 520 ==> sans-pays.tsv <== 521 Country Code Year Value 522 ABW 1960 54608 523 ABW 1961 55811 524 ABW 1962 56682 525 ABW 1963 57475 526 ABW 1964 58178 527 ABW 1965 58782 528 ABW 1966 59291 529 ABW 1967 59522 530 ABW 1968 59471 531 ... 532 533 Alors on peut faire une jointure avec 534 535 join --header -t' ' -1 2 -2 1 correspondance.tsv sans-pays.tsv 536 Country Code Country Name Year Value 537 ABW Aruba 1960 54608 538 ABW Aruba 1961 55811 539 ... 540 541 On saute les premières lignes puisque ce sont des header (`--header`), on 542 choisit la tabulation comme délimiteur (`-t' '`), dans le premier fichier on 543 joint sur le second champ (le code, `-1 2`), dans le second sur le premier (`-2 544 1`) et on passe les fichier en argument. tsv-utils a un équivalent un peu plus 545 versatile qui pour le même besoin s'utiliserait de la sorte : 546 547 tsv-join -H --filter-file correspondance.tsv -k 1 -a 2 sans-pays.tsv 548 Country Code Year Value Country Name 549 ABW 1960 54608 Aruba 550 ABW 1961 55811 Aruba 551 ABW 1962 56682 Aruba 552 ABW 1963 57475 Aruba 553 ... 554 555 Attention, je crois qu'il faut que la colonne sur laquelle faire la jointure 556 soit la même dans les deux fichiers. Limitation un peu étrange mais soit. 557 558 ### Compter les occurrences des valeurs d'une colonne 559 560 Pour continuer les exmples prenons le jeu de donnée ici : 561 https://github.com/datablist/sample-csv-files/raw/main/files/people/people-2000000.zip. 562 Attention le fichier pèse 66Mo zippé, 255M dézippé. Je prends un gros fichier 563 comme celui-ci pour vérifier que ce que l'on fait est au moins un peu 564 performant. Vous pouvez télécharger une version plus petite sur la page du [dépôt github](https://github.com/datablist/sample-csv-files). 565 566 Convertissons le en TSV et regardons ce qu'il y a dedans : 567 568 csv2tsv people.csv > people.tsv;head people.tsv | tsv-pretty 569 Index User Id First Name Last Name Sex Email Phone Date of birth Job Title 570 1 4defE49671cF860 Sydney Shannon Male tvang@example.net 574-440-1423x9799 2020-07-09 Technical brewer 571 2 F89B87bCf8f210b Regina Lin Male helen14@example.net 001-273-664-2268x90121 1909-06-20 Teacher, adult education 572 3 Cad6052BDd5DEaf Pamela Blake Female brent05@example.org 927-880-5785x85266 1964-08-19 Armed forces operational officer 573 4 e83E46f80f629CD Dave Hoffman Female munozcraig@example.org 001-147-429-8340x608 2009-02-19 Ship broker 574 5 60AAc4DcaBcE3b6 Ian Campos Female brownevelyn@example.net 166-126-4390 1997-10-02 Media planner 575 6 7ACb92d81A42fdf Valerie Patel Male muellerjoel@example.net 001-379-612-1298x853 2021-04-07 Engineer, materials 576 7 A00bacC18101d37 Dan Castillo Female billmoody@example.net (448)494-0852x63243 1975-04-09 Historic buildings inspector/conservation officer 577 8 B012698Cf31cfec Clinton Cochran Male glenn94@example.org 4425100065 1966-07-19 Engineer, mining 578 9 a5bd11BD7dA1a4B Gabriella Richard Female blane@example.org 352.362.4148x8344 2021-09-02 Wellsite geologist 579 580 Bon c'est pas facile à lire parce que les lignes sont vraiment longues mais soit. Si vous n'utilisez pas tsv-utils mais que vous voulez voir rapidement quels sont les chiffres qui correspondent aux entêtes vous pouvez faire : 581 582 head -n1 people.tsv | tr ' ' '\n' | nl 583 1 Index 584 2 User Id 585 3 First Name 586 4 Last Name 587 5 Sex 588 6 Email 589 7 Phone 590 8 Date of birth 591 9 Job Title 592 593 Imaginons que nous voulions compter le nombre de personnes identifiées comme "Male" dans ce fichier : 594 595 cut -f5 people.tsv | sort | uniq -c 596 1000505 Female 597 999495 Male 598 1 Sex 599 600 On ne retient que la colonne pertinente avec `cut`, on la tri pour que tous les 601 occurrences identiques soient les unes à côté des autres puis on les compte 602 avec `uniq` en lui précisant d'afficher le nombre d'occurrence en utilisant 603 `-c`. Il est nécessaire de trier les données avant de pouvoir les compter avec 604 `uniq`. `uniq` est très bête, il ne sait pas tenir les comptes d'une valeur à 605 une autre. 606 607 On voit qu'il y a une occurrence de "Sex", c'est l'entête que se ballade. Si vous ne 608 la voulez pas on peut initialement la supprimer avec `tail -n+2`. 609 610 Cette commande s'exécute en 1,4s. Certes le fichier est très gros mais c'est 611 aussi parce que cette nécessité de trier la colonne avant de compter est 612 coûteuse. Deux solutions à ça. Premièrement, donner un plus gros buffer à 613 `sort` avec `--buffer-size=NM` où `N` est un entier. Cela aide mais n'est pas 614 magique non plus. Si les performances sont vraiment cruciales alors je 615 suggère de dégainer à nouveau tsv-utils. 616 617 tsv-summarize --count --group-by 5 people.tsv 618 Sex 1 619 Male 999495 620 Female 1000505 621 622 Cette commande s'exécute en 0,3s. Autre avantage de `tsv-summarize` dans ce cas 623 est qu'il renvoie le résultat sous forme d'un TSV qu'on peut donc continuer à 624 piper facilement. Le résultat d'`uniq` doit être remanié. J'en donne un exemple 625 juste après. Pour un exemple similaire mais avec un peu plus de préparation, 626 imaginons que nous souhaitons grapher la fréquence des années de naissance des 627 personnes : 628 629 tail -n+2 people.tsv | cut -f8 | cut -d'-' -f1 | 630 sort -n | uniq -c | 631 sed -E 's/ +([0-9]+) ([0-9]+)/\2 \1/' | 632 gnuplot -p -e "plot '-'" 633 634 ![Répartition des années de naissance dans ce jeu de données](repart-ann-naissance.png) 635 636 Il semblerait que ce jeu de données ait été généré aléatoirement pour que la 637 répartition soit aussi régulières. Avec tsv-summarize : 638 639 cut -f8 people.tsv | cut -d'-' -f1 | tail -n+2 | 640 tsv-summarize --count -g 1 | 641 gnuplot -p -e "plot '-'" 642 643 Cela évite l'appel à `sed` pour remanier la sortie d'`uniq` et va plus de deux 644 fois plus vite. 645 646 ## Reproductibilité 647 648 J'espère avoir fait la démonstration, même courte, qu'il est très facile, 649 rapide et performant d'effectuer des traitements simples sur des gros volumes 650 de données à l'aide des commandes de bases du monde Unix. En plus de cette 651 propriété je pense qu'évoluer aussi longtemps que possible dans ce monde 652 logiciel là est bénéfique pour la reproductibilité de résultats scientifiques. 653 654 Les commandes majoritairement utilisées ici sont toutes : 655 656 1. Très anciennes 657 658 Être un vieux logiciel n'est pas un avantage pour la repro en soit mais cela 659 témoigne d'une certaine longévité *jusque là* qui permet d'être relativement 660 optimiste sur la possibilité qu'il soit là *à l'avenir*. Les commandes que l'on 661 a vu ont pour la plupart 20/30/40 ans et survivent avec relativement peu de 662 maintenance. De plus, étant déjà fondamentales à l'époque et le temps ayant 663 fait son affaires, elle se sont immiscées partout ce qui diminue les chances 664 qu'elles changent ou pire disparaissent. 665 666 2. Très stables 667 668 Du fait que chaque commande ne sache faire qu'un ensemble relativement 669 restreint de choses et soient isolées les une des autres, elles sont assez 670 rarement mises à jour. Ainsi les chances pour qu'une mise à jour vienne 671 modifier le comportement de votre code ou casser votre environnement sont 672 presque nulles. Dans le milieu on dit que ce sont des logiciels "matures". 673 674 3. Très peu buguées 675 676 Des décennies de développement et d'usages ont fait de ces commandes des 677 logiciels contenant peu de bugs. La présence de bugs n'est évidemment pas 678 souhaitable dans un contexte où l'on cherche à obtenir des résultats fiables et 679 à les reproduire. 680 681 4. Généralement disponibles par défaut 682 683 Étant historiquement[^9] le fondement de "l'espace utilisateurice" des systèmes 684 Unix, ces commandes sont naturellement (presque ?) toujours disponibles sur une 685 installation fraîche des systèmes héritier d'Unix. Rien besoin d'installer, pas 686 besoin d'une connexion internet, pas besoin de configurer quoi que soit. En 687 gros, dépendre de ces commandes revient plus ou moins à dépendre de la chaîne 688 de développement et de de distribution qui vous permet d'avoir un système 689 d'exploitation sur votre machine. Or vous ne feriez rien de ce genre sans OS de 690 toute façon donc vous consentez à pratiquement aucune dépendance supplémentaire 691 en utilisant ces logiciels. Je caricature mais la documentation pour permettre 692 de reproduire l'environnement de développement nécessaire à l'obtention des 693 mêmes résultats se limiterait alors presque à "Un OS Linux en état de 694 fonctionnement". Entre ça et lister les versions précises des dizaines de paquets 695 Python ou R installés mon choix est vite fait. 696 697 5. Relativement simples dans leurs implémentations 698 699 Ces commandes sont des logiciels assez simples **relativement** à d'autres 700 outils pour manipuler des données. Cela rend leur maintenance plus simple ce 701 qui favorise leur pérennité. De plus, puisqu'elles sont toutes indépendantes 702 les une des autres, la disparition d'une n'implique pas forcément la 703 disparition d'une autre. Je précise "relativement" puisqu'en réalité, avec 704 le temps et pour des raisons plus ou moins légitimes, les variantes les plus 705 répandues de ces commandes sont devenues assez complexes. Par exemple `sort` 706 fait : 707 708 curl -Ls https://github.com/coreutils/coreutils/raw/master/src/sort.c | wc -l 709 4847 710 711 4847 lignes de C[^10]. Trier n'est pas un problème trivial mais il est possible 712 de faire moins. Par exemple le projet [busybox] a réimplémenté `sort` en : 713 714 curl -Ls https://git.busybox.net/busybox/plain/coreutils/sort.c | wc -l 715 684 716 717 684 lignes. Cela dit, bien que je puisse imaginer que le projet GNU ait pris 718 un peu trop de poids "inutile" avec le temps il est manifeste qu'une partie 719 de tout ce code participe aux bonnes performances. Par exemple : 720 721 time -f '%e' cut -f5 people.tsv | sort | uniq -c 722 1.22 723 1000505 Female 724 999495 Male 725 1 Sex 726 727 time -f '%e' busybox cut -f5 people.tsv | busybox sort | busybox uniq -c 728 1.73 729 1000505 Female 730 999495 Male 731 1 Sex 732 733 On voit que pour ce traitement les versions busybox sont environ 30% plus 734 lentes. 735 736 6. Relativement performantes et accessibles via des interfaces simples et 737 performantes 738 739 Dans l'ensemble ces commandes sont performantes prises individuellement. Cela 740 permet de diminuer la charge sur le matériel et ainsi permettre de faire 741 tourner les traitements sur un plus grand nombre de machine. Cela permet à plus 742 de personnes y compris sur des machines peu puissantes et peu coûteuse de 743 produire ou reproduire le traitement. Ces outils ont également l'élégance 744 d'être accessible via des interfaces très simples, en ligne de commande, qui 745 peuvent tourner sur des machines peu puissantes. De nombreux outils de 746 traitement de données beaucoup plus complexes sont également accessibles en 747 ligne de commande et je pense qu'ils devraient, au moins initialement, être 748 enseignés via cette interface. Pour pouvoir tourner sur plus de machines mais 749 aussi pour inciter à l'utilisation des commandes que l'on a vu dans cet 750 article. On ne dégaine alors l'outil plus complexe que lorsqu'il le faut, au 751 détour d'un pipe, exactement comme nous l'avons fait avec les tsv-utils ou 752 gnuplot. 753 754 Évidemment ce n'est pas magique et pour des besoins un peu complexes il est 755 très facile d'écrire du code très peu performant avec nos commandes. J'ai 756 montré l'un de ces exemples plus tôt dans l'article. Dans ces cas il est 757 judicieux de revoir son code ou, si besoin, d'utiliser un nouvel outil. 758 Un très bon exemple de cela est documenté dans notre article sur [sed et ses 759 performances]. En somme, pour filtrer une fois sur une regex il est très 760 probable que `grep` soit le plus rapide mais s'il s'agit de l'enchaîner avec 761 deux ou trois autres opérations d'une manière un peu sophistiquée faites 762 attention. 763 764 7. Libres 765 766 Ces logiciels sont tous libres. Je n'ai pas envie de prendre ici le temps 767 d'expliquer pourquoi l'utilisation de logiciel et de formats libres devrait 768 être la toute première étape lorsque l'on souhaite produire de la recherche 769 reproductible. 770 771 ## Pour aller plus loin 772 773 Pour voir des exemples plus avancés vous pouvez jeter un coup d'oeil à cette 774 [tentative de reproduction] d'un [article analysant des données à propos d'un 775 tournoi de starcraft]. 776 777 ## Bêtisier 778 779 Quelques graphs un peu jolis mais inutilisables : 780 781 [oups - 40Ko](oups.png)\ 782 [oups le retour - 20Ko](oups2.png) 783 784 [^1]: Tabulation Seperated Data, du CSV mais séparé par une tabulation plutôt qu'un autre délimiteur. 785 [cuillère de deux mètres - 524Ko]: http://katzele.netlib.re/memes/visualstudio-pour-dix-lignes-dxml.png 786 [cet article d'une équipe d'eBay]: https://raw.githubusercontent.com/eBay/tsv-utils/master/docs/comparing-tsv-and-csv.md 787 [csv2tsv]: https://github.com/eBay/tsv-utils/tree/master/csv2tsv 788 [tsv-utils]: https://github.com/eBay/tsv-utils 789 [^2]: Pour translate 790 [^3]: Ici \t veut dire une tabulation. La commande exprime donc "traduit toutes les virgules en tabulation du fichier population.csv et met le résultat dans population.tsv" 791 [^4]: Pour insérer une tabulation plutôt que de provoquer l'autocomplétion vous pouvez faire ctrl+v puis appuyer sur la touche tabulation 792 [tsv-pretty]: https://github.com/eBay/tsv-utils/tree/master/tsv-pretty 793 [tsv-filter]: https://github.com/eBay/tsv-utils/tree/master/tsv-filter 794 [gnuplot]: http://www.gnuplot.info/ 795 [^5]: pour être honnête je n'ai pas fait beaucoup de graphs dans ma vie mais du peu que j'ai vu, aucun outil de graph n'est vraiment facile d'utilisation. 796 [^6]: A part les outils [tsv-utils] et [gnuplot], tous les outils que nous voyons ici sont installés par défaut sur (presque) toutes les distributions linux. Gnuplot ne pèse "que" 3,5Mo 797 [Mastering Regular Expressions]: https://www.oreilly.com/library/view/mastering-regular-expressions/0596528124/ 798 [^7]: J'aurais tendance à penser que dans ce cas particulier, si c'était le seul traitement que nous avions à faire alors ça ne vaudrait pas forcément le coup 799 [^8]: Pour le meilleur et pour le pire. Par exemple les variantes généralement les plus répandues sont celles de GNU c'est à dire souvent les plus lourdes et complexes. 800 [POSIX]: https://en.wikipedia.org/wiki/POSIX 801 [^9]: et c'est en réalité toujours le cas 802 [busybox]: https://busybox.net 803 [^10]: petite mise en pratique improptu hop 804 [sed et ses performances]: ../substitutions 805 [tentative de reproduction]: starcraft.html 806 [article analysant des données à propos d'un tournoi de starcraft]: https://www.kaggle.com/code/fulviocapra/sc2-iem-katowice-data-analytics