Les bases de Numpy


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.

22 thoughts on “Les bases de Numpy

  • Marien

    C’est dommage, à 4 mois près j’aurais pu utiliser cet article pour mon problème de Particle Swarm Optimization :(

    En fait le problème c’est que quand on ne connait pas bien on en vient vite à faire quelque chose de pas optimisé du tout et de très lent. Typiquement le coup des opérateurs qui s’appliquent à l’array complet, au début j’étais tenté de passer par une boucle for.

    Par contre il y a un truc que je n’ai pas réussi à résoudre, c’est pour limiter les valeurs d’un array en fonction de limites min et max. J’ai dû faire un truc comme (avec les tabs qui vont bien)

    def limit_array(array, bounds):
    for x in np.nditer(array, op_flags=['readwrite']):
    if x < bounds[0]:
    x[...] = bounds[0]
    elif x > bounds[1]:
    x[...] = bounds[1]

    return array

    Du coup je serais vachement intéressé si vous avez une meilleure solution ! :)

  • chabotsi

    Tu peux simplement faire :

    x[x  bound[1]] = bound[1]

    aussi, ce qui est sympa, quand on travaille directement dans ipython, c’est de faire :

    import pylab as p

    et comme ça, on a accès à tout numpy et matplotlib en faisant `p.mafonction()`, par exemple :

    »» import pylab as p
    »» x = p.linspace(0, 2*p.pi, 100)
    »» y = p.sin(x)
    »» p.plot(x, y, label="mon beau sinus")
    »» p.show()

    PS: Il tue le trombone MSWord quand on écrit un commentaire ;)

  • MatthL

    Faire des boucles for pour parcourir un tableau numpy, c’est l’erreur que tout le monde fait au début, surtout pour ceux venant du C++.

    Pour que les opérations numpy soit optimisées, il faut soit utilisé le slicing, soit les (très nombreuses) fonctions numpy à disposition.

    En ultime recours, on peut aussi utiliser cython qui s’interface facilement avec numpy, et alors on peut à nouveau faire des boucles for imbriquées.

  • Firuzzel

    Merci !

    Je bidouillais des scripts pour mes analyses mais là c’est l’orgasme matriciel.

    J’avais jamais trop pris le temps de m’y pencher sérieusement mais les maitres S&M ont frappé ! C’est à en redemander :)

  • Luigi

    Orthographe :

    fournit par Numpy => fourni par Numpy
    un différence majeure => une différence majeure
    – on en met => on ne met
    – vous lui appliquer => vous lui appliquez
    – diminue le nombre de ligne => diminue le nombre de lignes
    – 50 points répartis uniforméments => 50 points répartis uniformément
    – des poins => des points

  • Luigi

    Un gros avantage de arange sur range est la possibilité d’avoir un pas de type float:


    »» arange(0, 0.5, 0.1)
    array([0, 0.1, 0.2, 0.3, 0.4])

    Un problème de Numpy c’était aussi l’absence de support du 64 bits. Il fallait compiler à la main. Heureusement, depuis peu, il y a WinPython qui permet d’automatiser tout ça avec au choix un python 2.7 ou 3.3, en 32 ou 64 bits. C’est le créateur de python(x,y), dont on a déjà parlé sur S&M.

    Très bon article !

  • Josh

    A noter que pour une exprérience matlab friendly, il y a aussi spyder, avec ipython intégré et tout.

  • joshuafr

    Et pour s’amuser avec numpy en mode scientificos, y’a le grand scipy.
    Après, il existe THE module ultime qui déchire les fondements de maman avec les dataframes : pandas, l’indexation sur les séries temporelles est juste jouissif (rah, mon clavier est encore tout collant)

  • Sam Post author

    Si il y a des gens chaud pour un tuto scipy ou pandas, je suis tout ouï. Je ne maitrise ni l’une ni l’autre, et franchement, il y aurait bien besoin d’un peu de doc claire dessus.

  • Bash

    Une petite imprécision: Les array sont mutables: on ne peut pas ajouter d’élément, mais on peut changer la valeurs. Par exemple, le code suivant est tout à fait valable:

    import numpy as np
    a = np.arange(10)
    a[5] = 0
    # voire même
    a[:4] = 0 # change les valeurs d'index inférieures à 4 en 0
    a[a&lt;3] = -1 # change les valeurs inférieures à 3 en -1

    Enfin, pour jouer un peu avec numpy, le mieux est d’utiliser ipython avec l’option –pylab: il importe automatiquement les fonctions les plus utilisés, et quand on utilise matplotlib, il affiche et met automatiquement le graph à jour, sans utiliser pylab.show.

  • kontre

    @Marien: tu as la fonction clip pour ça: http://docs.scipy.org/doc/numpy/reference/generated/numpy.clip.html
    x = x.clip(bounds[0], bounds[1])

    Cet article est très bien, mais dites-vous bien qu’il effleure à peine la surface… Ce qui m’amène à ma remarque: la doc numpy est plutôt bien fichue (et bien mieux que matplotlib) mais il y a un paquet de fonctions, ce qui rend la lib difficile à aborder. Un point de départ pas mal est http://wiki.scipy.org/Tentative_NumPy_Tutorial (en anglais). C’est gros, mais ça présente juste la base. Dites vous que je ressens la même chose quand vous parlez de django ;)

    Il n’y a pas vraiment d’intérêt à un tuto sur scipy, c’est principalement un ensemble de fonction qui vient compléter numpy. Perso je m’en sers très très rarement, alors que import numpy as np (cete manière d’importer est aussi standard que le pep8) est de base dans mes scripts avec les imports de __future__.

    J’aurais plein de trucs à raconter, mais pour finir je dirais que sous windows les gens utilisent en général des distributions python scientifiques parce que c’est la merde à compiler à la main. Il existe principalement python(x,y) (la meilleure à mon sens mais que en 32 bits), WinPython (portable, vous pouvez l’installer sur une clé usb) et Anaconda.
    Sous Linux 99% des paquets sont dans les dépôts, le reste c’est pip.

    N’hésitez pas à poser des questions !

  • Max

    Dommage qu’on ait pas plus de contributeurs dans cette section super interressante.


    A ce propos je cherche un kador en matlab qui serait passé du côté obscur du python (numpy/opencv) pour porter un ptit bout de code (à vue de nez une centaine de lignes)

    à vot bon coeur msieux dames…

  • furankun

    Tout d’abord, merci pour l’article, pour les articles en fait. Etant un scientificos venant de Matlab de base j’avais attaqué Python directement via Numpy et Scipy, et vous m’avez montré la lumière avec le Python “simple”. Maintenant je pense avoir des bases plus solides pour pouvoir m’y remettre un peu plus facilement, le cas échéant (j’ai déjà réessayé et il y a quand même du boulot).
    Et donc Max, je ne prétends pas être un bourrin en Matlab ni en Python, mais je peux toujours jeter un oeil sur ton code si tu le souhaites. De toute façon si ça dépasse mes compétences je te le dirai très vite :D

    • Max

      @funrankun

      je te remercie grandement mais quelqu’un s’en est déjà occupé ;) . Le cher Kontre qui rôde souvent sur le blog ^^
      Si toutefois tu veux quand même jetter un oeil ne serait-ce que pour la beauté du sport demande moi ;)
      ça parle d’image processing, détection de la nettetée d’une image pour être précis.

  • kontre

    Je l’ai fait mais le résultat est faux ! J’ai corrigé un peu mais la correction est sur mon ordi du boulot, que je ne retrouverai que lundi…

  • furankun

    oui alors clairement c’est trop haut niveau pour moi… j’ose espérer que tu t’en es dépatouillé!
    Ceci mis à part je voulais apporter ce témoignage tout personnel: Numpy c’est de la bouse. Ou alors je n’ai pas compris. Mais franchement son intégration dans un bout de code m’a énervé toute la journée vendredi, pour une simple histoire de dimensions (de merde!)
    Si on déclare un array de deux fois trois éléments, ça fait un array de dimensions 2×3. normal.
    Si maintenant on déclare un array de UNE fois trois éléments, ça fait un… truc de dimensions 3x0. Et c’est là que les ennuis commencent.
    Les dimensions ne sont pas déclarées de manière homogène. Impossible d’itérer dessus le long de la deuxième dimension (puisqu’elle n’existe pas). Impossible d’append le long de cette dimension (idem). Bref, si jamais vous ne connaissez pas les dimensions de votre array dès le départ ou que vous devez le modifier en cours de route, vous êtes niqués.
    Une solution possible, pas ce que j’appelle le plus simple mais visiblement c’est le cas: ajouter une dimension après avoir examiné la tronche de votre array. Bonjour la fluidité.
    Exemple:

    # item = np.array de taille n lignes par 4 colonnes;
    # on veut récupérer la quatrième colonne 'depth' et 
    # l'ajouter à un buffer
     
    # get item depth and reshape it
    try:
      depth = item[:,3].reshape(len(item),1)
    except IndexError:
      depth = item[3]
     
    # give buffer a supplementary dimension if needed
    if len(buffer.shape) == 1:
      buffer= np.array([buffer]) # ce truc m'a gavé
     
    result = np.hstack((buffer, depth))
  • kontre

    Toi, tu as fait du Matlab où il y a implicitement un nombre infini de dimensions de taille 1 supplémentaires. En python ce n’est pas le cas.
    Si tu déclares un array de 3 éléments, ça a une dimension de longueur 3. Mais tu peux reshaper comme tu veux en rajoutant des dimensions de taille 1.
    Comment ça se fait que tu ne connaisses pas le nombre de dimensions dans ton code ? Il doit y avoir un truc pas clair avant. Ton souci vient du fait que si n est sensé valoir 1 il vaut 0

    Petites remarques sur ton code:
    .reshape(len(item),1) peut être remplacé par .reshape(-1,1) (la dimension avec -1 contient tout ce qui’ n’est pas rentré dans le reste)
    – dans le try depth aura une taille de (len(item),1) alors que dans le except depth n’aura pas de dimension, c’est un scalaire.
    – Pour rajouter une dimension, faire comme tu as fait fonctionne mais c’est moche. Tu peux soit faire buffer = buffer.reshape(1, -1), soit buffer = buffer[np.newaxis, :] (np.newaxis est en fait égal à None, c’est juste une écriture plus explicite)
    – Au final je pense que tu peux utiliser np.concatenate plutôt que np.hstack pour que ça concatène dans la bonne dimension. Ça devrait même pouvoir remplacer tout ton code.

    Numpy c’est pas de la bouse (même si y’a des défauts), il faut juste savoir l’utiliser. Si t’as pas peur de l’anglais, http://wiki.scipy.org/Tentative_NumPy_Tutorial couvre bien les bases.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.