PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Les bases de Numpy

mercredi 19 février 2014 à 20:18

Numpy est une lib destinée à la manipulation de grands ensembles de nombres et est très utilisée par la communauté scientifique.

Elle propose des types et des opérations beaucoup plus performants que ceux de la lib standard, et possède des raccourcis pour les traitements de masse.

Malheureusement, c’est aussi une lib complexe, et, comme souvent dans le monde de la science, les tutos pourraient être plus clairs.

Cet article ne prétend pas à une couverture exhaustive de Numpy, d’autant que je n’ai pas le niveau en maths pour faire une simple dérivée alors des opérations matricielles complexes…

Mais ça devrait permettre de démystifier le truc pour les gens qui regardent ça de loin comme si c’était un pingouin au Mali.

Commencez par installer la bestiole avec un pip install numpy. Faites-vous un café pendant que ça compile.

array sur image

A la base de Numpy, il y a la manipulation d’ensembles ordonnés de nombres. On peut faire les opérations voulues sur un type list ordinaire, mais ce serait lent, et ça prendrait pas mal de mémoire.

L’alternative est d’utiliser un type optimisé comme ndarray, fourni par Numpy.

Cela se manipule comme une tuple, avec une différence majeure : il ne peut contenir qu’un seul type de données. Donc on ne met que des int, ou que des str, que des bool, etc.

On peut construire un array à partir de n’importe quel itérable :

>>> from numpy import array
>>> array([1, 2, 3])
array([1, 2, 3])
>>> array(u"azerty")
array(u'azerty',
      dtype='<U6')
>>> array((1.0, 2.0, 3.0))
array([ 1.,  2.,  3.])

Mais souvent on utilisera une fonction générant un array automatiquement afin d’éviter de créer deux structures de données (la liste, puis l’array par exemple).

On peut utiliser arange, qui est l’équivalent de range, mais pour les arrays :

>>> from numpy import arange
>>> arange(1, 100, 2)
array([ 1,  3,  5,  7,  9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33,
       35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67,
       69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99])

Ainsi que des choses plus perfectionnées comme linspace, qui retourne un array de n nombre de valeurs uniformément réparties entre deux bornes:

>>> from numpy import linspace
>>> linspace(0, 100, 15) # 15 valeur entre 0 et 100
array([   0.        ,    7.14285714,   14.28571429,   21.42857143,
         28.57142857,   35.71428571,   42.85714286,   50.        ,
         57.14285714,   64.28571429,   71.42857143,   78.57142857,
         85.71428571,   92.85714286,  100.        ])

Comme les tuples, les arrays sont itérables, sliceables, indexables et de taille fixe :

>>> sistance = arange(10)
>>> for x in a: # iterable
...     print x
...
0
1
2
3
4
5
6
7
8
9
>>> sistance[2:4] # sliceable
array([2, 3])
>>> sistance[-1] # indexable
9
>>> sistance.append(11) # taille fixe
Traceback (most recent call last):
  File "<ipython-input-17-389b8ea2fe68>", line 1, in <module>
    sistance.append(11)
AttributeError: 'numpy.ndarray' object has no attribute 'append'

L’array représente donc une photographie figée de vos données, mais comme vous allez le voir, rapide et précise à manipuler.

Opérations groupées

La caractéristique marquante de l’array, c’est que si vous lui appliquez un opérateur mathématique, un nouvel array est retourné dont TOUTES les valeurs ont été modifiées.

Par exemple, si vous multipliez un array, un nouvel array est retourné avec toutes les valeurs multipliées :

>>> duku = array([1, 2, 3])
>>> au_milieu = duku * 2
>>> au_milieu
array([2, 4, 6])

En fait, numpy fait une boucle implicite – et performante – sur tout l’array pour chaque opération mathématique. Et ça devient intéressant quand on veut faire des opérations entre plusieurs arrays entre eux :

>>> duku
array([1, 2, 3])
>>> au_milieu
array([2, 4, 6])
>>> de_tram = duku + au_milieu
>>> de_tram
array([3, 6, 9])

Une autre dimension

Si on travaille sur une liste plate, l’array est pratique, mais on en reste là. Néanmoins sa grande force est sa capacité à travailler sur plusieurs dimensions, et donc modifier tout aussi facilement des arrays d’arrays d’arrays d’arrays (arrête !) :

>>> thorique = array([ [1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> thorique
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> thorique ** 3
array([[  1,   8,  27],
       [ 64, 125, 216],
       [343, 512, 729]])

L’opération a été appliquée à tous les éléments sans faire de boucle, et en suivant l’imbrication de la structure de données récursivement.

Mais la partie la plus funky, c’est que le slicing AUSSI, peut se faire sur plusieurs dimensions.

Vous connaissez le slicing à une dimension :

>>> import random
>>> ticence = array([[random.randint(0, 100) for x in range(5)] for x in range(5)])
>>> ticence
array([[77, 44, 93, 65,  3],
       [ 8, 64, 36, 80, 77],
       [69, 24, 57, 18, 99],
       [60, 33, 63, 71, 99],
       [33, 60, 98, 85, 70]])
>>> ticence[1:4] # récupération des élément de 1 (inclus) à 4 (exclus)
array([[ 8, 64, 36, 80, 77],
       [69, 24, 57, 18, 99],
       [60, 33, 63, 71, 99]])

Mais avec un array numpy, on peut utiliser une virgule après le premier slicing, et mettre un nouveau slicing qui va travailler sur la dimension suivante.

Par exemple, ici j’applique le slice 1:4 sur la première dimension (je diminue le nombre de lignes) et ensuite j’applique le slicing 0:3 sur la seconde dimension (je diminue le nombre d’éléments de chaque lignes restantes, donc des colonnes).

>>> ticence[1:4, 0:3]
array([[ 8, 64, 36],
       [69, 24, 57],
       [60, 33, 63]])

Ca marche comme ça :

array[slicing_sur_dimension1, slicing_sur_dimension2, slicing_sur_dimension3, etc]

Si on commence à avoir beaucoup de dimensions et qu’on ne veut toucher que la dernière dimension, on peut utiliser Ellipsis.

>>> d2 = array([[random.randint(0, 100) for x in range(5)] for x in range(5)])
>>> d3 = array([d2.copy() for x in range(5)])
>>> d4 = array([d3.copy()  for x in range(5)])
>>> d5 = array([d4.copy()  for x in range(5)])
>>> d6 = array([d5.copy()  for x in range(5)])
>>> d6[1:3,...,-3:-1]
          [... plein de trucs ...]
          [[ 9, 86],
           [16, 40],
           [63, 26],
           [51,  5],
           [ 3, 46]],
 
          [[ 9, 86],
           [16, 40],
           [63, 26],
           [51,  5],
           [ 3, 46]]]]]])

La dernière ligne, on prend un tableau à 6 dimensions, on applique un slicing 1:3 sur la première dimension, et un slicing -3:-1 sur la dernière dimension.

Il est vrai que je ne m’en sers pas souvent.

Ok, je ne m’en suis jamais servi de toute ma vie. A part pour ce tuto. Mais c’est super classe non ?

Attention, cela ne marche que si toutes les dimensions ont le même nombre d’éléments. Cela se voit facilement en cas d’erreur car si le nombre d’éléments n’est pas bon, numpy va afficher votre array en ligne et pas sous forme de tabulaire :

>>> array([[1, 2, 5, 7, 9, 7, 8],[1,9]])
array([[1, 2, 5, 7, 9, 7, 8], [1, 9]], dtype=object)

Vous voyez en plus qu’il précise ici dtype=object, alors qu’il ne l’a pas fait plus haut.

Matplotlib pour afficher tout ça

En théorie, la lib matplotlib n’a rien à voir avec numpy. En pratique les utilisateurs de numpy utilisent très souvent numpy + matplotlib + ipython pour avoir une équivalent de matlab en Python.

Matplotlib est une lib qui permet de dessiner des graphes, mais qui a la particularité d’être orienté interaction. C’est à dire qu’elle est plus destinée à fabriquer votre graphe à la main, en bidouillant vos données, et possède dont des facilités pour cela.

D’abord on pip install matplotlib, et on prie pour que ça marche car sur certains OS ça plante méchamment.

Et ensuite dans son shell, on peut créer un petit graphe facilement sans trop se soucier des réglages, ceux par défaut étant pas mal :

from pylab import plot, xlabel, ylabel, title, legend
from numpy import sin, pi, linspace
 
# On active le mode interactif.
# Cela permet de voir notre graph
# en popup et de le modifier
# en temps réel.
ion()
 
# Utilisation de mes vagues connaissances
# de trigo pour pondre une sinusite...
# Heu, une sinusoide.
 
# Un array de 50 points répartis uniformément
# entre 0 et 2pi. Ca va nous servir de
# première coordonnée pour nos points.
x = linspace(0, 2 * pi)
 
# La fonction sin() de numpy va
# faire un nouvel array avec le sinus
# des points de l'array précédent.
# Ca nous fait notre deuxième coordonnée.
y = sin(x)
 
# On dessine la courbe, et on lui donne un pti nom
plot(x, y, label=u"Moi")
# Si je me suis pas trop planté ça devrait osciller
# entre 1 et -1
 
# On labellise les abscisses et les ordonnées
# car des données sans une échelle claire ne
# servent à rien.
ylabel(u"Self esteem (sur l'echelle de Richter)")
xlabel(u'Temps passé sur Dota (en joule par km)')
 
# On titre notre œuvre
title("Brace yourself, the graph is comming")
# On active la légende car le sujet est légendaire
legend()

Ce qui nous affiche :

Graphe de courbe sinusoidale

J'imagine toujours un petit train sur ce genre de courbe

Vous ne vous transformerez pas tout de suite en chercheur du CNRS après avoir lu ce tuto, mais j’espère qu’il vous aura donné un peu envie de faire mumuse avec Python pour manipuler vos données scientifiques.

flattr this!