import numpy as np3 Structures homogènes
Les structures de données par défaut de Python permettent de gérer des données hétérogènes (par exemple des entiers et des chaînes de caractères). Cette particularité fait que les structures de données Python sont extrêmement flexibles, au détriment de la performance. En effet vu que des données hétérogènes doivent pouvoir être supportées, il n’est pas possible d’allouer une plage de mémoire fixe pour une structure de données, ce qui ralentit son utilisation. Particulièrement en mathématiques, il apparaît très régulièrement des ensembles de données homogènes de tailles fixes (liste d’entiers, vecteurs réels ou complexes, matrices…). Le module NumPy définit le type ndarray qui est optimisé pour de telles structures de données homogènes de tailles fixes. La documentation de NumPy est disponible ici.
Pour charger le module NumPy, il est d’usage de procéder ainsi:
Exercice 3.1 - Introduction à NumPy
Création. La taille et le type des éléments d’un tableau NumPy doivent être connus à l’avance. La première façon de créer un tableau NumPy est de construire un tableau rempli de zéros en spécifiant la taille et le type:
array0 = np.zeros(3, dtype=int) # vecteur de 3 entiers
array1 = np.zeros((2,4), dtype=float) # tableau de flottants de taille 2x4
array2 = np.zeros((2,2), dtype=complex) # matrice carrée complexe de taille 2x2
array3 = np.zeros((5,6,4)) # tableau tridimensionnel de flottantsLa seconde façon est de passer directement les données:
array4 = np.array([1,4,5]) # vecteur d'entiers (1,4,5)
array5 = np.array([[1.1,2.2,3.3,4.4],[1,2,3,4]]) # matrice de taille 2x4 de flottants
array6 = np.array([[1+1j,0.4],[3,1.5]]) # matrice complexe de taille 2x2NumPy va alors déterminer lui-même le type et la taille du tableau. À noter qu’il est possible de forcer le type:
array0 = np.array([1,4,5], dtype=complex) # vecteur de complexesLe type des éléments du tableau NumPy array1 peut être déterminé par array1.dtype. La taille de ce tableau est donnée par array1.shape. Les commandes suivantes permettent d’accéder aux éléments des tableaux :
array4[1] # retourne 4
array5[1,3] # retourne 4.0np.float64(4.0)À noter que les indices commencent à 0 et non pas à 1. Les tableaux NumPy sont mutables dans le sens où les données peuvent être modifiées mais en conservant le même type et la même taille:
array0[1] = 4
array1[1,3] = 3.3
array3[3,4,2] = 3Slicing. Le slicing permet d’accéder à certaines parties d’un tableau:
array4[2:3] # retourne les éléments d'indices compris entre 2 et 3
array1[0,:] # retourne la première ligne de array1
array1[:,-1] # retourne la dernière colonne de array1
array3[3,3:5,1:4] # retourne la sous-matrice correspondantearray([[0., 0., 0.],
[0., 3., 0.]])Itération. Il est possible d’itérer un tableau sur sa première dimension, par exemple pour retourner la somme des lignes:
for i in array5:
print(np.sum(i))11.0
10.0a. Étudier la documentation de la fonction arange et utiliser cette fonction pour générer les vecteurs \((5,6,7,8,9)\) et \((3,5,7,9)\)s.
b. Étudier la documentation de la fonction linspace et l’utiliser pour générer 10 points équidistribués dans l’intervalle \([2,5]\).
c. Lire la documentation de la fonction reshape et effectuer successivement les transformations suivantes:
\[(1,2,3,4,5,6)\to\begin{pmatrix}1 & 2\\ 3 & 4\\ 5 & 6 \end{pmatrix}\to\begin{pmatrix}1 & 2 & 3\\ 4 & 5 & 6 \end{pmatrix}\to\begin{pmatrix}1 & 4\\ 2 & 5\\ 3 & 6 \end{pmatrix}\]
Exercice 3.2 - Opérations sur les tableaux
Les opérations arithmétiques de base sur les tableaux NumPy sont effectuées élément par élément:
mat1 = np.array([[1,2.5,3],[5,6.1,8],[3,2,5]])
mat2 = np.array([[1,0.5,0],[0,0.9,8],[2,0,0]])
mat1 + mat2 # retourne la somme élément par élément
mat1 * mat2 # retourne le produit élément par élément (pas le produit matriciel)
10*mat1**2 # retourne 10 fois le carré des éléments de mat1array([[ 10. , 62.5, 90. ],
[250. , 372.1, 640. ],
[ 90. , 40. , 250. ]])La plupart des fonctions mathématiques définies par NumPy (voir https://numpy.org/doc/stable/reference/routines.math.html) sont également effectuées élément par élément:
np.cos(mat1) # retourne le cosinus élément par élément de mat1
np.exp(mat1) # retourne l'exponentielle élément par élément de mat1array([[2.71828183e+00, 1.21824940e+01, 2.00855369e+01],
[1.48413159e+02, 4.45857770e+02, 2.98095799e+03],
[2.00855369e+01, 7.38905610e+00, 1.48413159e+02]])Le produit matriciel peut être effectué d’une des trois façons suivantes:
np.dot(mat1,mat2)
mat1.dot(mat2)
mat1 @ mat2array([[ 7. , 2.75, 20. ],
[21. , 7.99, 48.8 ],
[13. , 3.3 , 16. ]])a. Donné un vecteur \((v_0,v_1,\dots,v_{n-1})\in\mathbb{R}^n\) la dérivée discrète de ce vecteur est définie par le vecteur \((d_0,d_1,\dots,d_{n-2})\in\mathbb{R}^{n-1}\) donné par \(d_i = v_{i+1}-v_{i}\) pour \(i=0,1,\dots,n-2\).
Écrire une fonction diff_list qui calcule la dérivée discrète d’une liste et une fonction diff_np qui fait la même opération mais sur des vecteurs NumPy en utilisant le slicing.
b. Soit a_list et a_np respectivement une liste et un tableau de 1 000 éléments tirés au hasard dans l’intervalle \([0,1]\):
a_list = [np.random.random() for _ in range(1000)]
a_np = np.random.random(1000)Comparer le temps d’exécution de diff_list(a_list) et de diff_np(a_np).
Exercice 3.3 - Matrice de Vandermonde
Pour \(p, n\in \mathbb{N}^*\) et \(\boldsymbol{x}= (x_1, \ldots, x_p)\) un vecteur de taille \(p\), la matrice de Vandermonde correspondante est définie par:
\[V(\boldsymbol{x},n)=\begin{pmatrix} 1 & x_1 & x_1^2 & \cdots & x_1^{n-1} & x_1^n \\ 1 & x_2 & x_2^2 & \cdots & x_2^{n-1} & x_2^n \\ \vdots & \vdots& \vdots & \ddots & \vdots & \vdots\\ 1 & x_{p-1} & x_{p-1}^2 & \cdots & x_{p-1}^{n-1} & x_{p-1}^n \\ 1 & x_p & x_p^2 & \cdots & x_p^{n-1} & x_p^n \end{pmatrix}.\]
a. Écrire une fonction qui construit la matrice \(V(\boldsymbol{x},n)\) élément par élément à l’aide d’une double boucle.
b. Après avoir établi une relation permettant d’écrire la \(k\)-ième colonne de \(V(\boldsymbol{x},n)\) uniquement en fonction de \(\boldsymbol{x}\) et de \(k\), écrire une deuxième fonction qui construit la matrice \(V(\boldsymbol{x},n)\) colonne par colonne à l’aide de cette relation.
c. Après avoir établi une relation entre la \(k\)-ième colonne de \(V(\boldsymbol{x},n)\), sa \((k-1)\)-ième colonne et le vecteur \(\boldsymbol{x}\), écrire une troisième fonction qui construit la matrice \(V(\boldsymbol{x},n)\) colonne par colonne à l’aide de cette relation.
d. Comparer les temps d’exécution de ces trois fonctions pour \(n=150\), \(p=100\) et \(\boldsymbol{x}\) généré aléatoirement.
Exercice 3.4 - Indexage de tableaux !
Le slicing permet de sélectionner des blocs dans un tableau, mais il est également possible de sélectionner des éléments disparates en utilisant un tableau comme indexage:
a = np.arange(12)**2 # tableau des carrés parfaits
i = np.array([1,3,8,5]) # tableau d'indices
a[i] # tableau des éléments de a aux places iarray([ 1, 9, 64, 25])À noter qu’il est également possible d’indexer par un tableau de dimension supérieure. Le résultat est alors un tableau de la même forme que l’index:
j = np.array([[3,4],[9,7]]) # tableau bidimensionnel d'indices
a[j] # sélectionne les éléments de a avec les indices jarray([[ 9, 16],
[81, 49]])Pour un tableau à plusieurs dimensions:
b = np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]])
i = np.array([0,1,2,2]) # tableau des premiers indices
j = np.array([1,0,3,1]) # tableau des seconds indices
b[i,j] # sélectionne les éléments d'indices ijarray([ 1, 4, 11, 9])Enfin il est possible d’indexer un tableau par un tableau de booléens:
c = np.array([[0,1,2,3],[4,5,6,7],[8,9,10,11]])
cond = (c >= 5) # tableau de booléens valant True si >= 5 et False sinon
c[cond] = 5 # assigne la valeur 5 à toutes les entrées de c plus grandes que 5Pour la suite, on considère les nombres:
[0.9602, -0.99, 0.2837, 0.9602, 0.7539, -0.1455, -0.99, -0.9111, 0.9602, -0.1455, -0.99, 0.5403, -0.99, 0.9602, 0.2837, -0.99, 0.2837, 0.9602][0.9602,
-0.99,
0.2837,
0.9602,
0.7539,
-0.1455,
-0.99,
-0.9111,
0.9602,
-0.1455,
-0.99,
0.5403,
-0.99,
0.9602,
0.2837,
-0.99,
0.2837,
0.9602]comme étant les résultats d’une mesure effectuée toutes les 0.1 seconde aux temps compris entre 2 et 3.7 secondes.
a. Les mesures étant censées être positives, modifier les données pour mettre 0 lorsque les valeurs sont négatives.
b. Calculer les temps pour lesquels les mesures précédentes sont maximales.
c. Pour chaque mesure maximale, retourner la mesure précédente, la mesure maximale et la mesure suivante. Si la mesure précédente ou la suivante n’existent pas, les remplacer par np.nan.