1, 2, 3 . . . Incanter


Incanter logoCet article est la suite de cet article abracadabra ! Incanter.

Pour traiter des données statistiques, les structures de données qui nous intéressent principalement sont les nombres et les tableaux de nombres.

Clojure utilise les types de données classiques pour représenter des textes String, Character, des Boolean (true/false), des nombres, plusieurs types de collections et deux types d’identifiants.

La documentation de l’interpréteur décrit de manière assez rapide les différents types de données http://clojure.org/reader.  Ce document http://clojure.org/data_structures décrit les structures de données disponibles de manière un peu plus approfondie. J’ai aussi trouvé ce document utile http://en.wikibooks.org/wiki/Learning_Clojure/Data_Types.

Les textes

Clojure repose sur les types Java. Donc java.lang.String et la manipulation des chaînes n’est pas très différente. Quelques opérations utiles :

user=> (type "A")
java.lang.String
user=> (str 144 " éléments") ; concatène les deux termes
144 éléments
nil
user=> (count "Incanter") ; compte le nombre de caractères de la chaîne
8

La quote ‘ a une signification particulière dans la syntaxe Clojure (on le verra plus loin). Il ne peut pas être utilisé pour les caractères. Les caractères sont définis par \. Il existe aussi des caractères spéciaux comme \tab \newline \space

user=> (type 'A')
java.lang.Exception: Unmatched delimiter: )
user=> (type \A)
java.lang.Character
user=> (println \A \tab \B)
A
        B
nil

Les identifiants

Clojure a 2 types d’identifiants, symbol et keyword.

Les  symbol sont des identifiants qui référencent quelque chose d’autre. Ce sont en général des noms de variable, de fonction, de classe.

Les keyword sont des identifiants qui référencent leur nom. Ce sont des sortes de chaînes constantes donc plus rapides à comparer. Elles sont principalement utilisées comme clés dans les hash-maps ou les méta-données des symbol. Un : est placé devant les keyword, mais il ne fait pas partie de son nom.

Ci-dessous la déclaration d’une variable avec un symbol x . On ne peut pas faire la même chose avec un keyword.

user=> (def y 1)
#'user/y
user=> y
1
user=> (def :y 1)
java.lang.Exception: First argument to def must be a Symbol (NO_SOURCE_FILE:221)
</code

Les keyword sont utilisés comme label dans les hash-maps. On voit que le keyword Alice retourne son nom.

user=> (def scores {:Alice 1, :Bob 2} )
#'user/scores
user=> (get scores :Alice)
1
user=> :Alice
"Alice"

Les  keyword sont aussi utilisés dans la définition des méta-données. Le nom de fonction hello est symbol. Un keyword :doc est utilisé pour ajouter une description. Les  keyword ne peuvent pas avoir de méta-données.

user=> (defn
  ^{:doc "pas très utile"}
  hello ([name] (println "Hello" name)))
#'user/hello
user=> (doc hello)
-------------------------
user/hello
([name])
  pas très utile
nil

Les symbol peuvent être utilisés là où des keyword seraient utilisés (même si c’est sans grand intérêt).
Essayons pour voir.
On ne peut pas utiliser le  symbol directement.

user=> (def scores {Alice 1, Bob 2})
java.lang.Exception: Unable to resolve symbol: Alice in this context (NO_SOURCE_FILE:375)

Contrairement aux keyword qui se résolvent sur eux même, les symbol tentent d’évaluer ce qu’ils référencent dès qu’on les utilise. Le keyword :Alice renvoie « Alice ». Le symbol Alice ne peut pas être évalué car il n’existe pas encore, et si on prend un symbol existant dans le contexte tel que y, il renvoie 1 dès qu’on utilise ce nom.

user=> :Alice
"Alice"
user=> (name y)
java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.Named (NO_SOURCE_FILE:0)
user=> y
1
user=> (name :Alice)
"Alice"

Le ‘ permet d’empêcher l’évaluation de l’expression et d’utiliser le symbol non évalué comme label.

user=> 'y
y
user=> (name 'y)
"y"user=> (def scores {'Alice 1, 'Bob 2}) ; pour démo, on utiliserait normalement :Alice :Bob
#'user/scores
user=> (get scores 'Bob)
2

Les nombres

Les nombres sont toujours « boxés » c’est à dire contenus dans un objet comme Integer pour le type int. Ces types sont dérivés de java.lang.Number.

Quelques différences avec Java. Pour les entiers, Clojure gère automatiquement la conversion vers des BigNum et ajoute un type Ratio. Pour les décimaux, ce sont par défaut des Doubles et Clojure préserve la précision lors des opérations numériques qui impliquent un Double.

La représentation de la constante influence le type. M indique un BigNumber et le . un décimal comme en Java.

user=> (type 1)
java.lang.Integer
user=> (type 1.0)
java.lang.Double
user=> (type 1M)
java.math.BigDecimal

Quelque soit le type de départ, le type est adapté pour contenir la valeur sans la tronquer lors des opérations arithmétiques. Un souci de moins.

user=> (type 123456789)
java.lang.Integer
user=> (type (* 123456789 123456789))
java.lang.Long
user=> (type (* 123456789 123456789 123456789))
java.math.BigInteger

Quelque soit le type utilisé les opérations de comparaison entre des types différents restent cohérentes.

user=> (== 1 1.0)
true
user=> (== 1.0 1M)
true
Ratio et divisions

Une spécificité de Clojure, le type Ratio permet de représenter les fractions d’entiers sans perte de précision. Il ne s’applique qu’aux entiers. La même opération retourne un décimal dès qu’une des valeurs est décimale.

user=> (/ 2 3)
2/3
user=> (type (/ 2 3)) ;; opération sur entiers
clojure.lang.Ratio
user=> (/ 2 3.0)
0.6666666666666666
user=> (type (/ 2 3.0)) ;; opération sur un entier et un décimal
java.lang.Double
user=> (/ 4 2) ;; opération sur entiers
2
user=> (type (/ 4 2) )
java.lang.Integer

Si on veut faire une division sur entiers, il faut coercer une des valeurs en float ou en double.

user=> (type (float 2))
java.lang.Float
user=> ( / (float 2) 3 )
0.6666667
user=> (type ( / (float 2) 3 ))
java.lang.Float

Même si les nombre sont par défaut traités en Double, la conversion n’est pas systématique si elle n’est pas nécessaire. Une conversion en double aurait donné un Double.

user=> ( / (double 2) 3)
0.6666666666666666
user=> ( type ( / (double 2) 3) )
java.lang.Double
Les opérations

Maintenant qu’est ce que je peux faire avec mes nombres ?

user=> (pos? -2) ;; is positive ?
false
user=> (min 3 1 5) ;; min
1
user=> (max 4 8 2) ;; max
8
user=> (int 1.2) ;; coercion en entier
1
user=> (inc 2) ;; incrémente de 1
3
user=> (dec 5) ;; décrémente de 1
4

La plupart des fonctions acceptent n valeurs. Qu’est ce qui se passe si j’ai plusieurs paramètres ?

Le résultat est à assez évident sur min ou max. Moins sur d’autres opérateurs. Le résultat est spécifié dans la documentation. Par exemple pour /, le premier terme est divisé par tous les suivants.

user=> (/ 12 2 3)
2

Pour <, la documentation indique « Returns non-nil if nums are in monotonically increasing order, otherwise false. » La suite doit être strictement croissante ? Mmm? testons …

user=> (< 1 2)
true
user=> (< 1 2 4)
true
user=> (< 1 5 4)
false

Encore plus de nombres

En général, lorsque l’on fait des statistiques on a de longs tableaux de nombres. Il nous faut donc des collections.

A quoi ça ressemble en Clojure ?

Les maps

Les maps sont représentées par {}. On en a vu un exemple plus haut. Les , sont facultatives mais améliorent la lisibilité. On peut utiliser tout type de valeurs comme clé (tout type de form dans la terminologie Clojure).

user=> (def scores { "Alice" 1, "Bob" 2} )
#'user/scores
user=> (type scores)
clojure.lang.PersistentArrayMap
user=> (get scores "Bob")
2
user=> (def scores { 15 "A", 22 "B"} )
#'user/scores
user=> (get scores 15)
"A"
Les listes

Les listes sont représentées entre (). C’est ce qu’on manipule depuis le début.

user=> user=> (def readings (1 2 3))
java.lang.ClassCastException: java.lang.Integer cannot be cast to clojure.lang.IFn (NO_SOURCE_FILE:398)

Diantre ! La magie ne marche pas sur les listes ?
Si, mais l’interpréteur évalue toutes les listes comme si c’est c’était des programmes.

Mais si on veut que la liste ne soit pas évaluées et restent des listes ??? Mmm J’ai déjà vu ça quelque part …

user=> (def readings '(1 2 3))
#'user/readings
user=> (type readings)
clojure.lang.PersistentList
user=> readings
(1 2 3)

La fonction range est un moyen rapide de générer une liste de nombres.

user=> (range -2 6)
(-2 -1 0 1 2 3 4 5)
Les vecteurs

Le vecteur est représenté entre [].

user=> (def persons [ "Alice" "Bob" "Charles"] )
#'user/persons
user=> (type persons)
clojure.lang.PersistentVector
user=> (persons 1)
"Bob"
user=> ([ "Alice", 2, :name ] 2)
:name

La liste et le vecteur  conservent l’ordre. La liste est à privilégier pour les accès séquentiels. Le vecteur permet un accès direct (par index) efficace.

Les sets

Il reste un dernier type de collection dans Clojure, le set qui maintient l’unicité. Il est représenté par #{}.

user=> #{1 3 1 4}
java.lang.IllegalArgumentException: Duplicate key: 1
user=> #{1 3 5 4}
#{1 3 4 5}
user=> (type #{1 3 5 4})
clojure.lang.PersistentHashSet
Quelques opérations sur les collections

La fonction doseq permet de parcourir l’itérateur de la collection.

user=>(def persons '( "Alice" "Bob" "Charles") )
user=> (doseq [p persons] (println p))
Alice
Bob
Charles
nil
user=> (count persons)
3

Toutes les callections sont modifiables.  conj permet d’ajouter des éléments. Attention l’effet de conj depend du type de collection.

user=> (conj '(1 2 3) 5)
(5 1 2 3)
user=> (conj [1 2 3] 5)
[1 2 3 5]
user=> (pop [1 2 3])
[1 2]

Un peu d’Incanter

Ah ! Quand même !

Incanter rajoute un type de donnée, la matrice. Une matrice ne peut contenir que des données numériques et n’a pas de nom de colonnes (même définition que dans R).

user=> (def A (matrix [[1 2 3] [4 5 6] [7 8 9]]))
java.lang.Exception: Unable to resolve symbol: matrix in this context (NO_SOURCE_FILE:450)

Whoops ! Jusqu’à présent tous les types étaient du Clojure. Ce type de données étant spécifique à Incanter, il faut charger la librairie en utilisant use. Et voilà ! matrix génère une matrice. Comme dans R, la matrice est un Vector replié.

user=> (use 'incanter.core)
nil
user=> (def A (matrix [[1 2 3] [4 5 6] [7 8 9]]))
#'user/A
user=> A
[1,0000 2,0000 3,0000
4,0000 5,0000 6,0000
7,0000 8,0000 9,0000]

Il existe différentes syntaxes pour créer des matrices à partir de vectors ou de listes. La syntaxe précédente concaténait des vecteurs lignes. Celle-ci découpe un vecteur en fonction du nombre de colonnes souhaitées.

user=> (def A2 (matrix [1 2 3 4 5 6 7 8] 2))
#'user/A2
user=> A2
[1,0000 2,0000
3,0000 4,0000
5,0000 6,0000
7,0000 8,0000]

Incanter ajoute également le type Dataset, qui est l’équivalent d’un tableau Excel ou d’une table en base de données. Le dataset comporte des noms de colonnes. Les colonnes peuvent être de tout type.

user=> (use 'incanter.datasets)
nil
user=> (dataset ["x1" "x2" "x3"]
[[1 2 3]
[4 5 6]
[7 8 9]])
#:incanter.core.Dataset{:column-names ["x1" "x2" "x3"], :rows ({"x3" 3, "x2" 2, "x1" 1} {"x3" 6, "x2" 5, "x1" 4} {"x3" 9, "x2" 8, "x1" 7})}

Ces structures de données sont généralement lues à partir de fichiers CSV ou d’une base de données.

user=> (use 'incanter.io)
nil
user=> (def data (read-dataset "datafile.csv" :header true))
nil

Et quelques statistiques pour finir et vous montrer qu’on n’a pas manipulé toutes ces collections pour rien aujourd’hui :

user=> (use ‘(incanter core stats) )
nil
user=> (mean ‘(20 10 30) )  ; médiane
20.0
user=> (sum ‘(20 10 30) )  ; somme
60.0

On reviendra en détail sur tout ça dans l’épisode 3.

2 réflexions sur “1, 2, 3 . . . Incanter

  1. Pingback: supercalifragilisticexpialidocious Incanter-3 « Claude au pays des 4J

  2. Pingback: abracadabra ! 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