.. _science:
Bibliothèques numériques de base
################################
.. contents:: Table des matières
:local:
..
1 Numpy
1.1 Tableaux
1.1.1 Création de tableaux
1.1.2 Manipulations sur les tableaux
1.1.3 Opérations de base
1.2 Entrées/sorties
1.3 Sous-modules
1.4 Fonctions avancées
1.4.1 Tableaux évolués
1.4.2 *Index tricks*
1.4.3 Sélections avancées
1.4.4 Performances
2 Scipy
2.1 Tour d'horizon
2.2 Quelques exemples complets
3 Matplotlib
3.1 Interface explicite
3.2 Anatomie d'une figure
3.3 Sauvegarde et affichage interactif
3.4 Figure et axes
3.5 Visualisation 3D
Numpy
=====
.. index:: pair: module; numpy
:mod:`numpy` est une bibliothèque *numérique* apportant le support
efficace de larges tableaux multidimensionnels, et de routines
mathématiques de haut niveau (fonctions spéciales, algèbre linéaire,
statistiques, etc.); p.ex.:
>>> x = numpy.linspace(-1, 1, 101) # Tableau 1D (vecteur)
>>> y = numpy.exp(-x**2 / 2) # Manipulation de tableaux
>>> ratio = image1 / image2 - 1 # Opération sur les tableaux
>>> image -= image.mean(axis=0) # Soustraction de la moyenne par colonne
L'objectif est non seulement de simplifier l'écriture du code en
utilisant des expressions « vectorielles » (sans boucle explicite),
mais d'exécuter ces calculs à la vitesse d'un langage compilé (le C
dans le cas présent).
.. note::
* La convention d'import utilisée est
>>> import numpy as np
* N'oubliez pas de `citer numpy `_ dans vos
publications et présentations utilisant de façon centrale ces outils.
.. rubric:: Liens:
- :ref:`Numpy User Guide `
- :ref:`Numpy Reference `
Tableaux
--------
.. index:: pair: numpy; ndarray
Un :class:`numpy.ndarray` (généralement appelé `array`) est un tableau
multidimensionnel *homogène*: tous les éléments doivent avoir le même
type, en général numérique. Les différentes dimensions sont appelées
des *axes*, tandis que le nombre de dimensions -- 0 pour un scalaire,
1 pour un vecteur, 2 pour une matrice, etc. -- est appelé le *rang*.
>>> a = np.array([1, 2, 3]) # Création d'un tableau 1D à partir d'une liste d'entiers
>>> a.ndim # Rang du tableau
1 # Vecteur (1D)
>>> a.shape # Format du tableau: par définition, len(shape)=ndim
(3,) # Vecteur 1D de longueur 3
>>> a.dtype # Type des données du tableau
dtype('int32') # Python 'int' = numpy 'int32' = C 'long'
>>> # Création d'un tableau 2D de float (de 0. à 12.) de shape 4×3
>>> b = np.arange(12, dtype=float).reshape(4, 3); b
array([[ 0., 1., 2.],
[ 3., 4., 5.],
[ 6., 7., 8.],
[ 9., 10., 11.]])
>>> b.shape # Nb d'éléments le long de chacune des dimensions
(4, 3) # 4 lignes, 3 colonnes
>>> b.size # Nb *total* d'éléments dans le tableau
12 # Par définition, size = prod(shape)
>>> b.dtype
dtype('float64') # Python 'float' = numpy 'float64' = C 'double'
Création de tableaux
''''''''''''''''''''
.. index:: pair: numpy; array
- :func:`numpy.array`: convertit une liste d'éléments homogènes ou
coercibles
>>> np.array([[1, 2],[3., 4.]]) # Liste de listes d'entiers et de réels
array([[ 1., 2.], # Tableau 2D de réels
[ 3., 4.]])
.. index::
pair: numpy; zeros
pair: numpy; ones
pair: numpy; full
- :func:`numpy.zeros` (resp. :func:`numpy.ones` et
:func:`numpy.full`): crée un tableau de format donné rempli de zéros
(resp. de uns et d'une valeur fixe)
>>> np.zeros((2, 1)) # Shape (2, 1): 2 lignes, 1 colonne, float par défaut
array([[ 0.],
[ 0.]])
>>> np.ones((1, 2), dtype=bool) # Shape (1, 2): 1 ligne, 2 colonnes, type booléen
array([[True, True]], dtype=bool)
>>> np.full((2, 2), np.NaN)
array([[ nan, nan],
[ nan, nan]])
.. index:: pair: numpy; arange
- :func:`numpy.arange`: crée une séquence de nombres, en spécifiant
éventuellement le *start*, le *end* et le *step* (similaire à
:func:`range` pour les listes)
>>> np.arange(10, 30, 5) # De 10 à 30 (exclu) par pas de 5, type entier par défaut
array([10, 15, 20, 25])
>>> np.arange(0.5, 2.1, 0.3) # Accepte des réels en argument, DANGER!
array([ 0.5, 0.8, 1.1, 1.4, 1.7, 2. ])
.. index::
pair: numpy; linspace
pair: numpy; geomspace
pair: numpy; logspace
- :func:`numpy.linspace` (resp. :func:`numpy.geomspace` and
:func:`numpy.logspace`): répartition uniforme (resp. logarithmique)
d'un nombre fixe de points entre un *start* et un *end* (préférable
à :func:`numpy.arange` sur des réels).
>>> np.linspace(0, 2*np.pi, 5) # 5 nb entre 0 et 2π *inclus*, type réel par défaut
array([ 0., 1.57079633, 3.14159265, 4.71238898, 6.28318531])
>>> np.geomspace(0.1, 10, 5) # 5 nb répartis log. entre 0.1 et 10
array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])
>>> np.logspace(-1, 1, 5) # 5 nb répartis log. entre 10**(-1) et 10**(+1)
array([ 0.1 , 0.31622777, 1. , 3.16227766, 10. ])
.. index:: pair: numpy; meshgrid
- :func:`numpy.meshgrid` est similaire à :func:`numpy.linspace` en 2D
ou plus:
>>> # 5 points entre 0 et 2 en "x", et 3 entre 0 et 1 en "y"
>>> x = np.linspace(0, 2, 5); x # Tableau 1D des x, (5,)
array([ 0. , 0.5, 1. , 1.5, 2. ])
>>> y = np.linspace(0, 1, 3); y # Tableau 1D des y, (3,)
array([ 0. , 0.5, 1. ])
>>> xx, yy = np.meshgrid(x, y) # Tableaux 2D des x et des y
>>> xx # Tableau 2D des x, (3, 5)
array([[ 0. , 0.5, 1. , 1.5, 2. ],
[ 0. , 0.5, 1. , 1.5, 2. ],
[ 0. , 0.5, 1. , 1.5, 2. ]])
>>> yy # Tableau 2D des y, (3, 5)
array([[ 0. , 0. , 0. , 0. , 0. ],
[ 0.5, 0.5, 0.5, 0.5, 0.5],
[ 1. , 1. , 1. , 1. , 1. ]])
.. tip:: voir également :data:`numpy.mgrid` dans `Index tricks`_.
.. rubric:: Tableaux aléatoires
.. index:: pair: module; numpy.random
- :func:`numpy.random.rand` crée un tableau d'un format donné de réels
aléatoires dans [0, 1[; :func:`numpy.random.randn` génère un tableau
d'un format donné de réels tirés aléatoirement d'une distribution
gaussienne (normale) standard :math:`\mathcal{N}(\mu=0, \sigma^2=1)`.
.. (μ=0, σ²=1).
Le sous-module :mod:`numpy.random` fournit des générateurs de
nombres aléatoires pour de nombreuses distributions discrètes et
continues.
Manipulations sur les tableaux
''''''''''''''''''''''''''''''
Les `array` 1D sont indexables comme les listes standard. En dimension
supérieure, chaque axe est indéxable indépendamment.
>>> x = np.arange(10) # Rampe 1D
>>> x[1::3] *= -1; x # Modification sur place ("in place")
array([ 0, -1, 2, 3, -4, 5, 6, -7, 8, 9])
.. rubric:: *Slicing*
.. index:: pair: numpy; slicing
Les sous-tableaux de rang < *N* d'un tableau de rang *N* sont appelées
*slices*: le (ou les) axe(s) selon le(s)quel(s) la *slice* a été découpée,
devenu(s) de longueur 1, est (sont) éliminé(s).
>>> y = np.arange(2*3*4).reshape(2, 3, 4); y # 2 plans, 3 lignes, 4 colonnes
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
>>> y[0, 1, 2] # 1er plan (axe 0), 2e ligne (axe 1), 3e colonne (axe 2)
6 # Scalaire, shape *()*, ndim 0
>>> y[0, 1] # = y[0, 1, :] 1er plan (axe 0), 2e ligne (axe 1)
array([4, 5, 6, 7]) # Shape (4,)
>>> y[0] # = y[0, :, :] 1er plan (axe 0)
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]]) # Shape (3, 4)
>>> y[0][1][2] # = y[0, 1, 2] en ~4× plus lent (slices successives)
6
>>> y[:, -1] # = y[:, 2, :] Dernière slice selon le 2e axe
array([[ 8, 9, 10, 11],
[20, 21, 22, 23]]) # Shape (2, 4)
>>> y[..., 0] # = y[:, :, 0] 1re slice selon le dernier axe
array([[ 0, 4, 8],
[12, 16, 20]]) # Shape (2, 3)
>>> # On peut vouloir garder explicitement la dimension "tranchée"
>>> y[..., 0:1] # 1re slice selon le dernier axe *en gardant le rang originel*
array([[[ 0],
[ 4],
[ 8]],
[[12],
[16],
[20]]])
>>> y[..., 0:1].shape
(2, 3, 1) # Le dernier axe a été conservé, il ne contient pourtant qu'un seul élément
.. rubric:: Modification de format
.. index::
pair: numpy; reshape
pair: numpy; ravel
:meth:`numpy.ndarray.reshape` modifie le format d'un tableau sans
modifier le nombre total d'éléments:
>>> y = np.arange(6).reshape(2, 3); y # Shape (6,) → (2, 3) (*size* inchangé)
array([[0, 1, 2],
[3, 4, 5]])
>>> y.reshape(2, 4) # Format incompatible (*size* serait modifié)
ValueError: total size of new array must be unchanged
:meth:`numpy.ndarray.ravel` « déroule » tous les axes et retourne un
tableau de rang 1:
>>> y.ravel() # Ordre C par défaut: *1st axis slowest, last axis fastest*
array([ 0, 1, 2, 3, 4, 5]) # Shape (2, 3) → (6,) (*size* inchangé)
>>> y.ravel('F') # Ordre Fortran: *1st axis fastest, last axis slowest*
array([0, 3, 1, 4, 2, 5])
.. index::
pair: numpy; transpose
pair: numpy; rollaxis
:meth:`numpy.ndarray.transpose` transpose deux axes, par défaut les
derniers (raccourci: :attr:`numpy.ndarray.T`):
>>> y.T # Transposition = y.transpose() (voir aussi *rollaxis*)
array([[0, 3],
[1, 4],
[2, 5]])
.. index::
pair: numpy; squeeze
pair: numpy; expand_dims
pair: numpy; newaxis
:meth:`numpy.ndarray.squeeze` élimine tous les axes de dimension 1.
:func:`numpy.expand_dims` ajoute un axe de dimension 1 en position
arbitraire. Cela est également possible en utilisant notation *slice*
avec :data:`numpy.newaxis`.
>>> y[..., 0:1].squeeze() # Élimine *tous* les axes de dimension 1
array([0, 3])
>>> np.expand_dims(y[..., 0], -1).shape # Ajoute un axe de dim. 1 en dernière position
(2, 1)
>>> y[:, np.newaxis].shape # Ajoute un axe de dim. 1 en 2de position
(2, 1, 3)
.. index::
pair: numpy; resize
:func:`numpy.resize` modifie le format en modifiant le nombre total
d'éléments:
>>> np.resize(np.arange(4), (2, 4)) # Complétion avec des copies du tableau
array([[0, 1, 2, 3],
[0, 1, 2, 3]])
>>> np.resize(np.arange(4), (4, 2))
array([[0, 1],
[2, 3],
[0, 1],
[2, 3]])
.. caution:: `np.resize(arr, shape)` (complétion avec des copies de `arr`) est
différent de `arr.resize(shape)` (complétion avec des 0).
.. rubric:: Exercice:
:ref:`matrice`
.. rubric:: Concaténation et *stacking*
.. index::
pair: numpy; concatenate
pair: numpy; hstack
pair: numpy; vstack
pair: numpy; dstack
pair: numpy; stack
:func:`numpy.concatenate` et :func:`numpy.stack` permet de regrouper
des tableaux selon des axes différents. Il existe en outre trois
empilements pré-définis:
>>> a = np.arange(6).reshape(2, 3); a
array([[0, 1, 2],
[3, 4, 5]])
>>> np.hstack((a, a)) # Stack horizontal (le long des colonnes) = np.c_[a, a]
array([[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]])
>>> np.vstack((a, a)) # Stack vertical (le long des lignes) = np.r_[a, a]
array([[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]])
>>> np.dstack((a, a)) # Stack en profondeur (le long des plans)
array([[[0, 0],
[1, 1],
[2, 2]],
[[3, 3],
[4, 4],
[5, 5]]])
>>> np.dstack((a, a))[..., 0]
array([[0, 1, 2],
[3, 4, 5]])
.. rubric:: *Broadcasting*
.. index::
pair: numpy; broadcasting
pair: numpy; newaxis
L':ref:`array broadcasting ` définit les régles selon
lesquelles deux tableaux de formats *différents* peuvent éventuellement
s'apparier.
1. Deux tableaux de même rang sont compatibles (*broadcastable*) si, pour chaque
axe, soit les tailles sont égales, soit l'une d'elles est exactement égale
à 1. P.ex. (5, 3) et (1, 3) sont des formats *broadcastable*, (5, 3) et
(5, 1) également, mais (5, 3) et (3, 1) ne le sont pas.
2. Si un tableau a un axe de taille 1, le tableau sera dupliqué à la volée
autant de fois que nécessaire selon cet axe pour attendre la taille de
l'autre tableau le long de cet axe. P.ex. un tableau (2, 1, 3) pourra être
transformé en tableau (2, 5, 3) en le dupliquant 5 fois le long du 2e axe
(`axis=1`).
3. La taille selon chaque axe après *broadcast* est égale au maximum de toutes
les tailles d'entrée le long de cet axe. P.ex. (5, 3, 1) × (1, 3, 4) → (5,
3, 4).
4. Si un des tableaux a un rang (`ndim`) inférieur à l'autre, alors son format
(`shape`) est précédé d'autant de 1 que nécessaire pour atteindre le même
rang. P.ex. (5, 3, 1) × (4,) = (5, 3, 1) × (1, 1, 4) → (5, 3, 4).
>>> a = np.arange(6).reshape(2, 3); a # Shape (2, 3)
array([[0, 1, 2],
[3, 4, 5]])
>>> b = np.array([10, 20, 30]); b # Shape (3,)
array([10, 20, 30])
>>> a + b # Shape (3,) ~ (1, 3) → (2, 3) = (1, 3) copié 2 fois
array([[10, 21, 32],
[13, 24, 35]]) # Shape (2, 3)
>>> c = np.array([10, 20]); c # Shape (2,)
array([10, 20])
>>> a + c # Shape (2,) ~ (1, 2) incompatible avec (2, 3)
ValueError: shape mismatch: objects cannot be broadcast to a single shape
>>> c[:, np.newaxis] # = c.reshape(-1, 1) Shape (2, 1)
array([[10],
[20]])
>>> a + c[:, np.newaxis] # Shape (2, 1) → (2, 3) = (2, 1) copié 3 fois
array([[10, 11, 12],
[23, 24, 25]])
Voir également cette `présentation
`_.
.. rubric:: Indexation évoluée (entiers)
.. index::
pair: numpy; take
>>> a = np.arange(1, 9); a
array([1, 2, 3, 4, 5, 6, 7, 8])
>>> a[[1, -1]] # Sélectionne les éléments 1 et -1
array([2, 8])
>>> a[[[1, 2], [1, 3]]] # Sélectionne les éléments 1, 2, 1, 3
array([[2, 3], # et reformate en (2, 2)
[2, 4]])
>>> np.take(a, [[1, 2], [1, 3]]) # Fonction équivalente
array([[2, 3],
[2, 4]])
.. rubric:: Indexation évoluée (booléens)
.. index::
pair: numpy; nonzero
>>> a = np.linspace(-1, 1, 5); a
array([-1. , -0.5, 0. , 0.5, 1. ])
>>> a >= 0 # Test logique: tableau de booléens
array([False, False, True, True, True], dtype=bool)
>>> (a >= 0).nonzero() # Indices des éléments ne s'évaluant pas à False
(array([2, 3, 4]),) # Indices des éléments >= 0
>>> a[(a >= 0).nonzero()] # Indexation par un tableau d'indices, pas pythonique :-(
array([ 0. , 0.5, 1. ])
>>> a[a >= 0] # Indexation par un tableau de booléens, pythonique :-D
array([ 0. , 0.5, 1. ])
>>> a[a < 0] -= 10; a # = np.where(a < 0, a - 10, a)
array([-11. , -10.5, 0. , 0.5, 1. ])
Opérations de base
''''''''''''''''''
>>> a = np.arange(3); a # Shape (3,), type *int*
array([0, 1, 2])
>>> b = 1. # ~ Shape (), type *float*
>>> c = a + b; c # *Broadcasting*: () → (1,) → (3,)
array([ 1., 2., 3.]) # *Upcasting*: int → float
>>> a += 1; a # Modification *in place* (plus efficace si possible)
array([ 1, 2, 3])
>>> a.mean() # *ndarray* dispose de nombreuses méthodes numériques de base
2.0
.. rubric:: *Ufuncs*
.. index::
pair: numpy; ufuncs
pair: numpy; all
pair: numpy; any
pair: numpy; allclose
:mod:`numpy` fournit de nombreuses fonctions mathématiques de base
(:func:`numpy.exp`, :func:`numpy.atan2`, etc.) s'appliquant directement
sur les éléments des tableaux d'entrée:
>>> x = np.linspace(0, 2*np.pi, 5) # [0, π/2, π, 3π/2, 2π]
>>> y = np.sin(x); y # sin(x) = [0, 1, 0, -1, 0]
array([ 0.00000000e+00, 1.00000000e+00, 1.22460635e-16,
-1.00000000e+00, -2.44921271e-16])
>>> y == [0, 1, 0, -1, 0] # Test d'égalité stricte (élément par élément)
array([ True, True, False, True, False], dtype=bool) # Attention aux calculs en réels (float)!
>>> np.all(np.sin(x) == [0, 1, 0, -1, 0]) # Test d'égalité stricte de tous les éléments
False
>>> np.allclose(y, [0, 1, 0, -1, 0]) # Test d'égalité approximative des éléments
True
.. rubric:: Opérations sur les axes
.. index:: pair: numpy; axis
>>> x = np.random.permutation(6).reshape(3, 2); x # 3 lignes, 2 colonnes
array([[3, 4],
[5, 1],
[0, 2]])
>>> x.min() # Minimum global (comportement par défaut: `axis=None`)
0
>>> x.min(axis=0) # Minima le long de l'axe 0 (i.e. l'axe des lignes)
array([0, 1]) # ce sont les minima colonne par colonne: (*3*, 2) → (2,)
>>> x.min(axis=1) # Minima le long de l'axe 1 (i.e. l'axe des colonnes)
array([3, 1, 0]) # ce sont les minima ligne par ligne (3, *2*) → (3,)
>>> x.min(axis=1, keepdims=True) # Idem mais en *conservant* le format originel
array([[3],
[1],
[0]])
>>> x.min(axis=(0, 1)) # Minima le long des axes 0 *et* 1 (c.-à-d. ici tous les axes)
0
.. rubric:: Opérations matricielles
.. index::
pair: numpy; identity
pair: numpy; dot
pair: numpy; @
pair: module; numpy.linalg
Les opérations de base s'appliquent sur les *éléments* des tableaux,
et n'ont pas une signification matricielle par défaut:
>>> m = np.arange(4).reshape(2, 2); m # Tableau de rang 2
array([[0, 1],
[2, 3]])
>>> i = np.identity(2, dtype=int); i # Tableau "identité" de rang 2 (type entier)
array([[1, 0],
[0, 1]])
>>> m * i # Attention! opération * sur les éléments
array([[0, 0],
[0, 3]])
>>> m @ i # Multiplication *matricielle* = np.matmul(m, i): M × I = M
array([[0, 1],
[2, 3]])
Le sous-module :mod:`numpy.linalg` fournit des outils spécifiques au
calcul matriciel (inverse, déterminant, valeurs propres, etc.).
.. rubric:: Exercices:
:ref:`mad`, :ref:`pull`
Entrées/sorties
---------------
.. index::
pair: numpy; save/load
pair: numpy; savetxt/loadtxt
pair: numpy; genfromtxt
:mod:`numpy` peut lire -- :func:`numpy.loadtxt` -- ou sauvegarder --
:func:`numpy.savetxt` -- des tableaux (uni- ou bi-dimensionnels) dans un simple
fichier ASCII:
>>> x = np.linspace(-1, 1, 100)
>>> np.savetxt('archive_x.dat', x) # Sauvegarde dans le fichier 'archive_x.dat'
>>> y = np.loadtxt("archive_x.dat") # Relecture à partir du fichier 'archive_x.dat'
>>> (x == y).all() # Test d'égalité stricte
True
.. Caution:: :func:`numpy.loadtxt` supporte les types composés, mais ne
supporte pas les données manquantes; utiliser alors la fonction
:func:`numpy.genfromtxt`, plus robuste mais plus lente.
Le format texte n'est pas optimal pour de gros tableaux (ou de rang > 2): il
peut alors être avantageux d'utiliser le format binaire `.npy`, beaucoup plus
compact (mais non *human readable*):
>>> x = np.linspace(-1, 1, 1e6)
>>> np.save('archive_x.npy', x) # Sauvegarde dans le fichier 'archive_x.npy'
>>> y = np.load("archive_x.npy") # Relecture à partir du fichier 'archive_x.npy'
>>> (x == y).all()
True
Il est enfin possible de *sérialiser* les tableaux à l'aide de la
bibliothèque standard :ref:`pickle `.
Sous-modules
------------
:mod:`numpy` fournit en outre quelques fonctionnalités
supplémentaires, parmis lesquelles les sous-modules suivants:
- :mod:`numpy.fft`: *Discrete Fourier Transform*;
- :mod:`numpy.random`: valeurs aléatoires;
- :mod:`numpy.polynomial`: manipulation des polynômes (racines, polynômes
orthogonaux, etc.).
Fonctions avancées
------------------
Tableaux évolués
''''''''''''''''
.. Hint:: Les tableaux *évolués* de :mod:`numpy` sont parfois suffisants, mais
pour une utilisation avancée, il peut être plus pertinent d'invoquer les
bibliothèques dédiées, p.ex. :ref:`Cours/science2:Pandas`.
.. rubric:: Types composés
.. index::
pair: numpy; dtype
pair: numpy; recarray
pair: numpy; genfromtxt
Par définition, tous les éléments d'un tableau *homogène* doivent être du même
type. Cependant, outre les types scalaires élémentaires -- `bool`, `int`,
`float`, `complex`, `str`, etc. -- :mod:`numpy` supporte les types *composés*,
c.-à-d. incluant plusieurs sous-éléments de types différents:
>>> dt = np.dtype([('nom', 'U10'), # 1er élément: une chaîne de 10 caractères unicode
... ('age', 'i'), # 2e élément: un entier
... ('taille', 'd')]) # 3e élément: un réel (double)
>>> arr = np.array([('Calvin', 6, 1.20), ('Hobbes', 5, 1.80)], dtype=dt); arr
array([('Calvin', 6, 1.2), ('Hobbes', 5, 1.8)],
dtype=[('nom', '>> arr[0] # Accès par élément
('Calvin', 6, 1.2)
>>> arr['nom'] # Accès par sous-type
array(['Calvin', 'Hobbes'], dtype='>> rec = arr.view(np.recarray); rec # Vue de type 'recarray'
rec.array([('Calvin', 6, 1.2), ('Hobbes', 5, 1.8)],
dtype=[('nom', '>> rec.nom # Accès direct par attribut
array(['Calvin', 'Hobbes'], dtype='>> dt = np.dtype([('M', 'U3'), # N° catalogue Messier
... ('NGC', 'i'), # N° New General Catalogue
... ('Type', 'U2'), # Code type
... ('Mag', 'f'), # Magnitude
... ('Size', 'f'), # Taille [arcmin]
... ('Distance', 'f'), # Distance [pc]
... ('RA', 'f'), # Ascension droite [h]
... ('Dec', 'f'), # Déclinaison [deg]
... ('Con', 'U3'), # Code constellation
... ('Season', 'U6'), # Saison
... ('Name', 'U30')]) # Nom alternatif
>>> messier = np.genfromtxt("Messier.csv", dtype=dt, delimiter=',', comments='#')
>>> messier[1]
('M1', 1952, 'Sn', 8.39999962, 5., 1930., 5.57499981, 22.0170002, 'Tau', 'winter', 'Crab Nebula')
>>> np.nanmean(messier['Mag'])
7.4927273
.. rubric:: Tableaux masqués
.. index:: pair: module; numpy.ma
Le sous-module :mod:`numpy.ma` ajoute le support des tableaux masqués
(*Masked Arrays*). Imaginons un tableau (4, 5) de réels (positifs ou
négatifs), sur lequel nous voulons calculer pour chaque colonne la
moyenne des éléments *positifs* uniquement:
>>> x = np.random.randn(4, 5); x
array([[-0.55867715, 1.58863893, -1.4449145 , 1.93265481, -0.17127422],
[-0.86041806, 1.98317832, -0.32617721, 1.1358607 , -1.66150602],
[-0.88966893, 1.36185799, -1.54673735, -0.09606195, 2.23438981],
[ 0.35943269, -0.36134448, -0.82266202, 1.38143768, -1.3175115 ]])
>>> x[x >= 0] # Donne les éléments >0 du tableau, sans leur indice
array([ 1.58863893, 1.93265481, 1.98317832, 1.1358607 , 1.36185799,
2.23438981, 0.35943269, 1.38143768])
>>> (x >= 0).nonzero() # Donne les indices ([i], [j]) des éléments positifs
(array([0, 0, 1, 1, 2, 2, 3, 3]), array([1, 3, 1, 3, 1, 4, 0, 3]))
>>> y = np.ma.masked_less(x, 0); y # Tableau où les éléments <0 sont masqués
masked_array(data =
[[-- 1.58863892701 -- 1.93265481164 --] # Données
[-- 1.98317832359 -- 1.13586070417 --]
[-- 1.36185798574 -- -- 2.23438980788]
[0.359432688656 -- -- 1.38143767743 --]],
mask =
[[ True False True False True] # Bit de masquage
[ True False True False True]
[ True False True True False]
[False True True False True]],
fill_value = 1e+20)
>>> m0 = y.mean(axis=0); m0 # Moyenne sur les lignes (axe 0)
masked_array(data = [0.359432688656 1.64455841211 -- 1.48331773108 2.23438980788],
mask = [False False True False False],
fill_value = 1e+20) # Le résultat est un *Masked Array*
>>> m0.filled(-1) # Conversion en tableau normal
array([ 0.35943269, 1.64455841, -1. , 1.48331773, 2.23438981])
*Index tricks*
''''''''''''''
.. index::
pair: numpy; r_
pair: numpy; c_
- :data:`numpy.r_` (resp. :data:`numpy.c_`) est un opérateur puissant avec une
notation évoluée (*index tricks*), permettant à la fois la génération
(équivalent à :func:`numpy.arange` et :func:`numpy.linspace`) et la
concaténation (:func:`numpy.concatenate`) le long du 1e axe (resp. du 2e axe):
>>> np.concatenate([[0], np.arange(1, 6, 2), np.zeros(2), np.linspace(1, 2, 3)])
array([0. , 1. , 3. , 5. , 0. , 0. , 1. , 1.5, 2. ])
>>> np.r_[0, 1:6:2, [0]*2, 1:2:3j] # Notez les crochets et les slices
array([0. , 1. , 3. , 5. , 0. , 0. , 1. , 1.5, 2. ])
>>> np.c_[1:6:2, 1:2:3j]
array([[1. , 1. ],
[3. , 1.5],
[5. , 2. ]])
.. index::
pair: numpy; mgrid
pair: numpy; ogrid
- Plus généralement, :data:`numpy.mgrid` permet de générer des rampes d'indices
(entiers) ou de coordonnées (réels) de rang arbitraire avec les *index
tricks*. Équivalent à :func:`numpy.linspace` en 1D et *similaire (mais
différent)* à :func:`numpy.meshgrid` en 2D.
>>> np.mgrid[:4, 1:6:2] # Grille 2D d'indices (entiers)
array([[[0, 0, 0], # :4 = [0, 1, 2, 3] selon l'axe 0
[1, 1, 1],
[2, 2, 2],
[3, 3, 3]],
[[1, 3, 5], # 1:6:2 = [1, 3, 5] selon l'axe 1
[1, 3, 5],
[1, 3, 5],
[1, 3, 5]]])
>>> np.mgrid[0:2*np.pi:5j] # Rampe de coordonnées (réels): 5 nb de 0 à 2π *inclus*
array([ 0., 1.57079633, 3.14159265, 4.71238898, 6.28318531])
>>> # 3 points entre 0 et 1 selon l'axe 0, et 5 entre 0 et 2 selon l'axe 1
>>> z = np.mgrid[0:1:3j, 0:2:5j]; z
array([[[ 0. , 0. , 0. , 0. , 0. ], # Axe 0 variable, axe 1 constant
[ 0.5, 0.5, 0.5, 0.5, 0.5],
[ 1. , 1. , 1. , 1. , 1. ]],
[[ 0. , 0.5, 1. , 1.5, 2. ], # Axe 0 constant, axe 1 variable
[ 0. , 0.5, 1. , 1.5, 2. ],
[ 0. , 0.5, 1. , 1.5, 2. ]]])
>>> z.shape
(2, 3, 5) # 2 plans 2D (y, x) de 3 lignes (y) × 5 colonnes (x)
>>> np.mgrid[0:1:5j, 0:2:7j, 0:3:9j].shape
(3, 5, 7, 9) # 3 volumes 3D (z, y, x) de 5 plans (z) × 7 lignes (y) × 9 colonnes (x)
- :data:`numpy.ogrid` est similaire à :data:`numpy.mgrid` mais permet de générer
des rampes d'indices ou de coordonnées compactes (*sparse*):
>>> np.ogrid[0:4, 1:6:2] # Rampes 2D d'indices (entiers)
[array([[0],
[1],
[2],
[3]]), array([[1, 3, 5]])]
.. attention:: à l'ordre de variation des indices dans les tableaux
multidimensionnels, et aux différences entre :func:`numpy.meshgrid` et
:data:`numpy.mgrid`/:data:`numpy.ogrid`.
Sélections avancées
'''''''''''''''''''
.. index::
pair: numpy; where
pair: numpy; select
pair: numpy; choose
* :func:`numpy.where` permet de sélectionner entre deux tableaux selon
une *unique* condition:
>>> a = np.arange(10) # [0, 1, ..., 9]
>>> np.where(a < 5, a, 10*a)
array([ 0, 1, 2, 3, 4, 50, 60, 70, 80, 90])
>>> np.where([[True, False],
... [False, True]],
... [[1, 2],
... [3, 4]],
... [[9, 8],
... [7, 6]])
array([[1, 8],
[7, 4]])
>>> y, x = np.ogrid[:3, :4] # y~(3, 1) et x~(1, 4)
>>> np.where(x > y, x, 10 + y) # Broadcast → (3, 4)
array([[10, 1, 2, 3],
[11, 11, 2, 3],
[12, 12, 12, 3]])
* :func:`numpy.select` permet de sélectionner entre plusieurs tableaux selon
*plusieurs* conditions:
>>> x = np.arange(1, 10) # [1, 2, ..., 9]
>>> conditions = [x <= 3, x <= 6, x >= 8] # 3 conditions...
>>> choices = [1, x, x**2] # ...3 choix...
>>> np.select(conditions, choices, 0) # ...et 1 valeur par défaut
array([ 1, 1, 1, 4, 5, 6, 0, 64, 81])
* :func:`numpy.choose` permet de sélectionner entre plusieurs tableaux:
>>> choices = [[ 0, 1, 2, 3], # choix #0
... [10, 11, 12, 13], # choix #1
... [20, 21, 22, 23]] # choix #2
>>> np.choose([0, 2, 1, 0], choices)
array([ 0, 21, 12, 3])
Performances
''''''''''''
.. danger:: *Premature optimization is the root of all evil* -- Donald Knuth
Même si :mod:`numpy` apporte un gain significatif en performance par
rapport à du Python standard, il peut être possible d'améliorer la
vitesse d'exécution par l'utilisation de bibliothèques externes dédiées,
p.ex.:
* :pypi:`numexpr` est un évaluateur optimisé d'expressions numériques:
>>> a = np.arange(1e6)
>>> b = np.arange(1e6)
>>> %timeit a*b - 4.1*a > 2.5*b
100 loops, best of 3: 11.4 ms per loop
>>> %timeit numexpr.evaluate("a*b - 4.1*a > 2.5*b")
100 loops, best of 3: 1.97 ms per loop
>>> %timeit np.exp(-a)
10 loops, best of 3: 60.1 ms per loop
>>> timeit numexpr.evaluate("exp(-a)") # Multi-threaded
10 loops, best of 3: 19.3 ms per loop
* :pypi:`Bottleneck` est une collection de fonctions accélérées, notamment pour
des tableaux contenant des :wen:`NaN` ou pour des statistiques glissantes;
* :pypi:`Theano`, pour optimiser l'évaluation des expressions mathématiques sur
les tableaux :mod:`numpy`, notamment par l'utilisation des :abbr:`GPU
(Graphics Processing Unit)` et de code C généré à la volée.
Voir également :ref:`ProfOpt`.
Scipy
=====
.. index:: pair: module; scipy
:mod:`scipy` est une bibliothèque *numérique* [#sym]_ d'algorithmes et de
fonctions mathématiques, basée sur les tableaux :class:`numpy.ndarray`,
complétant ou améliorant (en termes de performances) les fonctionnalités de
:mod:`numpy`.
.. note:: N'oubliez pas de `citer scipy & Co.
`_ dans vos publications et présentations
utilisant ces outils.
Tour d'horizon
--------------
Voici quelques sous-librairies d'intérêt (il en existe d'autres):
- :mod:`scipy.special`: fonctions spéciales (fonctions de Bessel, erf, gamma,
etc.),
- :mod:`scipy.constants`: constantes mathématiques et physiques (vitesse de la
lumière, :math:`\hbar`, etc.),
- :mod:`scipy.integrate`: intégration numérique (intégration numérique ou
d'équations différentielles),
- :mod:`scipy.differentiate`: différentiation numérique (dérivée, jacobienne,
hessienne),
- :mod:`scipy.optimize`: méthodes d'optimisation (minimisation,
moindres-carrés, zéros d'une fonction, etc.; voir également
:mod:`scipy.odr`),
- :mod:`scipy.interpolate`: interpolation (interpolation, splines),
- :mod:`scipy.fft`: transformées de Fourier,
- :mod:`scipy.linalg`: méthodes d'algèbre linéaire,
- :mod:`scipy.stats`: méthodes statistiques (fonctions et distributions statistiques),
- :mod:`scipy.signal`: traitement du signal (convolution, corrélation,
filtrage, ondelettes, etc.),
- :mod:`scipy.ndimage`: traitement d'images multi-dimensionnelles,
- :mod:`scipy.sparse`: traitement des matrices creuses,
- :mod:`scipy.io`: entrées/sorties.
.. rubric:: Liens:
- :doc:`Scipy Reference `
- :rtfd:`Scipy Cookbook `
.. seealso::
- `Scikits `_ (*mort?*): modules plus
spécifiques étroitement liés à :mod:`scipy`, parmi lesquels:
- `scikit-learn `_: *machine learning*,
- `scikit-image `_: *image processing*,
- `statsmodel `_: modèles statistiques,
- `Scipy Topical softwares `_.
.. rubric:: Exercices:
:ref:`numerique`, :ref:`romberg`, :ref:`rk`
Quelques exemples complets
--------------------------
* :doc:`scipy:tutorial/interpolate` (interpolation, lissage)
* :doc:`scipy:tutorial/integrate` (intégrales numériques, équations
différentielles)
- `odeint notebook `_
- `Zombie Apocalypse
`_
* :doc:`scipy:tutorial/optimize` (moindres carrés, ajustements, recherche de
zéros)
* :doc:`scipy:tutorial/signal` (splines, convolution, filtrage)
* :doc:`scipy:tutorial/linalg` (systèmes linéaires, moindres carrés,
décompositions)
* :doc:`scipy:tutorial/stats` (variables aléatoires, distributions, tests)
.. _matplotlibSec:
Matplotlib
==========
.. index:: pair: module; matplotlib
`Matplotlib `_ est une bibliothèque graphique de
visualisation 2D (avec un support pour la 3D, l'animation et l'interactivité),
permettant des sorties de haute qualité « prêtes à publier ». C'est à la fois
une bibliothèque de *haut niveau*, fournissant des fonctions de visualisation
« clés en main » (échelle logarithmique, histogramme, courbes de niveau, etc.,
voir la :ref:`galerie `), et de *bas niveau*, permettant de
modifier tous les éléments graphiques de la figure (titre, axes, couleurs et
styles des lignes, etc., voir `Anatomie d'une figure`_).
.. note::
* La convention d'import utilisée est
>>> import matplotlib.pyplot as plt
* N'oubliez pas de :doc:`citer matplotlib ` dans
vos publications et présentations utilisant cet outil.
Interface explicite
-------------------
.. index::
pair: matplotlib; pyplot
L'interface à privilégier dans l'utilisation de `matplotlib` est l'interface
*explicite* orientée objet::
fig, ax = plt.subplots() # Création d'une Figure 'fig' contenant un seul Axes 'ax'
x = np.linspace(-np.pi, np.pi, 100)
# Utilisation explicite des méthodes de l'objet 'ax'
ax.plot(x, np.sin(x), c='b', ls='-', label="Sinus") # Trace une courbe bleue trait plein
ax.plot(x, np.cos(x), c='r', ls=':', label="Cosinus") # Trace une courbe rouge pointillée
ax.set_xlabel("x [rad]") # Définit le nom de l'axe des abscisses
ax.set_ylabel("y") # Définit le nom de l'axe des ordonnées
ax.set_title("Sinus et Cosinus") # Définit le titre de la figure
ax.legend() # Ajoute une légende
fig.savefig("simple.png") # Sauvegarde la figure au format PNG
.. image:: simple.*
:align: center
:width: 60%
:alt: Figure simple.
.. attention:: Il existe également une :ref:`interface procédurale
` *implicite*, originellement très similaire à
MATLAB™ et qui doit être réservée (IMO) à l'analyse interactive:
>>> plt.plot(x, np.sin(x), 'b-', label="Sinus") # Courbe bleue trait plein
>>> plt.plot(x, np.cos(x), 'r:', label="Cosinus") # Courbe rouge pointillée
>>> plt.xlabel("x [rad]") # Ajoute le nom de l'axe des x
>>> plt.ylabel("y") # Ajoute le nom de l'axe des y
>>> plt.title("Sinus et Cosinus") # Ajoute le titre de la figure
>>> plt.legend() # Ajoute une légende
>>> plt.savefig("simple.png") # Enregistre la figure en PNG
Par la suite, nous utiliserons uniquement l'interface :abbr:`OO (Orientée
Objet)` plus puissante et explicite, même si beaucoup d'exemples en ligne
utilise encore l'interface implicite.
Anatomie d'une figure
---------------------
L'objet de base est le système d'axes :obj:`matplotlib.axes.Axes`, qui définit
et réalise la plupart des éléments graphiques (tracé de courbes, définition des
axes, annotations, etc.). Un ou plusieurs de ces systèmes d'axes sont
regroupés au sein d'une :obj:`matplotlib.figure.Figure`.
L'interface :abbr:`OO (Orientée Objet)` :mod:`matplotlib.pyplot` donne accès à
tous les éléments d'une figure (titre, axes, légende, etc.), qui peuvent alors
être ajustés (couleur, police, taille, etc.).
.. figure:: https://matplotlib.org/stable/_images/sphx_glr_anatomy_001.png
:align: center
:width: 60%
:target: https://matplotlib.org/stable/gallery/showcase/anatomy.html
:alt: Anatomie d'une figure.
Anatomie d'une figure.
Sauvegarde et affichage interactif
----------------------------------
.. index::
pair: matplotlib; savefig
pair: matplotlib; show
La méthode :meth:`matplotlib.figure.Figure.savefig` permet de sauvegarder la
figure dans un fichier dont le format est automatiquement défini par son
extension, `png` (*raster*), `[e]ps`, `pdf`, `svg` (*vector*), etc., via
différents :ref:`backends `.
Il est également possible d'afficher la figure dans une :doc:`fenêtre
interactive ` avec la commande
:func:`matplotlib.pyplot.show`:
.. image:: simpleShow.*
:align: center
:width: 60%
:alt: Figure simple (fenêtre interactive).
.. Attention::
* Ne pas oublier la commande `plt.show()` en fin d'un script pour un
affichage interactif des figures.
* Utiliser :command:`ipython --pylab` pour l'utilisation interactive des
figures dans une session :program:`ipython` (rien à faire de spécial pour
dans l'interface `jupyter`).
Figure et axes
--------------
.. index::
pair: matplotlib; Axes
pair: matplotlib; Figure
pair: matplotlib; subplots
La méthode la plus simple pour générer simultanément une figure et un ou
plusieurs systèmes axes est d'utiliser la fonction de haut niveau
:func:`matplotlib.pyplot.subplots`, p.ex.::
fig, ax = plt.subplots() # génère une figure et un système d'axes
fig, (axG, axD) = plt.subplots(nrows=1, ncols=2) # 1 fig, 1×2 axes
Ainsi, pour générer une figure contenant 2 (vertical) × 3 (horizontal) = 6
systèmes d'axes (**attention, numérotés de 1 à 6**)::
fig, axs = plt.subplots(nrows=2, ncols=3) # 1 fig, liste de 2×3 axes
for (i, j), ax in np.ndenumerate(axs): # énumeration multi-dimensionnelle
ax.set(xticks=[], yticks=[])
ax.text(0.5, 0.5,
f"axs[{i}][{j}]\n#{i*axs.shape[1] + j + 1}",
ha='center', va='center', size='large')
fig.suptitle("fig, axs = plt.subplots(nrows=2, ncols=3)", fontsize='x-large')
.. image:: subplots.*
:align: center
:width: 60%
:alt: Axes et figure.
Pour un contrôle plus fin de la disposition des systèmes d'axes dans une figure,
il est possible de générer les axes un à un via la méthode
:meth:`matplotlib.figure.Figure.add_subplot`::
fig = plt.figure()
for i in range(1, 4):
ax = fig.add_subplot(2, 3, i, xticks=[], yticks=[])
ax.text(0.5, 0.5, f"subplot(2, 3, {i})",
ha='center', va='center', size='large')
for i in range(3, 5):
ax = fig.add_subplot(2, 2, i, xticks=[], yticks=[])
ax.text(0.5, 0.5, f"subplot(2, 2, {i})",
ha='center', va='center', size='large')
.. image:: subplots2.*
:align: center
:width: 60%
:alt: Axes et figure.
.. index:: pair: matplotlib; GridSpec
Pour des mises en page plus complexes, il est possible d'utiliser des
:doc:`fonctionnalités avancées
`, p.ex.::
from matplotlib.gridspec import GridSpec
fig = plt.figure()
fig.suptitle("grid = GridSpec(2, 3)", fontsize='x-large')
noticks = dict(xticks=[], yticks=[]) # Remove all ticks
options = dict(ha='center', va='center', size='large') # Formatting options
grid = GridSpec(2, 3)
ax1 = fig.add_subplot(grid[0, :-1], **noticks)
ax1.text(0.5, 0.5, "grid[0, :-1]", **options)
ax2 = fig.add_subplot(grid[:, -1], **noticks)
ax3.text(0.5, 0.5, "grid[:, -1]", **options)
ax3 = fig.add_subplot(grid[1, 0], **noticks)
ax3.text(0.5, 0.5, "grid[1, 0]", **options)
ax4 = fig.add_subplot(grid[1, 1], **noticks)
ax4.text(0.5, 0.5, "grid[1, 1]", **options)
.. image:: gridspec.*
:align: center
:width: 60%
:alt: GridSpec.
Enfin, il est toujours possible de créer soi-même le système d'axes dans les
coordonnées relatives à la figure::
fig = plt.figure()
ax0 = fig.add_axes([0, 0, 1, 1], frameon=False,
xticks=np.linspace(0, 1, 11), yticks=np.linspace(0, 1, 11))
ax0.grid(True, ls='--')
ax1 = fig.add_axes([0.1, 0.2, 0.8, 0.6], xticks=[], yticks=[], fc='0.9')
ax1.text(0.5, 0.5, "[0.1, 0.2, 0.8, 0.6]", ha='center', va='center', size='large')
ax2 = fig.add_axes([0.2, 0.3, 0.4, 0.1], xticks=[], yticks=[], fc='0.8')
ax2.text(0.5, 0.5, "[0.2, 0.3, 0.4, 0.1]", ha='center', va='center', size='large')
.. image:: axes.*
:align: center
:width: 60%
:alt: Axes.
.. rubric:: Liens:
- :doc:`User's Guide `
- :ref:`Gallery `
- `Tutorial matplotlib
`_
- `Tutoriel matplotlib
`_ |fr|
.. seealso::
- :doc:`Third-party and user-contributed packages
`, une liste de bibliothèques
complétant `matplotlib`: statistiques, cartographie, mise en œuvre de la
`Grammar of Graphics
`_,
graphiques spécialisés, animations et interactivité, etc. ;
- :pypi:`seaborn`, une librairie de visualisation basée sur `matplotlib` et
incluant de nombreuses fonctions de haut niveau (cf. la `galerie
`__) ;
- :pypi:`mpld3`, un *backend* `matplotlib` interactif basé sur la
bibliothèque *javascript* `D3.js `_;
- :pypi:`plotly`, une bibliothèque graphique *web-based* de haut niveau
alternative à `matplotlib`, basée sur la bibliothèque *javascript*
`Plotly.js `_, elle même notamment fondée
sur `D3.js`_ ;
- :pypi:`bokeh`, une autre bibliothèque graphique *web-based* (idéale pour
les *dashboards*) ;
- :pypi:`plotext`, pour réaliser des figures directement dans le terminal!
De façon plus générale, cherchez votre bonheur dans `The Python Graph
Gallery `_!
.. rubric:: Exemples:
:ref:`figure.py `, :ref:`filtres2ndOrdre.py `
.. rubric:: Exercices:
:ref:`anscombe`, :ref:`julia`, :ref:`logistique`
Visualisation 3D
----------------
.. index:: pair: matplotlib; mplot3d
Matplotlib fournit d'emblée une interface :doc:`mplot3d
` pour des figures 3D assez simples:
.. figure:: https://matplotlib.org/stable/_images/sphx_glr_contourf3d_001.png
:align: center
:width: 60%
:target: https://matplotlib.org/stable/_images/sphx_glr_contourf3d_001.png
:alt: Exemple de figure matplotlib 3D.
Exemple de figure matplotlib 3D.
.. index:: pair: module; mayavi/mlab
Pour des visualisations plus complexes, `mayavi.mlab
`_ est une bibliothèque
graphique de visualisation 3D s'appuyant sur `Mayavi
`_.
.. figure:: http://docs.enthought.com/mayavi/mayavi/_images/example_mri.jpg
:align: center
:width: 40%
:target: http://docs.enthought.com/mayavi/mayavi/auto/example_mri.html
:alt: Imagerie par résonance magnétique.
Imagerie par résonance magnétique.
**Voir également:**
- `pyvista `_: *3D plotting and mesh analysis
through a streamlined interface for the Visualization Toolkit (VTK)*.
.. rubric:: Notes de bas de page et références bibliographiques
.. [#sym] Python dispose également d'une bibliothèque de calcul *formel*,
`sympy `_, et d'un environnement de calcul
mathématique, `sage `_.
.. |fr| image:: ../_static/france_flag_icon.png
:alt: Fr
.. |en| image:: ../_static/uk_flag_icon.png
:alt: En