Les manipulations d'un tableau comprennent :

  • la réorganisation du tableau (réindexation)
  • l'aggrégation de 2 tableaux ou plus
  • le découpage d'un tableau en 2 ou plus

Avant de regarder ces points, regardons comment Numpy présente les dimensions d'un tableau multidimensionnel avec la notion d'axes.

In [1]:
import numpy as np

# An array of marks of 3 exams for 4 students in two subjects 
# (therefore 6 marks per students or 12 per subjects)

#                    stud.1     stud.2     stud.3     stud.4
marks = np.array([[[7,13,11],  [7,7,13],  [5,9,11],  [7,17,15]],    # subject 1
                   [[8,12,14], [8,12,12], [8,12,10], [12,16,12]]])  # subject 2
marks.shape
Out[1]:
(2, 4, 3)

Les axes¶

Un tableau a des axes qui correspondent aux axes d'un repère dans l'espace. L'ordre des axes est celui de l'inclusion des crochets. En 2D un tableau de tableau est un tableau de lignes avec chaque ligne qui est un tableau 1D de valeurs. L'ordre est donc lignes puis colonnes (contrairement à l'axes $(x,y)$ dans l'espace). En 3D l'ordre est ligne, colonne, profondeur si on désire avoir une image, sinon c'est 0, 1 et 2.

De très nombreuses opérations sur les tableaux se font suivant un des axes du tableau aussi il est important de comprendre ce que sont les axes.

Regardons l'exemple des notes ci-dessus. Les axes sont

  1. les matières
  2. les étudiants
  3. les examens

Faire la moyenne des valeurs suivant l'axe 1 revient à prendre les données suivant l'axe 1 et effectuer les calculs dessus, donc ici en sortir une moyenne.

In [2]:
marks.mean(axis=1)   # give means for each exam in each subject
Out[2]:
array([[ 6.5, 11.5, 12.5],
       [ 9. , 13. , 12. ]])

Un autre facon de voir les axes est de les considérer comme des axes de projection. Si je projette un objet 3D suivant l'axe des $y$, le résultat est un objet 2D en $(x,z)$. On a ainsi une réduction de dimension.

Si je somme sur l'axe 0 un tableau de dimension (2,4,3) comme l'est notre tableau de notes, cela veut dire que je perds la dimension 0 et donc la dimension du résultat est (4,3).

In [3]:
marks.mean(axis=0).shape  # mean along axis 0 (subjects) therefore this axis disapears
Out[3]:
(4, 3)

Quelques fonctions qui supportent les axes¶

Toutes les fonctions qui s'appliquent à un ensemble de valeur pour produire un résultat doivent pouvoir utiliser de concept d'axe (je ne les ai pas toutes vérifiées mais n'hésitez pas à m'indiquer un contre-exemple). On a les fonctions mathématiques suivantes :

  • arithmétiques : sum, prod, cumsum, cumprod
  • statistiques : min, max, argmin, argmax, mean (moyenne), average (moyenne pondérée), std (écart type), var, median, percentile, quantile
  • autres : gradiant, diff, fft

De plus il est possible de trier les valeurs d'un tableau suivant l'axe de son choix avec sort. Par contre on ne peut les mélanger, avec shuffle, que suivant l'axe 0.

Appliquer une fonction suivant un axe¶

La fonction apply_along_axis permet d'appliquer une fonction 1D de son choix à un tableau suivant un axe. C'est l'axe qui va disparaître dans le résultat :

In [4]:
def diff_min_max(a):
    print('->', a, a.max() - a.min())
    return a.max() - a.min()

np.apply_along_axis(diff_min_max, axis=-1, arr=marks)   # -1 is the last axis, marks in our case
-> [ 7 13 11] 6
-> [ 7  7 13] 6
-> [ 5  9 11] 6
-> [ 7 17 15] 10
-> [ 8 12 14] 6
-> [ 8 12 12] 4
-> [ 8 12 10] 4
-> [12 16 12] 4
Out[4]:
array([[ 6,  6,  6, 10],
       [ 6,  4,  4,  4]])

Question : c'est l'écart entre les notes de quoi ?

Appliquer une fonction suivant plusieurs axes¶

Certaines opérations peuvent prendre une liste d'axes et non un seul axe.

In [5]:
print('a.max \n', marks.max(axis=(1,2)), '\n') 
print('a.max keepdim \n', marks.max(axis=(1,2), keepdims=True), '\n') 
a.max 
 [17 16] 

a.max keepdim 
 [[[17]]

 [[16]]] 

Question : à quoi correspondent les 2 valeurs sorties ?

Il est également peut utiliser la fonction apply_over_axes pour lui indiquer quelle fonction doit être appliquée suivant les axes donnés.

Attention la fonction donnée en argument recevra l'ensemble du tableau et l'axe sur lequel elle doit travailler, les axes étant donnés les uns après les autres et le tableau étant modifié à chaque étape.

In [6]:
def mymax(array, axis):
    print('Apply over axis', axis)
    print(array, '\n')
    return array.max(axis)

np.apply_over_axes(mymax, marks, axes=(1,2))
Apply over axis 1
[[[ 7 13 11]
  [ 7  7 13]
  [ 5  9 11]
  [ 7 17 15]]

 [[ 8 12 14]
  [ 8 12 12]
  [ 8 12 10]
  [12 16 12]]] 

Apply over axis 2
[[[ 7 17 15]]

 [[12 16 14]]] 

Out[6]:
array([[[17]],

       [[16]]])

Réorganisation d'un tableau¶

On a déjà vu reshape pour changer la forme d'un tableau, flatten pour l'applatir en 1 dimension, regardons d'autres fonctions de manipulation des tableaux.

Réordonner les axes¶

moveaxis déplace un axe¶

Dans notre exemple de notes, les 3 axes sont les matières, les étudiants et les examens. La fonction moveaxis permet de déplacer un axe. Si ainsi je désire que les examens deviennent le premier axe afin d'en faire ressortir les notes, je déplace l'axe 2 à la position 0 et les autres axes glissent pour faire de la place, l'axe 0 devient l'axe 1 et l'axe 1 devient l'axe 2 :

In [7]:
print('marks.shape = ',marks.shape, '\n')
b = np.moveaxis(marks, 2, 0) 
print('b.shape = ', b.shape)
b
marks.shape =  (2, 4, 3) 

b.shape =  (3, 2, 4)
Out[7]:
array([[[ 7,  7,  5,  7],
        [ 8,  8,  8, 12]],

       [[13,  7,  9, 17],
        [12, 12, 12, 16]],

       [[11, 13, 11, 15],
        [14, 12, 10, 12]]])

Il est plus simple de voir ainsi que le premier examen a été difficile.

swapaxes échange 2 axes¶

Plutôt que d'insérer un axe à une nouvelle position et faire glisser les autres, on peut vouloir en échanger deux. Voici comme avoir les notes pour chaque matière et chaque examen :

In [8]:
marks.swapaxes(1,2)
Out[8]:
array([[[ 7,  7,  5,  7],
        [13,  7,  9, 17],
        [11, 13, 11, 15]],

       [[ 8,  8,  8, 12],
        [12, 12, 12, 16],
        [14, 12, 10, 12]]])

transpose pour tout faire¶

Enfin transpose permet de réordonner tous les axes comme on veut, ainsi : transpose((2,0,1)) met

  • l'axe 2 en place 0,
  • l'axe 0 en place 1
  • l'axe 1 en place 2.

Un apply over axis plus simple et plus rapide¶

Malheureusement la fonction apply_over_axis n'est pas optimisée, aussi dans certaints il peut être préférable de faire une boucle sur son tableau, ce qui veut dire mettre les axes qui vont rester au début et ceux sur lesquels on fait notre réduction à la fin :

In [9]:
print("Means per students", [m.mean() for m in marks.transpose((1,0,2))])

%timeit [m.mean() for m in marks.transpose((1,0,2))]
%timeit np.apply_over_axes(np.mean, marks, axes=(0,2))
Means per students [10.833333333333334, 9.833333333333334, 9.166666666666666, 13.166666666666666]
24 µs ± 2.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
29.6 µs ± 1.23 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Changer l'ordre des éléments d'un tableau¶

On peut inverser les valeurs d'un tableau suivant un axe avec flip ce qui peut aussi être fait en l'indiquant au niveau des indices. Ainsi np.flip(a, n) est équivalent à a[:,:,..,::-1,:,..,:] avec ::-1 en $n$-ième position.

In [10]:
a = np.arange(24).reshape([2,3,4])
np.flip(a,0)
Out[10]:
array([[[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]],

       [[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]]])

On peut faire glisser les valeurs suivant un axe avec roll en spécifiant de combien on les fait glisser :

In [11]:
np.roll(a, 2, axis=1)              # roll elements by 2 along axis 1
Out[11]:
array([[[ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [ 0,  1,  2,  3]],

       [[16, 17, 18, 19],
        [20, 21, 22, 23],
        [12, 13, 14, 15]]])

La transposée s'applique aussi quelque soit la dimension. Par défaut elle inverse l'ordre des axes mais on peut spécifier l'ordre voulu en sortie.

In [12]:
a.T
Out[12]:
array([[[ 0, 12],
        [ 4, 16],
        [ 8, 20]],

       [[ 1, 13],
        [ 5, 17],
        [ 9, 21]],

       [[ 2, 14],
        [ 6, 18],
        [10, 22]],

       [[ 3, 15],
        [ 7, 19],
        [11, 23]]])
In [13]:
np.transpose(a, (0,2,1))
Out[13]:
array([[[ 0,  4,  8],
        [ 1,  5,  9],
        [ 2,  6, 10],
        [ 3,  7, 11]],

       [[12, 16, 20],
        [13, 17, 21],
        [14, 18, 22],
        [15, 19, 23]]])

Agrégation¶

Concaténation¶

La fonction de base est concatenate en indiquant l'axe choisi pour la concaténation. C'est à mon avis la méthode la plus sûre et elle marche quelque soit la dimension.

Cela étant on peut utiliser pour des tableaux 2D ou 3D :

  • vstack ou row_stack pour la concaténation vertical
  • hstack ou column_stack pour la concaténation horizontal
  • dstack pour la concaténation en profondeur (deep).

Toutes ces fonctions prennent une liste de tableaux à concaténer comme argument. Bien sûr les tailles des tableaux doivent être compatibles.

In [14]:
a = np.zeros((2,3))
b = np.ones((2,3))

print(np.concatenate((a,b), axis=0), '\n')   # same than vstack
print(np.hstack((a,b)))                      # same than concatenate with axis=1
[[0. 0. 0.]
 [0. 0. 0.]
 [1. 1. 1.]
 [1. 1. 1.]] 

[[0. 0. 0. 1. 1. 1.]
 [0. 0. 0. 1. 1. 1.]]

Empilage¶

A la différence de la concaténation, l'empilage ajoute une dimension. Empiler est utile pour stocker un paquet de tableaux 2D, des images par exemple, dans un tableau 3D. On utilise la fonction stack.

In [15]:
c = np.stack((a,b))   #  c[0] is a
c
Out[15]:
array([[[0., 0., 0.],
        [0., 0., 0.]],

       [[1., 1., 1.],
        [1., 1., 1.]]])

Notez que stack a une option axis pour indiquer la direction dans laquelle on désire stocker les tableaux donnés.

Découpage¶

La fonction inverse de la concaténation est le découpage avec split qui demande comme arguments :

  • le tableau à découper
  • en combien de morceaux ou à quels indices
  • la direction (l'axe)

Pour retrouver nos deux tableaux qui ont généré le résultat de la cellule précédante on coupe en 2 suivant l'axe 0. On peut aussi couper suivant un autre axe.

In [16]:
e,f = np.split(c, 2, 1)  # splits in 2 along axis 1
print("split part 1\n", e, '\n')
print("split part 2\n", f)
split part 1
 [[[0. 0. 0.]]

 [[1. 1. 1.]]] 

split part 2
 [[[0. 0. 0.]]

 [[1. 1. 1.]]]

Il existe aussi hsplit, vsplit et dsplit pour découper suivant les axes 0, 1 et 2.

From Python to Numpy¶

Si vous désirez creuser et regarder de nombreux exemples, vous pouvez lire le livre de N. Rougier From Python to Numpy.

Pandas aussi¶

On retrouvera ces manipulations avec Pandas qui est le super tableur de Python. Il travaille aussi sur des structures en forme de tableau mais sans la contrainte que toutes les valeurs soient du même type.

{{ PreviousNext("np02 Filtres.ipynb", "np04 Xarray.ipynb")}}

In [ ]: