3 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:

import numpy as np
Concepts abordés:

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 flottants

La 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 2x2

Numpy 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 complexes

Le 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.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] = 3

Slicing. 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 correspondante

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))

a) É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).

Indication

La documentation de la fonction arange est disponible ici.

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éments par éléments:

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 mat1

La plupart des fonctions mathématiques définies par Numpy (voir ici) sont aussi effectuées éléments par éléments:

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 mat1

Le produit matriciel peut être effectué d'une des trois façons suivantes, mais la dernière méthode est recommandée pour sa lisibilité (surtout quand plusieurs produits matriciels sont effectués):

np.dot(mat1,mat2)
mat1.dot(mat2)
mat1 @ mat2

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).

Indication

Dans Jupyter Lab, il est très facile de déterminer le temps pris par une cellule pour s'évaluer, il suffit de commencer la cellule par %%time, par exemple:

%%time
result = diff_list(a_list)

Pour évaluer la cellule à de multiples reprises et faire une moyenne sur le temps d'exécution afin d'obtenir un résultat plus précis, remplacer %%time par %%timeit. La documentation est disponible ici.

Réponse

Le temps d'exécution avec les tableaux Numpy devrait être approximativement 50 à 100 fois plus rapide qu'avec les listes !

Exercice 3.3: Représentations graphiques

Le module matplotlib permet de faire des représentations graphiques très variées. Pour l'utiliser, il est d'usage de l'importer ainsi:

%matplotlib inline
import matplotlib.pyplot as plt

A noter que la première ligne permet de représenter les graphiques directement dans Jupyter Lab, mais n'est pas indispensable.

Par exemple la fonction plot peut être utilisée pour représenter la fonction \( x^2 \):

x = np.linspace(0,1,50)
y = x**2
plt.plot(x,y)
plt.show()

Afin de définir une jolie figure pouvant être exportée, la syntaxe est la suivante:

plt.figure(figsize=(8,5)) # taille de la figure (en inches)
plt.title(r'Graphique de $x^2$') # titre de la figure (du code LaTeX peut être inclus)
plt.xlabel(r'$x$') # titre de l'axe horizontal
plt.ylabel(r'$y$') # titre de l'axe vertical
plt.plot(x, y, marker='o', label=r"$x^2$") # légende
plt.legend() # affiche la légende
plt.savefig("test.pdf") # exporte la figure en PDF
plt.savefig("test.png", dpi=100) # exporte la figure en PNG
plt.show()

La documentation de Matplotlib est disponible ici.

a) Représenter graphiquement sur la même figure les fonctions \( \sin(kx) \) et \( \cos(kx) \) pour \( k=1,2,3 \) pour \( x\in[0,2\pi] \). Faire en sorte que les graduations sur l'axe horizontal soient tous les \( \frac{\pi}{2} \):

Indication

Utiliser la fonction xticks ou les fonctions set_xticks et set_xticklabels décrites ici.

b) Regarder l'aide de la fonction imshow et l'utiliser pour représenter graphiquement une matrice de nombres aléatoires dans \( [0,1] \) de taille \( 10\times10 \):

c) ! Représenter graphiquement la fonction \( f(x,y) = \frac{-y}{5} + e^{-x^2-y^2} \) pour \( x\in[-3,3] \) et \( y\in[-3,3] \) en densité et avec des courbes de niveau:

d) !! Regarder les exemples disponibles ici et en choisir deux à comprendre et à modifier.

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 i

À 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 j

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 ij

Enfin il est possible d'indexer un tableau pour 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 5

Pour 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]

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.

© 2026, Julien Guillod. Licence CC BY-NC-ND