
Si vous avez déjà des notions en Python, vous connaissez certainement les objets tels que les listes, les tuples, les dictionnaires …Toutefois, ces objets sont d’usage général et manquent de maturité scientifiques et par dessus tout font trop recours aux boucles qui, comme nous le savons, sont synonymes de lourdeur d’exécution. C’est pour cette raison que pour les calculs numériques, l’analyses des données, …Python dispose de la librairie numpy qui permet la création et la manipulation des matrices.
Les matrices sont les objets mathématiques centraux de l’algèbre linéaire mais aussi, la structures de données la plus efficace pour effectuer des calculs scientifiques sur ordinateur. Par ce que, pour tout data Scientiste ou Analyste quant qui se respecte, numpy (et plus encore Scipy, matplotlib) et plus spécifiquement son objet ndarray est très indispensable, nous consacrerons ce tutoriel, à la manipulation de cet objet.
Sommaire
Importation de la librairie numpy et efficience d’un objet ndarray
Il existe différente manière d’importer une librairie, nous pour cet article nous chargerons numpy de la façon suivante : import numpy as np . En suite, nous allons faire une petite comparaison entre une liste créer avec la fonction arange() de numpy et une liste range() standards de python. Pourquoi spécialement ces deux fonctions ? Par ce qu’elles sont sensées générer des séquences/séries de chiffres. Nous allons comparer leur performance avec la librairie timeit.
|
1 2 3 4 5 6 7 8 9 10 11 |
>>> import numpy as np >>> import timeit >>> N = 1000 >>> np_list = np.arange(N) # liste ndarray >>> std_list = range(N) # liste standard >>> t_np = timeit.Timer("np_list.sum()","from __main__ import np_list") >>> t_std = timeit.Timer("sum(std_list)","from __main__ import std_list") >>> print("Temps avec ndarray : ", t_np.timeit()) Temps avec ndarray : 3.144534872488408 >>> print("Temps avec liste : ", t_std.timeit()) Temps avec liste : 23.97810386881762 |
Comme nous pouvons l’observer, sommer une liste standard de 1000 éléments ( obtenu avec range()), nécessite 8 fois plus de temps qu’avec un objet ndarray (obtenu avec arange()). Ces performances sont à relativiser en fonction du système utilisé.
Création et caractéristiques d’objets ndarray
Un objet de type ndarray est un tableau multidimensionnel (d’où le nd = n-dimension) prenant en charge des données de type homogène, c’est à dire que les données doivent être de même type integer, float, …etc. Structurellement, c’est un objet constitué des données et de métadonnées (informations sur les données). Ces derrières portent sur les dimensions, la taille et le type de données notamment. Pour créer un objet de type ndarray, on utilise la fonction array(), qui grâce à l’argument dtype permet de spécifier le type des données :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
>>> matrix = np.array([[3.2,5,.2,-5],[5.2,9.2,-3,4],[0,-1.2,7.2,8]]) >>> matrix # afficher la matrice array([[ 3.2, 5. , 0.2, -5. ], [ 5.2, 9.2, -3. , 4. ], [ 0. , -1.2, 7.2, 8. ]]) >>> type(matrix) # type d'objet <class 'numpy.ndarray'> >>> matrix.dtype # type des données contenues dtype('float64') >>> matrix.ndim # la dimension de l'objet 2 >>> matrix.shape # les dimensions de l'objet Nombre Lignes,colonnes (3, 4) >>> matrix.size # la taille de la matrice Nombre de Lignes * Nombre de Lignes Colonnes 12 >>> matrix.nbytes # la taille mémoire de la matrice 96 |
Nous venons de créer une matrice donc un tableau à deux dimensions de type implicite float,donc les caractéristiques essentielles restent les dimensions des lignes et colonnes qui nous sont renvoyées sous forme de tuple (lignes, colonnes).
Quelques matrices remarquables
Nous avons vu qu’on pouvais créer des matrices ou de façon plus général, des tableaux multidimensionnels avec la fonction array(). Mais en algèbre linéaire, nous aurons besoins de quelques matrices très pratiques pour opérer des calculs comme des matrices unitaires, diagonales, des séquences ou encore des matrices aléatoires qui s’avèrent très utiles en simulation numérique :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# les séquences np.arange(1.,20,2) # générer des nombres entre 1 et 20 avec un pas de 2 np.linspace(1,5,10) # générer 10 nombres compris entre 1 et 5 np.random.rand(10) # générer 5 nombres aléatoires compris entre 0 et 1 # Crée les matrices avec la fonction matrix() np.matrix("1 0;-1 3") # 2X2 np.matrix("1.1 0.2 5.5 0;-1.5 3.0 8.9 0.0;-1.5 6.2 5 -4")# 3X4 # Matrices diagonales np.diag(1,3) # matrice unitaire np.eye(3) np.identity(3) np.diag([-2.3,5.2,3,4]) # diagonal quelconque np.diag(np.arange(10,40,10)) # matrice ne contenant que des 0 np.zeros(5) # dimensions 1*5 np.zeros((5,4)) # dimensions 5*4 np.zeros_like(matrix) # clonage de dimensions et de type de données # matrice ne contenant que des 1 np.ones(5) ; np.ones((2,3)) np.ones_like(matrix) # clonage de dimensions et de type de données |
Nous avons jusqu’ici créé des tableaux de dimension inférieure ou égale à 2 car très communs. Noter qu’avec la fonction array(), nous pouvons créer des tableaux de N dimension d’où le nom de l’objet ndarray. Par exemple, l’objet x ci-dessous est un tableau de 4 dimensions :
|
1 2 3 4 5 |
x = np.array([[[[2.3,-1.,2.],[0.2,0,0],[-1.,5.2,7.0]],[[2.,1.2,1],[3.1,3,5],[1.2,-5,6]]], [[[2.1,1.1,9.],[1.2,3.3,7.8],[8.8,4.2,-3.]],[[1.2,-9,0],[6.2,2.3,1.2],[8.5,2.3,5.2]]], [[[0.2,1.2,1],[-0.2,2.3,2],[6.6,10.,.3]],[[4.2,5.2,3.8],[8.2,2.5,1.1],[5.2,-1.2,3.1]]], ]) x.ndim # verifier |
Manipulation des ndarray
-
Indexation et extraction d’éléments ou de sous-ensembles
L’indexation d’élément d’une matrice ou d’un ndarray en général, se fait entre les crochets []. Et selon que l’on veut sélectionner des éléments du début à la fin ou de la fin au début, on utilise un chiffre positif ou négatif, dans le cas où l’indexation est faite avec les chiffres(on peut faire une indexation booléenne). Aussi, l’indexation inter-dimension est séparée par une virgule en plus clair, si x est une matrice alors pour indexer l’élément situé à l’intersection de la 2ème ligne et de la 3ème colonne, on écrira : x[2,3]. Ici nous ne travaillerons que l’indexation d’objet de 1D et 2D. Si besoin la logique peut être généralisée aux dimensions supérieurs.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# objet 1D vecteur matrice 12X1 vector = np.array([1.6,-3,0,5,9,-2,3.4,-5,8.7,2,3,5],dtype = float) vector[0] # indexation du 1er élément vector[2:5] # indexation du 3ème au 5ème élément vector[1:12:2] # indexation du 2ème au 12ème élément en raison d'un pas de 2 vector[:5] # indexation des 5 premiers elements vector[5:] # indexation des éléments situés au delà du 5ème élément vector[::-1] # inverser l'ordre des éléments vector[[5,2,3]] # indexation d’éléments non contigus vector[-1] # indexation négative faisant référence au premier élément partant de la fin vector[vector < 0.0] # indexation booléenne des des valeurs négatives # objet 2D matrice 5X4 matrix = np.array([[5.2,6.2,-3.0,0.0], [9.2,3.5,-5.6,0.5], [2.2,0.5,3.4,-9.2], [-1.2,3.5,-5.6,2.5], [8.1,-1.2,2.6,3.2]],dtype = float) matrix[:,:] # indexation de toutes les lignes et colonnes matrix[:,2] # indexation de la 3ème colonne matrix[2,:] # indexation de la 3ème ligne matrix[:,1:4] # indexation de la 2ème colonne à la 5ème matrix[:3,:3] # les 3èmes lignes des 3 premières colonnes matrix[3,3] # indexation d'un élément en l’occurrence 2.5 matrix[matrix < 0.0] # indexation booléenne des valeurs négatives |
-
Opération de transformation
La fonction copy() : on pourrait s’interroger sur le bien fondé d’une telle fonction sachant qu’il suffirait peut être d’affecter un objet à un nouvel objet qui en contiendrait une réplique exacte. Il faut savoir en effet que la simple affectation d’un objet existant à un nouvel objet, crée seulement une nouvelle référence vers le même objet, et non un nouvel objet. Par exemple, observons de plus près le résultat des instructions ci-dessous :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> X = np.arange(1,10) >>> X array([1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> Y = X # affectation pour faire une "copie" de X >>> Y array([1, 2, 3, 4, 5, 6, 7, 8, 9]) >>> Y[1:5] = np.zeros(4) # modifions Y >>> Y # Y modifié array([1, 0, 0, 0, 0, 6, 7, 8, 9]) >>> X # X aussi s'en trouve affecté array([1, 0, 0, 0, 0, 6, 7, 8, 9]) >>> id(X) 1886180221056 >>> id(Y) 1886180221056 |
On voit bien que les deux variables pointent vers un même objet( ils ont la même adresse mémoire) et le résultat obtenu ou la dépendance entre ces deux variables n’est pas vraiment ce que nous recherchons. Ainsi, pour faire une vraie copie, indépendante, on utilise copy().
|
1 2 3 4 5 6 7 |
>>> X = np.arange(1,10) >>> Y = X.copy() >>> Y[:] = 0 # modifions Y >>> Y array([0, 0, 0, 0, 0, 0, 0, 0, 0]) >>> X # X n'est plus affecté array([1, 2, 3, 4, 5, 6, 7, 8, 9]) |
La fonction reshape() : cette fonction permet notamment de modifier l’attribut shape, que nous avons vu plus haut et donc de changer la dimension d’un objet.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> X = np.arange(20) >>> X.reshape(4,5) # transformer l'objet d'1D en 2D array([[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9], [10, 11, 12, 13, 14], [15, 16, 17, 18, 19]]) >>> X.reshape(2,2,5) # transformer l'objet d'1D en 3D array([[[ 0, 1, 2, 3, 4], [ 5, 6, 7, 8, 9]], [[10, 11, 12, 13, 14], [15, 16, 17, 18, 19]]]) |
Nous pourrions aboutir aux mêmes transformations en agissant sur l’attribut shape, comme-ceci : X.shape = (4,5)
Les fonctions ravel() et flatten() : Ces fonctions transforment tous les objets de dimensions supérieurs à 1 en des objets de 1D. Mais alors pourquoi 2 fonctions ? Par ce que l’une c’est-à-dire ravel() renvoie seulement une référence à l’objet de départ alors que l’autre c’est-à-dire flatten() crée un objet indépendant (comme avec la fonction copy() ce que l’on a vu ci-dessus) :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> matrix = np.array([[5.2,6.2,-3.0,0.0], ... [9.2,3.5,-5.6,0.5], ... [2.2,0.5,3.4,-9.2], ... [-1.2,3.5,-5.6,2.5], ... [8.1,-1.2,2.6,3.2]],dtype = float) >>> matrix.shape (5, 4) >>> matrix.ravel() array([ 5.2, 6.2, -3. , 0. , 9.2, 3.5, -5.6, 0.5, 2.2, 0.5, 3.4, -9.2, -1.2, 3.5, -5.6, 2.5, 8.1, -1.2, 2.6, 3.2]) >>> matrix.flatten() array([ 5.2, 6.2, -3. , 0. , 9.2, 3.5, -5.6, 0.5, 2.2, 0.5, 3.4, -9.2, -1.2, 3.5, -5.6, 2.5, 8.1, -1.2, 2.6, 3.2]) |
La fonction transpose() : La transposition est une opération courante en algèbre linaire, très pratique sur les matrices notamment. structurellement parlant, il s’agit de faire une rotation de la matrice par rapport à sa diagonale.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> matrix array([[ 5.2, 6.2, -3. , 0. ], [ 9.2, 3.5, -5.6, 0.5], [ 2.2, 0.5, 3.4, -9.2], [-1.2, 3.5, -5.6, 2.5], [ 8.1, -1.2, 2.6, 3.2]]) >>> matrix.shape (5, 4) >>> matrix.transpose() array([[ 5.2, 9.2, 2.2, -1.2, 8.1], [ 6.2, 3.5, 0.5, 3.5, -1.2], [-3. , -5.6, 3.4, -5.6, 2.6], [ 0. , 0.5, -9.2, 2.5, 3.2]]) >>> matrix.transpose().shape (4, 5) |
Opérations mathématiques et statistiques :
-
Les opérations et fonctions mathématiques générales
Avec les objets ndarray, les opérations basiques telles que l’addition, la soustractions, la multiplication et la division sont possibles et mieux encore numpy propose des fonctions pour ces opérations. Par ailleurs, numpy offre également la possibilité d’utiliser des fonctions mathématiques habituelles telles que , sin(), cos(), exp(), log()…Ces fonctions ont été baptisées “universal functions” ( ufunc). Voici quelques utilisations de ces fonctions :
|
1 2 3 4 5 6 7 8 9 10 11 |
A = np.array([[12.5,13.0],[14.0,-10.5]], dtype = float) B = np.array([[-12.5,15.0],[15.5,11.2]],dtype = float) A+B ; np.add(A,B) A-B ; np.subtract(A,B) A*B ; np.multiply(A,B) A/B ; np.divide(A,B) np.log(A) # logarithme de base e np.log10(A) # logarithme de base 10 np.sqrt(A) # racine carrée np.abs(A) # valeur absolue np.exp(A) # exponentiel e^A |
-
Quelques fonctions statistiques
Nous pouvons opérer de simples calculs statistiques tels que la somme, la moyenne, la médiane, la variance, l’écart-type…. Il faut savoir que pour toutes ces fonctions énumérées ci-dessous, sum(), mean(), max(),…, on a la l’argument axis, qui lorsqu’il est égal à 0, effectue l’opération pour chaque ligne et pour chaque colonne, si axis = 1 .
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
X = np.array([[1,3,3],[1,4,3],[1,3,4]],dtype = float) X.sum() # la somme X.sum(axis = 0) # somme par ligne X.sum(axis = 1) # somme par colonne X.mean() # la moyenne np.median(X) # la mediane np.percentile(X,(25,50,75)) # les 3 quartiles X.max() # le maximum X.argmax() # l'indice du maximum X.min() # le minimum X.argmin() # l'indice du minimum X.cumsum() # les sommes cumulées X.var() # la variance X.std() # l'écart-type |
Si l’on s’y connaît un peu en R, on aurait remarqué la fameuse fonction summary(), qui permet de faire d’afficher pour chaque colonne d’un objet, le minimum et le maximum, les trois quartiles et la moyenne. Pour ceux qui ont un faible pour cette fameuse fonction en voici une réplique :
|
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 |
>>> def summary(matrix): for i in range(matrix.shape[1]): print("Min. :",matrix[:,i].min(), "\n1st Qu. :",np.percentile(matrix[:,i],25), "\nMedian :" ,np.median(matrix[:,i]) , "\nMean :" , matrix[:,i].mean(), "\n3rd Qu. :", np.percentile(matrix[:,i],75) , "\nMax. :", matrix[:,i].max(),"\n") >>> summary(X) Min. : 1.0 1st Qu. : 1.0 Median : 1.0 Mean : 1.0 3rd Qu. : 1.0 Max. : 1.0 Min. : 3.0 1st Qu. : 3.0 Median : 3.0 Mean : 3.33333333333 3rd Qu. : 3.5 Max. : 4.0 Min. : 3.0 1st Qu. : 3.0 Median : 3.0 Mean : 3.33333333333 3rd Qu. : 3.5 Max. : 4.0 |
Toutefois, l’on peut faire appel à la fonction describe(), de la célébrissime librairie pandas. Il faudrait toutefois, penser à convertir l’objet ndarray en DataFrame ou autres types d’objets de cette librairie.
-
Quelques fonctions pratiques de l’Algèbre linéaire
Nous pouvons réaliser des opérations d’algèbre linéaire, comme le produit matriciel, la détermination du déterminant et le calcul de l’inverse d’une matrice et la décomposition de matrice. numpy pour se faire, dispose d’un sous-module linalg(linear algeber, en toutes lettres). Voyons ce que ce module nous propose comme fonctions :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
X = np.array([[1,3,3],[1,4,3],[1,3,4]],dtype = float) # déterminant de X np.det(X) np.linalg.det(X) # produit matriciel np.dot(np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")) np.linalg.multi_dot([np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")]) # transposée d'une matrice np.transpose(X) X.transpose() X.T # inverse d'une matrice np.linalg.inv(X) np.round(np.linalg.inv(X).dot(X)) # juste pour vérifier # valeurs et vecteurs propres np.linalg.eig(X) val.propres,vec.propres = np.linalg.eig(X) # les stockers séparement # les autres types de produit de matrices np.cross(np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")) np.inner(np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")) np.outer(np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")) np.kron(np.matrix("1 0;-1 3"),np.matrix("3 1;2 1")) |
On peut maintenant être tentée d’appliquer quelques fonctions à un vieux type d’exercice qui consiste à résoudre des systèmes d’équations à plusieurs inconnues celui ci-dessous :
![Rendered by QuickLaTeX.com \[ \begin{cases}-2x+y-z+5w-2v=11\\ x+3y+2z-3w+2v=31\\-x+4y+8z-7w+5v=64\\4x-2y+5z+3w+v =76\\5x+7y-3z+3w+2v=88 \end{cases} \]](https://www.ephiquant.com/wp-content/ql-cache/quicklatex.com-263d4d371f3922ee73fe41732dec3485_l3.png)
le système donné, nous devons le réécrire sous forme matricielle tel que
où :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
>>> A = np.matrix("-2 1 -1 5 -2; 1 3 2 -3 2;-1 4 8 -7 5; 4 -2 5 3 1; 5 7 -3 3 2") >>> b = np.matrix("11;31;64;76;88") >>> # Ax = b ==> solution x,y,z,w,v >>> np.linalg.inv(A).dot(b) matrix([[ 5.], [ 6.], [ 7.], [ 8.], [ 9.]]) >>> np.linalg.solve(A,b) matrix([[ 5.], [ 6.], [ 7.], [ 8.], [ 9.]]) |






