Travaux pratiques - Quelques éléments du langage Python¶
(correspond à 2 séances de TP)
Références externes utiles :
Les travaux pratiques emploient le langage Python et cette première séance a pour premier objectif de vous le faire découvrir. Vous pouvez, par ailleurs, trouver de (très) nombreux tutoriels Python sur Internet, en français ou en anglais (ou d’autres langues).
Parmi les caractéristiques notables du langage Python nous pouvons mentionner :
langage de programmation objet où tout (ou presque) est objet ;
typage fort : la conformité aux types est vérifiée, les conversions implicites de type ne sont pas autorisées ;
typage dynamique : le type est déterminé à l’exécution, une déclaration explicite du type d’une variable n’est pas nécessaire ;
gestion automatique de la mémoire par un mécanisme de « ramasse-miettes » ;
sensible à l’indentation : les structures de contrôle n’emploient pas de délimiteurs (comme
begin
etend
, ou{
et}
), tout est basé sur l’indentation (par ex. multiple de 4 espaces)sensible à la casse : différence entre majuscules et minuscules.
Si vous maîtrisez déjà les bases de Python, vous pouvez aller directement à la séance d’introduction à Scikit-learn.
En complément de cette introduction, vous pouvez suivre le tutoriel Python officiel en français.
Python 2 ou Python 3 ?¶
Depuis janvier 2020, la version 2 du langage Python n’est plus maintenue. Nous emploierons et vous recommendons Python 3 dans les TP.
Interpréteur Python¶
Python est un langage interprété pouvant s’exécuter sur diverses plateformes.
Si vous travaillez sur les machines des salles informatiques du CNAM, vous n’avez rien à faire, tout est déjà installé.
Si vous réalisez ces TP sur votre machine personnelle, vous pouvez consulter les instructions d’installation de scikit-learn pour installer les bons logiciels.
Il faut a minima un interpréteur Python (accessible via la commande python3
dans un terminal Linux/MacOS ou une console Windows). Pour plus de confort, nous recommandons également un environnement de programmation tel que Spyder ou Jupyter.
Note
Si vous êtes auditeur du CNAM, vous pouvez accéder à un serveur Jupyter en ligne en passant par Moodle (Mes enseignements > NFE205 > Accès au JupyterHub).
Vous pouvez alors y importer n’importe quel TP en Python. Pour ce faire, téléchargez le cahier (notebook) Jupyter du TP (clic droit > Enregistrer sous) à partir du bandeau en haut de l’énoncé. Dans JupyterHub, cliquez sur MonDossier/ puis sur le bouton de téléversement des fichiers pour importer le notebook Jupyter.
Documentation, aide en ligne de commande¶
L’interpréteur Python peut vous renvoyer de la documentation et de l’aide de différentes façons.
Par exemple, définissons un couple et examinons-le. La fonction native print
permet d’afficher le contenu d’une variable :
t = (8,9)
print(t)
Si l’on veut savoir quelles sont les méthodes de l’objet t
, on peut utiliser la fonction dir
:
dir(t)
# Affiche ['__add__', '__class__', ... 'count', 'index']
Pour avoir plus d’explications sur ces méthodes, il est possible d’appeler la fonction help
:
help(t)
# Affiche :
# Help on tuple object:
#
# class tuple(object)
# | tuple() -> empty tuple
# | tuple(iterable) -> tuple initialized from iterable's items
# |
# | If the argument is a tuple, the return value is the same object.
# |
# | Methods defined here:
# |
# | __add__(self, value, /)
# | Return self+value.
# |
# | ...
# |
# | __getattribute__(self, name, /)
En ligne de commande, vous pouvez appuyer sur la touche Espace
pour passer à la page suivante et sur la touche q
pour quitter l’aide (comme pour la commande man
de linux).
Une fois entré dans l’aide avec help()
, il est possible de demander une aide spécifique, par exemple d’avoir la liste des modules Python présents :
help()
# help> modules
# ...
# help> quit
La fonction id()
permet d’obtenir l’identifiant de l’objet dans l’interpréteur Python. Deux objets différents ont un identifiant différent.
print(id(t))
Entrez
t = ("3","4")
print(id(t))
Question :
Est-ce le même id
que plus haut ? Que s’est-il passé ?
Note
Les lignes commençant par #
sont des commentaires. Elles ne sont pas interprétées par Python mais servent à expliquer ou documenter le code.
Il est possible d’écrire des commentaires multilignes en les encadrant par """
.
La fonction print
permet d’afficher le contenu d’une variable dans le terminal. Par exemple :
x = 1276.99
print("Voici mon message")
print(x)
Il est possible d’injecter le contenu d’une variable dans une chaîne de caractères en utilisant la fonction format
:
# .format() va remplacer le contenu des accolades
message = "x vaut {}".format(x)
print(message)
On peut également utiliser le format court (f-strings, Python 3.6 et ultérieur) :
message = f"La variable x vaut {x}"
print(message)
Syntaxe¶
L’affectation de valeurs à une variable (ou l’association d’objets à des noms) est faite en utilisant le signe =
. Pour la vérification de l’égalité on employe deux signes =
collés, ==
. Les commentaires sur une seule ligne commencent par #
, les commentaires multi-lignes commencent et se terminent par """
.
Types de base¶
Différents types de base sont disponibles dans Python. Pour la plupart de ces types de base, dont bool
(booléen), int
(entier), float
(réel), str
(chaîne de caractères), tuple
(vecteur), les « variables » qui les possèdent sont immuables (immutable) : il n’est pas possible de changer leur valeur ! En revanche, il est possible de donner le même nom de variable à une nouvelle donnée, comme dans l’exemple ci-dessus que nous reprenons :
t = (8,9)
print(id(t))
t = ("3","4")
print(id(t))
Lors de ce qui ressemble à l’affectation d’une nouvelle valeur à une variable, t = ("3","4")
, nous avons en fait donné le même nom t
à un nouvel objet ("3","4")
, les identifiants de l’ancien t
et du nouveau t
sont donc naturellement différents.
entier = 1
reel = 3.14
chaine = "pi"
Un tuple peut être composé de données de types différents :
v = (entier, reel, chaine)
print(v)
# (1, 3.14, 'pi')
print(type(v))
# <class 'tuple'>
La fonction type()
permet d’examiner le type d’un objet.
Question
Affichez le type des variables entier
, reel
et chaine
.
Nous remarquerons enfin que les délimiteurs d’une chaîne de caractères peuvent être soit les "
, soit les '
; ce qui compte c’est la cohérence entre le début et la fin de la chaîne :
s = "toto"
print(s)
# 'toto'
s = 'toto'
print(s)
# 'toto'
s = "toto' # Erreur, il faut utiliser les mêmes guillemets pour ouvrir et fermer la chaîne !!
Il est possible de forcer la conversion d’un type à un autre en appelant la fonction du même :
nombre = 1764
chaine = str(1764)
print(nombre, type(nombre))
print(chaine, type(chaine))
chaine = "42.3"
nombre = float(chaine)
print(chaine, type(chaine))
print(nombre, type(nombre))
Listes¶
Les types de base modifiables (mutable) sont les listes et les dictionnaires. Une liste est un tableau unidimensionnel, dont les éléments peuvent avoir des types différents (et peuvent être des listes à leur tour) ; le premier élément est identifié par l’indice 0
, le dernier par l’indice -1
. Les exemples ci-dessous illustrent également l’utilisation des :
pour indiquer des intervalles d’indices. Ces mêmes règles concernant les indices sont valables pour les tuples.
liste = [entier, reel, chaine]
print(liste)
# [1, 3.14, 'pi']
print(type(liste))
# <class 'list'>
print(liste[0])
# 1
print(liste[2])
# 'pi'
print(liste[3]) # Erreur !
Les indices négatifs permettent d’accéder aux éléments en commençant par la fin de la liste :
print(liste[-1])
# 'pi'
print(liste[-2])
# 3.14
Le symbole :
permet d’effectuer du slicing, c’est-à-dire de découper des sous-listes :
print(liste[:1]) # Extrait la sous-liste de tous les éléments jusqu'à 1 (exclu)
# [1]
print(liste[:2]) # Extrait la sous-liste de tous les éléments jusqu'à 2 (exclu)
# [1, 3.14]
print(liste[:-1]) # Extrait la sous-liste jusqu'au dernier élément (exclu)
# [1, 3.14]
print(liste[:3])
# [1, 3.14, 'pi']
print(liste[0:]) # Extrait la sous-liste à partir du premier élément (inclus)
# [1, 3.14, 'pi']
print(liste[1:]) # Extrait la sous-liste à partir du deuxième élément (inclus)
# [3.14, 'pi']
Les listes disposent de plusieurs méthodes utiles (regardez-les avec help(liste)
), parmi lesquelles :
# Concaténation de listes
print(liste + liste)
# [1, 3.14, 'pi', 1, 3.14, 'pi']
# Longueur d'une liste
len(liste)
# Minimum et maximum d'une liste
print(min([3, 10, 1, 5, 24]))
print(max([1.24, -12.0, 5.99]))
# Attention, erreur si les éléments ne sont pas comparables :
print(min(['pi', '12', 3, 1]))
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: '<' not supported between instances of 'int' and 'str'
# Nombre d'occurrences d'un élément dans une liste
liste = [3.14, 12, 0, 3.14, 9, 3.14, 7]
liste.count(3.14)
# Tri d'une liste en ordre croissant
liste.sort()
print(liste)
# Avec copie
liste2 = sorted(liste)
# Suppression d'un élément de la liste
del liste[1]
print(liste)
# Cherche la position de l'élément dans la liste
print(liste.index(3.14))
# 1
print(liste.index(9))
# 4
# Renverse une liste
liste.reverse()
print(liste)
# [12, 9, 7, 3.14, 3.14, 0]
Considérons une nouvelle liste :
liste = [0, 2, 1, 9, 4, 5, 6, 8, 7, 3]
Question
En utilisant les fonctions vues ci-dessus, transformez liste
pour obtenir les chiffres de 1 à 9 dans l’ordre décroissant.
Dictionnaires¶
Les dictionnaires sont des tableaux associatifs, autrement dit des ensembles de paires {clé : valeur}
, par exemple :
cours = { 'RCP208' : 2016, 'RCP209' : 2017, 'RCP216' : 2017 }
print(cours)
# {'RCP216': 2017, 'RCP208': 2016, 'RCP209': 2017}
print(cours['RCP209'])
# 2017
cours['RCP210'] # Erreur ! La clé n'existe pas
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# KeyError: 'RCP210'
Il est possible de mélanger les types dans un dictionnaire (dans les clés et dans les valeurs), ainsi que d’avoir des valeurs qui sont des tuples ou des listes (ou des listes de listes, etc.) :
dico = { 1 : 'toto', 'titi' : 10, 1000 : (1,2,3) }
dico = { 1 : 'toto', 'titi' : 10, 1000 : [(1,2,3),['A','b']]}
Plusieurs méthodes spécifiques aux dictionnaires peuvent être utiles, par exemple :
# Obtenir les clés du dictionnaire
print(dico.keys())
# dict_keys(['titi', 1, 1000])
# Obtenir les valeurs du dictionnaire
print(dico.values())
# dict_values([10, 'toto', [(1, 2, 3), ['A', 'b']]])
# Obtenir les paires (clé, valeur)
print(dico.items())
# dict_items([('titi', 10), (1, 'toto'), (1000, [(1, 2, 3), ['A', 'b']])])
Question :
Que retourne chacune de ces méthodes ?
Pour tenter de clarifier la différence entre ce qui est immuable et ce qui est modifiable, prenons l’exemple suivant :
valeur = 1
liste = []
print(liste)
# []
liste.append(valeur)
print(liste)
# [1]
meme_liste = liste
print(meme_liste)
# [1]
meme_liste.append(3)
valeur = 2
print(valeur)
# 2
print(liste)
# [1, 3]
print(meme_liste)
# [1, 3]
Comment expliquer les résultats ? Rappelons d’abord que les « variables » de type int
sont immuables, alors que celles de type list
sont modifiables.
Dans la première ligne (valeur = 1
), une association est faite entre le nom valeur
et un objet qui est l’entier 1
. La seconde ligne (liste = []
) fait une association entre le nom liste
et un objet qui est une liste vide. L’appel de la méthode append
dans liste.append(valeur)
modifie cet objet liste vide en lui ajoutant l’objet désigné par le nom valeur
. Dans meme_liste = liste
on ajoute un nouveau nom pour l’objet désigné par liste
.
L’appel meme_liste.append(3)
modifie l’objet désigné par meme_liste
(et aussi par liste
) en lui ajoutant l’objet 3
(un entier). Avec valeur = 2
, le nom valeur
n’est plus associé à l’objet auquel il était associé avant (un entier 1
) mais à un nouvel objet (un entier 2
) ; cela ne peut avoir d’impact sur l’objet désigné par liste
ou meme_liste
, qui est une liste (vide au départ, ensuite un objet 1
lui a été ajouté, et enfin un objet 3
).
Ceci devrait permettre de comprendre aussi le passage de paramètres aux Fonctions, fonctions anonymes.
Structures de contrôle¶
Pour le contrôle de l’exécution d’un programme il est possible d’utiliser les tests de conditions avec if
/ elif
/ else
et les boucles for
ou while
. Remarquer l’utilisation de :
avant les lignes qui exigent un niveau d’indentation supérieur (et obligatoire !).
Les boucles for
permettent de parcourir n’importe quel itérable, typiquement des listes :
liste = range(6)
for valeur in liste:
if valeur in [0, 2, 4]:
# "break" quitte la boucle sans considérer "else"
# "continue" démarre l'itération suivante
continue
else:
print(valeur,' : impaire')
#1 : impaire
#3 : impaire
#5 : impaire
Les boucles while
s’exécutent tant qu’une condition est vérifiée :
i = 5
while i > 0:
print(i)
i = i-1
Question
À l’aide d’une boucle, écrire un code qui affiche les carrés des nombres entiers impairs de 1 à 13.
Il n’y a pas d’équivalent de switch
en Python, il faut employer if
/ elif
/ else
:
if liste[1] == 0:
print('liste[1] est 0')
elif liste[1] == 1:
print('liste[1] est 1')
else:
print('?')
Création et manipulation de listes avec la compréhension de liste (list comprehension) :
l1 = [1, 2]
l2 = [1, 2, 3]
[x * y for x in l1 for y in l2]
Forme équivalente :
print([x * y for x in l1 for y in l2])
# [1, 2, 3, 2, 4, 6]
Autres exemples :
print([x * y for x in l1 if x > 1 for y in l2])
# [2, 4, 6]
print(any([i % 3 for i in [x * y for x in l1 for y in l2]]))
# True
Question :
Expliquez ce que fait chacun de ces exemples.
Question :
À partir des listes suivantes de mois et respectivement de nombres de jours correspondants, construisez un dictionnaire qui associe à chaque mois son nombre de jours (années non bisextiles).
mois = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']
nbJours = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
Fonctions, fonctions anonymes¶
La définition d’une fonction commence par def
. Une fonction peut avoir zéro, un ou plusieurs arguments. Les arguments optionnels possèdent des valeurs par défaut. Une fonction peut retourner une valeur (ou un tuple, donc plusieurs valeurs) avec return
ou non.
Le premier exemple ci-dessous devrait aider à comprendre la transmission des paramètres :
def fonction(immuable, modifiable, optionnel='valeur par défaut'):
immuable = 'new'
modifiable.append(4)
print(immuable, modifiable, optionnel)
chaine1 = 'old'
liste = [1, 2]
chaine2 = 'nouvelle valeur'
fonction(chaine1, liste, chaine2)
# new [1, 2, 4] nouvelle valeur
Notre variable liste
a été modifiée à l’intérieur de la fonction mais pas la variable chaine1
car les chaînes de caractère sont immuables :
print(liste)
# [1, 2, 4]
print(chaine1)
# 'old'
fonction(chaine1, liste)
# new [1, 2, 4, 4] valeur par défaut
L’utilisation de print
plutôt que l’appel direct permet de voir que la fonction ne retourne rien (None
) :
print(fonction(chaine1, liste, chaine2))
# new [1, 2, 4, 4, 4] nouvelle valeur
# None
Ici, liste
est un nom associé à un objet liste, donc modifiable ; à chaque appel, la fonction ajoute un élément à cet objet. Le nom chaine1
est associé à un objet de type chaîne de caractères, donc immuable ; dans la fonction, le nom chaine1
est localement associé à un nouvel objet, la chaîne 'new'
, mais cela n’a pas d’impact sur l’association de chaine1
à l’objet 'old'
à l’extérieur de la fonction.
L’exemple suivant montre une fonction qui retourne un tuple :
def fonction2(immuable, modifiable, optionnel='valeur par défaut'):
immuable = 'new'
modifiable.append(4)
return immuable, modifiable, optionnel
chaine1 = 'old'
liste = [1, 2]
chaine2 = 'nouvelle valeur'
print(fonction2(chaine1, liste, chaine2))
# ('new', [1, 2, 4], 'nouvelle valeur')
Question
Écrire une fonction dernier_chiffre(x)
qui prend en entier un nombre à virgule x
et renvoie son dernier chiffre. Par exemple, dernier_chiffre(0.98)
renvoie 8 et dernier_chiffre(100002)
renvoie 2.
Les fonctions lambda ou fonctions anonymes comportent une seule instruction, par exemple :
fAdHoc = lambda x, y: x + y
print(fAdHoc(2,3))
# 5
Cette définition est une forme abrégée de
def fAdHoc(x, y): return(x + y)
Il n’est pas indispensable de donner un nom à une fonction lambda :
(lambda x, y: x + y)(4,6)
# 10
Une fonction lambda sans nom est qualifiée d’anonyme.
Question
Écrire une fonction lambda qui renvoie la plus grande des deux valeurs numériques passées en entrée.
Exceptions¶
Python propose un traitement standard des exceptions. L’exemple suivant indique la syntaxe à employer.
def autreFonction(n):
try: # instruction susceptible de lever une exception
print(1 / n)
except ZeroDivisionError: # si l'exception est levée
print('Division par 0 !')
else: # sinon
pass
finally:
# exécuté après le bloc "try" et le
# traitement des éventuelles erreurs
print(' fini...')
autreFonction(1)
autreFonction(0)
Question
Ajouter une exception au code suivant de sorte à ce qu’il affiche « Ce mot est absent » si la clé n’existe pas dans le dictionnaire.
d = {"titi" : "i", "tata": "a", "tutu": "u"}
mots = ["tata", "tete", "titi", "toto", "tutu"]
for mot in mots:
print(d[mot])
Classes et héritage¶
Il est utile mais pas indispensable de bien connaître la programmation objet pour bien suivre les travaux pratiques, ou pour faire le projet de l’UE RCP209.
En Python, on peut définir des variables partagées par toutes les instances d’une classe. Aussi, l’héritage multiple est possible. La définition d’une classe a la forme suivante (n’entrez pas ceci dans l’interpréteur Python, ce n’est pas une définition complète) :
class NouvelleClasse():
attribut_de_classe = ...
def __init__(self): # constructeur
# ...
pass
def methode(self, arg1, arg2, arg3): # methode de l'objet
# ...
pass
# ...
class NouvelleClasseHeritage(NouvelleClasse):
# ...
pass
Création d’une nouvelle instance de la classe :
instanceNouvelleClasse = NouvelleClasse()
Entrées et sorties¶
La fonction native open
permet d’ouvrir un flux de données depuis un fichier.
Cette fonction prend deux arguments : un chemin vers le fichier et le mode de lecture (lecture, écriture, binaire, etc.).
Exemple d’accès à un fichier texte :
# Ouverture du fichier en mode écriture ('w' pour 'write')
fichierTexte = open('texte.txt', 'w')
fichierTexte.write('Mon texte à moi\n')
fichierTexte.close()
# Ouverture du fichier en mode lecture
fichierTexte = open('texte.txt')
print(fichierTexte.read())
# Mon texte à moi
fichierTexte.close()
L’instruction with
permet de ne pas avoir à fermer manuellement le fichier en utilisant un gestionnaire de contexte.
# le mode 'a' permet d'ajouter des lignes sans écraser le fichier ('append')
with open('texte.txt', 'a') as fichierTexte:
fichierTexte.write('Et une ligne de plus\n')
with open('texte.txt') as fichierTexte:
print(fichierTexte.read())
# Mon texte à moi
# Et une ligne de plus
Dans ces exemples, les fichiers sont ouverts dans le répertoire dans lequel Python a été lancé. Il est possible de spécifier le chemin avant le nom du fichier.
Question :
Quelle conséquence a la suppression du second argument de la fonction open
?
Enfin, le module pickle
permet de stocker des objets Python dans des fichiers binaires et de les lire.
Utilisation d’un fichier binaire pour enregistrer ou lire des données/objets quelconques :
import pickle
liste = [1, 3.14, 'pi']
fichierBinaire = open('bin.dat', 'wb')
pickle.dump(liste, fichierBinaire)
fichierBinaire.close()
fichierBinaire = open('bin.dat', 'rb')
listeIdentique = pickle.load(fichierBinaire)
print(listeIdentique)
# [1, 3.14, 'pi']
Calculs avec NumPy et SciPy¶
NumPy et SciPy sont des bibliothèques logicielles très utiles dans la résolution de problèmes de modélisation à partir de données. Cette séance de TP vise à vous donner les connaissances de base nécessaires mais est loin de couvrir toutes les fonctionnalités de ces deux librairies. Pour aller plus loin, vous êtes invités à regarder les documentations en ligne indiquées plus haut.
NumPy est une bibliothèque de calcul scientifique dédié à la manipulation de matrices et de tableaux en multiple dimensions. L’utilisation de fonctionnalités de NumPy commence par l’importation de cette librairie
import numpy as np
L’emploi de raccourcis (ici np
plutôt que numpy
) permet de faciliter l’écriture des appels des fonctions de la librairie.
Le type de base dans NumPy est le tableau unidimensionnel ou multidimensionnel composé d’éléments de même type. La classe correspondante est ndarray
, à ne pas confondre avec la classe Python array.array
qui gère seulement des tableaux unidimensionnels (et présente des fonctionalités comparativement limitées).
Principaux attributs de ndarray
:
ndarray.ndim |
dimension du tableau (nombre d’axes) |
ndarray.shape |
tuple d’entiers indiquant la taille dans chaque dimension ;
ex : une matrice à n lignes et m colonnes : |
ndarray.size |
nombre total d’éléments du tableau |
ndarray.dtype |
type de (tous) les éléments du tableau ; il est possible
d’utiliser les types prédéfinis comme |
ndarray.data |
les données du tableau ; en général, pour accéder aux données d’un tableau on passe plutôt par les indices |
Création de tableaux¶
De nombreuses méthodes de création de tableaux sont disponibles. D’abord, un tableau peut être créé à partir d’une liste (ou d’un tuple) Python, à condition que tous les éléments soient de même type (le type des éléments du tableau est déduit du type des éléments de la liste ou tuple). NumPy infère automatiquement le type des éléments de la matrice à partir du types des objets Python.
ti = np.array([1, 2, 3, 4])
print(ti)
# array([1, 2, 3, 4])
print(ti.dtype)
# dtype('int64')
tf = np.array([1.5, 2.5, 3.5, 4.5])
print(tf.dtype)
# dtype('float64')
Note
int64
et float64
signifient que ces nombres sont encodés sur 64 bits (en double précision).
Les listes sont transformées en simples tableaux unidimensionnels, les listes de listes (de même taille) en tableaux bidimensionnels, et ainsi de suite. Par exemple :
tf2d = np.array([[1.5, 2, 3], [4, 5, 6]])
print(tf2d)
# array([[ 1.5, 2. , 3. ],
# [ 4. , 5. , 6. ]])
Le type du tableau peut être indiqué explicitement à la création, des conversions sont effectuées pour les valeurs fournies :
tff = np.array([[1, 2, 3], [4, 5, 6]], dtype=float)
print(tff)
# array([[ 1., 2., 3.],
# [ 4., 5., 6.]])
tfi = np.array([[1.5, 2, 3], [4, 5, 6]], dtype=np.int32)
print(tfi)
# array([[1, 2, 3],
# [4, 5, 6]])
print(tfi.dtype)
# dtype('int32')
On accède aux éléments du tableau NumPy avec la même syntaxe que pour accéder aux éléments d’une liste Python :
t = np.array([1, 2, 3, 4, 5, 6])
print(t[2]) # 3ème élément du tableau
Note
L’indexation des éléments commence à 0.
Pour un tableau multidimensionnel (par exemple, une matrice), il suffit d’indiquer la liste des indices de l’élément recherché :
t = np.array([[1,2,3], [4,5,6]])
print(t[0]) # 1ère ligne
print(t[0, 1]) # élément en 1ère ligne, 2ème colonne
Les tableaux NumPy possèdent plusieurs attributs utiles, comme shape
pour obtenir leur taille ou ndim
pour déterminer leur nombre de dimensions.
print(tfi.shape)
# (2, 3)
print(tfi.ndim)
# 2
print(tfi.size)
# 6
Il est souvent nécessaire de créer des tableaux remplis de 0, de 1, ou dont le contenu n’est pas initialisé. Par défaut, le type des tableaux ainsi créés est float64
.
Question
Créez un tableau de booléens (type bool
) à deux lignes et deux colonnes de sorte à ce que la première ligne contiennent uniquement des valeurs True
et la seconde uniquement des valeurs False
. Vérifiez ses dimensions et son type avec les attributs présentés ci-dessus.
Plutôt que de spécifier manuellement toutes les valeurs du tableau, NumPy dispose de fonctions préintégrées pour générer des tableaux et des matrices courantes.
# Créer un tableau de 3 lignes et 4 colonnes rempli de zéros
tz2d = np.zeros((3,4))
print(tz2d)
# array([[ 0., 0., 0., 0.],
# [ 0., 0., 0., 0.],
# [ 0., 0., 0., 0.]])
# Créer un tableau de 3 lignes et 4 colonnes rempli de uns
tu2d = np.ones((3,4))
print(tu2d)
# array([[ 1., 1., 1., 1.],
# [ 1., 1., 1., 1.],
# [ 1., 1., 1., 1.]])
id2d = np.eye(5)
print(id2d)
# ...
tni2d = np.empty((3,4))
print(tni2d)
# ...
Question :
Expliquez les deux derniers résultats obtenus.
Des tableaux peuvent être initialisés aussi par des séquences générées, par exemple :
ts1d = np.arange(0, 40, 5)
print(ts1d)
# array([ 0, 5, 10, 15, 20, 25, 30, 35])
ts1d2 = np.linspace(0, 35, 8)
print(ts1d2)
# array([ 0., 5., 10., 15., 20., 25., 30., 35.])
ta2d = np.random.rand(3,5)
print(ta2d)
# array([[ 0.80125647, 0.6638199 , 0.77308665, 0.97916913, 0.34123528],
# [ 0.50651726, 0.75819493, 0.20378306, 0.31850636, 0.57625289],
# [ 0.9262173 , 0.80369567, 0.10563466, 0.17166009, 0.87835762]])
Question :
Quel est le résultat de l’appel np.random.rand(3,5)
? Regardez la documentation NumPy.
Question :
Générez le contenu d’un tableau de même taille (3, 5) à partir plutôt d’une loi normale (de moyenne 0 et variance 1).
Les tableaux peuvent être redimensionnés en utilisant reshape
:
tr = np.arange(20)
print(tr)
# array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
# 17, 18, 19])
# Réordonne le tableau en une matrice à 4 lignes et 5 colonnes
tr = tr.reshape(4,5)
print(tr)
# array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
# Réordonne le tableau en une matrice à 2 lignes et 10 colonnes
print(tr.reshape(2,10))
# array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]])
print(tr.reshape(20))
# array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
# 17, 18, 19])
Question
Est-il possible de redimensionner le tableau t
suivant en une matrice à 4 lignes et 3 colonnes ? Si non, combien faudrait-il de lignes ?
t = np.array([0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2])
# complétez...
Affichage des tableaux¶
Les tableaux unidimensionnels sont affichés comme des listes, les tableaux bidimensionnels comme des matrices et les tableaux tridimensionnels comme des listes de matrices :
ta1d = np.random.rand(5)
print(ta1d)
# array([ 0.68431206, 0.22183178, 0.50668827, 0.40924377, 0.35185467])
ta3d = np.random.rand(2,3,5)
print(ta3d)
Si un tableau est considéré trop grand pour être affiché en entier, NumPy affiche le début et la fin, avec des ...
au milieu :
ta2d = np.random.rand(30,50)
print(ta2d)
Accès aux composantes d’un tableau, extraction de parties d’un tableau¶
Nous avons déjà lors du TP précédent vu l’utilisation de :
pour définir des intervalles d’indices dans l’accès à des listes. L’utilisation pour les tableaux unidimensionnels est similaire :
print(tr)
#array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
# 17, 18, 19])
# Accès aux 6 premiers éléments
print(tr[:6])
# array([0, 1, 2, 3, 4, 5])
# Parcours des 6 premiers éléments en avançant de 2 par 2
print(tr[:6:2]) # la dernière valeur indique le pas de l'échantillonnage
# array([0, 2, 4])
Les données ne sont pas copiées de tr
vers un nouveau tableau a
, la modification d’un élément de a
avec a[0] = 3
change aussi le contenu de tr[0]
.
a = tr[:2]
print(a)
# array([0, 1])
a[0] = 3
print(tr)
# array([ 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
# 17, 18, 19])
Pour obtenir une copie il faut utiliser copy()
:
a = tr[:2].copy()
print(a)
# array([3, 1])
a[0] = 0
print(a)
# array([0, 1])
print(tr)
# array([ 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
# 17, 18, 19])
On peut procéder de la même façon pour les tableaux multidimensionnels :
ta2d = np.random.rand(3,5)
print(ta2d)
# array([[ 0.27681055, 0.98760001, 0.44322499, 0.65422127, 0.21553808],
# [ 0.0691005 , 0.09843615, 0.62915029, 0.58226529, 0.28827975],
# [ 0.44979767, 0.30371289, 0.77210828, 0.30087438, 0.95948081]])
print(ta2d[0,0])
# 0.27681054817098449
# Extraction de la première ligne
print(ta2d[0,:])
# array([ 0.27681055, 0.98760001, 0.44322499, 0.65422127, 0.21553808])
print(ta2d[0])
# array([ 0.27681055, 0.98760001, 0.44322499, 0.65422127, 0.21553808])
# Extraction de la première colonne
print(ta2d[:,0])
# array([ 0.27681055, 0.0691005 , 0.44979767])
# Extraction d'une sous-matrice
print(ta2d[:2,:2])
# array([[ 0.27681055, 0.98760001],
# [ 0.0691005 , 0.09843615]])
Question :
Faites l’extraction des colonnes d’indice impaire de ta2d
.
Pour les tableaux multidimensionnels, lors des itérations c’est le dernier indice qui change le plus vite, ensuite l’avant-dernier, et ainsi de suite. Par exemple, pour les tableaux bidimensionels c’est l’indice de colonne qui change d’abord et ensuite celui de ligne (le tableau est lu ligne après ligne) :
for x in ta2d:
print(x)
# [ 0.27681055 0.98760001 0.44322499 0.65422127 0.21553808]
# [ 0.0691005 0.09843615 0.62915029 0.58226529 0.28827975]
# [ 0.44979767 0.30371289 0.77210828 0.30087438 0.95948081]
Lecture et écriture des données d’un tableau¶
Les fonctions de lecture / d’écriture de tableaux depuis /dans des fichiers sont variées, nous regarderons rapidement deux des plus simples et plus rapides car les fichiers de données ont en général des formats assez simples.
La fonction numpy.loadtxt(fname, dtype=<type 'float'>, comments='#', delimiter=None, converters=None, skiprows=0, usecols=None, unpack=False, ndmin=0)
, qui retourne un ndarray
, réalise une lecture à partir d’un fichier texte et est bien adaptée aux tableaux bidimensionnels ; chaque ligne de texte doit contenir un même nombre de valeurs. Les principaux paramètres sont :
fname
: fichier ou chaîne de caractères ; si le fichier a une extension.gz
ou.bz2
, il est d’abord décompressé.
dtype
: type, optionnel,float
par défaut.
comments
: chaîne de caractères, optionnel, indique une liste de caractères employée dans le fichier pour précéder des commentaires (à ignorer lors de la lecture).
delimiter
: chaîne de caractères, optionnel, indique la chaîne de caractères employée pour séparer des valeurs, par défaut “ “ (l’espace).
converters
: dictionnaire, optionnel, pour permettre des conversions.
skiprows
: entier, optionnel, pour le nombre de lignes à sauter en début de fichier (par défaut 0).
usecols
: séquence, optionnel, indique les colonnes à lire ; par ex.usecols = [1,4,5]
extrait la 2ème, 5ème et 6ème colonne ; par défaut toutes les colonnes sont extraites.
unpack
: booléen, optionnel,false
par défaut ; sitrue
, le tableau est transposé.
ndmin
: entier, optionnel, le tableau a au moinsndmin
dimensions ; par défaut0
.
(voir http://docs.scipy.org/doc/numpy/reference/generated/numpy.loadtxt.html)
D’abord, téléchargez le fichier geyser.txt
en cliquant sur ce lien ou en exécutant la commande wget http://cedric.cnam.fr/~crucianm/src/geyser.txt
dans un terminal.
Vérifiez que le fichier a bien été enregistré dans le répertoire où l’interpréteur Python s’exécute (ou dans celui du notebook Jupyter).
geyser = np.loadtxt('geyser.txt', delimiter=' ')
print(geyser.shape)
# (272, 2)
print(geyser[:10,:])
# array([[ 3.6 , 79. ],
# [ 1.8 , 54. ],
# [ 3.333, 74. ],
# [ 2.283, 62. ],
# [ 4.533, 85. ],
# [ 2.883, 55. ],
# [ 4.7 , 88. ],
# [ 3.6 , 85. ],
# [ 1.95 , 51. ],
# [ 4.35 , 85. ]])
Une fonction plus flexible est genfromtxt
, vous pouvez regarder son utilisation dans la documentation.
La fonction numpy.savetxt(fname, X, fmt='%.18e', delimiter=' ', newline='\n', header='', footer='', comments='# ')
permet d’écrire un tableau dans un fichier texte. Les paramètres sont :
fname
: fichier ; si le fichier a une extension.gz
ou.bz2
, il est compressé.
X
: le tableau à écrire dans le fichier texte.
fmt
: chaîne de caractères, optionnel ; indique le formatage du texte écrit.
delimiter
: chaîne de caractères, optionnel, indique la chaîne de caractères employée pour séparer des valeurs, par défaut `` `` (l’espace).
newline
: chaîne de caractères, optionnel, indique le caractère à employer pour séparer des lignes.
header
: chaîne de caractères, optionnel, indique le commentaire à ajouter au début du fichier.
footer
: chaîne de caractères, optionnel, indique le commentaire à ajouter à la fin du fichier.
comments
: caractère à ajouter avant header et footer pour en faire des commentaires ; par défaut#
.
(voir http://docs.scipy.org/doc/numpy/reference/generated/numpy.savetxt.html)
Utilisation :
np.savetxt('geyserv_nouveau.txt', geyser, delimiter=', ')
Dans un terminal, affichez les 10 premières lignes du fichier enregistré avec head geyserv_nouveau.txt
.
Autres opérations d’entrée et sortie :
fromfile(file[, dtype, count, sep])
: Construction d’un tableau à partir d’un fichier texte ou binaire.
fromregex(file, regexp, dtype)
: Construction d’un tableau à partir d’un fichier texte, avec un parseur d’expressions régulières.
genfromtxt()
: Fonction plus flexible pour la construction d’un tableau à partir d’un fichier texte, avec une gestion des valeurs manquantes.
load(file[, mmap_mode, allow_pickle, ...])
: Lecture de tableaux (ou autres objets) à partir de fichiers.npy
,.npz
ou autres fichiers de données sérialisées.
loadtxt(fname[, dtype, comments, delimiter, ...])
: Lecture de données à partir d’un fichier texte.
ndarray.tofile(fid[, sep, format])
: Ecriture d’un tableau dans un fichier texte ou binaire (par défaut).
save(file, arr[, allow_pickle, fix_imports])
: Ecriture d’un tableau dans un fichier binaire de type.npy
.
savetxt(fname, X[, fmt, delimiter, newline, ...])
: Ecriture d’un tableau dans un fichier texte.
savez(file, *args, **kwds)
: Ecriture de plusieurs tableaux dans un fichier de type.npz
sans compression.
savez_compressed(file, *args, **kwds)
: Ecriture de plusieurs tableaux dans un fichier de type.npz
avec compression.
Voir http://docs.scipy.org/doc/numpy/neps/npy-format.html pour le format des fichiers de type .npy
et .npz
.
Opérations simples sur les tableaux¶
Concaténation de tableaux bidimensionnels :
tu2d = np.ones((2,2))
tb2d = np.ones((2,2)) * 2
# Concatène les deux matrices selon leur première dimension (lignes)
tcl = np.concatenate((tu2d,tb2d))
print(tcl)
# array([[ 1., 1.],
# [ 1., 1.],
# [ 2., 2.],
# [ 2., 2.]])
# C'est équivalent à écrire :
tcl = np.concatenate((tu2d,tb2d), axis=0)
# ou encore
tcl = np.vstack((tu2d, tb2d))
Question
En utilisant la fonction .hstack()
, concaténez les tableaux tu2d
et tb2d
selon les colonnes.
Ajouter un tableau unidimensionnel comme colonne à un tableau bidimensionnel. newaxis
permet de créer une nouvelle dimension dans un tableau existant :
from numpy import newaxis
tu1d = np.ones(2)
print(tu1d)
# array([ 1., 1.])
print(tu1d[:,newaxis])
# array([[ 1.],
# [ 1.]])
print(np.column_stack((tu1d[:,newaxis],tb2d)))
# array([[ 1., 2., 2.],
# [ 1., 2., 2.]])
print(np.hstack((tu1d[:,newaxis],tb2d)))
# array([[ 1., 2., 2.],
# [ 1., 2., 2.]])
Opérations arithmétiques élément par élément :
tsomme = tb2d - tu1d
print(tsomme)
# array([[ 1., 1.],
# [ 1., 1.]])
print(tb2d*5)
# array([[ 10., 10.],
# [ 10., 10.]])
print(tb2d**2)
# array([[ 4., 4.],
# [ 4., 4.]])
tc = np.hstack((tb2d,tu1d[:,newaxis]))
print(tc)
# array([[ 2., 2., 1.],
# [ 2., 2., 1.]])
print(tc > 1)
# array([[ True, True, False],
# [ True, True, False]], dtype=bool)
print(tb2d * tb2d)
# array([[ 4., 4.],
# [ 4., 4.]])
tb2d *= 3
print(tb2d)
# array([[ 6., 6.],
# [ 6., 6.]])
tb2d += tu2d
print(tb2d)
# array([[ 7., 7.],
# [ 7., 7.]])
Question
Créez un tableau à 10 lignes et 10 colonnes dont tous les éléments valent 0,5.
NumPy expose également des méthodes pratiques pour extraire des statistiques d’un tableau :
print(tb2d.sum())
# 28.0
geyser = np.loadtxt('geyser.txt', delimiter=' ')
print(geyser.min())
# 1.6000000000000001
print(geyser.max())
# 96.0
print(geyser.sum())
# 20232.676999999996
print(geyser.sum(axis=0))
# array([ 948.677, 19284. ])
print(geyser.sum(axis=1))
...
Algèbre linéaire¶
g0 = geyser[:2,:]
print(g0)
# array([[ 3.6, 79. ],
# [ 1.8, 54. ]])
Transposition d’une matrice :
g0.transpose()
Inversion :
np.linalg.inv(g0)
Produit matriciel :
g0.dot(tu2d) # ou np.dot(g0,tu2d)
g0.dot(np.eye(2))
Trace :
g0.trace()
Résolution de systèmes linéaires :
y = np.array([[5.], [7.]])
np.linalg.solve(g0, y)
Valeurs et vecteurs propres :
vpvp = np.linalg.eig(g0)
print(vpvp)
Cela permet d’obtenir les valeurs propres (chacune répétée suivant sa multiplicité) et les vecteurs propres tels que la colonne vpvp[1][:,i]
est le vecteur propre unitaire (de norme égale à 1) correspondant à la valeur propre vpvp[0][i]
.
Question :
Comment vérifier facilement ce calcul ?
Vectorisation de fonctions¶
Des fonctions Python qui travaillent sur des scalaires peuvent être vectorisées, c’est à dire travailler sur des tableaux, élément par élément. Par exemple :
def addsubtract(a,b):
if a > b:
return a - b
else:
return a + b
print(addsubtract(2,3))
# 5
vec_addsubtract = np.vectorize(addsubtract)
print(tu2d)
# array([[ 1., 1.],
# [ 1., 1.]])
print(g0)
# array([[ 3.6, 79. ],
# [ 1.8, 54. ]])
print(vec_addsubtract(g0,tu2d))
# array([[ 2.6, 78. ],
# [ 0.8, 53. ]])
SciPy¶
SciPy ajoute de très nombreuses fonctions permettant de faire des calculs scientifiques. Il est possible d’importer SciPy en entier avec import scipy
mais il est préférable de se limiter aux fonctionnalités utilisées, par exemple from scipy import linalg
.
scipy.linalg
contient toutes les fonctions de numpy.linalg
plus quelques autres. Par ailleurs, scipy.linalg
est systématiquement compilée avec un support BLAS/LAPACK, alors que pour numpy.linalg
cela n’est pas obligatoire. Les calculs peuvent ainsi être plus rapides si c’est scipy.linalg
qui est utilisée.
Exemples :
from scipy import linalg
print(linalg.det(g0))
# 52.19999999999998
print(linalg.norm(g0)) # par défaut, norme de Frobenius
# 95.776823918941901
print(linalg.norm(g0,'fro'))
# 95.776823918941901
print(linalg.norm(g0,1)) # norme L1
# 133.0
print(linalg.eig(g0))
# (array([ 0.92097563+0.j, 56.67902437+0.j]), array([[-0.99942549, -0.83004523],
# [ 0.03389222, -0.55769609]]))
Statistiques¶
De nombreuses fonctionnalités statistiques sont disponibles dans le module scipy.stats
, comme la génération de valeurs aléatoires suivant différentes distributions, des statistiques descriptives et des tests statistiques.
Regardez les fonctionnalités de scipy.stats
sur http://docs.scipy.org/doc/scipy/reference/tutorial/stats.html.
Question :
Générez en utilisant scipy.stats
une matrice 6 x 6 dont les valeurs sont issues d’une loi normale de moyenne 1 et écart-type 3. Calculez les statistiques descriptives par ligne et par colonne. Que constatez-vous ?
Question :
Calculez les statistiques descriptives par ligne et par colonne pour le tableau geyser
.
Visualisation avec Matplotlib¶
Matplotlib permet de construire de très nombreux types de graphiques directement à partir de l’interpréteur Python et nous sera très utile pour visualiser des données, des fonctions de décision, etc. Nous nous servirons principalement de matplotlib.pyplot
, à importer en début de session :
import matplotlib.pyplot as plt
Construisons d’abord un graphique très simple :
plt.plot(np.random.rand(100)) # données générées aléatoirement
plt.show() # affichage du graphique
L’appel à np.random.rand(100)
a généré un tableau unidimensionnel de 100 valeurs aléatoires suivant une distribution uniforme, chaque valeur a été affichée (l’abscisse est l’indice de la valeur dans le tableau, l’ordonnée est la valeur même) et les valeurs d’indices succesifs ont été reliées entre elles. Cette représentation n’a de sens que si un lien existe entre indices successifs ; par exemple, les données correspondent à une série chronologique et les indices correspondent aux mesures successives.
Utiliser plt.show(block=False)
à la place de plt.show()
permet de continuer à travailler en conservant le graphique. Néanmoins, les appels suivants à plt.plot()
modifieront le même graphique (tant que sa fenêtre graphique n’est pas fermée).
Une alternative est d’utiliser le mode interactif avec plt.ion()
(pour y entrer) et plt.ioff()
pour en sortir. En mode interactif plt.show()
n’est plus nécessaire, le graphique est affiché et modifié directement.
Les données geyser
représentent deux telles séries chronologiques : les durées des éruptions du geyser Old Faithful et les durées des pauses entre éruptions successives. Pour mieux comprendre ce qui est affiché il est nécessaire d’indiquer des étiquettes pour les axes, d’ajouter une légende et éventuellement de donner un titre au graphique. C’est ce que font les instructions suivantes :
plt.plot(geyser)
plt.title('Old Faithful Geyser Data') # intitulé du graphique
plt.xlabel('numéro éruption') # signification axe des x
plt.ylabel('durée éruption/pause (min)') # signification axe des y
plt.legend(('Durée éruption', 'Durée pause'), loc='best')
plt.show()
On observe que chaque colonne du tableau geyser
a été représentée comme une série à part.
Question :
Les deux séries ont des moyennes et des écart-types très différents. Transformez le tableau geyser
pour obtenir une moyenne de 0 et un écart-type de 1 pour chacune de ces séries et affichez-les de nouveau.
Lorsque les observations sont issues de tirages indépendants, il est d’usage de représenter le vecteur correspondant à chaque observation comme un point, éventuellement identifié par son indice. Il est possible de transmettre à la fonction plot
deux tableaux unidimensionnels (de même taille) représentant les coordonnées \(x\) et respectivement \(y\) des données :
plt.plot(geyser[:,0], geyser[:,1])
plt.show()
Le résultat est illisible car par défaut les points d’indices successifs sont reliés entre eux. Il faut modifier l’affichage, en précisant par ex. que l’on souhaite afficher chaque point comme un +
de couleur rouge :
plt.plot(geyser[:,0], geyser[:,1],'r+')
plt.show()
Regardez la description complète de la commande plot
sur http://matplotlib.org/api/pyplot_summary.html.
Question :
Multipliez une des colonnes par 2.5 et affichez le résultat.
Il est également possible d’afficher des nuages de points en 3D en utilisant la commande scatter
:
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(np.random.randn(100), np.random.randn(100), np.random.randn(100))
plt.show()
Le graphique peut être tourné en 3D à l’aide de la souris (bouton gauche maintenu appuyé).
Il est posible d’avoir plusieurs figures ouvertes en même temps si chacune a son propre identifiant (fig1
, fig2
, …) et si les appels à la fonction show()
ne sont pas bloquants (utilisation de plt.show(block=False)
).
Regardez les commandes utilisables pour obtenir des graphiques en 3D sur http://matplotlib.org/mpl_toolkits/mplot3d/tutorial.html.
Question :
En vous servant de la documentation, modifiez la couleur des points et ajoutez des étiquettes sur les axes.
Question :
Complétez le tableau geyser
d’une troisième colonne issue de tirages aléatoires et affichez le résultat sous forme graphique.
Il est possible d’afficher les étiquettes textuelles associées aux observations. Pour cela, il faut d’abord chercher des données adaptées en entrant dans une fenêtre terminal (ou en les téléchargeant depuis ce lien):
wget http://cedric.cnam.fr/~crucianm/src/mammals.csv
Vous pouvez regarder le contenu de ce fichier. Nous sélectionnons pour une visualisation les colonnes suivantes :
durée sommeil sans rêve (h/jour)
durée totale sommeil (h/jour)
index d’exposition durant le sommeil (1-5)
La lecture des données sera faite dans deux tableaux différents, un pour les trois variables numériques et l’autre pour les noms des espèces :
import numpy as np # si pas encore fait
import matplotlib.pyplot as plt # si pas encore fait
mammals248 = np.loadtxt('mammals.csv', delimiter=';', usecols=[2,4,8], skiprows=1)
print(mammals248[:5,:])
noms = np.genfromtxt('mammals.csv', dtype='str', delimiter=';', usecols=[0], skip_header=1)
print(noms)
Pour l’affichage :
from mpl_toolkits.mplot3d import Axes3D # si pas encore fait
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
for i in range(len(noms)):
x,y,z = mammals248[i,0],mammals248[i,1],mammals248[i,2]
ax.scatter(x,y,z)
ax.text(x,y,z,noms[i])
plt.show()
Il est possible d’utiliser le clic droit de la souris pour faire un zoom.
Question :
Pour améliorer la lisibilité, affichez seulement une observation sur deux.