PYTHON - TUTORIEL

Par Bertrand Delorme, Data Analyst

A. Bases & Index
  • A.1. Création d'un DataFrame
  • A.2. Importez vos tableaux
  • A.3. Index & Réindex
  • B. Localisations de données
  • B.1. Localisations simples
  • B.2. Restrictions de DataFrame
  • B.3. Localisations à restrictions multiples
  • B.4. Repères de données manquantes
  • C. Manipulations
  • C.1. Opérations numériques internes
  • C.2. Tri d'un DataFrame
  • C.3. La fonction groupby()
  • C.4. La fonction pivot_table()
  • C.5. Appliquez une fonction sur une colonne
  • D. Jointures & Concaténations
  • D.1. Concaténation
  • D.2. Jointure interne
  • D.3. Jointure externe
  • E. Date & Heure dans un DataFrame
  • E.1. Le module DateTime et mise en contexte
  • E.2. Interprétez vos données en temps et en heure
  • E.3. Récupérez un indice temporel
  • E.4. Triez votre tableau de façon chronologique
  • E.5. Représentez graphiquement une évolution temporelle
  • GRAPHIQUES SOUS MATPLOTLIB
    A. Pyplot & Généralités
  • A.1. Introduction à Matplotlib
  • A.2. Traçage d'un graphe simple
  • A.3. Arguments de traçages
  • A.4. Titres & Légendes
  • A.5. Taille & Axes
  • A.6. Couleurs sous Matplotlib
  • B. Graphiques spécifiques
  • B.1. Diagramme en bâtons
  • B.2. Diagramme circulaire
  • B.3. Nuage de points
  • B.4. Histogramme
  • B.5. Boîte à moustaches
  • B.6. Utiliser Pylab
  • C. Disposition de graphes multiples
  • C.1. Figures & Axes
  • C.2. Fixation des marges
  • C.3. Disposition multiple
  • C.4. Graphiques insérés
  • C.5. Ajustements
  • D. Graphiques à partir d'un DataFrame
  • D.1. Mise en contexte & Support
  • D.2. Histogramme selon une variable qualitative
  • D.3. Créez un diagramme circulaire
  • D.4. Nuage de points selon deux variables quantitatives
  • D.5. Comparez vos boîtes à moustaches
  • D.6. Affichez la matrices des corrélations du DataFrame
  • 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.

    In [1]:
    x = 4
    x
    
    Out[1]:
    4

    Dans cet exemple, nous avons déclaré, puis initialisé la variable x avec la valeur 4. Ce qu'il s’est passé plusieurs choses :

    • Python a « deviné » que la variable était un entier. On dit que Python est un langage au typage dynamique.
    • Python a alloué (réservé) l’espace en mémoire pour y accueillir un entier. Chaque type de variable prend plus ou moins d’espace en mémoire. Python a aussi fait en sorte qu’on puisse retrouver la variable sous le nom x.
    • Enfin, Python a assigné la valeur 4 à la variable x.

    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:

    • Les entiers int.
    • Les nombres décimaux float.
    • Les chaînes de caractères str.

    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.

    In [2]:
    a = 3.14
    print(a)
    
    y = "Hello World!"
    print(y)
    
    z = '''C'est pas dur de coder!'''
    print(z)
    
    3.14
    Hello World!
    C'est pas dur de coder!
    

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

    In [3]:
    x = 4
    print(x+6)
    print(x-3)
    print(x*5)
    print(x/2)
    
    10
    1
    20
    2.0
    
    In [4]:
    y = 18
    x*(y-16)+10
    
    Out[4]:
    18

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

    In [5]:
    2**10
    
    Out[5]:
    1024

    Pour obtenir le quotient et le reste d'une division entière, on utilise respectivement les symboles // et modulo %.

    In [6]:
    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)
    
    Rappelons que la division euclidienne 20/7 se décompose comme : 20 = 2*7 + 6 .
    Quotient de la division euclidienne :  2
    Reste de la division euclidienne :  6
    

    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.

    In [7]:
    i = 5
    i = i+1
    i
    
    Out[7]:
    6
    In [8]:
    i = 10
    i+=4
    i
    
    Out[8]:
    14

    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.

    In [9]:
    round(4.7345)
    
    Out[9]:
    5

    Vous pouvez aussi utiliser round(x, n) pour arrondir à n nombres après la virgule.

    In [10]:
    round(3.14159, 2)
    
    Out[10]:
    3.14

    Pour les chaînes de caractères, deux opérations sont possibles: l'addition et la multiplication.

    In [11]:
    chaine = " Salut "
    print(chaine)
    print(chaine + "Python!")
    print(chaine * 4)
    
     Salut 
     Salut Python!
     Salut  Salut  Salut  Salut 
    

    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.

    In [12]:
    x = 10
    print( type(x) )
    
    y = 2.5
    print( type(y) )
    
    z = "Hey!"
    print( type(z) )
    
    <class 'int'>
    <class 'float'>
    <class 'str'>
    

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

    In [13]:
    print( int(34.8) )
    print( float(4) )
    print( str(55) )
    
    type(3/4) # La division de 2 integers renvoie un float !
    
    34
    4.0
    55
    
    Out[13]:
    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.

    In [14]:
    import math
    

    Voici les fonctions mathématiques spécifiques utilisables avec le module math avec un flottant x quelconque :

    • math.sqrt(x) : renvoie la racine carrée de x sous forme de float.
    • math.floor(x) : renvoie la partie entière sous forme de float.
    • math.ceil(x) : arrondit x à l'entier supérieur sous forme de int.
    • math.factorial(n) : renvoie le factoriel de n.
    • math.fmod(x, y) : calcule x modulo y.
    • math.pow(x, n) : calcule x à la puissance n.
    • math.exp(x) : renvoie l'exponentielle de x.
    • math.log(x) : renvoie le logarithme de x.
    • math.log10(x) : renvoie le logarithme en base 10 de x.
    • math.log(x, 2) : renvoie le logarithme en base 2 de x.
    In [15]:
    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))
    
    Racine de 25 =  5.0
    4! =  24
    2¹⁰ =  1024.0
    exp(2.5) =  12.182493960703473
    

    Le module math permet également des tests :

    • math.isinf(x) : teste si x est infini (inf) et renvoie True si c'est le cas.
    • math.isnan(x) : test si x est NaN (Not a Number) et renvoie True si c'est le cas.
    In [16]:
    x = 4
    print(math.isinf(x), math.isnan(x))
    
    False False
    

    Enfin le module math produit aussi fonctions trigonométriques suivantes :

    • math.pi : renvoie la constante π.
    • math.degrees(x) : avec x en radians, convertit x en degrés.
    • math.radians(x) : avec x en degrés, convertit x en radians.
    • math.cos(x) : calcule le cosinus de x avec x en radians.
    • math.sin(x) : calcule le sinus de x avec x en radians.
    • math.tan(x) : calcule la tangeante de x avec x en radians.
    In [17]:
    t = math.pi / 6
    print(math.cos(t))
    print(math.sin(t))
    print(math.tan(t))
    
    0.8660254037844387
    0.49999999999999994
    0.5773502691896257
    

    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.

    In [18]:
    print (" Hello world !")
    print (" Hello world !", end ="")
    print (" It's me !")
    
     Hello world !
     Hello world ! 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.

    In [19]:
    print(3*6) ; print( "hey!") ; tartampion = 4 ; print(tartampion)
    
    18
    hey!
    4
    

    Il est également possible d’afficher le contenu de plusieurs variables (quel que soit leur type) en les séparant par des virgules.

    In [20]:
    x = "John a" ; y = 40 ; z = "ans !"
    print(x, y, z)
    
    John a 40 ans !
    

    La méthode .format() permet une meilleure organisation de l’affichage des variables.

    In [21]:
    x = 12
    print("J'ai {} euros sur moi.".format(x))
    
    J'ai 12 euros sur moi.
    

    On peut également y mettre plusieurs variables, en précisant l'ordre dans les accolades (le plus petit chiffre est 0!).

    In [22]:
    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))
    
    John a 25 ans, il a 12 euros sur lui et a mangé 40 pommes hier.
    

    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.

    In [23]:
    x = 32
    print("John a %d ans." %x)
    
    John a 32 ans.
    
    In [24]:
    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 ))
    
     On a 4500 G et 2575 C -> prop GC = 0.478041.
    

    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 :

    • Désigner l’endroit où sera placée la variable dans la chaîne de caractères.
    • Préciser le type de variable à formater, d pour un entier (i fonctionne également) ou f pour un float.
    • Éventuellement pour indiquer le format voulu. Ici .2 signifie une précision de deux décimales.

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

    In [25]:
    animaux = "girafe tigre"
    len(animaux)
    
    Out[25]:
    12

    On peut également connaître la lettre en i-ème position avec la localisation [ ], le 0 compte.

    In [26]:
    animaux[4]
    
    Out[26]:
    'f'

    On peut aussi utiliser les tranches (cf. chapitre suivant sur les listes).

    In [27]:
    animaux[3:10:2]
    
    Out[27]:
    'aetg'

    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 \".

    In [28]:
    print("Un retour à la ligne \n puis une tabulation \t puis un guillemet \"")
    
    Un retour à la ligne 
     puis une tabulation 	 puis un guillemet "
    

    Vous pouvez aussi utiliser astucieusement des guillemets doubles ou simples pour déclarer votre chaîne de caractères.

    In [29]:
    print ('Python est un "super" langage de programmation')
    
    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.

    In [30]:
    car = '''souris
    chat
    abeille'''
    car
    
    Out[30]:
    'souris\nchat\nabeille'

    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.

    In [31]:
    val = "3.4 17.2 atom"
    val
    
    Out[31]:
    '3.4 17.2 atom'

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

    In [32]:
    val2 = val.split()
    val2
    
    Out[32]:
    ['3.4', '17.2', 'atom']

    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.

    In [33]:
    float(val2[0]) + float(val2[1])
    
    Out[33]:
    20.599999999999998

    ① Minuscules & Majuscules : Les méthodes .lower() et .upper() renvoient un texte en minuscule et en majuscule respectivement.

    In [34]:
    car = "CunÉgoNDE!"
    print(car.lower(), car.upper())
    
    cunégonde! CUNÉGONDE!
    

    Vous pouvez aussi utiliser la méthode .capitalize() qui met la majuscule qu'à la première lettre.

    In [35]:
    quoi = "quoi? "
    vous = "vous n'avez jamais vu Les Visiteurs?"
    
    quoi.capitalize()+vous.capitalize()
    
    Out[35]:
    "Quoi? Vous n'avez jamais vu les visiteurs?"

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

    In [36]:
    vous.split()
    
    Out[36]:
    ['vous', "n'avez", 'jamais', 'vu', 'Les', 'Visiteurs?']
    In [37]:
    vous.split("a")
    
    Out[37]:
    ["vous n'", 'vez j', 'm', 'is vu Les Visiteurs?']
    In [38]:
    devise = "Liberté,Égalité,Fraternité"
    devise.split(",")
    
    Out[38]:
    ['Liberté', 'Égalité', 'Fraternité']

    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.

    In [39]:
    animaux = "girafe tigre singe souris"
    
    animaux.split(maxsplit = 1)
    
    Out[39]:
    ['girafe', 'tigre singe souris']
    In [40]:
    animaux.split(maxsplit = 2)
    
    Out[40]:
    ['girafe', 'tigre', 'singe souris']

    ③ Recherche : La méthode .find(), quant à elle, recherche une chaîne de caractères passée en argument.

    • Si l’élément recherché est trouvé, alors l’indice du début de l’élément dans la chaîne de caractères est renvoyé.
    • Si l’élément n’est pas trouvé, alors la valeur -1 est renvoyée.
    • Si l’élément recherché est trouvé plusieurs fois, seul l’indice de la première occurrence est renvoyé.
    In [41]:
    car = "Six scies scient six troncs."
    
    print(car.find("i"))
    print(car.find("a"))
    print(car.find("sci"))
    
    1
    -1
    4
    

    ④ Comptage : La méthode .count(), elle, compte le nombre d'occurrences des caracères donnés en argument.

    In [42]:
    car = "Six scies scient six troncs."
    
    car.count("i")
    
    Out[42]:
    4

    ⑤ Remplacements : La méthode .replace() remplace les chaînes caractères par ce que vous lui indiquez.

    In [43]:
    car = "Six scies scient six troncs."
    
    car.replace("troncs","citrons")
    
    Out[43]:
    'Six scies scient six citrons.'

    ⑥ Vérifications : La méthode .startswith() vérifie si une chaîne de caractères commence par l'argument.

    In [44]:
    car.startswith("Six")
    
    Out[44]:
    True
    In [45]:
    car.startswith("Anakin Skywalker")
    
    Out[45]:
    False

    ⑦ Nettoyage : La méthode .strip() permet de « nettoyer les bords » d’une chaîne de caractères.

    In [46]:
    car = " Six scies scient six troncs. "
    
    car.strip()
    
    Out[46]:
    'Six scies scient six troncs.'

    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.

    In [47]:
    car = "\nSix scies scient six troncs.\t"
    
    car.strip()
    
    Out[47]:
    'Six scies scient six troncs.'

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

    In [48]:
    seq = ["A", "T", "G", "A", "T"]
    "-".join(seq)
    
    Out[48]:
    'A-T-G-A-T'

    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.

    In [49]:
    animaux = ["girafe ", "tigre ", "singe ", "souris "]
    tailles = [5, 2.5 , 1.75 , 0.15]
    mixte = ["girafe ", 5, "souris ", 0.15]
    
    animaux
    
    Out[49]:
    ['girafe ', 'tigre ', 'singe ', 'souris ']

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

    Soyez très attentifs au fait que les indices d’une liste de n éléments commence à 0 et se termine à n-1.

    In [50]:
    animaux = ["girafe ", "tigre ", "singe ", "souris "]
    animaux[1]
    
    Out[50]:
    'tigre '

    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.

    In [51]:
    ani1 = ['girafe ', 'tigre ']
    ani2 = ['singe ', 'souris ']
    
    ani1 + ani2
    
    Out[51]:
    ['girafe ', 'tigre ', 'singe ', 'souris ']
    In [52]:
    ani2 * 3
    
    Out[52]:
    ['singe ', 'souris ', 'singe ', 'souris ', 'singe ', 'souris ']

    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.

    In [53]:
    a = []
    a
    
    Out[53]:
    []

    Puis lui ajouter deux éléments, l’un après l’autre, d’abord avec la concaténation.

    In [54]:
    a = a + [15]
    a += [-5]
    a
    
    Out[54]:
    [15, -5]

    Puis avec la méthode append().

    In [55]:
    a.append(13)
    a.append(-3)
    a
    
    Out[55]:
    [15, -5, 13, -3]

    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 : 0123
    Indiçages négatifs : -4-3-2-1
    Les indices négatifs reviennent à compter à partir de la fin. Leur principal avantage est que vous pouvez accéder au dernier élément d’une liste à l’aide de l’indice -1 sans pour autant connaître la longueur de cette liste. L’avant-dernier élément a lui l’indice -2, l’avant-avant dernier l’indice -3, etc...

    In [56]:
    animaux = ["girafe ", "tigre ", "singe ", "souris "]
    print(animaux[-1], animaux[-4])
    
    souris  girafe 
    

    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[ m : ] → renvoie la liste à partir du m-ième élément.
    • liste[ : n ] → renvoie la liste jusqu'au n-1-ième élément.
    • liste[ m : n ] → renvoie les éléments du m-ième au n-1-ième.
    • liste[ m : -n ] → renvoie les éléments du m-ième au -(n+1)-ième.
    In [57]:
    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])
    
    liste[3:] →  ['d', 'e', 'f', 'g', 'h', 'i', 'j']
    liste[:2] →  ['a', 'b']
    liste[2:4] →  ['c', 'd']
    liste[1:-1] →  ['b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
    

    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.

    In [58]:
    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::])
    
    liste[0:6:2] →  ['a', 'c', 'e']
    liste[::4] →  ['a', 'e', 'i']
    liste[4::] →  ['e', 'f', 'g', 'h', 'i', 'j']
    

    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.

    In [59]:
    animaux = ["girafe ", "tigre ", "singe ", "souris "]
    
    len(animaux)
    
    Out[59]:
    4

    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.

    In [60]:
    list(range(5))
    
    Out[60]:
    [0, 1, 2, 3, 4]

    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.

    In [61]:
    list(range(5, 10))
    
    Out[61]:
    [5, 6, 7, 8, 9]

    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.

    In [62]:
    list(range(0, 20, 2))
    
    Out[62]:
    [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
    In [63]:
    list(range(100, 2001, 300))
    
    Out[63]:
    [100, 400, 700, 1000, 1300, 1600, 1900]

    La fonction np.arange(n) génère range(n) directement sous la forme d'un tableau.

    In [64]:
    import numpy as np
    
    m = np.arange(3, 15, 2)
    m
    
    Out[64]:
    array([ 3,  5,  7,  9, 11, 13])

    Notez bien la différence:

    • np.arange(n) retourne un objet de type numpy.ndarray.
    • range(n) retourne un objet de type range.

    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.

    In [65]:
    np.linspace(3, 9, 6)
    
    Out[65]:
    array([3. , 4.2, 5.4, 6.6, 7.8, 9. ])

    Sachez qu’il est tout à fait possible de construire des listes de listes. Cette fonctionnalité peut parfois être très pratique.

    In [66]:
    ani1 = ["girafe", "tigre"]
    ani2 = ["singe", "souris"]
    ani3 = ["chien", "chat"]
    
    zoo = [ani1, ani2, ani3]
    zoo
    
    Out[66]:
    [['girafe', 'tigre'], ['singe', 'souris'], ['chien', 'chat']]

    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.

    In [67]:
    zoo[1]
    
    Out[67]:
    ['singe', 'souris']

    Pour accéder à un élément de la sous-liste, on utilise un double indiçage.

    In [68]:
    zoo[1][0]
    
    Out[68]:
    'singe'

    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.

    In [69]:
    a = [1, 2, 3]
    a.append (5)
    a
    
    Out[69]:
    [1, 2, 3, 5]

    Ceci est équivaut à la méthode ci-dessous.

    In [70]:
    a = [1, 2, 3]
    a += [5]
    a
    
    Out[70]:
    [1, 2, 3, 5]

    ② Insertion : La méthode .insert() insère un objet dans une liste avec un indice déterminé.

    In [71]:
    a = [1, 2, 3]
    a. insert (2, -15)
    a
    
    Out[71]:
    [1, 2, -15, 3]

    ③ Suppression par indice : L'instruction del() supprime un élément d’une liste à un indice déterminé.

    In [72]:
    a = [1, 2, 3]
    del(a[1])
    a
    
    Out[72]:
    [1, 3]

    ④ Suppression par élément : La méthode .remove() supprime un élément d’une liste à partir de sa valeur à sa première rencontre.

    In [73]:
    a = [1, 2, 3, 4, 3, 3, 5]
    a.remove(3)
    a
    
    Out[73]:
    [1, 2, 4, 3, 3, 5]

    ⑤ Tri : La méthode .sort() trie la liste.

    In [74]:
    a = [5, 2, 1, 4, 3]
    a.sort()
    a
    
    Out[74]:
    [1, 2, 3, 4, 5]

    ⑥ Inversion : La méthode .reverse() inverse la liste.

    In [75]:
    a = [1, 2, 3, 4, 5]
    a.reverse()
    a
    
    Out[75]:
    [5, 4, 3, 2, 1]

    ⑦ Comptage : La méthode .count() compte le nombre d’éléments (passés en argument) dans une liste.

    In [76]:
    a = [1, 2, 3, 4, 3, 3, 5, 4, 1, 5, 5, 5, 4]
    a.count(5)
    
    Out[76]:
    4

    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 = [].

    In [77]:
    seq = " CAAAGGTAACGC "
    
    seq_list = []
    for base in seq : 
        seq_list.append(base)
        
    seq_list
    
    Out[77]:
    [' ', 'C', 'A', 'A', 'A', 'G', 'G', 'T', 'A', 'A', 'C', 'G', 'C', ' ']

    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.

    In [78]:
    seq = " CAAAGGTAACGC "
    
    list(seq)
    
    Out[78]:
    [' ', 'C', 'A', 'A', 'A', 'G', 'G', 'T', 'A', 'A', 'C', 'G', 'C', ' ']

    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.

    In [79]:
    a = [1, 2, 3, 4]
    4 in a
    
    Out[79]:
    True
    In [80]:
    5 in a
    
    Out[80]:
    False

    La variation avec not permet, a contrario, de vérifier qu’un élément n’est pas dans une liste.

    In [81]:
    4 not in a
    
    Out[81]:
    False
    In [82]:
    5 not in a
    
    Out[82]:
    True

    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.

    In [83]:
    x = [1, 2, 3]
    y = x
    y.append(4)
    x
    
    Out[83]:
    [1, 2, 3, 4]
    In [84]:
    x = [1, 2, 3]
    y = x[:]
    y.append(4)
    x
    
    Out[84]:
    [1, 2, 3]

    Si vous êtes débutant, sautez ce sous-chapitre pour le moment.

    1°) Nombres pairs compris entre 0 et 30 :

    In [85]:
    print ([i for i in range (31) if i % 2 == 0])
    
    [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30]
    

    2°) Jeu sur la casse des mots d’une phrase :

    In [86]:
    message = "C'est sympa la BioInfo "
    
    msg_lst = message.split ()
    
    print ([ [ m.upper(), len (m)] for m in msg_lst ])
    
    [["C'EST", 5], ['SYMPA', 5], ['LA', 2], ['BIOINFO', 7]]
    

    3°) Formatage d’une séquence avec 60 caractères par ligne :

    In [87]:
    # 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 ))
    
    AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    

    4°) Formatage FASTA d’une séquence (avec la ligne de commentaire) :

    In [88]:
    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 ))
    
     >Sé quence de 150 alanines \ n AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    

    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.

    In [89]:
    animaux = ["girafe ", "tigre ", "singe ", "souris "]
    
    for animal in animaux:
        print(animal)
    
    girafe 
    tigre 
    singe 
    souris 
    

    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.

    In [90]:
    for animal in animaux:
        print(animal)
        print(animal*2)
    print("C'est fini")
    
    girafe 
    girafe girafe 
    tigre 
    tigre tigre 
    singe 
    singe singe 
    souris 
    souris souris 
    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.

    In [91]:
    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.
    
    tigre 
    singe 
    

    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.

    In [92]:
    for i in range(4):
        print(i)
    
    0
    1
    2
    3
    

    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.

    In [93]:
    for prenom in ['Joe', 'Bill', 'John']:
        print(prenom)
    
    Joe
    Bill
    John
    

    Revenons à notre liste animaux. Nous allons maintenant parcourir cette liste, mais cette fois par une itération sur ses indices.

    In [94]:
    animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
    
    for i in range(4):
        print(animaux[i])
    
    girafe 
    tigre 
    singe 
    souris 
    

    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.

    In [95]:
    animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
    
    for i in range(len(animaux)):
        print("L' animal {} est un(e) {}". format (i, animaux[i]))
    
    L' animal 0 est un(e) girafe 
    L' animal 1 est un(e) tigre 
    L' animal 2 est un(e) singe 
    L' animal 3 est un(e) souris 
    

    Python possède toutefois la fonction enumerate() qui vous permet d’itérer sur les indices et les éléments eux-mêmes.

    In [96]:
    animaux = ['girafe ', 'tigre ', 'singe ', 'souris ']
    
    for i, animal in enumerate(animaux):
        print ("L' animal {} est un(e) {}".format(i, animal))
    
    L' animal 0 est un(e) girafe 
    L' animal 1 est un(e) tigre 
    L' animal 2 est un(e) singe 
    L' animal 3 est un(e) souris 
    

    Python est capable d’effectuer toute une série de comparaisons entre le contenu de deux variables.

    Syntaxe PythonSignification
    == 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 à
    Python renvoie la valeur True si la comparaison est vraie et False si elle est fausse. True et False sont des booléens (un nouveau type de variable). Faites bien attention à ne pas confondre l’opérateur d’affectation = qui affecte une valeur à une variable et l’opérateur de comparaison == qui compare les valeurs de deux variables. Vous pouvez également effectuer des comparaisons sur des chaînes de caractères.

    In [97]:
    a = "ok"
    
    a=="okk"
    
    Out[97]:
    False

    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.

    In [98]:
    "u" < "v"
    
    Out[98]:
    True

    "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

    In [99]:
    " ali " < " alo"
    
    Out[99]:
    True
    In [100]:
    " abb " < " ada"
    
    Out[100]:
    True

    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.

    In [101]:
    i = 1
    while i <= 4:
        print (i)
        i = i + 1
    
    1
    2
    3
    4
    

    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 :

      1. Initialisation de la variable d’itération avant la boucle (ligne 1).
      1. Test de la variable d’itération associée à l’instruction while (ligne 2).
      1. Mise à jour de la variable d’itération dans le corps de la boucle (ligne 4).

    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.

    In [102]:
    i = 0
    while i < 10:
        reponse = input (" Entrez un entier supérieur à 10 : ")
        i = int (reponse)
    
     Entrez un entier supérieur à 10 : 11
    

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

    In [103]:
    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.
    
    La variable i = 1
    La variable i = 2
    On incrémente i de 4. i est maintenant égale à 7
    La variable i = 7
    La variable i = 8
    On incrémente i de 4. i est maintenant égale à 13
    La variable i = 13
    La variable i = 14
    On incrémente i de 4. i est maintenant égale à 19
    La variable i = 19
    

    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.

    In [104]:
    x = 2
    
    if x == 2 : 
        print("C'est bon!")
    
    C'est bon!
    
    In [105]:
    if x == "tigre" : 
        print("Hein? Quoi?")
    

    Il y a plusieurs remarques à faire concernant ces deux exemples :

    • Dans le premier exemple, le test étant vrai, l’instruction print("C'est bon!") est exécutée. Dans le second exemple, le test est faux et rien n’est affiché.
    • Les blocs d’instructions dans les tests doivent forcément être indentés comme pour les boucles for et while. L’indentation indique la portée des instructions à exécuter si le test est vrai.
    • Comme avec les boucles for et while, la ligne qui contient l’instruction if se termine par le caractère deux-points « : ».

    De nouveau, faites bien attention à l’indentation ! Vous devez être très rigoureux sur ce point.

    In [106]:
    nombres = [4, 5, 6]
    
    for nb in nombres :
        if nb == 5:
            print (" Le test est vrai ")
    print (" car la variable nb vaut {}". format (nb ))
    
     Le test est vrai 
     car la variable nb vaut 6
    

    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.

    In [107]:
    x = 3
    
    if x == 2 :
        print("C'est bon!")
    else:
        print("C'est pas bon!!")
    
    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.

    In [108]:
    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 ")
    
     choix d'une thymine 
    

    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 1OpérateurCondition 2Résultat
    VRAIOUVRAIVRAI
    VRAIOUFAUXVRAI
    FAUXOUVRAIVRAI
    FAUXOUFAUXFAUX
    VRAIETVRAIVRAI
    VRAIETFAUXFAUX
    FAUXETVRAIFAUX
    FAUXETFAUXFAUX
    In [109]:
    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.")
    
    Le test 1 est faux.
    Le test 2 est vrai.
    

    L’instruction break stoppe la boucle.

    In [110]:
    for i in range (5):
        if i > 2:
            break
        print (i)
    
    0
    1
    2
    

    L’instruction continue saute à l’itération suivante, sans exécuter la suite du bloc d’instructions de la boucle.

    In [111]:
    for i in range (5):
        if i == 2:
            continue
        print (i)
    
    0
    1
    3
    4
    

    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.

    In [112]:
    def inverse(x):
        if x != 0: return 1/x
        else: pass
    
    print(inverse(4))
    print(inverse(0))
    
    0.25
    None
    

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

    In [113]:
    1/10 == 0.1
    
    Out[113]:
    True

    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.

    In [114]:
    (3 - 2.7) == 0.3
    
    Out[114]:
    False
    In [115]:
    3 - 2.7
    
    Out[115]:
    0.2999999999999998

    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.

    In [116]:
    def inverse(x):
        y = 1.0/x
        return(y)
    
    a = inverse(4)
    print(a)
    
    0.25
    

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

    In [117]:
    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.")
    
    0.25
    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.

    In [118]:
    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.")
    
    0.5
    1.0
    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.

    In [119]:
    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.")
    
    0.5
    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.

    In [120]:
    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.")
    
    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.

    In [121]:
    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)
    
    erreur, message : Valeur nulle interdite, fonction inverse.
    

    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.

    In [122]:
    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)
    
    Appel à une fonction non définie.
    

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

    In [123]:
    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)
    
    erreur float division by zero
    

    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.

    In [124]:
    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.")
    
    erreur  float division by zero
    -1000000000
    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.

    In [125]:
    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)
    
        Chaîne de caractères contenant aussi autre chose que des chiffres.
          :  123a
    

    En redéfinissant l’opérateur __str__ d’une exception, il est possible d’afficher des messages plus explicites avec la seule instruction print.

    In [126]:
    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.

    In [127]:
    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)
    
    exception AucunChiffre, depuis la fonction conversion avec le paramètre 123a.
    Fonction :  conversion
    

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

    In [128]:
    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.

    In [129]:
    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)
    
    (False, 0)
    (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 »:

    1. À laquelle vous passez aucune, une ou plusieurs variable(s) entre parenthèses. Ces variables sont appelées arguments. Il peut s’agir de n’importe quel type d’objet Python.
    2. Qui effectue une action.
    3. Et qui renvoie un objet Python ou rien du tout.

    Par exemple, si vous appelez la fonction len() de la manière suivante.

    In [130]:
    len([10, 11, 12])
    
    Out[130]:
    3

    Voici ce qui se passe :

    1. Vous appelez len() en lui passant une liste en argument (ici la liste [0, 1, 2]).
    2. La fonction calcule la longueur de cette liste.
    3. Elle vous renvoie un entier égal à cette longueur.

    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.

    In [131]:
    def carre(x):
        return(x**2)
    
    carre(4)
    
    Out[131]:
    16

    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 :

    In [132]:
    res = carre(5)
    print(res)
    
    25
    

    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.

    In [133]:
    def hello():
        print("bonjour!")
        
    hello()
    
    bonjour!
    

    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.

    In [134]:
    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)
    
    Out[134]:
    12

    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.

    In [135]:
    def carre_cube(x): return(x**2, x**3)
    carre_cube(3)
    
    Out[135]:
    (9, 27)

    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.

    In [136]:
    def carre_cube(x): return([x**2, x**3])
    carre_cube(3)
    
    Out[136]:
    [9, 27]

    Renvoyer un tuple ou une liste de deux éléments (ou plus) est très pratique en conjonction avec l’affectation multiple.

    In [137]:
    z1, z2 = carre_cube(4)
    z2
    
    Out[137]:
    64

    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.

    In [138]:
    def fct(x=4): return(x+6)
    
    fct()
    
    Out[138]:
    10
    In [139]:
    fct(12)
    
    Out[139]:
    18

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

    In [140]:
    def fct(x = 1 , y = 2 , z = 3) : return(x, y, z)
    
    fct()
    
    Out[140]:
    (1, 2, 3)
    In [141]:
    fct(3, 5, 9)
    
    Out[141]:
    (3, 5, 9)

    Python permet même de rentrer les arguments par mot-clé dans un ordre arbitraire.

    In [142]:
    fct(z = 88 , x = 1, y = 'π')
    
    Out[142]:
    (1, 'π', 88)

    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.

    In [143]:
    # Définition d'une fonction carre() :
    def carre (x):
        y = x **2
        return y
    
    # Programme principal :
    z = 5
    resultat = carre (z)
    print ( resultat )
    
    25
    

    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.

    In [144]:
    # 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))
    
    [36, 25, 16, 9, 4, 1, 0, 1, 4, 9, 16]
    

    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 :

    3! =3*2*1 = 6

    4! =4*3*2*1 = 30

    n! =n*(n-1)*...*2*1
    Voici le code Python avec une fonction récursive.

    In [145]:
    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)
    
    Out[145]:
    24

    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.

    In [146]:
    def ma_fonction(): print(x)
        
    x=3
    ma_fonction()
    
    3
    

    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.

    In [147]:
    def ma_fonction ():
        global x
        x = x + 1
    
    x = 1
    ma_fonction()
    x
    
    Out[147]:
    2

    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.

    In [148]:
    import random
    

    Prenons par exemple la liste suivante contenant 5 fruits.

    In [149]:
    panier = ["Pomme", "Poire", "Banane", "Ananas", "Orange"]
    panier
    
    Out[149]:
    ['Pomme', 'Poire', 'Banane', 'Ananas', 'Orange']

    Si l'on veut prendre un élément au hasard de cette liste appelée "panier", on utilise la fonction random.choices().

    In [150]:
    random.choices(panier)
    
    Out[150]:
    ['Ananas']

    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.

    In [151]:
    random.choices(panier)[0]
    
    Out[151]:
    'Banane'

    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.

    In [152]:
    random.choices(panier, k=8)
    
    Out[152]:
    ['Orange', 'Ananas', 'Banane', 'Ananas', 'Poire', 'Orange', 'Ananas', 'Poire']

    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.

    In [153]:
    random.sample(panier, k=2)
    
    Out[153]:
    ['Ananas', 'Pomme']

    Pour commencer, la fonction random.random() va générer un nombre aléatoire compris entre 0 et 1.

    In [154]:
    random.random()
    
    Out[154]:
    0.24419739641438687

    La fonction random.randint( a, b ) va générer un entier aléatoire compris entre a et b inclus.

    In [155]:
    random.randint(10, 20)
    
    Out[155]:
    12

    La fonction random.uniform( a, b ) va suivre la loi uniforme pour générer un flottant aléatoire entre a et b.

    In [156]:
    random.uniform(100, 101)
    
    Out[156]:
    100.18872160410643

    La fonction random.gauss(m, s) va suivre la distribution normale de moyenne m et d'écart-type s.

    In [157]:
    random.gauss(0, 1)
    
    Out[157]:
    0.18453935458923895

    On considère deux jeux de hasard :

    • Le jeu A est un jeu de pile ou face avec une pièce biaisée (pile avec une probabilité de p=0.49). On lance la pièce. Si l'on obtient pile, on gagne un euro, sinon on perd un euro.
    • Le jeu B est un jeu avec deux pièces biaisées. La pièce 1 donne pile avec une probabilité p1 = 0.09 et la pièce 2 donne pile avec une probabilité p2 = 0.74. Si la somme en jeu de K euros est un multiple de 3, on lance la pièce 1, sinon on lance la pièce 2. Comme dans le jeu A, si l'on obtient pile, on gagne un euro, sinon on perd un euro.

    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 ?

    In [158]:
    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 net est de : 5945.2 €
    

    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.

    In [159]:
    import pandas as pd
    
    df=pd.DataFrame({c: random.choices(range(-10, 10), k=4) for c in range(3)})
    df
    
    Out[159]:
    0 1 2
    0 0 9 9
    1 -8 7 -10
    2 -9 -1 9
    3 3 -10 6

    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 :

    • Le nom.
    • Le prénom.
    • L'âge.
    • Le lieu de résidence.

    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.

    In [160]:
    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.

    In [161]:
    bertrand = Personne()
    bertrand.nom
    
    Out[161]:
    'Dupont'

    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.

    In [162]:
    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)
    
    Dupont Jean 34 ans Paris
    

    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.

    In [163]:
    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)
    
    Micado Steve 25 ans Paris
    

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

    In [164]:
    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.

    In [165]:
    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)
    
    0
    1
    2
    

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

    In [166]:
    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.

    In [167]:
    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)
    
    Vous savez, moi je ne crois pas qu'il y ait de bonnes ou de mauvaises situations.
    _____________________________________________________________________________________________________________________________
    Vous savez, moi je ne crois pas qu'il y ait de bonnes ou de mauvaises situations.
    Moi, si je devais résumer ma vie aujourd'hui avec vous, je dirais que c'est d'abord des rencontres.
    

    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.

    In [168]:
    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)
    
    Vous savez, moi je ne crois pas qu'il y ait de bonnes ou de mauvaises situations.
    Moi, si je devais résumer ma vie aujourd'hui avec vous, je dirais que c'est d'abord des rencontres.
    Des gens qui m'ont tendu la main, à un moment où je ne pouvais pas, où j'étais seul chez moi.
    

    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.

    In [169]:
    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()
    
    Salut tout le monde.
    La forme ?
    
    

    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.

    In [170]:
    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()
    
    Jusqu'à présent, 0 objets ont été créés.
    
    In [171]:
    a = Compteur()
    Compteur.combien()
    
    Jusqu'à présent, 1 objets ont été créés.
    
    In [172]:
    b = Compteur()
    Compteur.combien()
    
    Jusqu'à présent, 2 objets ont été créés.
    

    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.

    In [173]:
    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.

    In [174]:
    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()
    
    Mon attribut est ok.
    
    In [175]:
    dir(un_test)
    
    Out[175]:
    ['__class__',
     '__delattr__',
     '__dict__',
     '__dir__',
     '__doc__',
     '__eq__',
     '__format__',
     '__ge__',
     '__getattribute__',
     '__gt__',
     '__hash__',
     '__init__',
     '__init_subclass__',
     '__le__',
     '__lt__',
     '__module__',
     '__ne__',
     '__new__',
     '__reduce__',
     '__reduce_ex__',
     '__repr__',
     '__setattr__',
     '__sizeof__',
     '__str__',
     '__subclasshook__',
     '__weakref__',
     'afficher_attribut',
     'mon_attribut']

    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.

    In [176]:
    un_test = Test()
    un_test.__dict__
    
    Out[176]:
    {'mon_attribut': 'ok'}

    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.

    In [177]:
    un_test.__dict__["mon_attribut"] = "plus ok"
    un_test.afficher_attribut()
    
    Mon attribut est plus ok.
    

    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.

    In [178]:
    ani1 = {}
    ani1["nom"] = "girafe"
    ani1["taille"] = 5.0
    ani1["poids"] = 1100
    ani1
    
    Out[178]:
    {'nom': 'girafe', 'taille': 5.0, 'poids': 1100}

    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.

    In [179]:
    ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
    ani2
    
    Out[179]:
    {'nom': 'singe', 'poids': 70, 'taille': 1.75}

    Mais rien ne nous empêche d’ajouter une clé et une valeur supplémentaire.

    In [180]:
    ani2["age"]=15
    ani2
    
    Out[180]:
    {'nom': 'singe', 'poids': 70, 'taille': 1.75, 'age': 15}

    Pour récupérer la valeur associée à une clé donnée, il suffit d’utiliser la syntaxe suivante dictionnaire["cle"].

    In [181]:
    ani2["nom"]
    
    Out[181]:
    'singe'

    Il est possible d’obtenir toutes les valeurs d’un dictionnaire à partir de ses clés.

    In [182]:
    ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
    
    for key in ani2: print(key, ani2[key])
    
    nom singe
    poids 70
    taille 1.75
    

    Les méthodes .keys() et .values() renvoient, comme vous pouvez vous en doutez, les clés et les valeurs d’un dictionnaire.

    In [183]:
    ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
    ani2
    
    Out[183]:
    {'nom': 'singe', 'poids': 70, 'taille': 1.75}
    In [184]:
    ani2.keys()
    
    Out[184]:
    dict_keys(['nom', 'poids', 'taille'])
    In [185]:
    ani2.values()
    
    Out[185]:
    dict_values(['singe', 70, 1.75])

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

    In [186]:
    list(ani2.values())
    
    Out[186]:
    ['singe', 70, 1.75]

    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.

    In [187]:
    dico = {0: "t", 1: "o", 2: "t", 3: "o"}
    dico.items()
    
    Out[187]:
    dict_items([(0, 't'), (1, 'o'), (2, 't'), (3, 'o')])

    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.

    In [188]:
    if "poids" in ani2: print("La clef n'existe pas dans ani2.")
        
    if "singe" in ani2: print("C'est pas une clef, justement!")
    
    La clef n'existe pas dans ani2.
    

    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.

    In [189]:
    ani1 = {'nom': 'girafe', 'taille': 5.0, 'poids': 1100}
    ani2 = {"nom": "singe", "poids": 70, "taille": 1.75}
    
    animaux = [ani1, ani2]
    animaux
    
    Out[189]:
    [{'nom': 'girafe', 'taille': 5.0, 'poids': 1100},
     {'nom': 'singe', 'poids': 70, 'taille': 1.75}]
    In [190]:
    for ani in animaux: print(ani["nom"])
    
    girafe
    singe
    

    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.

    In [191]:
    liste_animaux = [["girafe", 2], ["singe", 3]]
    dict(liste_animaux)
    
    Out[191]:
    {'girafe': 2, 'singe': 3}

    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.

    In [192]:
    tuple_animaux = (("girafe", 2), ("singe", 3))
    dict(tuple_animaux)
    
    Out[192]:
    {'girafe': 2, 'singe': 3}

    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 .

    In [193]:
    x = (1, 2, 3) ; x
    
    Out[193]:
    (1, 2, 3)
    In [194]:
    x[1]
    
    Out[194]:
    2
    In [195]:
    x[0:2]
    
    Out[195]:
    (1, 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.

    In [196]:
    x=x + (4,)
    x
    
    Out[196]:
    (1, 2, 3, 4)

    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.

    In [197]:
    for i, elt in enumerate ([75 , -75, 0]):
        print(i, elt)
    
    0 75
    1 -75
    2 0
    
    In [198]:
    for obj in enumerate ([75 , -75, 0]):
        print(obj, type(obj))
    
    (0, 75) <class 'tuple'>
    (1, -75) <class 'tuple'>
    (2, 0) <class 'tuple'>
    

    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.

    In [199]:
    dico = {"pinson ": 2, "merle ": 3}
    
    for key , val in dico . items ():
        print (key, val)
    
    pinson  2
    merle  3
    

    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.

    In [200]:
    liste = [(i, i+1, i +2) for i in range (5, 8)]
    liste
    
    Out[200]:
    [(5, 6, 7), (6, 7, 8), (7, 8, 9)]
    In [201]:
    for x, y, z in liste :
        print (x, y, z)
    
    5 6 7
    6 7 8
    7 8 9
    

    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.

    In [202]:
    def fct():
        return 3, 14
    
    x, y = fct()
    print(x, y)
    
    3 14
    

    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.

    In [203]:
    resultat = fct ()
    x = resultat [0]
    y = resultat [1]
    print (x, y)
    
    3 14
    

    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.

    In [204]:
    def fct():
        return 1, 2, 3, 4
    
    x, _, y, _ = fct ()
    print(x, y)
    
    1 3
    

    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 { }.

    In [205]:
    s = {1, 2, 3, 4}
    s
    
    Out[205]:
    {1, 2, 3, 4}

    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.

    In [206]:
    set ( range (5))
    
    Out[206]:
    {0, 1, 2, 3, 4}

    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.

    In [207]:
    l = [random.randint(0, 9) for i in range (10)]
    l
    
    Out[207]:
    [4, 4, 6, 9, 4, 2, 6, 6, 9, 8]

    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.

    In [208]:
    list(set([7 , 9, 6, 6, 7, 3, 8, 5, 6, 7]))
    
    Out[208]:
    [3, 5, 6, 7, 8, 9]

    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 !

    In [209]:
    seq = "atctcgatcgatcgcgctagctagctcgccatacgtacgactacgt"
    set(seq)
    
    Out[209]:
    {'a', 'c', 'g', 't'}
    In [210]:
    [( base , seq . count ( base )) for base in set ( seq )]
    
    Out[210]:
    [('a', 10), ('c', 15), ('t', 11), ('g', 10)]

    Les sets permettent aussi l’évaluation d’union ou d’intersection mathématiques en conjonction avec les opérateurs respectivement | et &.

    In [211]:
    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)
    
    Out[211]:
    {1, 3, 5}
    In [212]:
    set(L) | set(L2)
    
    Out[212]:
    {0, 1, 2, 3, 4, 5}

    Il est également possible de générer des dictionnaires de compréhension.

    In [213]:
    dico = {"a ": 10, "g ": 10, "t ": 11, "c ": 15}
    dico.items()
    
    Out[213]:
    dict_items([('a ', 10), ('g ', 10), ('t ', 11), ('c ', 15)])
    In [214]:
    {key : val *2 for key , val in dico . items ()}
    
    Out[214]:
    {'a ': 20, 'g ': 20, 't ': 22, 'c ': 30}
    In [215]:
    {key : val for key , val in enumerate (" toto ")}
    
    Out[215]:
    {0: ' ', 1: 't', 2: 'o', 3: 't', 4: 'o', 5: ' '}
    In [216]:
    seq = " atctcgatcgatcgcgctagctagctcgccatacgtacgactacgt "
    
    {base : seq . count ( base ) for base in set ( seq )}
    
    Out[216]:
    {'c': 15, 'g': 10, 'a': 10, ' ': 2, 't': 11}

    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.

    In [217]:
    {i for i in range (11)}
    
    Out[217]:
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    In [218]:
    {i**2 for i in range (11)}
    
    Out[218]:
    {0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100}

    Dans ce chapitre, vous devrez importer les modules numpy et pandas, qu'on nommera usuellement ici respectivement np et pd.

    In [219]:
    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.

    In [220]:
    s = pd.Series([101, 202,  303])
    s
    
    Out[220]:
    0    101
    1    202
    2    303
    dtype: int64

    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.

    In [221]:
    s = pd.Series([101, 202, 303], dtype = float)
    s
    
    Out[221]:
    0    101.0
    1    202.0
    2    303.0
    dtype: float64

    On peut mettre la valeur numpy.nan pour les valeurs non déterminées (qui apparaissent alors comme NaN).

    In [222]:
    s = pd.Series([101, np.NaN, 202, 304])
    s
    
    Out[222]:
    0    101.0
    1      NaN
    2    202.0
    3    304.0
    dtype: float64

    On peut également créer une série à partir d'une liste.

    In [223]:
    L = [4, 4, 5, 5]
    s = pd.Series(L)
    s
    
    Out[223]:
    0    4
    1    4
    2    5
    3    5
    dtype: int64

    Comme pour les listes, on peut aussi créer une série avec la fonction range().

    In [224]:
    s = pd.Series(range(95, 101, 2))
    s
    
    Out[224]:
    0    95
    1    97
    2    99
    dtype: int64

    On peut également créer une série à valeurs répétées avec le paramètre index.

    In [225]:
    s = pd.Series(4, index=range(3))
    s
    
    Out[225]:
    0    4
    1    4
    2    4
    dtype: int64

    On peut déterminer la taille d'une série avec la fonction len() ou bien avec .size().

    In [226]:
    s = pd.Series(range(4))
    len(s)
    
    Out[226]:
    4

    Les paramètres statistiques d'une série sont visibles avec la fonction .describe().

    In [227]:
    s = pd.Series([4, 5, 5, 1, 3, 2, 2, 2, 2, 4])
    s.describe()
    
    Out[227]:
    count    10.000000
    mean      3.000000
    std       1.414214
    min       1.000000
    25%       2.000000
    50%       2.500000
    75%       4.000000
    max       5.000000
    dtype: float64

    Une série se comporte de façon très similaire à une array numpy et aussi un dictionnaire.

    In [228]:
    s =  pd.Series(["zéro", "un", "deux", "trois", "quatre", "cinq", "six"])
    s
    
    Out[228]:
    0      zéro
    1        un
    2      deux
    3     trois
    4    quatre
    5      cinq
    6       six
    dtype: object

    On récupère une valeur dans une série de la même manière que dans les listes.

    In [229]:
    s[2]
    
    Out[229]:
    'deux'

    Il est courant d'utiliser .iloc[] aussi pour récupérer la valeur correspondante à l'indice.

    In [230]:
    s.iloc[2]
    
    Out[230]:
    'deux'

    On peut aussi en renvoyer plusieurs en utilisant doublement les crochets [[ ]].

    In [231]:
    s[[1, 3, 5, 0]]
    
    Out[231]:
    1       un
    3    trois
    5     cinq
    0     zéro
    dtype: object

    On renvoie avec s[a:b] la série avec les valeurs des indices a inclus à b exclus.

    In [232]:
    s[2:5]
    
    Out[232]:
    2      deux
    3     trois
    4    quatre
    dtype: object

    On renvoie avec s[:b] la série avec les valeurs des indices 0 inclus à b exclus.

    In [233]:
    s[:3]
    
    Out[233]:
    0    zéro
    1      un
    2    deux
    dtype: object

    On renvoie avec s[a:] la série avec les valeurs à partir de l'indice a inclus.

    In [234]:
    s[3:]
    
    Out[234]:
    3     trois
    4    quatre
    5      cinq
    6       six
    dtype: object

    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.

    In [235]:
    a = np.array([1, 2, 3.5])
    a
    
    Out[235]:
    array([1. , 2. , 3.5])

    Un array se tiendra sur plusieurs lignes comme un enchaînement de listes.

    In [236]:
    a = np.array([[1, 2, 3], [4, 5, 6]])
    a
    
    Out[236]:
    array([[1, 2, 3],
           [4, 5, 6]])

    Un array contient donc un certain nombre de lignes et de colonnes. Basons-nous sur l'array ci-dessous.

    In [237]:
    a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
    a
    
    Out[237]:
    array([[ 1,  2,  3],
           [ 4,  5,  6],
           [ 7,  8,  9],
           [10, 11, 12]])

    On obtient la taille totale de l'array avec la fonction .size.

    In [238]:
    a.size
    
    Out[238]:
    12

    On obtient les dimensions de l'array avec la fonction .shape.

    In [239]:
    a.shape
    
    Out[239]:
    (4, 3)

    Basons-nous sur l'array ci-dessous (c'est le même en fait...).

    In [240]:
    a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]])
    a
    
    Out[240]:
    array([[ 1,  2,  3],
           [ 4,  5,  6],
           [ 7,  8,  9],
           [10, 11, 12]])

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

    In [241]:
    a[2, 1]
    
    Out[241]:
    8

    On peut utiliser une boucle pour modifier les séries.

    In [242]:
    s = pd.Series(range(5))
    
    for i in s.index:
        s[i]*=10
        
    s
    
    Out[242]:
    0     0
    1    10
    2    20
    3    30
    4    40
    dtype: int64

    On utilise une double boucle pour les arrays.

    In [243]:
    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
    
    Out[243]:
    array([[30, 90, 10, 20, 80, 60],
           [40, 80, 70, 30, 50, 90],
           [60, 50, 20, 70, 10, 40],
           [80, 70, 50, 40, 30, 10]])

    Repérer les NaN se fait avec la fonction isnull().

    In [244]:
    s = pd.Series([101, np.NaN, 202, 304])
    s.isnull()
    
    Out[244]:
    0    False
    1     True
    2    False
    3    False
    dtype: bool

    Dans ce chapitre, vous devrez au importer les modules numpy et pandas, qu'on nommera usuellement ici respectivement np et pd.

    In [245]:
    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.

    In [246]:
    df = pd.DataFrame( { "alpha": [4, 3, 2, 1],
                         "beta": [11, 21, 31, 41],
                         "gamma": [100, 200, 300, 400] },
                      index=["a", "b", "c", "d"])
    df
    
    Out[246]:
    alpha beta gamma
    a 4 11 100
    b 3 21 200
    c 2 31 300
    d 1 41 400

    On peut également, après coup, renommer les indices et les colonnes d'un DataFrame.

    In [247]:
    df.index = range(4)
    df.columns = ["De Gaulle", "Pompidou", "Giscard"]
    df
    
    Out[247]:
    De Gaulle Pompidou Giscard
    0 4 11 100
    1 3 21 200
    2 2 31 300
    3 1 41 400

    Vous pouvez aussi ne renommer qu'une colonne spécifique avec la méthode .rename().

    In [248]:
    df.rename(columns={"Giscard" : "Chirac"})
    
    Out[248]:
    De Gaulle Pompidou Chirac
    0 4 11 100
    1 3 21 200
    2 2 31 300
    3 1 41 400

    Vous pouvez sinon créer un DataFrame à partir d'un array déjà existant.

    In [249]:
    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
    
    Out[249]:
    un deux trois quatre cinq six
    0 3 9 1 2 8 6
    1 4 8 7 3 5 9
    2 6 5 2 7 1 4
    3 8 7 5 4 3 1

    Vous pouvez aussi créer un DataFrame à partir de listes existantes, pourvu que celles-ci aient la même longueur.

    In [250]:
    L1 = ["Salut,", "J'aime", "Allez"]
    L2 = ["ça", "les", "les"]
    L3 = ["va?", "pâtes.", "Bleus!"]
    
    df = pd.DataFrame({"^_^" : L1,
                       "è_é" : L2,
                       "O_o" : L3})
    df
    
    Out[250]:
    ^_^ è_é O_o
    0 Salut, ça va?
    1 J'aime les pâtes.
    2 Allez les Bleus!

    On peut aussi utiliser une liste de dictionnaires (manuellement, c'est pas très pratique mais on peut!).

    In [251]:
    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
    
    Out[251]:
    A B C D
    0 1.1 2 3.3 4
    1 2.7 10 5.4 7
    2 5.3 9 1.5 15

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

    In [252]:
    oceanie = pd.read_csv("data/tuto/oceanie.csv")
    oceanie
    
    Out[252]:
    pays capitale
    0 Australie Canberra
    1 Fidji Suva
    2 Kirabati Bairiki
    3 Kiribati Tarawa
    4 Micronésie Palikir
    5 Nauru Yaren
    6 Niue Alofi
    7 Nouvelle-Calédonie Nouméa
    8 Nouvelle-Zélande Wellington
    9 Palaos Ngerulmud
    10 Papousie Nouvelle-Guinée Port Moresby
    11 Polynésie française Papeete
    12 Samoa Apia
    13 Tonga Nuku'alofa
    14 Tuvalu Funafuti
    15 Vanuatu Port Vila
    16 Îles Cook Avarua
    17 Îles Marshall Dalap-Uliga-Darrit
    18 Îles Salomon Honiara

    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 :

    • Si pas de header (noms de colonnes) dans le fichier initial : df = pandas.read_csv('myFile.csv', sep = '\t', names = ['X1', 'X2', 'X3']).
    • Si il y a déjà un header dans le fichier : df = pandas.read_csv('myFile.csv', sep = '\t', names = ['X1', 'X2', 'X3'], header = 0).

    pd.read_csv('myFile.csv', sep = '\t', na_values = ['-']) : on donne avec na_values la liste des valeurs qui doivent être assimilées à NaN :

    • Par défaut, '', '#N/A', 'N/A', 'NA', 'NULL', 'null', 'NaN', 'n/a', 'nan' sont tous interprêtés comme nan.
    • Mettre na_filter = False en laissant le défaut pour na_values pour éviter que les cellules vides se transforment en 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 :

    • pandas.read_csv('myFile.csv', sep = ' ', index_col = 0, skiprows = lambda x : x > 0 and x sup= 5, nrows = 10).
    • skiprows doit renvoyer True ssi on doit sauter la ligne.

    Écriture d'un dataframe dans un fichier :

    • df.to_csv('myFile.csv', sep = '\t') : écrit le dataframe avec une tabulation comme séparateur (le défaut est une virgule).
    • header = False : supprime le header des colonnes (défaut est de l'inclure).
    • index = False : supprime le nom des lignes (défaut est de l'inclure).
    • na_rep = '-' : imprime les valeurs NaN comme '-' (le défaut est la chaîne vide).
    • Pour écrire sur stdout : df.to_csv(sys.stdout, sep = '\t', index = False) ou alors print(df.to_csv(sep = '\t', index = False)).
    • Pour mettre un titre à la colonne d'index : df.to_csv(..., index = True, index_label = 'firstColumn').
    • df.to_csv('myfile.csv', sep = ' ', mode = 'a') : pour faire un append.

    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:

    • Leur nom.
    • Leur âge.
    • Leur salaire.
    • Leur ville de résidence.
    • Leur numéro de département.

    Évitez généralement de mettre des accents dans les noms des colonnes, nous y reviendrons.

    In [253]:
    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
    
    Out[253]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

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

    In [254]:
    df.set_index("nom")
    
    Out[254]:
    age salaire ville departement
    nom
    Aurore 37 4500 Paris 75
    Bertrand 29 3200 Paris 75
    Christine 24 1500 Bordeaux 33
    Denis 40 2600 Strasbourg 67
    Eudes 18 900 Paris 75
    François 52 3500 Strasbourg 67
    Géraldine 40 2900 Strasbourg 67
    Henry 28 2900 Bordeaux 33

    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.

    In [255]:
    df
    
    Out[255]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [256]:
    df.set_index("nom", inplace=True)
    df
    
    Out[256]:
    age salaire ville departement
    nom
    Aurore 37 4500 Paris 75
    Bertrand 29 3200 Paris 75
    Christine 24 1500 Bordeaux 33
    Denis 40 2600 Strasbourg 67
    Eudes 18 900 Paris 75
    François 52 3500 Strasbourg 67
    Géraldine 40 2900 Strasbourg 67
    Henry 28 2900 Bordeaux 33

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

    In [257]:
    df.reset_index(inplace=True)
    df
    
    Out[257]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [258]:
    df.index = random.choices(range(200, 1000), k=8)
    df
    
    Out[258]:
    nom age salaire ville departement
    695 Aurore 37 4500 Paris 75
    314 Bertrand 29 3200 Paris 75
    412 Christine 24 1500 Bordeaux 33
    922 Denis 40 2600 Strasbourg 67
    477 Eudes 18 900 Paris 75
    523 François 52 3500 Strasbourg 67
    306 Géraldine 40 2900 Strasbourg 67
    794 Henry 28 2900 Bordeaux 33

    Si vous exéctuez .reset_index() avec inplace=True, votre DataFrame conservera en colonne l'index précédent.

    In [259]:
    df.reset_index(inplace=True)
    df
    
    Out[259]:
    index nom age salaire ville departement
    0 695 Aurore 37 4500 Paris 75
    1 314 Bertrand 29 3200 Paris 75
    2 412 Christine 24 1500 Bordeaux 33
    3 922 Denis 40 2600 Strasbourg 67
    4 477 Eudes 18 900 Paris 75
    5 523 François 52 3500 Strasbourg 67
    6 306 Géraldine 40 2900 Strasbourg 67
    7 794 Henry 28 2900 Bordeaux 33

    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.

    In [260]:
    df.set_index("index", inplace=True) # On réindexe le DataFrame par la colonne "index".
    df
    
    Out[260]:
    nom age salaire ville departement
    index
    695 Aurore 37 4500 Paris 75
    314 Bertrand 29 3200 Paris 75
    412 Christine 24 1500 Bordeaux 33
    922 Denis 40 2600 Strasbourg 67
    477 Eudes 18 900 Paris 75
    523 François 52 3500 Strasbourg 67
    306 Géraldine 40 2900 Strasbourg 67
    794 Henry 28 2900 Bordeaux 33
    In [261]:
    df.reset_index(drop=True, inplace=True) # Et là on vous montre la réinitialisation de l'index sans conservation.
    df
    
    Out[261]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    Enfin, on peut aussi réorganiser les colonnes comme bon nous semble avec la fonction .reindex(columns=[]).

    In [262]:
    df.reindex(columns=["salaire", "age", "nom", "departement", "ville"])
    
    Out[262]:
    salaire age nom departement ville
    0 4500 37 Aurore 75 Paris
    1 3200 29 Bertrand 75 Paris
    2 1500 24 Christine 33 Bordeaux
    3 2600 40 Denis 67 Strasbourg
    4 900 18 Eudes 75 Paris
    5 3500 52 François 67 Strasbourg
    6 2900 40 Géraldine 67 Strasbourg
    7 2900 28 Henry 33 Bordeaux

    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.

    In [263]:
    df
    
    Out[263]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [264]:
    df.iloc[6]
    
    Out[264]:
    nom             Géraldine
    age                    40
    salaire              2900
    ville          Strasbourg
    departement            67
    Name: 6, dtype: object

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

    In [265]:
    df.loc[df["nom"]=="Géraldine"]
    
    Out[265]:
    nom age salaire ville departement
    6 Géraldine 40 2900 Strasbourg 67

    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.

    In [266]:
    df.loc[df["nom"]=="Géraldine", "departement"]
    
    Out[266]:
    6    67
    Name: departement, dtype: int64

    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.

    In [267]:
    df.loc[df["ville"]=="Paris"]
    
    Out[267]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    4 Eudes 18 900 Paris 75

    On peut aussi en tirer les valeurs d'une colonne spécifique à cette restriction, comme les salaires des parisiens, ici.

    In [268]:
    df.loc[df["ville"]=="Paris", "salaire"]
    
    Out[268]:
    0    4500
    1    3200
    4     900
    Name: salaire, dtype: int64

    Rappelons le DataFrame df, une fois n'est pas coutume.

    In [269]:
    df
    
    Out[269]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [270]:
    df[["nom", "salaire"]]
    
    Out[270]:
    nom salaire
    0 Aurore 4500
    1 Bertrand 3200
    2 Christine 1500
    3 Denis 2600
    4 Eudes 900
    5 François 3500
    6 Géraldine 2900
    7 Henry 2900

    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.

    In [271]:
    df.loc[:] # Renvoie tout le DataFrame.
    
    Out[271]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    df[ a ] → renvoie la ligne a.

    In [272]:
    df.loc[4] # Renvoie les données de la ligne 4.
    
    Out[272]:
    nom            Eudes
    age               18
    salaire          900
    ville          Paris
    departement       75
    Name: 4, dtype: object

    df.loc[ : a ] → renvoie les lignes 0 à a incluses.

    In [273]:
    df.loc[:4] # Renvoie les lignes 0 à 4.
    
    Out[273]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75

    df.loc[ a : ] → renvoie les lignes a à la dernière incluses.

    In [274]:
    df.loc[4:] # Renvoie le tableau à partir de la ligne 4.
    
    Out[274]:
    nom age salaire ville departement
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    df.loc[ a : b ] → renvoie les lignes a à b incluses.

    In [275]:
    df.loc[3:6] # Renvoie le tableau de la 4ème à la 7ème ligne.
    
    Out[275]:
    nom age salaire ville departement
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67

    df.iloc[ : , c : d ] → renvoie l'ensemble des colonnes c incluse à d non incluse.

    In [276]:
    df.iloc[:, 2:4] # Renvoie l'ensemble des colonnes 2 incluste à 4 non incluse.
    
    Out[276]:
    salaire ville
    0 4500 Paris
    1 3200 Paris
    2 1500 Bordeaux
    3 2600 Strasbourg
    4 900 Paris
    5 3500 Strasbourg
    6 2900 Strasbourg
    7 2900 Bordeaux

    df.iloc[ : , c ] → renvoie l'ensemble de la colonne c.

    In [277]:
    df.iloc[:, 3] # Renvoie l'ensemble de la colonne 3 (ville).
    
    Out[277]:
    0         Paris
    1         Paris
    2      Bordeaux
    3    Strasbourg
    4         Paris
    5    Strasbourg
    6    Strasbourg
    7      Bordeaux
    Name: ville, dtype: object

    df.loc[ a , : ] → renvoie les données de la ligne a.

    In [278]:
    df.loc[3, :] # Renvoie l'ensemble de la ligne 3.
    
    Out[278]:
    nom                 Denis
    age                    40
    salaire              2600
    ville          Strasbourg
    departement            67
    Name: 3, dtype: object

    df.iloc[ a : b , c : d ] → renvoie l'ensemble les lignes a incluse à b non incluse et les colonnes c incluse à d non incluse.

    In [279]:
    df.iloc[3:6, 2:4] # Renvoie les lignes 3 à 5 et les colonnes 2 à 3.
    
    Out[279]:
    salaire ville
    3 2600 Strasbourg
    4 900 Paris
    5 3500 Strasbourg

    Les opérateurs & pour ET et | pour OU entrent en compte si l'on veut restreindre le DataFrame selon plusieurs conditions.

    In [280]:
    df
    
    Out[280]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [281]:
    df.loc[(df["age"]<30) & (df["ville"]=="Paris")]
    
    Out[281]:
    nom age salaire ville departement
    1 Bertrand 29 3200 Paris 75
    4 Eudes 18 900 Paris 75

    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.

    In [282]:
    df.loc[(df["age"]<30) | (df["ville"]=="Paris")]
    
    Out[282]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    4 Eudes 18 900 Paris 75
    7 Henry 28 2900 Bordeaux 33

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

    In [283]:
    df.head(2)
    
    Out[283]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    In [284]:
    df.tail(2)
    
    Out[284]:
    nom age salaire ville departement
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    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.

    In [285]:
    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
    
    Out[285]:
    nom age salaire ville departement
    0 Aurore 30.0 4500.0 Paris 75
    1 Bertrand NaN 3200.0 Paris 75
    2 Christine NaN NaN Bordeaux 33
    3 Denis 40.0 2600.0 Strasbourg 67

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

    In [286]:
    df.fillna(0)
    
    Out[286]:
    nom age salaire ville departement
    0 Aurore 30.0 4500.0 Paris 75
    1 Bertrand 0.0 3200.0 Paris 75
    2 Christine 0.0 0.0 Bordeaux 33
    3 Denis 40.0 2600.0 Strasbourg 67

    Si vous voulez vous débarasser des lignes contenant une valeur nulle, vous pouvez utiliser la fonction .dropna().

    In [287]:
    df.dropna()
    
    Out[287]:
    nom age salaire ville departement
    0 Aurore 30.0 4500.0 Paris 75
    3 Denis 40.0 2600.0 Strasbourg 67

    Si vous voulez vous débarasser des colonnes contenant une valeur nulle, vous pouvez utiliser la fonction .dropna(axis=1).

    In [288]:
    df.dropna(axis=1)
    
    Out[288]:
    nom ville departement
    0 Aurore Paris 75
    1 Bertrand Paris 75
    2 Christine Bordeaux 33
    3 Denis Strasbourg 67

    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.

    In [289]:
    df.loc[df["age"].isnull()]
    
    Out[289]:
    nom age salaire ville departement
    1 Bertrand NaN 3200.0 Paris 75
    2 Christine NaN NaN Bordeaux 33

    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.

    In [290]:
    df = pd.DataFrame({c: random.choices(range(1, 21), k=4) for c in range(4)})
    df.columns = ["alpha", "beta", "gamma", "delta"]
    df
    
    Out[290]:
    alpha beta gamma delta
    0 12 10 2 19
    1 8 15 18 1
    2 18 3 17 1
    3 7 17 8 14

    On peut aussi déterminer les extremums sur les colonnes numériques :

    • Minimum : avec la fonction .min().
    • Maximum : avec la fonction .max().
    In [291]:
    print(list(df["alpha"]))
    
    print("Minimum : ", df["alpha"].min())
    print("Maximum : ", df["alpha"].max())
    
    [12, 8, 18, 7]
    Minimum :  7
    Maximum :  18
    

    On peut aussi déterminer les mesures de tendance centrale avec :

    • Moyenne : avec la fonction .mean().
    • Écart-type : avec la fonction .std().
    • Variance : avec la fonction .var().
    In [292]:
    print(list(df["alpha"]))
    
    print("Moyenne : ", df["alpha"].mean())
    print("Écart-type : ", df["alpha"].std())
    print("Variance : ", df["alpha"].var())
    
    [12, 8, 18, 7]
    Moyenne :  11.25
    Écart-type :  4.9916597106239795
    Variance :  24.916666666666668
    

    On peut obtenir les pourcentages sur les lignes par une simple opération.

    In [293]:
    100*(df.T / df.T.sum()).T
    
    Out[293]:
    alpha beta gamma delta
    0 27.906977 23.255814 4.651163 44.186047
    1 19.047619 35.714286 42.857143 2.380952
    2 46.153846 7.692308 43.589744 2.564103
    3 15.217391 36.956522 17.391304 30.434783

    Il est encore plus simple d'obtenir les pourcentages sur les colonnes.

    In [294]:
    100*df/df.sum()
    
    Out[294]:
    alpha beta gamma delta
    0 26.666667 22.222222 4.444444 54.285714
    1 17.777778 33.333333 40.000000 2.857143
    2 40.000000 6.666667 37.777778 2.857143
    3 15.555556 37.777778 17.777778 40.000000

    On peut aussi facilement afficher les valeurs centrées-réduites.

    In [295]:
    (df - df.mean()) / df.std(ddof = 0)
    
    Out[295]:
    alpha beta gamma delta
    0 0.173494 -0.231372 -1.399469 1.289461
    1 -0.751809 0.694117 1.021234 -0.974958
    2 1.561450 -1.527058 0.869940 -0.974958
    3 -0.983135 1.064313 -0.491705 0.660456

    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.

    In [296]:
    df = pd.DataFrame({c: random.choices(range(1, 121), k=4) for c in range(4)})
    df.columns = ["D", "C", "B", "A"]
    df
    
    Out[296]:
    D C B A
    0 5 59 54 14
    1 49 35 112 60
    2 97 68 77 27
    3 72 59 23 55

    On peut trier un DataFrame :

    • Selon les colonnes avec la méthode .sort_index(axis = 1, ascending = True)</font>.
    • Selon les lignes avec la méthode .sort_index(axis = 0, ascending = False)</font>.

    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.

    In [297]:
    df.sort_index(axis=1, ascending=True, inplace=True)
    df
    
    Out[297]:
    A B C D
    0 14 54 59 5
    1 60 112 35 49
    2 27 77 68 97
    3 55 23 59 72
    In [298]:
    df.sort_index(axis=0, ascending=False)
    
    Out[298]:
    A B C D
    3 55 23 59 72
    2 27 77 68 97
    1 60 112 35 49
    0 14 54 59 5

    On peut aussi trier le DataFrame selon les valeurs d'une colonne spécifique avec .sort_values("nom_colonne").

    In [299]:
    df.sort_values("C") # DataFrame trié selon les valeurs de la colonne "C".
    
    Out[299]:
    A B C D
    1 60 112 35 49
    0 14 54 59 5
    3 55 23 59 72
    2 27 77 68 97

    On peut aussi trier le DataFrame selon plusieurs colonnes spécifiques avec .sort_values(["nom_colonne1", "nom_colonne2"]).

    In [300]:
    df.sort_values(by = ["C", "A"], ascending = [True, False]) # Tri selon "C", puis selon "A" (décroissant).
    
    Out[300]:
    A B C D
    1 60 112 35 49
    3 55 23 59 72
    0 14 54 59 5
    2 27 77 68 97

    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:

    • "jour" : Le jour où les tirs ont été effectués (Jour 1 ou Jour 2).
    • "nom" : Le nom du participant.
    • "tirs" : Le nombre de flèches ayant touché la zone de couleur de la variable "zone".
    • "zone" : La zone de couleur touchée par les tirs.
    • "points" : Le nombre de points que les tirs ont rapporté (tirs*100, 75 ou 50 selon la zone).
    In [301]:
    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
    
    Out[301]:
    jour nom tirs zone points
    0 Jour 1 Aurore 30 bleue 1500.0
    1 Jour 1 Bertrand 28 bleue 1400.0
    2 Jour 1 Clara 51 bleue 2550.0
    3 Jour 1 Aurore 40 jaune 3000.0
    4 Jour 1 Bertrand 44 jaune 3300.0
    5 Jour 1 Clara 37 jaune 2775.0
    6 Jour 1 Aurore 30 rouge 3000.0
    7 Jour 1 Bertrand 28 rouge 2800.0
    8 Jour 1 Clara 12 rouge 1200.0
    9 Jour 2 Aurore 50 bleue 2500.0
    10 Jour 2 Bertrand 35 bleue 1750.0
    11 Jour 2 Clara 40 bleue 2000.0
    12 Jour 2 Aurore 25 jaune 1875.0
    13 Jour 2 Bertrand 45 jaune 3375.0
    14 Jour 2 Clara 30 jaune 2250.0
    15 Jour 2 Aurore 25 rouge 2500.0
    16 Jour 2 Bertrand 20 rouge 2000.0
    17 Jour 2 Clara 30 rouge 3000.0

    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.

    In [302]:
    df.groupby("nom").sum()
    
    Out[302]:
    tirs points
    nom
    Aurore 200 14375.0
    Bertrand 200 14625.0
    Clara 200 13775.0

    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.

    In [303]:
    df.groupby("zone").mean()
    
    Out[303]:
    tirs points
    zone
    bleue 39.000000 1950.000000
    jaune 36.833333 2762.500000
    rouge 24.166667 2416.666667

    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.

    In [304]:
    df.groupby(["nom", "zone"]).sum()
    
    Out[304]:
    tirs points
    nom zone
    Aurore bleue 80 4000.0
    jaune 65 4875.0
    rouge 55 5500.0
    Bertrand bleue 63 3150.0
    jaune 89 6675.0
    rouge 48 4800.0
    Clara bleue 91 4550.0
    jaune 67 5025.0
    rouge 42 4200.0

    On peut effectuer un groupby() selon les fonctions suivantes :

    • Le nombre dans le groupe : size().
    • Minimum et maximum : min() et max().
    • Moyenne : mean().
    • Somme et produit : sum() et prod().
    • Variance et écart-type (normalisés par défaut pas n-1) : var(), std().
    • Médiane : median().
    • Quantile : quantile(q = 0.2).
    • Écart absolu (median absolute deviate) : mad().
    • Nombre de valeurs uniques : nunique().
    • Rang dans le groupe : rank().

    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 :

    • df.groupby("A").agg("min") ou df.groupby("A").agg(min).
    • df.groupby("A").agg("mean") ou df.groupby("A").agg(np.mean).
    • df.groupby("A").agg("mad") ou df.groupby("A").agg(pd.DataFrame.mad).

    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.

    In [305]:
    df.groupby("nom").agg(["sum", "mean", "max"])
    
    Out[305]:
    tirs points
    sum mean max sum mean max
    nom
    Aurore 200 33.333333 50 14375.0 2395.833333 3000.0
    Bertrand 200 33.333333 45 14625.0 2437.500000 3375.0
    Clara 200 33.333333 51 13775.0 2295.833333 3000.0

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

    In [306]:
    import seaborn as sns
    titanic = sns.load_dataset("titanic") # La bibliothèque seaborn fournit quelques datasets comme celui-ci.
    titanic
    
    Out[306]:
    survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
    0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no False
    1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes False
    2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes True
    3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes False
    4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no True
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
    886 0 2 male 27.0 0 0 13.0000 S Second man True NaN Southampton no True
    887 1 1 female 19.0 0 0 30.0000 S First woman False B Southampton yes True
    888 0 3 female NaN 1 2 23.4500 S Third woman False NaN Southampton no False
    889 1 1 male 26.0 0 0 30.0000 C First man True C Cherbourg yes True
    890 0 3 male 32.0 0 0 7.7500 Q Third man True NaN Queenstown no True

    891 rows × 15 columns

    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.

    In [307]:
    titanic.pivot_table("survived", index="sex", columns="class")
    
    Out[307]:
    class First Second Third
    sex
    female 0.968085 0.921053 0.500000
    male 0.368852 0.157407 0.135447

    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.

    In [308]:
    titanic.pivot_table("survived", index="sex", columns="class", aggfunc="sum")
    
    Out[308]:
    class First Second Third
    sex
    female 91 70 72
    male 45 17 47

    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.

    In [309]:
    titanic.dropna(inplace=True)
    age = pd.cut(titanic["age"], [0, 18, 80])
    titanic.pivot_table("survived", ["sex", age], "class")
    
    Out[309]:
    class First Second Third
    sex age
    female (0, 18] 0.909091 1.000000 0.500000
    (18, 80] 0.968254 0.875000 0.666667
    male (0, 18] 0.800000 1.000000 1.000000
    (18, 80] 0.397436 0.333333 0.250000

    Il peut vous arriver de vouloir modifier de façon unilatérale une colonne d'un DataFrame. Reprenons notre DataFrame créé à la partie A.

    In [310]:
    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
    
    Out[310]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

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

    In [311]:
    def AnneeNaissance(age):
        annee = 2021 - age
        return(int(annee))
    
    df["age"] = df["age"].apply(AnneeNaissance)
    df
    
    Out[311]:
    nom age salaire ville departement
    0 Aurore 1984 4500 Paris 75
    1 Bertrand 1992 3200 Paris 75
    2 Christine 1997 1500 Bordeaux 33
    3 Denis 1981 2600 Strasbourg 67
    4 Eudes 2003 900 Paris 75
    5 François 1969 3500 Strasbourg 67
    6 Géraldine 1981 2900 Strasbourg 67
    7 Henry 1993 2900 Bordeaux 33

    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.

    In [312]:
    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
    
    Out[312]:
    nom age salaire
    0 Aurore 37 4500
    1 Bertrand 29 3200
    2 Christine 24 1500
    3 Denis 40 2600
    In [313]:
    df2
    
    Out[313]:
    nom age salaire
    4 Eudes 18 900
    5 François 52 3500
    6 Géraldine 40 2900
    7 Henry 28 2900

    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.

    In [314]:
    df3 =  pd.concat([df1, df2])
    df3
    
    Out[314]:
    nom age salaire
    0 Aurore 37 4500
    1 Bertrand 29 3200
    2 Christine 24 1500
    3 Denis 40 2600
    4 Eudes 18 900
    5 François 52 3500
    6 Géraldine 40 2900
    7 Henry 28 2900

    Vous pouvez aussi joindre deux DataFrames l'un à côté de l'autre en précisant l'argument axis=1.

    In [315]:
    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
    
    Out[315]:
    nom age salaire ville departement
    0 Aurore 37 4500 Paris 75
    1 Bertrand 29 3200 Paris 75
    2 Christine 24 1500 Bordeaux 33
    3 Denis 40 2600 Strasbourg 67
    4 Eudes 18 900 Paris 75
    5 François 52 3500 Strasbourg 67
    6 Géraldine 40 2900 Strasbourg 67
    7 Henry 28 2900 Bordeaux 33

    Pour les jointures, basons-nous sur les 2 DataFrames suivants.

    In [316]:
    df1 = pd.DataFrame({"A": [3, 5], "B": [1, 2]})
    df2 = pd.DataFrame({"A": [5, 3, 7], "C": [9, 2, 0]})
    
    df1
    
    Out[316]:
    A B
    0 3 1
    1 5 2
    In [317]:
    df2
    
    Out[317]:
    A C
    0 5 9
    1 3 2
    2 7 0

    La jointure simple pd.merge(df1, df2) utilise les noms des colonnes qui sont communs.

    In [318]:
    pd.merge(df1, df2)
    
    Out[318]:
    A B C
    0 3 1 2
    1 5 2 9

    On peut aussi utiliser df1.merge(df2).

    In [319]:
    df1.merge(df2)
    
    Out[319]:
    A B C
    0 3 1 2
    1 5 2 9

    Prenons maintenant ces 2 DataFrames qui n'ont pas de colonnes communes.

    In [320]:
    df1 = pd.DataFrame({"A": [3, 5], "B": [1, 2]})
    df2 = pd.DataFrame({"A": [5, 3, 7], "B": [9, 2, 0]})
    
    df1
    
    Out[320]:
    A B
    0 3 1
    1 5 2
    In [321]:
    df2
    
    Out[321]:
    A B
    0 5 9
    1 3 2
    2 7 0

    On peut indiquer explicitement les colonnes sur lequelles on veut faire la jointure si c'est une partie des colonnes de même nom.

    In [322]:
    pd.merge(df1, df2, on = ["A"])
    
    Out[322]:
    A B_x B_y
    0 3 1 2
    1 5 2 9
    In [323]:
    pd.merge(df1, df2, on = ["A"])
    
    Out[323]:
    A B_x B_y
    0 3 1 2
    1 5 2 9

    Les lignes qui n'ont pas la clef commune sont quand mêmes présentes avec l'argument how = "outer".

    In [324]:
    pd.merge(df1, df2, how="outer")
    
    Out[324]:
    A B
    0 3 1
    1 5 2
    2 5 9
    3 3 2
    4 7 0

    On peut aussi faire une jointure externe gauche ou droite comme en sql avec how = "left" ou how = "right".

    In [325]:
    pd.merge(df1, df2, how="right")
    
    Out[325]:
    A B
    0 5 9
    1 3 2
    2 7 0

    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.

    In [326]:
    import datetime as dt
    
    dt.date.today() # Construit la date du jour.
    
    Out[326]:
    datetime.date(2021, 5, 8)

    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:

    • id_prod : l'identifiant du produit vendu.
    • price : le prix de l'article.
    • categ : la catégorie à laquelle appartient le produit (0, 1 ou 2).
    • date_et_heure : la date et l'heure à laquelle le produit a été acheté.
    In [327]:
    ventes = pd.read_csv("data/tuto/ventes.csv")
    ventes
    
    Out[327]:
    id_prod price categ date_et_heure
    0 0_1090 13.78 0.0 2021-12-19 02:44:12
    1 1_364 10.30 1.0 2021-11-15 20:46:25
    2 1_713 33.99 1.0 2021-11-15 20:40:00
    3 0_1378 13.96 0.0 2021-08-23 16:56:15
    4 0_1470 19.53 0.0 2021-06-11 21:02:39
    ... ... ... ... ...
    336708 1_351 28.99 1.0 2021-11-21 17:16:06
    336709 1_727 16.99 1.0 2021-11-21 17:40:50
    336710 0_1342 19.49 0.0 2022-02-22 22:55:44
    336711 1_395 28.99 1.0 2021-08-23 16:44:27
    336712 0_2104 7.98 0.0 2021-10-02 20:31:58

    336713 rows × 4 columns

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

    In [328]:
    dh = "2021-04-20 16:55:44"
    
    pd.to_datetime(dh)
    
    Out[328]:
    Timestamp('2021-04-20 16:55:44')

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

    In [329]:
    ventes[["date_et_heure"]].info()
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 336713 entries, 0 to 336712
    Data columns (total 1 columns):
    date_et_heure    336713 non-null object
    dtypes: object(1)
    memory usage: 2.6+ MB
    

    C'est pourquoi il est toujours recommandé de convertir au préalable une variable date/heure en datetime.

    In [330]:
    ventes["date_et_heure"] = pd.to_datetime(ventes["date_et_heure"])
    ventes[["date_et_heure"]].info()
    
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 336713 entries, 0 to 336712
    Data columns (total 1 columns):
    date_et_heure    336713 non-null datetime64[ns]
    dtypes: datetime64[ns](1)
    memory usage: 2.6 MB
    

    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.

    In [331]:
    ventes
    
    Out[331]:
    id_prod price categ date_et_heure
    0 0_1090 13.78 0.0 2021-12-19 02:44:12
    1 1_364 10.30 1.0 2021-11-15 20:46:25
    2 1_713 33.99 1.0 2021-11-15 20:40:00
    3 0_1378 13.96 0.0 2021-08-23 16:56:15
    4 0_1470 19.53 0.0 2021-06-11 21:02:39
    ... ... ... ... ...
    336708 1_351 28.99 1.0 2021-11-21 17:16:06
    336709 1_727 16.99 1.0 2021-11-21 17:40:50
    336710 0_1342 19.49 0.0 2022-02-22 22:55:44
    336711 1_395 28.99 1.0 2021-08-23 16:44:27
    336712 0_2104 7.98 0.0 2021-10-02 20:31:58

    336713 rows × 4 columns

    Avec les fonctions .date() et .time() appliquées sur un datetime, on peut respectivemenet récupérer la date et l'heure en question.

    In [332]:
    dh = pd.to_datetime("2021-04-20 16:55:44")
    
    jour = dh.date()
    heure = dh.time()
    
    print(jour)
    print(heure)
    
    2021-04-20
    16:55:44
    

    Mais on peut également récupérer:

    • L'année, le mois et le jour respectivement avec les fonctions .year, .month et .day.
    • L'heure, la minute et la seconde respectivement avec les fonctions .hour, .minute et .second.
    In [333]:
    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))
    
    Aujourd'hui, on est le 20 du 4-ième mois de l'année 2021.
    À l'heure où j'écris, il est 16h55 et 44 secondes se sont écoulées.
    

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

    In [334]:
    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
    
    Out[334]:
    id_prod price categ date heure
    0 0_1090 13.78 0.0 2021-12-19 02:44:12
    1 1_364 10.30 1.0 2021-11-15 20:46:25
    2 1_713 33.99 1.0 2021-11-15 20:40:00
    3 0_1378 13.96 0.0 2021-08-23 16:56:15
    4 0_1470 19.53 0.0 2021-06-11 21:02:39
    ... ... ... ... ... ...
    336708 1_351 28.99 1.0 2021-11-21 17:16:06
    336709 1_727 16.99 1.0 2021-11-21 17:40:50
    336710 0_1342 19.49 0.0 2022-02-22 22:55:44
    336711 1_395 28.99 1.0 2021-08-23 16:44:27
    336712 0_2104 7.98 0.0 2021-10-02 20:31:58

    336713 rows × 5 columns

    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.

    In [335]:
    ventes = ventes.sort_values(["date", "heure"]).reset_index(drop=True)
    ventes
    
    Out[335]:
    id_prod price categ date heure
    0 0_1259 11.99 0.0 2021-03-01 00:01:07
    1 0_1390 19.37 0.0 2021-03-01 00:02:26
    2 0_1352 4.50 0.0 2021-03-01 00:02:38
    3 0_1458 6.55 0.0 2021-03-01 00:04:54
    4 0_1358 16.49 0.0 2021-03-01 00:05:18
    ... ... ... ... ... ...
    336708 1_370 13.11 1.0 2022-02-28 23:56:57
    336709 1_456 28.27 1.0 2022-02-28 23:56:57
    336710 0_1538 8.61 0.0 2022-02-28 23:57:12
    336711 0_1403 15.99 0.0 2022-02-28 23:59:02
    336712 0_1775 6.99 0.0 2022-02-28 23:59:58

    336713 rows × 5 columns

    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.

    In [336]:
    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.

    In [337]:
    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).

    In [338]:
    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>""")
    
    Out[338]:

    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 :

    • Avec une approche objet : plus compliquée et plus verbeuse, mais plus évoluée.
    • Via des appels de fonctions, avec pyplot : plus simple. pyplot fournit ainsi des raccourcis.

    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 :

    • Une figure qui peut être sauvée dans un fichier : figure.
    • Un graphe (graphique) individuel appartenant à une figure qui peut en compter plusieurs : axe.
    • Un axe de coordonnées appartenant à un objet axe : axis.
    • Divers objets graphiques qui dérivent de la classe Artist et qui participent au graphe. Ce sont par exemple des rectangles, des lignes, du texte. Ces différents objets peuvent être totalement configurés (y compris individuellement) pour adapter le graphe aux besoins en appelant dessus des méthodes pour modifier leurs propriétés !

    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.

    In [339]:
    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.



    • Pour tracer une droite constante horizontale d'ordonnée a : plt.axhline(a).
    • Pour tracer une droite constante verticale d'abscisse a : plt.axvline(a).

    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.

    In [340]:
    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 :

    • color → Détermine la couleur du graphe.
    • linewidth → Pour l'épaisseur des lignes.
    • linestyleLe style de ligne.
    • labelLe nom de la courbe.
    • markerfacecolor → Détermine la couleur des bordures.
    • markersize → Détermine l'épaisseur des bordures.
    • marker → Détermine le symbole.
    In [341]:
    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 :

    • plt.title( "Titre du graphe" ) : Pour donner le titre du graphe.
    • plt.xlabel( "Titre de l'axe" ) : Pour nommer l'axe des abscisses.
    • plt.ylabel( "Titre de l'axe" ) : Pour nommer l'axe des ordonnées.

    Vous pouvez éventuellement ajouter à ces trois instructions les paramètres :

    • fontsize : Pour déterminer la taille du texte.
    • color : Pour déterminer la couleur du texte.
    • pad : Pour déterminer l'espace sous le titre, uniquement pour plt.title().
    In [342]:
    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 :

    • plt.xticks( [ ] ) : Pour les étiquettes des abscisses.
    • plt.yticks( [ ] ) : Pour les étiquettes des ordonnées.
    • fontsize : Argument que vous pouvez leur donner pour la taille des étiquettes.

    Pour déterminer les limites des axes, vous devez inscrire dans votre bloc de code :

    • plt.xlim( a , b ) : Pour les limites des abscisses de a à b.
    • plt.ylim( c , d ) : Pour les limites des ordonnées de c à d.
    • plt.xlim(0, 100) : graduation entre 0 et 100.
    • plt.xlim(xmin = 0) : pour changer seulement le minimum (pareil avec pyplot.xlim(xmax = 0))
    • plt.xscale('log') : pour utiliser une échelle logarithmique sur l'axe des x.
    In [343]:
    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:

    • La taille des légendes avec fontsize = ....
    • Sa position sur le graphe avec loc = ....
    In [344]:
    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:

    • En haut à gauche avec "upper left".
    • En haut à droite avec "upper right".
    • En bas à gauche/droite avec "lower left/right".
    • Python choisit le meilleur emplacement avec "best".

    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.

    In [345]:
    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.

    In [346]:
    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.

    In [347]:
    from IPython.display import Image
    Image(filename="data/tuto/colors.png", width=650)
    
    Out[347]:

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

    • range(5) sont les positions de début de chaque barre sur l'axe des x.
    • [1, 3, 3, 5, 4] sont les hauteurs de barres.
    In [348]:
    plt.bar(range(5), [1, 3, 3, 5, 4])
    plt.show()
    

    Mais vous pouvez également ajouter des paramètres:

    • width est la largeur relative de chaque barre (par défaut, 0.8, c'est à dire 20% d'espace vide entre chaque barre).
    • color = "red" : la couleur des barres.
    • edgecolor = "blue" : la couleur des encadrements.
    • linewidth = 2 : l'épaisseur des traits.
    • yerr = [0.3, 0.3, 0.2] : les valeurs des barres d'erreur.
    • ecolor = "yellow" : la couleur des barres d'erreur.
    • capsize = 3 : la longueur du trait des barres d'erreur.
    • log = True : pour avoir les coordonnées y en log.
    • orientation = "horizontal"/"vertical" : orientation horizontale ou verticale.
    • linestyle = "dashed" : style des lignes ("solid", "dashed", "dashot", "dotted").
    In [349]:
    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.

    In [350]:
    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().

    In [351]:
    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.

    In [352]:
    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.

    In [353]:
    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().

    In [354]:
    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:

    • normalize = True : fait un camembert complet en normalisant la somme des valeurs.
    • explode = [0, 0.2, 0, 0, 0] : array de la même taille que le tableau de données indiquant de quelle fraction de rayon la part doit être éloignée du centre.
    • colors = ['red', 'green', 'yellow'] : séquence de couleurs à utiliser pour les parts (recyclé si pas assez de couleurs).
    • labeldistance = 1.3 : la distance des labels au centre (> 1 pour être à l'extérieur du cercle).
    • autopct = lambda x: str(round(x, 2)) + '%' : une fonction qui prend le pourcentage de la part et renvoie ce qui doit être affiché pour ce pourcentage.
    • pctdistance = 0.7 : le distance au centre à laquelle le pourcentage précédent doit être affiché (1 = sur le cercle).
    • shadow = True : indique qu'il faut afficher une ombre.

    Vous pouvez aussi au préalable régler la taille des indices d'affichage avec mpl.rcParams["font.size"].

    In [355]:
    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().

    In [356]:
    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:

    • c = 'red' ou color = 'red' : la couleur des points (défaut est 'black').
    • edgecolor = 'none' : évite que le symbole soit entouré d'un trait noir.
    • s = 10 : taille du symbole au carré (le multiplier par 4 si on veut agrandir la taille d'un facteur 2). Le défaut est 20.

    Le paramètre marker peut prendre les arguments suivants.

    SymboleAffichage.........SymboleAffichage
    "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().

    In [357]:
    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:

    • plt.hist(x, bins = range(11)) : on peut donner aussi les limites des intervalles explicitement plutôt qu'un nombre d'intervalles.
    • plt.hist(x, bins = [0, 5, 6, 7, 8, 9, 10]) : on peut très bien donner des bins de taille inégale.
    • plt.hist(x, range = (0, 10), bins = 10) : donne le minimum et le maximum des valeurs représentées et le nombre de bins. Par défaut, range est (min(x), max(x)).
    • plt.hist(x, density = True) : trace les fréquences plutôt que les nombres en ordonnée (somme vaut 1).
    • plt.hist(x, weights = range(len(x))) : attribut un poids à chaque valeur.
    • align = 'mid' : les barres sont centrées entre les 2 extrémités du bin (le défaut).
    • align = 'left' : indique que les barres sont centrées sur l'extrémité gauche du bin ('right' pour la droite).
    • orientation = 'horizontal' : histogramme horizontal.
    • rwidth = 0.5 : les barres sont réduites de largeur de 50% (avec un espace entre elles).
    • log = True : axe des y en log.
    • color = 'yellow' : la couleur des barres.
    • edgecolor = 'red' : la couleur des bordures. edgecolor = 'none' pour ne pas avoir de bordure.
    • hatch = '/' : les hachures. Valeurs possibles : '/', '\', '|', '-', '+', 'x', 'o', 'O', '.', '*'
    • histtype = 'step' : trace une ligne plutôt que des barres.
    • plt.hist(v, cumulative = True) : distribution cumulée
    • plt.hist(v, cumulative = -1) : distribution cumulée renversée.

    Vous pouvez encore superposer deux histogrammes avec une série de listes.

    In [358]:
    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().

    In [359]:
    # 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.

    In [360]:
    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.

    In [361]:
    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.

    In [362]:
    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().

    In [363]:
    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 :

    • Notion de figure qui peut regrouper plusieurs graphes, modélisé par un objet Figure. Pour créer une figure, on peut alors faire par exemple plt.figure(2).
    • Notion de graphe dans cette figure, modélisé par un objet Axes. Pour créer un graphe, on fait par exemple plt.subplot(212).

    Taille d'une figure :

    • Pour fixer la taille de la figure largeur puis hauteur (en pouces) : figure = plt.figure(figsize = (10, 10)).
    • Pour avoir la taille de la figure (tuple renvoyé) : figure.get_size_inches().
    In [364]:
    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()
    
    Out[364]:
    array([8., 4.])

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

    In [365]:
    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 :

    • plt.figure(1) : ouvre la figure numéro 1 (appel implicite à la figure numéro 1 si pas d'appel à figure()).
    • plt.subplot(2,1,1) : partage la figure en 2 x 1 emplacements de graphes (2 lignes et 1 colonne) et sélectionne le 1er emplacement pour les instructions graphiques suivantes. Les numéros des graphes sont comptés par ligne.
    • plt.subplot(211) : c'est une alternative à plt.subplot(2,1,1), dans le cas ou les dimensions et le numéro de graphe sont inférieurs à 10.
    • Si on ne fait pas d'appel à subplot, par défaut, il y a un appel plt.subplot(111) (1 emplacement).

    Fermeture d'une figure :

    • plt.close() : ferme la figure courante. Nécessaire pour libérer toutes les ressources occupées, même si la figure n'est plus visible.
    • plt.close(2) : ferme la figure 2.
    • plt.close('all') : ferme toutes les figures.
    In [366]:
    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 :

    • myfig, (ax1, ax2) = plt.subplots(nrows = 2, ncols = 3) : permet de définir à l'avance des graphes, pour les remplir après.
    • plt.gcf().get_axes() : retourne la liste des graphes présents sur la figure courante.
    • squeeze = True : si un seul graphe, un seul axis retourné, si une seule colonne ou un seule ligne, array 1d retournée, sinon array 2d. C'est le défaut. Si squeeze = False, c'est toujours une array 2d qui est retournée. on peut boucler simplement sur les axis en mettant à plat l'array des axis : myfig, axes = plt.subplots(nrows = 5, ncols = 4); axes = axes.ravel().

    Sharex :

    • sharex = False ou sharex = 'none' : les graduations de l'axe des x sont indépendantes entre les graphes. C'est le défaut.
    • sharex = True ou sharex = 'all' : tous les graphes utilisent les mêmes graduations de l'axe des x.
    • sharex = 'row' : tous les graphes d'une même ligne utilisent les mêmes graduations de l'axe des x.
    • sharex = 'col' : tous les graphes d'une même colonne utilisent les mêmes graduations de l'axe des x.
    • sharey : comme sharex, mais pour l'axe des y.

    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.

    In [367]:
    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 :

    • pyplot.gcf().subplots_adjust(left = 0.2, bottom = 0.2, right = 0.9, top = 0.9, wspace = 0, hspace = 0) : permet de régler toutes les marges.
    • left et bottom : valeurs entre 0 et 1 qui indiquent où commencent les graphes par rapport à la figure.
    • right et top : valeurs entre 0 et 1 supérieures à left et bottom respectivement qui indiquent où finissent les graphes par rapport à la figure.
    • wspace et hspace : espace horizontal et vertical respectivement entre graphes, mais cette fois-ci en pouces (valeurs peuvent être supérieures à 1).
    • Les valeurs par défaut sont left = 0.125, bottom = 0.1, right = 0.9, top = 0.9, wspace = 0.2 et hspace = 0.2. on peut très bien modifier seulement certaines de ces valeurs, par exemple pyplot.gcf().subplots_adjust(hspace = 2)
    In [368]:
    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 :

    • myfig, (ax1, ax2) = pyplot.subplots(nrows = 2, ncols = 1) : renvoie la Figure et une array de AxesSubplot.
    • On peut alors remplir ax1 et ax2 en les passant à une fonction qui prend cet argument (par exemple celles dans seaborn) après avoir rempli les axes, il faut souvent indiquer la taille de la figure : pyplot.gcf().set_size_inches(8, 8).

    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:

    • pays : Le nom du pays (tout simplement).
    • continent : Le continent affecté au pays.
    • population : Le nombre d'habitants du pays en question.
    • sous_nutrition : Le nombre d'habitants en sous-nutrition du pays en question relevé par l'ONU en 2017.
    • pourcentage_sn : Le pourcentage de la population en sous-nutrition au sein du pays donné relevé par l'ONU en 2017.
    In [369]:
    world = pd.read_csv("data/tuto/monde.csv")
    world
    
    Out[369]:
    pays continent population sous_nutrition pourcentage_sn
    0 Afghanistan Asie 36296110.0 10600000.0 29.20
    1 Afrique du Sud Afrique 57009760.0 3500000.0 6.14
    2 Albanie Europe 2884170.0 200000.0 6.93
    3 Algérie Afrique 41389190.0 1600000.0 3.87
    4 Allemagne Europe 82658410.0 0.0 0.00
    ... ... ... ... ... ...
    162 Émirats arabes unis Asie 9487200.0 200000.0 2.11
    163 Équateur Amérique 16785360.0 1300000.0 7.74
    164 États-Unis Amérique 325084760.0 0.0 0.00
    165 Éthiopie Afrique 106399920.0 21600000.0 20.30
    166 Îles Salomon Océanie 636040.0 57000.0 8.96

    167 rows × 5 columns

    On peut tracer un histogramme qui représenterait la somme de la variable population selon la variable qualitative continent.

    In [370]:
    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.

    In [371]:
    pop = world.groupby("continent").sum()[["population"]].reset_index()
    pop
    
    Out[371]:
    continent population
    0 Afrique 1.112643e+09
    1 Amérique 9.931975e+08
    2 Asie 4.446008e+09
    3 Europe 7.383762e+08
    4 Océanie 3.194842e+07
    In [372]:
    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.

    In [373]:
    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.

    In [374]:
    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.

    In [375]:
    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.

    In [376]:
    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 :

    • product : Le produit acheté.
    • color : La couleur du produit.
    • client_gender : Le sexe du client qui a acheté le produit.
    In [377]:
    shopping = pd.read_csv("data/tables/shopping.csv")
    shopping
    
    Out[377]:
    product color client_gender
    0 Pant grey m
    1 Pant green f
    2 Sweet red m
    3 Tee-shirt green f
    4 Tee-shirt blue f
    ... ... ... ...
    395 Sweet grey m
    396 Tee-shirt blue m
    397 Sweet violet m
    398 Sweet green m
    399 Tee-shirt violet m

    400 rows × 3 columns

    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 :

    • k lignes (nombre de modalités de X).
    • p colonnes (nombres de modalités de Y).
    • Les totaux en lignes (effectif de chaque modalité de X).
    • Les totaux en colonnes (effectif de chaque modalité de Y).
    • Le total général (nombre n d'individus étudiés).
    In [378]:
    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
    
    Tableau de contingence réel :
    
    Out[378]:
    client_gender f m Total
    color
    blue 46.0 48.0 94.0
    green 15.0 36.0 51.0
    grey 55.0 41.0 96.0
    red 23.0 38.0 61.0
    violet 54.0 44.0 98.0
    total 193.0 207.0 400.0

    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.

    In [379]:
    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
    
    Tableau de contingence théorique :
    
    Out[379]:
    f m
    blue 45.3550 48.6450
    green 24.6075 26.3925
    grey 46.3200 49.6800
    red 29.4325 31.5675
    violet 47.2850 50.7150

    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.

    In [380]:
    c = c.fillna(0)
    
    mesure = (c-indep)**2 / indep
    
    xi_n = mesure.sum().sum()
    
    print("Le Chi-2, ici, est de {}.".format(xi_n))
    
    Le Chi-2, ici, est de 14.968547906299902.
    

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

    In [381]:
    ddl = (len(c)-1) * (len(c.columns)-1)
    ddl
    
    Out[381]:
    4

    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.

    In [382]:
    import scipy.stats as st
    
    pvalue = st.chi2_contingency(c)[1]
    pvalue
    
    Out[382]:
    0.004766897397889381

    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 :

    • Une corrélation forte si l'indice de corrélation est proche de 1.
    • Une corrélation nulle si l'indice de corrélation est proche de 0.
    • Une anticorrélation si l'indice de corrélation est proche de -1.


    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.

    In [383]:
    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 :

    • Price : Le prix de la voiture.
    • Country : Le pays où elle est vendue.
    • Reliability : Son degré de fiabilité.
    • Mileage : Le nombre de milliers de kilomètres au compteur.
    • Type : Sa catégorie (variable qualitative).
    • Weight : Son poids en kilogrammes.
    • Disp. : Le nombre de modèles disponibles.
    • HP : La puissance de la voiture (Horsepower).
    In [384]:
    cars = pd.read_csv("data/tuto/cars.csv")
    cars.head()
    
    Out[384]:
    Price Country Reliability Mileage Type Weight Disp. HP
    0 8895 USA 4.0 33 Small 2560 97 113
    1 7402 USA 2.0 33 Small 2345 114 90
    2 6319 Korea 4.0 37 Small 1845 81 63
    3 6635 Japan/USA 5.0 32 Small 2260 91 92
    4 6599 Japan 5.0 32 Small 2440 113 103

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

    In [385]:
    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 :

    • S'il est négatif alors il s'agit d'une corrélation linéaire négative.
    • S'il est positif alors il s'agit d'une corrélation linéaire positive.
    • Si r est égal à 0 alors on peut dire qu'il n'y a pas de corrélation linéaire entre les deux variables étudiées.
    In [386]:
    import scipy.stats as st
    
    r, pvalue = st.pearsonr(X, Y)
    print("Coefficient de corrélation de Pearson : ", r)
    print("p-value = ", pvalue)
    
    Coefficient de corrélation de Pearson :  0.6536433023252349
    p-value =  1.495483462543096e-08
    

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

    In [387]:
    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()
    
    Out[387]:
    OLS Regression Results
    Dep. Variable: Price R-squared: 0.427
    Model: OLS Adj. R-squared: 0.417
    Method: Least Squares F-statistic: 43.27
    Date: Sat, 08 May 2021 Prob (F-statistic): 1.50e-08
    Time: 17:47:37 Log-Likelihood: -566.79
    No. Observations: 60 AIC: 1138.
    Df Residuals: 58 BIC: 1142.
    Df Model: 1
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    HP 86.1440 13.096 6.578 0.000 59.929 112.359
    intercept 2075.9467 1652.089 1.257 0.214 -1231.068 5382.961
    Omnibus: 7.961 Durbin-Watson: 1.468
    Prob(Omnibus): 0.019 Jarque-Bera (JB): 9.560
    Skew: 0.501 Prob(JB): 0.00839
    Kurtosis: 4.679 Cond. No. 518.


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    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.

    In [388]:
    lr.params
    
    Out[388]:
    HP             86.144013
    intercept    2075.946714
    dtype: float64

    Dans le cas présent, cette droite vérifie l'équation y = 86,144014x + 2075,946714.

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

    In [389]:
    lr.rsquared
    
    Out[389]:
    0.42724956667463854

    Ici, le coefficient de détermination r² ≃ 0,43. Prob (F-statistic) correspond à la p-value de r.

    In [390]:
    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.

    In [391]:
    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)
    
    Out[391]:
    (0.7330188227217953, 4.087376901897988e-11)

    Nous présentons ici le DataFrame iris, qui contient une liste de 150 fleurs et présente les variables suivantes :

    • Id : L'identifiant de la fleur.
    • SepalLengthCm : La longueur du sépal (en cm).
    • SepalWidthCm : La largeur du sépal (en cm).
    • PetalLengthCm : La longueur du pétal (en cm).
    • PetalWidthCm : La largueur du pétal (en cm).
    • Species : Son espèce (seposa, versicolor ou virginica).
    In [392]:
    iris = pd.read_csv("data/tuto/iris.csv")
    iris
    
    Out[392]:
    Id SepalLengthCm SepalWidthCm PetalLengthCm PetalWidthCm Species
    0 1 5.1 3.5 1.4 0.2 Iris-setosa
    1 2 4.9 3.0 1.4 0.2 Iris-setosa
    2 3 4.7 3.2 1.3 0.2 Iris-setosa
    3 4 4.6 3.1 1.5 0.2 Iris-setosa
    4 5 5.0 3.6 1.4 0.2 Iris-setosa
    ... ... ... ... ... ... ...
    145 146 6.7 3.0 5.2 2.3 Iris-virginica
    146 147 6.3 2.5 5.0 1.9 Iris-virginica
    147 148 6.5 3.0 5.2 2.0 Iris-virginica
    148 149 6.2 3.4 5.4 2.3 Iris-virginica
    149 150 5.9 3.0 5.1 1.8 Iris-virginica

    150 rows × 6 columns

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

    In [393]:
    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.

    In [394]:
    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.

    In [395]:
    iris_by_species = pd.DataFrame(iris.groupby("Species").mean()["SepalWidthCm"])
    iris_by_species
    
    Out[395]:
    SepalWidthCm
    Species
    Iris-setosa 3.418
    Iris-versicolor 2.770
    Iris-virginica 2.974

    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.

    In [396]:
    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))
    
    Moyenne Générale =  3.054
    

    La Somme des Carrés Totale (SCT) est la somme des carrés entre toutes les observations et la moyenne globale (MG).

    In [397]:
    SCT = ((iris["SepalWidthCm"]-MG)**2).sum()
    print("SCT = ", SCT)
    
    SCT =  28.0126
    

    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.

    In [398]:
    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))
    
    SCR =  17.035
    

    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.

    In [399]:
    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))
    
    SCE =  10.9776
    

    On retrouve bien la somme des carrés totale en faisant l'addition de la somme des carrés inter-groupes et intra-groupes.

    In [400]:
    print("On a bien SCR + SCE = {0} et SCT = {1}, magique, hein?".format(round(SCR+SCE,4), SCT))
    
    On a bien SCR + SCE = 28.0126 et SCT = 28.0126, magique, hein?
    

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

    In [401]:
    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)
    
    Degré de liberté inter-groupes :  2
    Degré de liberté intra-groupes :  147
    

    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.

    In [402]:
    F_statistic = SCE*ddl_intra / (SCR*ddl_inter)
    F_statistic
    
    Out[402]:
    47.36446140299387

    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.

    In [403]:
    eta_carre = SCE / SCT
    eta_carre
    
    Out[403]:
    0.39188079649871876

    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 :

    • obs : mois-jour.
    • maxO3 : teneur maximale en ozone observée sur la journée ( en μ\gr/m3 ).
    • T9, T12 et T15 : température observée à 9h, 12h et 15h.
    • Ne9, Ne12 et Ne15 : nébulosité observée à 9h, 12h et 15h.
    • Vx9, Vx12 et Vx15 : composante est-ouest du vent à 9h, 12h et 15h.
    • maxO3 : teneur maximale en ozone observée à 12h.
    • vent : orientation du vent à 12h.
    • pluie : occurrence ou non de précipitations.
    In [404]:
    ozone = pd.read_csv("data/tuto/ozone.txt", sep=";", decimal=",")
    ozone
    
    Out[404]:
    obs maxO3 T9 T12 T15 Ne9 Ne12 Ne15 Vx9 Vx12 Vx15 maxO3v vent pluie
    0 601 87 15.6 18.5 18.4 4 4 8 0.6946 -1.7101 -0.6946 84 Nord Sec
    1 602 82 17.0 18.4 17.7 5 5 7 -4.3301 -4.0000 -3.0000 87 Nord Sec
    2 603 92 15.3 17.6 19.5 2 5 4 2.9544 1.8794 0.5209 82 Est Sec
    3 604 114 16.2 19.7 22.5 1 1 0 0.9848 0.3473 -0.1736 92 Nord Sec
    4 605 94 17.4 20.5 20.4 8 8 7 -0.5000 -2.9544 -4.3301 114 Ouest Sec
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
    107 925 84 13.3 17.7 17.8 3 5 6 0.0000 -1.0000 -1.2856 76 Sud Sec
    108 927 77 16.2 20.8 22.1 6 5 5 -0.6946 -2.0000 -1.3681 71 Sud Pluie
    109 928 99 16.9 23.0 22.6 6 4 7 1.5000 0.8682 0.8682 77 Sud Sec
    110 929 83 16.9 19.8 22.1 6 5 3 -4.0000 -3.7588 -4.0000 99 Ouest Pluie
    111 930 70 15.7 18.6 20.7 7 7 7 0.0000 -1.0419 -4.0000 83 Sud Sec

    112 rows × 14 columns

    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.

    In [405]:
    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.

    In [406]:
    reg_simp = smf.ols('maxO3 ~ T12', data=ozone).fit()
    reg_simp.summary()
    
    Out[406]:
    OLS Regression Results
    Dep. Variable: maxO3 R-squared: 0.615
    Model: OLS Adj. R-squared: 0.612
    Method: Least Squares F-statistic: 175.8
    Date: Sat, 08 May 2021 Prob (F-statistic): 1.51e-24
    Time: 17:48:01 Log-Likelihood: -478.91
    No. Observations: 112 AIC: 961.8
    Df Residuals: 110 BIC: 967.3
    Df Model: 1
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept -27.4196 9.033 -3.035 0.003 -45.322 -9.517
    T12 5.4687 0.412 13.258 0.000 4.651 6.286
    Omnibus: 1.154 Durbin-Watson: 1.101
    Prob(Omnibus): 0.562 Jarque-Bera (JB): 1.242
    Skew: 0.196 Prob(JB): 0.537
    Kurtosis: 2.664 Cond. No. 119.


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    Nous obtenons des statistiques sur les coefficients obtenus :

    • Leur valeur.
    • Leur écart-type.
    • La statistique de test de Student.
    • La p-valeur (le test effectué sur le paramètre est ici le test de significativité : le paramètre vaut 0 versus le paramètre est différent de 0), ainsi que des statistiques sur le modèle général ($R^{2}$, $AIC$, etc...).

    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.

    In [407]:
    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.

    In [408]:
    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.

    In [409]:
    ozone["residu_s"] = reg_simp.resid
    ozone
    
    Out[409]:
    obs maxO3 T9 T12 T15 Ne9 Ne12 Ne15 Vx9 Vx12 Vx15 maxO3v vent pluie maxO3_ajust_s residu_s
    0 601 87 15.6 18.5 18.4 4 4 8 0.6946 -1.7101 -0.6946 84 Nord Sec 73.751034 13.248966
    1 602 82 17.0 18.4 17.7 5 5 7 -4.3301 -4.0000 -3.0000 87 Nord Sec 73.204166 8.795834
    2 603 92 15.3 17.6 19.5 2 5 4 2.9544 1.8794 0.5209 82 Est Sec 68.829218 23.170782
    3 604 114 16.2 19.7 22.5 1 1 0 0.9848 0.3473 -0.1736 92 Nord Sec 80.313456 33.686544
    4 605 94 17.4 20.5 20.4 8 8 7 -0.5000 -2.9544 -4.3301 114 Ouest Sec 84.688404 9.311596
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
    107 925 84 13.3 17.7 17.8 3 5 6 0.0000 -1.0000 -1.2856 76 Sud Sec 69.376086 14.623914
    108 927 77 16.2 20.8 22.1 6 5 5 -0.6946 -2.0000 -1.3681 71 Sud Pluie 86.329009 -9.329009
    109 928 99 16.9 23.0 22.6 6 4 7 1.5000 0.8682 0.8682 77 Sud Sec 98.360116 0.639884
    110 929 83 16.9 19.8 22.1 6 5 3 -4.0000 -3.7588 -4.0000 99 Ouest Pluie 80.860325 2.139675
    111 930 70 15.7 18.6 20.7 7 7 7 0.0000 -1.0419 -4.0000 83 Sud Sec 74.297903 -4.297903

    112 rows × 16 columns

    On peut maintenant afficher l'histogramme des résidus.

    In [410]:
    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.

    In [411]:
    a_prevoir = pd.DataFrame({"T12":[19]})
    maxO3_prev = reg_simp.predict(a_prevoir)
    print(round(maxO3_prev[0], 2))
    
    76.49
    

    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 :

    • obs : mois-jour.
    • maxO3 : teneur maximale en ozone observée sur la journée ( en μ\gr/m3 ).
    • T9, T12 et T15 : température observée à 9h, 12h et 15h.
    • Ne9, Ne12 et Ne15 : nébulosité observée à 9h, 12h et 15h.
    • Vx9, Vx12 et Vx15 : composante est-ouest du vent à 9h, 12h et 15h.
    • maxO3 : teneur maximale en ozone observée à 12h.
    • vent : orientation du vent à 12h.
    • pluie : occurrence ou non de précipitations.
    In [412]:
    ozone = pd.read_csv("data/tuto/ozone.txt", sep=";", decimal=",")
    ozone
    
    Out[412]:
    obs maxO3 T9 T12 T15 Ne9 Ne12 Ne15 Vx9 Vx12 Vx15 maxO3v vent pluie
    0 601 87 15.6 18.5 18.4 4 4 8 0.6946 -1.7101 -0.6946 84 Nord Sec
    1 602 82 17.0 18.4 17.7 5 5 7 -4.3301 -4.0000 -3.0000 87 Nord Sec
    2 603 92 15.3 17.6 19.5 2 5 4 2.9544 1.8794 0.5209 82 Est Sec
    3 604 114 16.2 19.7 22.5 1 1 0 0.9848 0.3473 -0.1736 92 Nord Sec
    4 605 94 17.4 20.5 20.4 8 8 7 -0.5000 -2.9544 -4.3301 114 Ouest Sec
    ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
    107 925 84 13.3 17.7 17.8 3 5 6 0.0000 -1.0000 -1.2856 76 Sud Sec
    108 927 77 16.2 20.8 22.1 6 5 5 -0.6946 -2.0000 -1.3681 71 Sud Pluie
    109 928 99 16.9 23.0 22.6 6 4 7 1.5000 0.8682 0.8682 77 Sud Sec
    110 929 83 16.9 19.8 22.1 6 5 3 -4.0000 -3.7588 -4.0000 99 Ouest Pluie
    111 930 70 15.7 18.6 20.7 7 7 7 0.0000 -1.0419 -4.0000 83 Sud Sec

    112 rows × 14 columns

    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.

    In [413]:
    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]
    
    R² =  0.7545614396200057
    
    Out[413]:
    coef std err t P>|t| [0.025 0.975]
    Intercept 12.7055 13.109 0.969 0.335 -13.289 38.700
    T9 -0.6360 1.035 -0.615 0.540 -2.688 1.416
    T12 2.5060 1.399 1.791 0.076 -0.269 5.281
    T15 0.7138 1.137 0.628 0.531 -1.540 2.968
    Ne9 -2.7606 0.892 -3.096 0.003 -4.529 -0.993
    Ne12 -0.3719 1.346 -0.276 0.783 -3.041 2.297
    Ne15 0.0903 0.999 0.090 0.928 -1.891 2.072
    maxO3v 0.3777 0.061 6.171 0.000 0.256 0.499

    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.

    In [414]:
    reg_multi = smf.ols("maxO3~T9+T12+T15+Ne9+Ne12+maxO3v", data=ozone).fit()
    reg_multi.summary().tables[1]
    
    Out[414]:
    coef std err t P>|t| [0.025 0.975]
    Intercept 12.8492 12.950 0.992 0.323 -12.829 38.527
    T9 -0.6298 1.027 -0.613 0.541 -2.667 1.407
    T12 2.5602 1.258 2.034 0.044 0.065 5.055
    T15 0.6579 0.949 0.693 0.490 -1.223 2.539
    Ne9 -2.7653 0.886 -3.122 0.002 -4.522 -1.009
    Ne12 -0.3080 1.139 -0.270 0.787 -2.567 1.951
    maxO3v 0.3775 0.061 6.202 0.000 0.257 0.498

    On voit maintenant que c'est Ne12 la moins significative, avec une p-valeur de 0.79.

    In [415]:
    reg_multi = smf.ols("maxO3~T9+T12+T15+Ne9+maxO3v", data=ozone).fit()
    reg_multi.summary().tables[1]
    
    Out[415]:
    coef std err t P>|t| [0.025 0.975]
    Intercept 11.2844 11.534 0.978 0.330 -11.583 34.152
    T9 -0.7313 0.952 -0.768 0.444 -2.619 1.157
    T12 2.6649 1.192 2.235 0.027 0.301 5.028
    T15 0.6682 0.944 0.708 0.481 -1.203 2.539
    Ne9 -2.9258 0.655 -4.470 0.000 -4.223 -1.628
    maxO3v 0.3796 0.060 6.314 0.000 0.260 0.499

    On voit maintenant que l'on peut retirer T9.

    In [416]:
    reg_multi = smf.ols("maxO3~T12+T15+Ne9+maxO3v", data=ozone).fit()
    reg_multi.summary().tables[1]
    
    Out[416]:
    coef std err t P>|t| [0.025 0.975]
    Intercept 9.1368 11.168 0.818 0.415 -13.003 31.277
    T12 2.2318 1.048 2.129 0.036 0.154 4.310
    T15 0.6277 0.941 0.667 0.506 -1.237 2.492
    Ne9 -2.9639 0.651 -4.550 0.000 -4.255 -1.673
    maxO3v 0.3702 0.059 6.301 0.000 0.254 0.487

    Puis T15.

    In [417]:
    reg_multi = smf.ols("maxO3~T12+Ne9+maxO3v", data=ozone).fit()
    reg_multi.summary().tables[1]
    
    Out[417]:
    coef std err t P>|t| [0.025 0.975]
    Intercept 9.7622 11.100 0.879 0.381 -12.241 31.765
    T12 2.8531 0.481 5.937 0.000 1.901 3.806
    Ne9 -3.0242 0.643 -4.700 0.000 -4.300 -1.749
    maxO3v 0.3757 0.058 6.477 0.000 0.261 0.491

    Maintenant, les pvalues sont toutes inférieures à 5%. Affichons donc le sommaire tout entier.

    Affichons maintenant le sommaire tout entier avec .summary().

    In [418]:
    reg_multi.summary()
    
    Out[418]:
    OLS Regression Results
    Dep. Variable: maxO3 R-squared: 0.752
    Model: OLS Adj. R-squared: 0.745
    Method: Least Squares F-statistic: 109.1
    Date: Sat, 08 May 2021 Prob (F-statistic): 1.46e-32
    Time: 17:48:06 Log-Likelihood: -454.30
    No. Observations: 112 AIC: 916.6
    Df Residuals: 108 BIC: 927.5
    Df Model: 3
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 9.7622 11.100 0.879 0.381 -12.241 31.765
    T12 2.8531 0.481 5.937 0.000 1.901 3.806
    Ne9 -3.0242 0.643 -4.700 0.000 -4.300 -1.749
    maxO3v 0.3757 0.058 6.477 0.000 0.261 0.491
    Omnibus: 9.766 Durbin-Watson: 1.884
    Prob(Omnibus): 0.008 Jarque-Bera (JB): 23.605
    Skew: 0.020 Prob(JB): 7.48e-06
    Kurtosis: 5.249 Cond. No. 805.


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    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.

    In [419]:
    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))
    
    84.08
    

    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.

    In [420]:
    alpha = 0.05
    n = ozone.shape[0]
    p = 4
    
    analyses = pd.DataFrame({"obs":np.arange(1, n+1)})
    analyses
    
    Out[420]:
    obs
    0 1
    1 2
    2 3
    3 4
    4 5
    ... ...
    107 108
    108 109
    109 110
    110 111
    111 112

    112 rows × 1 columns

    On peut calculer les leviers comme ceci, en sachant que le seuil des leviers est de $2∗\frac{p}{n}$ .

    In [421]:
    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.

    In [422]:
    analyses.loc[analyses["levier"]>seuil_levier, :]
    
    Out[422]:
    obs levier
    21 22 0.081663
    28 29 0.080777
    30 31 0.076282
    56 57 0.079123
    71 72 0.085491
    79 80 0.101387
    80 81 0.095683
    99 100 0.071728
    105 106 0.089376

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

    In [423]:
    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.

    In [424]:
    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.

    In [425]:
    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])]
    
    Out[425]:
    [2.0678328061248994, 1.5277727396873013, 1.4747178841142814]

    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 :

    • D'analyser la normalité des résidus sur la droite de Henry.
    • D'analyser l'homoscédasticité des résidus sur le nuage de la variance résiduelle.
    In [426]:
    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.

    In [427]:
    shapiro(reg_multi.resid)
    
    Out[427]:
    ShapiroResult(statistic=0.9623235464096069, pvalue=0.0030248425900936127)

    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 :

    • ind : le numéro du patient.
    • sbp : tension artérielle systolique.
    • tobacco : tabac cumulé (en kg).
    • ldl : cholestérol de lipoprotéines de faible densité.
    • adiposity : adiposité.
    • famhist : antécédents familiaux.
    • typea : comportement type A.
    • obesity : obésité.
    • alcohol : consomation courante d'alcool.
    • age : âge au moment de l'attaque cardiaque.
    • chd : maladie coronarienne.
    In [428]:
    maladie = pd.read_csv("data/tuto/maladie.txt", sep=";", decimal=".")
    maladie
    
    Out[428]:
    ind sbp tobacco ldl adiposity famhist typea obesity alcohol age chd
    0 1 160 12.00 5.73 23.11 Present 49 25.30 97.20 52 1
    1 2 144 0.01 4.41 28.61 Absent 55 28.87 2.06 63 1
    2 3 118 0.08 3.48 32.28 Present 52 29.14 3.81 46 0
    3 4 170 7.50 6.41 38.03 Present 51 31.99 24.26 58 1
    4 5 134 13.60 3.50 27.78 Present 60 25.99 57.34 49 1
    ... ... ... ... ... ... ... ... ... ... ... ...
    457 459 214 0.40 5.98 31.72 Absent 64 28.45 0.00 58 0
    458 460 182 4.20 4.41 32.10 Absent 52 28.61 18.72 52 1
    459 461 108 3.00 1.59 15.23 Absent 40 20.09 26.64 55 0
    460 462 118 5.40 11.61 30.79 Absent 64 27.35 23.97 40 0
    461 463 132 0.00 4.82 33.41 Present 62 14.70 0.00 46 1

    462 rows × 11 columns

    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.

    In [429]:
    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.

    In [430]:
    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
    
    Out[430]:
    age prop_chd
    0 15 0.029851
    1 25 0.029851
    2 25 0.210526
    3 35 0.210526
    4 35 0.309524
    5 45 0.309524
    6 45 0.428571
    7 55 0.428571
    8 55 0.546154
    9 65 0.546154

    On peut alors représenter graphiquement ces proportions.

    In [431]:
    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().

    In [432]:
    reg_log1 = smf.glm("chd ~ age", data=maladie, family=sm.families.Binomial()).fit()
    reg_log1.summary()
    
    Out[432]:
    Generalized Linear Model Regression Results
    Dep. Variable: chd No. Observations: 462
    Model: GLM Df Residuals: 460
    Model Family: Binomial Df Model: 1
    Link Function: logit Scale: 1.0000
    Method: IRLS Log-Likelihood: -262.78
    Date: Sat, 08 May 2021 Deviance: 525.56
    Time: 17:48:18 Pearson chi2: 445.
    No. Iterations: 4
    Covariance Type: nonrobust
    coef std err z P>|z| [0.025 0.975]
    Intercept -3.5217 0.416 -8.465 0.000 -4.337 -2.706
    age 0.0641 0.009 7.513 0.000 0.047 0.081
    On obtient les paramètres estimés : $\hat{\beta}_{1}=−3.5$ et $\hat{\beta}_{2}=0.064$, on les enregistre.

    Dans le but de tracer la courbe logistique entre les abscisses $x=15$ et $x=65$, on définit une séquence de 15 à 65 par pas de 500, puis on la place dans la variable x. On calcule ensuite les ordonnées de la courbe, grâce à l'expression de la courbe en S : $f(x) = \frac{e^{\beta_{1}+\beta_{2}x}}{1+e^{\beta_{1}+\beta_{2}x}}$.
    Nous plaçons ces ordonnées dans la variable y. Enfin, avec x et y, nous créons un DataFrame.
    In [433]:
    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
    
    Out[433]:
    age prop_chd
    0 15.000000 0.071752
    1 15.100200 0.072181
    2 15.200401 0.072612
    3 15.300601 0.073046
    4 15.400802 0.073482
    ... ... ...
    495 64.599198 0.650131
    496 64.699399 0.651591
    497 64.799599 0.653048
    498 64.899800 0.654502
    499 65.000000 0.655953

    500 rows × 2 columns

    On souhaite superposer la fonction de lien obtenue par régression logistique sur le graphique précédent.

    In [434]:
    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.

    In [435]:
    reg_log2 = smf.glm("chd~sbp+tobacco+ldl+adiposity+famhist+typea+obesity+alcohol+age",
                       data=maladie, family=sm.families.Binomial()).fit()
    reg_log2.summary()
    
    Out[435]:
    Generalized Linear Model Regression Results
    Dep. Variable: chd No. Observations: 462
    Model: GLM Df Residuals: 452
    Model Family: Binomial Df Model: 9
    Link Function: logit Scale: 1.0000
    Method: IRLS Log-Likelihood: -236.07
    Date: Sat, 08 May 2021 Deviance: 472.14
    Time: 17:48:19 Pearson chi2: 452.
    No. Iterations: 5
    Covariance Type: nonrobust
    coef std err z P>|z| [0.025 0.975]
    Intercept -6.1507 1.308 -4.701 0.000 -8.715 -3.587
    famhist[T.Present] 0.9254 0.228 4.061 0.000 0.479 1.372
    sbp 0.0065 0.006 1.135 0.256 -0.005 0.018
    tobacco 0.0794 0.027 2.984 0.003 0.027 0.132
    ldl 0.1739 0.060 2.915 0.004 0.057 0.291
    adiposity 0.0186 0.029 0.635 0.526 -0.039 0.076
    typea 0.0396 0.012 3.214 0.001 0.015 0.064
    obesity -0.0629 0.044 -1.422 0.155 -0.150 0.024
    alcohol 0.0001 0.004 0.027 0.978 -0.009 0.009
    age 0.0452 0.012 3.728 0.000 0.021 0.069

    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 :

    • rdt : rendement de blé (en quintaux par hectare).
    • ble : variété de blé (A, B, C ou D).
    • phyto : traitement phytosanitaire (1 si positif, 0 sinon).
    In [436]:
    ble = pd.read_csv("data/tuto/ble.txt", sep=";", decimal=".")
    ble
    
    Out[436]:
    parcelle variete phyto rdt
    0 1 V1 Avec 5652
    1 2 V1 Avec 5583
    2 3 V1 Avec 5612
    3 4 V1 Avec 5735
    4 5 V1 Avec 5704
    ... ... ... ... ...
    75 76 V4 Sans 5809
    76 77 V4 Sans 5627
    77 78 V4 Sans 5881
    78 79 V4 Sans 5649
    79 80 V4 Sans 5799

    80 rows × 4 columns

    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.

    In [437]:
    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.

    In [438]:
    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é.

    In [439]:
    anova_variete = smf.ols("rdt~variete", data=ble).fit()
    anova_variete.summary()
    
    Out[439]:
    OLS Regression Results
    Dep. Variable: rdt R-squared: 0.448
    Model: OLS Adj. R-squared: 0.426
    Method: Least Squares F-statistic: 20.53
    Date: Sat, 08 May 2021 Prob (F-statistic): 7.67e-10
    Time: 17:48:20 Log-Likelihood: -492.86
    No. Observations: 80 AIC: 993.7
    Df Residuals: 76 BIC: 1003.
    Df Model: 3
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 5633.8000 26.300 214.211 0.000 5581.419 5686.181
    variete[T.V2] -49.7000 37.194 -1.336 0.185 -123.779 24.379
    variete[T.V3] -169.2000 37.194 -4.549 0.000 -243.279 -95.121
    variete[T.V4] 118.4000 37.194 3.183 0.002 44.321 192.479
    Omnibus: 2.869 Durbin-Watson: 2.355
    Prob(Omnibus): 0.238 Jarque-Bera (JB): 2.611
    Skew: -0.085 Prob(JB): 0.271
    Kurtosis: 3.868 Cond. No. 4.79


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    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.

    In [440]:
    sm.stats.anova_lm(anova_variete, typ=2)
    
    Out[440]:
    sum_sq df F PR(>F)
    variete 851844.55 3.0 20.525327 7.674413e-10
    Residual 1051387.00 76.0 NaN NaN

    On réalise ensuite l'ANOVA sur le pesticide utilisé.

    In [441]:
    anova_phyto = smf.ols("rdt~phyto", data=ble).fit()
    anova_phyto.summary()
    
    Out[441]:
    OLS Regression Results
    Dep. Variable: rdt R-squared: 0.001
    Model: OLS Adj. R-squared: -0.012
    Method: Least Squares F-statistic: 0.04134
    Date: Sat, 08 May 2021 Prob (F-statistic): 0.839
    Time: 17:48:21 Log-Likelihood: -516.58
    No. Observations: 80 AIC: 1037.
    Df Residuals: 78 BIC: 1042.
    Df Model: 1
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 5612.2250 24.692 227.291 0.000 5563.067 5661.383
    phyto[T.Sans] -7.1000 34.920 -0.203 0.839 -76.619 62.419
    Omnibus: 3.177 Durbin-Watson: 1.446
    Prob(Omnibus): 0.204 Jarque-Bera (JB): 1.881
    Skew: 0.101 Prob(JB): 0.390
    Kurtosis: 2.276 Cond. No. 2.62


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    Puis on observe les p-values grâce à anova_lm( anova_phyto , typ = 2 ).

    In [442]:
    sm.stats.anova_lm(anova_phyto, typ=2)
    
    Out[442]:
    sum_sq df F PR(>F)
    phyto 1008.20 1.0 0.041341 0.839411
    Residual 1902223.35 78.0 NaN NaN

    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.

    In [443]:
    anova_variete_phyto = smf.ols("rdt~variete*phyto", data=ble).fit()
    anova_variete_phyto.summary()
    
    Out[443]:
    OLS Regression Results
    Dep. Variable: rdt R-squared: 0.451
    Model: OLS Adj. R-squared: 0.398
    Method: Least Squares F-statistic: 8.458
    Date: Sat, 08 May 2021 Prob (F-statistic): 1.62e-07
    Time: 17:48:21 Log-Likelihood: -492.59
    No. Observations: 80 AIC: 1001.
    Df Residuals: 72 BIC: 1020.
    Df Model: 7
    Covariance Type: nonrobust
    coef std err t P>|t| [0.025 0.975]
    Intercept 5628.1000 38.086 147.772 0.000 5552.176 5704.024
    variete[T.V2] -34.4000 53.862 -0.639 0.525 -141.772 72.972
    variete[T.V3] -167.6000 53.862 -3.112 0.003 -274.972 -60.228
    variete[T.V4] 138.5000 53.862 2.571 0.012 31.128 245.872
    phyto[T.Sans] 11.4000 53.862 0.212 0.833 -95.972 118.772
    variete[T.V2]:phyto[T.Sans] -30.6000 76.173 -0.402 0.689 -182.448 121.248
    variete[T.V3]:phyto[T.Sans] -3.2000 76.173 -0.042 0.967 -155.048 148.648
    variete[T.V4]:phyto[T.Sans] -40.2000 76.173 -0.528 0.599 -192.048 111.648
    Omnibus: 2.691 Durbin-Watson: 2.399
    Prob(Omnibus): 0.260 Jarque-Bera (JB): 2.415
    Skew: 0.008 Prob(JB): 0.299
    Kurtosis: 3.851 Cond. No. 12.5


    Warnings:
    [1] Standard Errors assume that the covariance matrix of the errors is correctly specified.

    Puis on observe les p-values grâce à anova_lm( anova_variete_phyto , typ = 2 ).

    In [444]:
    sm.stats.anova_lm(anova_variete_phyto)
    
    Out[444]:
    df sum_sq mean_sq F PR(>F)
    variete 3.0 851844.55 283948.183333 19.574935 2.205108e-09
    phyto 1.0 1008.20 1008.200000 0.069504 7.928138e-01
    variete:phyto 3.0 5968.20 1989.400000 0.137146 9.375236e-01
    Residual 72.0 1044410.60 14505.702778 NaN NaN

    On voit sur le tableau 3 lignes :

    • variete : qui teste l'effet de la variété ;
    • phyto : qui teste l'effet du pesticide ;
    • variete:phyto : qui teste les interactions pesticide-variété.

    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.