Contenu

2.1. Numpy (tableaux de données multi-dimensionnels)

Librairie de calcul numérique permettant notamment de manipuler des tableaux de dimension quelconque.

%matplotlib inline
from jyquickhelper import add_notebook_menu
add_notebook_menu()
run previous cell, wait for 2 seconds

2.1.1. Introduction

  • numpy est un module utilisé dans presque tous les projets de calcul numérique sous Python

    • Il fournit des structures de données performantes pour la manipulation de vecteurs, matrices et tenseurs plus généraux

    • numpy est écrit en C et en Fortran d’où ses performances élevées lorsque les calculs sont vectorisés (formulés comme des opérations sur des vecteurs/matrices)

Pour utiliser numpy il faut commencer par l’importer :

import numpy as np

Dans la terminologie numpy, vecteurs, matrices et autres tenseurs sont appelés arrays.

2.1.2. Création d’arrays numpy

Plusieurs possibilités:

  • à partir de listes ou n-uplets Python

  • en utilisant des fonctions dédiées, telles que arange, linspace, etc.

  • par chargement à partir de fichiers

2.1.2.1. À partir de listes

Au moyen de la fonction numpy.array :

# un vecteur : l'argument de la fonction est une liste Python
v = np.array([1, 3, 2, 4])
print(v)
print(type(v))
[1 3 2 4]
<class 'numpy.ndarray'>

Pour définir une matrice (array de dimension 2 pour numpy):

# une matrice : l'argument est une liste de liste
M = np.array([[1, 2], [3, 4]])
print(M)
[[1 2]
 [3 4]]
M[0, 0]
1

Les objets v et M sont tous deux du type ndarray (fourni par numpy)

type(v), type(M)
(numpy.ndarray, numpy.ndarray)

v et M ne diffèrent que par leur taille, que l’on peut obtenir via la propriété shape :

v.shape
(4,)
M.shape
(2, 2)

Pour obtenir le nombre d’éléments d’un array :

v.size
4
M.size
4

On peut aussi utiliser numpy.shape et numpy.size

np.shape(M)
(2, 2)

Les arrays ont un type qu’on obtient via dtype :

print( M)
print(M.dtype)
[[1 2]
 [3 4]]
int64

Les types doivent être respectés lors d’assignations à des arrays

M[0,0] = "hello"
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/2b/cj2pm60x61s5qlxpmr7g7km00000gn/T/ipykernel_6810/3388738246.py in <module>
----> 1 M[0,0] = "hello"

ValueError: invalid literal for int() with base 10: 'hello'

On peut modifier le type d’un array après sa déclaration en utilisant astype

a = np.array([1,2,3], dtype=np.int64)
b = np.array([2,2,3], dtype=np.int64)
b = b.astype(float)
print(a / b)
[0.5 1.  1. ]

On peut définir le type de manière explicite en utilisant le mot clé dtype en argument:

M = np.array([[1, 2], [3, 4]], dtype=complex)
M
array([[1.+0.j, 2.+0.j],
       [3.+0.j, 4.+0.j]])
  • Autres types possibles avec dtype : int, float, complex, bool, object, etc.

  • On peut aussi spécifier la précision en bits: int64, int16, float128, complex128.

2.1.2.2. En utilisant des fonctions de génération d’arrays

2.1.2.3. arange

# create a range
x = np.arange(0, 10, 2) # arguments: start, stop, step
x
array([0, 2, 4, 6, 8])
x = np.arange(-1, 1, 0.1)
x
array([-1.00000000e+00, -9.00000000e-01, -8.00000000e-01, -7.00000000e-01,
       -6.00000000e-01, -5.00000000e-01, -4.00000000e-01, -3.00000000e-01,
       -2.00000000e-01, -1.00000000e-01, -2.22044605e-16,  1.00000000e-01,
        2.00000000e-01,  3.00000000e-01,  4.00000000e-01,  5.00000000e-01,
        6.00000000e-01,  7.00000000e-01,  8.00000000e-01,  9.00000000e-01])

2.1.2.3.1. linspace and logspace

# avec linspace, le début et la fin SONT inclus
np.linspace(0, 10, 25)
array([ 0.        ,  0.41666667,  0.83333333,  1.25      ,  1.66666667,
        2.08333333,  2.5       ,  2.91666667,  3.33333333,  3.75      ,
        4.16666667,  4.58333333,  5.        ,  5.41666667,  5.83333333,
        6.25      ,  6.66666667,  7.08333333,  7.5       ,  7.91666667,
        8.33333333,  8.75      ,  9.16666667,  9.58333333, 10.        ])
np.linspace(0, 10, 11)
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])
print(np.logspace(0, 10, 10, base=np.e))
[1.00000000e+00 3.03773178e+00 9.22781435e+00 2.80316249e+01
 8.51525577e+01 2.58670631e+02 7.85771994e+02 2.38696456e+03
 7.25095809e+03 2.20264658e+04]

2.1.2.3.2. mgrid

x, y = np.mgrid[0:5, 0:5] 
x
array([[0, 0, 0, 0, 0],
       [1, 1, 1, 1, 1],
       [2, 2, 2, 2, 2],
       [3, 3, 3, 3, 3],
       [4, 4, 4, 4, 4]])
y
array([[0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4]])

2.1.2.3.3. Données aléatoires

from numpy import random
# tirage uniforme dans [0,1]
random.rand(5,5)  # ou np.random.rand
array([[0.24864548, 0.63089037, 0.77389506, 0.81783434, 0.79596002],
       [0.85558193, 0.22401831, 0.55064774, 0.77261266, 0.17076234],
       [0.95389589, 0.90503278, 0.11467454, 0.25471487, 0.74490302],
       [0.84278478, 0.0603186 , 0.59791704, 0.11026734, 0.00240362],
       [0.81703152, 0.36956662, 0.79248384, 0.67138247, 0.72257048]])
# tirage suivant une loi normale standard
random.randn(5,5)
array([[-0.45940848, -0.08739989,  1.23701621, -0.99410567,  0.71004674],
       [ 0.92328688, -0.42949293, -0.83454461,  0.78319392,  0.42515375],
       [-1.76969754,  0.99760808,  0.73659765, -0.04455947,  2.33848227],
       [-0.46933546,  1.14501506, -0.80759395,  0.03592718, -0.53140356],
       [-0.58504991, -0.05095495,  0.58677105, -0.36585961, -1.49440989]])

2.1.2.3.4. diag

# une matrice diagonale
np.diag([1,2,3])
array([[1, 0, 0],
       [0, 2, 0],
       [0, 0, 3]])
# diagonale avec décalage par rapport à la diagonale principale
np.diag([1,2,3], k=1)
array([[0, 1, 0, 0],
       [0, 0, 2, 0],
       [0, 0, 0, 3],
       [0, 0, 0, 0]])

2.1.2.3.5. zeros, ones et identity

np.zeros((3,), dtype=int)  # attention zeros(3,3) est FAUX
array([0, 0, 0])
np.ones((3,3))
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
print(np.zeros((3,), dtype=int))
print(np.zeros((1, 3), dtype=int))
print(np.zeros((3, 1), dtype=int))
[0 0 0]
[[0 0 0]]
[[0]
 [0]
 [0]]
print(np.identity(3))
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

2.1.2.4. À partir de fichiers d’E/S

2.1.2.4.1. Fichiers séparés par des virgules (CSV)

Un format fichier classique est le format CSV (comma-separated values), ou bien TSV (tab-separated values). Pour lire de tels fichiers utilisez numpy.genfromtxt. Par exemple:

data = np.genfromtxt('files/DONNEES.csv', delimiter=',')
data
array([[ 1.,  2.,  3.,  4.,  5.],
       [ 6.,  7.,  8.,  9., 10.],
       [11., 12., 13., 14., 15.],
       [16., 17., 18., 19., 20.]])
data.shape
(4, 5)

A l’aide de numpy.savetxt on peut enregistrer un array numpy dans un fichier txt:

M = random.rand(3,3)
M
array([[0.93055858, 0.78556744, 0.74477941],
       [0.86289541, 0.21311052, 0.19970051],
       [0.83596086, 0.16492924, 0.5769627 ]])
np.savetxt("random-matrix.txt", M)
np.savetxt("random-matrix.csv", M, fmt='%.5f', delimiter=',') # fmt spécifie le format

2.1.2.4.2. Format de fichier Numpy natif

Pour sauvegarder et recharger des array numpy : numpy.save et numpy.load :

np.save("random-matrix.npy", M)
np.load("random-matrix.npy")
array([[0.93055858, 0.78556744, 0.74477941],
       [0.86289541, 0.21311052, 0.19970051],
       [0.83596086, 0.16492924, 0.5769627 ]])

2.1.2.5. Autres propriétés des arrays numpy

M
array([[0.93055858, 0.78556744, 0.74477941],
       [0.86289541, 0.21311052, 0.19970051],
       [0.83596086, 0.16492924, 0.5769627 ]])
M.dtype
dtype('float64')
M.itemsize # octets par élément
8
M.nbytes # nombre d'octets
72
M.nbytes / M.size
8.0
M.ndim # nombre de dimensions
2
print(np.zeros((3,), dtype=int).ndim)
print( np.zeros((1, 3), dtype=int).ndim)
print (np.zeros((3, 1), dtype=int).ndim)
1
2
2

2.1.3. Manipulation et Opérations sur les arrays

2.1.3.1. Indexation

# v est un vecteur, il n'a qu'une seule dimension -> un seul indice
v[0]
1
# M est une matrice, ou un array à 2 dimensions -> deux indices 
M[1,1]
0.21311051767796552

Contenu complet :

M
array([[0.93055858, 0.78556744, 0.74477941],
       [0.86289541, 0.21311052, 0.19970051],
       [0.83596086, 0.16492924, 0.5769627 ]])

La deuxième ligne :

M[1]
array([0.86289541, 0.21311052, 0.19970051])

On peut aussi utiliser :

M[1,:] # 2 ème ligne (indice 1)
array([0.86289541, 0.21311052, 0.19970051])
M[:,1] # 2 ème colonne (indice 1)
array([0.78556744, 0.21311052, 0.16492924])
print(M.shape)
print( M[1,:].shape, M[:,1].shape)
(3, 3)
(3,) (3,)

On peut assigner des nouvelles valeurs à certaines cellules :

M[0,0] = 1
M
array([[1.        , 0.78556744, 0.74477941],
       [0.86289541, 0.21311052, 0.19970051],
       [0.83596086, 0.16492924, 0.5769627 ]])
# on peut aussi assigner des lignes ou des colonnes
M[1,:] = -1
# M[1,:] = [1, 2, 3]
M
array([[ 1.        ,  0.78556744,  0.74477941],
       [-1.        , -1.        , -1.        ],
       [ 0.83596086,  0.16492924,  0.5769627 ]])

2.1.3.2. Slicing ou accès par tranches

Slicing fait référence à la syntaxe M[start:stop:step] pour extraire une partie d’un array :

A = np.array([1,2,3,4,5])
A
array([1, 2, 3, 4, 5])
A[1:3]
array([2, 3])

Les tranches sont modifiables :

A[1:3] = [-2,-3]
A
array([ 1, -2, -3,  4,  5])

On peut omettre n’importe lequel des argument dans M[start:stop:step]:

A[::] # indices de début, fin, et pas avec leurs valeurs par défaut
array([ 1, -2, -3,  4,  5])
A[::2] # pas = 2, indices de début et de fin par défaut
array([ 1, -3,  5])
A[:3] # les trois premiers éléments
array([ 1, -2, -3])
A[3:] # à partir de l'indice 3
array([4, 5])
M = np.arange(12).reshape(4, 3)
print( M)
[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]

On peut utiliser des indices négatifs :

A = np.array([1,2,3,4,5])
A[-1] # le dernier élément
5
A[-3:] # les 3 derniers éléments
array([3, 4, 5])

Le slicing fonctionne de façon similaire pour les array multi-dimensionnels

A = np.array([[n+m*10 for n in range(5)] for m in range(5)])

A
array([[ 0,  1,  2,  3,  4],
       [10, 11, 12, 13, 14],
       [20, 21, 22, 23, 24],
       [30, 31, 32, 33, 34],
       [40, 41, 42, 43, 44]])
A[1:4, 1:4]  # sous-tableau
array([[11, 12, 13],
       [21, 22, 23],
       [31, 32, 33]])
# sauts
A[::2, ::2]
array([[ 0,  2,  4],
       [20, 22, 24],
       [40, 42, 44]])
A
array([[ 0,  1,  2,  3,  4],
       [10, 11, 12, 13, 14],
       [20, 21, 22, 23, 24],
       [30, 31, 32, 33, 34],
       [40, 41, 42, 43, 44]])
A[[0, 1, 3]]
array([[ 0,  1,  2,  3,  4],
       [10, 11, 12, 13, 14],
       [30, 31, 32, 33, 34]])

2.1.3.3. Indexation avancée (fancy indexing)

Lorsque qu’on utilise des listes ou des array pour définir des tranches :

row_indices = [1, 2, 4]
print( A)
print("\n")
print ( A[row_indices])
# print( A.shape)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]


[[10 11 12 13 14]
 [20 21 22 23 24]
 [40 41 42 43 44]]
A[[1, 2]][:, [3, 4]] = 0  # ATTENTION !
print( A)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
print ( A[[1, 2], [3, 4]])
[13 24]
A[np.ix_([1, 2], [3, 4])] = 0
print ( A)
[[ 0  1  2  3  4]
 [10 11 12  0  0]
 [20 21 22  0  0]
 [30 31 32 33 34]
 [40 41 42 43 44]]

On peut aussi utiliser des masques binaires :

B = np.arange(5)
B
array([0, 1, 2, 3, 4])
row_mask = np.array([True, False, True, False, False])
print(  B[row_mask])
print(  B[[0,2]])
[0 2]
[0 2]
# de façon équivalente
row_mask = np.array([1,0,1,0,0], dtype=bool)
B[row_mask]
array([0, 2])
# ou encore
a = np.array([1, 2, 3, 4, 5])
print(  a < 3)
print(  B[a < 3])
[ True  True False False False]
[0 1]
print(  A,"\n")
print(  A[:, a < 3])
[[ 0  1  2  3  4]
 [10 11 12  0  0]
 [20 21 22  0  0]
 [30 31 32 33 34]
 [40 41 42 43 44]] 

[[ 0  1]
 [10 11]
 [20 21]
 [30 31]
 [40 41]]

2.1.3.4. Opérations élément par élément

On déclare aa et bb sur lesquelles nous allons illustrer quelques opérations

a = np.ones((3,2))
b = np.arange(6).reshape(a.shape)
print(a)
b
[[1. 1.]
 [1. 1.]
 [1. 1.]]
array([[0, 1],
       [2, 3],
       [4, 5]])

Les opérations arithmétiques avec les scalaires, ou entre arrays s’effectuent élément par élément. Lorsque le dtype n’est pas le même ( aa contient des float, bb contient des int), numpy adopte le type le plus “grand” (au sens de l’inclusion).

print( (a + b)**2 )
print( np.abs( 3*a - b ) )
f = lambda x: np.exp(x-1)
print( f(b) )
[[ 1.  4.]
 [ 9. 16.]
 [25. 36.]]
[[3. 2.]
 [1. 0.]
 [1. 2.]]
[[ 0.36787944  1.        ]
 [ 2.71828183  7.3890561 ]
 [20.08553692 54.59815003]]
1/b
/var/folders/2b/cj2pm60x61s5qlxpmr7g7km00000gn/T/ipykernel_6947/4032182506.py:1: RuntimeWarning: divide by zero encountered in true_divide
  1/b
array([[       inf, 1.        ],
       [0.5       , 0.33333333],
       [0.25      , 0.2       ]])

2.1.3.5. Broadcasting

Que se passe-t-il si les dimensions sont différentes?

c = np.ones(6)
c
array([1., 1., 1., 1., 1., 1.])
b+c   # déclenche une exception
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/2b/cj2pm60x61s5qlxpmr7g7km00000gn/T/ipykernel_6947/1847358893.py in <module>
----> 1 b+c   # déclenche une exception

ValueError: operands could not be broadcast together with shapes (3,2) (6,) 
c = np.arange(3).reshape((3,1))
print(b,c, sep='\n\n')
b+c
[[0 1]
 [2 3]
 [4 5]]

[[0]
 [1]
 [2]]
array([[0, 1],
       [3, 4],
       [6, 7]])

L’opération précédente fonctionne car numpy effectue ce qu’on appelle un broadcasting de c : une dimension étant commune, tout se passe comme si on dupliquait c sur la dimension non-partagée avec b. Vous trouverez une explication visuelle simple ici :

a = np.zeros((3,3))
a[:,0] = -1
b = np.array(range(3))
print(a + b)
[[-1.  1.  2.]
 [-1.  1.  2.]
 [-1.  1.  2.]]

2.1.4. Extraction de données à partir d’arrays et création d’arrays

2.1.4.1. where

Un masque binaire peut être converti en indices de positions avec where

x = np.arange(0, 10, 0.5)
print ( x)
mask = (x > 5) * (x < 7.5)
print(  mask)
indices = np.where(mask)
indices
[0.  0.5 1.  1.5 2.  2.5 3.  3.5 4.  4.5 5.  5.5 6.  6.5 7.  7.5 8.  8.5
 9.  9.5]
[False False False False False False False False False False False  True
  True  True  True False False False False False]
(array([11, 12, 13, 14]),)
x[indices] # équivalent à x[mask]
array([5.5, 6. , 6.5, 7. ])

2.1.4.2. diag

Extraire la diagonale ou une sous-diagonale d’un array :

print ( A)
np.diag(A)
[[ 0  1  2  3  4]
 [10 11 12  0  0]
 [20 21 22  0  0]
 [30 31 32 33 34]
 [40 41 42 43 44]]
array([ 0, 11, 22, 33, 44])
np.diag(A, -1)
array([10, 21, 32, 43])

2.1.5. Algèbre linéaire

La performance des programmes écrit en Python/Numpy dépend de la capacité à vectoriser les calculs (les écrire comme des opérations sur des vecteurs/matrices) en évitant au maximum les boucles for/while.

Vous avez un éventail de fonctions pour faire de l’algèbre linéaire dans numpy ou dans scipy. Cela peut vous servir si vous cherchez à faire une décomposition matricielle particulière (LU, QR, SVD,…), si vous vous intéressez aux valeurs propres d’une matrice, etc…

2.1.5.1. Opérations scalaires

On peut effectuer les opérations arithmétiques habituelles pour multiplier, additionner, soustraire et diviser des arrays avec/par des scalaires :

v1 = np.arange(0, 5)
print (v1)
[0 1 2 3 4]
v1 * 2
array([0, 2, 4, 6, 8])
v1 + 2
array([2, 3, 4, 5, 6])
A = np.array([[n+m*10 for n in range(5)] for m in range(5)])
print(  A)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
print(  A * 2)
[[ 0  2  4  6  8]
 [20 22 24 26 28]
 [40 42 44 46 48]
 [60 62 64 66 68]
 [80 82 84 86 88]]
print(  A + 2)
[[ 2  3  4  5  6]
 [12 13 14 15 16]
 [22 23 24 25 26]
 [32 33 34 35 36]
 [42 43 44 45 46]]

2.1.5.2. Opérations terme-à-terme sur les arrays

Les opérations par défaut sont des opérations terme-à-terme :

A = np.array([[n+m*10 for n in range(5)] for m in range(5)])
print ( A)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
A * A # multiplication terme-à-terme
array([[   0,    1,    4,    9,   16],
       [ 100,  121,  144,  169,  196],
       [ 400,  441,  484,  529,  576],
       [ 900,  961, 1024, 1089, 1156],
       [1600, 1681, 1764, 1849, 1936]])
(A + A.T) / 2
array([[ 0. ,  5.5, 11. , 16.5, 22. ],
       [ 5.5, 11. , 16.5, 22. , 27.5],
       [11. , 16.5, 22. , 27.5, 33. ],
       [16.5, 22. , 27.5, 33. , 38.5],
       [22. , 27.5, 33. , 38.5, 44. ]])
print(  v1)
print(  v1 * v1)
[0 1 2 3 4]
[ 0  1  4  9 16]

En multipliant des arrays de tailles compatibles, on obtient des multiplications terme-à-terme par ligne :

A.shape, v1.shape
((5, 5), (5,))
print(  A)
print(  v1)
print(  A * v1)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
[0 1 2 3 4]
[[  0   1   4   9  16]
 [  0  11  24  39  56]
 [  0  21  44  69  96]
 [  0  31  64  99 136]
 [  0  41  84 129 176]]

2.1.5.3. Algèbre matricielle

Comment faire des multiplications de matrices ? Deux façons :

  • en utilisant les fonctions dot;

  • en utiliser le type matrix.

print( A.shape)
print( A)
print( type(A))
(5, 5)
[[ 0  1  2  3  4]
 [10 11 12 13 14]
 [20 21 22 23 24]
 [30 31 32 33 34]
 [40 41 42 43 44]]
<class 'numpy.ndarray'>
print( np.dot(A, A))  # multiplication matrice
print( A * A ) # multiplication élément par élément
[[ 300  310  320  330  340]
 [1300 1360 1420 1480 1540]
 [2300 2410 2520 2630 2740]
 [3300 3460 3620 3780 3940]
 [4300 4510 4720 4930 5140]]
[[   0    1    4    9   16]
 [ 100  121  144  169  196]
 [ 400  441  484  529  576]
 [ 900  961 1024 1089 1156]
 [1600 1681 1764 1849 1936]]
A.dot(v1)
array([ 30, 130, 230, 330, 430])
np.dot(v1, v1)
30

Avec le type matrix de Numpy

M = np.matrix(A)
v = np.matrix(v1).T # en faire un vecteur colonne
M * v
matrix([[ 30],
        [130],
        [230],
        [330],
        [430]])
# produit scalaire
v.T * v
matrix([[30]])
# avec les objets matrices, c'est les opérations standards sur les matrices qui sont appliquées
v + M*v
matrix([[ 30],
        [131],
        [232],
        [333],
        [434]])

Si les dimensions sont incompatibles on provoque des erreurs :

v = np.matrix([1,2,3,4,5,6]).T
np.shape(M), np.shape(v)
((5, 5), (6, 1))
M * v
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/2b/cj2pm60x61s5qlxpmr7g7km00000gn/T/ipykernel_6947/2280171313.py in <module>
----> 1 M * v

/opt/anaconda3/lib/python3.8/site-packages/numpy/matrixlib/defmatrix.py in __mul__(self, other)
    216         if isinstance(other, (N.ndarray, list, tuple)) :
    217             # This promotes 1-D vectors to row vectors
--> 218             return N.dot(self, asmatrix(other))
    219         if isscalar(other) or not hasattr(other, '__rmul__') :
    220             return N.dot(self, other)

<__array_function__ internals> in dot(*args, **kwargs)

ValueError: shapes (5,5) and (6,1) not aligned: 5 (dim 1) != 6 (dim 0)

Voir également les fonctions : inner, outer, cross, kron, tensordot. Utiliser par exemple help(kron).

On peut calculer l’inverse ou le déterminant de \(A\)

A = np.tril(np.ones((3,3)))
b = np.diag([1,2, 3])
print(A)
print("-------")
print(np.linalg.det(A))  # déterminant de la matrice A
print("-------")
inv_A = np.linalg.inv(A)   # inverse de la matrice A
print(inv_A)
print("-------")
print(inv_A.dot(A))
[[1. 0. 0.]
 [1. 1. 0.]
 [1. 1. 1.]]
-------
1.0
-------
[[ 1.  0.  0.]
 [-1.  1.  0.]
 [ 0. -1.  1.]]
-------
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]

… résoudre des systèmes d’equations linéaires du type \(Ax=b\)

x = np.linalg.solve(A, np.diag(b))
print(np.diag(b))
print(x)
print(A.dot(x))
[1 2 3]
[1. 1. 1.]
[1. 2. 3.]

… ou encore obtenir les valeurs propres de \(A\)

np.linalg.eig(A)
(array([1., 1., 1.]),
 array([[ 0.00000000e+00,  0.00000000e+00,  4.93038066e-32],
        [ 0.00000000e+00,  2.22044605e-16, -2.22044605e-16],
        [ 1.00000000e+00, -1.00000000e+00,  1.00000000e+00]]))
np.linalg.eigvals(A)
array([1., 1., 1.])

2.1.5.4. Transformations d’arrays ou de matrices

  • Plus haut .T a été utilisé pour transposer l’objet matrice v

  • On peut aussi utiliser la fonction transpose

Autres transformations :

C = np.matrix([[1j, 2j], [3j, 4j]])
C
matrix([[0.+1.j, 0.+2.j],
        [0.+3.j, 0.+4.j]])
np.conjugate(C)
matrix([[0.-1.j, 0.-2.j],
        [0.-3.j, 0.-4.j]])

Transposée conjuguée :

C.H
matrix([[0.-1.j, 0.-3.j],
        [0.-2.j, 0.-4.j]])

Parties réelles et imaginaires :

np.real(C) # same as: C.real
matrix([[0., 0.],
        [0., 0.]])
np.imag(C) # same as: C.imag
matrix([[1., 2.],
        [3., 4.]])

Argument et module :

np.angle(C+1) 
matrix([[0.78539816, 1.10714872],
        [1.24904577, 1.32581766]])
np.abs(C)
matrix([[1., 2.],
        [3., 4.]])

2.1.5.5. Caclul matriciel

2.1.5.6. Analyse de données

Numpy propose des fonctions pour calculer certaines statistiques des données stockées dans des arrays :

data = np.vander([1, 2, 3, 4])
print( data)
print( data.shape)
[[ 1  1  1  1]
 [ 8  4  2  1]
 [27  9  3  1]
 [64 16  4  1]]
(4, 4)

2.1.5.6.1. mean

# np.mean(data)
print( np.mean(data, axis=0))
[25.   7.5  2.5  1. ]
# la moyenne de la troisième colonne
np.mean(data[:,2])
2.5

2.1.5.6.2. variance et écart type

np.var(data[:,2]), np.std(data[:,2])
(1.25, 1.118033988749895)

2.1.5.7. min et max

data[:,2].min()
1
data[:,2].max()
4
data[:,2].sum()
10
data[:,2].prod()
24

2.1.5.7.1. sum, prod, et trace

d = np.arange(0, 10)
d
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# somme des éléments
np.sum(d)
45

ou encore :

d.sum()
45
# produit des éléments
np.prod(d+1)
3628800
# somme cumulée
np.cumsum(d)
array([ 0,  1,  3,  6, 10, 15, 21, 28, 36, 45])
# produit cumulé
np.cumprod(d+1)
array([      1,       2,       6,      24,     120,     720,    5040,
         40320,  362880, 3628800])
# équivalent à diag(A).sum()
np.trace(data)
9

2.1.5.8. Calculs avec parties d’arrays

en utilisant l’indexation ou n’importe quelle méthode d’extraction de donnés à partir des arrays

data
array([[ 1,  1,  1,  1],
       [ 8,  4,  2,  1],
       [27,  9,  3,  1],
       [64, 16,  4,  1]])
np.unique(data[:,1]) 
array([ 1,  4,  9, 16])
mask = data[:,1] == 4
np.mean(data[mask,3])
1.0

2.1.5.9. Calculs avec données multi-dimensionnelles

Pour appliquer min, max, etc., par lignes ou colonnes :

m = random.rand(3,4)
m
array([[0.93442735, 0.87099207, 0.17577932, 0.67129581],
       [0.8198531 , 0.05267227, 0.99672342, 0.69230584],
       [0.29042857, 0.71206665, 0.67469746, 0.26979785]])
# max global 
m.max()
0.996723416763222
# max dans chaque colonne
m.max(axis=0)
array([0.93442735, 0.87099207, 0.99672342, 0.69230584])
# max dans chaque ligne
m.max(axis=1)
array([0.93442735, 0.99672342, 0.71206665])

Plusieurs autres méthodes des classes array et matrix acceptent l’argument (optional) axis keyword argument.

2.1.6. Génération de nombres aléatoires et statistiques

Le module : numpy.random apporte à python la possibilité de générer un échantillon de taille nn directement, alors que le module natif de python ne produit des tirages que un par un. Le module numpy.random est donc bien plus efficace si on veut tirer des échantillon conséquents. Par ailleurs, scipy.stats fournit des méthodes pour un très grand nombre de distributions et quelques fonctions classiques de statistiques.

Par exemple, on peut obtenir un array 4x3 de tirages gaussiens standard (soit en utilisant randn ou normal :

np.random.randn(4,3)
array([[ 0.05524557,  0.55336726, -1.1627065 ],
       [-0.36085927, -0.54374562, -2.05864731],
       [-0.94184756,  1.68148183,  0.40946403],
       [ 0.64721499, -1.25286187, -1.91732065]])

Pour se convaincre que numpy.random est plus efficace que le module random de base de python. On effectue un grand nombre de tirages gaussiens standard, en python pur et via numpy.

N = int(1e4)
from random import normalvariate
%timeit [normalvariate(0,1) for _ in range(N)]
6.82 ms ± 573 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit np.random.randn(N)
334 µs ± 37.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

2.1.7. Copy et “deep copy”

Pour des raisons de performance Python ne copie pas automatiquement les objets (par exemple passage par référence des paramètres de fonctions).

A = np.array([[0,  2],[ 3,  4]])
A
array([[0, 2],
       [3, 4]])
B = A
# changer B affecte A
B[0,0] = 10
B
array([[10,  2],
       [ 3,  4]])
A
array([[10,  2],
       [ 3,  4]])
B = A
print( B is A)
True

Pour éviter ce comportement, on peut demander une copie profonde (deep copy) de A dans B

#B = np.copy(A)
B = A.copy()
# maintenant en modifiant B, A n'est plus affecté
B[0,0] = -5

B
array([[-5,  2],
       [ 3,  4]])
A  # A est aussi modifié !
array([[10,  2],
       [ 3,  4]])
print( A - A[:,0] ) # FAUX
print (A - A[:,0].reshape((2, 1)))  # OK
[[ 0 -1]
 [-7  1]]
[[ 0 -8]
 [ 0  1]]

2.1.8. Changement de forme et de taille, et concaténation des arrays

A
array([[10,  2],
       [ 3,  4]])
n, m = A.shape
B = A.reshape((1,n*m))
B
array([[10,  2,  3,  4]])
B[0,0:5] = 5 # modifier l'array

B
array([[5, 5, 5, 5]])
A
array([[5, 5],
       [5, 5]])

2.1.8.1. Attention !

La variable originale est aussi modifiée ! B n’est qu’une nouvelle vue de A.

Pour transformer un array multi-dimmensionel en un vecteur. Mais cette fois-ci, une copie des données est créée :

B = A.flatten()
B
array([5, 5, 5, 5])
B[0:5] = 10
B
array([10, 10, 10, 10])
A # A ne change pas car B est une copie de A
array([[5, 5],
       [5, 5]])

2.1.8.2. Ajouter une nouvelle dimension avec newaxis

par exemple pour convertir un vecteur en une matrice ligne ou colonne :

v = np.array([1,2,3])
np.shape(v)
(3,)
# créer une matrice à une colonne à partir du vectuer v
v[:, np.newaxis]
array([[1],
       [2],
       [3]])
v[:,np.newaxis].shape
(3, 1)
# matrice à une ligne
v[np.newaxis,:].shape
(1, 3)

2.1.8.3. Concaténer, répéter des arrays

En utilisant les fonctions repeat, tile, vstack, hstack, et concatenate, on peut créer des vecteurs/matrices plus grandes à partir de vecteurs/matrices plus petites :

2.1.8.3.1. repeat et tile

a = np.array([[1, 2], [3, 4]])
a
array([[1, 2],
       [3, 4]])
# répéter chaque élément 3 fois
np.repeat(a, 3) # résultat 1-d
array([1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4])
# on peut spécifier l'argument axis
np.repeat(a, 3, axis=1)
array([[1, 1, 1, 2, 2, 2],
       [3, 3, 3, 4, 4, 4]])

Pour répéter la matrice, il faut utiliser tile

# répéter la matrice 3 fois
np.tile(a, 3)
array([[1, 2, 1, 2, 1, 2],
       [3, 4, 3, 4, 3, 4]])

2.1.8.3.2. concatenate

b = np.array([[5, 6]])
np.concatenate((a, b), axis=0)
array([[1, 2],
       [3, 4],
       [5, 6]])
np.concatenate((a, b.T), axis=1)
array([[1, 2, 5],
       [3, 4, 6]])

2.1.8.3.3. hstack et vstack

np.vstack((a,b))
array([[1, 2],
       [3, 4],
       [5, 6]])
np.hstack((a,b.T))
array([[1, 2, 5],
       [3, 4, 6]])

2.1.9. Itérer sur les éléments d’un array

  • Dans la mesure du possible, il faut éviter l’itération sur les éléments d’un array : c’est beaucoup plus lent que les opérations vectorisées

  • Mais il arrive que l’on n’ait pas le choix…

v = np.array([1,2,3,4])

for element in v:
    print(element)
1
2
3
4
M = np.array([[1,2], [3,4]])

for row in M:
    print ("row", row)
    
    for element in row:
        print( element)
row [1 2]
1
2
row [3 4]
3
4

Pour obtenir les indices des éléments sur lesquels on itère (par exemple, pour pouvoir les modifier en même temps) on peut utiliser enumerate :

for row_idx, row in enumerate(M):
    print ("row_idx", row_idx, "row", row)
    
    for col_idx, element in enumerate(row):
        print( "col_idx", col_idx, "element", element)
       
        # update the matrix M: square each element
        M[row_idx, col_idx] = element ** 2
row_idx 0 row [1 2]
col_idx 0 element 1
col_idx 1 element 2
row_idx 1 row [3 4]
col_idx 0 element 3
col_idx 1 element 4
# chaque élément de M a maintenant été élevé au carré
M
array([[ 1,  4],
       [ 9, 16]])

2.1.10. Utilisation d’arrays dans des conditions

Losqu’on s’intéresse à des conditions sur tout on une partie d’un array, on peut utiliser any ou all :

M
array([[ 1,  4],
       [ 9, 16]])
if (M > 5).any():
    print( "au moins un élément de M est plus grand que 5")
else:
    print ("aucun élément de M n'est plus grand que 5")
au moins un élément de M est plus grand que 5
if (M > 5).all():
    print ("tous les éléments de M sont plus grands que 5")
else:
    print( "tous les éléments de M sont plus petits que 5")
tous les éléments de M sont plus petits que 5

2.1.11. Type casting

On peut créer une vue d’un autre type que l’original pour un array

M =np.array([[-1,2], [0,4]])
M.dtype
dtype('int64')
M2 = M.astype(float)
M2
array([[-1.,  2.],
       [ 0.,  4.]])
M2.dtype
dtype('float64')
M3 = M.astype(bool)
M3
array([[ True,  True],
       [False,  True]])