NumPy, ou "Numerical Python", est une bibliothèque fondamentale pour la programmation scientifique en Python. Elle est la pierre angulaire de presque tout l'écosystème scientifique en Python : les logiciels MatPlotLib, SciPy et Pandas s'appuient notamment sur elle.
Elle fournit des structures de données pour stocker efficacement des tableaux multidimensionnels (vecteurs et matrices), ainsi que des fonctions pour travailler avec ces tableaux. Le type le plus important de NumPy est la classe ndarray (pour n dimensional array) qui permettra d'instancier nos vecteurs et nos matrices.
NumPy est principalement codée en C : il en résulte qu'elle est très performante en termes de temps d'exécution.
NumPy, est né de la fusion des bibliothèques Numeric et numarray en 2006. Tandis que Numeric a introduit les arrays en Python dans les années 1990, numarray a été développé parallèlement pour gérer de grands ensembles de données. La coexistence de ces deux bibliothèques a créé une fragmentation dans la communauté. Reconnaissant le besoin d'unifier les capacités de ces outils, Travis Oliphant a créé NumPy, combinant les meilleures caractéristiques des deux, établissant ainsi la norme pour le calcul numérique en Python.
Avant de pouvoir utiliser NumPy, il faut l'installer. En premier lieu, je vous conseille l'utilisation d'un environnement virtuel (venv) pour gérer votre projet, bien que cela ne soit pas une obligation. Pour plus d'informations sur l'utilisation de venv : https://docs.python.org/fr/3/library/venv.html
Ensuite, vous pouvez utilisez pip, pour déployer le logiciel avec la commande suivante:
pip install numpy
Une fois installé, vous pouvez importer NumPy dans votre script ou notebook Python. Traditionnellement, NumPy est associé à l'alias np dans votre programme (mais encore une fois, cela n'est pas une obligation).
import numpy as np
Pour l'heure je voudrais surtout développer le premier point. Imaginons que l'on souhaite manipuler une matrice en deux dimensions et de taille 5000 x 5000. Et partons du principe que nous ne souhaitons pas utiliser de librairie additionnelle : seul le langage Python nous intéresse. Voici un exemple de code permettant de produire une telle matrice (elle sera initialisée avec des valeurs aléatoire) et de produire quelques calculs à partir de cette dernière.
import math
import random
import time
# On prend le temps (via un timestamp) au démarrage du programme
begin = time.time()
# Création de la matrice 5000 x 5000 avec des valeurs aléatoires
matrix = [[random.random() for _ in range(5000)] for _ in range(5000)]
# Création d'une matrice contenant les sinus des valeurs de la matrice initiale
sin_matrix = [[math.sin(value) for value in row] for row in matrix]
# On prend le temps à la fin du programme et on affiche la durée
end = time.time()
print(f"({len(matrix)}, {len(matrix[0])}) - ({len(sin_matrix)}, {len(sin_matrix[0])})")
print(f"Duration: {end - begin:.2f} sec.")
(5000, 5000) - (5000, 5000) Duration: 2.47 sec.
Comme vous le constatez, nous avons ajouter des appels à la fonction time afin d'estimer le temps nécessaire pour produire nos résultats.
Note : bien entendu, les temps calculés seront différents d'une machine à une autre en fonction de la puissance de calcule de chaque machine.
A noter qu'on aurait pu faire pire en termes d'exécution si nous n'avions pas utilisé les listes en compréhension (list comprehension, en anglais). Effectivement, cette construction est optimisée par rapport à un parcours des éléments avec une boucle for traditionnelle.
import copy
import math
import random
import time
# On prend le temps (via un timestamp) au démarrage du programme
begin = time.time()
# Création de la matrice 5000 x 5000 avec des valeurs aléatoires
matrix = [[random.random() for _ in range(5000)] for _ in range(5000)]
# Création d'une matrice contenant les sinus des valeurs de la matrice initiale
sin_matrix = copy.deepcopy(matrix) # On duplique toutes les listes de la matrice (pas très efficace).
middle = time.time()
for line in sin_matrix: # On parcours les lignes et les colonnes (pas très efficace non plus).
for i, x in enumerate(line):
line[i] = math.sin(x)
# On prend le temps à la fin du programme et on affiche la durée (ici, en deux parties)
end = time.time()
print(f"({len(matrix)}, {len(matrix[0])}) - ({len(sin_matrix)}, {len(sin_matrix[0])})")
print(f"Duration1: {middle - begin:.2f} sec.")
print(f"Duration2: {end - middle:.2f} sec.")
(5000, 5000) - (5000, 5000) Duration1: 5.84 sec. Duration2: 2.78 sec.
Testons maintenant un programme équivalent, mais en s'appuyant sur la librairie NumPy.
import numpy as np
import time
# On prend le temps (via un timestamp) au démarrage du programme
begin = time.time()
# Création de la matrice 5000 x 5000 avec des valeurs aléatoires
matrix_np = np.random.rand(5000, 5000)
# Création d'une matrice contenant les sinus des valeurs de la matrice initiale
sin_matrix_np = np.sin(matrix_np)
# On prend le temps à la fin du programme et on affiche la durée
end = time.time()
print(f"{matrix_np.shape} - {sin_matrix_np.shape}")
print(f"Duration: {end - begin:.2f} sec.")
(5000, 5000) - (5000, 5000) Duration: 0.25 sec.
Je pense que le résultat est sans appel : NumPy est bien plus performant !!! Mais pourquoi ?
La version NumPy du programme est plus performante que la version pure Python pour plusieurs raisons:
Optimisation C : les opérations principales de NumPy sont implémentées en C, un langage compilé qui est généralement plus rapide que l'interprétation du code Python. Lorsque vous effectuez une opération avec NumPy, vous appelez en réalité des routines écrites en C qui ont été optimisées pour la performance. De plus, comme en C, on peut garantir que tous les éléments du tableau sont de même types (donc cohérent entre eux), cela nous affranchit de devoir systématiquement tester la nature de chaque donnée (contrairement à math.sin, par exemple, qui elle teste systématiquement si la valeur passée en paramètre est bien un nombre).
Opérations vectorisées : NumPy est conçu pour effectuer des opérations "vectorisées", c'est-à-dire des opérations qui s'appliquent simultanément à tous les éléments d'un tableau, sans nécessité d'une boucle explicite. Les opérations vectorisées sont optimisées pour tirer parti des architectures modernes de CPU, en utilisant des instructions SIMD (Single Instruction, Multiple Data) lorsque cela est possible.
Mémoire contiguë : les tableaux NumPy sont stockés dans des régions contiguës de mémoire, contrairement aux listes Python qui sont des structures liées. Cette contiguïté permet à NumPy d'accéder aux données de manière plus efficace, en tirant parti de la hiérarchie de la mémoire cache du processeur.
Moins d'overhead : en Python pur, chaque objet (même un simple nombre) a un overhead d'information (type, référence, etc.). N'oubliez pas qu'en Python, ce n'est pas la variable qui porte l'information de type, mais bien chaque donnée. Avec NumPy et grace au typage statique du langage C, les données sont stockées sous forme de tableaux denses sans cet overhead, ce qui rend les opérations plus rapides et la mémoire plus efficace.
Opérations parallèles : certains aspects de NumPy peuvent tirer parti des capacités multithread des processeurs modernes, permettant à plusieurs cœurs de travailler simultanément sur différentes parties des données.
En somme, en utilisant NumPy, vous bénéficiez d'optimisations à plusieurs niveaux - depuis l'implémentation en C jusqu'à l'utilisation efficace de la mémoire et des capacités du CPU - qui ne sont tout simplement pas présentes lors de l'utilisation de Python pur pour des tâches similaires.