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 !

Une réflexion sur “supercalifragilisticexpialidocious Incanter-3

  1. Pingback: 1, 2, 3 . . . Incanter « Claude au pays des 4J

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s