Une variable est une zone de la mémoire de l’ordinateur dans laquelle une valeur est stockée. Aux yeux du programmeur, cette variable est définie par un nom, alors que pour l’ordinateur, il s’agit en fait d’une adresse, c’est-à-dire d’une zone particulière de la mémoire.
En Python, la déclaration d’une variable et son initialisation se font en même temps.
x = 4
x
Dans cet exemple, nous avons déclaré, puis initialisé la variable x avec la valeur 4. Ce qu'il s’est passé plusieurs choses :
Sachez par ailleurs que l’opérateur d’affectation = s’utilise dans un certain sens. Par exemple, l’instruction x = 4 signifie qu’on attribue la valeur située à droite de l’opérateur = (ici, 4) à la variable située à gauche (ici, x). D’autres langages de programmation comme R utilisent les symboles <- pour rendre l’affectation d’une variable plus explicite, par exemple x <- 4.
Enfin, dans l’instruction x = y - 3, l’opération y - 3 est d’abord évaluée et ensuite le résultat de cette opération est affecté à la variable x.
Le type d’une variable correspond à la nature de celle-ci. Les trois principaux types dont nous aurons besoin dans un premier temps sont:
Bien sûr, il existe de nombreux autres types (par exemple, les booléens, les nombres complexes, etc.). Dans l’exemple précédent, nous avons stocké un nombre entier (int) dans la variable x, mais il est tout à fait possible de stocker des floats, des chaînes de caractères (string ou str) ou de nombreux autres types de variable que nous verrons par la suite.
a = 3.14
print(a)
y = "Hello World!"
print(y)
z = '''C'est pas dur de coder!'''
print(z)
Python reconnaît certains types de variable automatiquement (entier, float). Par contre, pour une chaîne de caractères, il faut l’entourer de guillemets (doubles, simples, voire trois guillemets successifs doubles ou simples) afin d’indiquer à Python le début et la fin de la chaîne de caractères.
Dans l’interpréteur, l’affichage direct du contenu d’une chaîne de caractères se fait avec des guillemets simples, quel que
soit le type de guillemets utilisé pour définir la chaîne de caractères.
En Python, comme dans la plupart des langages de programmation, c’est le point qui est utilisé comme séparateur décimal.
Ainsi, 3.14 est un nombre reconnu comme un float en Python alors que ce n’est pas le cas de 3,14.
Le nom des variables peut être constitué de lettres minuscules (a à z), majuscules (A à Z), de chiffres (0 à 9) ou du caractère souligné (_).
Vous ne pouvez pas utiliser d’espace dans un nom de variable. Par ailleurs, un nom de variable ne doit pas débuter par un chiffre et il n’est pas recommandé de le faire débuter par le caractère _ (sauf cas très particuliers).
De plus, il faut absolument éviter d’utiliser un mot « réservé » par Python comme nom de variable (par exemple : print,
range, for, etc.).
Enfin, Python est sensible à la casse, ce qui signifie que les variables TesT, test ou TEST sont différentes.
Les quatre opérations arithmétiques de base +, -, * et / se font de manière simple sur les types numériques (nombres entiers et floats).
x = 4
print(x+6)
print(x-3)
print(x*5)
print(x/2)
y = 18
x*(y-16)+10
Remarquez toutefois que si vous mélangez les types entiers et floats, le résultat est renvoyé comme un float (car ce type est plus général). Par ailleurs, l’utilisation de parenthèses permet de gérer les priorités.
L’opérateur puissance utilise les symboles **.
2**10
Pour obtenir le quotient et le reste d'une division entière, on utilise respectivement les symboles // et modulo %.
print("Rappelons que la division euclidienne 20/7 se décompose comme : 20 = 2*7 + 6 .")
print("Quotient de la division euclidienne : ", 20//7)
print("Reste de la division euclidienne : ", 20%7)
Les symboles +, -, *, /, **, // et % sont appelés opérateurs, car ils réalisent des opérations sur les variables.
Enfin, il existe des opérateurs « combinés » qui effectue une opération et une affectation en une seule étape.
i = 5
i = i+1
i
i = 10
i+=4
i
L’opérateur += effectue une addition puis affecte le résultat à la même variable. Cette opération s’appelle une « incrémentation ». Les opérateurs -=, *= et /= se comportent de manière similaire pour la soustraction, la multiplication et la division.
Enfin, je vous invite à appréhender dès maintenant la fonction round(x) qui arrondit le flottant x en un entier.
round(4.7345)
Vous pouvez aussi utiliser round(x, n) pour arrondir à n nombres après la virgule.
round(3.14159, 2)
Pour les chaînes de caractères, deux opérations sont possibles: l'addition et la multiplication.
chaine = " Salut "
print(chaine)
print(chaine + "Python!")
print(chaine * 4)
L'opérateur d'addition + concatène (assemble) deux chaînes de caractères. L'opérateur de multiplication * entre un nombre entier et une chaîne de caractères duplique (répète) plusieurs fois une chaîne de caractères.
Vous observez que les opérateurs + et * se comportent différemment s’il s’agit d’entiers ou de chaînes de caractères : 2 + 2 est une addition, et "2" + "2" est une concaténation. On appelle ce comportement redéfinition des opérateurs.
Si vous ne vous souvenez plus du type d’une variable, utilisez la fonction type() qui vous le rappellera.
x = 10
print( type(x) )
y = 2.5
print( type(y) )
z = "Hey!"
print( type(z) )
Nous verrons plus tard ce que signifie le mot class.
En programmation, on est souvent amené à convertir les types, c’est-à-dire passer d’un type numérique à une chaîne de caractères ou vice-versa. En Python, rien de plus simple avec les fonctions int(), float() et str().
print( int(34.8) )
print( float(4) )
print( str(55) )
type(3/4) # La division de 2 integers renvoie un float !
Toute conversion d’une variable d’un type en un autre est appelé casting en anglais, il se peut que vous croisiez ce terme si vous consultez d’autres ressources.
Python est un langage dit orienté objet, il se peut que dans la suite du cours nous employions le mot objet pour désigner une variable. Par exemple, « une variable de type entier » sera pour nous équivalent à « un objet de type entier ». Nous verrons ce que le mot « objet » signifie réellement (tout comme le mot « classe »).
Par ailleurs, nous avons rencontré plusieurs fois des fonctions dans ce chapitre, notamment avec type(), int(), float() et str(). Nous avons également vu la fonction print(). On reconnaît qu’il s’agit d’une fonction car son nom est suivi de parenthèses (par exemple, type()).
En Python, la syntaxe générale est fonction(). Ce qui se trouve entre les parenthèses d’une fonction est appelé argument et c’est ce que l’on « passe » à la fonction. Pour l’instant, on retiendra qu’une fonction est une sorte de boîte à qui on passe un argument, qui effectue une action et qui peut renvoyer un résultat ou plus généralement un objet.
Certaines opérations mathématiques sur les variables nécessitent l'appel d'une fonction, par exemple la racine carrée. Pour les utiliser, il vous faut importer le module math.
import math
Voici les fonctions mathématiques spécifiques utilisables avec le module math avec un flottant x quelconque :
print("Racine de 25 = ", math.sqrt(25))
print("4! = ", math.factorial(4))
print("2¹⁰ = ", math.pow(2, 10))
print("exp(2.5) = ", math.exp(2.5))
Le module math permet également des tests :
x = 4
print(math.isinf(x), math.isnan(x))
Enfin le module math produit aussi fonctions trigonométriques suivantes :
t = math.pi / 6
print(math.cos(t))
print(math.sin(t))
print(math.tan(t))
Nous avons rencontré la fonction print() qui affiche une chaîne de caractères (le fameux "Hello world!"). En fait, la fonction print() affiche l’argument qu’on lui passe entre parenthèses et un retour à ligne. Ce retour à ligne supplémentaire est ajouté par défaut. Si toutefois, on ne veut pas afficher ce retour à la ligne, on peut utiliser l’argument par « mot-clé » end.
print (" Hello world !")
print (" Hello world !", end ="")
print (" It's me !")
Ligne 1. On a utilisé l’instruction print() classiquement en passant la chaîne de caractères "Hello world!" en argument.
Ligne 2. On a ajouté un second argument end="", en précisant le mot-clé end. Nous aborderons les arguments par mot-clé
dans le chapitre 9 Fonctions. Pour l’instant, dites-vous que cela modifie le comportement par défaut des fonctions.
Une autre manière de s’en rendre compte est d’utiliser deux fonctions print() à la suite.
Dans la portion de code suivante, le caractère « ; » sert à séparer plusieurs instructions Python sur une même ligne.
print(3*6) ; print( "hey!") ; tartampion = 4 ; print(tartampion)
Il est également possible d’afficher le contenu de plusieurs variables (quel que soit leur type) en les séparant par des virgules.
x = "John a" ; y = 40 ; z = "ans !"
print(x, y, z)
La méthode .format() permet une meilleure organisation de l’affichage des variables.
x = 12
print("J'ai {} euros sur moi.".format(x))
On peut également y mettre plusieurs variables, en précisant l'ordre dans les accolades (le plus petit chiffre est 0!).
x = 12
y = 40
z = 25
print("John a {2} ans, il a {0} euros sur lui et a mangé {1} pommes hier.".format(x, y, z))
Le signe \ en fin de ligne permet de poursuivre la commande sur la ligne suivante. Cette syntaxe est pratique lorsque vous voulez taper une commande longue.
Enfin, il est possible de préciser sur combien de caractères vous voulez qu’un résultat soit écrit et comment se fait l’alignement (à gauche, à droite ou centré). Dans la portion de code suivante, le caractère « ; » sert de séparateur entre les instructions sur une même ligne.
Dans d’anciens livres ou programmes Python, il se peut que vous rencontriez l’écriture formatée avec le style suivant.
x = 32
print("John a %d ans." %x)
nb_G = 4500
nb_C = 2575
prop_GC = ( nb_G + nb_C )/14800
print (" On a %d G et %d C -> prop GC = %f." %(nb_G , nb_C , prop_GC ))
La syntaxe est légèrement différente. Le symbole % est d’abord appelé dans la chaîne de caractères (ci-dessus %d, %d et %.2f) pour :
Le signe % est rappelé une seconde fois (% (nb_G, nb_C, prop_GC)) pour indiquer les variables à formater. Cette ancienne façon de formater une chaîne de caractères vous est présentée à titre d’information. Ne l’utilisez pas dans vos programmes.
Revenons quelques instants sur la notion de méthode abordée dans ce chapitre avec format(). En Python, on peut considérer chaque variable comme un objet sur lequel on peut appliquer des méthodes. Une méthode est simplement une fonction qui utilise et/ou agit sur l’objet lui-même, les deux étant connectés par un point. La syntaxe générale est de la forme objet.méthode().
La fonction len() appliquée à une chaîne de caractères permet de connaître sa longueur (son nombre de lettres).
animaux = "girafe tigre"
len(animaux)
On peut également connaître la lettre en i-ème position avec la localisation [ ], le 0 compte.
animaux[4]
On peut aussi utiliser les tranches (cf. chapitre suivant sur les listes).
animaux[3:10:2]
Il existe certains caractères spéciaux comme \n pour le retour à la ligne. Le caractère \t produit une tabulation.
Si vous voulez écrire des guillemets simples ou doubles et que ceux-ci ne soient pas confondus avec les
guillemets de déclaration de la chaîne de caractères, vous pouvez utiliser \' ou \".
print("Un retour à la ligne \n puis une tabulation \t puis un guillemet \"")
Vous pouvez aussi utiliser astucieusement des guillemets doubles ou simples pour déclarer votre chaîne de caractères.
print ('Python est un "super" langage de programmation')
Quand on souhaite écrire un texte sur plusieurs lignes, il est très commode d’utiliser les guillemets triples qui conservent le formatage.
car = '''souris
chat
abeille'''
car
Une tâche courante en Python est de lire une chaîne de caractères (provenant par exemple d’un fichier), d’extraire des valeurs de cette chaîne de caractères puis ensuite de les manipuler.
On considère par exemple la chaîne de caractères val.
val = "3.4 17.2 atom"
val
On souhaite extraire les valeurs 3.4 et 17.2 pour ensuite les additionner.
Dans un premier temps, on découpe la chaîne de caractères avec la méthode .split() (qu'on va voir dans la partie D.4.).
val2 = val.split()
val2
On obtient alors une liste de chaînes de caractères. On transforme ensuite les deux premiers éléments de cette liste en floats (avec la fonction float()) pour pouvoir les additionner.
float(val2[0]) + float(val2[1])
① Minuscules & Majuscules : Les méthodes .lower() et .upper() renvoient un texte en minuscule et en majuscule respectivement.
car = "CunÉgoNDE!"
print(car.lower(), car.upper())
Vous pouvez aussi utiliser la méthode .capitalize() qui met la majuscule qu'à la première lettre.
quoi = "quoi? "
vous = "vous n'avez jamais vu Les Visiteurs?"
quoi.capitalize()+vous.capitalize()
② Séparations : Il existe une méthode associée aux chaînes de caractères qui est particulièrement pratique, la méthode .split() qui vous rend la liste des mots séparés selon le caractère que vous entrez dans les parenthèses, par défaut les espaces.
vous.split()
vous.split("a")
devise = "Liberté,Égalité,Fraternité"
devise.split(",")
Il est également intéressant d’indiquer à .split() le nombre de fois qu’on souhaite découper la chaîne de caractères avec maxsplit.
animaux = "girafe tigre singe souris"
animaux.split(maxsplit = 1)
animaux.split(maxsplit = 2)
③ Recherche : La méthode .find(), quant à elle, recherche une chaîne de caractères passée en argument.
car = "Six scies scient six troncs."
print(car.find("i"))
print(car.find("a"))
print(car.find("sci"))
④ Comptage : La méthode .count(), elle, compte le nombre d'occurrences des caracères donnés en argument.
car = "Six scies scient six troncs."
car.count("i")
⑤ Remplacements : La méthode .replace() remplace les chaînes caractères par ce que vous lui indiquez.
car = "Six scies scient six troncs."
car.replace("troncs","citrons")
⑥ Vérifications : La méthode .startswith() vérifie si une chaîne de caractères commence par l'argument.
car.startswith("Six")
car.startswith("Anakin Skywalker")
⑦ Nettoyage : La méthode .strip() permet de « nettoyer les bords » d’une chaîne de caractères.
car = " Six scies scient six troncs. "
car.strip()
La méthode .strip() enlève les espaces situés sur les bords de la chaîne de caractère mais pas ceux situés entre des caractères visibles. En réalité, cette méthode enlève n’importe quel combinaison « d’espace(s) blanc(s) » sur les bords.
car = "\nSix scies scient six troncs.\t"
car.strip()
⑧ Conversion d'une liste en une chaîne de caractères : La méthode .join() permet de convertir une liste de chaînes de caractères (exclusivement) en une chaîne de caractères.
seq = ["A", "T", "G", "A", "T"]
"-".join(seq)
Une liste est une structure de données qui contient une série de valeurs.
Python autorise la construction de liste contenant des valeurs de types différents (par exemple entier et chaîne de caractères), ce qui leur confère une grande flexibilité.
Une liste est déclarée par une série de valeurs (n’oubliez pas les guillemets, simples ou doubles, s’il s’agit de chaînes de caractères) séparées par des virgules, et le tout encadré par des crochets.
animaux = ["girafe ", "tigre ", "singe ", "souris "]
tailles = [5, 2.5 , 1.75 , 0.15]
mixte = ["girafe ", 5, "souris ", 0.15]
animaux
Lorsque l’on affiche une liste, Python la restitue telle qu’elle a été saisie.
Vous pouvez appeler les éléments d'une liste par leur position. Ce numéro est appelé indice (ou index) de la liste.
Liste : | [ "girafe", | "tigre", | "singe", | "souris" ] |
Indices : | 0 | 1 | 2 | 3 |
animaux = ["girafe ", "tigre ", "singe ", "souris "]
animaux[1]
Tout comme les chaînes de caractères, les listes supportent l'opérateur + de concaténation, ainsi que l'opérateur * pour la duplication.
ani1 = ['girafe ', 'tigre ']
ani2 = ['singe ', 'souris ']
ani1 + ani2
ani2 * 3
l'opérateur + est très pratique pour concaténer deux listes.
Vous pouvez aussi utiliser la méthode append() lorsque vous souhaitez ajouter un seul élément à la fin d’une liste.
a = []
a
Puis lui ajouter deux éléments, l’un après l’autre, d’abord avec la concaténation.
a = a + [15]
a += [-5]
a
Puis avec la méthode append().
a.append(13)
a.append(-3)
a
Dans l’exemple ci-dessus, nous ajoutons des éléments à une liste en utilisant l’opérateur de concaténation + ou la méthode append(). Nous vous conseillons dans ce cas précis d’utiliser la méthode append() dont la syntaxe est plus élégante.
La liste peut également être indexée avec des nombres négatifs selon le modèle suivant :
Liste : | [ "girafe", | "tigre", | "singe", | "souris" ] |
Indices : | 0 | 1 | 2 | 3 |
Indiçages négatifs : | -4 | -3 | -2 | -1 |
animaux = ["girafe ", "tigre ", "singe ", "souris "]
print(animaux[-1], animaux[-4])
Un autre avantage des listes est la possibilité de sélectionner une partie d’une liste en utilisant un indiçage construit sur le modèle [m:n+1] pour récupérer tous les éléments, du m-ième au n-ième de l’élément m inclus à l’élément n+1 exclu. On dit alors qu’on récupère une tranche de la liste.
liste = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
print("liste[3:] → ", liste[3:])
print("liste[:2] → ", liste[:2])
print("liste[2:4] → ", liste[2:4])
print("liste[1:-1] → ", liste[1:-1])
Notez que lorsqu’aucun indice n’est indiqué à gauche ou à droite du symbole deux-points, Python prend par défaut tous les éléments depuis le début ou tous les éléments jusqu’à la fin respectivement. On peut aussi préciser le pas en ajoutant un symbole deux-points supplémentaire et en indiquant le pas par un entier.
liste = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
print("liste[0:6:2] → ", liste[0:6:2])
print("liste[::4] → ", liste[::4])
print("liste[4::] → ", liste[4::])
Finalement, on se rend compte que l’accès au contenu d’une liste fonctionne sur le modèle liste[début:fin:pas].
L’instruction len() vous permet de connaître la longueur d’une liste, c’est-à-dire le nombre d’éléments que contient la liste.
animaux = ["girafe ", "tigre ", "singe ", "souris "]
len(animaux)
La commande range(n) génère l'ensemble des nombres entiers de 0 inclus à n exclu, donc de 0 à n-1.
L’instruction range() est une fonction spéciale en Python qui génère des nombres entiers compris dans un intervalle. Lorsqu’elle est utilisée en combinaison avec la fonction list(), on obtient une liste d’entiers.
list(range(5))
La commande list(range(10)) a généré une liste contenant tous les nombres entiers de 0 inclus à 10 exclu. Nous verrons l’utilisation de la fonction range() toute seule dans le chapitre suivant.
On peut aussi indiquer à la fonction range() le point de départ en lui précisant en premier argument. La commande range(k, n) avec k < n génère l'ensemble des nombres entiers de k inclus à n exclu, donc de k à n-1.
list(range(5, 10))
On peut aussi indiquer le pas de la série en troisième argument. La commande range(k, n, p) avec k < n génère l'ensemble des nombres entiers de k inclus à n exclu tous les p pas, donc de k à n-1. Si on veut partir de 0, il faut le préciser en premier argument.
list(range(0, 20, 2))
list(range(100, 2001, 300))
La fonction np.arange(n) génère range(n) directement sous la forme d'un tableau.
import numpy as np
m = np.arange(3, 15, 2)
m
Notez bien la différence:
La fonction np.linespace() permet d’obtenir un tableau 1D allant d’une valeur de départ à une valeur de fin avec un nombre donné d’éléments.
np.linspace(3, 9, 6)
Sachez qu’il est tout à fait possible de construire des listes de listes. Cette fonctionnalité peut parfois être très pratique.
ani1 = ["girafe", "tigre"]
ani2 = ["singe", "souris"]
ani3 = ["chien", "chat"]
zoo = [ani1, ani2, ani3]
zoo
Dans cet exemple, chaque sous-liste contient une catégorie d’animal et le nombre d’animaux pour chaque catégorie. Pour accéder à un élément de la liste, on utilise l’indiçage habituel.
zoo[1]
Pour accéder à un élément de la sous-liste, on utilise un double indiçage.
zoo[1][0]
On verra un peu plus loin qu’il existe en Python des dictionnaires qui sont également très pratiques pour stocker de l’information structurée. On verra aussi qu’il existe un module nommé NumPy qui permet de créer des listes ou des tableaux de nombres (vecteurs et matrices) et de les manipuler.
Pour la partie C sur les listes, assurez-vous d'avoir acquis les parties A et B du chapitre Boucles et Comparaisons.
① Ajout d'éléments : La méthode append() ajoute un élément à la fin d’une liste.
a = [1, 2, 3]
a.append (5)
a
Ceci est équivaut à la méthode ci-dessous.
a = [1, 2, 3]
a += [5]
a
② Insertion : La méthode .insert() insère un objet dans une liste avec un indice déterminé.
a = [1, 2, 3]
a. insert (2, -15)
a
③ Suppression par indice : L'instruction del() supprime un élément d’une liste à un indice déterminé.
a = [1, 2, 3]
del(a[1])
a
④ Suppression par élément : La méthode .remove() supprime un élément d’une liste à partir de sa valeur à sa première rencontre.
a = [1, 2, 3, 4, 3, 3, 5]
a.remove(3)
a
⑤ Tri : La méthode .sort() trie la liste.
a = [5, 2, 1, 4, 3]
a.sort()
a
⑥ Inversion : La méthode .reverse() inverse la liste.
a = [1, 2, 3, 4, 5]
a.reverse()
a
⑦ Comptage : La méthode .count() compte le nombre d’éléments (passés en argument) dans une liste.
a = [1, 2, 3, 4, 3, 3, 5, 4, 1, 5, 5, 5, 4]
a.count(5)
De nombreuses méthodes ci-dessus (.append(), .sort(), etc.) modifient la liste mais ne renvoient rien, c’est-à-dire qu’elles ne renvoient pas d’objet récupérable dans une variable. Il s’agit d’un exemple d’utilisation de méthode (donc de fonction particulière) qui fait une action mais qui ne renvoie rien. Pensez-y dans vos utilisations futures des listes. Certaines méthodes ou instructions des listes décalent les indices d’une liste (par exemple .insert(), del, etc.). Enfin, pour obtenir une liste exhaustive des méthodes disponibles pour les listes, utilisez la fonction dir(ma_liste) (ma_liste étant une liste).
La méthode .append() est très pratique car on peut l’utiliser pour construire une liste au fur et à mesure des itérations d’une boucle. Pour cela, il est commode de définir préalablement une liste vide de la forme maliste = [].
seq = " CAAAGGTAACGC "
seq_list = []
for base in seq :
seq_list.append(base)
seq_list
Remarquez que dans cet exemple, vous pouvez directement utiliser la fonction list() qui prend n’importe quel objet séquentiel (liste, chaîne de caractères, etc.) et qui renvoie une liste.
seq = " CAAAGGTAACGC "
list(seq)
Cette méthode est certes plus simple, mais il arrive parfois qu’on doive utiliser des boucles tout de même, comme lorsqu’on lit un fichier. On rappelle que l’instruction list(seq) convertit un objet de type chaîne de caractères en un objet de type liste (il s’agit donc d’une opération de casting). De même que list(range(10)) convertit un objet de type range en un objet de type list.
L’opérateur in teste si un élément fait partie d’une liste.
a = [1, 2, 3, 4]
4 in a
5 in a
La variation avec not permet, a contrario, de vérifier qu’un élément n’est pas dans une liste.
4 not in a
5 not in a
Il est très important de savoir que l’affectation d’une liste (à partir d’une liste préexistante) crée en réalité une référence et non une copie.
x = [1, 2, 3]
y = x
y.append(4)
x
x = [1, 2, 3]
y = x[:]
y.append(4)
x
Si vous êtes débutant, sautez ce sous-chapitre pour le moment.
1°) Nombres pairs compris entre 0 et 30 :
print ([i for i in range (31) if i % 2 == 0])
2°) Jeu sur la casse des mots d’une phrase :
message = "C'est sympa la BioInfo "
msg_lst = message.split ()
print ([ [ m.upper(), len (m)] for m in msg_lst ])
3°) Formatage d’une séquence avec 60 caractères par ligne :
# Exemple d'une sé quence de 100 alanines.
seq = "A" * 100
width = 60
seq_split = [ seq [i:i+ width ] for i in range (0, len( seq), width )]
print ("\ n". join ( seq_split ))
4°) Formatage FASTA d’une séquence (avec la ligne de commentaire) :
com = "Sé quence de 150 alanines "
seq = "A" * 150
width = 60
seq_split = [ seq [i:i+ width ] for i in range (0, len( seq), width )]
print (" >"+ com +"\ n "+"\ n". join ( seq_split ))
En programmation, on est souvent amené à répéter plusieurs fois une instruction. Incontournables à tout langage de programmation, les boucles vont nous aider à réaliser cette tâche de manière compacte et efficace. Si votre liste ne contient que 4 éléments, ceci est encore faisable mais imaginez qu’elle en contienne 100 voire 1000! Pour remédier à cela, il faut utiliser les boucles.
animaux = ["girafe ", "tigre ", "singe ", "souris "]
for animal in animaux:
print(animal)
La variable animal est appelée variable d’itération, elle prend successivement les différentes valeurs de la liste animaux à chaque itération de la boucle.
On verra un peu plus loin dans ce chapitre que l’on peut choisir le nom que l’on veut pour cette variable. Celle-ci est créée par Python la première fois que la ligne contenant le for est exécutée (si elle existait déjà son contenu serait écrasé). Une fois la boucle terminée, cette variable d’itération animal ne sera pas détruite et contiendra ainsi la dernière valeur de la liste animaux (ici la chaîne de caractères souris).
Notez bien les types des variables utilisées ici : animaux est une liste sur laquelle on itère, et animal est une chaîne de caractères car chaque élément de la liste est une chaîne de caractères. Nous verrons plus loin que la variable d’itération peut être de n’importe quel type selon la liste parcourue. En Python, une boucle itère toujours sur un objet dit séquentiel
(c’est-à-dire un objet constitué d’autres objets) tel qu’une liste. Nous verrons aussi plus tard d’autres objets séquentiels sur lesquels on peut itérer dans une boucle.
D’ores et déjà, prêtez attention au caractère deux-points « : » à la fin de la ligne débutant par for. Cela signifie que la boucle for attend un bloc d’instructions, en l’occurrence toutes les instructions que Python répétera à chaque itération de la boucle. On appelle ce bloc d’instructions le corps de la boucle. Comment indique-t-on à Python où ce bloc commence et se termine ? Cela est signalé uniquement par l’indentation, c’est-à-dire le décalage vers la droite de la (ou des) ligne(s) du bloc d’instructions.
Dans l’exemple suivant, le corps de la boucle contient deux instructions : print(animal) et print(animal*2) car elles sont indentées par rapport à la ligne débutant par for.
for animal in animaux:
print(animal)
print(animal*2)
print("C'est fini")
La ligne 4 print("C'est fini") ne fait pas partie du corps de la boucle car elle est au même niveau que le for (c’està-dire non indentée par rapport au for). Notez également que chaque instruction du corps de la boucle doit être indentée de la même manière (ici 4 espaces).
Dans les exemples ci-dessus, nous avons exécuté une boucle en itérant directement sur une liste. Une tranche d’une liste étant elle même une liste, on peut également itérer dessus.
animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
for animal in animaux [1:3]: print ( animal ) # Oui, s'il n'y a qu'une seule ligne dans le bloc, on peut le mettre devant.
Python possède la fonction range() que nous avons rencontrée précédemment et qui est aussi bien commode pour faire une boucle sur une liste d’entiers de manière automatique.
for i in range(4):
print(i)
Contrairement à la création de liste avec list(range(4)), la fonction range() peut être utilisée telle quelle dans une boucle.
Il n’est pas nécessaire de taper for i in list(range(4)): même si cela fonctionnerait également.
En effet, range() est une fonction qui a été spécialement conçue pour cela 1, c’est-à-dire que l’on peut itérer directement dessus.
Pour Python, il s’agit d’un nouveau type, par exemple dans l’instruction x = range(3) la variable x est de type range (tout comme on avait les types int, float, str ou list) à utiliser spécialement avec les
boucles.
L’instruction list(range(4)) se contente de transformer un objet de type range en un objet de type list. Si vous vous
souvenez bien, il s’agit d’une fonction de casting, qui convertit un type en un autre. Il n’y aucun
intérêt à utiliser dans une boucle la construction for i in list(range(4)):. C’est même contre-productif.
En effet,
range() se contente de stocker l’entier actuel, le pas pour passer à l’entier suivant, et le dernier entier à parcourir, ce qui revient
à stocker seulement 3 nombres entiers et ce quelle que soit la longueur de la séquence, même avec un range(1000000). Si on utilisait list(range(1000000)), Python construirait d’abord une liste de 1 million d’éléments dans la mémoire puis itérerait dessus, d’où une énorme perte de temps !
Dans l’exemple précédent, nous avons choisi le nom i pour la variable d’itération. Ceci est une habitude en informatique et indique en général qu’il s’agit d’un entier (le nom i vient sans doute du mot indice ou index en anglais). Nous vous conseillons de suivre cette convention afin d’éviter les confusions, si vous itérez sur les indices vous pouvez appeler la variable d’itération i (par exemple dans for i in range(4):). Si, par contre, vous itérez sur une liste comportant des chaînes de caractères, mettez un nom explicite pour la variable d’itération.
for prenom in ['Joe', 'Bill', 'John']:
print(prenom)
Revenons à notre liste animaux. Nous allons maintenant parcourir cette liste, mais cette fois par une itération sur ses indices.
animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
for i in range(4):
print(animaux[i])
La variable i prendra les valeurs successives 0, 1, 2 et 3 et on accèdera à chaque élément de la liste animaux par son indice (i.e. animaux[i]). Notez à nouveau le nom i de la variable d’itération car on itère sur les indices. Quand utiliser l’une ou l’autre des 2 méthodes ? La plus efficace est celle qui réalise les itérations directement sur les éléments.
Toutefois, il se peut qu’au cours d’une boucle vous ayez besoin des indices, auquel cas vous devrez itérer sur les indices.
animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
for i in range(len(animaux)):
print("L' animal {} est un(e) {}". format (i, animaux[i]))
Python possède toutefois la fonction enumerate() qui vous permet d’itérer sur les indices et les éléments eux-mêmes.
animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
for i, animal in enumerate(animaux):
print ("L' animal {} est un(e) {}".format(i, animal))
Python est capable d’effectuer toute une série de comparaisons entre le contenu de deux variables.
Syntaxe Python | Signification |
== | est égal à |
!= | n'est pas égal à |
<= | est inférieur ou égal à |
< | est strictement inférieur à |
>= | est supérieur ou égal à |
> | est strictement supérieur ou égal à |
a = "ok"
a=="okk"
Dans le cas des chaînes de caractères, a priori seuls les tests == et != ont un sens. En fait, on peut aussi utiliser les opérateurs <, >, <= et >=. Dans ce cas, l’ordre alphabétique est pris en compte.
"u" < "v"
"a" est inférieur à "b" car le caractère a est situé avant le caractère b dans l’ordre alphabétique. En fait, c’est l’ordre ASCII 2 des caractères qui est pris en compte (à chaque caractère correspond un code numérique), on peut donc aussi comparer des caractères spéciaux (comme # ou ~) entre eux. Enfin, on peut comparer des chaînes de caractères de plusieurs caractères
" ali " < " alo"
" abb " < " ada"
Dans ce cas, Python compare les deux chaînes de caractères, caractère par caractère, de la gauche vers la droite (le premier caractère avec le premier, le deuxième avec le deuxième, etc). Dès qu’un caractère est différent entre l’une et l’autre des deux chaînes, il considère que la chaîne la plus petite est celle qui présente le caractère ayant le plus petit code ASCII (les caractères suivants de la chaîne de caractères sont ignorés dans la comparaison), comme dans l’exemple "abb" < "ada" ci-dessus.
Une autre alternative à l’instruction for couramment utilisée en informatique est la boucle while. Le principe est simple. Une série d’instructions est exécutée tant qu’une condition est vraie.
i = 1
while i <= 4:
print (i)
i = i + 1
Remarquez qu’il est encore une fois nécessaire d’indenter le bloc d’instructions correspondant au corps de la boucle (ici, les instructions lignes 3 et 4). Une boucle while nécessite généralement trois éléments pour fonctionner correctement :
Faites bien attention aux tests et à l’incrémentation que vous utilisez car une erreur mène souvent à des « boucles infinies » qui ne s’arrêtent jamais. Vous pouvez néanmoins toujours stopper l’exécution d’un script Python à l’aide de la combinaison de touches Ctrl-C (c’est-à-dire en pressant simultanément les touches Ctrl et C).
La boucle while combinée à la fonction input() peut s’avérer commode lorsqu’on souhaite demander à l’utilisateur une valeur numérique.
i = 0
while i < 10:
reponse = input (" Entrez un entier supérieur à 10 : ")
i = int (reponse)
La fonction input() prend en argument un message (sous la forme d’une chaîne de caractères), demande à l’utilisateur d’entrer une valeur et renvoie celle-ci sous forme d’une chaîne de caractères. Il faut ensuite convertir cette dernière en entier (avec la fonction int()).
Le mot-clé continue permet de… continuer une boucle, en repartant directement à la ligne du while (ou du for).
i = 1
while i < 20: # Tant que i est inférieure à 20.
if i % 3 == 0:
i += 4 # On ajoute 4 à i
print("On incrémente i de 4. i est maintenant égale à", i)
continue # On retourne au while sans exécuter les autres lignes.
print("La variable i =", i)
i += 1 # Dans le cas classique on ajoute juste 1 à i.
Comme vous le voyez, tous les trois tours de boucle,i s'incrémente de 4. Arrivé au mot-clé continue, Python n'exécute pas la fin du bloc mais revient au début de la boucle en testant à nouveau la condition du while. Autrement dit, quand Python arrive à la ligne 6, il saute à la ligne 2 sans exécuter les lignes 7 et 8. Au nouveau tour de boucle, Python reprend l'exécution normale de la boucle (continue n'ignore la fin du bloc que pour le tour de boucle courant).
Les tests sont un élément essentiel à tout langage informatique si on veut lui donner un peu de complexité car ils permettent à l’ordinateur de prendre des décisions. Pour cela, Python utilise l’instruction if ainsi qu’une comparaison que nous avons abordée au chapitre précédent.
x = 2
if x == 2 :
print("C'est bon!")
if x == "tigre" :
print("Hein? Quoi?")
Il y a plusieurs remarques à faire concernant ces deux exemples :
De nouveau, faites bien attention à l’indentation ! Vous devez être très rigoureux sur ce point.
nombres = [4, 5, 6]
for nb in nombres :
if nb == 5:
print (" Le test est vrai ")
print (" car la variable nb vaut {}". format (nb ))
Les deux codes pourtant très similaires produisent des résultats très différents. Si vous observez avec attention l’indentation des instructions sur la ligne 5, vous remarquerez que dans le code 1, l’instruction est indentée deux fois, ce qui signifie qu’elle appartient au bloc d’instructions du test if. Dans le code 2, l’instruction de la ligne 5 n’est indentée qu’une seule fois, ce qui fait qu’elle n’appartient plus au bloc d’instructions du test if, d’où l’affichage de car la variable nb vaut xx pour toutes les valeurs de nb.
Parfois, il est pratique de tester si la condition est vraie ou si elle est fausse dans une même instruction if. Plutôt que d’utiliser deux instructions if, on peut se servir des instructions if et else.
x = 3
if x == 2 :
print("C'est bon!")
else:
print("C'est pas bon!!")
On peut utiliser une série de tests dans la même instruction if, notamment pour tester plusieurs valeurs d’une même variable. Par exemple, on se propose de tirer au sort une base d’ADN puis d’afficher le nom de cette dernière. Dans le code suivant, nous utilisons l’instruction random.choice(liste) qui renvoie un élément choisi au hasard dans une liste. L’instruction import random sera vue plus tard dans le chapitre Modules, admettez pour le moment qu’elle est nécessaire.
import random
base = random . choice ([" a", "t", "c", "g"])
if base == "a":
print (" choix d'une adé nine ")
elif base == "t":
print (" choix d'une thymine ")
elif base == "c":
print (" choix d'une cytosine ")
elif base == "g":
print (" choix d'une guanine ")
Dans cet exemple, Python teste la première condition, puis, si et seulement si elle est fausse, teste la deuxième et ainsi de suite. . . Le code correspondant à la première condition vérifiée est exécuté puis Python sort du bloc d’instructions du if.
Les tests multiples permettent de tester plusieurs conditions en même temps en utilisant des opérateurs booléens. Les deux opérateurs les plus couramment utilisés sont le OU et le ET.
Condition 1 | Opérateur | Condition 2 | Résultat |
VRAI | OU | VRAI | VRAI |
VRAI | OU | FAUX | VRAI |
FAUX | OU | VRAI | VRAI |
FAUX | OU | FAUX | FAUX |
VRAI | ET | VRAI | VRAI |
VRAI | ET | FAUX | FAUX |
FAUX | ET | VRAI | FAUX |
FAUX | ET | FAUX | FAUX |
x = 2
y = 4
# Test 1 :
if x == 2 and y == 3000 :
print("Le test 1 est vrai.")
else:
print("Le test 1 est faux.")
# Test 2 :
if x == 2 or y == 3000 :
print("Le test 2 est vrai.")
else:
print("Le test 2 est faux.")
L’instruction break stoppe la boucle.
for i in range (5):
if i > 2:
break
print (i)
L’instruction continue saute à l’itération suivante, sans exécuter la suite du bloc d’instructions de la boucle.
for i in range (5):
if i == 2:
continue
print (i)
L’instruction pass ne fait rien. Elle peut être utilisée lorsqu’une instruction est nécessaire pour fournir une syntaxe correcte, mais qu’aucune action ne doit être effectuée.
def inverse(x):
if x != 0: return 1/x
else: pass
print(inverse(4))
print(inverse(0))
Lorsque l’on souhaite tester la valeur d’une variable de type float, le premier réflexe serait d’utiliser l’opérateur d’égalité.
1/10 == 0.1
Toutefois, nous vous le déconseillons formellement. Pourquoi ? Python stocke les valeurs numériques des floats sous forme de nombres flottants (d’où leur nom!), et cela mène à certaines limitations 1.
(3 - 2.7) == 0.3
3 - 2.7
Nous voyons que le résultat de l’opération 3 - 2.7 n’est pas exactement 0.3 d’où le False en ligne 2. En fait, ce problème ne vient pas de Python, mais plutôt de la manière dont un ordinateur traite les nombres flottants (comme un rapport de nombres binaires). Ainsi certaines valeurs de float ne peuvent être qu’approchées. Une manière de s’en rendre compte est d’utiliser l’écriture formatée en demandant l’affichage d’un grand nombre de décimales.
Une exception est un objet qui indique que le programme ne peut continuer son exécution. Le type de l’exception donne une indication sur le type de l’erreur rencontrée. L’exception contient généralement un message plus détaillé. Les exceptions héritent du type Exception.
On décide par exemple qu’on veut rattraper toutes les erreurs du programme et afficher un message d’erreur. Le programme suivant appelle la fonction qui retourne l’inverse d’un nombre.
def inverse(x):
y = 1.0/x
return(y)
a = inverse(4)
print(a)
Évidemment, si vous entrez inverse(0), Python vous renverra une erreur avec le fameux message détaillé sous le Traceback (habituez-vous à le voir souvent, celui-là...). Lorsque x == 0, le programme effectue une division par zéro et déclenche une erreur. L’interpréteur Python affiche ce qu’on appelle la pile d’appels ou pile d’exécution. La pile d’appel permet d’obtenir la liste de toutes les fonctions pour remonter jusqu’à celle où l’erreur s’est produite.
Afin de rattraper l’erreur, on insère le code susceptible de produire une erreur entre les mots clés try et except.
try:
a = inverse(4)
print(a)
b = inverse(0) # Déclenche une exception.
print(b)
except:
print("Le programme a déclenché une erreur, mon cœur.")
Le programme essaye d’exécuter les quatre instructions incluses entre les instructions try et except. Si une erreur se produit, le programme exécute alors les lignes qui suivent l’instruction except. L’erreur se produit en fait à l’intérieur de la fonction mais celle-ci est appelée à l’intérieur d’un code « protégé » contre les erreurs. Ceci explique les lignes affichées par le programme. Il est aussi possible d’ajouter une clause qui sert de préfixe à une liste d’instructions qui ne sera exécutée que si aucune exception n’est déclenchée.
try:
print(inverse(2)) # Pas d'erreur.
print(inverse(1)) # Pas d'erreur non plus.
except:
print("Le programme a déclenché une erreur, mon cœur.")
else:
print("Tout s'est bien passé, oklm.")
Ce dernier programme attrape l’erreur et affiche un message. Ce programme ne s’arrête jamais, il ne plante jamais. Pour résumer, la syntaxe suivante permet d’attraper toutes les erreurs qui se produisent pendant l’exécution d’une partie du programme. Cette syntaxe permet en quelque sorte de protéger cette partie du programme contre les erreurs.
try :
# ... instructions à protéger.
except :
# ... que faire en cas d'erreur.
else :
# ... que faire lorsque aucune erreur n'est apparue.
Toute erreur déclenchée alors que le programme exécute les instructions qui suivent le mot-clé try déclenche immédiatement l’exécution des lignes qui suivent le mot-clé except. Dans le cas contraire, le programme se poursuit avec l’exécution des lignes qui suivent le mot-clé else. Cette dernière partie est facultative, la clause else peut ou non être présente. Le bout de code prévoit ce qu’il faut faire dans n’importe quel cas.
Lorsqu’une section de code est protégée contre les exceptions, son exécution s’arrête à la première erreur d’exécution. Le reste du code n’est pas exécuté. Par exemple, dès la première erreur qui correspond au calcul d’une puissance non entière d’un nombre négatif, l’exécution du programme suivant est dirigée vers l’instruction qui suit le mot-clé except.
try:
print(inverse(2))
print(inverse(0)) # Première erreur.
print(inverse(4))
print((-2.1) ** 0.5) # Cette ligne produirait une erreur mais le programme n'arrive jamais jusqu'ici.
except Exception:
print("Le programme a déclenché une erreur, mon cœur.")
Cette écriture n’est par recommandée car le programme intercepte toutes les erreurs quelles qu’elles soient. Mieux vaut de n’attraper que les exceptions prévues sans risquer de masquer celles qui n’étaient pas prévues et qui pourraient être la conséquence d’un bug.
Lorsqu’une fonction détecte une erreur, il lui est possible de déclencher une exception par l’intermédiaire du mot-clé raise. La fonction inverse compare x à 0 et déclenche l'exception ValueError si x est nul. Cette exception est attrapée plus bas.
def inverse(x):
if x == 0:
raise ValueError
y = 1.0 / x
return y
try:
print(inverse(0)) # Erreur.
except ValueError:
print("Erreur de type ValueError, ça marche pas.")
Il est parfois utile d’associer un message à une exception afin que l’utilisateur ne soit pas perdu. Le programme qui suit est identique au précédent à ceci près qu’il associe à l’exception ValueError qui précise l’erreur et mentionne la fonction où elle s’est produite. Le message est ensuite intercepté plus bas.
def inverse(x):
if x == 0:
raise ValueError("Valeur nulle interdite, fonction inverse.")
y = 1.0 / x
return y
try:
print(inverse(0)) # Erreur.
except ValueError as exc:
print("erreur, message :", exc)
Le déclenchement d’une exception suit la syntaxe suivante : raise exception_type(message).
Cette instruction lance l’exception exception_type associée au message message. Le message est facultatif, lorsqu’il n’y en a pas, la syntaxe se résume à raise exception_type.
Et pour attraper cette exception et le message qui lui est associé, il faut utiliser la syntaxe décrite au paragraphe précédent.
L’instruction help(ZeroDivisionError) retourne l’aide associée à l’exception ZeroDivisionError. Celle-ci indique que l’exception ZeroDivisionError est en fait un cas particulier de l’exception ArithmeticError, elle-même un cas particulier de StandardError.
Comme pour les boucles, il est possible d’imbriquer les portions protégées de code les unes dans les autres. Dans l’exemple qui suit, la première erreur est l’appel à une fonction non définie, ce qui déclenche l’exception NameError.
def inverse(x):
y = 1.0 / x
return y
try:
try:
print(inverses(0)) # Fonction inexistante --> Exception NameError
print(inverse(0)) # Division par zéro --> ZeroDivisionError
except NameError:
print("Appel à une fonction non définie.")
except ZeroDivisionError as exc:
print("erreur", exc)
En revanche, dans le second exemple, les deux lignes print(inverse(0)) et print(inverses(0)) ont été permutées. La première exception déclenchée est la division par zéro. La première clause except n’interceptera pas cette erreur puisqu’elle n’est pas du type recherché.
try:
try:
print(inverse(0)) # Division par zéro --> ZeroDivisionError
print(inverses(0)) # Fonction inexistante --> exception NameError
except NameError:
print("Appel à une fonction non définie.")
except ZeroDivisionError as exc:
print("erreur", exc)
Une autre imbrication possible est l’appel à une fonction qui inclut déjà une partie de code protégée. L’exemple suivant appelle la fonction inverse qui intercepte les exceptions de type ZeroDivisionError pour retourner une grande valeur lorsque x=0. La seconde exception générée survient lors de l’appel à la fonction inverses qui déclenche l’exception NameError, elle aussi interceptée.
def inverse(x):
try:
y = 1.0 / x
except ZeroDivisionError as exc:
print("erreur ", exc)
if x > 0:
return 1000000000
else:
return -1000000000
return y
try:
print(inverse(0)) # Division par zéro --> la fonction inverse sait gérer
print(inverses(0)) # Fonction inexistante --> exception NameError
except NameError:
print("Appel à une fonction non définie.")
Pour définir sa propre exception, il faut créer une classe qui dérive d’une classe d’exception existante par exemple, la classe Exception. L’exemple suivant crée une exception AucunChiffre qui est lancée par la fonction conversion lorsque la chaîne de caractères qu’elle doit convertir ne contient pas que des chiffres.
class AucunChiffre(Exception):
"""
Chaîne de caractères contenant aussi autre chose que des chiffres.
"""
pass
def conversion(s):
"""
Conversion d'une chaîne de caractères en entier.
"""
if not s.isdigit():
raise AucunChiffre(s)
return int(s)
try:
s = "123a"
print(s, " = ", conversion(s))
except AucunChiffre as exc: # On affiche ici le commentaire associé à la classe d'exception et le message associé.
print(AucunChiffre.__doc__, " : ", exc)
En redéfinissant l’opérateur __str__ d’une exception, il est possible d’afficher des messages plus explicites avec la seule instruction print.
class AucunChiffre(Exception):
"""
Chaîne de caractères contenant aussi autre chose que des chiffres.
"""
def __str__ (self):
return "{0} {1}".format(self.__doc__, Exception.__str__(self))
Il est parfois utile qu’une exception contienne davantage d’informations qu’un simple message. L’exemple suivant reprend l’exemple du paragraphe précédent. L’exception AucunChiffre inclut cette fois-ci un paramètre supplémentaire contenant le nom de la fonction où l’erreur a été déclenchée.
La classe AucunChiffre possède dorénavant un constructeur qui doit recevoir deux paramètres : une valeur et un nom de fonction. L’exception est levée à l’aide de l’instruction raise AucunChiffre(s, "conversion") qui regroupe dans un T-uple les paramètres à envoyer à l’exception.
L’opérateur __str__ a été modifié de façon à ajouter ces deux informations dans le message associé à l’exception. Ainsi, l’instruction print(exc) présente à l’avant dernière ligne de cet exemple affiche un message plus complet.
class AucunChiffre(Exception):
"""
Chaîne de caractères contenant aussi autre chose que des chiffres.
"""
def __init__(self, s, f=""):
Exception.__init__(self, s)
self.s = s
self.f = f
def __str__(self):
return "exception AucunChiffre, depuis la fonction {0} avec le paramètre {1}.".format(self.f, self.s)
def conversion(s):
"""
Conversion d'une chaîne de caractères en entier.
"""
if not s.isdigit(): raise AucunChiffre(s, "conversion")
return int(s)
try:
s = "123a"
i = conversion(s)
print(s, " = ", i)
except AucunChiffre as exc:
print(exc)
print("Fonction : ", exc.f)
Étant donné que le programme déclenche une exception dans la section de code protégée, les deux derniers affichages sont les seuls exécutés correctement. Ils produisent les deux lignes qui suivent.
① Les itérateurs :Les itérateurs sont des outils qui permettent de parcourir des objets qui sont des ensembles, comme une liste, un dictionnaire. Ils fonctionnent toujours de la même manière. Arrivée à la troisième itération, l’exception StopIteration est déclenchée. Cette exception indique à une boucle for de s’arrêter.
class point_espace:
# ...
class class_iter:
def __init__(self, ins):
self._n = 0
self._ins = ins
def __iter__(self) :
return self
def next(self):
if self._n <= 2:
v = self._ins[self._n]
self._n += 1
return v
else:
raise StopIteration
def __iter__(self):
return point_espace.class_iter(self)
Cet exemple montre seulement que les exceptions n’interviennent pas seulement lors d’erreurs mais font parfois partie intégrante d’un algorithme.
② Exception ou valeur aberrante :Sans exception, une solution pour indiquer un cas de mauvaise utilisation d’une fonction est de retourner une valeur aberrante. Retourner -1 pour une fonction dont le résultat est nécessairement positif est une valeur aberrante. Cette convention permet de signifier à celui qui appelle la fonction que son appel n’a pu être traité correctement. Dans l’exemple qui suit, la fonction racine_carree retourne un couple de résultats, True ou False pour savoir si le calcul est possible, suivi du résultat qui n’a un sens que si True est retournée en première valeur.
def racine_carree(x):
if x < 0: return False, 0
else: return True, x ** 0.5
print(racine_carree(-1)) # (False, 0)
print(racine_carree(1)) # (True, 1.0)
Plutôt que de compliquer le programme avec deux résultats ou une valeur aberrante, on préfère souvent déclencher une exception, ici, ValueError. La plupart du temps, cette exception n’est pas déclenchée. Il est donc superflu de retourner un couple plutôt qu’une seule et unique valeur.
En programmation, les fonctions sont très utiles pour réaliser plusieurs fois la même opération au sein d’un programme.
Elles rendent également le code plus lisible et plus clair en le fractionnant en blocs logiques.
Vous connaissez déjà certaines fonctions Python. Par exemple math.cos(angle) du module math renvoie le cosinus de
la variable angle exprimé en radian. Vous connaissez aussi des fonctions internes à Python comme range() ou len().
Pour l’instant, une fonction est à vos yeux une sorte de « boîte noire »:
Par exemple, si vous appelez la fonction len() de la manière suivante.
len([10, 11, 12])
Voici ce qui se passe :
Pour définir une fonction, Python utilise le mot-clé def.
Si on souhaite que la fonction renvoie quelque chose, il faut utiliser le mot-clé return.
def carre(x):
return(x**2)
carre(4)
Notez que la syntaxe de def utilise les deux-points comme les boucles for et while ainsi que les tests if, un bloc d’instructions est donc attendu. De même que pour les boucles et les tests, l’indentation de ce bloc d’instructions (qu’on appelle le corps de la fonction) est obligatoire. Dans l’exemple précédent, nous avons passé un argument à la fonction carre() qui nous a renvoyé (ou retourné) une valeur que nous avons immédiatement affichée à l’écran avec l’instruction print(). Que veut dire valeur renvoyée ? Et bien cela signifie que cette dernière est récupérable dans une variable :
res = carre(5)
print(res)
Ici, le résultat renvoyé par la fonction est stocké dans la variable res. Notez qu’une fonction ne prend pas forcément un argument et ne renvoie pas forcément une valeur.
def hello():
print("bonjour!")
hello()
Le nombre d’arguments que l’on peut passer à une fonction est variable. Nous avons vu ci-dessus des fonctions auxquelles on passait 0 ou 1 argument. Dans les chapitres précédents, vous avez rencontré des fonctions internes à Python qui prenaient au moins 2 arguments. Souvenez-vous par exemple de range(1, 10) ou encore range(1, 10, 2). Le nombre d’argument est donc laissé libre à l’initiative du programmeur qui développe une nouvelle fonction. Une particularité des fonctions en Python est que vous n’êtes pas obligé de préciser le type des arguments que vous lui passez, dès lors que les opérations que vous effectuez avec ces arguments sont valides. Python est en effet connu comme étant un langage au « typage dynamique », c’est-à-dire qu’il reconnaît pour vous le type des variables au moment de l’exécution.
def multiplication(x, y): return(x*y) # Oui, on peut aussi tout aligner si le bloc d'instruction ne contient qu'une ligne.
multiplication(3, 4)
L’opérateur * reconnaît plusieurs types (entiers, floats, chaînes de caractères, listes). Notre fonction multiplication() est donc capable d’effectuer des tâches différentes ! Même si Python autorise cela, méfiez-vous tout de même de cette grande flexibilité qui pourrait conduire à des surprises dans vos futurs programmes. En général, il est plus judicieux que chaque argument ait un type précis (int, floats, str, etc...) et pas l’un ou l’autre.
Un énorme avantage en Python est que les fonctions sont capables de renvoyer plusieurs objets à la fois, comme dans cette fraction de code.
def carre_cube(x): return(x**2, x**3)
carre_cube(3)
En réalité Python ne renvoie qu’un seul objet, mais celui-ci peut être séquentiel, c’est-à-dire contenir lui même d’autres objets. Dans notre exemple Python renvoie un objet de type tuple, type que nous verrons dans le chapitre Dictionnaires & tuples (grosso modo, il s’agit d’une sorte de liste avec des propriétés différentes). Notre fonction pourrait tout autant renvoyer une liste.
def carre_cube(x): return([x**2, x**3])
carre_cube(3)
Renvoyer un tuple ou une liste de deux éléments (ou plus) est très pratique en conjonction avec l’affectation multiple.
z1, z2 = carre_cube(4)
z2
Cela permet de récupérer plusieurs valeurs renvoyées par une fonction et de les affecter à la volée à des variables différentes.
Définition : Lorsqu’on définit une fonction def fct(x, y): les arguments x et y sont appelés arguments positionnels (positional arguments). Il est strictement obligatoire de les préciser lors de l’appel de la fonction. De plus, il est nécessaire de respecter le même ordre lors de l’appel que dans la définition de la fonction. Dans l’exemple ci-dessus, 2 correspondra à x et 3 correspondra à y. Finalement, tout dépendra de leur position, d’où leur qualification de positionnel. Mais il est aussi possible de passer un ou plusieurs argument(s) de manière facultative et de leur attribuer une valeur par défaut.
def fct(x=4): return(x+6)
fct()
fct(12)
Définition : Un argument défini avec une syntaxe def fct(arg=val): est appelé argument par mot-clé (en anglais keyword argument). Le passage d’un tel argument lors de l’appel de la fonction est facultatif. Ce type d’argument ne doit pas être confondu avec les arguments positionnels présentés ci-dessus, dont la syntaxe est def fct(arg):.
def fct(x = 1 , y = 2 , z = 3) : return(x, y, z)
fct()
fct(3, 5, 9)
Python permet même de rentrer les arguments par mot-clé dans un ordre arbitraire.
fct(z = 88 , x = 1, y = 'π')
Lorsqu’on manipule des fonctions, il est essentiel de bien comprendre comment se comportent les variables.
Une variable est dite locale lorsqu’elle est créée dans une fonction. Elle n’existera et ne sera visible que lors de l’exécution de la fonction.
Une variable est dite globale lorsqu’elle est créée dans le programme principal. Elle sera visible partout dans le programme.
Nous allons prendre un exemple simple qui vous aidera à mieux saisir ces concepts. Observez le code suivant.
# Définition d'une fonction carre() :
def carre (x):
y = x **2
return y
# Programme principal :
z = 5
resultat = carre (z)
print ( resultat )
Nous avons vu des fonctions qui étaient appelées depuis le programme principal. Il est en fait possible d’appeler une fonction depuis une autre fonction. Et plus généralement, on peut appeler une fonction de n’importe où à partir du moment où elle est visible par Python (c’est-à-dire chargée dans la mémoire). Observez cet exemple.
# Définition des fonctions :
def polynome (x): return (x **2 - 2*x + 1)
def calc_vals (debut , fin):
liste_vals = []
for x in range (debut , fin + 1):
liste_vals . append ( polynome (x))
return liste_vals
# Programme principal
print (calc_vals (-5, 5))
Nous appelons depuis le programme principal la fonction calc_vals(), puis à l’intérieur de celle-ci nous appelons l’autre fonction polynome(). L’espace mémoire alloué à polynome() est grisé, indiquant que cette fonction est en cours d’exécution. La fonction appelante calc_vals() est toujours là (sur un fond blanc) car son exécution n’est pas terminée. Elle est en quelque sorte figée dans le même état qu’avant l’appel de polynome(), et on pourra ainsi noter que ses variables locales (debut, fin, liste_vals et x) sont toujours là . De manière générale, les variables locales d’une fonction ne seront détruites que lorsque l’exécution de celle-ci sera terminée. Dans notre exemple, les variables locales de calc_vals() ne seront détruites que lorsque la boucle sera terminée et que la liste liste_vals sera retournée au programme principal. Enfin, notez bien que la fonction calc_vals() appelle la fonction polynome() à chaque itération de la boucle. Ainsi, le programmeur est libre de faire tous les appels qu’il souhaite. Une fonction peut appeler une autre fonction, cette dernière peut appeler une autre fonction et ainsi de suite (et autant de fois qu’on le veut). Une fonction peut même s’appeler elle-même, cela s’appelle une fonction récursive (voir la rubrique suivante). Attention toutefois à retrouver vos petits si vous vous perdez dans les appels successifs !
Une fonction récursive est une fonction qui s’appelle elle-même. Les fonctions récursives permettent d’obtenir une efficacité redoutable dans la résolution de certains algorithmes comme le tri rapide 2 (en anglais quicksort). Oublions la recherche d’efficacité pour l’instant et concentrons-nous sur l’exemple de la fonction mathématique factorielle. Nous vous rappelons que la factorielle s’écrit avec un ! et se définit de la manière suivante :
def factoriel(n):
if n==0: return(1) # Je vous rappelle que par convention: 0!=1.
else: return(n*factoriel(n-1)) # Appel récursif.
factoriel(4)
Même si les fonctions récursives peuvent être ardues à comprendre, notre propos est ici de vous illustrer qu’une fonction qui en appelle une autre (ici il s’agit d’elle-même) reste « figée » dans le même état, jusqu’à ce que la fonction appelée lui renvoie une valeur.
Il est très important lorsque l’on manipule des fonctions de connaître la portée des variables (scope en anglais), c’est-à-dire savoir là où elles sont visibles. On a vu que les variables créées au sein d’une fonction ne sont pas visibles à l’extérieur de celle-ci car elles étaient locales à la fonction. Lorsqu’une variable est déclarée dans le programme principal, elle est visible dans celui-ci ainsi que dans toutes les fonctions. On a vu qu’on parlait de variable globale.
def ma_fonction(): print(x)
x=3
ma_fonction()
Dans ce cas, la variable x est visible dans le module principal et dans toutes les fonctions du module. Toutefois, Python ne permet pas la modification d’une variable globale dans une fonction. Si on veut vraiment modifier une variable globale dans une fonction, il faut utiliser le mot-clé global.
def ma_fonction ():
global x
x = x + 1
x = 1
ma_fonction()
x
Dans ce dernier cas, le mot-clé global a forcé la variable x à être globale plutôt que locale au sein de la fonction.
Pour ce chapitre, il vous faut impérativement importer le module random.
import random
Prenons par exemple la liste suivante contenant 5 fruits.
panier = ["Pomme", "Poire", "Banane", "Ananas", "Orange"]
panier
Si l'on veut prendre un élément au hasard de cette liste appelée "panier", on utilise la fonction random.choices().
random.choices(panier)
Attention! Cela renvoie une liste à un seul élément. Pour récupérer l'élément aléatoire, précisez l'indiçage [0] devant la fonction.
random.choices(panier)[0]
Si l'on veut maintenant choisir avec remise k éléments du panier (k peut donc être supérieur à la longueur de la liste si on veut), on utilise aussi la fonction random.choices( liste, k = ) en précisant la valeur de k en deuxième argument.
random.choices(panier, k=8)
Si l'on veut maintenant choisir sans remise k éléments du panier (où k doit donc être inférieur à la longueur de la liste), on utilise la fonction random.sample( liste, k = ). Attention, k doit être inférieur à la longueur de la liste.
random.sample(panier, k=2)
Pour commencer, la fonction random.random() va générer un nombre aléatoire compris entre 0 et 1.
random.random()
La fonction random.randint( a, b ) va générer un entier aléatoire compris entre a et b inclus.
random.randint(10, 20)
La fonction random.uniform( a, b ) va suivre la loi uniforme pour générer un flottant aléatoire entre a et b.
random.uniform(100, 101)
La fonction random.gauss(m, s) va suivre la distribution normale de moyenne m et d'écart-type s.
random.gauss(0, 1)
On considère deux jeux de hasard :
Le jeu A est clairement perdant. Le jeu B l'est aussi (vous pourrez le vérifier). À présent, on va mixer les deux ! En effet, à chaque tour, on lance une pièce (cette fois-ci...) équilibrée ! Si l'on a pile, on joue au jeu A, sinon on joue au jeu B.
On suppose que le joueur a 0 euros comme capital de départ. Après avoir joué 1.000.000 de parties, quel est le statut du jeu, du point de vue du joueur ?
def pieceEquilibree():
if random.uniform(0,1) <= 0.5: return 1
else: return -1
def pieceJeuA():
if random.uniform(0,1) <= 0.49: return 1
else: return -1
def pieceJeuB1():
if random.uniform(0,1) <= 0.09: return 1
else: return -1
def pieceJeuB2():
if random.random() <= 0.74: return 1
else: return -1
moy = 0
for i in range(10):
n = 1000000
gain = 0
for i in range(n):
jeu = pieceEquilibree()
if jeu == 1:
gain += pieceJeuA()
else:
if gain % 3 == 0:
gain += pieceJeuB1()
else:
gain += pieceJeuB2()
moy += gain
print("le gain net est de :", moy /10, "€")
Le gain est donc bel et bien positif, autour des 6.000 €. C'est ce que l'on appelle le paradoxe de Parrondo !
Si vous êtes à l'aise avec les DataFrames, voici une fonction pour en générer facilement.
import pandas as pd
df=pd.DataFrame({c: random.choices(range(-10, 10), k=4) for c in range(3)})
df
Dans ce chapitre, nous allons créer nos premières classes, nos premiers attributs et nos premières méthodes. Nous allons aussi essayer de comprendre les mécanismes de la programmation orientée objet en Python. Une classe est un peu un modèle suivant lequel on va créer des objets. C'est dans la classe que nous allons définir nos méthodes et attributs, les attributs étant des variables contenues dans notre objet.
Choix du modèle : Pour l'instant, nous allons modéliser une personne. Que va-t-on trouver dans les caractéristiques d'une personne ? Beaucoup de choses, vous en conviendrez. On ne va en retenir que quelques-unes :
Cela nous fait donc quatre attributs. Ce sont les variables internes à notre objet, qui vont le caractériser. Une personne telle que nous la modélisons sera caractérisée par son nom, son prénom, son âge et son lieu de résidence. Pour définir les attributs de notre objet, il faut définir un constructeur dans notre classe.
Pour définir une nouvelle classe, on utilise le mot-clé class. Sa syntaxe est assez intuitive : class NomDeLaClasse. N'exécutez pas encore ce code, nous ne savons pas comment définir nos attributs et nos méthodes.
Nous avons défini les attributs qui allaient caractériser notre objet de classe Personne. Maintenant, il faut définir dans notre classe une méthode spéciale, appelée un constructeur, qui est appelée invariablement quand on souhaite créer un objet depuis notre classe.
Concrètement, un constructeur est une méthode de notre objet se chargeant de créer nos attributs. En vérité, c'est même la méthode qui sera appelée quand on voudra créer notre objet.
class Personne: # Définition de notre classe Personne.
"""Classe définissant une personne caractérisée par son nom, son prénom, son âge et son lieu de résidence."""
def __init__(self): # Notre méthode constructeur. Pour l'instant, on ne va définir qu'un seul attribut.
self.nom = "Dupont"
Voyons en détail :
D'abord, la définition de la classe. Elle est constituée du mot-clé class, du nom de la classe et des deux points rituels « : ».
Une docstring commentant la classe. C'est une excellente habitude à prendre et je vous encourage à le faire systématiquement.
La définition de notre constructeur. Il s'agit d'une définition presque « classique » d'une fonction. Elle a pour nom init, toujours entourré de deux underscores des deux côtés et c'est invariable : en Python, tous les constructeurs s'appellent ainsi. Notez que, dans notre définition de méthode, nous passons un premier paramètre nommé self.
On va simplement définir un seul attribut pour l'instant dans notre constructeur. Nous trouvons l'instanciation de notre attribut nom. On crée une variable self.nom et on lui donne comme valeur Dupont. Je vais détailler un peu plus bas ce qui se passe ici.
Quand on tape Personne(), on appelle le constructeur de notre classe Personne, d'une façon quelque peu indirecte. Celui-ci prend en paramètre la variable self. Il s'agit de notre objet en train de se créer.
On écrit dans cet objet l'attribut nom le plus simplement du monde : self.nom = "Dupont". À la fin de l'appel au constructeur, Python renvoie notre objet self modifié, avec notre attribut. On va réceptionner le tout dans notre variable bertrand.
Nous allons alors créer un objet issu de notre classe.
bertrand = Personne()
bertrand.nom
Quand on demande à l'interpréteur d'afficher directement notre objet bertrand, il nous sort quelque chose d'un peu imbuvable. L'essentiel est la mention précisant la classe dont l'objet est issu. On peut donc vérifier que c'est bien notre classe Personne dont est issu notre objet. On essaye ensuite d'afficher l'attribut nom de notre objet bertrand et on obtient Dupont (la valeur définie dans notre constructeur). Notez qu'on utilise le point (.), toujours utilisé pour une relation d'appartenance. nom est un attribut de l'objet bertrand.
Notre constructeur pourrait éviter de donner les mêmes valeurs par défaut à chaque fois, tout de même! Dans un premier temps, on va se contenter de définir les autres attributs : le prénom, l'âge et le lieu de résidence.
class Personne:
def __init__(self): # Notre méthode constructeur.
"""Constructeur de notre classe. Chaque attribut va être instancié avec une valeur par défaut... original"""
self.nom = "Dupont"
self.prenom = "Jean"
self.age = 34
self.lieu_residence = "Paris"
jean = Personne()
print(jean.nom, jean.prenom, jean.age, "ans", jean.lieu_residence)
Dans beaucoup de tutoriels, on déconseille de modifier un attribut d'instance (un attribut d'un objet) comme on vient de le faire, en faisant simplement objet.attribut = valeur. Si vous venez d'un autre langage, vous pourrez avoir entendu parler des accesseurs et mutateurs. Ces concepts sont repris dans certains tutoriels Python, mais ils n'ont pas précisément lieu d'être dans ce langage. Pour l'instant, il vous suffit de savoir que, quand vous voulez modifier un attribut d'un objet, vous écrivez objet.attribut = nouvelle_valeur.
Il nous reste encore à faire un constructeur un peu plus intelligent. Pour l'instant, quel que soit l'objet créé, il possède les mêmes nom, prénom, âge et lieu de résidence. On peut les modifier par la suite, bien entendu, mais on peut aussi faire en sorte que le constructeur prenne plusieurs paramètres, disons… le nom et le prénom, pour commencer.
class Personne:
def __init__(self, nom, prenom):
"""Constructeur de notre classe"""
self.nom = nom
self.prenom = prenom
self.age = 25
self.lieu_residence = "Paris"
steve = Personne("Micado", "Steve")
print(steve.nom, steve.prenom, steve.age, "ans", steve.lieu_residence)
__N'oubliez pas que le premier paramètre doit être self.__ En dehors de cela, un constructeur est une fonction plutôt classique : vous pouvez définir des paramètres, par défaut ou non, nommés ou non. Quand vous voudrez créer votre objet, vous appellerez le nom de la classe en passant entre parenthèses les paramètres à utiliser.
Dans les exemples que nous avons vus jusqu'à présent, nos attributs sont contenus dans notre objet. Ils sont propres à l'objet : si vous créez plusieurs objets, les attributs nom, prenom,… de chacun ne seront pas forcément identiques d'un objet à l'autre. Mais on peut aussi définir des attributs dans notre classe.
class Compteur:
"""Cette classe possède un attribut de classe qui s'incrémente à chaque fois que l'on crée un objet de ce type"""
objets_crees = 0 # Le compteur vaut 0 au départ.
def __init__(self):
"""À chaque fois qu'on crée un objet, on incrémente le compteur"""
Compteur.objets_crees += 1
On définit notre attribut de classe directement dans le corps de la classe, sous la définition et la docstring, avant la définition du constructeur. Quand on veut l'appeler dans le constructeur, on préfixe le nom de l'attribut de classe par le nom de la classe. Et on y accède de cette façon également, en dehors de la classe.
print(Compteur.objets_crees)
a = Compteur() # On crée un premier objet.
print(Compteur.objets_crees)
b = Compteur() # On crée un deuxième objet.
print(Compteur.objets_crees)
À chaque fois qu'on crée un objet de typeCompteur, l'attribut de classe objets_crees s'incrémente de 1. Cela peut être utile d'avoir des attributs de classe, quand tous nos objets doivent avoir certaines données identiques. Nous aurons l'occasion d'en reparler par la suite.
Les attributs sont des variables propres à notre objet, qui servent à le caractériser. Les méthodes sont plutôt des actions, comme nous l'avons vu dans la partie précédente, agissant sur l'objet. Par exemple, la méthode append de la classe list permet d'ajouter un élément dans l'objet list manipulé.
Pour créer nos premières méthodes, nous allons modéliser un tableau noir. Notre tableau va posséder une surface (un attribut) sur laquelle on pourra écrire, que l'on pourra lire et effacer. Pour créer notre classe TableauNoir et notre attribut surface.
class TableauNoir:
"""Classe définissant une surface sur laquelle on peut écrire, que l'on peut lire et effacer, par jeu de méthodes.
L'attribut modifié est 'surface'"""
def __init__(self):
"""Par défaut, notre surface est vide"""
self.surface = ""
Nous avons déjà créé une méthode, aussi vous ne devriez pas être trop surpris par la syntaxe que nous allons voir. Notre constructeur est en effet une méthode, elle en garde la syntaxe. Nous allons donc écrire notre méthode ecrire pour commencer.
class TableauNoir:
def __init__(self):
self.surface = ""
def ecrire(self, message_a_ecrire):
"""Méthode permettant d'écrire sur la surface du tableau.
Si la surface n'est pas vide, on saute une ligne avant de rajouter le message à écrire"""
if self.surface != "":
self.surface += "\n"
self.surface += message_a_ecrire
tab = TableauNoir()
tab.ecrire("Vous savez, moi je ne crois pas qu'il y ait de bonnes ou de mauvaises situations.")
print(tab.surface)
print("_"*125)
tab.ecrire("Moi, si je devais résumer ma vie aujourd'hui avec vous, je dirais que c'est d'abord des rencontres.")
print(tab.surface)
Notre méthode ecrire charge d'écrire sur notre surface, en rajoutant un saut de ligne pour séparer chaque message.
On retrouve ici notre paramètre self. Il est temps de voir un peu plus en détail à quoi il sert.
Dans nos méthodes d'instance, qu'on appelle également des méthodes d'objet, on trouve dans la définition ce paramètre self. L'heure est venue de comprendre ce qu'il signifie.
Une chose qui a son importance : quand vous créez un nouvel objet, ici un tableau noir, les attributs de l'objet sont propres à l'objet créé. Si vous créez plusieurs tableaux noirs, ils ne vont pas tous avoir la même surface. Donc les attributs sont contenus dans l'objet.
En revanche, les méthodes sont contenues dans la classe qui définit notre objet. C'est très important. Quand vous tapeztab.ecrire(…), Python va chercher la méthode ecrire non pas dans objet tab, mais dans la classe TableauNoir.
TableauNoir.ecrire(tab, "Des gens qui m'ont tendu la main, à un moment où je ne pouvais pas, où j'étais seul chez moi.")
print(tab.surface)
Comme vous le voyez, quand vous tapez tab.ecrire(…), cela revient au même que si vous écrivez TableauNoir.ecrire(tab, …). Votre paramètre self, c'est l'objet qui appelle la méthode. C'est pour cette raison que vous modifiez la surface de l'objet en appelant self.surface.
Pour résumer, quand vous devez travailler dans une méthode de l'objet sur l'objet lui-même, vous allez passer par self.
Le nom self est une très forte convention de nommage. Je vous déconseille de changer ce nom. Certains programmeurs, qui trouvent qu'écrire self à chaque fois est excessivement long, l'abrègent en une unique lettres. Évitez ce raccourci. De manière générale, évitez de changer le nom. Une méthode d'instance travaille avec le paramètre self.
Nous devons encore coder lire qui va se charger d'afficher notre surface et effacer qui va effacer le contenu de notre surface. Si vous avez compris ce que je viens d'expliquer, vous devriez écrire ces méthodes sans aucun problème, elles sont très simples. Sinon, n'hésitez pas à relire, jusqu'à ce que le déclic se fasse.
class TableauNoir:
def __init__(self):
self.surface = ""
def ecrire(self, message_a_ecrire):
if self.surface != "":
self.surface += "\n"
self.surface += message_a_ecrire
def lire(self):
"""Cette méthode se charge d'afficher, grâce à print, la surface du tableau"""
print(self.surface)
def effacer(self):
"""Cette méthode permet d'effacer la surface du tableau"""
self.surface = ""
tab = TableauNoir()
tab.lire()
tab.ecrire("Salut tout le monde.")
tab.ecrire("La forme ?")
tab.lire()
tab.effacer()
tab.lire()
Et voilà ! Avec nos méthodes bien documentées, un petit coup de help(TableauNoir) et vous obtenez une belle description de l'utilité de votre classe. C'est très pratique, n'oubliez pas les docstrings.
Comme on trouve des attributs propres à la classe, on trouve aussi des méthodes de classe, qui ne travaillent pas sur l'instance self mais sur la classe même. C'est un peu plus rare mais cela peut être utile parfois. Notre méthode de classe se définit exactement comme une méthode d'instance, à la différence qu'elle ne prend pas en premier paramètre self (l'instance de l'objet) mais cls (la classe de l'objet).
En outre, on utilise ensuite une fonction built-in de Python pour lui faire comprendre qu'il s'agit d'une méthode de classe, pas d'une méthode d'instance.
class Compteur:
"""Cette classe possède un attribut de classe qui s'incrémente à chaque fois que l'on crée un objet de ce type"""
objets_crees = 0 # Le compteur vaut 0 au départ
def __init__(self):
"""À chaque fois qu'on crée un objet, on incrémente le compteur"""
Compteur.objets_crees += 1
def combien(cls):
"""Méthode de classe affichant combien d'objets ont été créés"""
print("Jusqu'à présent, {} objets ont été créés.".format(
cls.objets_crees))
combien = classmethod(combien)
Compteur.combien()
a = Compteur()
Compteur.combien()
b = Compteur()
Compteur.combien()
Une méthode de classe prend en premier paramètre non pas self mais cls. Ce paramètre contient la classe (ici Compteur).
Notez que vous pouvez appeler la méthode de classe depuis un objet instancié sur la classe. Vous auriez par exemple pu écrire a.combien().
Enfin, pour que Python reconnaisse une méthode de classe, il faut appeler la fonction classmethod qui prend en paramètre la méthode que l'on veut convertir et renvoie la méthode convertie.
Si vous êtes un peu perdus, retenez la syntaxe de l'exemple. La plupart du temps, vous définirez des méthodes d'instance comme nous l'avons vu plutôt que des méthodes de classe.
On peut également définir des méthodes statiques. Elles sont assez proches des méthodes de classe sauf qu'elles ne prennent aucun premier paramètre, ni self ni cls. Elles travaillent donc indépendamment de toute donnée, aussi bien contenue dans l'instance de l'objet que dans la classe.
Voici la syntaxe permettant de créer une méthode statique.
class Test:
"""Une classe de test tout simplement"""
def afficher():
"""Fonction chargée d'afficher quelque chose"""
print("On affiche la même chose.")
print("peu importe les données de l'objet ou de la classe.")
afficher = staticmethod(afficher)
Si vous vous emmêlez un peu avec les attributs et méthodes de classe, ce n'est pas bien grave. Retenez surtout les attributs et méthodes d'instance, c'est essentiellement sur ceux-ci que je me suis attardé et c'est ceux que vous retrouverez la plupart du temps.
Les noms de méthodes encadrés par deux soulignés de part et d'autre sont des méthodes spéciales. Ne nommez pas vos méthodes ainsi. Nous découvrirons plus tard ces méthodes particulières.
La première technique d'introspection que nous allons voir est la fontction dir. Elle prend en paramètre un objet et renvoie la liste de ses attributs et méthodes.
class Test:
"""Une classe de test tout simplement"""
def __init__(self):
"""On définit dans le constructeur un unique attribut"""
self.mon_attribut = "ok"
def afficher_attribut(self):
"""Méthode affichant l'attribut 'mon_attribut'"""
print("Mon attribut est {0}.".format(self.mon_attribut))
# Créons un objet de la classe Test
un_test = Test()
un_test.afficher_attribut()
dir(un_test)
La fonction dir renvoie une liste comprenant le nom des attributs et méthodes de l'objet qu'on lui passe en paramètre. Vous pouvez remarquer que tout est mélangé, c'est normal : pour Python, les méthodes, les fonctions, les classes, les modules sont des objets. Ce qui différencie en premier lieu une variable d'une fonction, c'est qu'une fonction est exécutable (callable). La fonction dir se contente de renvoyer tout ce qu'il y a dans l'objet, sans distinction.
Par défaut, quand vous développez une classe, tous les objets construits depuis cette classe posséderont un attribut spécial dict. Cet attribut est un dictionnaire qui contient en guise de clés les noms des attributs et, en tant que valeurs, les valeurs des attributs.
un_test = Test()
un_test.__dict__
C'est un attribut un peu particulier car ce n'est pas vous qui le créez, c'est Python. Il est entouré de deux signes soulignés de part et d'autre, ce qui traduit qu'il a une signification pour Python et n'est pas un attribut « standard ». Vous verrez plus loin dans ce cours des méthodes spéciales qui reprennent la même syntaxe.
Vous pouvez modifier ce dictionnaire. Sachez qu'en modifiant la valeur de l'attribut, vous modifiez aussi l'attribut dans l'objet.
un_test.__dict__["mon_attribut"] = "plus ok"
un_test.afficher_attribut()
De manière générale, ne faites appel à l'introspection que si vous avez une bonne raison de le faire et évitez ce genre de syntaxe.
Il est quand même plus propre d'écrire objet.attribut = valeurqueobjet.__dict__[nom_attribut] = valeur.
Nous n'irons pas plus loin dans ce chapitre. Je pense que vous découvrirez dans la suite de ce livre l'utilité des deux méthodes que je vous ai montrées.
Les dictionnaires sont très pratiques lorsque vous manipulez des structures complexes à décrire et que les listes présentent leurs limites.
Les dictionnaires sont des collections non ordonnées d’objets, c’est-à-dire qu’il n’y a pas de
notion d’ordre (i.e. pas d’indice).
On accède aux valeurs d’un dictionnaire par des clés.
ani1 = {}
ani1["nom"] = "girafe"
ani1["taille"] = 5.0
ani1["poids"] = 1100
ani1
En premier, on définit un dictionnaire vide avec les accolades { } (tout comme on peut le faire pour les listes avec []).
Ensuite, on remplit le dictionnaire avec différentes clés ("nom", "taille", "poids") auxquelles on affecte des valeurs
("girafe", 5.0, 1100).
Vous pouvez mettre autant de clés que vous voulez dans un dictionnaire (tout comme vous pouvez ajouter autant d’éléments que vous voulez dans une liste). Un dictionnaire est affiché sans ordre particulier.
On peut aussi initialiser toutes les clés et les valeurs d’un dictionnaire en une seule opération.
ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
ani2
Mais rien ne nous empêche d’ajouter une clé et une valeur supplémentaire.
ani2["age"]=15
ani2
Pour récupérer la valeur associée à une clé donnée, il suffit d’utiliser la syntaxe suivante dictionnaire["cle"].
ani2["nom"]
Il est possible d’obtenir toutes les valeurs d’un dictionnaire à partir de ses clés.
ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
for key in ani2: print(key, ani2[key])
Les méthodes .keys() et .values() renvoient, comme vous pouvez vous en doutez, les clés et les valeurs d’un dictionnaire.
ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
ani2
ani2.keys()
ani2.values()
Les mentions dict_keys et dict_values indiquent que nous avons à faire à des objets un peu particuliers. Ils ne sont pas indexables (on ne peut pas retrouver un élément par indice, par exemple dico.keys()[0] renverra une erreur). Si besoin, nous pouvons les transformer en liste avec la fonction list().
list(ani2.values())
Toutefois, ce sont des objets « itérables », donc utilisables dans une boucle.
Enfin, il existe la méthode items() qui renvoie un nouvel objet dict_items.
dico = {0: "t", 1: "o", 2: "t", 3: "o"}
dico.items()
Celui-ci n’est pas indexable (on ne peut pas retrouver un élément par un indice) mais il est itérable. Notez la syntaxe particulière qui ressemble à la fonction enumerate() vue au chapitre 5 Boucles et comparaisons. On itère à la fois sur key et sur val. On verra plus bas que cela peut-être utile pour construire des dictionnaires de compréhension.
Pour vérifier si une clé existe dans un dictionnaire, on peut utiliser le test d’appartenance avec l’instruction in qui renvoie un booléen.
if "poids" in ani2: print("La clef n'existe pas dans ani2.")
if "singe" in ani2: print("C'est pas une clef, justement!")
En créant une liste de dictionnaires qui possèdent les mêmes clés, on obtient une structure qui ressemble à une base de données.
ani1 = {'nom': 'girafe', 'taille': 5.0, 'poids': 1100}
ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
animaux = [ani1, ani2]
animaux
for ani in animaux: print(ani["nom"])
Vous constatez ainsi que les dictionnaires permettent de gérer des structures complexes de manière plus explicite que les listes.
La fonction dict() va convertir l’argument qui lui est passé en dictionnaire. Il s’agit donc d’une fonction de casting comme int(), str(), etc. Toutefois, l’argument qui lui est passé doit avoir une forme particulière : un objet séquentiel contenant d’autres objets séquentiels de 2 éléments.
liste_animaux = [["girafe", 2], ["singe", 3]]
dict(liste_animaux)
Ou un tuple de tuples de 2 éléments (cf. rubrique suivante pour la définition d’un tuple), ou encore une combinaison liste / tuple.
tuple_animaux = (("girafe", 2), ("singe", 3))
dict(tuple_animaux)
Les tuples (« n-uplets » en français) correspondent aux listes à la différence qu’ils sont non modifiables.
On a vu que les listes pouvaient être modifiées par références, notamment lors de la copie de listes. Les tuples s’affranchissent de ce problème puisqu’ils sont non modifiables. Pratiquement, ils utilisent les parenthèses au lieu des
crochets .
x = (1, 2, 3) ; x
x[1]
x[0:2]
L’affectation et l’indiçage fonctionnent comme avec les listes. Mais si on essaie de modifier un des éléments du tuple, Python renvoie un message d’erreur. Si vous voulez ajouter un élément (ou le modifier), vous devez créer un autre tuple.
x=x + (4,)
x
Pratiquement, nous avons déjà croisé les tuples avec la fonction enumerate(). Cette dernière permettait d’itérer en même temps sur les indices et les éléments d’une liste.
for i, elt in enumerate ([75 , -75, 0]):
print(i, elt)
for obj in enumerate ([75 , -75, 0]):
print(obj, type(obj))
En fin de compte, la fonction enumerate() itère sur une série de tuples.
Pouvoir séparer i et elt dans la boucle est possible du fait que Python autorise l’affectation multiple du style i, elt = 0, 75. Dans le même ordre d’idée, nous avons vu à la rubrique précédente la méthode .dict_items() qui permettait d’itérer
sur des couples clé / valeur d’un dictionnaire.
dico = {"pinson ": 2, "merle ": 3}
for key , val in dico . items ():
print (key, val)
On voit que cette méthode .dict_items() itère comme enumerate() sur une série de tuples.
Sur la même base, on peut finalement itérer sur 3 valeurs en même temps à partir d’une liste de tuples de 3 éléments.
liste = [(i, i+1, i +2) for i in range (5, 8)]
liste
for x, y, z in liste :
print (x, y, z)
On pourrait concevoir la même chose sur 4 éléments, ou finalement autant que l’on veut. La seule restriction est d’avoir une correspondance systématique entre le nombre de variables d’itération (par exemple 3 ci-dessus avec x, y, z) et la longueur de chaque sous-tuple de la liste sur laquelle on itère (chaque sous-tuple a 3 éléments ci-dessus).
L’affectation multiple est un mécanisme très puissant et important en Python. Pour rappel, il permet d’effectuer sur une même ligne plusieurs affectations en même temps, par exemple : x, y, z = 1, 2, 3. On voit que cette syntaxe correspond à un tuple de chaque côté de l’opérateur =. Notez qu’il serait possible de le faire également avec les listes : [x, y, z] = [1, 2, 3]. Toutefois, cette syntaxe est alourdie par la présence des crochets. On préfèrera donc la première syntaxe avec les tuples sans parenthèse.
Remarque: Nous avons appelé l’opération x, y, z = 1, 2, 3 affectation multiple pour signifier que l’on affectait des valeurs à
plusieurs variables en même temps. Toutefois, vous pourrez rencontrer aussi l’expression tuple unpacking que l’on pourrait
traduire par « désempaquetage de tuple ». Cela signifie que l’on décompose le tuple initial 1, 2, 3 en 3 variables différentes
(comme si on vidait son sac à dos, d’où le terme désempaquetage !).
Nous avions croisé l’importance de l’affectation multiple lorsqu’une fonction renvoyait plusieurs valeurs.
def fct():
return 3, 14
x, y = fct()
print(x, y)
La syntaxe x, y = fct() permet de récupérer les 2 valeurs renvoyées par la fonction et de les affecter à la volée dans 2 variables différentes. Cela évite l’opération laborieuse de récupérer d’abord le tuple, puis de créer les variables en utilisant l’indiçage.
resultat = fct ()
x = resultat [0]
y = resultat [1]
print (x, y)
Quand une fonction renvoie plusieurs valeurs mais que l’on ne souhaite pas les utiliser toutes dans la suite du code, on peut utiliser le nom de variable _ (underscore) pour indiquer que certaines valeurs ne nous intéressent pas.
def fct():
return 1, 2, 3, 4
x, _, y, _ = fct ()
print(x, y)
Cela envoie le message à celui qui lit le code « je me fiche des valeurs récupérées dans ces variables _ ». Notez que l’on peut utiliser une ou plusieurs variables underscores(s). Dans l’exemple ci-dessus, la 2ème et la 4ème variable renvoyées par la fonction seront ignorées dans la suite du code. Cela a le mérite d’éviter la création de variables dont on ne se sert pas.
Le underscore est couramment utilisé dans les noms de variable pour séparer les mots et être explicite, par exemple seq_ADN ou liste_listes_residus. On verra que ce style de nommage est appelé snake_case. Toutefois, il faut éviter d’utiliser les underscores en début et/ou en fin de nom de variable (e.g. var, var, var, var__). On verra au chapitre 19 Avoir la classe avec les objets que ces underscores ont une signification particulière.
Les containers de type set représentent un autre type d’objet séquentiel qui peut se révéler très pratique. Ils ont la particularité d’être non modifiables, non ordonnés et de ne contenir qu’une seule copie maximum de chaque élément. Pour créer un nouveau set on peut utiliser les accolades { }.
s = {1, 2, 3, 4}
s
Notez que la répétition du 3 dans la définition du set en ligne 1 donne au final un seul 3 car chaque élément ne peut être présent qu’une seule fois. A quoi différencie-t-on un set d’un dictionnaire alors que les deux utilisent des accolades ? Le set sera défini seulement par des valeurs {val1, val2, ...} alors que le dictionnaire aura toujours des couples clé/valeur {clé1: val1, clé2: val2, ...}. En général, on utilisera la fonction interne à Python set() pour générer un nouveau set. Celle-ci prend en argument n’importe quel objet itérable et le convertit en set.
set ( range (5))
Nous avons dit plus haut que les sets ne sont pas ordonnés, il est donc impossible de récupérer un élément par sa position. Il est également impossible de modifier un de ses éléments. Par contre, les sets sont itérables.
Les containers de type set sont très utiles pour rechercher les éléments uniques d’une suite d’éléments. Cela revient à éliminer tous les doublons.
l = [random.randint(0, 9) for i in range (10)]
l
On peut bien sûr transformer dans l’autre sens un set en liste. Cela permet par exemple d’éliminer les doublons de la liste initiale tout en récupérant une liste à la fin.
list(set([7 , 9, 6, 6, 7, 3, 8, 5, 6, 7]))
On peut faire des choses très puissantes. Par exemple, un compteur de lettres en combinaison avec une liste de compréhension, le tout en une ligne !
seq = "atctcgatcgatcgcgctagctagctcgccatacgtacgactacgt"
set(seq)
[( base , seq . count ( base )) for base in set ( seq )]
Les sets permettent aussi l’évaluation d’union ou d’intersection mathématiques en conjonction avec les opérateurs respectivement | et &.
L = [3, 3, 5, 1, 3, 4, 1, 1, 4, 4]
L2 = [3, 0, 5, 3, 3, 1, 1, 1, 2, 2]
set(L) & set(L2)
set(L) | set(L2)
Il est également possible de générer des dictionnaires de compréhension.
dico = {"a ": 10, "g ": 10, "t ": 11, "c ": 15}
dico.items()
{key : val *2 for key , val in dico . items ()}
{key : val for key , val in enumerate (" toto ")}
seq = " atctcgatcgatcgcgctagctagctcgccatacgtacgactacgt "
{base : seq . count ( base ) for base in set ( seq )}
De manière générale, tout objet sur lequel on peut faire une double itération du type for var1, var2 in obj est utilisable pour créer un dictionnaire de compréhension. Si vous souhaitez aller plus loin, vous pouvez consulter cet article 3 sur le site Datacamp. Il est également possible de générer des sets de compréhension sur le même modèle que les listes de compréhension.
{i for i in range (11)}
{i**2 for i in range (11)}
Dans ce chapitre, vous devrez importer les modules numpy et pandas, qu'on nommera usuellement ici respectivement np et pd.
import numpy as np
import pandas as pd
Une série est un vecteur de valeurs d'une variable.
Une série se programme en Python par la fonction pandas.Series() (ici pd.Series()), et présente automatiquement une indexation des valeurs qu'elle contient selon l'ordre dans lequel on les rentre.
s = pd.Series([101, 202, 303])
s
Nous avons ici créé une série de valeurs entières, mais une série peut très bien contenir des flottants ou des chaînes de caractères. On peut préciser le type souhaité d'une série avec le paramètre dtype.
s = pd.Series([101, 202, 303], dtype = float)
s
On peut mettre la valeur numpy.nan pour les valeurs non déterminées (qui apparaissent alors comme NaN).
s = pd.Series([101, np.NaN, 202, 304])
s
On peut également créer une série à partir d'une liste.
L = [4, 4, 5, 5]
s = pd.Series(L)
s
Comme pour les listes, on peut aussi créer une série avec la fonction range().
s = pd.Series(range(95, 101, 2))
s
On peut également créer une série à valeurs répétées avec le paramètre index.
s = pd.Series(4, index=range(3))
s
On peut déterminer la taille d'une série avec la fonction len() ou bien avec .size().
s = pd.Series(range(4))
len(s)
Les paramètres statistiques d'une série sont visibles avec la fonction .describe().
s = pd.Series([4, 5, 5, 1, 3, 2, 2, 2, 2, 4])
s.describe()
Une série se comporte de façon très similaire à une array numpy et aussi un dictionnaire.
s = pd.Series(["zéro", "un", "deux", "trois", "quatre", "cinq", "six"])
s
On récupère une valeur dans une série de la même manière que dans les listes.
s[2]
Il est courant d'utiliser .iloc[] aussi pour récupérer la valeur correspondante à l'indice.
s.iloc[2]
On peut aussi en renvoyer plusieurs en utilisant doublement les crochets [[ ]].
s[[1, 3, 5, 0]]
On renvoie avec s[a:b] la série avec les valeurs des indices a inclus à b exclus.
s[2:5]
On renvoie avec s[:b] la série avec les valeurs des indices 0 inclus à b exclus.
s[:3]
On renvoie avec s[a:] la série avec les valeurs à partir de l'indice a inclus.
s[3:]
En soi, une série peut être vue comme un tableau à une seule ligne. Un tableau simple se crée donc de la même façon.
a = np.array([1, 2, 3.5])
a
Un array se tiendra sur plusieurs lignes comme un enchaînement de listes.
a = np.array([[1, 2, 3], [4, 5, 6]])
a
Un array contient donc un certain nombre de lignes et de colonnes. Basons-nous sur l'array ci-dessous.
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
a
On obtient la taille totale de l'array avec la fonction .size.
a.size
On obtient les dimensions de l'array avec la fonction .shape.
a.shape
Basons-nous sur l'array ci-dessous (c'est le même en fait...).
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
a
Attention, le 0 compte toujours! On accède donc à l'élément de la i-ème ligne et de la j-ème colonne avec la localisation a[i-1, j-1].
a[2, 1]
On peut utiliser une boucle pour modifier les séries.
s = pd.Series(range(5))
for i in s.index:
s[i]*=10
s
On utilise une double boucle pour les arrays.
a = np.array([[3, 9, 1, 2, 8, 6],
[4, 8, 7, 3, 5, 9],
[6, 5, 2, 7, 1, 4],
[8, 7, 5, 4, 3, 1]])
for i in range(a.shape[0]):
for j in range(a.shape[1]):
a[i][j]*=10
a
Repérer les NaN se fait avec la fonction isnull().
s = pd.Series([101, np.NaN, 202, 304])
s.isnull()
Dans ce chapitre, vous devrez au importer les modules numpy et pandas, qu'on nommera usuellement ici respectivement np et pd.
import numpy as np
import pandas as pd
Un dataframe se comporte comme un dictionnaire dont les clefs sont les noms des colonnes et les valeurs sont des séries. Il se compose de lignes caractérisées par son index, et de colonnes caractérisées par leur nom.
Ci-dessous la nomenclature standard pour créer un DataFrame.
df = pd.DataFrame( { "alpha": [4, 3, 2, 1],
"beta": [11, 21, 31, 41],
"gamma": [100, 200, 300, 400] },
index=["a", "b", "c", "d"])
df
On peut également, après coup, renommer les indices et les colonnes d'un DataFrame.
df.index = range(4)
df.columns = ["De Gaulle", "Pompidou", "Giscard"]
df
Vous pouvez aussi ne renommer qu'une colonne spécifique avec la méthode .rename().
df.rename(columns={"Giscard" : "Chirac"})
Vous pouvez sinon créer un DataFrame à partir d'un array déjà existant.
a = np.array([[3, 9, 1, 2, 8, 6],
[4, 8, 7, 3, 5, 9],
[6, 5, 2, 7, 1, 4],
[8, 7, 5, 4, 3, 1]])
df = pd.DataFrame( a, index=range(4), columns=["un", "deux", "trois", "quatre", "cinq", "six"] )
df
Vous pouvez aussi créer un DataFrame à partir de listes existantes, pourvu que celles-ci aient la même longueur.
L1 = ["Salut,", "J'aime", "Allez"]
L2 = ["ça", "les", "les"]
L3 = ["va?", "pâtes.", "Bleus!"]
df = pd.DataFrame({"^_^" : L1,
"è_é" : L2,
"O_o" : L3})
df
On peut aussi utiliser une liste de dictionnaires (manuellement, c'est pas très pratique mais on peut!).
df = pd.DataFrame([{'A': 1.1, 'B': 2, 'C': 3.3, 'D': 4},
{'A': 2.7, 'B': 10, 'C': 5.4, 'D': 7},
{'A': 5.3, 'B': 9, 'C': 1.5, 'D': 15}])
df
Vous pouvez générer un DataFrame à partir d'un tableau csv existant dans le dossier parent du noyau de votre interpréteur Python, Jupyter Notebook me concernant. Pour ce faire, il vous faut utiliser pd.read_csv("eventuel_document/mon_fichier.csv").
Tenez, si vous ne connaissez pas encore toutes les capitales océaniques, voici pour vous une occasion de les apprendre. 😉
oceanie = pd.read_csv("data/tuto/oceanie.csv")
oceanie
pd.read_csv("myFile.csv") : par défaut, suppose qu'il y a un header (header=0) et qu'il n'y a pas de noms de colonne (index_col=None).
sep = '\t' ou delimiter = '\t' : indique que le séparateur est une tabulation plutôt qu'une virgule.
df = pandas.read_csv('myFile.csv', sep = ' ', header = None) : lecture d'une table sans header.
Si tableau avec étiquettes de lignes et de colonnes : df = pandas.read_csv('myFile.csv', sep = '\t', index_col = 0)
index_col est en fait le numéro de colonne contenant l'index.
Si tableau avec étiquettes de colonnes seulement : df = pandas.read_csv('myFile.csv', sep = '\t').
Si tableau avec étiquettes de lignes seulement : df = pandas.read_csv('myFile.csv', sep = '\t', header = None, index_col = 0).
Si tableau sans étiquettes de lignes ni de colonnes : df = pandas.read_csv('myFile.csv', sep = '\t', header = None).
df = pandas.read_csv(fileName, sep = '\t', converters = {'barcode': str} ): convertit la colonne barcode avec la fonction str.
On peut imposer des noms aux colonnes :
pd.read_csv('myFile.csv', sep = '\t', na_values = ['-']) : on donne avec na_values la liste des valeurs qui doivent être assimilées à NaN :
Pour préciser les types de certaines colonnes : read_csv(myFile, sep = '\t', dtype = {'col1': str, 'col2': int, 'col4': numpy.float}).
On peut lire seulement le début d'un tableau : df = pandas.read_csv(..., nrows = 1000) (utile pour le debugging).
df = pandas.read_csv(myFile, sep = ' ', usecols = range(12)) : lit seulement les 12 premières colonnes.
df = pandas.read_csv(fileName, sep = ' ', usecols = ['A', 'C', 'G'] : lit seulement certaines colonnes (on doit toutes les préciser par index, ou toutes par nom).
df = pandas.read_csv('myFile.csv', sep = '\t', skiprows = [0, 1]) : lit le fichier en sautant les lignes 0 et 1 du fichier (donc ici, le header est sur la 3ème ligne, d'index 2). Très utile si ces lignes contiennent des intitulés qui perturbent alors complètement le typage de chaque colonne !
skip_footer = 5 : saute les 5 dernières lignes du fichier.
Attention : si nom de colonne répété, le deuxième est modifié avec une extension .1, le troisième avec une extension .2, etc ...
Pour lire les 10 premières lignes en ayant sauté les 5 premières après le header :
Écriture d'un dataframe dans un fichier :
Créons et observons le DataFrame suivant qui nous servira à plusieurs reprises. Il renvoit une liste de personnes catégorisées selon les colonnes par:
Évitez généralement de mettre des accents dans les noms des colonnes, nous y reviendrons.
df = pd.DataFrame( {"nom": ["Aurore", "Bertrand", "Christine", "Denis", "Eudes", "François", "Géraldine", "Henry"],
"age": [37, 29, 24, 40, 18, 52, 40, 28],
"salaire": [4500, 3200, 1500, 2600, 900, 3500, 2900, 2900],
"ville": ["Paris", "Paris", "Bordeaux", "Strasbourg", "Paris", "Strasbourg", "Strasbourg", "Bordeaux"],
"departement": [75, 75, 33, 67, 75, 67, 67, 33]})
df
Ce DataFrame est indexé par défaut (par les entiers de 1 à 7). Mais comme chaque prénom est différent, on peut l'indexé par la colonne "nom" avec la fonction .set_index().
df.set_index("nom")
Cependant, nous venons de demander à Python de nous afficher df indexé par les noms, mais en aucun cas nous lui avons demandé d'effectuer une quelconque modification sur le DataFrame. Nous lui avons demandé de nous renvoyer un résultat, et non d'effectuer une action sur nos données. En effet, si l'on affiche df à nouveau, celui-ci se présentera comme à son état d'origine.
df
Si vous voulez définivement modifier df, vous devez alors ajouter le paramètre inplace=True, qui effectuera donc la modification définitive sur votre DataFrame.
df.set_index("nom", inplace=True)
df
Admettons maintenant que l'on veuille réinitialiser l'index par défaut. Pour cela, il vous faudra utiliser la fonction .reset_index() aussi avec la mention inplace=True si vous voulez que ce soit modifié, sinon, Python se contentera de vous afficher le DataFrame sans modification généralisée comme avec .set_index().
df.reset_index(inplace=True)
df
Imaginez maintenant que vous ayez un DataFrame aux indices complètement mal fichus (ce qui vous arrivera souvent, croyez-moi!) un peu comme dans l'exemple suivant.
df.index = random.choices(range(200, 1000), k=8)
df
Si vous exéctuez .reset_index() avec inplace=True, votre DataFrame conservera en colonne l'index précédent.
df.reset_index(inplace=True)
df
Si cela ne vous intéresse pas de conserver cet index, il vous faut ajouter le paramètre drop=True en plus de inplace=True.
df.set_index("index", inplace=True) # On réindexe le DataFrame par la colonne "index".
df
df.reset_index(drop=True, inplace=True) # Et là on vous montre la réinitialisation de l'index sans conservation.
df
Enfin, on peut aussi réorganiser les colonnes comme bon nous semble avec la fonction .reindex(columns=[]).
df.reindex(columns=["salaire", "age", "nom", "departement", "ville"])
Lorsque vous manipulerez des DataFrames au volume massif (par exemple de l'ordre de 30 mille lignes et 100 colonnes, c'est très courant!), vous serez souvent amener à récupérer une donnée bien ancrée au beau milieu du tableau. Plutôt que de passer 1h sur votre fichier Excel à repérer désespérément cette valeur, Python vous offre des fonctions de localisation vous permettant d'y accéder en une ligne de code. On rappelle que l'on se base sur le DataFrame suivant.
df
Si vous souhaitez connaître les données d'une personne en particulier, et que vous connaissez l'indice de la ligne correspondante, vous pouvez utiliser la fonction .iloc[]. On peut par exemple observer les informations de Géraldines car on sait que l'indice de sa ligne est 6.
df.iloc[6]
Mais si vous avez un tableau massif, et qu'il est difficile de retrouver cet indice, il vous est possible de retrouver la ligne que vous cherchez en rentrant une donnée spécifique à cette ligne (le nom dans notre exemple) en utilisant la fonction .loc[] où il vous faudra préciser à l'intérieur des crochets le(s) paramètre(s) spécifique(s) souhaité(s). Dans notre exemple, on veut que le "nom" soit "Géraldine".
df.loc[df["nom"]=="Géraldine"]
Veillez bien à utiliser le symbole double == et non = car il s'agit d'un test et non d'une égalité.
Si vous voulez récupérer uniquement la valeur spécifique d'une colonne de cette ligne, vous pouvez attribuer à la fonction .loc[] un deuxième attribut qui sera le nom de la colonne en question. Par exemple, on peut récupérer le numéro de département de Géraldine de la manière suivante.
df.loc[df["nom"]=="Géraldine", "departement"]
Si la condition que vous rentrez dans la fonction n'est pas unique, vous obtiendrez la restriction du DataFrame à la condition en vigueur. Par exemple, affichons l'ensemble des individus qui habite à Paris.
df.loc[df["ville"]=="Paris"]
On peut aussi en tirer les valeurs d'une colonne spécifique à cette restriction, comme les salaires des parisiens, ici.
df.loc[df["ville"]=="Paris", "salaire"]
Rappelons le DataFrame df, une fois n'est pas coutume.
df
Pour commencer, voici une méthode qui vous permettra de n'afficher que les colonnes qui vous intéressent. Il vous suffit de réécrire le nom du DataFrame suivit des doubles crochets [[ ]] et d'y reporter les noms des colonnes qui vous intéressent. Prenons par exemple le cas de df où l'on ne souhaite qu'afficher les noms et les salaires.
df[["nom", "salaire"]]
Les fonctions suivantes renvoient une restriction spécifique du DataFrame.
Attention! Je vous rappelle que la première ligne et la première colonne sont en réalité la ligne 0 et la colonne 0 pour Python. Par exemple, si vous regradez le tableau, la 2ème ligne que vous voyez sera la ligne 1, la 1ère ligne étant la ligne 0, etc...
df[:] → renvoie l'ensemble du DataFrame.
df.loc[:] # Renvoie tout le DataFrame.
df[ a ] → renvoie la ligne a.
df.loc[4] # Renvoie les données de la ligne 4.
df.loc[ : a ] → renvoie les lignes 0 à a incluses.
df.loc[:4] # Renvoie les lignes 0 à 4.
df.loc[ a : ] → renvoie les lignes a à la dernière incluses.
df.loc[4:] # Renvoie le tableau à partir de la ligne 4.
df.loc[ a : b ] → renvoie les lignes a à b incluses.
df.loc[3:6] # Renvoie le tableau de la 4ème à la 7ème ligne.
df.iloc[ : , c : d ] → renvoie l'ensemble des colonnes c incluse à d non incluse.
df.iloc[:, 2:4] # Renvoie l'ensemble des colonnes 2 incluste à 4 non incluse.
df.iloc[ : , c ] → renvoie l'ensemble de la colonne c.
df.iloc[:, 3] # Renvoie l'ensemble de la colonne 3 (ville).
df.loc[ a , : ] → renvoie les données de la ligne a.
df.loc[3, :] # Renvoie l'ensemble de la ligne 3.
df.iloc[ a : b , c : d ] → renvoie l'ensemble les lignes a incluse à b non incluse et les colonnes c incluse à d non incluse.
df.iloc[3:6, 2:4] # Renvoie les lignes 3 à 5 et les colonnes 2 à 3.
Les opérateurs & pour ET et | pour OU entrent en compte si l'on veut restreindre le DataFrame selon plusieurs conditions.
df
Si l'on veut, par exemple, ne garder que le moins de 30 ans ET qui habitent à Paris, on effectue une localisation double avec l'opérateur &. La restriction ne conservera que les lignes où ces conditions sont toutes deux vérifiées.
df.loc[(df["age"]<30) & (df["ville"]=="Paris")]
Si l'on veut, maintenant, ne garder que le moins de 30 ans OU qui habitent à Paris, on effectue une localisation double avec l'opérateur |. La restriction ne conservera que les lignes où au moins une de ces conditions est vérifiée.
df.loc[(df["age"]<30) | (df["ville"]=="Paris")]
Enfin, on peut afficher plus facilement les n premières lignes ou bien les n dernières lignes d'un DataFrame respectivement avec les fonctions .head(n) et .tail(n).
df.head(2)
df.tail(2)
Refaisons df, en plus petit pour y voir plus clair, où l'on ne connaît pas les âges de Bertrand et Christine ni le salaire cette dernière. Ces valeurs sont donc représentées dans le tableau par des NaN.
df = pd.DataFrame( {"nom": ["Aurore", "Bertrand", "Christine", "Denis"],
"age": [30, np.NaN, np.NaN, 40],
"salaire": [4500, 3200, np.NaN, 2600],
"ville": ["Paris", "Paris", "Bordeaux", "Strasbourg"],
"departement": [75, 75, 33, 67]})
df
La fonction .fillna(a) remplace les valeurs manquantes par a. N'oubliez pas d'apporter la mention inplace=True si vous voulez conserver cette modification. On remplace souvent les valeurs manquantes par 0. Vous serez donc parfois amenés à écrire df.fillna(0, inplace=True).
df.fillna(0)
Si vous voulez vous débarasser des lignes contenant une valeur nulle, vous pouvez utiliser la fonction .dropna().
df.dropna()
Si vous voulez vous débarasser des colonnes contenant une valeur nulle, vous pouvez utiliser la fonction .dropna(axis=1).
df.dropna(axis=1)
Si vous voulez maintenant repérer à quelle ligne il manque telle ou telle donnée (dans le cas d'un tableau massif), il vous faudra utiliser la localisation df.loc[ df[ "nom_colonne" ].isnull() ]. Repérons par exemple les individus où on n'a pas leur âge.
df.loc[df["age"].isnull()]
Pour cette partie, prenons un DataFrame ne comportant que des valeurs numériques. On le génère aléatoirement comme montré dans la partie C.3 du chapitre sur les fonctions.
df = pd.DataFrame({c: random.choices(range(1, 21), k=4) for c in range(4)})
df.columns = ["alpha", "beta", "gamma", "delta"]
df
On peut aussi déterminer les extremums sur les colonnes numériques :
print(list(df["alpha"]))
print("Minimum : ", df["alpha"].min())
print("Maximum : ", df["alpha"].max())
On peut aussi déterminer les mesures de tendance centrale avec :
print(list(df["alpha"]))
print("Moyenne : ", df["alpha"].mean())
print("Écart-type : ", df["alpha"].std())
print("Variance : ", df["alpha"].var())
On peut obtenir les pourcentages sur les lignes par une simple opération.
100*(df.T / df.T.sum()).T
Il est encore plus simple d'obtenir les pourcentages sur les colonnes.
100*df/df.sum()
On peut aussi facilement afficher les valeurs centrées-réduites.
(df - df.mean()) / df.std(ddof = 0)
Reprenons un DataFrame numérique aléatoire. Gardez en tête que pour toute modification définitive, il faut préciser inplace=True. Reprenons un DataFrame numérique aléatoire, bien que les algorithmes de tri peuvent aussi se faire selon l'ordre alphabétique des chaînes de caractères.
df = pd.DataFrame({c: random.choices(range(1, 121), k=4) for c in range(4)})
df.columns = ["D", "C", "B", "A"]
df
On peut trier un DataFrame :
Notez que le tri se fera par défaut dans l'ordre croissant. Pour l'ordre décroissant, il vous faudra préciser l'argument ascending = False.
df.sort_index(axis=1, ascending=True, inplace=True)
df
df.sort_index(axis=0, ascending=False)
On peut aussi trier le DataFrame selon les valeurs d'une colonne spécifique avec .sort_values("nom_colonne").
df.sort_values("C") # DataFrame trié selon les valeurs de la colonne "C".
On peut aussi trier le DataFrame selon plusieurs colonnes spécifiques avec .sort_values(["nom_colonne1", "nom_colonne2"]).
df.sort_values(by = ["C", "A"], ascending = [True, False]) # Tri selon "C", puis selon "A" (décroissant).
Pandas groupby est un outil assez puissant pour l'analyse des données. Cependant, il n'est pas très intuitif pour les débutants de l'utiliser car la sortie de groupby n'est pas un objet Pandas Dataframe, mais un objet Pandas DataFrameGroupBy . Un objet DataFrame peut être visualisé facilement, mais pas pour un objet Pandas DataFrameGroupBy. Si un objet ne peut pas être visualisé, cela rend sa manipulation plus difficile. On peut cependant remettre cet objet à l'intérieur d'un DataFrame.
Pour appréhender ce chapitre, nous utiliserons un DataFrame relevant la situation suivante: Aurore, Bertrand et Clara participent à un tournoi d'archerie sur 2 jours. Chaque jour, chacun tire 100 flèches (donc 200 au total). Un tir dans la zone rouge rapporte 100 points, dans la jaune 75 points et dans la bleue 50 points.
Le DataFrame df présente les variables suivantes:
df = pd.DataFrame( {"jour": 9*["Jour 1"]+9*["Jour 2"],
"nom": ["Aurore", "Bertrand", "Clara"]*6,
"tirs": [30, 28, 51, 40, 44, 37, 30, 28, 12, 50, 35, 40, 25, 45, 30, 25, 20, 30],
"zone": (["bleue"]*3+["jaune"]*3+["rouge"]*3)*2})
P = [50, 75, 100]
k = 0
for z in ["bleue", "jaune", "rouge"]:
df.loc[df["zone"]==z, "points"]=df["tirs"]*P[k]
k+=1
df
Avec df.groupby("A").sum(), on obtient un DataFrame indexé par la colonne A où les autres colonnes numériques sont sommés selone la donnée de la colonne A.
Dans notre exemple, avec df.groupby("nom").sum(), on peut observer les scores finaux des trois participants à la fin du deuxième jour.
df.groupby("nom").sum()
Mais on peut aussi faire les moyennes avec df.groupby("A").mean().
Dans notre exemple, avec df.groupby("zone").mean(), on peut observer la moyenne des nombres de tirs et des points (tous participants confondus) marqués dans chaque zone de couleur.
df.groupby("zone").mean()
On peut aussi inclure plusieurs variables, df.groupby(["A", "C"]).sum() va effectuer le groupement d'abord selon la colonne A puis par la colonne C.
Dans notre exemple, avec df.groupby(["nom", "zone"]).sum(), on peut observer la somme des nombres de tirs et de points pour chaque zone de couleur, pour chacun des participants, jours 1 et 2 confondus.
df.groupby(["nom", "zone"]).sum()
On peut effectuer un groupby() selon les fonctions suivantes :
On peut aussi faire l"aggrégation en donnant soit le le nom de la fonction, soit la fonction elle même avec la fonction agg :
On peut alors effectuer le calcul de plusieurs stats sur plusieurs variables en même temps. Par exemple, si l'on veut observer en même temps la somme, la moyenne et le maximum de points marqués par chacun des joueurs.
df.groupby("nom").agg(["sum", "mean", "max"])
Mentionnons maintenant les tableaux croisés dynamiques. Vous êtes peut-être familier avec ce concept, par exemple parce que vous les avez utilisé dans des logiciels tableurs. Ces tableaux, encore appelés tables de pivots (ou pivot table), permettent de synthétiser les données contenues dans un DataFrame. Essayons de voir cela par l'exemple.
Pour cette partie, nous nous servirons d'un DataFrame type issu du module seaborn appelé "titanic".
import seaborn as sns
titanic = sns.load_dataset("titanic") # La bibliothèque seaborn fournit quelques datasets comme celui-ci.
titanic
Pour voir la répartition des survivants en fonction de leurs sexes et de leur type de billet, nous n'avons besoin que d'une seule ligne.
titanic.pivot_table("survived", index="sex", columns="class")
Par défaut, la fonction pivot_table groupe les données en fonction des critères que nous spécifions, et agrège les résultats en moyenne. Nous pouvons spécifier d'autres fonctions. Par exemple, si nous voulons savoir quelle est le nombre total de survivants dans chaque cas, nous utiliserons la fonction sum. Bien sûr, cela ne fonctionne que parce que les auteurs du dataset ont judicieusement choisi de représenter la survie avec des 0 et des 1.
titanic.pivot_table("survived", index="sex", columns="class", aggfunc="sum")
La fonction pivot_table est très puissante, et permet même de faire des agrégations à plusieurs niveaux. Par exemple, nous pouvons voir l'âge des survivants comme une dimension supplémentaires. Le nombre exact d'années nous intéressant peu, nous regrouperons les âge en deux catégories, grâce à la fonction cut.
titanic.dropna(inplace=True)
age = pd.cut(titanic["age"], [0, 18, 80])
titanic.pivot_table("survived", ["sex", age], "class")
Il peut vous arriver de vouloir modifier de façon unilatérale une colonne d'un DataFrame. Reprenons notre DataFrame créé à la partie A.
df = pd.DataFrame( {"nom": ["Aurore", "Bertrand", "Christine", "Denis", "Eudes", "François", "Géraldine", "Henry"],
"age": [37, 29, 24, 40, 18, 52, 40, 28],
"salaire": [4500, 3200, 1500, 2600, 900, 3500, 2900, 2900],
"ville": ["Paris", "Paris", "Bordeaux", "Strasbourg", "Paris", "Strasbourg", "Strasbourg", "Bordeaux"],
"departement": [75, 75, 33, 67, 75, 67, 67, 33]})
df
Admettons par exemple que vous préféreriez avoir l'année de naissance de chaque individu plutôt que son âge (nous sommes en 2021 à l'heure où l'on rédige ce cours et nous admettons que les anniversaires de chacun est déjà passé, arrêtez de toujours chercher des noises, comme ça...). Pour ce faire, la meilleure option est de créer en premier lieu une fonction qui effectue la transformation souhaitée, puis l'appliquer à la colonne en question avec la fonction apply().
def AnneeNaissance(age):
annee = 2021 - age
return(int(annee))
df["age"] = df["age"].apply(AnneeNaissance)
df
Reprenons notre DataFrame de la partie A, et divisons-le en plusieurs DataFrames pour vous montrer les différentes perspectives de la concaténation en le reconstruisant.
df1 = pd.DataFrame( {"nom": ["Aurore", "Bertrand", "Christine", "Denis"],
"age": [37, 29, 24, 40],
"salaire": [4500, 3200, 1500, 2600]})
df2 = pd.DataFrame( {"nom": ["Eudes", "François", "Géraldine", "Henry"],
"age": [18, 52, 40, 28],
"salaire": [900, 3500, 2900, 2900]}, index=range(4, 8))
df1
df2
Pour joindre les deux DataFrames l'un au-dessus de l'autre, il faut utiliser la fonction pd.concat([df1, df2]). Veillez à ne pas oublier les crochets à l'intérieur des parenthèses.
df3 = pd.concat([df1, df2])
df3
Vous pouvez aussi joindre deux DataFrames l'un à côté de l'autre en précisant l'argument axis=1.
df4 = pd.DataFrame( {"ville": ["Paris", "Paris", "Bordeaux", "Strasbourg", "Paris", "Strasbourg", "Strasbourg", "Bordeaux"],
"departement": [75, 75, 33, 67, 75, 67, 67, 33]})
df = pd.concat([df3, df4], axis=1)
df
Pour les jointures, basons-nous sur les 2 DataFrames suivants.
df1 = pd.DataFrame({"A": [3, 5], "B": [1, 2]})
df2 = pd.DataFrame({"A": [5, 3, 7], "C": [9, 2, 0]})
df1
df2
La jointure simple pd.merge(df1, df2) utilise les noms des colonnes qui sont communs.
pd.merge(df1, df2)
On peut aussi utiliser df1.merge(df2).
df1.merge(df2)
Prenons maintenant ces 2 DataFrames qui n'ont pas de colonnes communes.
df1 = pd.DataFrame({"A": [3, 5], "B": [1, 2]})
df2 = pd.DataFrame({"A": [5, 3, 7], "B": [9, 2, 0]})
df1
df2
On peut indiquer explicitement les colonnes sur lequelles on veut faire la jointure si c'est une partie des colonnes de même nom.
pd.merge(df1, df2, on = ["A"])
pd.merge(df1, df2, on = ["A"])
Les lignes qui n'ont pas la clef commune sont quand mêmes présentes avec l'argument how = "outer".
pd.merge(df1, df2, how="outer")
On peut aussi faire une jointure externe gauche ou droite comme en sql avec how = "left" ou how = "right".
pd.merge(df1, df2, how="right")
Python a la capacité de repérer les données temporelles lorsqu'une variable interprète une date et/ou une heure. Pour utiliser ces données dans le cadre d'interprétations temporelles, le module datetime s'impose qu'on importera ici sous l'abréviation dt.
import datetime as dt
dt.date.today() # Construit la date du jour.
Dans ce chapitre, nous nous baserons sur un exemple type. Le DataFrame suivant représente la listes des 336 713 transactions d'une entreprise fictive de mars 2021 à février 2022 et comprend les variables suivantes:
ventes = pd.read_csv("data/tuto/ventes.csv")
ventes
Veuillez observer que pour le moment, le DataFrame n'est trié selon aucune des variables mentionnées. Bien entendu, nous nous intéresserons au tri chronologique de ce tableau.
Une date et une heure sont généralement interprétées initialement comme des chaîne de caractères (string). Afin d'exploiter les indications chronologiques qu'elles représentent, il faut au préalable les convertir en datetime avec la fonction pd.to_datetime().
dh = "2021-04-20 16:55:44"
pd.to_datetime(dh)
Lorsque vous importez un DataFrame, Python ne reconnaît pas initialement une variable comme une date et vous l'interprétera par défaut comme une liste d'objets (usuellement comme des strings).
ventes[["date_et_heure"]].info()
C'est pourquoi il est toujours recommandé de convertir au préalable une variable date/heure en datetime.
ventes["date_et_heure"] = pd.to_datetime(ventes["date_et_heure"])
ventes[["date_et_heure"]].info()
Notez dorénavant que la variable "date_et_heure" est bien convertie en datetime et que cela ne change en rien la visibilité du tableau.
ventes
Avec les fonctions .date() et .time() appliquées sur un datetime, on peut respectivemenet récupérer la date et l'heure en question.
dh = pd.to_datetime("2021-04-20 16:55:44")
jour = dh.date()
heure = dh.time()
print(jour)
print(heure)
Mais on peut également récupérer:
dh = pd.to_datetime("2021-04-20 16:55:44")
print("Aujourd'hui, on est le {0} du {1}-ième mois de l'année {2}.".format(dh.day, dh.month, dh.year))
print("À l'heure où j'écris, il est {2}h{1} et {0} secondes se sont écoulées.".format(dh.second, dh.minute, dh.hour))
À partir de ça, nous pouvons créer deux variables date et heure dans "ventes" à partir de date_et_heure. Pour cela, je vous conseille fortement d'initialiser une fonction qui vous renvoie l'indice temporel souhaité, et d'utiliser la fonction apply(), vue dans cette partie, sur la variable d'ores et déjà convertie en datetime.
def get_date(DH): return(DH.date())
def get_hour(DH): return(DH.time())
ventes["date"] = ventes["date_et_heure"].apply(get_date)
ventes["heure"] = ventes["date_et_heure"].apply(get_hour)
ventes.drop(columns="date_et_heure", inplace=True) # Nous pouvons supprimer cette variable, nous n'en aurons plus besoin.
ventes
Notez cependant que notre DataFrame n'est toujours pas trié chronologiquement, c'est ce que nous allons voir à présent.
Comme nous l'avons vu dans cette partie, on peut utiliser la fonction .sort_values("date", "heure") pour trier notre DataFrame de façon chronologique, ce qui sera bien plus pratique pour les interprétations graphiques.
ventes = ventes.sort_values(["date", "heure"]).reset_index(drop=True)
ventes
En effet, Python reconnaît les tries chronologiques du datetime.
Dans cette partie, assurez-vous au préalable d'être à l'aise avec l'interprétation graphique depuis un DataFrame étudié dans ce chapitre. Représentons par exemple l'évolution du chiffre d'affaire au long de la période étudiée.
import warnings ; warnings.filterwarnings('ignore') # Ne tenez pas compte de cette ligne.
import matplotlib.pyplot as plt
plt.figure(figsize=(15, 5))
ca_days = ventes.groupby("date").sum()[["price"]]
plt.plot(ca_days["price"], color="green")
plt.title("Évolution du chiffre d'affaires au cours de l'année", fontsize=20)
plt.xlabel("")
plt.ylabel("Chiffre d'affaires en euros", fontsize=14)
plt.show()
On peut aussi montrer l'évolution de chacune des 3 catégories de produits.
plt.figure(num=None, figsize=(12, 5), dpi=80)
couleur = ["steelblue", "crimson", "goldenrod"]
for i in range(3):
cat = ventes.loc[ventes["categ"]==i].groupby("date").sum()[["price"]]
plt.plot(cat["price"], color=couleur[i], linewidth=1, label= "Catégorie %d" %i)
plt.title("Évolution du chiffre d'affaires au cours de l'année", fontsize=25)
plt.xlabel("")
plt.ylabel("Chiffre d'affaires", fontsize=14)
plt.legend()
plt.show()
Pour les graphiques à venir, il vous faudra importer le module matplotlib.pyplot, qu'on importera ici sous l'abréviation plt. Voici d'ailleurs aussi un module très pratique tiré directement de la librairie HTML pour centrer les graphiques présentés (c'est plus joli).
import matplotlib.pyplot as plt
from IPython.core.display import HTML
HTML("""<style> .output_png {display: table-cell; text-align: center; vertical-align: middle} </style>""")
Matplotlib est une librairie qui permet de tracer des graphes (dans le sens graphiques), inspiré de Matlab au départ. Il permet de faire des graphes qui peuvent être comlètement adaptés si besoin. Sur une figure, on peut tracer plusieurs graphes.
Il inclus 2 façons de l'utiliser :
Matplotlib rend ainsi possible la création de graphes à l'intérieur d'applications complexes autorisées par le langage python, et ceci sans quitter le langage python.
Notions principales pour les graphes sous matplotlib :
Afin d'afficher le graphe courant dans le bloc de code, il vous faudra préciser la mention plt.show().
L'exemple de base est de tracer un graphe avec des valeurs de x et des valeurs de y en reliant les points dans l'ordre de la liste.
plt.plot([1, 2, 3, 4, 5], [7, 1, 4, 1, 7])
plt.show()
Voilà les paramètres par défaut lorsque vous générez un graphe simple. Mais nous allons voir que divers paramètres sont modifiables avec des arguments à donner à pyplot.
Lorsque vous tracez une graphique, il est parfois utile d'y indiquer certaines droites constantes à titre de repère.
Vous pouvez y ajouter les mêmes paramètres que nous décrirons dans la partie suivante.
Vous pouvez aussi tracer une fonction en l'entrant simplement dans les paramètres.
plt.axhline(0.5, color="red", linestyle="--")
plt.axvline(2, color="saddlebrown", linewidth=5)
x = np.linspace(0, 10, 1000)
plt.plot(x, np.sin(x))
plt.show()
On peut indiquer à pyplot les arguments suivants :
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A",
linestyle = "dashed", color = "goldenrod", linewidth = 2,
marker = "o", markerfacecolor = "green", markersize = 5)
plt.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B",
linestyle = "dashed", color = "red", linewidth = 3,
marker = "o", markerfacecolor = "blue", markersize = 6)
plt.show()
Nous reviendrons sur l'importance des labels dans la partie suivante.
Pour donner un titre à votre graphe et à vos axes, vous devez inscrire dans votre bloc de code :
Vous pouvez éventuellement ajouter à ces trois instructions les paramètres :
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5])
plt.title("Voilà un graphique", fontsize=22, color="crimson", pad=20)
plt.xlabel("Axe des abscisses", fontsize=15, color="darkblue")
plt.ylabel("Axe des ordonnées", fontsize=15, color="darkgoldenrod")
plt.show()
Pour déterminer les étiquettes des axes, vous devez inscrire dans votre bloc de code :
Pour déterminer les limites des axes, vous devez inscrire dans votre bloc de code :
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5])
plt.xlim(2, 5)
plt.ylim(3, 7)
plt.title("Voilà un graphique", fontsize=22, color="crimson")
plt.xlabel("Axe des abscisses", fontsize=15, color="darkblue")
plt.ylabel("Axe des ordonnées", fontsize=15, color="darkgoldenrod")
plt.show()
Il est très important, notamment lorsque vous générez plusieurs tracés sur un même graphique, de savoir à quoi correspond telle ou telle courbe. C'est pour cela qu'on précise l'argument label au pyplot.
Mais pour afficher les légendes, il faut en plus préciser dans le bloc de code l'instruction plt.legend() qui peut, entre autres, prendre 2 arguments très importants:
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A",
linestyle = "dashed", color = "goldenrod", linewidth = 2,
marker = "o", markerfacecolor = "green", markersize = 5)
plt.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B",
linestyle = "dashed", color = "red", linewidth = 3,
marker = "o", markerfacecolor = "blue", markersize = 6)
plt.title("Voilà deux graphiques", fontsize=20)
plt.legend(fontsize=15, loc="lower right")
plt.show()
Pour l'instruction loc, on a:
Si vous souhaitez modifier la taille de votre graphe dans l'affichage, vous pouvez utiliser plt.figure(figsize=(h, v), dpi=...) en tout début d'instruction. Le premier paramètre définit la longueur horizontale, et le deuxième la longueur verticale. L'argument dpi, lui, définit la résolution graphique de l'affichage.
plt.figure(figsize=(15, 2), dpi=80)
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A",
linestyle = "dashed", color = "goldenrod", linewidth = 2, marker = "o", markerfacecolor = "green", markersize = 5)
plt.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B",
linestyle = "dashed", color = "red", linewidth = 3, marker = "o", markerfacecolor = "blue", markersize = 6)
plt.title("Graphique aplati"), plt.xlabel("Axe des abscisses"), plt.ylabel("Axe des ordonnées")
plt.legend(loc="lower right") ; plt.show()
Vous l'aurez compris, l'argument fontsize au sein d'un paramètre permet de définir la taille d'affichage.
plt.figure(figsize=(15, 4), dpi=80)
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A",
linestyle = "dashed", color = "goldenrod", linewidth = 2, marker = "o", markerfacecolor = "green", markersize = 5)
plt.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B",
linestyle = "dashed", color = "red", linewidth = 3, marker = "o", markerfacecolor = "blue", markersize = 6)
plt.title("Graphique aplati", fontsize=30)
plt.xlabel("Axe des abscisses", fontsize=15), plt.ylabel("Axe des ordonnées", fontsize=15)
plt.legend(fontsize=15, loc="lower right") ; plt.show()
L'argument color ou colors dans les paramètres d'un plot permet de choisir les couleurs de son graphique. Voici un tableau réunissant les couleurs "classiques" que vous pouvez sélectionner.
from IPython.display import Image
Image(filename="data/tuto/colors.png", width=650)
En réalité, il existe 16 millions de couleurs en informatique. Pour plus de choix de coloris, voici en lien le tableau des couleurs RGB.
Pour tracer un diagramme simple avec une seule série de valeurs, vous pouvez utiliser plt.bar( range(5), [1, 3, 3, 5, 4] ) où:
plt.bar(range(5), [1, 3, 3, 5, 4])
plt.show()
Mais vous pouvez également ajouter des paramètres:
plt.bar(range(5), [1, 3, 3, 5, 4], color="yellow", edgecolor="steelblue", linewidth=10,
yerr=[0.3, 0.7, 0.4, 0.2, 0.1], ecolor="red", linestyle="dashed")
plt.show()
Vous pouvez aussi exposer deux séries de valeurs en les distinguant par leurs positions d'abscisse.
y1 = [1, 2, 4, 3]
y2 = [3, 4, 4, 3]
r1 = range(len(y1))
r2 = [x + 0.4 for x in r1]
plt.bar(r1, y1, width = 0.4, color = ["yellow" for i in y1], edgecolor = ["blue" for i in y1], linewidth = 2)
plt.bar(r2, y2, width = 0.4, color = ["pink" for i in y1], edgecolor = ["green" for i in y1], linewidth = 4)
plt.xticks([r + 0.2 for r in range(len(y1))], ["A", "B", "C", "D"])
plt.show()
Vous pouvez aussi différencier un deuxième axe des ordonnées avec plt.gca().twinx().
plt.bar([1, 2, 3], [4, 6, 1], color="purple")
plt.ylabel("Axe de A, B et C", fontsize=15, color="purple")
plt.gca().twinx()
plt.bar([4, 5, 6], [40000, 60000, 10000], color="crimson")
plt.ylabel("Axe de D, E et F", fontsize=15, color="crimson")
plt.xticks(range(1, 7), ["A", "B", "C", "D", "E", "F"])
plt.show()
Vous pouvez encore superposer deux séries avec l'argument bottom.
y1 = [1, 2, 4, 3]
y2 = [3, 4, 4, 3]
r = range(len(y1))
plt.bar(r, y1, width = 0.8, color = ["crimson" for i in y1], edgecolor = ["black" for i in y1],
linestyle = "solid", hatch ="/", linewidth = 3)
plt.bar(r, y2, width = 0.8, bottom = y1,
color=["goldenrod" for i in y1], edgecolor=["blue" for i in y1], linestyle="dotted", hatch="o", linewidth=3)
plt.xticks(range(len(y1)), ["A", "B", "C", "D"])
plt.show()
Enfin, vous pouvez refaire tout ça horizontalement avec plt.barh, auquel cas l'argument bottom devient left.
y1 = [1, 2, 4, 3]
y2 = [3, 4, 4, 3]
r = range(len(y1))
plt.barh(r, y1, color = ["crimson" for i in y1], edgecolor = ["black" for i in y1],
linestyle = "solid", hatch ="/", linewidth = 3)
plt.barh(r, y2, left = y1,
color=["goldenrod" for i in y1], edgecolor=["blue" for i in y1], linestyle="dotted", hatch="o", linewidth=3)
plt.yticks(range(len(y1)), ["A", "B", "C", "D"])
plt.show()
Pour tracer un camembert, vous pouvez utiliser plt.pie().
plt.pie([1, 2, 3, 4, 10], labels = ["A", "B", "C", "D", "E"], normalize = True)
plt.legend()
plt.show()
Vous pouvez également ajouter des paramètres:
Vous pouvez aussi au préalable régler la taille des indices d'affichage avec mpl.rcParams["font.size"].
import matplotlib as mpl
mpl.rcParams["font.size"] = 14
plt.pie([1, 2, 3, 4, 10], labels = ["A", "B", "C", "D", "E"], normalize = True,
explode=[0.5, 0.2, 0, 0, 0], autopct=lambda x:str(round(x,2))+"%", shadow=True)
plt.show()
Pour créer un nuage de points, on peut utiliser plt.scatter().
x = [1, 2, 3, 4, 5]
plt.scatter(x, [1, 2, 3, 4, 5], c = "purple")
plt.scatter(x, [1, 4, 9, 16, 25], c = "darkgoldenrod")
plt.show()
Vous pouvez également ajouter des paramètres:
Le paramètre marker peut prendre les arguments suivants.
Symbole | Affichage | ......... | Symbole | Affichage |
"o" | rond | ......... | "H" | hexagone. |
"s" | carré | ......... | "p" | pentagone |
"+" | croix verticale | ......... | "." | point |
"x" | croix diagonale | ......... | ">" | triangle vers la droite |
"*" | étoile | ......... | "v" | triangle vers le bas |
"D" | losange | ......... | "|" | trait vertical ('_' pour horizontal) |
"d" | losange allongé | ......... | "1" | croix à 3 branches vers le bas |
Pour créer un histogramme, on peut utiliser plt.hist().
plt.hist([1, 2, 2, 3, 4, 4, 4, 4, 4, 5, 5], range = (0, 5), bins = 5, color = "yellow", edgecolor = "red")
plt.xlabel("valeurs")
plt.ylabel("nombres")
plt.title("Exemple d\" histogramme simple")
plt.show()
Vous pouvez également ajouter des paramètres:
Vous pouvez encore superposer deux histogrammes avec une série de listes.
x1 = [1, 2, 2, 3, 4, 4, 4, 4, 4, 5, 5]
x2 = [1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 5, 5, 5]
bins = [x + 0.5 for x in range(0, 6)]
plt.hist([x1, x2], bins = bins, color = ["yellow", "green"],
edgecolor = "red", hatch = "/", label = ["x1", "x2"],
histtype = "barstacked")
plt.ylabel("valeurs") ; plt.xlabel("nombres")
plt.title("2 histogrammes superposés") ; plt.legend()
plt.show()
Pour créer une boîte à moustaches, on utilise plt.boxplot().
# On définit ici les paramètres des boxplots :
medianprops = {"color":"black"}
meanprops = {"marker":"o", "markeredgecolor":"black", "markerfacecolor":"firebrick"}
plt.boxplot([1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 5, 5, 5], showfliers=False, medianprops=medianprops, vert=False,
patch_artist=True, showmeans=True, meanprops=meanprops)
plt.show()
Pylab fournit une interface procédurale à la librairie graphique matplotlib orientée objet. Elle est basée sur un modèle très proche de Matlab™. De la sorte, la grande majorité des commandes pylab ont leur équivalent Matlab™ avec des arguments similaires. Les commandes les plus importantes sont expliquées avec des exemples en console interactive.
from matplotlib.pylab import *
Matplotlib est fournie avec un jeu de paramètres par défaut qui permet de personnaliser toute sorte de propriétés. Vous pouvez contrôler les réglages par défaut de (presque) toutes les propriétés : taille du graphique, résolution en points par pouce (dpi), épaisseur du trait, couleurs, styles, vues, repères, grilles, textes, polices de caractères, etc. Bien que les réglages par défaut répondent à la plupart des cas courants, vous pourriez être amenés à en modifier quelques-uns pour des cas plus spécifiques.
from matplotlib.pylab import plot, show
X = np.linspace(-np.pi, np.pi, 256,endpoint=True)
C,S = np.cos(X), np.sin(X)
plot(X,C)
plot(X,S)
show()
La mise en place des paramètres d'affichage avec pylab fonctionnent comme avec pyplot.
figure(figsize=(8, 5), dpi=80) # On crée un graphique de 8x6 pouces avec une résolution de 80 points par pouce.
subplot(1,1,1) # On crée une nouvelle vue dans une grille de 1 ligne x 1 colonne.
X = np.linspace(-np.pi, np.pi, 256,endpoint=True)
C,S = np.cos(X), np.sin(X)
plot(X, C, color="blue", linewidth=1.0, linestyle="-") # On trace la fonction cosinus en bleu avec un trait plein
# de 1 pixel d'épaisseur.
plot(X, S, color="grey", linewidth=1.0, linestyle="-") # Idem avec la fonction sinus.
xlim(-4.0,4.0) # limites de l'axe (O,x) des abscisses.
xticks(np.linspace(-4,4,9,endpoint=True)) # Graduations de l'axe (O,x) des abscisses
ylim(-1.0,1.0) # limites de l'axe (O,y) des ordonnées.
yticks(np.linspace(-1,1,5,endpoint=True)) # Graduations de l'axe (O,y) des ordonnées.
show() # On affiche le résultat à l'écran.
Donner la représentation graphique au voisinage de 0 de la fonction f définie par: f(x) = |x sin(1/x)|.
Sous pylab la fonction donnant la valeur absolue est absolute().
from matplotlib.pylab import plot, absolute, sin, arange
X = arange(-0.5,0.5,0.001)
Y = absolute(X*sin(1/X))
plot(X,Y)
show()
Il y a 2 notions :
Taille d'une figure :
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A", linestyle = "dashed", color = "goldenrod", linewidth = 2,
marker = "o", markerfacecolor = "green", markersize = 5)
ax.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B", linestyle = "dashed", color = "red", linewidth = 3,
marker = "o", markerfacecolor = "blue", markersize = 6)
ax.set_title("Voilà deux graphiques", fontsize=20)
plt.legend(fontsize=15, loc="lower right")
plt.show()
fig.get_size_inches()
Pour avoir des marges minimales : plt.tight_layout() permet d'ajuster automatiquement les marges si certaines étiquettes sont particulièrement longues (c'est matplotlib qui calcule).
L'instruction plt.tight_layout(rect = [0, 0, 1, 0.9]) indique le rectangle sur lequel mettre les graphes, ce qui permet de laisser de la place pour le suptitle par exemple. Le défaut est [0, 0, 1, 1].
Si plusieurs graphes, on peut ajuster les marges supplémentaires avec plt.tight_layout(pad=0.4, w_pad=0.5, h_pad=1.0).
plt.figure(figsize=(8, 4))
plt.plot([1, 2, 3, 4, 5], [1, 3, 4, 4, 5], label = "A", linestyle = "dashed", color = "goldenrod", linewidth = 2,
marker = "o", markerfacecolor = "green", markersize = 5)
plt.plot([1, 2, 3, 4, 5], [5, 4, 2, 2, 1], label = "B", linestyle = "dashed", color = "red", linewidth = 3,
marker = "o", markerfacecolor = "blue", markersize = 6)
plt.title("Voilà deux graphiques", fontsize=20)
plt.legend(fontsize=15, loc="lower right")
plt.tight_layout(rect=[0, 0, 1, 0.9], pad=0.4, w_pad=0.5, h_pad=1.0)
plt.show()
Traçage de graphes multiples sur la même figure :
Fermeture d'une figure :
plt.figure(1)
plt.subplot(1, 2, 1)
plt.scatter(range(5), [x ** 2 for x in range(5)], color = "blue")
plt.subplot(2, 2, 2)
plt.plot(range(5), color = "red")
plt.subplot(2, 2, 4)
plt.bar(range(5), range(5), color = "green")
plt.show()
Subplots :
Sharex :
On peut aussi positionner un graphe "en insert" où l'on veut en donnant la position x et y du point en bas à gauche de ce graphe, avec des valeurs entre 0 et 1, et les dimensions largeur et hauteur, entre 0 et 1, et en utilisant la méthode add_axes de Figure.
figure = plt.figure()
axes = figure.add_subplot(111) # Renvoie un objet AxesSubplot, sous classe de Axes.
axes.scatter(range(5), [x ** 2 for x in range(5)])
axes.set_xlim(0, 4)
axes.set_xlabel("axe des x")
axes2 = figure.add_axes([0.3, 0.5, 0.3, 0.3]) # Renvoie un objet Axes.
axes2.patch.set_color("lightyellow")
axes2.plot(range(5), range(5))
plt.show()
Réglage des marges :
figure = plt.figure(figsize = (15, 4)) # Exemple avec des petites marges.
plt.gcf().subplots_adjust(left = 0.1, bottom = 0.1,
right = 0.9, top = 0.9,
wspace = 0, hspace = 0.1)
axes = figure.add_subplot(2, 1, 1)
axes.set_xlabel("Axe des x")
axes.set_ylabel("Axe des y")
axes.set_title("Titre du graphe 1")
axes.scatter(range(5), [x ** 2 for x in range(5)], s = 50, color = "blue")
axes = figure.add_subplot(2, 1, 2)
axes.set_xlabel("Axe des x")
axes.set_ylabel("Axe des y")
axes.set_title("Titre du graphe 2")
axes.scatter(range(5), [x ** 2 for x in range(5)], s = 50, color = "red")
plt.show()
figure = plt.figure(figsize = (15, 4)) # Exemple avec des grandes marges.
plt.gcf().subplots_adjust(left = 0.3, bottom = 0.3,
right = 0.7, top = 0.7,
wspace = 0, hspace = 2)
axes = figure.add_subplot(2, 1, 1)
axes.set_xlabel("Axe des x")
axes.set_ylabel("Axe des y")
axes.set_title("Titre du graphe 1")
axes.scatter(range(5), [x ** 2 for x in range(5)], s = 50, color = "blue")
axes = figure.add_subplot(2, 1, 2)
axes.set_xlabel("Axe des x")
axes.set_ylabel("Axe des y")
axes.set_title("Titre du graphe 2")
axes.scatter(range(5), [x ** 2 for x in range(5)], s = 50, color = "red")
plt.show()
Pour avoir la position d'un graphe dans une figure : bpyplot.gca().get_position().get_points() renvoie une array du type [[x0,y0],[x1,y1]] avec (x0, y0) coordonnées du point en bas à gauche et (x1, y1) coordonnées du point en haut à droite.
Pour créer des subplots a priori et ensuite les remplir avec des figures :
Nous présentons donc notre DataFrame principal world, où seront collectées les données relevées sur la FAO, qui présente pour le moment les variables:
world = pd.read_csv("data/tuto/monde.csv")
world
On peut tracer un histogramme qui représenterait la somme de la variable population selon la variable qualitative continent.
plt.figure(figsize=(8, 4))
plt.bar(world["continent"], world["population"], color="saddlebrown")
plt.title("Population mondiale par continent", fontsize=17, pad=10)
plt.ylabel("Nombre d'habitants", fontsize=13)
plt.show()
Dans le cas de données relativement massives, je vous conseille au préalable d'effectuer un groupby sur le tableau selon la variable qualitative souhaitée.
pop = world.groupby("continent").sum()[["population"]].reset_index()
pop
plt.figure(figsize=(8, 4))
plt.bar(pop["continent"], pop["population"], color="firebrick")
plt.title("Population mondiale par continent", fontsize=17, pad=10)
plt.ylabel("Nombre d'habitants", fontsize=13)
plt.show()
Voilà sinon un exemple de code pour obtenir ce diagramme sous forme de camembert. Le groupby est incontournable.
plt.figure(figsize=(7, 7))
pop = world.groupby("continent").sum()[["population"]].reset_index()
palette = ["limegreen", "crimson", "goldenrod", "mediumvioletred", "darkslateblue"]
pop["population"].plot(kind="pie", labels=pop["continent"],
colors=palette, autopct=lambda x: str(round(x, 2))+"%", shadow=True)
plt.title("Répartition de la population mondiale", fontsize=25)
plt.ylabel("")
plt.show()
Vous pouvez aussi utiliser plot() pour créer un nuage de points. Par exemple, observons les pourcentages de sous-nutrition en fonctions de leurs effectifs.
plt.figure(figsize=(8, 4))
world2 = world.loc[world["population"]<10**9]
plt.plot(world2["sous_nutrition"], world2["pourcentage_sn"], "o", color="crimson")
plt.title("Nuage de points", fontsize=20, pad=10)
plt.xlabel("Effectifs de la sous-nutrition", fontsize=13)
plt.ylabel("Pourcentage de sous-nutrition", fontsize=13)
plt.show()
Lorsque vous étudiez une variable quantitative (comme les effectifs de sous-nutrition dans notre tableau) selon une deuxième variable qualitative (prenons les cinq continents dans notre tableau), il vous sera souvent utile d'en afficher les boxplots pour afficher les mesures de tendances centrales de chaque modalité de la variable qualitative.
continents = world["continent"].unique() # On obtient ici la liste des modalités de la variable qualitative.
# Ici, la liste des cinq continents.
SN = []
for i in continents:
SN.append(world[world["continent"]==i]["sous_nutrition"])
plt.figure(figsize=(18, 5))
bp = plt.boxplot(SN, labels=continents, medianprops={"color":"steelblue"},
showfliers=False, vert=False, patch_artist=True, showmeans=True,
meanprops={"marker":"o", "markeredgecolor":"steelblue", "markerfacecolor":"crimson"})
palette = ["mediumvioletred", "crimson", "darkslateblue", "goldenrod", "limegreen"]
couleur_palette = 0 # En bonus, voici une petite méthode pour colorer différemment vos boxplots.
for patch in bp["boxes"]:
patch.set_color(palette[couleur_palette])
couleur_palette+=1
plt.title("Mesures de tendance centrale de la sous-nutrition dans le monde", fontsize=25)
plt.show()
Vous aurez besoin du module seaborn pour afficher la matrices de corrélations avec heatmap.
import seaborn as sns
plt.figure(figsize=(5, 5))
sns.heatmap(world.corr(), annot=True, vmax=.8, square=True, cbar="plasma")
plt.axis("equal")
plt.title("Corrélations des variables", fontsize=20)
plt.show()
Nous présentons ici le DataFrame shopping, qui présente les variables suivantes :
shopping = pd.read_csv("data/tables/shopping.csv")
shopping
L'objectif est de savoir si le genre d'une personne influence son choix de colori.
Pour déterminer s'il existe une relation entre les deux caractères étudiés, on construit le tableau de contingence réel, c'est-à-dire un tableau dénombrant les modalités croisées des deux caractères X et Y. Ce tableau aura donc :
X = "color"
Y = "client_gender"
c = shopping[[X, Y]].pivot_table(index=X, columns=Y, aggfunc=len) # On calcules les effectifs de genre selon les couleurs.
cont = c.copy()
tx = shopping[X].value_counts() # Rend les totaux de chaque couleur.
ty = shopping[Y].value_counts() # Rend les totaux d'achats selon les deux genres.
cont.loc[:, "Total"] = tx # Ajoute les totaux de couleurs au tableau.
cont.loc["total", :] = ty # Ajoute les totaux des achats des deux genres au tableau.
cont.loc["total", "Total"] = len(shopping) # Ajoute la somme total des produits achetés au tableau (en bas à droite).
print("Tableau de contingence réel :")
cont
le tableau de contingence théorique représente le tableau de contingence dans le cas où le genre de la personne n'influent pas sur le coloris acheté. C'est à dire qu'on a autant d'hommes que de femmes qui sont suceptible d'acheter chaque colori.
tx = pd.DataFrame(tx)
ty = pd.DataFrame(ty)
tx.columns = ["foo"]
ty.columns = ["foo"]
indep = tx.dot(ty.T) / len(shopping)
indep.sort_index(axis=1, inplace=True)
indep.sort_index(inplace=True)
print("Tableau de contingence théorique :")
indep
Observez les écart entre le tableau de contingence théorique et réel. En particulier pour les couleurs vert, violet, gris et rouge.
L'objectif est maintenant de savoir si les écarts entre le tableau de contingence réel et le tableau de contingence théorique sont significatifs ou non. Pour cela, on mesure le Chi-2.
c = c.fillna(0)
mesure = (c-indep)**2 / indep
xi_n = mesure.sum().sum()
print("Le Chi-2, ici, est de {}.".format(xi_n))
On va pouvoir conclure en allant lire la table du Chi-2. (Nous verrons ensuite comment nous en passer en Python mais il est important de bien comprendre le mécanisme derrière ce test).
D'abord il faut connaitre notre le degré de liberté, qui ce calcul selon la formule :
ddl = (nb de lignes - 1)(nb de colonnes - 1).
ddl = (len(c)-1) * (len(c.columns)-1)
ddl
On sait désormais que notre degré de liberté est de 4.
Il faut maintenant définir un seuil de tolérance, qu'on va mettre à 5% ici. Un seuil de tolérance est un seuil qui nous permet de définir le pourcentage acceptable de chances de ce tromper. En général, on le fixe à 5% (j'ai 5% de chance d'en tirer une conclusion fausse) mais il peut être plus bas (1%,
0.5%, etc.). Cependant on le met rarement plus haut.
Les lois du chi-2 que je vous invite à voir ici nous donnent la limite maxiumale au-dessous de laquelle on peut accepter l'hypothèse qu'il n'y a pas de corrélation entre les deux variables. En observant le tableau fournit en lien, on tombe sur 9,4877, hors notre Chi-2 vaut 14,97, il est donc supérieur. On en déduit qu'il existe bien une corrélation entre les couleurs et les genres.
La p-value est une variable essentielle, elle représente le pourcentage de chance de rejetter l'hypothèse nulle (qui dit qu'il n'y a pas de corrélation) à tort. Si la p-value est
inférieure à 0.05, on peut donc conclure qu'on peut rejetter l'hypothèse nulle au seuil de 5%.
Pour la calculer, vous aurez besoin du module scipy.stats et d'utiliser la méthode ci-dessous.
import scipy.stats as st
pvalue = st.chi2_contingency(c)[1]
pvalue
Ici la p-value est de 0.0048, on peut donc rejetter sereinement l'hypothèse nulle puisque les chances de la rejeter à tort sont de 0.48%. C'est grâce à la p-value que vous allez pouvoir tirer vos conclusions par rapport aux tests du Chi-2.
Calculer le Chi-2 et la pvalue, c'est bien, mais cela ne vous informe que sur une chose : savoir si oui ou non, il y a corrélation. Cela ne vous précise pas, si c'est le cas, le "degré" de corrélation pour chaque modalité. La méthode la plus illustrative de l'analyse des corrélations entre deux variables qualitatives est donc le tableau de contingence coloré, qui indique :
On observe donc sur le tableau de contingence coloré ("heatmap" en anglais) : plus la couleur est claire, plus l'écart entre la valeur théorique attendue et la valeur réelle est significatif. A contrario, plus la couleur est foncée et plus l'écart entre la valeur théorique et la valeur réel est non significatif.
c = c.fillna(0)
mesure = (c-indep)**2 / indep
xi_n = mesure.sum().sum()
sns.heatmap(mesure/xi_n, annot=indep-c)
plt.show()
Tout d'abord, on voit bien que en ce qui concerne le choix de la couleur bleu, le genre de la personne importe peu : femmes comme homme choisissent ce colori à part égale. Ce qui n'est pas du tout le cas pour la couleur verte qui est beaucoup plus choisie par les femmes que par les hommes.
Nous présentons ici le DataFrame cars, qui contient une liste de 60 voitures et présente les variables suivantes :
cars = pd.read_csv("data/tuto/cars.csv")
cars.head()
L'objectif est de savoir si le prix d'un véhicule Price est corrélé linéairement à sa puissance HP.
Il est important en premier lieu de réaliser une représentation graphique des deux variables quantitatives étudiées pour vérifier leur dispersion via un nuage de points (aussi appelé graphique de dispersion).
plt.figure(figsize=(10, 5))
X = cars["HP"]
Y = cars["Price"]
plt.scatter(X, Y, color="indigo")
plt.title("Diagramme de dispersion puissance, prix de la voiture", fontsize=25)
plt.xlabel("Puissance du moteur en nombre de chevaux", fontsize=20)
plt.ylabel("Prix de la voiture", fontsize=20)
plt.show()
La variable X ( ici la puissance du moteur ) est appelée variable explicative.
La variable Y ( ici le prix de la voiture ) est elle appelée variable expliquée.
La variable X sert donc a expliquer la
variable Y. Dans notre cas, on cherche a savoir si la puissance du moteur (variable explicative) explique le prix de la
voiture (variable expliquée ou dépendante).
Si vous ne voyez pas ne serait-ce qu'une légère tendance linéaire sur le graphique de dispersion, il est inutile de poursuivre l'analyse d'une éventuelle corrélation.
Le coefficient de corrélation noté r s'appelle le coefficient de corrélation de Bravais-Pearson. On peut le calculer facilement grâce à la bibliothèque Scipy. Il est associé avec sa p-value qui indique s'il est significatif ou non. Le coefficient de corrélation est toujours compris entre -1 et 1 :
import scipy.stats as st
r, pvalue = st.pearsonr(X, Y)
print("Coefficient de corrélation de Pearson : ", r)
print("p-value = ", pvalue)
Ici, on trouve un coefficient de corrélation d'environ 0.65, ce qui veut dire que les deux variables sont corrélées positivement. Il s'agit ici d'une forte corrélation (> 0.50) : plus la puissance du moteur de la voiture est élevé, plus son prix est suceptible d'être élevé également.
La p-value est également un indicateur très important, elle nous permet de savoir si le coefficient de corrélation calculé est signifiactif ou pas (c'est-à-dire s'il est en dessous d'un certain seuil définit en amont). Souvent on définit se seuil à 5%, si la p-value est inférieur à 5% alors on peut en conclure que r est significatif au seuil de 5%.
On veut désormais pouvoir tracer la droite de régression linéaire qui permet de modéliser le prix théorique d'une voiture en fonction de sa puissance moteur. Pour cela, on fait appel au calcul de la régression linéaire disponible dans la bibliothèque Statsmodels. Pour rendre compte des résultats, on peut utiliser la fonction summary2() mais on peut aussi afficher summary().
import statsmodels.api as sm
import statsmodels.formula.api as smf
X = cars[["HP"]] # Attention, cette fois-ci, il s'agit bien de la restriction du DataFrame à la variable HP, non de la liste.
Y = cars["Price"]
X = X.assign(intercept=[1]*X.shape[0])
lr = sm.OLS(Y, X).fit()
lr.summary()
Cette regression linéaire nous permet de retrouver la droite de regression de Y en X. Cette droite prend la forme : a + bx. Le a est l'ordonnée à l'origine (c'est la valeur y lorsque x=0). Le b est le coefficient de régression, il représente la pente de la droite.
On peut retrouver ces deux constantes en utilisant lr.params.
lr.params
Dans le cas présent, cette droite vérifie l'équation y = 86,144014x + 2075,946714.
Le r² est le coefficient de détermination, il mesure le pouvoir prédictive de la variable explicative X. C'est le carré du coefficient de détermination. On peut le lire sur le summary ou bien le retrouver avec lr.squared.
lr.rsquared
Ici, le coefficient de détermination r² ≃ 0,43. Prob (F-statistic) correspond à la p-value de r.
plt.figure(figsize=(10, 5))
a, b = lr.params["HP"], lr.params["intercept"] # On stocke d'abord a et b dans les variables correspondantes.
X = cars["HP"]
Y = cars["Price"]
plt.plot(X, Y, "o", color="indigo")
plt.plot(np.arange(min(X), max(X)), [a*x+b for x in np.arange(min(X), max(X))], color="crimson", linewidth=2)
plt.title("Diagramme de dispersion puissance, prix de la voiture", fontsize=25)
plt.xlabel("Puissance du moteur en nombre de chevaux", fontsize=20)
plt.ylabel("Prix de la voiture", fontsize=20)
plt.show()
On en conclut qu'il existe bel et bien une corrélation linéaire positive entre la puissance du moteur et le prix de la voiture (puisque r = 0.65). La p-value étant inférieur au seuil de 5%, les résultats sont donc significatifs.
Les calculs précédent sont très sensible aux outliers, ce individus qui diffèrent fortement des autres. Parfois en enlevant seulement quelqu'uns d'entre eux on peut obtenir des résultats complétement diff
érent.
Prenons l'exemple de notre jeu de donnée. Une remarque une voiture ayant une très forte puissance de moteur (la seule
supérieur à 220) qui a un prix malgré tout assez faible comparé aux autres voitures très puissantes. Enlevons cet individu
et observons l'impact de cette suppression sur les résultats.
plt.figure(figsize=(10, 5))
cars = cars.loc[cars["HP"]<220]
X = cars[["HP"]]
Y = cars["Price"]
X = X.assign(intercept=[1]*X.shape[0])
lr = sm.OLS(Y, X).fit()
a, b = lr.params["HP"], lr.params["intercept"] # On stocke d'abord a et b dans les variables correspondantes.
X = cars["HP"]
Y = cars["Price"]
plt.plot(X, Y, "o", color="indigo")
plt.plot(np.arange(min(X), max(X)), [a*x+b for x in np.arange(min(X), max(X))], color="crimson", linewidth=2)
plt.title("Diagramme de dispersion puissance, prix de la voiture", fontsize=25)
plt.xlabel("Puissance du moteur en nombre de chevaux", fontsize=20)
plt.ylabel("Prix de la voiture", fontsize=20)
plt.show()
st.pearsonr(Y, X)
Nous présentons ici le DataFrame iris, qui contient une liste de 150 fleurs et présente les variables suivantes :
iris = pd.read_csv("data/tuto/iris.csv")
iris
Pour commencer, vous pouvez observer la différenciation de chaque variable quantitative deux à deux selon la variable qualitative avec un pairplot issu de la bibliothèque seaborn (attention, l'exécution de ce code peut prendre un peu de temps).
import seaborn as sns
sns.pairplot(iris, hue="Species")
plt.show()
Nous allons nous concentrer sur la relation entre la largeur des sépales SepalWidthCm et la variété Species de l'iris. On veux s'avoir s'il existe une corrélation entre ces deux variables.
Dans cette étude, la variable qualitative X appelée le facteur est la variable explicative.
Tandis que la variable quantitative Y est la variable à expliquer.
Afin de voir si cela vaut le coup de prolonger l'analyse de cette corrélation, on peut analyser les mesures de tendance centrale de chaque modalité de la variable qualitative X avec les boxplots de chacune de ces modalités en fonction de la variable quantitative Y.
import seaborn as sns
X = "Species"
Y = "SepalWidthCm"
sns.boxplot(y=iris[X], x=iris[Y], # Attention à bien intervertir X et Y ici.
showmeans=True, medianprops={"color":"black"},
meanprops={"marker":"o", "markeredgecolor":"black", "markerfacecolor":"gold"})
plt.show()
Sur ce graphique nous avons pris soin d'afficher la largeur moyenne des sépales moyenne de chaque espèce d'iris à l'aide de points jaunes. À vue d'oeil, il semblerait que ces moyennes ne soient pas homogènes mais nous allons vérifier cela avec une ANOVA.
Contrairement à ce que son nom indique, l'ANOVA (ANalyse Of VAriance) s'intéresse aux différences des moyennes de différentes populations. Concrètement si on reprend notre exemple avec le dataset des iris, nous avons 3 populations : celle des setosa, celle des versicolor et celle des virginica. Chacune de ces populations a 50 individus pour un total de 150 individus. On peut observer ces moyennes avec un groupby sur la variable qualitative du DataFrame iris.
iris_by_species = pd.DataFrame(iris.groupby("Species").mean()["SepalWidthCm"])
iris_by_species
Il y avait peu de chance pour que ces moyennes soient égales (genre vraiment très peu si vous suivez bien). Mais l'objectif maintenant est de savoir si ces différences entre ces moyennes sont significatives ou non.
modalites = iris["Species"].unique()
groupes = [] # On construit ici une liste des trois listes de valeurs pour chaque modalité de la variable qualitative.
for m in modalites: groupes.append( iris[iris["Species"]==m]["SepalWidthCm"] )
mean_list = list(iris_by_species["SepalWidthCm"]) # La liste des moyennes des trois modalités de SepalWidthCm.
MG = iris["SepalWidthCm"].mean() # La moyenne globale des largeurs de sépal.
print("Moyenne Générale = ", round(MG, 4))
La Somme des Carrés Totale (SCT) est la somme des carrés entre toutes les observations et la moyenne globale (MG).
SCT = ((iris["SepalWidthCm"]-MG)**2).sum()
print("SCT = ", SCT)
La Somme des Carrés intRa-groupes (SCR) est la somme des carrés des écarts entre tous les individus dans chaque groupe et la moyenne de chaque groupe.
SCR_list = []
for k in range(len(mean_list)):
SCR_list.append(((groupes[k]-mean_list[k])**2).sum())
SCR = np.sum(SCR_list)
print("SCR = ", round(SCR, 4))
La Somme des Carrés intEr-groupes (SCE) est la somme des carrés des écarts entre la moyenne de chaque groupe et la moyenne de chaque globale de l'ensemble des individus.
SCE_list = []
for k in range(len(mean_list)):
SCE_list.append(((mean_list[k]-MG)**2)*len(groupes[k]))
SCE = np.sum(SCE_list)
print("SCE = ", round(SCE, 4))
On retrouve bien la somme des carrés totale en faisant l'addition de la somme des carrés inter-groupes et intra-groupes.
print("On a bien SCR + SCE = {0} et SCT = {1}, magique, hein?".format(round(SCR+SCE,4), SCT))
Avant d'aller plus loin, on a également besoin de connaitre les degrés de liberté pour l'inter-groupes ET l'intra-groupes. La formule du degré de liberté inter-groupes est simple il s'agit de K-1 où K est le nombre de populations différentes. Et pour connaitre les degrés de liberté intra-groupes, il faut appliquer la formule (n-K).
n = len(iris["Species"])
k = len(modalites)
ddl_inter = k-1
ddl_intra = n-k
print("Degré de liberté inter-groupes : ", ddl_inter)
print("Degré de liberté intra-groupes : ", ddl_intra)
Nous nous fixerons le seuil de tolérance standard de α = 5%.
La statistique F suit une loi de Fisher-Snedechor et suit la formule : SCE x ddl_intra / (SCR x ddl_inter).
On rejettera l'hypothèse nulle selon laquelle il n'y a pas de corrélation dès que la statistique F sera supérieur au quantile d'ordre (1-α) de la loi de Fisher-Snedechor que je vous invite à voir ici, désignée par FK-1, n-K; 1-α. On cherche donc la valeur théorique F2, 147 ; 0.95 dans la table de loi fournie en lien dans notre cas. On cherche donc la valeur de la colonne 2 et de la ligne 147. La valeur qui s'y approche le plus (colonne 2, ligne 150) est 3.06. Donc F2, 147 ; 0.95 = 3.06.
F_statistic = SCE*ddl_intra / (SCR*ddl_inter)
F_statistic
On voit donc que la Fstatistic est nettement supérieur à F2, 147 ; 0.95, par conséquent on rejette l'hypothèse nulle au seuil de 5% : il existe bien une corrélation entre la largeur des sépales et la variété de l'iris.
Le rapport de corrélation η² mesure l'intensité d'une relation entre une variable qualitative et une variable quantitative : η² = SCE/SCT.
eta_carre = SCE / SCT
eta_carre
La différence entre les variétés d'iris rend donc compte de 39.19% de la variété totale de la largeur des sépales.
Nous présentons ici le DataFrame ozone, issu du Laboratoire de mathématiques appliquées de l'Agrocampus Ouest qui contient 112 données recueillies à Rennes durant l'été 2001, qui contient les 14 variables suivantes :
ozone = pd.read_csv("data/tuto/ozone.txt", sep=";", decimal=",")
ozone
Objectif de l'analyse : On souhaite étudier le lien entre le pic d'ozone journalier maxO3 et un certain nombre de facteurs potentiellement explicatifs afin de proposer un modèle de régression permettant de prévenir la population.
On peut représenter graphiquement le nuage de points de la teneur maximale en ozone observée sur la journée maxO3 en fonction de la température observée à 12h T12.
fig, ax = plt.subplots(figsize=(6, 6))
ax = sns.scatterplot(x="T12", y="maxO3", data=ozone, color="saddlebrown")
ax.set(xlabel="T12", ylabel="MaxO3")
ax.xaxis.set_major_locator(plt.MaxNLocator(5))
ax.set_title("Teneur maximale observée selon la température à 12h", fontsize=20)
plt.grid()
plt.show()
Ce nuage de points nous fait penser à un alignement selon une forme qui n'est pas très loin d'une droite.
Nous lançons une régression linéaire simple sur le nuage de points de la partie précédente. Pour connaître les coefficients de la modélisation, nous affichons le sommaire de la régression.
reg_simp = smf.ols('maxO3 ~ T12', data=ozone).fit()
reg_simp.summary()
Nous obtenons des statistiques sur les coefficients obtenus :
Les p-valeurs sont inférieures à 5 %. À un niveau de test de 5 %, on rejette donc l'hypothèse selon laquelle le paramètre est égal à 0 : les paramètres sont donc significativement différents de 0. Ici, on voit que la variable T12 est significative.
Quant au $R^{2}$, il est de l'ordre de 0.6. Ce n'est pas très élevé, mais ceci est logique au vu de la dispersion du nuage de points originel.
On peut maintenant afficher la droite de régression linéaire sur le nuage de points.
ax = sns.lmplot(x="T12", y="maxO3", data=ozone, ci=None, scatter_kws = {"color": "saddlebrown"}, line_kws={"color":"black"})
ax.set(xlabel="T12", ylabel="MaxO3")
plt.title("Teneur maximale observée selon la température à 12h", fontsize=20)
plt.grid()
plt.show()
On peut également représenter les valeurs ajustées en fonction des valeurs observées.
fig, ax = plt.subplots(figsize=(6, 6))
ozone["maxO3_ajust_s"] = reg_simp.predict()
X_plot = [ozone["maxO3"].min(), ozone["maxO3"].max()]
ax = sns.scatterplot(x="maxO3", y="maxO3_ajust_s", data=ozone, color="saddlebrown")
plt.plot(X_plot, X_plot, color="black")
ax.set_title("Teneur maximale observée selon la température à 12h", fontsize=20)
ax.set(xlabel="MaxO3", ylabel="MaxO3 ajusté")
plt.grid()
plt.show()
La droite qui s'affiche est la première bissectrice. Si le modèle était parfait, les valeurs réelles et les valeurs ajustées seraient égales, donc sur un tel graphique, les points seraient alignés sur la droite d'équation $y=x$, soit la première bissectrice.
On peut ajouter les résidus au tableau grâce à reg_simp.resid.
ozone["residu_s"] = reg_simp.resid
ozone
On peut maintenant afficher l'histogramme des résidus.
plt.figure(figsize=(5, 4))
plt.hist(ozone["residu_s"], density=True, color="saddlebrown")
plt.title("Histogramme des résidus", fontsize=22)
plt.xlabel("Résidus")
plt.grid()
plt.show()
L'allure de l'histogramme est assez classique : centrée et à peu près symétrique.
Prévoyons maintenant la concentration en ozone d'une journée. Sachant que la température prévue de cette journée est de 19 °C, on peut utiliser notre modèle de régression à des fins de prévision, grâce à la commande reg_simp.predict.
a_prevoir = pd.DataFrame({"T12":[19]})
maxO3_prev = reg_simp.predict(a_prevoir)
print(round(maxO3_prev[0], 2))
On obtient une concentration d'ozone d'environ 76.5. Jusqu'à maintenant, la régression linéaire ne fait intervenir qu'une seule variable explicative. Nous allons maintenant voir comment ça se passe quand il y en a plusieurs.
Pour ceux qui n'ont pas suivi la partie A, nous représentons ici le DataFrame ozone, issu du Laboratoire de mathématiques appliquées de l'Agrocampus Ouest qui contient 112 données recueillies à Rennes durant l'été 2001, qui contient les 14 variables suivantes :
ozone = pd.read_csv("data/tuto/ozone.txt", sep=";", decimal=",")
ozone
Objectif de l'analyse : On cherche cette fois-ci à expliquer maxO3 en fonction de l'ensemble des autres variables.
Le premier objectif est de repérer quelles sont les variables qui n'ont aucune incidence sur la variable à expliquer.
Nous nous fixerons le seuil de tolérance standard de α = 5%.
On pratique la régression linéaire multiple à l'aide de smf.ols().fit(). Les paramètres non-significatifs seront ceux dont _la pvalue est supérieure au niveau de test (5%)_ . Voici une méthode pour n'afficher que les paramètres du sommaire.
reg_multi = smf.ols("maxO3~T9+T12+T15+Ne9+Ne12+Ne15+maxO3v", data=ozone).fit()
print("R² = ", reg_multi.rsquared)
reg_multi.summary().tables[1]
On constate ici que certains paramètres ne sont pas significativement différents de 0, car leur p-valeur n'est pas inférieure à 5 %, le niveau de test que nous souhaitons.
Le $R^{2}$ vaut environ 0.75, et le $R^{2}$ ajusté est d'environ 0.74. Cette valeur est plus élevée qu'en régression linéaire simple, et c'est logique, car lorsque l'on rajoute des variables explicatives potentielles, on accroît naturellement la valeur de ces $R^{2}$.
On va donc maintenant retirer les variables non significatives. On commence alors par la variable la moins significative : Ne15, car elle a une p-valeur de 0.93.
reg_multi = smf.ols("maxO3~T9+T12+T15+Ne9+Ne12+maxO3v", data=ozone).fit()
reg_multi.summary().tables[1]
On voit maintenant que c'est Ne12 la moins significative, avec une p-valeur de 0.79.
reg_multi = smf.ols("maxO3~T9+T12+T15+Ne9+maxO3v", data=ozone).fit()
reg_multi.summary().tables[1]
On voit maintenant que l'on peut retirer T9.
reg_multi = smf.ols("maxO3~T12+T15+Ne9+maxO3v", data=ozone).fit()
reg_multi.summary().tables[1]
Puis T15.
reg_multi = smf.ols("maxO3~T12+Ne9+maxO3v", data=ozone).fit()
reg_multi.summary().tables[1]
Maintenant, les pvalues sont toutes inférieures à 5%. Affichons donc le sommaire tout entier.
Affichons maintenant le sommaire tout entier avec .summary().
reg_multi.summary()
Le 𝑅2 vaut environ 0.75, tout comme le 𝑅2 ajusté. On peut donc utiliser ce modèle à des fins de prévisions.
Si l'on souhaite prévoir la concentration journalière en ozone, sachant que la température prévue à 12h T12 sera de 15 °C, que la valeur de Ne9 sera de 2, et que la concentration maxO3 de la veille vaut 100, alors on saisit les lignes suivantes.
a_prevoir = pd.DataFrame({"T12": 15, "Ne9": 2, "maxO3v": 100}, index=[0])
maxO3_prev = reg_multi.predict(a_prevoir)
print(round(maxO3_prev[0], 2))
On obtient alors une concentration maxO3 de 84.
Nous allons ici réaliser les tests à un niveau α = 5%.
Récupérons le nombre d'individus n de l'échantillon et le nombre de variables p. Nous allons mener des analyses sur les valeurs atypiques et/ou influentes en travaillant sur un dataframe appelé analyses.
alpha = 0.05
n = ozone.shape[0]
p = 4
analyses = pd.DataFrame({"obs":np.arange(1, n+1)})
analyses
On peut calculer les leviers comme ceci, en sachant que le seuil des leviers est de $2∗\frac{p}{n}$ .
analyses["levier"] = reg_multi.get_influence().hat_matrix_diag
seuil_levier = 2*p/n
plt.figure(figsize=(15, 5))
plt.bar(analyses["obs"], analyses["levier"], color="crimson")
plt.plot([0, 115], [seuil_levier, seuil_levier], color="black")
plt.title("Détermination des leviers", fontsize=25)
plt.xlabel("Observations", fontsize=20)
plt.ylabel("Leviers", fontsize=20)
plt.xticks(np.arange(0, 115, step=5))
plt.show()
Pour sélectionner les points pour lesquels le levier est supérieur au seuil, on exécute la ligne suivante.
analyses.loc[analyses["levier"]>seuil_levier, :]
Si l'on souhaite maintenant calculer les résidus studentisés, nous écrivons ceci, sachant que le seuil pour les résidus studentisés est une loi de Student à n-p-1 degrés de liberté.
from scipy.stats import t, shapiro
analyses["rstudent"] = reg_multi.get_influence().resid_studentized_internal
seuil_rstudent = t.ppf(1-alpha/2,n-p-1)
plt.figure(figsize=(15, 5))
plt.bar(analyses["obs"], analyses["rstudent"], color="crimson")
plt.plot([0, 115], [seuil_rstudent, seuil_rstudent], color="black")
plt.plot([0, 115], [-seuil_rstudent, -seuil_rstudent], color="black")
plt.title("Analyse des résidus studentisés", fontsize=25)
plt.xlabel("Observation", fontsize=20)
plt.ylabel("Résidus studentisés", fontsize=20)
plt.xticks(np.arange(0, 115, step=5))
plt.show()
Le seuil des distances de Cook est n-p. On peut alors détecter les observations influentes.
influence = reg_multi.get_influence().summary_frame()
analyses["dcooks"] = influence["cooks_d"]
seuil_dcook = 4/(n-p)
plt.figure(figsize=(15, 5))
plt.bar(analyses["obs"], analyses["dcooks"], color="crimson")
plt.plot([0, 115], [seuil_dcook, seuil_dcook], color="black")
plt.title("Distances de Cook", fontsize=25)
plt.xlabel("Observation", fontsize=20)
plt.ylabel("Leviers", fontsize=20)
plt.xticks(np.arange(0, 115, step=5))
plt.show()
On ne retire des points qu'après avoir vérifié qu'ils sont effectivement atypiques, voire aberrants, au vu du modèle estimé.
Il nous faut maintenant vérifier la colinéarité des variables.
from statsmodels.stats.outliers_influence import variance_inflation_factor
variables = reg_multi.model.exog
[variance_inflation_factor(variables, i) for i in np.arange(1,variables.shape[1])]
Ici, tous les coefficients sont inférieurs à 10, il n'y a donc pas de problème de colinéarité.
Pour l'analyse des résidus, voici comment d'une traite :
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 5))
sm.qqplot(reg_multi.resid, ax=ax1, line="45", fit=True, color="crimson")
ax1.set_title("Droite de Henry - Normalité des résidus", fontsize=20)
ax1.set_xlabel("Quantiles théoriques" ,fontsize=16)
ax1.set_ylabel("Quantiles observés", fontsize=16)
ax2.plot(reg_multi.fittedvalues, reg_multi.resid, ".", color="crimson", alpha=0.3)
ax2.set_title("Nuage de la variance résiduelle - Homoscédasticité", fontsize=20)
ax2.set_xlabel("", fontsize=16)
ax2.set_ylabel("Résidus", fontsize=16)
plt.show()
On peut aussi tester la normalité des résidus avec le test de Shapiro-Wilk.
shapiro(reg_multi.resid)
Ici, l'hypothèse de normalité est remise en cause (p-value = 0.003 < 0.05).
Néanmoins, l'observation des résidus, le fait qu'ils ne soient pas très différents d'une distribution symétrique, et le fait que l'échantillon soit de taille suffisante (supérieure à 30) permettent de dire que les résultats obtenus par le modèle linéaire gaussien ne sont pas absurdes, même si le résidu n'est pas considéré comme étant gaussien.
Nous présentons ici le DataFrame maladie, issu d'un article dans le South African Medical Journal (Rousseauw et al, 1983), qui contient les informations de 462 patients d'Afrique du Sud. On y trouve les 12 variables suivantes :
maladie = pd.read_csv("data/tuto/maladie.txt", sep=";", decimal=".")
maladie
Objectif de l'analyse : Prévoir la présence (1) ou l'absence (0) d'une maladie cardio-vasculaire chd en fonction de constantes de santé chez un individu.
Pour étudier le fait d'être malade en fonction de l'âge, on peut visualiser le nuage de points.
plt.figure(figsize=(12, 2))
sns.scatterplot(x="age", y="chd", data=maladie, color="mediumvioletred")
plt.title("Présence de maladie selon l'âge", fontsize=25)
plt.xlabel("Âges", fontsize=20)
plt.ylabel("Patient Malade (chd)", fontsize=15)
plt.xticks(range(15, 66, 5), fontsize=20)
plt.yticks(range(2), fontsize=25)
plt.show()
Il y a des 0 et des 1, mais il est ici difficile de dire si l'on est plus ou moins malade en fonction de l'âge.
On voit également qu'une régression linéaire sur un tel nuage de points n'aurait aucun sens, car elle nous donnerait des valeurs qui ne seraient quasiment jamais sur 0 ni 1.
On peut calculer des classes d'âge et les proportions de malades associées.
maladie["cl_age"] = pd.cut(maladie["age"], bins = np.arange(15, 75, 10), right=False)
prop = pd.crosstab(maladie["cl_age"], maladie["chd"], normalize="index")
prop_chd = pd.DataFrame(data = {"age": np.concatenate((np.array([15]),
np.repeat(np.arange(25,65,10), repeats=2),
np.array([65])), axis=0),
"prop_chd": np.repeat(prop.loc[:,1].values, repeats=2)})
prop_chd
On peut alors représenter graphiquement ces proportions.
plt.figure(figsize=(12, 6))
ax = sns.scatterplot(x="age", y="chd", data=maladie, color="mediumvioletred")
plt.plot(prop_chd["age"], prop_chd["prop_chd"], color="black", label="Proportion de malades")
plt.title("Calcul des proportions de malades", fontsize=25)
plt.xlabel("Âges", fontsize=20)
plt.ylabel("Patient Malade (chd)", fontsize=20)
plt.xticks(range(15, 66, 5), fontsize=20)
plt.yticks(fontsize=15)
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
plt.show()
On y voit une fonction en escalier avec une forme de "S".
Effectuons donc une régression logistique de CHD en fonction de l'âge avec smf.glm().fit().
reg_log1 = smf.glm("chd ~ age", data=maladie, family=sm.families.Binomial()).fit()
reg_log1.summary()
beta1 = reg_log1.params[0]
beta2 = reg_log1.params[1]
x = np.linspace(start=15, stop=65, num=500)
y = np.exp(beta1+beta2*x)/(1+np.exp(beta1+beta2*x))
reg_log = pd.DataFrame(data={"age": x, "prop_chd": y})
reg_log
On souhaite superposer la fonction de lien obtenue par régression logistique sur le graphique précédent.
plt.figure(figsize=(12, 6))
ax = sns.scatterplot(x="age", y="chd", data=maladie, color="mediumvioletred")
plt.plot(prop_chd["age"], prop_chd["prop_chd"], color="black", label="Proportion de malades")
plt.plot(reg_log["age"], reg_log["prop_chd"], color="red", label="Courbe logistique")
plt.title("Calcul des proportions de malades", fontsize=25)
plt.xlabel("Âges", fontsize=20)
plt.ylabel("Patient Malade (chd)", fontsize=20)
plt.xticks(range(15, 66, 5), fontsize=20)
plt.yticks(fontsize=15)
ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))
plt.show()
La courbe rouge est celle qui est obtenue par régression logistique. Si l'on avait voulu considérer l'ensemble des variables médicales (et non pas seulement l'âge comme jusqu'à présent), nous aurions écrit la ligne suivante.
reg_log2 = smf.glm("chd~sbp+tobacco+ldl+adiposity+famhist+typea+obesity+alcohol+age",
data=maladie, family=sm.families.Binomial()).fit()
reg_log2.summary()
Certaines des variables obtenues ont des p-valeurs qui sont inférieures au niveau de test de 5 %, ce qui nous indique qu'elles sont bien significatives. Certaines autres ne sont pas en dessous de ce seuil. On peut donc passer sur une procédure de sélection en retirant les variables non significatives au fur et à mesure, comme vu précédemment.
Nous présentons ici le DataFrame ble qui contient les rendements de blé pour 80 parcelles en fonction de la variété de blé, et qui contient les 3 variables suivantes :
ble = pd.read_csv("data/tuto/ble.txt", sep=";", decimal=".")
ble
Objectif de l'analyse : Nous cherchons à comprendre si les facteurs ont une influence sur le rendement de blé rdt.
On peut visualiser l'influence de la variété en affichant les boîtes à moustaches.
ax = sns.boxplot(x="variete", y="rdt", data=ble, color="goldenrod")
plt.xlabel("Variété de blé")
plt.ylabel("Rendement")
plt.title("Boîtes à moustaches")
plt.show()
Les 4 variétés semblent assez différentes, même si l'ordre de grandeur de ces écarts n'est pas très grand. La question sera de savoir si ces écarts sont significatifs ou pas. C'est l'ANOVA qui nous permettra de répondre à cette question.
Étudions maintenant l'influence de la présence ou non de pesticide sur le rendement.
ax = sns.boxplot(x="phyto", y="rdt", data=ble, color="goldenrod")
plt.xlabel("Traitement phytosanitaire")
plt.ylabel("Rendement")
plt.title("Boîtes à moustaches")
plt.show()
Ici, les boîtes à moustaches ne sont pas très distinctes, même s'il y a un peu plus de variance dans le cas "SANS pesticide". La présence de pesticide a-t-elle un impact sur le rendement ? L'ANOVA nous permet de confirmer ou d'infirmer cette intuition. Lançons l'ANOVA pour tester l'influence de la variété de blé.
anova_variete = smf.ols("rdt~variete", data=ble).fit()
anova_variete.summary()
On y voit les paramètres estimés (dans la colonne "Estimate"), mais ici, ce ne sont pas les paramètres qui nous intéressent le plus.
Ce qui nous intéresse réellement, c'est le test de Fisher. La p-valeur de ce test ($7.67∗10^{-7}$) est très petite et largement inférieure à 5%. On rejette donc l'hypothèse H0 selon laquelle $α_{1}=α_{2}=α_{3}=α_{4}=0$.
La variété de blé a donc bien un effet sur le rendement, comme nous en avions l'intuition en regardant les boîtes à moustaches. Pour obtenir le tableau de l'analyse de la variance, on utilise la commande anova_lm.
sm.stats.anova_lm(anova_variete, typ=2)
On réalise ensuite l'ANOVA sur le pesticide utilisé.
anova_phyto = smf.ols("rdt~phyto", data=ble).fit()
anova_phyto.summary()
Puis on observe les p-values grâce à anova_lm( anova_phyto , typ = 2 ).
sm.stats.anova_lm(anova_phyto, typ=2)
On trouve ici une p-valeur de 0.8, ce qui est très au-dessus de 5 %. On ne rejette donc pas l'hypothèse H0 selon laquelle $α_{1}=α_{2}=0$.
Il n'y a pas ici d'effet du pesticide sur le rendement de blé, tout au moins pas d'effet significatif.
Jusqu'ici, nous avons étudié les 2 facteurs (variété et pesticide) séparément. Cependant, la variété et le pesticide peuvent avoir des interactions qui influent sur le rendement.
En effet, même si l'on a montré que, globalement, le pesticide n'a pas d'effet sur le rendement, il se peut que, pour une variété précise, il y ait quand même un effet du pesticide sur le rendement. L'ANOVA à 2 facteurs va nous permettre d'étudier ces éventuelles interactions.
anova_variete_phyto = smf.ols("rdt~variete*phyto", data=ble).fit()
anova_variete_phyto.summary()
Puis on observe les p-values grâce à anova_lm( anova_variete_phyto , typ = 2 ).
sm.stats.anova_lm(anova_variete_phyto)
On voit sur le tableau 3 lignes :
La p-valeur des interactions (93,75 %) est très largement supérieure à 5 % ; on en déduit donc que les interactions n'ont pas d'impact sur le rendement.
En pratique, on part toujours du tableau de l'ANOVA à 2 facteurs pour tester les interactions. Si elles sont significatives, on les conserve. Sinon, on teste séparément les 2 facteurs séparément par des ANOVA à 1 facteur.