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 np2. 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 numpyPour 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 numpy3. 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_3darray([[[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()
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 complex648
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) -> 2Db_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 10b_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) # Puissancea_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 int16uint8 [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 float64float64 [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+23c_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_binnp.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ébutq8 (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 :
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 :
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 . 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()
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 automatiquementa_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 fichierClé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.