Depuis pas mal de temps, je m’intéresse à Clojure, mais j’ai eu beaucoup de mal à trouver un projet pour m’y mettre un peu sérieusement.
Certains vous diront que c’est un “General Purpose Language”, qu’on peut donc l’utiliser pour n’importe quel projet. Mais lorsqu’on part sur un nouveau langage, surtout un langage si différent, on voudrait trouver un domaine où il se montre meilleur que les autres.
Je pense que la semaine dernière, j’ai trouvé un usage où Clojure brille particulièrement : l’exploration de données.
Revenons au départ, Clojure est un LISP et LISP ça ne veut pas dire “Lot of Insipid, Stupid Parentheses”, mais LISt Processing. Ce qui donne un bon indice sur ce qu’aime manipuler le langage.
De plus, dans sa dernière version (1.9) Clojure embarque une nouvelle fonctionnalité Clojure.Spec. Cette librairie vous permet de définir à quoi doivent ressembler vos données (dans la suite de l’article, ces définitions seront nommées “spec”). Bien sûr, on peut définir le type, mais on peut par exemple définir un domaine de valeur, une longueur pour une chaîne de caractère, … Ensuite, cette définition peut être confrontée à vos données pour ne conserver que celles qui correspondent ou au contraire comprendre en quoi certaines ne correspondent pas.
Ajoutez à cela le REPL (Read–Eval–Print Loop) pour un développement plus rapide et vous voilà avec une belle boîte à outils pour bricoler vos données.
Pour l’un de nos projets, nous écrivons une application web qui vise à compléter un outil client lourd existant.
Nous allons donc devoir intégrer leurs données. Dans un premier temps, cette intégration se fera par l’intermédiaire d’un fichier CSV. Le client nous envoie un premier fichier pour exemple. Ce premier fichier contient des champs faciles (nom, prénom), mais aussi des champs codés (sexe, pays, …).
J’ai lu récemment des choses sur “spec-provider” (https://akvo.org/blog/production-data-never-lies/), cette librairie se propose de lire vos données et essaie d’en écrire la spec (type de données, nullité, …). Je me dis que c’est le moment de montrer à mes collègues de quoi Clojure est capable !
Etape 1 : Le client du pays -6 est un %SEXE%
Avant de penser qu’on pourrait être aider par Clojure, un collègue a ouvert le fichier, entre autres on notera qu’il a repéré une colonne “sexe” avec des valeurs 1 et 2, où 2 semble associé à des prénoms féminins. Pour cette colonne, c’est réglé ! Pas si sûr…
Il est temps de vous montrer un peu de code.
https://gist.github.com/Charlynux/b6f1b1db5649e563ac2a8d5adec4772b
Nous nous appuyons sur deux librairies : data.csv et spec-provider.
On voit deux fonctions utilitaires : `csv-data->maps`, qui transforme le fichier CSV en liste de maps clef-valeur, les clefs sont générées à partir de la première ligne du CSV et `write-dataset-edn!`, qui écrit de la donnée Clojure dans un fichier adapté (.edn est au Clojure, ce que .json est au Javascript).
Je n’ai aucun mérite à l’écriture de ces fonctions : la première se trouve telle quelle dans le README de data.csv et la seconde se trouve en cherchant “write EDN file”.
Ensuite dans la fonction main, on branche tout ça ensemble. On remarquera que j’utilise `stats/collect` de spec-provider. Dans un premier temps je ne cherche pas à générer les specs, mais simplement à avoir une vision globale de mes données.
Voici un extrait de fichier de statistiques :
:ville { :distinct-values #{"" "GENERAC" "ST ETIENNE" "DOMPIERRE SUR MER" "BOURGANEUF" "ST AVERTIN" "LE CLOITRE PLEYBEN" "BARR" "PLOUAY" "DONCHERY"}, :sample-count 51528, :pred-map {#object[clojure.core$string_QMARK___5132 0x60b4beb4 "clojure.core$string_QMARK___5132@60b4beb4"] { :sample-count 51528, :min-length 0, :max-length 35 }, :hit-distinct-values-limit true }
A la lecture de ce fichier, on découvre quelques surprises :
- le pays est un code étrange (“-1”, “-6”, “12”)
- un champ date montre des exemples de la forme YYYYMMDD mais indique une longueur max à plus de 20…
- le sexe a 3 valeurs possibles 1, 2 et … %SEXE% (???)
En 25 lignes de code, nous venons de révéler un certain nombre de problèmes dans le fichier. Problèmes que nous pouvons remonter au client.
Interlude : Les doublons du collègue
Le concept a bien plu, alors en attendant les réponses du client, on se dit qu’on va essayer sur un autre projet.
Avec un collègue, on transforme son fichier Excel en CSV et on le met en entrée. Le fichier étant plutôt maîtrisé, les statistiques n’intéressent pas particulièrement mon collègue.
Par contre, une colonne de son fichier le dérange. Elle devrait contenir un identifiant unique, mais il y a trouvé des doublons et il se demande si je ne peux pas lui en sortir la liste.
On change l’appel à `stats/collect` par 4 lignes de code.
(map :reference) (frequencies) ; Retourne des tuples référence/nb d’occurrences (filter #(> (second %) 1)) (sort-by second)
Et voilà la liste des doublons dans la colonne référence triés par nombre d’occurrences !
Mon collègue va pouvoir indiquer à son client quelles sont les références qui posent problème, plutôt que de dire “Nous avons trouvé des doublons. Pourriez-vous les supprimer s’il vous plaît ?”.
Etape 2 : Le nouveau fichier
Suite à nos remarques, notre client a revu son fichier CSV. Sans attendre on le met en entrée de notre moulinette et on regarde les statistiques.
- Les pays sont maintenant “”, “France” et “Belgique”.
- Le sexe est devenu civilité et contient désormais uniquement “Monsieur” ou “Madame”.
Il est temps de passer à une nouvelle problématique: dans un fichier CSV tout est “String”. Pour aller plus loin dans l’analyse, il va donc falloir regarder colonne par colonne ce qu’elles pourraient contenir (entier, décimal, date ou string).
Une fois la liste faite, on écrit les conversions en respectant deux règles :
- Si la conversion échoue (ex. NumberFormatException), on conserve la valeur initiale sous forme de string.
- Si la valeur est une chaîne vide, on met la valeur à null.
On place la conversion avant le calcul des statistiques et on regarde le résultat.
- Ce qui semblait être un identifiant est parfois null. (14/52000 enregistrements)
- Les poids vont jusqu’à 6000 kg (l’application gère des êtres humains, pas des éléphants).
- Si la majorité des conversions semblent avoir fonctionnées sans problème, certains champs décimaux contiennent encore des strings.
Ce dernier point pose une nouvelle question. Quelles sont ces valeurs que nous ne sommes pas parvenues à convertir ?
Et sur ce point, `stats/collect` ne va pas nous aider. Il est temps d’utiliser la fonctionnalité première de spec-provider : générer des specs. On remplace l’appel à `stats/collect` par `sp/infer-specs` et on copie-colle le résultat dans notre programme.
Les lignes ressemblent à cela :
(s/def ::nb-enfants (s/nilable integer?))
Même sans connaissance de clojure.spec, on comprend que le nombre d’enfants est un entier qui peut-être nul. Au passage, on note une nouvelle question pour le client, est-il normal qu’un entier soit nullable alors qu’une valeur à 0 serait peut-être plus cohérente ?
Laissons cela de côté pour l’instant, il nous faut nous occuper des champs à “double type” :
(s/def::poids (s/nilable (s/or :double double? :string string?)))
Nous allons modifier ces specs en enlevant le “or string” et affirmer que nous ne voulons que des décimaux.
Cette fois, on demande à clojure.spec de nous expliquer en quoi un enregistrement ne respecte pas la spec et on exclut ceux qui correspondent (pour lesquels l’explication est null).
(map #(s/explain-data ::our-spec %)) (filter (complement nil?))
Ce qui nous donne une liste de ce type :
#:clojure.spec.alpha{:problems ({:path [ :poids :clojure.spec.alpha/pred], :pred clojure.core/double?, :val “.”, } { :path [:poids :clojure.spec.alpha/nil], :pred nil?, :val “.” }), :value { ;; tout l’objet de la ligne en question } }
C’est un peu verbeux pour être envoyer tel quel au client, mais pour l’instant, on va se contenter de lui remonter manuellement les problèmes.
Ici, on peut voir que le poids n’est ni un décimal, ni null et que sa valeur est “.” (d’où l’intérêt d’avoir conserver la valeur lors d’une conversion ratée).
En continuant notre exploration et sans ajouter beaucoup de code, nous avons trouver de nouveaux points de discussion avec notre client.
Conclusion
Nous avons vu qu’avec très peu de code et un peu de compréhension de Clojure, nous avons été en mesure de révéler des choses inattendues aussi bien sur les données du domaine (poids impossibles) que sur l’intégration du fichier (valeurs décimales incorrectes).
Mais il faut surtout retenir qu’il ne s’agit que d’une aide à l’analyse et non d’une analyse automatique, un certain nombre d’étape ont été réalisé manuellement. Certains de nos questionnements dépassent la simple technique et il nous faudra attendre des réponses de la part des experts du domaine.