Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Licence CC BY-NC-ND Thierry Parmentelat & Arnaud Legout Inria - UCA

Complément - niveau basique

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.ion()
<contextlib.ExitStack at 0x7fa1882cc560>

J’espère que vous êtes à présent convaincus qu’il est possible de faire énormément de choses avec numpy en faisant des opérations entre tableaux, et sans aller référencer un par un les éléments des tableaux, ni faire de boucle for.

Il est temps maintenant de voir que l’on peut aussi manipuler les tableaux numpy avec des index.

Indexation par des entiers et tuples

La façon la plus naturelle d’utiliser un tableau est habituellement à l’aide des indices. On peut aussi bien sûr accéder aux éléments d’un tableau numpy par des indices :

# une fonction qui crée un tableau
# tab[i, j] = i + 10 * j
def background(n):
    i = np.arange(n)
    j = i.reshape((n, 1))
    return i + 10 * j
a5 = background(5)
print(a5)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]

Avec un seul index on obtient naturellement une ligne :

a5[1]
array([10, 11, 12, 13, 14])
# que l'on peut à nouveau indexer
a5[1][2]
np.int64(12)
# ou plus simplement indexer par un tuple
a5[1, 2]
The history saving thread hit an unexpected error (OperationalError('database is locked')).History will not be written to the database.
np.int64(12)
# naturellement on peut affecter une case
# individuellement
a5[2][1] = 221
a5[3, 2] += 300
print(a5)
[[  0   1   2   3   4]
 [ 10  11  12  13  14]
 [ 20 221  22  23  24]
 [ 30  31 332  33  34]
 [ 40  41  42  43  44]]
# ou toute une ligne
a5[1] = np.arange(100, 105)
print(a5)
[[  0   1   2   3   4]
 [100 101 102 103 104]
 [ 20 221  22  23  24]
 [ 30  31 332  33  34]
 [ 40  41  42  43  44]]
# et on on peut aussi changer
# toute une ligne par broadcasting
a5[4] = 400
print(a5)
[[  0   1   2   3   4]
 [100 101 102 103 104]
 [ 20 221  22  23  24]
 [ 30  31 332  33  34]
 [400 400 400 400 400]]

Slicing

Grâce au slicing on peut aussi référencer une colonne :

a5 = background(5)
print(a5)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
a5[:, 3]
array([ 3, 13, 23, 33, 43])

C’est un tableau à une dimension, mais vous pouvez tout de même modifier la colonne par une affectation :

a5[:, 3] = range(300, 305)
print(a5)
[[  0   1   2 300   4]
 [ 10  11  12 301  14]
 [ 20  21  22 302  24]
 [ 30  31  32 303  34]
 [ 40  41  42 304  44]]

Ou, ici également bien sûr, par broadcasting :

# on affecte un scalaire à une colonne
a5[:, 2] = 200
print(a5)
[[  0   1 200 300   4]
 [ 10  11 200 301  14]
 [ 20  21 200 302  24]
 [ 30  31 200 303  34]
 [ 40  41 200 304  44]]
# ou on ajoute un scalaire à une colonne
a5[:, 4] += 400
print(a5)
[[  0   1 200 300 404]
 [ 10  11 200 301 414]
 [ 20  21 200 302 424]
 [ 30  31 200 303 434]
 [ 40  41 200 304 444]]

Les slices peuvent prendre une forme générale :

a8 = background(8)
print(a8)
[[ 0  1  2  3  4  5  6  7]
 [10 11 12 13 14 15 16 17]
 [20 21 22 23 24 25 26 27]
 [30 31 32 33 34 35 36 37]
 [40 41 42 43 44 45 46 47]
 [50 51 52 53 54 55 56 57]
 [60 61 62 63 64 65 66 67]
 [70 71 72 73 74 75 76 77]]
# toutes les lignes de rang 1, 4, 7
a8[1::3]
array([[10, 11, 12, 13, 14, 15, 16, 17], [40, 41, 42, 43, 44, 45, 46, 47], [70, 71, 72, 73, 74, 75, 76, 77]])
# toutes les colonnes de rang 1, 5, 9
a8[:, 1::4]
array([[ 1, 5], [11, 15], [21, 25], [31, 35], [41, 45], [51, 55], [61, 65], [71, 75]])
# et on peut bien sûr les modifier
a8[:, 1::4] = 0
print(a8)
[[ 0  0  2  3  4  0  6  7]
 [10  0 12 13 14  0 16 17]
 [20  0 22 23 24  0 26 27]
 [30  0 32 33 34  0 36 37]
 [40  0 42 43 44  0 46 47]
 [50  0 52 53 54  0 56 57]
 [60  0 62 63 64  0 66 67]
 [70  0 72 73 74  0 76 77]]

Du coup, le slicing peut servir à extraire des blocs :

# un bloc au hasard dans a8
print(a8[5:8, 2:5])
[[52 53 54]
 [62 63 64]
 [72 73 74]]

newaxis

On peut utiliser également le symbole spécial np.newaxis en conjonction avec un slice pour “décaler” les dimensions :

X = np.arange(1, 7)
print(X)
[1 2 3 4 5 6]
X.shape
(6,)
Y = X[:, np.newaxis]
print(Y)
[[1]
 [2]
 [3]
 [4]
 [5]
 [6]]
Y.shape
(6, 1)

Et ainsi de suite :

Z = Y[:, np.newaxis]
Z
array([[[1]], [[2]], [[3]], [[4]], [[5]], [[6]]])
Z.shape
(6, 1, 1)

De cette façon, par exemple, en combinant le slicing pour créer X et Y, et le broadcasting pour créer leur somme, je peux créer facilement la table de tous les tirages de 2 dés à 6 faces :

dice2 = X + Y
print(dice2)
[[ 2  3  4  5  6  7]
 [ 3  4  5  6  7  8]
 [ 4  5  6  7  8  9]
 [ 5  6  7  8  9 10]
 [ 6  7  8  9 10 11]
 [ 7  8  9 10 11 12]]

Ou tous les tirages à trois dés :

dice3 = X + Y + Z
print(dice3)
[[[ 3  4  5  6  7  8]
  [ 4  5  6  7  8  9]
  [ 5  6  7  8  9 10]
  [ 6  7  8  9 10 11]
  [ 7  8  9 10 11 12]
  [ 8  9 10 11 12 13]]

 [[ 4  5  6  7  8  9]
  [ 5  6  7  8  9 10]
  [ 6  7  8  9 10 11]
  [ 7  8  9 10 11 12]
  [ 8  9 10 11 12 13]
  [ 9 10 11 12 13 14]]

 [[ 5  6  7  8  9 10]
  [ 6  7  8  9 10 11]
  [ 7  8  9 10 11 12]
  [ 8  9 10 11 12 13]
  [ 9 10 11 12 13 14]
  [10 11 12 13 14 15]]

 [[ 6  7  8  9 10 11]
  [ 7  8  9 10 11 12]
  [ 8  9 10 11 12 13]
  [ 9 10 11 12 13 14]
  [10 11 12 13 14 15]
  [11 12 13 14 15 16]]

 [[ 7  8  9 10 11 12]
  [ 8  9 10 11 12 13]
  [ 9 10 11 12 13 14]
  [10 11 12 13 14 15]
  [11 12 13 14 15 16]
  [12 13 14 15 16 17]]

 [[ 8  9 10 11 12 13]
  [ 9 10 11 12 13 14]
  [10 11 12 13 14 15]
  [11 12 13 14 15 16]
  [12 13 14 15 16 17]
  [13 14 15 16 17 18]]]

J’en profite pour introduire un utilitaire qui n’a rien à voir, mais avec np.unique, vous pourriez calculer le nombre d’occurrences dans le tableau, et ainsi calculer les probabilités d’apparition de tous les nombres entre 3 et 18 :

np.unique(dice3, return_counts=True)
(array([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]), array([ 1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]))

Différences avec les listes

Avec l’indexation et le slicing, on peut créer des tableaux qui sont des vues sur des fragments d’un tableau ; on peut également déformer leur dimension grâce à newaxis ; on peut modifier ces fragments, en utilisant un scalaire, un tableau, ou une slice sur un autre tableau. Les possibilités sont infinies.

Il est cependant utile de souligner quelques différences entre les tableaux numpy et, les listes natives, pour ce qui concerne les indexations et le slicing.

On ne peut pas changer la taille d’un tableau avec le slicing

La taille d’un objet numpy est par définition constante ; cela signifie qu’on ne peut pas, par exemple, modifier sa taille totale avec du slicing ; c’est à mettre en contraste avec, si vous vous souvenez :

Listes
# on peut faire ceci
liste = [0, 1, 2]
liste[1:2] = [100, 102, 102]
liste
[0, 100, 102, 102, 2]
Tableaux
# on ne peut pas faire cela
array = np.array([0, 1, 2])
try:
    array[1:2] = np.array([100, 102, 102])
except Exception as e:
    print(f"OOPS, {type(e)}, {e}")
OOPS, <class 'ValueError'>, could not broadcast input array from shape (3,) into shape (1,)
On peut modifier un tableau en modifiant une slice

Une slice sur un objet numpy renvoie une vue sur un extrait du tableau, et en changeant la vue on change le tableau ; ici encore c’est à mettre en contraste avec ce qui se passe sur les listes :

Listes
# une slice d'une liste est une shallow copy
liste = [0, 1, 2]
liste[1:2]
[1]
# en modifiant la slice,
# on ne modifie pas la liste
liste[1:2][0] = 999999
liste
[0, 1, 2]
Tableaux
# une slice d'un tableau numpy est un extrait du tableau
array = np.array([0, 1, 2])
array[1:2]
array([1])
array[1:2][0] = 100
array
array([ 0, 100, 2])