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.

Tutoriel NumPy

NumPy constitue la base du calcul scientifique en Python. Elle s’articule principalement autour d’un objet tableau multidimensionnel performant et propose en plus de nombreuses fonctions utiles pour l’algèbre linéaire, la transformée de Fourier ou encore la génération de nombres aléatoires.

1. Introduction à NumPy

NumPy, abréviation de Numerical Python, est une bibliothèque open-source dédiée au langage Python. C’est un outil incontournable pour effectuer des calculs numériques et scientifiques. Elle permet de manipuler efficacement de grands tableaux et matrices multidimensionnels, et met à disposition un large éventail de fonctions mathématiques avancées pour traiter ces structures.

Pourquoi choisir NumPy ?

  • Rapidité : Les opérations réalisées avec NumPy sont codées en C, ce qui les rend nettement plus rapides que les structures natives de Python.

  • Richesse fonctionnelle : NumPy propose de nombreuses fonctions pour l’algèbre linéaire, la transformée de Fourier, la génération aléatoire, et bien d’autres.

  • Simplicité d’utilisation : Sa syntaxe compacte et expressive facilite l’écriture du code scientifique.

  • Compatibilité : NumPy constitue le socle de nombreuses autres bibliothèques scientifiques populaires (telles que SciPy, Pandas, Matplotlib ou Scikit-learn).

Pour commencer, importons le module numpy — il est d’usage de l’importer sous l’alias np :

import numpy as np

2. Installation

Sur Google Colab, NumPy est déjà disponible par défaut ; aucune installation supplémentaire n’est donc nécessaire.

Si NumPy n’est pas encore installé sur votre système, vous pouvez l’ajouter facilement avec pip :

pip install numpy

Pour les utilisateurs d’Anaconda, NumPy est généralement déjà présent, mais il peut aussi être installé ou mis à jour avec :

conda install numpy

3. Création de Tableaux (Arrays)

À partir de listes Python avec np.array

Bien entendu, il est possible de créer un ndarray à partir d’une liste Python classique en utilisant simplement la fonction array :

mon_tableau = np.array([[1, 2, 3, 4], [10, 20, 30, 40]])
print(mon_tableau)
[[ 1  2  3  4]
 [10 20 30 40]]

np.zeros

La fonction zeros permet de générer un tableau rempli de zéros, selon la taille souhaitée :

zeros_1d = np.zeros(5)
print(zeros_1d)
[0. 0. 0. 0. 0.]

Créer un tableau à deux dimensions (une matrice) est tout aussi simple : il suffit d’indiquer un tuple précisant le nombre de lignes et de colonnes. Par exemple, voici une matrice 3x4 :

zeros_2d = np.zeros((3, 4))
print(zeros_2d)
[[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]

Un peu de vocabulaire

  • Dans NumPy, chaque dimension d’un tableau s’appelle un axe (axis).

  • Le nombre total d’axes est appelé le rang (rank).

    • Par exemple, une matrice 3x4 est de rang 2 (c’est un tableau à deux dimensions).

    • Le premier axe compte 3 éléments, le second en a 4.

  • La forme (shape) d’un tableau désigne la liste des longueurs de ses axes.

    • Ainsi, la matrice précédente a pour forme (3, 4).

    • Le rang correspond donc au nombre d’éléments dans la forme.

  • La taille (size) correspond au nombre total d’éléments du tableau, soit le produit des longueurs de tous les axes (par exemple, 3×4 = 12).

a = np.zeros((3, 4))
print("Tableau a :\n", a)
print("Forme de a :", a.shape)
print("Rang de a (ndim) :", a.ndim)
print("Taille de a :", a.size)
Tableau a :
 [[0. 0. 0. 0.]
 [0. 0. 0. 0.]
 [0. 0. 0. 0.]]
Forme de a : (3, 4)
Rang de a (ndim) : 2
Taille de a : 12

Tableaux N-dimensionnels

Il est aussi possible de créer un tableau de dimension et de rang quelconques. Par exemple, voici un tableau tridimensionnel (rang 3) de forme (2, 3, 4) :

zeros_3d = np.zeros((2, 3, 4))
zeros_3d
array([[[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]], [[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]])

Type de tableau

Les tableaux NumPy sont de type ndarray :

print(type(np.zeros((3, 4))))
<class 'numpy.ndarray'>

np.ones

NumPy propose aussi de nombreuses autres fonctions pour générer des ndarray. Par exemple, voici une matrice 3x4 entièrement composée de uns :

uns_2d = np.ones((3, 4))
print(uns_2d)
[[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]

np.full

Génère un tableau selon la forme indiquée, où chaque élément est initialisé à la valeur choisie. Par exemple, voici une matrice 3x4 remplie de π.

pi_matrice = np.full((3, 4), np.pi)
print(pi_matrice)
[[3.14159265 3.14159265 3.14159265 3.14159265]
 [3.14159265 3.14159265 3.14159265 3.14159265]
 [3.14159265 3.14159265 3.14159265 3.14159265]]

np.empty

Un tableau 2x3 non initialisé : son contenu est imprévisible, car il reflète simplement ce qui se trouve en mémoire au moment de sa création.

vide = np.empty((2, 3))
print(vide) # Le contenu peut varier
[[-0.80114362 -0.99913515  0.75390225]
 [-0.83907153  0.0044257   0.84385396]]

np.arange

Il est possible de créer un ndarray à l’aide de la fonction arange de NumPy, dont le fonctionnement rappelle celui de la fonction range en Python :

sequence1 = np.arange(1, 5)
print(sequence1)
[1 2 3 4]

Cette méthode fonctionne également avec des nombres à virgule flottante :

sequence_flottante = np.arange(1.0, 5.0)
print(sequence_flottante)
[1. 2. 3. 4.]

On peut également spécifier un pas personnalisé si besoin :

sequence_pas = np.arange(1, 5, 0.5)
print(sequence_pas)
[1.  1.5 2.  2.5 3.  3.5 4.  4.5]

Toutefois, avec des valeurs flottantes, le nombre d’éléments obtenus dans le tableau peut parfois être imprévisible. Par exemple :

print(np.arange(0, 5/3, 1/3)) # en fonction des erreurs de virgule flottante, la valeur max est 4/3 ou 5/3.
print(np.arange(0, 5/3, 0.333333333))
print(np.arange(0, 5/3, 0.333333334))
[0.         0.33333333 0.66666667 1.         1.33333333 1.66666667]
[0.         0.33333333 0.66666667 1.         1.33333333 1.66666667]
[0.         0.33333333 0.66666667 1.         1.33333334]

np.linspace

C’est pourquoi, lorsqu’on manipule des flottants, il est souvent recommandé d’utiliser linspace plutôt que arange. La fonction linspace permet de générer un tableau contenant un nombre précis de valeurs, réparties de façon régulière entre deux bornes (la borne supérieure est incluse, contrairement à arange) :

points_lineaires = np.linspace(0, 5/3, 6)
print(points_lineaires)
[0.         0.33333333 0.66666667 1.         1.33333333 1.66666667]

np.random.rand et np.random.randn

Le module random de NumPy propose plusieurs fonctions pour créer des ndarray contenant des valeurs aléatoires. Par exemple, voici une matrice 3x4 remplie de nombres flottants tirés au hasard entre 0 et 1 (distribution uniforme) :

aleatoire_uniforme = np.random.rand(3, 4)
print(aleatoire_uniforme)
[[0.28905675 0.141813   0.29090784 0.38532696]
 [0.60588094 0.25324726 0.98204758 0.97880268]
 [0.88914976 0.9674826  0.11050907 0.62498318]]

Voici un exemple de matrice 3x4 composée de nombres flottants générés selon une distribution normale standard (moyenne 0 et variance 1) :

aleatoire_normal = np.random.randn(3, 4)
print(aleatoire_normal)
[[-0.40323019 -1.05244044 -2.44211088  1.56561049]
 [-0.70107569  1.41718532  1.17608599 -1.52716331]
 [-0.03441619  0.43314182 -1.31830359  0.50051512]]

Pour vous donner une idée de l’apparence de ces distributions, utilisons Matplotlib :

import matplotlib.pyplot as plt

plt.hist(np.random.rand(100000), density=True, bins=100, histtype="step", color="blue", label="rand")
plt.hist(np.random.randn(100000), density=True, bins=100, histtype="step", color="red", label="randn")
plt.axis([-2.5, 2.5, 0, 1.1])
plt.legend(loc="upper left")
plt.title("Distributions Aléatoires")
plt.xlabel("Valeur")
plt.ylabel("Densité")
plt.show()
<Figure size 640x480 with 1 Axes>

np.fromfunction

Il est aussi possible d’initialiser un ndarray à partir d’une fonction personnalisée :

def ma_fonction(z, y, x):
    return x + 10 * y + 100 * z

tableau_fonction = np.fromfunction(ma_fonction, (3, 2, 10))
print(tableau_fonction)
[[[  0.   1.   2.   3.   4.   5.   6.   7.   8.   9.]
  [ 10.  11.  12.  13.  14.  15.  16.  17.  18.  19.]]

 [[100. 101. 102. 103. 104. 105. 106. 107. 108. 109.]
  [110. 111. 112. 113. 114. 115. 116. 117. 118. 119.]]

 [[200. 201. 202. 203. 204. 205. 206. 207. 208. 209.]
  [210. 211. 212. 213. 214. 215. 216. 217. 218. 219.]]]

NumPy commence par créer trois tableaux (un par dimension), chacun ayant la forme (3, 2, 10). Chaque tableau contient, pour son axe respectif, la valeur de la coordonnée correspondante. Par exemple, dans le tableau z, tous les éléments correspondent à leur position le long de l’axe z. Dans l’expression x + 10 * y + 100 * z, les variables x, y et z sont donc elles-mêmes des ndarray. L’essentiel à retenir est que la fonction ma_fonction n’est appelée qu’une seule fois pour générer tout le tableau, ce qui rend cette méthode très efficace.

Types de données (dtype)

Les tableaux NumPy sont aussi performants car tous leurs éléments partagent le même type (souvent numérique). Le type de données utilisé peut être vérifié grâce à l’attribut dtype :

c_entier = np.arange(1, 5)
print(c_entier.dtype, c_entier)

c_flottant = np.arange(1.0, 5.0)
print(c_flottant.dtype, c_flottant)
int64 [1 2 3 4]
float64 [1. 2. 3. 4.]

Il est possible de préciser explicitement le type de données souhaité lors de la création d’un tableau, en utilisant le paramètre dtype :

d_complexe = np.arange(1, 5, dtype=np.complex64)
print(d_complexe.dtype, d_complexe)
complex64 [1.+0.j 2.+0.j 3.+0.j 4.+0.j]
  • Entiers signés :

    • int8, int16, int32, int64

  • Entiers non signés :

    • uint8, uint16, uint32, uint64

  • Nombres à virgule flottante :

    • float16, float32, float64

  • Nombres complexes :

    • complex64, complex128

Pour la liste complète des types disponibles, référez-vous à la documentation officielle.

itemsize

L’attribut itemsize indique la taille, en octets, occupée par chaque élément du tableau :

e = np.arange(1, 5, dtype=np.complex64)
print(e.itemsize) # 8 octets pour complex64
8

Tampon de données (data)

En réalité, les données d’un tableau sont stockées en mémoire sous la forme d’un tampon d’octets linéaire (une seule dimension). Ce tampon est accessible via l’attribut data, bien que son utilisation soit rarement nécessaire.

f = np.array([[1, 2], [1000, 2000]], dtype=np.int32)
print(f.data)
<memory at 0x77ff923eaa80>

Il est possible que plusieurs objets ndarray partagent le même espace mémoire : ainsi, toute modification apportée à l’un se répercute automatiquement sur les autres.

4. Manipulation de la Forme des Tableaux

En place (modification de l’attribut shape)

Modifier la forme d’un ndarray se fait simplement en assignant une nouvelle valeur à son attribut shape, à condition que le nombre total d’éléments demeure inchangé.

g = np.arange(24)
print("Tableau g initial :\n", g)
print("Rang initial :", g.ndim)

g.shape = (6, 4)
print("\nTableau g après g.shape = (6, 4) :\n", g)
print("Nouveau rang :", g.ndim)

g.shape = (2, 3, 4)
print("\nTableau g après g.shape = (2, 3, 4) :\n", g)
print("Nouveau rang :", g.ndim)
Tableau g initial :
 [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23]
Rang initial : 1

Tableau g après g.shape = (6, 4) :
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]
 [20 21 22 23]]
Nouveau rang : 2

Tableau g après g.shape = (2, 3, 4) :
 [[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]
Nouveau rang : 3

reshape

La méthode reshape retourne un nouvel objet ndarray qui partage les mêmes données que l’original : toute modification de l’un impactera donc l’autre.

g = np.arange(24).reshape(2,3,4) # Pour repartir de g en 3D
g2 = g.reshape(4, 6)
print("Tableau g2 (4x6) :\n", g2)
print("Rang de g2 :", g2.ndim)

# Modifier un élément dans g2
g2[1, 2] = 999
print("\nTableau g2 modifié :\n", g2)
print("Tableau g original (aussi modifié) :\n", g) # L'élément correspondant dans g a été modifié.
Tableau g2 (4x6) :
 [[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]
 [18 19 20 21 22 23]]
Rang de g2 : 2

Tableau g2 modifié :
 [[  0   1   2   3   4   5]
 [  6   7 999   9  10  11]
 [ 12  13  14  15  16  17]
 [ 18  19  20  21  22  23]]
Tableau g original (aussi modifié) :
 [[[  0   1   2   3]
  [  4   5   6   7]
  [999   9  10  11]]

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

ravel

La fonction ravel permet d’obtenir un tableau unidimensionnel qui référence aussi les mêmes données que l’original :

g_aplati = g.ravel()
print(g_aplati)
[  0   1   2   3   4   5   6   7 999   9  10  11  12  13  14  15  16  17
  18  19  20  21  22  23]

5. Indexation et Découpage

Tableaux unidimensionnels

On peut accéder aux tableaux NumPy à une dimension à peu près de la même façon qu’avec des listes Python classiques :

a_idx = np.array([1, 5, 3, 19, 13, 7, 3])
print("a_idx[3] :", a_idx[3])
print("a_idx[2:5] :", a_idx[2:5])
print("a_idx[2:-1] :", a_idx[2:-1])
print("a_idx[:2] :", a_idx[:2])
print("a_idx[2::2] :", a_idx[2::2]) # De l'index 2 à la fin, par pas de 2
print("a_idx[::-1] :", a_idx[::-1]) # Tableau inversé
a_idx[3] : 19
a_idx[2:5] : [ 3 19 13]
a_idx[2:-1] : [ 3 19 13  7]
a_idx[:2] : [1 5]
a_idx[2::2] : [ 3 13  3]
a_idx[::-1] : [ 3  7 13 19  3  5  1]

Bien sûr, vous pouvez modifier les éléments :

a_idx[3] = 999
print("a_idx modifié (élément) :\n", a_idx)
a_idx modifié (élément) :
 [  1   5   3 999  13   7   3]

Vous pouvez également modifier une tranche (slice) d’un ndarray :

a_idx[2:5] = [997, 998, 999]
print("a_idx modifié (tranche) :\n", a_idx)
a_idx modifié (tranche) :
 [  1   5 997 998 999   7   3]

Différences avec les listes Python standard

À la différence des listes Python, si vous affectez une valeur unique à une portion (slice) d’un ndarray, celle-ci sera reproduite sur toute la tranche selon le principe du broadcasting :

a_idx[2:5] = -1
print("a_idx après assignation d'une valeur unique à une tranche :\n", a_idx)
a_idx après assignation d'une valeur unique à une tranche :
 [ 1  5 -1 -1 -1  7  3]

De plus, il n’est pas possible d’augmenter ou de diminuer la taille d’un ndarray de cette façon :

try:
    a_idx[2:5] = [1, 2, 3, 4, 5, 6]  # trop long
except ValueError as e:
    print(e)
could not broadcast input array from shape (6,) into shape (3,)

Vous ne pouvez pas non plus supprimer d’éléments :

try:
    del a_idx[2:5]
except ValueError as e:
    print(e)
cannot delete array elements

Et surtout, les slices d’un ndarray constituent en réalité des vues sur les mêmes données. Ainsi, toute modification apportée à une tranche se répercute automatiquement sur le tableau d’origine !

a_original = np.array([1, 5, -1, -1, -1, 7, 3])
tranche_a = a_original[2:6]
tranche_a[1] = 1000
print("a_original (modifié par la tranche) :\n", a_original)

a_original[3] = 2000 # Modifier l'original
print("tranche_a (aussi modifiée) :\n", tranche_a)
a_original (modifié par la tranche) :
 [   1    5   -1 1000   -1    7    3]
tranche_a (aussi modifiée) :
 [  -1 2000   -1    7]

Si vous voulez une copie des données, vous devez utiliser la méthode copy :

autre_tranche = a_original[2:6].copy()
autre_tranche[1] = 3000
print("a_original (non touché par la copie) :\n", a_original)

a_original[3] = 4000
print("autre_tranche (non affectée par la modification de l'original) :\n", autre_tranche)
a_original (non touché par la copie) :
 [   1    5   -1 2000   -1    7    3]
autre_tranche (non affectée par la modification de l'original) :
 [  -1 3000   -1    7]

Tableaux multidimensionnels

Pour les tableaux à plusieurs dimensions, on accède aux éléments en donnant un indice ou une tranche pour chaque axe, séparés par des virgules :

b_multi = np.arange(48).reshape(4, 12)
print("b_multi :\n", b_multi)
print("b_multi[1, 2] (ligne 1, col 2) :", b_multi[1, 2])
print("b_multi[1, :] (ligne 1, toutes les colonnes) :\n", b_multi[1, :])
print("b_multi[:, 1] (toutes les lignes, colonne 1) :\n", b_multi[:, 1])
b_multi :
 [[ 0  1  2  3  4  5  6  7  8  9 10 11]
 [12 13 14 15 16 17 18 19 20 21 22 23]
 [24 25 26 27 28 29 30 31 32 33 34 35]
 [36 37 38 39 40 41 42 43 44 45 46 47]]
b_multi[1, 2] (ligne 1, col 2) : 14
b_multi[1, :] (ligne 1, toutes les colonnes) :
 [12 13 14 15 16 17 18 19 20 21 22 23]
b_multi[:, 1] (toutes les lignes, colonne 1) :
 [ 1 13 25 37]

Attention : notez la différence subtile entre ces deux expressions :

print("b_multi[1, :].shape :", b_multi[1, :].shape) # (12,) -> 1D
print("b_multi[1:2, :].shape :", b_multi[1:2, :].shape) # (1, 12) -> 2D
b_multi[1, :].shape : (12,)
b_multi[1:2, :].shape : (1, 12)

La première syntaxe retourne la ligne 1 comme un tableau 1D de forme (12,), alors que la seconde fournit cette même ligne sous forme d’un tableau 2D de forme (1, 12).

Indexation avancée (Fancy indexing)

Il est aussi possible d’indiquer une liste d’indices spécifiques : c’est ce qu’on appelle l’indexation avancée (fancy indexing).

print("b_multi[(0, 2), 2:5] (lignes 0 et 2, colonnes 2 à 4) :\n", b_multi[(0, 2), 2:5])
print("b_multi[:, (-1, 2, -1)] (toutes lignes, colonnes -1, 2, -1) :\n", b_multi[:, (-1, 2, -1)])
b_multi[(0, 2), 2:5] (lignes 0 et 2, colonnes 2 à 4) :
 [[ 2  3  4]
 [26 27 28]]
b_multi[:, (-1, 2, -1)] (toutes lignes, colonnes -1, 2, -1) :
 [[11  2 11]
 [23 14 23]
 [35 26 35]
 [47 38 47]]

En fournissant plusieurs tableaux d’indices, on obtient un ndarray unidimensionnel regroupant les éléments situés aux coordonnées correspondantes.

print("b_multi[(-1, 2, -1, 2), (5, 9, 1, 9)] :\n", b_multi[(-1, 2, -1, 2), (5, 9, 1, 9)])
# renvoie un tableau 1D avec b_multi[-1, 5], b_multi[2, 9], b_multi[-1, 1] et b_multi[2, 9]
b_multi[(-1, 2, -1, 2), (5, 9, 1, 9)] :
 [41 33 37 33]

Dimensions supérieures

Tout cela s’applique également aux tableaux ayant plus de deux dimensions :

c_3d = b_multi.reshape(4, 2, 6)
print("c_3d :\n", c_3d)
print("c_3d[2, 1, 4] (matrice 2, ligne 1, col 4) :", c_3d[2, 1, 4])
print("c_3d[2, :, 3] (matrice 2, toutes lignes, col 3) :\n", c_3d[2, :, 3])
c_3d :
 [[[ 0  1  2  3  4  5]
  [ 6  7  8  9 10 11]]

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

 [[24 25 26 27 28 29]
  [30 31 32 33 34 35]]

 [[36 37 38 39 40 41]
  [42 43 44 45 46 47]]]
c_3d[2, 1, 4] (matrice 2, ligne 1, col 4) : 34
c_3d[2, :, 3] (matrice 2, toutes lignes, col 3) :
 [27 33]

Si vous omettez les coordonnées pour certains axes, alors tous les éléments de ces axes sont renvoyés :

print("c_3d[2, 1] (équivalent à c_3d[2, 1, :]) :\n", c_3d[2, 1])
c_3d[2, 1] (équivalent à c_3d[2, 1, :]) :
 [30 31 32 33 34 35]

Points de suspension (...)

On peut aussi utiliser les points de suspension (...) pour inclure l’ensemble des axes non explicitement mentionnés.

print("c_3d[2, ...] (équivalent à c_3d[2, :, :]) :\n", c_3d[2, ...])
print("c_3d[2, 1, ...] (équivalent à c_3d[2, 1, :]) :\n", c_3d[2, 1, ...])
print("c_3d[2, ..., 3] (équivalent à c_3d[2, :, 3]) :\n", c_3d[2, ..., 3])
print("c_3d[..., 3] (équivalent à c_3d[:, :, 3]) :\n", c_3d[..., 3])
c_3d[2, ...] (équivalent à c_3d[2, :, :]) :
 [[24 25 26 27 28 29]
 [30 31 32 33 34 35]]
c_3d[2, 1, ...] (équivalent à c_3d[2, 1, :]) :
 [30 31 32 33 34 35]
c_3d[2, ..., 3] (équivalent à c_3d[2, :, 3]) :
 [27 33]
c_3d[..., 3] (équivalent à c_3d[:, :, 3]) :
 [[ 3  9]
 [15 21]
 [27 33]
 [39 45]]

Indexation booléenne

On peut aussi passer un tableau de valeurs booléennes pour indiquer, sur un axe donné, quels éléments doivent être sélectionnés.

b_bool = np.arange(48).reshape(4, 12)
lignes_actives = np.array([True, False, True, False])
print("b_bool[lignes_actives, :] :\n", b_bool[lignes_actives, :]) # Lignes 0 et 2, toutes colonnes

colonnes_actives = np.array([False, True, False] * 4)
print("b_bool[:, colonnes_actives] :\n", b_bool[:, colonnes_actives]) # Toutes lignes, colonnes 1, 4, 7 et 10
b_bool[lignes_actives, :] :
 [[ 0  1  2  3  4  5  6  7  8  9 10 11]
 [24 25 26 27 28 29 30 31 32 33 34 35]]
b_bool[:, colonnes_actives] :
 [[ 1  4  7 10]
 [13 16 19 22]
 [25 28 31 34]
 [37 40 43 46]]

np.ix_

L’indexation booléenne n’est pas directement applicable à plusieurs axes en même temps, mais il est possible de contourner cette limitation grâce à la fonction ix_ :

print("b_bool[np.ix_(lignes_actives, colonnes_actives)] :\n", b_bool[np.ix_(lignes_actives, colonnes_actives)])
b_bool[np.ix_(lignes_actives, colonnes_actives)] :
 [[ 1  4  7 10]
 [25 28 31 34]]

Lorsque vous utilisez un tableau de booléens ayant la même forme que le ndarray, cela retourne un tableau 1D regroupant tous les éléments correspondant aux positions où la condition est True. Cette méthode est souvent employée avec des opérateurs de comparaison :

print("b_bool[b_bool % 3 == 1] :\n", b_bool[b_bool % 3 == 1])
b_bool[b_bool % 3 == 1] :
 [ 1  4  7 10 13 16 19 22 25 28 31 34 37 40 43 46]

6. Opérations sur les Tableaux

Les opérateurs arithmétiques classiques (+, -, *, /, //, **, etc.) sont utilisables avec les ndarray : ils effectuent les calculs élément par élément.

a_op = np.array([14, 23, 32, 41])
b_op = np.array([5,  4,  3,  2])
print("a_op + b_op  =", a_op + b_op)
print("a_op - b_op  =", a_op - b_op)
print("a_op * b_op  =", a_op * b_op) # Multiplication élément par élément
print("a_op / b_op  =", a_op / b_op)
print("a_op // b_op =", a_op // b_op) # Division entière
print("a_op % b_op  =", a_op % b_op)
print("a_op ** b_op =", a_op ** b_op) # Puissance
a_op + b_op  = [19 27 35 43]
a_op - b_op  = [ 9 19 29 39]
a_op * b_op  = [70 92 96 82]
a_op / b_op  = [ 2.8         5.75       10.66666667 20.5       ]
a_op // b_op = [ 2  5 10 20]
a_op % b_op  = [4 3 2 1]
a_op ** b_op = [537824 279841  32768   1681]

Attention, l’opérateur * correspond à une multiplication élément par élément, et non à une multiplication matricielle (qui sera présentée plus tard). Les tableaux doivent avoir des formes compatibles : sinon, NumPy appliquera les règles de diffusion (broadcasting).

Broadcasting (Diffusion)

En pratique, si NumPy attend des tableaux de même forme mais qu’ils diffèrent, il applique les règles de broadcasting :

Première règle Si les tableaux n’ont pas le même rang, on ajoute des dimensions de taille 1 au début du tableau le plus petit jusqu’à ce que leurs rangs soient égaux.

h = np.arange(5).reshape(1, 1, 5)
print("h (forme (1,1,5)) :\n", h)
# Ajoutons un tableau 1D de forme (5,) à ce tableau 3D.
resultat_h = h + [10, 20, 30, 40, 50]  # équivalent à : h + [[[10, 20, 30, 40, 50]]]
print("h + [10, 20, 30, 40, 50] :\n", resultat_h)
h (forme (1,1,5)) :
 [[[0 1 2 3 4]]]
h + [10, 20, 30, 40, 50] :
 [[[10 21 32 43 54]]]

Deuxième règle Les tableaux avec un 1 le long d’une dimension particulière agissent comme s’ils avaient la taille du tableau avec la plus grande forme le long de cette dimension. La valeur de l’élément du tableau est répétée le long de cette dimension.

k = np.arange(6).reshape(2, 3)
print("k (forme (2,3)) :\n", k)
# Ajoutons un tableau 2D de forme (2,1) à ce ndarray 2D de forme (2,3).
resultat_k = k + [[100], [200]]  # équivalent à : k + [[100, 100, 100], [200, 200, 200]]
print("k + [[100], [200]] :\n", resultat_k)
k (forme (2,3)) :
 [[0 1 2]
 [3 4 5]]
k + [[100], [200]] :
 [[100 101 102]
 [203 204 205]]

En combinant les règles 1 & 2, nous pouvons faire ceci :

print("k + [100, 200, 300] :\n", k + [100, 200, 300])
# après la règle 1: k + [[100, 200, 300]], et après la règle 2: k + [[100, 200, 300], [100, 200, 300]]
k + [100, 200, 300] :
 [[100 201 302]
 [103 204 305]]

Et aussi, très simplement :

print("k + 1000 :\n", k + 1000) # équivalent à : k + [[1000, 1000, 1000], [1000, 1000, 1000]]
k + 1000 :
 [[1000 1001 1002]
 [1003 1004 1005]]

Troisième règle Après les règles 1 & 2, les tailles de tous les tableaux doivent correspondre.

try:
    k + [33, 44] # (2,3) et (2,) -> erreur
except ValueError as e:
    print(e)
operands could not be broadcast together with shapes (2,3) (2,) 

Les règles de broadcasting sont utilisées dans de nombreuses opérations NumPy, pas seulement les opérations arithmétiques.

Upcasting (Conversion de type ascendante)

Lorsque l’on combine des tableaux de types (dtype) différents, NumPy effectue automatiquement une conversion vers le type le plus large permettant de représenter toutes les valeurs possibles, peu importe les valeurs réellement présentes.

k1 = np.arange(0, 5, dtype=np.uint8)
print(k1.dtype, k1)

k2 = k1 + np.array([5, 6, 7, 8, 9], dtype=np.int8)
print(k2.dtype, k2) # Le résultat est int16
uint8 [0 1 2 3 4]
int16 [ 5  7  9 11 13]

Notez que int16 est requis pour représenter toutes les valeurs possibles de int8 et uint8 (de -128 à 255), même si dans ce cas un uint8 aurait suffi.

k3 = k1 + 1.5
print(k3.dtype, k3) # Le résultat est float64
float64 [1.5 2.5 3.5 4.5 5.5]

Opérateurs conditionnels

Les opérateurs conditionnels s’appliquent également élément par élément :

m = np.array([20, -5, 30, 40])
print("m < [15, 16, 35, 36] :", m < [15, 16, 35, 36])
m < [15, 16, 35, 36] : [False  True  True False]

Et en utilisant le broadcasting :

print("m < 25 :", m < 25)  # équivalent à m < [25, 25, 25, 25]
m < 25 : [ True  True False False]

Ceci est très utile en conjonction avec l’indexation booléenne :

print("m[m < 25] :", m[m < 25])
m[m < 25] : [20 -5]

7. Fonctions Mathématiques et Statistiques

Un grand nombre de fonctions mathématiques et statistiques sont proposées pour travailler avec les ndarray.

Méthodes des ndarray

Certaines fonctions sont simplement des méthodes de ndarray, par exemple :

a_stats = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print("a_stats :\n", a_stats)
print("moyenne de a_stats =", a_stats.mean())
a_stats :
 [[-2.5  3.1  7. ]
 [10.  11.  12. ]]
moyenne de a_stats = 6.766666666666667

Notez que cela calcule la moyenne de tous les éléments du ndarray, quelle que soit sa forme. Voici quelques autres méthodes utiles de ndarray :

for func in (a_stats.min, a_stats.max, a_stats.sum, a_stats.prod, a_stats.std, a_stats.var):
    print(func.__name__, "=", func())
min = -2.5
max = 12.0
sum = 40.6
prod = -71610.0
std = 5.084835843520964
var = 25.855555555555554

Ces fonctions acceptent un argument optionnel axis qui vous permet de demander que l’opération soit effectuée sur les éléments le long de l’axe donné. Par exemple :

c_axes = np.arange(24).reshape(2, 3, 4)
print("c_axes :\n", c_axes)
print("c_axes.sum(axis=0) (somme sur les matrices) :\n", c_axes.sum(axis=0))
print("c_axes.sum(axis=1) (somme sur les lignes de chaque matrice) :\n", c_axes.sum(axis=1))
c_axes :
 [[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

 [[12 13 14 15]
  [16 17 18 19]
  [20 21 22 23]]]
c_axes.sum(axis=0) (somme sur les matrices) :
 [[12 14 16 18]
 [20 22 24 26]
 [28 30 32 34]]
c_axes.sum(axis=1) (somme sur les lignes de chaque matrice) :
 [[12 15 18 21]
 [48 51 54 57]]

Vous pouvez également sommer sur plusieurs axes :

print("c_axes.sum(axis=(0, 2)) (somme sur les matrices et les colonnes) :\n", c_axes.sum(axis=(0, 2)))
# 0+1+2+3 + 12+13+14+15,  4+5+6+7 + 16+17+18+19,  8+9+10+11 + 20+21+22+23
c_axes.sum(axis=(0, 2)) (somme sur les matrices et les colonnes) :
 [ 60  92 124]

Fonctions universelles (ufuncs)

NumPy propose aussi des fonctions rapides appliquées à chaque élément, appelées fonctions universelles (ufunc). Il s’agit de versions vectorisées de fonctions simples. Par exemple, la fonction square crée un nouvel ndarray dans lequel chaque valeur correspond au carré de l’élément original :

a_ufunc = np.array([[-2.5, 3.1, 7], [10, 11, 12]])
print("np.square(a_ufunc) :\n", np.square(a_ufunc))
np.square(a_ufunc) :
 [[  6.25   9.61  49.  ]
 [100.   121.   144.  ]]

Voici quelques autres ufuncs unaires utiles :

print("Original ndarray :\n", a_ufunc)
for func in (np.abs, np.sqrt, np.exp, np.log, np.sign, np.ceil, np.modf, np.isnan, np.cos):
    print("\n", func.__name__)
    with np.errstate(invalid='ignore'): # Pour ignorer les avertissements sqrt/log de nombres négatifs
        print(func(a_ufunc))
Original ndarray :
 [[-2.5  3.1  7. ]
 [10.  11.  12. ]]

 absolute
[[ 2.5  3.1  7. ]
 [10.  11.  12. ]]

 sqrt
[[       nan 1.76068169 2.64575131]
 [3.16227766 3.31662479 3.46410162]]

 exp
[[8.20849986e-02 2.21979513e+01 1.09663316e+03]
 [2.20264658e+04 5.98741417e+04 1.62754791e+05]]

 log
[[       nan 1.13140211 1.94591015]
 [2.30258509 2.39789527 2.48490665]]

 sign
[[-1.  1.  1.]
 [ 1.  1.  1.]]

 ceil
[[-2.  4.  7.]
 [10. 11. 12.]]

 modf
(array([[-0.5,  0.1,  0. ],
       [ 0. ,  0. ,  0. ]]), array([[-2.,  3.,  7.],
       [10., 11., 12.]]))

 isnan
[[False False False]
 [False False False]]

 cos
[[-0.80114362 -0.99913515  0.75390225]
 [-0.83907153  0.0044257   0.84385396]]

Les avertissements sont dus au fait que sqrt() et log() ne sont pas définis pour les nombres négatifs, c’est pourquoi il y a une valeur np.nan (Not a Number) dans la sortie.

Ufuncs binaires

Il existe également de nombreuses ufuncs binaires, qui s’appliquent élément par élément à deux ndarray. Les règles de broadcasting sont appliquées si les tableaux n’ont pas la même forme :

x_bin = np.array([1, -2, 3, 4])
y_bin = np.array([2, 8, -1, 7])
print("np.add(x_bin, y_bin) :", np.add(x_bin, y_bin))  # équivalent à x_bin + y_bin
print("np.greater(x_bin, y_bin) :", np.greater(x_bin, y_bin))  # équivalent à x_bin > y_bin
print("np.maximum(x_bin, y_bin) :", np.maximum(x_bin, y_bin))
print("np.copysign(x_bin, y_bin) :", np.copysign(x_bin, y_bin)) # Copie le signe de y_bin vers x_bin
np.add(x_bin, y_bin) : [ 3  6  2 11]
np.greater(x_bin, y_bin) : [False False  True False]
np.maximum(x_bin, y_bin) : [2 8 3 7]
np.copysign(x_bin, y_bin) : [ 1.  2. -3.  4.]

8. Itération sur les Tableaux

Parcourir un ndarray se fait de façon analogue à une liste Python classique. Pour les tableaux multidimensionnels, l’itération s’effectue selon le premier axe.

c_iter = np.arange(24).reshape(2, 3, 4)  # Un tableau 3D (composé de deux matrices 3x4)
print("c_iter :\n", c_iter)

print("\nItération sur le premier axe :")
for m_iter in c_iter:
    print("Élément (matrice) :\n", m_iter)

print("\nItération sur tous les éléments avec .flat :")
for i_iter in c_iter.flat:
    print("Élément :", i_iter, end=" ")
print()
c_iter :
 [[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

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

Itération sur le premier axe :
Élément (matrice) :
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
Élément (matrice) :
 [[12 13 14 15]
 [16 17 18 19]
 [20 21 22 23]]

Itération sur tous les éléments avec .flat :
Élément : 0 Élément : 1 Élément : 2 Élément : 3 Élément : 4 Élément : 5 Élément : 6 Élément : 7 Élément : 8 Élément : 9 Élément : 10 Élément : 11 Élément : 12 Élément : 13 Élément : 14 Élément : 15 Élément : 16 Élément : 17 Élément : 18 Élément : 19 Élément : 20 Élément : 21 Élément : 22 Élément : 23 

9. Empilement et Division de Tableaux

Il est souvent utile d’empiler différents tableaux. NumPy offre plusieurs fonctions pour cela. Commençons par créer quelques tableaux.

q1 = np.full((3, 4), 1.0)
q2 = np.full((4, 4), 2.0)
q3 = np.full((3, 4), 3.0)
print("q1:\n", q1)
print("q2:\n", q2)
print("q3:\n", q3)
q1:
 [[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]]
q2:
 [[2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]]
q3:
 [[3. 3. 3. 3.]
 [3. 3. 3. 3.]
 [3. 3. 3. 3.]]

np.vstack (empilement vertical)

Empilons-les verticalement en utilisant vstack :

q4 = np.vstack((q1, q2, q3)) # q2 a 4 lignes, q1 et q3 en ont 3.
# Pour que vstack fonctionne, toutes les matrices doivent avoir le même nombre de colonnes.
# q1 (3,4), q2 (4,4), q3 (3,4) -> OK
print("q4 (vstack de q1, q2, q3)) :\n", q4)
print("Forme de q4 :", q4.shape) # (3+4+3, 4) = (10, 4)
q4 (vstack de q1, q2, q3)) :
 [[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [3. 3. 3. 3.]
 [3. 3. 3. 3.]
 [3. 3. 3. 3.]]
Forme de q4 : (10, 4)

np.hstack (empilement horizontal)

Nous pouvons également empiler les tableaux horizontalement en utilisant hstack :

q5 = np.hstack((q1, q3)) # q1 (3,4), q3 (3,4) -> OK
print("q5 (hstack de q1, q3) :\n", q5)
print("Forme de q5 :", q5.shape) # (3, 4+4) = (3, 8)
q5 (hstack de q1, q3) :
 [[1. 1. 1. 1. 3. 3. 3. 3.]
 [1. 1. 1. 1. 3. 3. 3. 3.]
 [1. 1. 1. 1. 3. 3. 3. 3.]]
Forme de q5 : (3, 8)

C’est possible parce que q1 et q3 ont tous deux 3 lignes. Mais comme q2 a 4 lignes, il ne peut pas être empilé horizontalement avec q1 et q3 :

try:
    q_err = np.hstack((q1, q2, q3))
except ValueError as e:
    print(e)
all the input array dimensions except for the concatenation axis must match exactly, but along dimension 0, the array at index 0 has size 3 and the array at index 1 has size 4

np.concatenate

La fonction concatenate empile les tableaux le long de n’importe quel axe existant donné.

q7_axis0 = np.concatenate((q1, q2, q3), axis=0)  # Équivalent à vstack si les dimensions des colonnes correspondent
print("q7_axis0 (concatenate sur axis=0) :\n", q7_axis0)
print("Forme de q7_axis0 :", q7_axis0.shape)

q7_axis1 = np.concatenate((q1, q3), axis=1) # Équivalent à hstack si les dimensions des lignes correspondent
print("\nq7_axis1 (concatenate sur axis=1) :\n", q7_axis1)
print("Forme de q7_axis1 :", q7_axis1.shape)
q7_axis0 (concatenate sur axis=0) :
 [[1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [1. 1. 1. 1.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [3. 3. 3. 3.]
 [3. 3. 3. 3.]
 [3. 3. 3. 3.]]
Forme de q7_axis0 : (10, 4)

q7_axis1 (concatenate sur axis=1) :
 [[1. 1. 1. 1. 3. 3. 3. 3.]
 [1. 1. 1. 1. 3. 3. 3. 3.]
 [1. 1. 1. 1. 3. 3. 3. 3.]]
Forme de q7_axis1 : (3, 8)

np.stack

La fonction stack empile les tableaux le long d’un nouvel axe. Tous les tableaux doivent avoir la même forme.

q8 = np.stack((q1, q3))
print("q8 (stack de q1, q3) :\n", q8)
print("Forme de q8 :", q8.shape) # (2, 3, 4) -> un nouvel axe a été ajouté au début
q8 (stack de q1, q3) :
 [[[1. 1. 1. 1.]
  [1. 1. 1. 1.]
  [1. 1. 1. 1.]]

 [[3. 3. 3. 3.]
  [3. 3. 3. 3.]
  [3. 3. 3. 3.]]]
Forme de q8 : (2, 3, 4)

Division de Tableaux (split, vsplit, hsplit)

La division est l’opposé de l’empilement. Par exemple, utilisons la fonction vsplit pour diviser une matrice verticalement. Créons d’abord une matrice 6x4 :

r_split = np.arange(24).reshape(6, 4)
print("r_split :\n", r_split)
r_split :
 [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]
 [16 17 18 19]
 [20 21 22 23]]

Divisons-la maintenant en trois parties égales, verticalement :

r1_split, r2_split, r3_split = np.vsplit(r_split, 3)
print("r1_split :\n", r1_split)
print("r2_split :\n", r2_split)
print("r3_split :\n", r3_split)
r1_split :
 [[0 1 2 3]
 [4 5 6 7]]
r2_split :
 [[ 8  9 10 11]
 [12 13 14 15]]
r3_split :
 [[16 17 18 19]
 [20 21 22 23]]

Il existe également une fonction split qui divise un tableau le long de n’importe quel axe donné. Appeler vsplit est équivalent à appeler split avec axis=0. Il existe également une fonction hsplit, équivalente à appeler split avec axis=1 :

r4_split, r5_split = np.hsplit(r_split, 2)
print("r4_split :\n", r4_split)
print("r5_split :\n", r5_split)
r4_split :
 [[ 0  1]
 [ 4  5]
 [ 8  9]
 [12 13]
 [16 17]
 [20 21]]
r5_split :
 [[ 2  3]
 [ 6  7]
 [10 11]
 [14 15]
 [18 19]
 [22 23]]

10. Transposition de Tableaux

La méthode transpose crée une nouvelle vue sur les données d’un ndarray, avec les axes permutés dans l’ordre donné. Créons un tableau 3D :

t_trans = np.arange(24).reshape(4, 2, 3)
print("t_trans (forme (4,2,3)) :\n", t_trans)
t_trans (forme (4,2,3)) :
 [[[ 0  1  2]
  [ 3  4  5]]

 [[ 6  7  8]
  [ 9 10 11]]

 [[12 13 14]
  [15 16 17]]

 [[18 19 20]
  [21 22 23]]]

Créons maintenant un ndarray tel que les axes 0, 1, 2 (profondeur, hauteur, largeur) soient réorganisés en 1, 2, 0 (profondeur→largeur, hauteur→profondeur, largeur→hauteur) :

t1_trans = t_trans.transpose((1, 2, 0))
print("t1_trans (axes 1,2,0) :\n", t1_trans)
print("Forme de t1_trans :", t1_trans.shape) # (2, 3, 4)
t1_trans (axes 1,2,0) :
 [[[ 0  6 12 18]
  [ 1  7 13 19]
  [ 2  8 14 20]]

 [[ 3  9 15 21]
  [ 4 10 16 22]
  [ 5 11 17 23]]]
Forme de t1_trans : (2, 3, 4)

Par défaut, transpose inverse l’ordre des dimensions :

t2_trans = t_trans.transpose()  # équivalent à t.transpose((2, 1, 0))
print("t2_trans (transpose par défaut) :\n", t2_trans)
print("Forme de t2_trans :", t2_trans.shape) # (3, 2, 4)
t2_trans (transpose par défaut) :
 [[[ 0  6 12 18]
  [ 3  9 15 21]]

 [[ 1  7 13 19]
  [ 4 10 16 22]]

 [[ 2  8 14 20]
  [ 5 11 17 23]]]
Forme de t2_trans : (3, 2, 4)

L’attribut T est un raccourci pour transpose() (il inverse les axes) :

m_T = np.arange(10).reshape(2, 5)
print("m_T :\n", m_T)
print("m_T.T :\n", m_T.T)
m_T :
 [[0 1 2 3 4]
 [5 6 7 8 9]]
m_T.T :
 [[0 5]
 [1 6]
 [2 7]
 [3 8]
 [4 9]]

L’attribut T n’a pas d’effet sur les tableaux de rang 0 (vides) ou de rang 1. Pour transposer un tableau 1D, il faut d’abord le remodeler en matrice ligne (2D) :

m2_T = np.arange(5)
m2r_T = m2_T.reshape(1, 5)
print("m2r_T.T :\n", m2r_T.T)
m2r_T.T :
 [[0]
 [1]
 [2]
 [3]
 [4]]

NumPy met à disposition la fonction pratique swapaxes pour permuter deux axes d’un tableau. Par exemple, on peut créer une nouvelle vue de t_trans où la profondeur et la hauteur sont interverties :

t3_trans = t_trans.swapaxes(0, 1)  # équivalent à t_trans.transpose((1, 0, 2))
print("t3_trans (swapaxes 0,1) :\n", t3_trans)
print("Forme de t3_trans :", t3_trans.shape) # (2, 4, 3)
t3_trans (swapaxes 0,1) :
 [[[ 0  1  2]
  [ 6  7  8]
  [12 13 14]
  [18 19 20]]

 [[ 3  4  5]
  [ 9 10 11]
  [15 16 17]
  [21 22 23]]]
Forme de t3_trans : (2, 4, 3)

11. Algèbre Linéaire

Les tableaux 2D de NumPy peuvent être utilisés pour représenter efficacement des matrices en Python.

Multiplication de matrices (dot ou @)

Créons deux matrices et exécutons une multiplication matricielle en utilisant la méthode dot() ou l’opérateur @ (Python 3.5+).

n1_mat = np.arange(10).reshape(2, 5)
n2_mat = np.arange(15).reshape(5, 3)
print("n1_mat (2x5) :\n", n1_mat)
print("n2_mat (5x3) :\n", n2_mat)

produit_mat = n1_mat.dot(n2_mat)
# ou produit_mat = n1_mat @ n2_mat
print("n1_mat . n2_mat (produit matriciel) :\n", produit_mat)
print("Forme du produit :", produit_mat.shape) # (2x3)
n1_mat (2x5) :
 [[0 1 2 3 4]
 [5 6 7 8 9]]
n2_mat (5x3) :
 [[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]
 [12 13 14]]
n1_mat . n2_mat (produit matriciel) :
 [[ 90 100 110]
 [240 275 310]]
Forme du produit : (2, 3)

Remarque importante : comme déjà signalé, l’opération n1_mat * n2_mat ne réalise pas une multiplication matricielle, mais une multiplication élément par élément (également appelée produit d’Hadamard).

Inverse et pseudo-inverse de matrices

Le module numpy.linalg offre de nombreuses fonctions pour l’algèbre linéaire, notamment la fonction inv qui permet de calculer l’inverse d’une matrice carrée :

import numpy.linalg as linalg

m3_inv = np.array([[1, 2, 3], [5, 7, 11], [21, 29, 31]])
print("m3_inv :\n", m3_inv)

inverse_m3 = linalg.inv(m3_inv)
print("Inverse de m3_inv :\n", inverse_m3)
m3_inv :
 [[ 1  2  3]
 [ 5  7 11]
 [21 29 31]]
Inverse de m3_inv :
 [[-2.31818182  0.56818182  0.02272727]
 [ 1.72727273 -0.72727273  0.09090909]
 [-0.04545455  0.29545455 -0.06818182]]

Vous pouvez également calculer la pseudo-inverse en utilisant pinv (utile pour les matrices non carrées ou singulières) :

pseudo_inverse_m3 = linalg.pinv(m3_inv)
print("Pseudo-inverse de m3_inv :\n", pseudo_inverse_m3)
Pseudo-inverse de m3_inv :
 [[-2.31818182  0.56818182  0.02272727]
 [ 1.72727273 -0.72727273  0.09090909]
 [-0.04545455  0.29545455 -0.06818182]]

Matrice identité

Le produit d’une matrice par son inverse renvoie la matrice identité (avec de petites erreurs de virgule flottante) :

identite_attendue = m3_inv.dot(linalg.inv(m3_inv))
print("m3_inv . inv(m3_inv) :\n", identite_attendue)
m3_inv . inv(m3_inv) :
 [[ 1.00000000e+00 -1.66533454e-16  0.00000000e+00]
 [ 6.31439345e-16  1.00000000e+00 -1.38777878e-16]
 [ 5.21110932e-15 -2.38697950e-15  1.00000000e+00]]

Vous pouvez créer une matrice identité de taille NxN en appelant la fonction eye(N) :

identite_3x3 = np.eye(3)
print("Matrice identité 3x3 :\n", identite_3x3)
Matrice identité 3x3 :
 [[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

Décomposition QR

La fonction qr calcule la décomposition QR d’une matrice :

q_qr, r_qr = linalg.qr(m3_inv)
print("Matrice Q de QR :\n", q_qr)
print("Matrice R de QR :\n", r_qr)
print("Q . R (devrait être égal à m3_inv) :\n", q_qr.dot(r_qr))
Matrice Q de QR :
 [[-0.04627448  0.98786672  0.14824986]
 [-0.23137241  0.13377362 -0.96362411]
 [-0.97176411 -0.07889213  0.22237479]]
Matrice R de QR :
 [[-21.61018278 -29.89331494 -32.80860727]
 [  0.           0.62427688   1.9894538 ]
 [  0.           0.          -3.26149699]]
Q . R (devrait être égal à m3_inv) :
 [[ 1.  2.  3.]
 [ 5.  7. 11.]
 [21. 29. 31.]]

Déterminant

La fonction det calcule le déterminant d’une matrice :

determinant_m3 = linalg.det(m3_inv)
print("Déterminant de m3_inv :", determinant_m3)
Déterminant de m3_inv : 43.99999999999997

Valeurs propres et vecteurs propres

La fonction eig calcule les valeurs propres et vecteurs propres d’une matrice carrée :

valeurs_propres, vecteurs_propres = linalg.eig(m3_inv)
print("Valeurs propres (λ) :\n", valeurs_propres)
print("Vecteurs propres (v) :\n", vecteurs_propres)
# Vérification : m3_inv . v - λ*v devrait être proche de 0
print("m3_inv . v - λ*v :\n", m3_inv.dot(vecteurs_propres) - valeurs_propres * vecteurs_propres)
Valeurs propres (λ) :
 [42.26600592 -0.35798416 -2.90802176]
Vecteurs propres (v) :
 [[-0.08381182 -0.76283526 -0.18913107]
 [-0.3075286   0.64133975 -0.6853186 ]
 [-0.94784057 -0.08225377  0.70325518]]
m3_inv . v - λ*v :
 [[ 6.21724894e-15  1.66533454e-15 -3.10862447e-15]
 [ 3.55271368e-15  5.30131494e-15 -5.32907052e-15]
 [ 3.55271368e-14  5.38458167e-15 -9.76996262e-15]]

Décomposition en valeurs singulières (SVD)

La fonction svd prend une matrice et renvoie sa décomposition en valeurs singulières : M=UΣVTM = U \Sigma V^T

m4_svd = np.array([[1, 0, 0, 0, 2], [0, 0, 3, 0, 0], [0, 0, 0, 0, 0], [0, 2, 0, 0, 0]])
print("m4_svd :\n", m4_svd)

U_svd, S_diag_svd, Vt_svd = linalg.svd(m4_svd) # Vt est V transposé
print("U de SVD :\n", U_svd)
print("Valeurs singulières (diagonale de Σ) :\n", S_diag_svd)
print("Vt (V transposé) de SVD :\n", Vt_svd)
m4_svd :
 [[1 0 0 0 2]
 [0 0 3 0 0]
 [0 0 0 0 0]
 [0 2 0 0 0]]
U de SVD :
 [[ 0.  1.  0.  0.]
 [ 1.  0.  0.  0.]
 [ 0.  0.  0.  1.]
 [ 0.  0. -1.  0.]]
Valeurs singulières (diagonale de Σ) :
 [3.         2.23606798 2.         0.        ]
Vt (V transposé) de SVD :
 [[-0.          0.          1.         -0.          0.        ]
 [ 0.4472136   0.          0.          0.          0.89442719]
 [ 0.         -1.          0.          0.          0.        ]
 [ 0.          0.          0.          1.          0.        ]
 [-0.89442719  0.          0.          0.          0.4472136 ]]

La fonction svd renvoie juste les valeurs de la diagonale de Σ, mais nous voulons la matrice Σ complète, alors créons-la :

Sigma_svd = np.zeros(m4_svd.shape)
# Créez une matrice diagonale à partir de S\_diag\_svd, de forme (min(m, n), min(m, n)).
# Placez ensuite cette matrice diagonale dans le coin supérieur gauche de la matrice Sigma\_svd, dont la forme est (m, n).

min_dim = min(m4_svd.shape)
Sigma_svd[:min_dim, :min_dim] = np.diag(S_diag_svd)

print("Σ (matrice des valeurs singulières) :\n", Sigma_svd)
# Vérification : U . Σ . Vt devrait être égal à m4_svd
print("U . Σ . Vt :\n", U_svd.dot(Sigma_svd).dot(Vt_svd))
Σ (matrice des valeurs singulières) :
 [[3.         0.         0.         0.         0.        ]
 [0.         2.23606798 0.         0.         0.        ]
 [0.         0.         2.         0.         0.        ]
 [0.         0.         0.         0.         0.        ]]
U . Σ . Vt :
 [[1. 0. 0. 0. 2.]
 [0. 0. 3. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 2. 0. 0. 0.]]

Diagonale et trace

print("Diagonale de m3_inv :", np.diag(m3_inv))
print("Trace de m3_inv :", m3_inv.trace())  # équivalent à np.diag(m3_inv).sum()
Diagonale de m3_inv : [ 1  7 31]
Trace de m3_inv : 39

Résolution de systèmes d’équations linéaires

La fonction solve résout un système d’équations scalaires linéaires, tel que :

  • 2x+6y=62x + 6y = 6

  • 5x+3y=95x + 3y = -9

coeffs = np.array([[2, 6], [5, 3]])
depvars = np.array([6, -9])
solution = linalg.solve(coeffs, depvars)
print("Solution (x, y) :", solution)

# Vérifions la solution :
print("coeffs . solution :", coeffs.dot(solution)) # Devrait être égal à depvars
print("np.allclose(coeffs.dot(solution), depvars) :", np.allclose(coeffs.dot(solution), depvars))
Solution (x, y) : [-3.  2.]
coeffs . solution : [ 6. -9.]
np.allclose(coeffs.dot(solution), depvars) : True

12. Vectorisation

Plutôt que d’effectuer les opérations élément par élément sur le tableau, il est bien plus efficace d’utiliser directement les opérations sur l’ensemble du tableau, une approche appelée vectorisation. Ceci permet de profiter pleinement des optimisations offertes par NumPy.

Par exemple, disons que nous voulons générer un tableau 768x1024 basé sur la formule sin(xy/40.5)sin(xy/40.5). Une mauvaise option serait de faire le calcul en Python en utilisant des boucles imbriquées :

import math
# Mauvaise option (inefficace)
data_boucle = np.empty((768, 1024))
for y in range(768):
    for x in range(1024):
        data_boucle[y, x] = math.sin(x * y / 40.5)

C’est vrai, ce code fonctionne, mais il reste très lent car il repose sur des boucles en Python pur. Pour optimiser ce calcul, adoptons une approche vectorisée : commençons par utiliser la fonction meshgrid de NumPy, qui permet de générer des grilles de coordonnées à partir de vecteurs.

x_coords = np.arange(0, 1024)  # [0, 1, 2, ..., 1023]
y_coords = np.arange(0, 768)   # [0, 1, 2, ..., 767]
X_grid, Y_grid = np.meshgrid(x_coords, y_coords)

print("Forme de X_grid :", X_grid.shape) # (768, 1024)
print("Forme de Y_grid :", Y_grid.shape) # (768, 1024)
Forme de X_grid : (768, 1024)
Forme de Y_grid : (768, 1024)

On constate ici que X_grid et Y_grid sont deux tableaux de dimension 768x1024 : chaque valeur de X_grid représente une coordonnée horizontale, tandis que chaque valeur de Y_grid correspond à une coordonnée verticale. Il ne reste plus qu’à calculer le résultat final en appliquant les opérations directement sur ces tableaux :

data_vectorise = np.sin(X_grid * Y_grid / 40.5)
print("Forme de data_vectorise :", data_vectorise.shape)
Forme de data_vectorise : (768, 1024)

C’est beaucoup plus rapide et concis. Nous pouvons visualiser ces données en utilisant la fonction imshow de Matplotlib.

import matplotlib.pyplot as plt

fig = plt.figure(figsize=(7, 6))
plt.imshow(data_vectorise, cmap="viridis") # 'hot' est une autre bonne cmap pour cela
plt.title("sin(xy/40.5) vectorisé")
plt.show()
<Figure size 700x600 with 1 Axes>

13. Sauvegarde et Chargement de Tableaux

NumPy facilite la sauvegarde et le chargement des ndarray en format binaire ou texte.

Format binaire .npy

Créons un tableau aléatoire et sauvegardons-le.

a_save = np.random.rand(2, 3)
print("a_save :\n", a_save)
np.save("mon_tableau.npy", a_save) # L'extension .npy est ajoutée automatiquement
a_save :
 [[0.15784066 0.414422   0.11634643]
 [0.15684217 0.56725685 0.76156938]]

Pour charger ce fichier dans un tableau NumPy, appelez simplement load :

a_charge_npy = np.load("mon_tableau.npy")
print("a_charge_npy :\n", a_charge_npy)
a_charge_npy :
 [[0.15784066 0.414422   0.11634643]
 [0.15684217 0.56725685 0.76156938]]

Format texte (par exemple, .csv)

Essayons de sauvegarder le tableau au format texte :

np.savetxt("mon_tableau.csv", a_save)

Ceci est un fichier CSV avec des espaces comme délimiteurs par défaut. Vous pouvez définir un délimiteur différent :

np.savetxt("mon_tableau_virgule.csv", a_save, delimiter=",")

Pour charger ce fichier, utilisez simplement loadtxt :

a_charge_csv = np.loadtxt("mon_tableau_virgule.csv", delimiter=",")
print("a_charge_csv :\n", a_charge_csv)
a_charge_csv :
 [[0.15784066 0.414422   0.11634643]
 [0.15684217 0.56725685 0.76156938]]

Format compressé .npz (pour plusieurs tableaux)

Il est également possible de sauvegarder plusieurs tableaux dans un seul fichier compressé :

b_save = np.arange(24, dtype=np.uint8).reshape(2, 3, 4)
np.savez("mes_tableaux.npz", mon_a=a_save, mon_b=b_save)

L’extension .npz est ajoutée automatiquement. Vous chargez ensuite ce fichier comme suit :

mes_tableaux_charges = np.load("mes_tableaux.npz")
print(type(mes_tableaux_charges)) # C'est un objet de type NpzFile
<class 'numpy.lib.npyio.NpzFile'>

C’est un objet de type dictionnaire qui charge les tableaux paresseusement (au moment où on y accède) :

print("Clés disponibles :", list(mes_tableaux_charges.keys()))
print("mes_tableaux_charges['mon_a'] :\n", mes_tableaux_charges['mon_a'])
print("mes_tableaux_charges['mon_b'] :\n", mes_tableaux_charges['mon_b'])
mes_tableaux_charges.close() # Bonne pratique de fermer le fichier
Clés disponibles : ['mon_a', 'mon_b']
mes_tableaux_charges['mon_a'] :
 [[0.15784066 0.414422   0.11634643]
 [0.15684217 0.56725685 0.76156938]]
mes_tableaux_charges['mon_b'] :
 [[[ 0  1  2  3]
  [ 4  5  6  7]
  [ 8  9 10 11]]

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

14. Conclusion et Prochaines Étapes

Vous maîtrisez désormais les bases de NumPy, mais il existe encore de nombreuses fonctionnalités à découvrir. Le meilleur moyen de progresser est de pratiquer avec NumPy et d’explorer sa documentation de référence, où vous trouverez de nombreuses autres fonctions et possibilités utiles.