Présentation Incanter à l’Open World Forum2012

L’Open World Forum commence aujourd’hui et se tiendra jusqu’à demain. C’est une conférence Open Source gratuite qui s’adresse à tous.

Vous pouvez trouver des informations plus détaillées sur le track Code sur le site de l’OSDC.

Je ferai une présentation sur l’analyse de données avec Incanter samedi après midi. Les slides et le repository de code sont accessibles

Présentation Clojure au JUG Summer Camp

J’ai le plaisir de faire une présentation Incanter / Clojure au JUG Summer Camp. Le but est de couvrir quelques librairies utiles pour faire des analyses et des rapport sur de grosses masses de données.

Le code des exemples se trouve sous Github https://github.com/cfalguiere/ClojureBigDataJugSummerCamp2012

Les slides de la présentation en pdf sont sur SlideShare

 

Quickie sur Incanter à Devoxx France

Et voilà, ce premier Devoxx France est terminé. Un superbe évènement, des speakers passionnants, des participants tous plus motivés les uns que les autres, et une équipe d’organisation soudée pour donner le meilleur d’elle même durant ces 3 jours. Du Java, mais pas que ça et surtout beaucoup de rencontres, de discussions enflammées et de plans pour refaire le monde en mieux. On attend avec l’impatience l’édition Devoxx World à Anvers fin 2012 et le prochain Devoxx France en 2013. Bon là on est sur les rotules, il faut qu’on récupère.

Merci aussi à l’équipe des Cast Codeurs qui a permis à 4 Duchess d’intervenir pendant l’enregistrement Live à la fin de Devoxx France.  On s’est bien amusées et on attend la sortie avec impatience pour le réécouter.

J’ai uploadé les slides de mon quickie sur Incanter  http://www.slideshare.net/claude.falguiere/incanter-devoxx

Le code des examples est sur Github https://github.com/cfalguiere/Demo-Incanter-Devoxx-France

Le temps du quickie est passé très vite. Je prépare une version atelier de 1h pour présenter Incanter et Clojure et pratiquer un peu Clojure en l’appliquant à l’analyse de données.

Des nouvelles de la saga Incanter

La magie ne marche pas très bien sur les virus et autres microbes ;-)

Après quelques batailles, j’ai fini par en venir à bout, mais Devoxx arrive bientôt avec toutes les choses qu’il faut préparer pour cet événement, en particulier une courte présentation sur Incanter.

La sage du magicien n’est pas terminée, mais elle reprendra probablement après Devoxx.

Alohomora Incanter-4

Incanter logo

Quand on ne sait plus trop où trouver des formules magiques, il reste Harry Potter, Alohomora l’incantation qui ouvre les portes, les portes de la voie des statistiques.

Allez au boulot, on a du rangement à faire.

Cet article fait partie d’une suite qui commence avec cet article épisode 1.

Recharger les données

First things first, charger nos librairies et nos données et on reprend où on en était dans l’épisode 3.

user=> (use '(incanter core io datasets stats))
nil
user=> (def data (read-dataset
"/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))

#'user/data

Il est grandement temps de faire une fonction qui charge les résultats. On peut être sorcier et fainéant.

user=> (defn read-results []
  (read-dataset "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv" :header true))

#'user/read-results

N'oubliez pas les parenthèses, sinon au lieu du prince charmant vous auriez une jolie mangouste, jolie mais bon ça n'est pas ce que vous vouliez. En l'occurrence, vous ne voulez pas que data représente la fonction mais son résultat.

user=>(def data read-results)
#'user/data
user=> data
#<user$read_results user$read_results@6bd46c20>

Donc voilà, le tour exécuté correctement et quelques donnnées présentes.

user=>(def data (read-results))
#'user/data

Pour le moment j'expérimente interactivement, donc peu importe l'IDE. Lançons un éditeur de texte pour copier ce code et chargeons le dans le REPL.

(use '(incanter core io datasets stats))

(def result-file-name "/Users/cfalguiere/Documents/2012-01/RD-Clojure/Workspace/incanter/resources/sample-results.csv")

(defn read-results []
  (read-dataset result-file-name :header true))

(def data (read-results))

J'aimerais bien indiquer le répertoire courant pour éviter tout ces chemins mais à la différence de R on ne peut pas changer le répertoire courant, parce que la JVM ne permet pas de le faire. Mais au fait quel serait mon répertoire courant ? Une rapide recherche sur Google indique ça :

user=> (. (java.io.File. ".") getCanonicalPath)
"/Applications"

Un peu ésotérique. C'est plus clair avec un exemple plus évident comme la méthode toUpperCase de la classe String. Le . applique la méthode à l'objet.

user=>(. "aaa" toUpperCase)
"AAA"

Il ne reste plus qu'à charger le programme et data est disponibles pour d'autres tours.

user=>(load-file "/Users/cfalguiere/Documents/2012-01/RD-Clojure/workspace/incanter/tuto1.clj")
#'user/data
user=> (nrow data)
209

Agrégeons, agrégeons, ils en restera toujours quelque chose

En général, j'aime bien avoir une vue d'ensemble de mes données : temps max, temps moyen, taux d'erreur ...

Qu'est ce que j'ai là dedans ?

Le dataset est une map qui contient un vecteur de labels et une liste de lignes de données. La fonction col-names retourne les labels des colonnes (c'est équivalent (:column-names data))

user=> (col-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]

Les labels JMeter ne sont pas des plus parlants pour des raisons de compacité du résultat.

Pour chaque relevé nous disposons des informations suivantes :

  • :t le temps de réponse,
  • :lt le 'latency time' c'est à dire le temps qui s'est écoulé jusqu'à la réception du premier octet de la réponse,
  • :by le nombre d'octets de la réponse,
  • :na : le nombre d'utilisateurs simulés au moment de cette requête,
  • :rc le code retour de la requête HTTP, habituellement 200 ou 302,
  • :s le résultat des assertions sur les contenus reçus (true ou false),
  • :rm : un message,
  • :lb le label soit le nom donné au sample dans le script JMeter soit une url,
  • :ts  le timestamp du relevé.

Les 4 premières lignes sont des séries numériques. Les autres sont des catégories (l'équivalent des factors pour R). Elles vont servir à filtrer (pour les statuts) ou à regrouper les données.

Le module stats fourni les statistiques habituelles. Pour faire quelques expériementations, on va générer une série de nombres dont la moyenne est connue. sample-normal construit une liste de 1000 nombres distribués selon une loi normale centrée sur 10 avec un écart-type de 1. Sans surprise, la moyenne est autour de 10 et l'écart type est autour de 1.

user=> (def mysample (sample-normal 1000 :mean 10 :std 1))
(10.099828740537856 9.903470850168006 9.52228847634955 9.003518490739022 9.171579274811812 ...
user=> (mean mysample)
9.941436842660105
user=> (sd mysample)
1.0007997747452984

Et min et max ?

présentation des 4 quartiles

Un magicien ne voit pas les choses aussi simplement. Le min est la valeur qui est inférieure à tous les autres relevés. Et symétriquement le max est la valeur qui est supérieure à tous les autres relevés.

Mais les magiciens aiment bien découper les données en tranches. Donc ils veulent aussi savoir quelle est la valeur qui sépare les relevés en deux groupes, inférieurs et supérieurs à une valeur qui s'appelle la médiane.

Pour améliorer le spectacle, les données sont découpées en quatre tranches par pas de 25% comme le montre le schéma de droite, ce sont des quartiles. On voit sur le schéma que la surface rouge qui représente les 25% les plus bas se termine vers 9, la surface bleu qui représente les 25% suivants se termine vers 10 etc.

Les valeurs de 25% sont arbitraires. La forme générale de cet agrégat est le quantile. La fonction quantile retourne le min, la valeur maximale pour 25% des relevés, 50% (la médiane) et 75%, et le max (le quantile 100%).

user=> (quantile mysample)
(6.078738192126153 9.284660278736457 9.9269193454763 10.609659921191085 12.899443382831647)

Le monde étant ce qu'il est les sorciers qui font du test de charge ne s'intéressent pas aux quantiles des magiciens. En fait la seule chose qui les intéressent, c'est l'impact des 5% ou 10% de temps les plus longs. Donc ils calculent la valeurs maximale pour 90% ou 95% des relevés.

user=> (quantile mysample :probs 0.95)
11.591767882673627
user=> (quantile mysample :probs [0.9 0.95])
(11.278966306153395 11.591767882673627)

Qu'est ce que ça donne sur nos données de test ?

user=> (mean ($ :t data))
1429.579831932773
user=> (sd ($ :t data))
2237.25502018713

On peut faire toutes ces opérations sur la même série de données en utilisant la fonction associée au dataset with-data. Elle  factorise le dataset et permet de définir l'expression à utiliser, ici construire un tableau contenant les différentes métriques. $data représente la série à l'intérieur du with-data.

user=> (with-data ($ :t data)
  [(mean $data)(sd $data)])

[1429.579831932773 2237.25502018713]

Si on rajoute le quantile ça nous donne ce qui suit. Un bon début, mais on a un tableau des différents indicateurs plus une liste avec les quantiles.

user=> (with-data ($ :t data)
  [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])])

[119 1429.579831932773 2237.25502018713 (10.0 708.0 3393.4 4130.999999999982 13007.0)]

Mmm ... un coup de baguette magique flatten et les données se retrouvent bien alignées dans le tableau.

user=> (with-data ($ :t data)
  (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) )

(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Une variable pour stocker le résultat et on sauve

user=> (def stats
  (with-data ($ :t data)
    (flatten [(count $data)(mean $data)(sd $data)(quantile $data :probs[0 0.5 0.9 0.95 1])]) ) )

#'user/stats
user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4 4130.999999999982 13007.0)

Quoique, pas tout de suite apparemment.

user=> (save 'stats "stats.csv")
java.lang.IllegalArgumentException: No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)

En quête d'une multiméthode ...

Que peut bien être une multiméthode ?

Vous trouverez une réponse complète dans cet article sur le polymorphisme en Clojure. En quelques mots, c'est le mécanisme qui permet à une fonction de se comporter différement en fonction des paremètres qu'on lui passe (des nombres ou des chaînes par exemple).

Vous avez probablement déjà noté que les fonctions en clojure sont souvent définies pour plusieurs nombres d'arguments (l'arité de la fonction si vous voulez briller la nuit au prochain ParisJUG). Clojure permet qu'une fonction soit surchargée (overloaded en anglais) et elle peut ainsi avoir une définition différente selon le nombre d'arguments.

Ci-après un exemple très simple de surcharge qui crée une map rectangle avec 1 ou 2 paramètres. La forme a un seul paramètre utilise la forme a 2 paramètres pour construire un carré.

user=> (defn make-rectangle
  ([width] (make-rectangle width width))
  ([width height] {:width width :height height}))

#'user/make-rectangle
user=> (make-rectangle 20)
{:width 20, :height 20}
user=> (make-rectangle 10 20)
{:width 10, :height 20}

Autre exemple plus compliqué, la multiplication implémentée de manière récursive en utilisant 4 formes :

(def mult
  (fn this
    ([] 1)
    ([x] x)
    ([x y] (* x y))
    ([x y & more]
      (apply this (this x y) more))))

La fonction est définie pour 0, 1, 2 arguments et un nombre quelconque d'arguments. Le & indique que ce qui se trouve à droite contient une liste d'arguments, et c'est par ce moyen qu'on définie une fonction variadique (pour quand vous ne brillerez plus assez avec arité).

Décryptons un peu cette multiplication. Tout d'abord, cet exemple n'utilise pas la macro defn, on a donc deux les étapes, décrire la fonction (fn) et la nommer (def). La multiplication utilise 4 variantes. Dans le cas général (4) je prend les 2 paramètres les plus à gauche, je leur applique "moi-même" (this x y) et j'applique ensuite "moi-même" en utilisant ce résultat et tous les paramètres qui restent à droite (more). Si je n'ai que 2 paramètres (3), je les multiple. Si je n'en ai plus qu'un (2), je le renvoie. Si je n'en ai plus (1) je renvoie 1.

Et pourquoi apply ? apply est utilisé dans les contextes où le nombre d'élements n'est pas connu à la compilation et donc typiquement lorsque l'on déconstruit une séquence. Par exemple

user=> (+ 1 2 3 4 5)
15
user=> (+ [1 2 3 4 5])
java.lang.ClassCastException (NO_SOURCE_FILE:0)
user=> (apply + [1 2 3 4 5])
15

L'addition du contenu de la séquence n'est pas possible directement. L'utilisation d'apply permet de déconstruire le tableau pour en lister les éléments et d'effectuer l'opération. Dans le cas de la fonction mult, apply permet de traiter more qui est une séquence.

Et ma multiméthode ?

La multiméthode est un mécanisme plus puissant qui permet de gérer le polymorphisme. La syntaxe est un peu différente et basée sur les verbes defmulti et defmethod.

L'exemple suivant montre une multiméthode "rencontre" entre différentes espèces. Si le paramètre 1 est un lapin et le paramètre 2 un lion, le comportement de "rencontre" est équivalent à la fonction "s'enfuit". Dans le cas inverse, elle est équivalente à "mange"

(defmulti encounter (fn [x y] [(:Species x) (:Species y)]))
(defmethod encounter [:Bunny :Lion] [b l] :run-away)
(defmethod encounter [:Lion :Bunny] [l b] :eat)

L'exemple complet se trouve dans cet article sur le polymorphisme en Clojure.

Les multimethodes peuvent dispatcher en fonction des types des paramètres mais également sur des valeurs des arguments, leur nombre ou des méta-données selon la syntaxe.

Bref,  "No method in multimethod 'save' for dispatch value: class clojure.lang.Symbol (NO_SOURCE_FILE:0)" signifie seulement "je n'ai pas d'implémentation de save pour le type que tu m'envoie". Oui, je sais ..., mais les magiciens ne vont pas dévoiler tous leurs tours si facilement.

Enfin sauvé

stats est une liste et  save est une fonction Incanter qui fonctionnera que sur une matrice ou un dataset.
Qu'à celà ne tienne, on va construire un dataset avec ces données. Il faut lui indiquer les noms de colonnes et les valeurs.

user=> stats
(119 1429.579831932773 2237.25502018713 10.0 708.0 3393.4
4130.999999999982 13007.0)
user=> (def statsds
 (dataset ["count", "mean", "sd", "min", "median", "q90", "q95", "max"] stats) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"count" 119} {"count" 1429.579831932773} {"count" 2237.25502018713} {"count" 10.0} {"count" 708.0} {"count" 3393.4} {"count" 4130.99999
9999982} {"count" 13007.0})}
user=> (view statsds)

Mmm, est ce bien ce que l'on veut ? Non, en fait le dataset n'est pas dans le bon sens. Il devrait avoir 1 ligne avec une colonne par indicateur.

Si vous allez sur la fonction dataset vous pouvez voir le source sous github. La fonction dataset attend une séquence de séquences ou une séquence de maps. Avec une simple liste, dataset a considéré que l'on voulait une liste de plusieurs lignes de 1 colonne.

La façon la plus simple de résoudre le problème est de transformer la liste en dataset en la transposant pour qu'elle constitue 1 ligne de plusieurs colonnes. Ensuite on lui ajoute des noms de colonnes.

user=> (def statsds (col-names
  (to-dataset stats :transpose true) ["count", "mean", "sd", "min", "median", "q90", "q95", "max"]) )

#'user/statsds
user=> statsds
#:incanter.core.Dataset{:column-names ["count" "mean" "sd" "min" "median" "q90" "q95" "max"],
:rows ({"max" 13007.0, "q95" 4130.999999999982, "q90" 3393.4, "median" 708.0, "min" 10.0, "sd" 2237.25502018713, "mean" 1429.579831932773, "count" 119})}
user=> (view statsds)
Le dataset est bien dans le bon sens et on peut le sauver
user=> (save statsds "/Users/cfalguiere/Documents/stats.csv")
nil

Il manque juste une dernière info utile le taux d'erreurs et le top n.
Pour le taux d"erreurs, on verra plus tard (surtout qu'il n'y en a pas dans le jeu de données) mais pour le top 5 des pires temps voici l'incantation trier-les-lignes-renverser-l-ordre-et-prendre-les-5-premiers

user=> (take 5 (reverse (sort-by :t (:rows data))))
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay"}
{:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} ...

Et voilà d'autres tours de sorciers inscrits dans le grimoire. Dans l'épisode 5 on regroupera ces données pour avoir des résultats plus détaillés, par label par exemple et on fera des graphes de ces données.

supercalifragilisticexpialidocious Incanter-3

Incanter logo

Continuons notre voyage dans les sortilèges d’Incanter avec l’incantation de Mary Poppins,

supercalifragilisticexpialidocious

Le balai arrive. En route pour le monde où nos tableaux de nombres vont se ranger tout seuls !

Cet article fait partie d’une suite. L"épisode 1 présentait quelques bases de Clojure et Incanter.  L’épisode 2 présentait les types.

Où trouver des grimoires ?

Nous allons manipuler un peu plus d’API et quelques documents vont être bien utiles.

Clojure ainsi que la librairie présentent l’API complète ainsi que des cheat sheets pour regrouper les commandes les plus fréquentes.

To use or not to use ?

Les librairies sont organisées en modules. Pour utiliser une des formules magiques vous devez d’abord charger les librairies correspondantes.

user=> (use 'incanter.core)
nil
user=> (use 'incanter.io)
nil
user=> (use 'incanter.datasets)
nil

Un peu laborieux au bout d'un moment, non ?

use ou require sont des fonctions du package clojure.core qui chargent des librairies, c'est à dire un ensemble de ressources qui se trouvent dans un package Java. Un fichier Clojure à la racine de cette librairie sert de descripteur.

On a souvent à charger différentes librairies avec le même préfixe (par exemple incanter.core incanter.io ...)
On peut réécrire ces lignes en plus compact en utilisant la forme suivante (prefix list).

user=> (use '(incanter core io datasets))
nil

Vous noterez que les librairies sont maintenant passées dans une liste. Le premier élément est le préfixe, les autres sont les librairies sans le prefixe. Attention, les noms passés dans la liste ne doivent pas contenir de . .

Et bien sûr une  ' pour empêcher l'évaluation de la liste.

Convoquer les données

Pour commencer, il vous faut quelques tableaux de nombres.

Incanter propose un moyen rapide de créer un dataset à partir d'un fichier CSV. Si vous n'avez pas de données sous la main, Incanter est livré avec quelques jeux de données (http://liebke.github.com/incanter/datasets-api.html) que l'on peur charger en utilisant la fonction get-dataset.

Commençons avec un résultat de test JMeter transformé en CSV. Un tir étalon pour ne pas avoir trop de données pour le moment.

user=> (def data (read-dataset "/Users/cfalguiere/Workspaces/Diapason/report-data/20111217-ETALON_WSW_TPS2011.csv" :header true))
#'user/data

Petite vérification, avons nous bien des données ? capture d'écran du résultat de view

user=> (view data)
#<JFrame javax.swing.JFrame[frame1,0,22,400x600,invalid,layout=java.awt.BorderLayout,title=Incanter Dataset,resizable,normal,defaultCloseOperation=HIDE_ON_CLOSE,rootPane=javax.swing.JRootPane[,0,22,400x578,invalid,layout=javax.swing.JRootPane$RootLayout,alignmentX=0.0,alignmentY=0.0,border=,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]>
user=>

La fonction view permet de visualiser ce dataset.

Combien êtes vous ?

La fonction de dénombrement Clojure est count.

user=> (count [1 2 3])
3
user=> (count "Incanter")
8

Donc voilà

user=> (count data)
2

Mmmmm ... 2 ... ???

Regardons de plus près à quoi ressemble ce dataset.

user=> data
#:incanter.core.Dataset{
  :column-names [:lb :t :lt :ts :s :rc :rm :by :na],
  :rows (
    {:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"}
    {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"}
    ;...
    {:na 1, :by 861, :rm "OK", :rc 200, :s "true", :ts 1.324109908066E12, :lt 21, :t 21, :lb "/AjaxLastViewedDisplay"}
  )}

Le dataset est une map composée de 2 éléments :column-names qui contient tous les labels de colonnes dans un vector et :rows qui est une liste comportant une map par ligne de données.

On obtient un résultat plus pertinent en comptant les entrées de la liste. Incanter fournit aussi une fonction nrow qui compte correctement les lignes d'un dataset (n'oubliez pas que le dataset n'est pas une structure de données Clojure)

user=> (count (:rows data))
119
user=> (nrow data)
119

Le count de l'élément :rows de data nous donne bien le même compte que le nrow du dataset.

Découper en tranches

Quelques incantations Clojure pour se faire la main. Affectons les lignes du dataset a une variable pour les manipuler plus facilement.

user=> (def datarows (:rows data))
#'user/datarows
La Clojure way

Donne moi les deux premières lignes !

user=> (take 2 datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})

Donne moi tous les labels !

On va utiliser la fonction map pour appliquer à chaque ligne une fonction qui renvoie le label.

user=> (map :lb datarows)
("/" "/CategoryDisplay" "/--product--.html" ...

Et tout à la fois !

user=> (take 2 (map :lb datarows))
("/" "/CategoryDisplay")
L'Incanter way

Incanter propose des fonctions de manipulation de dataset. La fonction utilisée pour extraire des données est sel plus souvent utiilisée via son alias $.

L'alias $ a plusieurs formes. Il peut être utilisé avec la colonne seule (identifié par sa position, son nom, ou une collection de ces indications), ou bien la ligne et la colonne. Le dernier paramètre est le dataset.

La fonction sel place le dataset en premier argument et spécifie l'axe par des keywords.

Donne moi tous les labels !

user=> ($ :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :cols 0)
("/" "/CategoryDisplay" "/--product--.html" ...

Un usage typique est celui-ci, extraire une série, ici le temps de réponse et calculer un agrégat tel que la moyenne :

user=> (mean ($ :t data) )
1429.579831932773

Vous avez testé et ça ne marche pas, jeune sorcier ? C'est normal, il faut le package incanter.stats qui n'a pas été chargé pour le moment.

Donne moi les deux premières lignes !

Pour la fonction sel le paramètre qui suit :rows indique soit un numéro particulier de ligne, soit une liste de numéros de lignes, soit un range qui va générer cette liste de numéros de lignes.

Lorqu'on utilise $ le premier paramètre représente la ligne (ou les lignes) et le second la colonne.

user=> (sel data :rows (range 2) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}
user=> (sel data :rows '(0 1))

Ce qui est équivalent à :

user=> ($ (range 2) :all data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}

Vous noterez le :all. La syntaxe oblige à spécifier deux paramètres si l'on veut indiquer des lignes, les lignes puis les colonnes. :all indique que toutes les colonnes doivent être conservées.

Donne moi les labels des cinq premières lignes !

user=> user=> ($ (range 5) :lb data)
("/" "/CategoryDisplay" "/--product--.html" ...
user=> (sel data :rows (range 5) :cols :lb)
("/" "/CategoryDisplay" "/--product--.html" ...

Toutes sortes de combinaisons sont possibles. L'expression suivante retourne un dataset ne comportant plus que les colonnes sélectionnées et les deux premières lignes.

user=> ($ (range 2) [:lb :t] data)
#:incanter.core.Dataset{:column-names [:lb :t], :rows ({:t 808, :lb "/"} {:t 8523, :lb "/CategoryDisplay"})}

S'il y a plusieurs colonnes, le résultat est un dataset, mais s'il n'y a qu'une ligne la structure de données est également différente. Le résultat est une liste et non un map. Les éléments sont ordonnés conformément aux colonnes du dataset.

user=> (sel data :rows 0)
("/" 808 794 1.324109256665E12 "false" 200 "OK" 27985 1)
user=> (:column-names data)
[:lb :t :lt :ts :s :rc :rm :by :na]

Pour finir, il existe un mot clé :not qui permet d'exprimer des listes par exclusion. Dans l'exemple suivant, l'expression conserve toutes les colonnes sauf :by et :na pour les deux premières lignes.

user=> ($ (range 2) [:not :by :na] data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm], :rows ({:rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"})}

Trouver l'élu

Attention jeune sorcier ça se complique.

Pour analyser les résultats on aura souvent besoin de retrouver des sous ensemble sur des critères particuliers, tous les relevés pour une url données (:lb), les relevés avec des temps très élevés (:t), ou les relevés en erreur (statut :s, return code :rc, message :rm).

La Clojure way

Les collections Clojure ont une fonction filter filter. La ligne suivante donne tous les nombres pairs du vecteur [1 2 3 4 5 6].

user=> (filter even? [1 2 3 4 5 6])
(2 4 6)

Dans le cas plus général on ne pourra pas utiliser une fonction existante et il faudra écrire sa propre fonction filtre.

user=> (defn filterAlice [name] (= name "Alice"))
#'user/filterAlice
user=> (filter filterAlice ["Alice", "Bob", "Charles"])
("Alice")

D'une manière générale on utilisera plutôt une fonction anonyme. Ce qui nous donne :

user=> (filter (fn[name](= name "Alice")) ["Alice", "Bob", "Charles"])
("Alice")

C'est un peu verbeux et vous trouverez plus souvent une forme abrégée utilisant #.

user=> (filter #(= % "Alice" ) ["Alice", "Bob", "Charles"])
("Alice")

# est appelé la macro dispatch et a plusieurs effets décrits ici. Celui qui nous intéresse ici est #(...) qui est un équivalent de (fn [args] (...)).

user=> ( #(+ 1 %) 2 )
3
user=> ( #(+ 1 %1 %2) 2 3 )
6

% représente  l'argument, ou les arguments, de la fonction anonyme, ici le paramètre :name implicite.

Cette fonction anonyme appliquée sur la liste de prénom donne bien le même résultat.

user=> (filter #(= % "Alice") ["Alice", "Bob", "Charles"])
("Alice")

Et sur mes données maintenant ?
Donne moi toutes les lignes dont le label est "/" !

user=> (filter #(= % "/") datarows)
()

C'est vide ? Normal, chaque ligne de la liste est une map. Il faut donc récupérer le :lb.

user=> (filter #(= (:lb %) "/") datarows)
({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})

Autre critère, les temps supérieurs à 10s ? Changeons simplement de fonction pour  #(> (:t %) 10000).

user=> (filter #(> (:t %) 10000) datarows)
({:na 1, :by 172777, :rm "OK", :rc 200, :s "true", :ts 1.324109405557E12, :lt 1398, :t 13007, :lb "/CategoryDisplay-REF"} {:na 0, :by 33268, :rm "OK", :rc 200, :s "true", :ts 1.324109406957E12, :lt 11602, :t 11607, :lb "http://www.witre.se/spannare_85915M.html?leafcode=85919&fromSearch=true"})

Et pour finir comment exprimer la négation ? Par exemple, trouver tous les relevés dont le statut n'est pas OK.

user=> (count (filter #(= (:rm %) "OK") datarows))
97
user=> (count (filter #(not= (:rm %) "OK") datarows))
22
user=> (count (remove #(= (:rm %) "OK") datarows))
22

Le total fait bien 119. Deux solutions sont possibles, remove ou filter avec l'expression complémentaire. La fonction remove n'altère pas la liste initiale.

L'incanter Way

Incanter propose la fonction query-dataset. Cette fonction a elle aussi un alias $where.

Les prédicats de query-dataset peuvent être exprimés dans un langage voisin du langage de requête de MongoDB.

Donne moi tous les relevés de la home page !

Ces relevés sont ceux dont la colonne :lb vaut "/".

user=> (query-dataset data {:lb "/"} )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

La même chose exprimée avec l'alias $where

user=> ($where {:lb "/"} data)
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

On peut aussi exprimer plusieurs critères. Ci-dessous, les temps entre 8s et 10s.

user=> (query-dataset data {:t {:$gt 8000 :$lt 10000} } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 28383, :rm "OK", :rc 200, :s "false", :ts 1.324109266396E12, :lt 8511, :t 8523, :lb "/CategoryDisplay"} {:na 1, :by 33235, :rm "OK", :rc 200, :s "true", :ts 1.324109282891E12, :lt 8967, :t 8981, :lb "/--product--.html"} {:na 1, :by 30959, :rm "OK", :rc 200, :s "false", :ts 1.324109547672E12, :lt 8787, :t 8792, :lb "/CategoryDisplay"})}

Ou bien les code retours qui ne font pas partie de la liste acceptée, 200 (OK) ni 302 (redirect).

user=> (query-dataset data {:rc {:$nin #{200 302} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}
user=> (query-dataset data {:rm {:$nin #{"OK" "Found"} } } )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ()}

Les opérateurs disponibles dans les requêtes sont :$gt, :$lt, :$gte, :$lte, :$eq, :$ne, :$in, :$nin, $fn.

Les prédicats peuvent également être définis de manière classique par une fonction anonyme. La fonction s'applique à la ligne.

user=> (query-dataset data #(= (:lb %) "/" ) )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

Si vous avez déjà des fonctions dans vos grimoires, vous pouvez bien sûr les utiliser directement.

user=> (defn isHomePage [row] (= (:lb row) "/"))
#'user/isHomePage
user=> (query-dataset data isHomePage )
#:incanter.core.Dataset{:column-names [:lb :t :lt :ts :s :rc :rm :by :na], :rows ({:na 1, :by 27985, :rm "OK", :rc 200, :s "false", :ts 1.324109256665E12, :lt 794, :t 808, :lb "/"} {:na 1, :by 27974, :rm "OK", :rc 200, :s "false", :ts 1.324109395646E12, :lt 209, :t 213, :lb "/"} {:na 1, :by 27994, :rm "OK", :rc 200, :s "false", :ts 1.324109537182E12, :lt 247, :t 252, :lb "/"} {:na 1, :by 27983, :rm "OK", :rc 200, :s "false", :ts 1.324109682199E12, :lt 344, :t 349, :lb "/"} {:na 1, :by 27980, :rm "OK", :rc 200, :s "false", :ts 1.32410980267E12, :lt 212, :t 217, :lb "/"})}

Voilà, beaucoup de sortilèges a expérimenter en attendant l'épisode 4. Attention à ne pas faire trop de dégâts !