numpy et le calcul matriciel#

Ce chapitre traite d’un module en particulier : le module numpy. numpy est un raccourci pour Numerical Python : cette librairie a donc pour vocation de fournir des outils de calcul numérique en Python. Ce module permet donc de manipuler des vecteurs et des matrices (voire des tenseurs d’ordre supérieur).

Avant toute chose, vous devrez importer ce module :

import numpy as np

Vous remarquez ici que lors de l’import, le nom du module est renommé en np : il s’agit d’une habitude très répandue qui permet de ne pas surcharger inutilement la suite de votre code.

De plus, sachez que ce chapitre est très succinct et très loin de couvrir l’ensemble des fonctionnalités numpy, vous êtes donc fortement incités à utiliser la documentation numpy pour trouver ce qui pourrait vous être utile pour votre usage.

Tableaux multi-dimensionnels#

Les tableaux multi-dimensionnels sont les objets de base en numpy. On peut créer un vecteur comme suit :

vec = np.array([1, 4, 6, 7])

print(vec.ndim)
1

Dans ce chapitre, nous allons nous concentrer sur des vecteurs (ndim = 1, comme dans l’exemple ci-dessus) et des matrices (ndim = 2), mais il faut savoir que les tableaux multi-dimensionnels numpy peuvent stocker des tenseurs d’ordre quelconque.

Voici quelques exemples de manipulations élémentaires sur les tableaux numpy :

# Multiplication par une constante
print(2.5 * vec)

# Accès au type des données stockées
print(vec.dtype)

# Accès à la taille du vecteur
print(vec.shape)

# Définition d'une matrice
A = np.array([[0, 1], [2, 3]])
print(A)

# Transposition
print(A.T)
[ 2.5 10.  15.  17.5]
int64
(4,)
[[0 1]
 [2 3]]
[[0 2]
 [1 3]]

On remarque d’ores et déjà que les tableaux numpy ont un type associé. On ne pourra donc pas stocker dans un tableau numpy des données de types hétérogènes, comme on peut le faire dans le cas des listes Python par exemple.

Produit matriciel et opérations « élément à élément »#

Une chose importante à comprendre en numpy est que le produit par défaut entre deux tableaux est le produit élément à élément, et non pas le produit matriciel, comme on peut le voir dans cet exemple :

A = np.array([[0, 1], [2, 3]])
print(A)
[[0 1]
 [2 3]]
I = np.array([[1, 0], [0, 1]])
print(I)
[[1 0]
 [0 1]]
A * I
array([[0, 0],
       [0, 3]])

Il est toutefois possible d’effectuer un produit matriciel à l’aide de l’opérateur @, et alors on retrouve bien la propriété attendue qui est que le produit de A par la matrice identité retourne la matrice A :

A @ I
array([[0, 1],
       [2, 3]])

De même, lorsqu’on écrit A ** 2, on obtient l’élévation au carré de chacun des éléments de A et non pas le produit de A par lui-même :

A ** 2
array([[0, 1],
       [4, 9]])
A @ A
array([[ 2,  3],
       [ 6, 11]])

Les choses sont plus simples pour l’addition puisqu’il n’y a alors pas de confusion possible :

A + A
array([[0, 2],
       [4, 6]])

Constructeurs de tableaux usuels#

numpy permet de définir très simplement des tableaux remplis de 0, de 1, la matrice identité, ou des séquences de valeurs :

np.zeros((2, 3))  # (2, 3) est la taille de la matrice à produire
array([[0., 0., 0.],
       [0., 0., 0.]])
np.ones((2, 3))  # (2, 3) est la taille de la matrice à produire
array([[1., 1., 1.],
       [1., 1., 1.]])
np.eye(2)  # eye -> matrice identité
array([[1., 0.],
       [0., 1.]])
np.arange(10)  # arange -> équivalent de range pour les listes
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# Vecteur de 11 valeurs espacées régulièrement entre 0 et 1
np.linspace(0, 1, 11)
array([0. , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1. ])
m = np.arange(10)
# Redimensionner m pour qu'il ait 5 lignes et 2 colonnes
m.reshape((5, 2))
array([[0, 1],
       [2, 3],
       [4, 5],
       [6, 7],
       [8, 9]])

Accès à des sous-parties des tableaux#

Comme pour les listes, les tableaux numpy peuvent être accédés par « tranches » (slice), comme dans les exemples suivants :

M = np.array([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19]])
M
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14],
       [15, 16, 17, 18, 19]])
# Indices de ligne jusqu'à 2 (exclu)
# Indices de colonne à partir de 3 (inclus)
M[:2, 3:]
array([[3, 4],
       [8, 9]])
# Indices de ligne de 1 (inclus) à 3 (exclu)
# Tous les indices de colonne
M[1:3, :]
array([[ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])

On peut également utiliser des listes (ou des ndarray) d’indices pour accéder aux éléments d’un tableau situés aux indices correspondants :

liste_indices = [0, 4]
v = np.array([1, 5, 7, 9, 12])
v[liste_indices]
array([ 1, 12])

Ici, v[liste_indices] est un ndarray constitué des éléments [v[0], v[4]]. De la même façon, si les indices sont stockés dans une structure à deux dimensions :

indices = np.array([[0, 4],
                    [1, 3]])
v[indices]
array([[ 1, 12],
       [ 5,  9]])

On voit que, ici, v[indices] est un ndarray à 2 dimensions constitué des éléments

v[0] v[4]
v[1] v[3]

Opérations élémentaires sur les tableaux#

Une fois un tableau défini, on peut très facilement calculer :

  • la somme de ses éléments :

np.sum(M)  # Peut aussi s'écrire M.sum()
190
  • sa plus petite / plus grande valeur :

np.min(M)  # Peut aussi s'écrire M.min()
0
np.max(M)  # Peut aussi s'écrire M.max()
19
  • la moyenne / l’écart-type de ses éléments :

np.mean(M)  # Peut aussi s'écrire M.mean()
9.5
np.std(M)  # Peut aussi s'écrire M.std()
5.766281297335398

Il est à noter que pour toutes ces opérations, deux syntaxes co-existent :

print(np.min(M))
print(M.min())
0
0

De plus, on peut également effectuer ces opérations ligne par ligne, ou colonne par colonne, comme ci-dessous :

# On somme sur les lignes (dimension numéro 0)
# Donc on obtient un résultat par colonne
M.sum(axis=0)
array([30, 34, 38, 42, 46])

Enfin, on peut très facilement créer des masques binaires, tels que :

M > 5  # Vaut True à chaque position telle que l'élément correspondant dans M est > 5
array([[False, False, False, False, False],
       [False,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True]])

Ce qui permet de compter simplement le nombre de valeurs d’un tableau vérifiant une condition :

np.sum(M > 5)
14

Bonnes pratiques#

Vous devrez, tant que faire se peut, utiliser les fonctions prédéfinies en numpy pour vos manipulations de tableaux multi-dimensionnels, plutôt que de recoder les opérations élémentaires. Il est notamment fortement déconseillé de parcourir les valeurs d’un tableau numpy au sein d’une boucle, pour des raisons d’efficacité (autrement dit, de temps de calcul), comme illustré ci-dessous :

vec = np.ones((100, 10))
%%timeit  
# timeit permet de mesurer le temps d'exécution d'un morceau de code
vec.sum()
2.4 µs ± 67.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%%timeit  
# timeit permet de mesurer le temps d'exécution d'un morceau de code 
s = 0
for v in vec:  # À ne JAMAIS faire !
    s += v
130 µs ± 1.85 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Exercices#

Exercice 10.1

Calculez, en numpy, la somme des n premiers entiers, pour n fixé.

Exercice 10.2

Supposons qu’on ait stocké dans le tableau suivant les notes reçues par 2 étudiants à 3 examens :

notes = np.array(
  [[10, 12],
   [15, 16],
   [18, 12]]
)
  1. Calculez la moyenne de chacun des deux étudiants.

  2. Calculez le nombre de notes supérieures à 12 contenues dans ce tableau