diff --git a/rapport.md b/rapport.md index ddb2e3e..8d537ca 100644 --- a/rapport.md +++ b/rapport.md @@ -1,6 +1,6 @@ --- -title: IFT-7022 - Traitement automatique de la langue naturelle -author: François Pelletier (908144032) +ntitle: IFT-7022 - Traitement automatique de la langue naturelle +author: François Pelletier (908 144 032) date: 30 septembre 2019 geometry: "left=3cm,right=3cm,top=2cm,bottom=2cm" output: pdf_document @@ -12,11 +12,11 @@ output: pdf_document L'objectif de ce problème est de convertir un ensemble de questions en affirmations à l'aide d'expressions régulières et règles de substitution. -Comme il est important de garder une forme la plus générale possible pour ces expressions, il est attendu à ce qu'un certain nombre de questions ne soient pas converties adéquatement. Sinon, il aurait été facile d'y aller avec une solution triviale qui associe une expression différente pour chaque question. +Comme on souhaite garder la forme la plus générale possible pour ces expressions, on s'attend à ce qu'un certain nombre de questions ne soient pas converties adéquatement. Sinon, il aurait été facile d'y aller avec une solution triviale qui associe une expression différente pour chaque question. ### Approche utilisée -L'approche utilisée a été d'identifier d'abord les mots questions, ensuite d'identifier la structure générale de la phrase, dont le verbe principal et les noms à remplacer, le cas échéant. Puis, j'ai identifié les différents types de réponses requises, et lorsqu'un même motif de question nécessitait deux types de réponses différentes, j'ai séparé en deux expressions consécutives. Ensuite, j'ai construit quelques règles qui appliquent des corrections mineures au niveau des signes et de la ponctuation. +L'approche utilisée a été de trouver d'abord les mots interrogatifs, ensuite la structure générale de la phrase, dont le verbe principal et les noms à remplacer, le cas échéant. Puis, on a identifié les différents types de réponses requises, et lorsqu'un même motif de question nécessitait deux types de réponses différentes, on a séparé en deux expressions consécutives. Ensuite, on a construit quelques règles qui appliquent des corrections mineures au niveau des signes et de la ponctuation. Toutes ces règles sont enchainées les unes à la suite des autres. Comme la conversion élimine le point d'interrogation à la fin de la phrase, les motifs subséquents au premier qui a donné une correspondance sont ainsi ignorés. Il n'y a donc aucun traitement conditionnel en dehors de ceux inclus dans les expressions régulières. @@ -24,23 +24,23 @@ Toutes ces règles sont enchainées les unes à la suite des autres. Comme la co Les motifs suivent cet ordre. Sauf indication contraire, le groupe affirmatif à convertir est situé après le motif recherché : -1. Mot-question **qui**, suivi d'un verbe auxiliaire, sous forme d'énumération partielle -2. Mot-question **qu'est-ce qu'** -3. Mot-question **que** suivi d'un verbe d'état ou d'action. J'ai ici utilisé une énumération partielle pour identifier les verbes présents dans les questions en échantillon. -4. Mot-question **où** ou de la forme **dans quelle** suivi d'une catégorie géographique, sous forme d'énumération partielle. J'ai aussi capturé la forme particulière **quelle est la capitale** où je remplace le mot **quelle** par où, afin de correspondre au motif précédent. Ceci me permet d'éviter une règle additionnelle. -5. Mot-question **quel** suivi soit du mot âge et du verbe avoir, soit du verbe auxiliaire et d'un nom représentant une quantité, sous forme d'énumération -6. Mot-question **quel** suivi du verbe être (forme générique du motif précédent) -7. Mot question **quand** ou groupe **en quelle année** -8. Mot question **pourquoi** -9. Mot question **combien** suivi de **de**. -10. Mot question **combien** suivi de **y a-t-il**. La différence ici est que la réponse conserve l'ordre de la question et qu'il faut transformer **y a-t-il** en **il y a**. -11. Mot-question **quel** à la fin de la question. Ce motif sert à attraper une forme moins fréquente de questions où la question est posée après le groupe affirmatif. +1. Mot interrogatif **qui**, suivi d'un verbe auxiliaire, sous forme d'énumération partielle ; +2. Mot interrogatif **qu'est-ce que **; +3. Mot interrogatif **que** suivi d'un verbe d'état ou d'action. On a ici utilisé une énumération partielle pour identifier les verbes présents dans les questions en échantillon ; +4. Mot interrogatif **où** ou de la forme **dans quelle** suivi d'une catégorie géographique, sous forme d'énumération partielle. On a aussi capturé la forme particulière **quelle est la capitale** où on remplace le mot **quelle** par où, afin de correspondre au motif précédent. Ceci permet d'éviter une règle additionnelle ; +5. Mot interrogatif **quel** suivi soit du mot âge et du verbe avoir, soit du verbe auxiliaire et d'un nom représentant une quantité, sous forme d'énumération ; +6. Mot interrogatif **quel** suivi du verbe être (forme générique du motif précédent) ; +7. mot interrogatif **quand** ou groupe **en quelle année **; +8. mot interrogatif **pourquoi **; +9. mot interrogatif **combien** suivi du mot **de **; +10. mot interrogatif **combien** suivi de **y a-t-il**. La différence ici est que la réponse conserve l'ordre de la question et qu'il faut transformer **y a-t-il** en **il y a **; +11. Mot interrogatif **quel** à la fin de la question. Ce motif sert à attraper une forme moins fréquente de questions où la question est posée après le groupe affirmatif. Quelques modifications au niveau de la phrase viennent ensuite *nettoyer* la résultante : 12. Suppression des inversions *-il*, *-elle*, *-t-il*, *-t-elle* 13. Inversion du pronom *vous* -14. Retrait des doubles espaces +14. Retrait des espaces multiples 15. Retrait des espaces au début de la phrase 16. Retrait des espaces après les apostrophes @@ -54,9 +54,9 @@ Situation 1. `Q : Qui était Galileo?`: - Comme cette question correspond au motif 1, la réponse que le module produit est **Luc Lamontagne était Galileo.**, alors qu'elle aurait du tomber dans la catégorie *autre*. Pour ce faire, cela aurait nécessité une analyse syntaxique. -Situation 2. `Q : A quelle distance se trouve la ligne de service du net au tennis?`: +Situation 2. `Q : À quelle distance se trouve la ligne de service du net au tennis?`: -- Il me reste le mot distance au début de l'affirmation, que je n'arrive pas à enlever sans devoir faire une expression trop spécifique. +- Il reste le mot `distance` au début de l'affirmation, qu'on n'arrive pas à enlever sans devoir faire une expression trop spécifique. Situation 3. `Q : Combien de litres dans un gallon?`: @@ -64,98 +64,98 @@ Situation 3. `Q : Combien de litres dans un gallon?`: Situation 4. `Q : Que fait-on avec des drapeaux usés ou périmés?`: -- Encore une fois, c'est la seule version où la réponse est au début et non à la fin. Ma réponse est équivalente. +- Encore une fois, c'est la seule version où la réponse est au début et non à la fin. Cette réponse est équivalente. #### Fichier de test -Sans apporter aucune modification au code d'origine, j'obtiens 10 erreurs avec le fichier de questions de test. +Sans apporter aucune modification au code d'origine, on obtient 10 erreurs avec le fichier de questions de test. -J'apporte les modifications suivantes pour diminuer le nombre d'erreurs: +On apporte les modifications suivantes pour diminuer le nombre d'erreurs: -- Ajout du mot `profondeur` à la règle 5: enlève une erreur -- J'ajoute un groupe de capture optionnel `est né` à la règle 7, enlève une erreur. -- Séparation de la règle 1 pour le **qui** en deux expression pour gérer les temps de verbe présent et participe passé. La réponse n'est pas au même endroit dans la phrase pour ces deux situations: enlève trois erreurs -- Ajout d'une expression régulière pour gérer `quel est le nom du chef` après celles qui gère le mot-question **qui**: enlève une erreur. -- Je sépare la règle 4 pour gérer `capitale` et `emplacement` séparément. J'enlève deux erreurs dans le fichier de test mais j'ajoute une erreur dans le fichier d'entrainement à la question `Quelle est la capitale de la Yougoslavie?`, car il y a un `à` de trop dans la réponse. +- Ajout du mot `profondeur` à la règle 5: ceci élimine une erreur +- On ajoute un groupe de capture optionnel `est né` à la règle 7, ceci élimine une erreur. +- Séparation de la règle 1 pour le **qui** en deux expression pour gérer les temps de verbe présent et participe passé. La réponse n'est pas au même endroit dans la phrase pour ces deux situations: ceci élimine trois erreurs. +- Ajout d'une expression régulière pour gérer `quel est le nom du chef` après celles qui gère le mot-question **qui**: ceci élimine une erreur. +- On sépare la règle 4 pour gérer `capitale` et `emplacement` séparément. Ceci élimine deux erreurs dans le fichier de test, mais ajoute une erreur dans le fichier d'entrainement à la question: `Quelle est la capitale de la Yougoslavie?`, car il y a un `à` de trop dans la réponse. -J'élimine donc 8 erreurs à l'aide de 3 nouvelles expressions régulières et en améliorant une expression existante. Il subsiste 2 erreurs: +On élimine donc 8 erreurs à l'aide de 3 nouvelles expressions régulières et en améliorant une expression existante. Il subsiste 2 erreurs: Situation 1. `Q : À quelle vitesse est le son?`: -- Même erreur que la situation 2 dans le fichier d'entrainement +- Même erreur qu'à la situation 2 dans le fichier d'entrainement Situation 2. `Q : Combien de cœurs a une pieuvre?`: -- Même erreur que la situation 3 dans le fichier d'entrainement +- Même erreur qu'à la situation 3 dans le fichier d'entrainement ## Origine du nom de famille -Dans cette section, nous allons créer des modèles pour les noms de famille provenant de 18 langues d'origine. Nous disposons de jeux d'entrainement et de test étiquetés, ce qui nous permettra d'analyser la performance de nos différents modèles d'origine. +Dans cette section, on crée des modèles pour les noms de famille provenant de 18 langues d'origine. On dispose d'échantillons d'entrainement et de test étiquetés, ce qui permettra d'analyser la performance des différents modèles d'origine. -Pour chaque origine, nous allons normaliser les noms de famille en les mettant en minuscules et les convertissant en encodage ASCII. Nous allons ensuite construire des unigrammes pour chacun des noms de familles. Ceci nous permettra tout d'abord de disposer de listes de lettres au lieu de mots, ce qui facilitera la création des bigrammes et trigrammes. Celà nous permet aussi de mesurer la taille $V$ du vocabulaire, soit le nombre de caractères différents, incluant un caractère virtuel pour représenter la fin de mot. +Pour chaque origine, on normalise les noms de famille en les mettant en minuscules et les convertissant en encodage ASCII. On construit des unigrammes pour chacun des noms de famille. Ceci permettra tout d'abord de disposer de listes de lettres au lieu de mots, ce qui facilitera la création des bigrammes et trigrammes. Cela permet aussi de mesurer la taille $V$ du vocabulaire, soit le nombre de caractères différents, incluant un caractère virtuel pour représenter la fin de mot. ### Création des $k$-grammes -Pour convertir les mots en unigrammes, on utilise la fonction `list` qui retourne simplement une liste des caractères d'un mot. Puis, pour créer des $k$-grammes, on ajoute $k-1$ caractères de début de mot au début de la liste et $k-1$ caractères de fin de mot à la fin de cette liste. Ensuite, pour créer la liste des $k$-grammes, on en prend les éléments $k$ à la fois et on les joint ensemble dans une seule chaîne de caractères. Soit $U$ la liste des unigrammes, on sélectionne +Pour convertir les mots en unigrammes, on utilise la fonction `list` qui retourne simplement une liste des caractères d'un mot. Puis, pour créer des $k$-grammes, on ajoute $k-1$ caractères de début de mot au début de la liste et $k-1$ caractères de fin de mot à la fin de cette liste. Ensuite, pour créer la liste des $k$-grammes, on en prend les éléments $k$ à la fois et on les joint ensemble dans une seule chaine de caractères. Soit $U$ la liste des unigrammes, on sélectionne: $$U[i] \ldots U[i+k-1], \forall i \in [0,|U|-k]$$ ### Création des modèles On crée un modèle différent pour chacune des origines. Pour chaque nom présent dans la liste associée à une origine, on applique l'opération de normalisation, puis on extrait les unigrammes dans une liste. On accumule ceux-ci dans une liste de listes. -Sur chacun des mots transformés en unigrammes, on développe les $k$-grammes pour $k \in [1,4]$. On effectue le compte de tous les $k$-grammes pour une origine, qu'on stocke dans un dictionnaire, puis on enregistre cette information (qui est notre modèle de langue) dans un dictionnaire de modèles dont la clé est de la forme (origine,$k$). Ceci nous permettra d'accéder aux modèles pour différentes valeurs de $k$ simultanément. +Sur chacun des mots transformés en unigrammes, on développe les $k$-grammes pour $k \in [1,4]$. On effectue le compte de tous les $k$-grammes pour une origine, qu'on stocke dans un dictionnaire. On enregistre cette information (qui est un modèle de langue) dans un dictionnaire de modèles dont la clé est de la forme (origine,$k$). Ceci permettra d'accéder aux modèles pour différentes valeurs de $k$ simultanément. ### Origine la plus probable -Le calcul de l'origine la plus probable d'un nom consiste à évaluer, pour chacune des origines, la probabilité de chacun des $k$-grammes de ce nom. Comme l'évaluation de la probabilité implique la multiplication des probabilités individuelles, des nombre inférieurs à 1, on préfère additionner le logarithme de celles-ci. +Le calcul de l'origine la plus probable d'un nom consiste à évaluer, pour chacune des origines, la probabilité de chacun des $k$-grammes de ce nom. Comme l'évaluation de la probabilité implique la multiplication des probabilités individuelles, des nombres inférieurs à 1, on préfère additionner le logarithme de celles-ci. -Pour calculer cette log-probabilité, on utilise aussi le lissage de Laplace, afin de tenir compte des $k$-grammes qui auraient pu ne pas apparaître lors de la création du modèle. L'équation prend la forme +Pour calculer cette log-probabilité, on utilise aussi le lissage de Laplace, afin de tenir compte des $k$-grammes qui auraient pu ne pas apparaître lors de la création du modèle. L'équation prend la forme: $$ log(p) = \sum_{i=1}^{K} log\left(\frac{C_i+1}{N_i+V}\right) $$ -- $K$ est le nombre de $k$-grammes du nom. -- $C_i$ est la fréquence du $i^{eme}$ $k$-gramme dans le modèle (origine,$k$) -- $N_i$ est la fréquence du $i^{eme}$ ($k-1$)-gramme formé par les $k-1$ premières lettres du $k$-gramme dans le modèle (origine,$k-1$) +- $K$ est le nombre de $k$-grammes du nom; +- $C_i$ est la fréquence du $i^{eme}$ $k$-gramme dans le modèle (origine,$k$); +- $N_i$ est la fréquence du $i^{eme}$ ($k-1$)-gramme formé par les $k-1$ premières lettres du $k$-gramme dans le modèle (origine,$k-1$); - $V$ est la taille du vocabulaire, soit le nombre de caractères observés, plus le caractère de fin de mot. $V=29$ dans ce problème. -On peut aussi utiliser la mesure de perplexité pour évaluer le modèle le plus probable. Dans ce cas, on utilise la mesure suivante, que l'on désire minimiser +On peut aussi utiliser la mesure de perplexité $e^H$ pour évaluer le modèle le plus probable. Dans ce cas, on utilise la mesure suivante, qu'on désire minimiser. $$ -PERP = \sqrt[K]{\prod_{i=1}^{K}\left(\frac{C_i+1}{N_i+V}\right)^{-1}} +e^H = \sqrt[K]{\prod_{i=1}^{K}\left(\frac{C_i+1}{N_i+V}\right)^{-1}} $$ En utilisant les log-probabilités, on obtient $$ -PERP = \exp\left({- \frac{1}{K}\sum_{i=1}^{K} log\left(\frac{C_i+1}{N_i+V}\right)}\right) +e^H = \exp\left({- \frac{1}{K}\sum_{i=1}^{K} log\left(\frac{C_i+1}{N_i+V}\right)}\right) $$ ### Performance des modèles -#### Modèles unigrammes +#### Modèles avec unigrammes -On observe qu'il n'est généralement pas possible de prédire l'origine d'un nom en considérant seulement les lettres du nom. Seul le portugais et le polonais présentent une performance acceptable. +On observe qu'il n'est généralement pas possible de prédire l'origine d'un nom en considérant seulement les lettres du nom. Seuls le portugais et le polonais présentent une performance acceptable pour les modèles avec unigrammes. ![](images/2_seaborn_hm_echec_1.png) -#### Modèles bigrammes +#### Modèles avec bigrammes -Les bigrammes permettent d'identifier correctement les noms de quelques origines avec une bonne fiabilité. La majorité des noms sont bien classés, mais le taux d'erreur est encore élevé pour la majorité des langues. On remarque que certaines langues sont confondues entre elles (anglais-irlandais-écossais; japonais-coréen; vietnamien-chinois). +Les modèles avec bigrammes permettent d'identifier correctement les noms de quelques origines avec une bonne fiabilité. La majorité des noms sont bien classés, mais le taux d'erreur est encore élevé pour la majorité des langues. On remarque que certaines langues sont confondues entre elles (anglais-irlandais-écossais ; japonais-coréen ; vietnamien-chinois). ![](images/2_seaborn_hm_echec_2.png) -#### Modèles trigrammes +#### Modèles avec trigrammes -Les modèles trigrammes ont une très bonne précision pour environ la moitié des origines. On observe ici que certaines origines ne sont plus reconnues du tout, ce qui amplifie le phénomène observé avec les bigrammes. On peut poser l'hypothèse que nous avons aussi vu une quantité beaucoup moins élevé des trigrammes possibles, donc le lissage de Laplace prend beaucoup d'importance et les modèles perdent davantage en précision. +Les modèles avec trigrammes ont une très bonne précision pour environ la moitié des origines. On observe ici que certaines origines ne sont plus reconnues du tout, ce qui amplifie le phénomène observé avec les bigrammes. On peut aussi poser l'hypothèse que, vu une quantité beaucoup moins élevée des trigrammes possibles, donc le lissage de Laplace prend beaucoup d'importance et les modèles perdent davantage en précision. ### Conclusion -Le fait d'utiliser la log-probabilité ou la perplexité n'a pas vraiment d'impact sur les résultats, pour chaque modèle, on observe une différence d'un ou deux noms classés différemment entre les deux mesures. +Le fait d'utiliser la mesure de log-probabilité ou la mesure de perplexité n'a pas vraiment d'impact sur les résultats. Pour chaque modèle, on observe une différence d'un ou deux noms classés différemment entre les deux mesures. -On observe que les bigrammes apportent un gain significatif au niveau de la performance de la classification, mais que les trigrammes ont tendance à faire ressortir certains excellents modèles tout en produisant d'autres modèles plutôt médiocres.. +On observe que les bigrammes apportent un gain significatif au niveau de la performance de la classification, mais que les trigrammes ont tendance à faire ressortir certains excellents modèles tout en produisant d'autres modèles plutôt médiocres. ![](images/2_seaborn_hm_echec_3.png) @@ -163,9 +163,9 @@ On observe que les bigrammes apportent un gain significatif au niveau de la perf ### Description des données -Ce problème consiste à classifier un ensemble de questions en anglais à l'aide d'un ensemble de catégories de réponses. Nous disposons d'un échantillon d'entraînement composé de 5556 questions annotées et d'un échantillon de test composé de 500 questions. +Ce problème consiste à classifier un ensemble de questions en anglais à l'aide d'un ensemble de catégories de réponses. On dispose d'un échantillon d'entrainement composé de 5556 questions annotées et d'un échantillon de test composé de 500 questions. -La distribution des catégories de questions dans l'échantillon d'entraînement est présentée dans le tableau suivant: +La distribution des catégories de questions dans l'échantillon d'entrainement est présentée dans le tableau suivant: ![](images/3_sn_plot_count_category_train.png) @@ -179,24 +179,24 @@ Voici maintenant la distribution pour l'échantillon test ### Algorithme de vectorisation -Afin de classifier les questions, nous allons utiliser des vecteurs de fréquences de mots. Comme l'utilisation de mots rares pourrait créer une forme de surentrainement, et de mots trop fréquents, une instabilité des modèles, nous devons trouver où tronquer la distribution de mots. De plus, nous pouvons nous poser la question sur l'inclusion ou non des mot vides. +Afin de classifier les questions, on utilise des vecteurs de fréquences de mots. Comme l'utilisation de mots rares pourrait créer une forme de surentrainement, et de mots trop fréquents, une instabilité des modèles, on doit trouver où tronquer la distribution de mots. De plus, on doit décider si on inclut ou non les mots vides (*stop words*). Pour ce faire, on effectue une recherche sur une grille composée de ces trois paramètres: - fréquence minimale dans le corpus (1 à 20) - proportion maximale dans le corpus (0,005 à 0,100) - indicateur booléen d'exclusion des mots vides. -Pour chacune des combinaisons, une vectorisation est effectuée, puis un modèle de Bayes naïf est entraîné avec une validation croisée, en conservant le paramètre $\alpha=1$. Le score moyen est ensuite conservé. La combinaison qui présente le meilleur score moyen est conservée et est utilisée pour créer un CountVectorizer. Il est important de noter que l'on considère le vocabulaire comme étant fermé au contenu du corpus d'entraînement. +Pour chacune des combinaisons, on effectue une vectorisation, puis un modèle de Bayes naïf est entrainé avec une validation croisée en conservant le paramètre $\alpha=1$. Le score moyen est ensuite conservé. La combinaison qui présente le meilleur score moyen est conservée puis utilisée pour créer un CountVectorizer. On considère le vocabulaire comme étant fermé au contenu du corpus d'entrainement. -Pour créer ce vocabulaire depuis l'échantillon d'entraînement, on utilise la méthode `fit_transform`. Pour créer les vecteurs de fréquences de l'échantillon de test, il faut éviter de recréer le vocabulaire en utilisant la méthode `transform`. +Pour créer ce vocabulaire depuis l'échantillon d'entrainement, on utilise la méthode `fit_transform`. Pour créer les vecteurs de fréquences de l'échantillon de test, il faut éviter de recréer le vocabulaire en utilisant la méthode `transform`. ### Apprentissage -Les données sont maintenant préparées pour entraîner le modèle de Bayes Naïf. Afin de déterminer le paramètre $\alpha$ optimal, nous allons utiliser recherche sur grille et la validation croisée avec 5 sous-échantillons. Comme nous cherchons un paramètre d'un modèle d'apprentissage, nous pouvons effectuer ces étapes à l'aide d'un objet `GridSearchCV` et de sa méthode `fit`. Avec un score de 0.6834, nous conservons la valeur $\alpha=1$. +Les données sont maintenant préparées pour entrainer le modèle de Bayes naïf. Afin de déterminer le paramètre $\alpha$ optimal, on utilise recherche sur grille et la validation croisée avec 5 sous-échantillons. Comme on cherche un paramètre d'un modèle d'apprentissage, on effectue ces étapes à l'aide d'un objet `GridSearchCV` et de sa méthode `fit`. Avec un score de 0.6834, on conserve la valeur $\alpha=1$. ### Résultats -Le modèle a une exactitude de 0.8362 pour l'échantillon d'entraînement et de 0.6220 pour l'échantillon de test. Nous sommes donc en présence d'une forme de surapprentissage. Considérons aussi que les proportions de chaque étiquettes varient grandement entre les deux échantillons. +Le modèle a une exactitude de 0.8362 pour l'échantillon d'entrainement et de 0.6220 pour l'échantillon de test. C'est une forme de surapprentissage. On considère aussi que les proportions de chaque étiquette varient grandement entre les deux échantillons. Les résultats détaillés sont présentés ci-dessous sous forme de matrices de confusion. @@ -204,12 +204,12 @@ Les résultats détaillés sont présentés ci-dessous sous forme de matrices de ![](images/3_seaborn_df_cm_predict_test.png) -On remarque immédiatement que le modèle ne fais pas la disctinction entre les deux catégories `DEFINITION` et `ENTITY`. Comme ce sont respectivement les catégories les plus fréquentes de l'échantillon d'entraînement et de test respectivement, on explique une bonne partie de la perte de performance. +On remarque immédiatement que le modèle ne fait pas la distinction entre les deux catégories `DEFINITION` et `ENTITY`. Comme ce sont respectivement les catégories les plus fréquentes de l'échantillon d'entrainement et de test respectivement, on explique une bonne partie de la perte de performance. ## Notes -J'ai utilisé quelques librairies additionnelles pour produire les figures présentes dans ce rapport directement depuis les fonctions du problème. Ces librairies sont: +On a utilisé quelques librairies additionnelles pour produire les figures présentes dans ce rapport directement depuis les fonctions du problème. Ces librairies sont: ```python import matplotlib.pyplot as plt @@ -223,8 +223,8 @@ from pandas import DataFrame - [Documentation de Seaborn](https://seaborn.pydata.org) - [Documentation de Python 3.7: The Python Standard Library](https://docs.python.org/3.7/library/) - Stack Overflow: - - [Réponse #7716358](https://stackoverflow.com/a/7716358) - - [Réponse #280156](https://stackoverflow.com/a/280156) + - [Réponse #7716358](https://stackoverflow.com/a/7716358) + - [Réponse #280156](https://stackoverflow.com/a/280156)