Académique Documents
Professionnel Documents
Culture Documents
Luca Massaron
John Paul Mueller
Data science avec Python pour les Nuls
Titre de l’édition originale : Python For Data Science For Dummies, 2nd Edition
Copyright © 2019 Wiley Publishing, Inc.
Pour les Nuls est une marque déposée de Wiley Publishing, Inc.
For Dummies est une marque déposée de Wiley Publishing, Inc.
Édition française publiée an accord avec Wiley Publishing, Inc.
© Éditions First, un département d’Édi8, Paris, 2019.
Éditions First, un département d’Édi8
12, avenue d’Italie
75013 Paris – France
Tél. : 01 44 16 09 00
Fax : 01 44 16 09 01
Courriel : firstinfo@editionsfirst.fr
Site Internet : www.pourlesnuls.fr
ISBN : 978-2-412-05072-9
ISBN numérique : 9782412053461
Dépôt légal : novembre 2019.
Traduction : Olivier Engler
Mise en page : Enredos e Legendas Unip. Lda
Cette œuvre est protégée par le droit d’auteur et strictement réservée à l’usage privé du client. Toute
reproduction ou diffusion au profit de tiers, à titre gratuit ou onéreux, de tout ou partie de cette œuvre est
strictement interdite et constitue une contrefaçon prévue par les articles L 335-2 et suivants du Code de
la propriété intellectuelle. L’éditeur se réserve le droit de poursuivre toute atteinte à ses droits de
propriété intellectuelle devant les juridictions civiles ou pénales.
Ce livre numérique a été converti initialement au format EPUB par Isako www.isako.com à partir de
l'édition papier du même ouvrage.
Introduction
De plus en plus de domaines d’activité trouvent intérêt à exploiter des données informatiques,
et nombreux sont ceux qui en adoptent les techniques sans tambour ni trompette. Vous-même
générez de nouvelles données lors de chacune de vos visites du réseau Internet. La croissance
du réseau Internet est phénoménale, comme le montrent les statistiques
(https://www.internetworldstats.com/emarketing.htm). La science des données (nous
parlerons de datalogie dans ce livre) a pour but de transformer cet incroyable volume de
données en quelque chose d’utile, quelque chose qui puisse vous servir au quotidien pour
réaliser une vaste palette de tâches ou pour bénéficier de services proposés par d’autres, bref
pour transformer ces données en informations.
En fait, vous avez certainement déjà profité de la datalogie sans vous en rendre compte.
Lorsque vous utilisez votre navigateur Web pour chercher un site, vous avez obtenu des
réponses ainsi que des suggestions d’autres critères de recherche. Ces critères sont produits
par la datalogie. Celui qui a rendu visite à son médecin et s’est fait confirmer que cette petite
boule au coude n’était pas une tumeur a profité du fait que le médecin s’est appuyé pour son
diagnostic sur des données scientifiques. Autrement dit, vous manipulez des données au
quotidien sans vous en rendre compte. Ce livre a pour objectif de vous montrer comment
exploiter la datalogie pour réaliser différentes tâches pratiques, mais aussi de faire un tour
d’horizon de ses différents domaines d’emploi. Une fois que vous savez comment résoudre un
problème lié à des données et utiliser cette science, vous obtenez un sérieux avantage ; vous
augmentez ainsi vos chances de promotion si l’avancement de votre carrière vous importe.
Contenu du livre
Le principal objectif de ce livre est de vous libérer de toute angoisse face à cette science des
données. Il va vous montrer qu’il s’agit non seulement d’une science intéressante, mais qu’elle
est tout à fait à votre portée au moyen du langage Python. Vous pourriez croire qu’il faut être
un expert en informatique pour réussir un projet de datalogie, mais c’est absolument faux. Le
langage Python est livré avec une foule de librairies de fonctions qui vont réaliser tout le
travail ardu à votre place. Vous n’allez même pas vous rendre compte de tous les efforts qui
vous seront épargnés, et vous n’aurez pas besoin de vous en soucier. Ce qui vous reste à définir
est l’ensemble des tâches que vous voulez réaliser ; Python se charge de vous les rendre
accessibles.
Le livre prend soin de montrer l’importance qu’il y a à bien choisir ses outils. Vous allez
commencer avec Anaconda, un regroupement de logiciels qui combine IPython et Jupyter
Notebook, deux outils qui simplifient énormément le travail avec Python. Vous allez utiliser
IPython dans un environnement totalement interactif. Le code source que vous allez saisir dans
l’autre outil, Jupyter Notebook (que nous abrégerons en Notebook) aura un aspect visuel bien
peaufiné, et vous permettra de combiner des éléments de présentation dans le même
document que celui contenant le code source. L’approche est un peu différente de celle d’un
environnement de développement intégré EDI. Pour pouvoir expérimenter les exemples du
livre plus facilement dans d’autres plates-formes que Windows, nous présenterons un
environnement de développement appelé Google Colab qui va permettre d’essayer quasiment
tous les exemples du livre depuis une tablette ou (si l’écran n’est pas trop petit) votre
telliphone (smartphone).
Le livre va présenter un bon nombre de techniques intéressantes pour créer des graphiques à
partir de vos analyses au moyen de MatPlotLib, et le livre donne tous les détails pour y
parvenir. Nous avons également expliqué en détail les différentes ressources disponibles, et
notamment les paquetages. Vous verrez comment utiliser Scikit-learn pour effectuer des
traitements de données vraiment étonnants. Nombreux sont par exemple ceux qui aimeraient
savoir comment fonctionne la reconnaissance d’écriture manuscrite. Si vous en êtes, ce livre va
vous mettre le pied à l’étrier dans ce domaine.
Si vous vous inquiétez de la mise en place de cet environnement de travail, ce livre ne vous
laisse pas sur le bord du chemin. Dans les premiers chapitres, vous trouverez des instructions
d’installation complètes pour Anaconda, puis un guide pour bien démarrer en datalogie avec
Jupyter Notebook comme avec Google Colab. Le but est de vous permettre de démarrer le plus
vite possible, en commençant avec des exemples simples pour que le code source ne soit
jamais un obstacle à votre progression.
Tous les exemples du livre utilisent la version 3.x du langage Python, qui est la plus récente.
Pour simplifier l’assimilation des différents concepts, nous avons établi quelques conventions :
» Le texte que vous devez saisir tel quel est présenté dans cette police en gras. La seule
exception concerne le graissage des entrées de liste dans les explications.
» Un mot imprimé en italique dans une séquence à saisir doit être remplacé par une valeur
réelle. Par exemple, lorsque vous lisez « Saisissez nom_util puis frappez Entrée », vous
devez bien sûr remplacer nom_util par votre nom d’utilisateur.
» Les adresses Web et les extraits de code dans le texte sont imprimés en police non
proportionnelle. Si vous lisez une version numérique du livre, vous pouvez visiter les sites
correspondant aux liens présentés dans le texte, comme ceci : http : www.site.com.
» Lorsque vous devez choisir une commande dans un menu, la séquence réalisée est
présentée comme ceci : Fichier/Nouveau.
Cette icône marque une mise en garde ou bien souligne un concept pouvant être à l’origine
d’une équivoque.
Trajectoire de lecture
Vous êtes prêt à vous lancer dans votre découverte de la datalogie avec Python ? Si vous n’avez
encore jamais abordé ce domaine, commencez dans l’ordre avec le Chapitre 1, puis progressez
au rythme qui vous permettra de bien assimiler tout le contenu.
Si vous êtes très impatient de découvrir ce qu’on peut faire avec Python, vous pouvez aller
immédiatement lire le Chapitre 3, tout en sachant que certains éléments mentionnés resteront
obscurs avant un chapitre ultérieur. Si vous avez déjà installé l’environnement Anaconda, vous
pouvez passer directement au Chapitre 5 (mais prenez le temps de survoler le Chapitre 3 pour
découvrir ce que nous présupposons en écrivant ce livre). Si vous comptez utiliser une tablette,
lisez le Chapitre 4 afin de bien connaître les contraintes de l’environnement Web de Google
Colab pour les exemples. Il ne permet en effet pas de les exécuter tous. Rappelons enfin que si
vous installez Anaconda, il est conseillé de disposer au moins de la version 3.6.5 de Python
pour obtenir de bons résultats avec des exemples.
Si vous avez déjà une bonne maîtrise de Python et avez installé Anaconda, vous pouvez ignorer
les premiers chapitres et vous rendre directement au Chapitre 5. Vous reviendrez en arrière si
nécessaire. Prenez le temps d’assimiler chaque technique avant de passer à la suivante. En
effet, chaque exemple de code source, chaque procédure et chaque technique représentent
une leçon indispensable et vous risqueriez de passer à côté d’un élément vital si vous
progressez trop vite.
Les auteurs
John Paul Mueller est auteur de plus de 100 livres techniques abordant les réseaux, la sécurité,
la gestion de bases de données et la programmation.
Luca Massaron est data scientiste spécialisé dans l’organisation et l’interprétation de données
big data pour les transformer en smart data. Il est expert GDE (Google Developer Expert) en
apprentissage machine.
Terminologie française
Tout au long du livre, vous trouverez quelques termes nouveaux. Parcourez le tableau suivant
pour prendre vos repères.
N. d. T. : Il est possible que d’éminents experts du domaine concerné soient inclinés à préférer
d’autres termes français ou à conserver l’anglais. Je serais ravi de lire leurs commentaires à
l’adresse fournie en fin d’introduction.
Contacter le traducteur
Vous pouvez utiliser l’adresse suivante :
firstinfo@efirst.com
en commençant le sujet de votre message par la mention PYDASC.
PARTIE 1
Entrée en datalogie avec Python
V ous pourriez croire que la science des données, que nous appelons datalogie, est une
technologie toute nouvelle pour vous ; c’est faux. Bien sûr, la datalogie suppose d’utiliser
des techniques statistiques sophistiquées et d’exploiter d’énormes volumes de données, le
Big Data. Mais la raison d’être de la datalogie est de vous aider à prendre les meilleures
décisions, à créer des suggestions pour de nouveaux choix basés sur les choix antérieurs,
même à permettre à un robot de reconnaître des objets. En réalité, la datalogie est utilisée
dans un tel nombre de domaines qu’il est impossible d’en trouver un qui ne va pas être impacté
par la datalogie. Autrement dit, la datalogie est cet instrument permettant de profiter des
merveilles de la technologie. Sans datalogie, la plupart des avantages dont vous profitez au
quotidien seraient inaccessibles. C’est une des raisons pour lesquelles un des métiers les plus
attirants en ce début de XXIe siècle est celui de datalogue, ou ingénieur en datascience.
Pour pouvoir pratiquer la datalogie sans être un génie des mathématiques, il faut se munir
d’outils. Il en existe un vaste choix, mais Python est particulièrement approprié pour simplifier
l’exploitation des données grâce à l’existence d’un nombre énorme de librairies de fonctions
mathématiques pour Python. C’est ce qui vous permet de réaliser certaines opérations sans
connaître en détail leur fonctionnement. D’autre part, le langage Python permet d’adopter
différents styles d’écriture (des paradigmes de programmation) ; vous pouvez bien sûr utiliser
un autre langage pour créer des applications de datalogie, mais Python vous simplifie le
travail. C’est donc un choix naturel pour tous ceux qui veulent privilégier l’intelligence au
labeur.
Ce chapitre vous propose un aperçu de ce qu’il est possible de faire avec Python. Il ne s’agit
pas d’un guide d’apprentissage du langage Python ; il va donner quelques conseils de base
dans la perspective de la datalogie. Si vous avez besoin d’une introduction au langage Python,
vous pouvez vous tourner vers le livre correspondant dans la même collection, Python pour les
Nuls.
CHOIX D’UN LANGAGE DE DATALOGIE
En un demi-siècle, ce sont des dizaines d’infolangages (langages de programmation) qui sont apparus,
la plupart étant dédiés à un domaine d’application spécifique, afin de simplifier l’automatisation d’un
ensemble d’activités. Il est donc indispensable de choisir le langage le mieux adapté à vos objectifs.
Un tournevis est préférable à un marteau pour serrer une vis. Les spécialistes de la science des
données ne choisissent que parmi un petit nombre d’infolangages. Voici ceux qui sont les plus utilisés
en datalogie, dans l’ordre décroissant des préférences :
» Python (polyvalent) : nombreux sont les datalogues qui adoptent Python à cause de son vaste
choix de librairies de fonctions, parmi lesquelles NumPy, MatPlotLib et Scikit-learn. Python est un
langage précis qui permet aisément de lancer des traitements en parallèle sur de grands
volumes de données, ce qui réduit les temps de traitement et d’analyse. La communauté des
développeurs en datalogie a produit plusieurs ateliers de développement appelés IDE, et
notamment Anaconda. Le concept de calepin interactif de l’outil Jupyter Notebook incorporé dans
Anaconda simplifie énormément les calculs scientifiques. (Nous verrons comment utiliser Jupyter
Notebook dans le Chapitre 3.) Le langage Python représente en outre un excellent intermédiaire
pour faire coopérer plusieurs langages tels que le C, le C++ ou le Fortran. La documentation de
Python explique comment créer les extensions nécessaires. Python simplifie les fonctions de
détection de motifs remarquables, qui permet par exemple à un robot de détecter un objet à
partir d’un bloc de pixels d’un flux vidéo. Python est également très utilisé dans bien d’autres
domaines scientifiques.
» R (orienté statistique) : ce langage partage de nombreuses caractéristiques avec Python, mais il
s’exprime d’une autre façon. D’ailleurs, certains programmeurs se servent de Python et de R de
façon interchangeable, parfois simultanément. R est fourni avec un environnement spécifique qui
évite d’avoir à installer un atelier tel qu’Anaconda. En revanche, R ne semble pas pouvoir
coopérer aussi bien que Python avec d’autres langages.
» SQL (gestion de bases de données) : le sigle anglais SQL signifie Structured Query Language ;
c’est un langage pour réaliser des requêtes dans des bases de données. Il est donc plus orienté
données que calculs. Les données sont devenues le capital par excellence des entreprises
modernes, et une excellente gestion des données est indispensable. Toute entreprise ayant un
minimum d’envergure se fonde sur des bases relationnelles, qui sont exploitées au moyen du
langage SQL. Les applications correspondantes sont des SGBDR (Système de Gestion de Base de
Données Relationnelle), en anglais DBMS. Le langage SQL offre un certain nombre de fonctions
d’analyse et de traitement des données. Certaines opérations de datalogie seront très
performantes de cette manière puisque l’accès aux données est direct. Les personnes qui se
chargent de la maintenance des bases (les administrateurs de bases de données), se servent
sans cesse du langage SQL pour maintenir leurs bases en bon état, mais vont rarement réaliser
des analyses approfondies. En revanche, les datalogues peuvent se servir du langage SQL pour
réaliser certaines tâches d’exploitation, et créer des scripts qu’ils peuvent ensuite rendre
disponibles aux administrateurs pour leurs propres besoins.
» Java (polyvalent) : certains projets de datalogie supposent de réaliser d’autres genres de
traitements informatiques, pour lesquels il est préférable d’utiliser un langage à usage général
répandu. Le langage Java offre une énorme quantité de librairies de fonctions, et la plupart ne
sont pas du tout orientées datalogie. De plus, c’est un langage qui se distingue par la qualité de
son orientation objet, si on le compare aux autres langages de cette liste. C’est un langage à
typage strict offrant des performances tout à fait correctes. Certains le privilégient donc pour les
versions définitives de leur projet. En revanche, Java n’est pas très pratique pour les phases
d’expérimentation et les interrogations à la volée.
» Scala (polyvalent) : le langage Scala se fonde sur la machine virtuelle JVM de Java ; il partage
donc avec Java certains avantages et inconvénients. Scala se distingue de Java et ressemble plus
à Python par son excellent support de l’approche de programmation dite fonctionnelle fondée sur
le principe de calcul lambda.
Le système de calcul par grappe de processeurs (cluster computing) nommé Apache Spark a été
écrit en langage Scala. Vous disposez donc avec Scala d’un excellent support des traitements
parallélisés, avec d’énormes volumes de données. Un des inconvénients de Scala est sa difficulté
de configuration, sa courbe d’apprentissage pentue et l’absence d’un jeu complet de librairies de
fonctions pour la datalogie.
L’émergence de la datalogie
L’expression « science des données » est assez récente puisqu’elle est apparue en 2001 sous la
plume de William S. Cleveland dans un article célèbre. Ce n’est qu’un an plus tard que le
Conseil International des Sciences a reconnu la science des données en créant un comité
spécifique. L’université de Columbia a rejoint le mouvement en 2003 en lançant la revue
technique Journal of Data Science.
N. d. T. : La palette de techniques utilisées en sciences de données correspond à des
technologies pour transformer des données en informations, un peu comme une distillation
dont on ne conserve que la partie essentielle. L’expression « science des données » étant
malcommode, nous avons songé à trouver un autre terme. Les mots donnéelogie et donnétique
ne conviennent pas (et « donnétique » est déjà utilisé pour la gestion du cycle de vie des
données et leur archivage). Le mot anglais data provient d’un mot latin qui a donné « date » en
français. Il est très connu en langue française et nous proposons donc de l’adopter. Nous
proposons donc le terme « datalogie » , une technologie centrée sur les données.
Les fondements mathématiques de la datalogie existent depuis des siècles, puisqu’il s’agit de
visualiser et d’analyser des statistiques et des probabilités. Le terme « statistique » est apparu
en 1749, mais il en existait sans doute depuis bien longtemps. C’est ainsi que l’historien
nommé Thucydide dans son histoire de la guerre du Péloponnèse avait décrit comment les
Athéniens avaient calculé la hauteur du mur de la ville de Platées au cinquième siècle avant J.-
C. en comptant le nombre de briques d’une section non enduite de la muraille. Pour plus de
certitude, ils avaient pris soin de calculer la moyenne du nombre de briques comptées par
plusieurs soldats.
Les techniques de quantification et de compréhension des statistiques sont plus récentes. Une
description de l’importance des statistiques est apparue dès le neuvième siècle ; un certain Al
Kindi en parlait dans son manuscrit sur le décryptage des messages codés. Il y montrait
comment combiner des statistiques à une analyse de fréquence pour déchiffrer des messages.
Dès leur début, les statistiques ont donc été utilisées pour résoudre des tâches apparemment
impossibles. La datalogie poursuit dans cette voie, et c’est pourquoi certains la comparent à
une sorte de magie.
Visualisation
La phase de visualisation consiste à rendre visibles les motifs remarquables dans les données
afin de pouvoir prendre des décisions. Cela suppose de pouvoir distinguer les données qui ne
font pas partie du motif. Vous pouvez endosser le rôle d’un sculpteur de données qui supprime
les données non pertinentes, aberrantes, pour que les autres puissent admirer la superbe
statue d’informations qui était cachée à l’intérieur. Mais pour que cette œuvre ne soit pas
visible qu’à vos yeux, il faut prévoir de faire un effort de représentation.
Figure 1.1 : Données chargées dans des variables pour pouvoir les manipuler.
Figure 1.2 : Le contenu d’une variable sert à entraîner un modèle de régression linéaire.
Une des raisons pour lesquelles nous avons choisi l’outil Jupyter Notebook dans ce livre est
qu’il permet de créer facilement des représentations visuelles joliment présentées directement
dans l’application. Si vous revenez à la Figure 1.3, vous voyez que la série de valeurs peut être
directement réutilisée par un collègue statisticien. Bien sûr, tout le monde ne peut pas
directement comprendre ce genre de données, mais ceux qui connaissent les statistiques et la
datalogie y trouveront déjà de quoi établir des hypothèses et détecter des tendances.
Chapitre 2
Capacités du langage Python
DANS CE CHAPITRE :
» Origines de Python
Pourquoi Python ?
Au départ, Python est l’œuvre d’une seule personne, le Néerlandais Guido van Rossum. Python
n’est pas vraiment une nouveauté : Guido avait commencé à travailler en décembre 1989 à un
remplacement du langage ABC qui ne lui suffisait pas. Les objectifs exacts qu’il recherchait ne
sont pas tous connus, mais l’un des principaux était de maintenir une compatibilité avec le
langage ABC tout en demandant d’écrire moins de lignes. Le résultat est bien supérieur à ABC
et permet de créer des applications de toutes sortes dans toutes sortes de domaines. Mieux
encore, Python permet d’adopter quatre styles de programmation différents. Bref, Guido est
parti du langage ABC, en a déterminé les limitations et a créé un nouveau langage qui en était
exempt. C’est un bon exemple d’amélioration largement supérieure à son prédécesseur.
Python a beaucoup évolué et offre actuellement deux lignées distinctes. La lignée 2.x maintient
la compatibilité avec les anciennes versions de Python, ce qui n’est pas le cas de la lignée 3.x
qui lui succède. Ce problème de compatibilité doit être pris en compte, y compris en datalogie,
car certaines des librairies requises ne fonctionnent pas encore avec la lignée 3.x, mais cette
contrainte disparaît progressivement. Vous êtes convié à utiliser la lignée 3.x pour tous vos
nouveaux projets parce que la lignée 2.x va finir par être abandonnée (voyez
https://pythonclock.org/ pour tous détails). Ce livre n’utilise que du code pour la lignée 3.x.
Du fait qu’au cours de sa carrière Guido a travaillé pour plusieurs entreprises, les licences des
différentes versions de Python divergent. Cependant, la fondation PSF (Python Software
Foundation) est dorénavant propriétaire des droits de toutes les versions de Python. Vous
n’avez donc pas à vous inquiéter au niveau des droits d’utilisation du langage (sauf si vous
utilisez encore une très ancienne version).
Les infolangages servent à écrire des instructions de traitement de données. L’ordinateur, comme
déjà dit, ne connaît que les zéros et les un du langage binaire. Python est particulièrement bien
adapté aux projets de datalogie, mais il ne convient pas à tous les domaines. Pour chaque tâche à
réaliser, vous devez choisir le langage qui vous semble plus approprié. Dès qu’un langage semble
devenir un obstacle, il faut voir si un autre ne pourrait pas mieux convenir. Les infolangages sont là
pour rendre service aux humains, pas aux ordinateurs.
Philosophie de Python
Guido van Rossum s’était lancé dans la création de Python à ses heures perdues. Il voulait
obtenir un résultat le plus vite possible tout en créant un langage souple, fonctionnant sur
toutes les plates-formes et acceptant des extensions. Python dispose de toutes ces qualités, et
de bien d’autres. Bien sûr, il a fallu faire des choix, par exemple au niveau de la facilité d’accès
aux rouages internes du système. Vous trouverez un historique de Python sur la page wiki
correspondante. Les anglophones pourraient également consulter le site http://python-
history.blogspot.com/2009/01/introduction-and-overview.html.
Un avant-goût du langage
Python a été conçu en sorte de rester facile à lire et relire tout en étant rapide à écrire. Une
seule ligne de code source Python permet de déclencher un volume de traitement pour lequel il
faut plusieurs lignes dans d’autres langages. Voici par exemple comment demander à Python
de faire afficher un petit message à l’écran :
Il s’agit ici d’un appel de fonction valable pour la lignée 3.x. En version 2.x, le programmeur
utilisait une instruction nommée print (sans paire de parenthèses). Dans la lignée 3.x de
Python, si vous écrivez print sans les parenthèses, vous serez sanctionné par un message
d’erreur :
Insérer cette simple fonction permet d’afficher aussi bien du texte que le contenu d’un objet ou
des valeurs numériques, avec une seule instruction. Pour quitter cette session interactive sur
ligne de commande, il suffit de saisir le nom de fonction quit(), puis de valider par la touche
Entrée. Dans ce livre, nous utiliserons un véritable environnement de travail qui porte le nom
Jupyter Notebook. Il permet réellement de charger l’équivalent d’un calepin de travail (un
notebook) dans lequel se trouve le code source.
De l’importance de l’indentation
Le langage Python se distingue de la plupart de ses concurrents par le fait que le nombre
d’espaces en début de chaque ligne joue un rôle sur la logique des programmes, ce qu’on
appelle l’indentation. Les débutants en Python font souvent l’erreur de ne pas indenter
suffisamment les lignes qui doivent être des sous-ensembles d’autres lignes. Nous verrons cela
en pratique plus loin. Sachez dès à présent qu’il faut bien faire attention à l’alignement des
premiers caractères au niveau des colonnes. Par exemple, dans un groupe de lignes qui doivent
être exécutées seulement dans certaines conditions, c’est-à-dire un bloc conditionnel if, les
lignes conditionnelles doivent toutes être décalées vers la droite de deux ou quatre espaces ou
d’un pas de tabulation. Voici un exemple :
print("Indentons bien !")
if 1 < 2:
print("1 vaut moins que 2")
Ci-dessus, la seconde fonction print() doit être décalée par rapport à la ligne précédente
pour que cette instruction d’affichage ne soit exécutée que si la condition testée est satisfaite,
autrement dit, si la valeur 1 est inférieure à la valeur 2 (ce qui est normalement le cas dans
notre univers). En cas d’erreur, vous verrez apparaître un message du compilateur.
Dans ce livre, nous considérons Anaconda comme un produit en soi, avec lequel vous allez travailler.
En réalité, Anaconda est le fruit du regroupement de plusieurs applications open source. Vous pouvez
vous servir de chacune d’elles de façon individuelle dans certains cas. Dans la plupart des chapitres
du livre, nous utilisons l’éditeur Jupyter Notebook, mais il n’est pas inutile de savoir qu’il y a d’autres
applications regroupées sous le terme Anaconda.
La plupart des datalogues se servent aussi d’Anaconda en tant que produit global, mais
certains des logiciels regroupés dans ce paquetage peuvent avoir fait l’objet de mises à jour
plus récemment que le dernier regroupement en date. Par exemple, Jupyter Notebook est
disponible dans une version plus récente que celle du paquetage Anaconda
(http://jupyter.org/). Pour éviter tout souci, vous n’utiliserez pour les exercices de ce livre
que la version de Jupyter Notebook qui est fournie avec la version la plus récente du paquetage
Anaconda, et non la version disponible séparément.
Pour démarrer l’interpréteur Python directement, vous pouvez donc saisir comme commande
dans cette fenêtre le mot python, puis valider par Entrée, ce qui donne le résultat montré dans
la Figure 2.1.
Pour quitter l’interpréteur, vous utilisez la commande quit() en validant par Entrée (nous ne
le préciserons plus). Pour un rappel des options de ligne de commande de l’interpréteur,
saisissez la commande python - ? . La Figure 2.2 montre quelques-unes des options
disponibles pour influer sur l’environnement de travail.
Figure 2.2 : Rappel des options de ligne de commande de l’interpréteur Python.
En faisant exécuter un script au démarrage de l’interpréteur, vous pouvez utiliser une version
personnalisée de l’utilitaire concerné. Tous les scripts sont stockés dans le sous-répertoire
nommé scripts. Par exemple, si vous saisissez la commande suivante :
python Anaconda3/scripts/Jupyter-script.py
vous démarrez l’environnement Jupyter sans avoir besoin de passer par l’interface graphique
de votre système. Vous pouvez même spécifier d’autres options pour modifier le script lui-
même. Voici quelques paramètres que vous pouvez utiliser avec le script pour obtenir des
informations au sujet de Jupyter :
» --version : affiche le numéro de version de Jupyter Notebook.
» --config-dir : affiche le répertoire de configuration de l’outil Jupyter Notebook qui
contient les informations de configuration.
» --data-dir : affiche le répertoire contenant les données d’application Jupyter et non
celui des projets. En général, les projets sont stockés dans votre répertoire d’utilisateur.
» --runtime-dir : affiche le nom du répertoire contenant les fichiers d’exécution de
Jupyter, qui est normalement un sous-répertoire du répertoire data.
» --paths : affiche la liste des chemins d’accès que Jupyter doit consulter.
Pour obtenir la liste des chemins d’accès Jupyter, il suffit donc de taper la commande suivante :
Vous trouverez toute une série de fichiers exécutables dans le même sous-répertoire scripts,
qui sont des versions compilées de certains scripts pour permettre une exécution plus rapide.
Pour démarrer l’environnement du navigateur Jupyter Notebook, saisissez la commande
suivante :
python Anaconda3/scripts/Jupyter-notebook-script.py
Figure 2.3 : L’environnement IPython qui apporte quelques enrichissements par rapport à l’interpréteur Python standard.
Figure 2.4 : L’outil QTConsole permet de contrôler Jupyter dans une interface graphique.
Cet environnement fonctionne sur le même principe que tous les environnements de
développement existants. La partie gauche est réservée à un éditeur pour le code source. Ce
code est sauvegardé dans un fichier script. Il faut donc enregistrer au moins une fois avant de
lancer l’exécution. Dans la partie droite, le volet supérieur propose deux onglets pour inspecter
les objets du programme, explorer les variables et interagir avec les fichiers. Le volet inférieur
droit est celui de la console Python qui donne également accès à un historique des messages et
à la console Jupyter. La partie supérieure correspond à la barre de menus qui permet de piloter
votre environnement.
Les performances et l’exactitude sont deux critères majeurs en datalogie, mais ils sont
divergents : vos décisions doivent les combiner de la façon la plus adéquate à chaque projet.
Capacités de visualisation
Avec Python, vous pouvez explorer l’environnement de données sans avoir à utiliser un outil de
mise au point, comme ce serait le cas avec des langages compilés. Par exemple, la fonction
print() permet de voir le contenu de tous les objets de façon interactive. Vous pouvez donc
charger des données puis jouer avec. En observant vos données sous plusieurs angles, vous
allez trouver de nouvelles approches. À en juger d’après les discussions en ligne, cette
expérimentation avec les données est ce qui rend la pratique de la datalogie particulièrement
agréable.
Parmi les outils réunis dans Anaconda pour jouer avec les données, un des plus intéressants est
l’environnement interactif IPython que nous venons de présenter. Tout ce que vous faites dans
cet environnement est temporaire : vous ne risquez donc pas d’endommager les données. Vous
pouvez donc charger un jeu de données juste pour voir ce qu’il contient, comme le montre la
Figure 2.6. Ne vous inquiétez pas de la signification du contenu de cette fenêtre pour l’instant.
Nous allons commencer à programmer à partir du Chapitre 5.
Figure 2.6 : Exemple de chargement d’un jeu de données pour expérimentation.
Les jeux de données au format Scikit-learn se présentent sous forme de bancs, qui sont des
structures de données. Un tel jeu de données est structuré au moyen de fonctions dans le code,
et ce code définit des clés qui identifient des valeurs, ces valeurs correspondant à des colonnes
de données. Chaque ligne de données correspond à une clé unique, même si les valeurs des
colonnes se répètent dans les lignes. Le jeu de fonctions correspondantes permet de traiter le
jeu de données en fonction des besoins de l’application.
Avant de pouvoir manipuler un jeu de données, il faut pouvoir le rendre accessible dans votre
environnement local. La Figure 2.7 montre un processus d’importation qui utilise la fonction
keys() pour afficher la liste des clés qui permettent ensuite d’accéder aux données du jeu.
Figure 2.7 : Utilisation d’une fonction pour connaître les clés d’un jeu.
Une fois que vous possédez la liste des clés, vous pouvez accéder aux différents éléments de
données. La Figure 2.8 montre tous les noms des caractéristiques d’un jeu de données nommé
Boston. Grâce à Python, vous pouvez récolter suffisamment d’informations au sujet d’un jeu
pour pouvoir vous lancer dans son analyse en profondeur.
La librairie qui porte le nom SciPy propose des routines de traitement numériques, notamment
d’intégration et d’optimisation. C’est une librairie à usage général qui répond à des besoins
variés et assure le support des librairies plus spécifiques telles que Scikit-learn, Scikit-image et
statsmodels.
Certains de ces noms correspondent à un titre d’un des chapitres suivants. Vous pouvez donc
en déduire dès maintenant que la librairie Scikit-learn est la plus importante du livre, même si
elle dépend pour fonctionner de librairies plus élémentaires.
https://pypi.python.org/pypi/beautifulsoup4/
Chapitre 3
Mise en place de l’atelier Python
DANS CE CHAPITRE :
» Obtention d’une solution préconfigurée
our pouvoir pratiquer la datalogie avec Python, il faut d’abord mettre en place un atelier et
P récupérer les jeux de données et les exemples pour mettre en pratique les exercices du livre.
Voyons donc d’abord comment configurer votre machine.
Le livre se base sur la version 5.5.0 de Jupyter Notebook fournie avec Anaconda 5.2.0. La
version de Python concernée est la 3.6.5. C’est celle qui a servi à créer les exemples. Vous
devez donc utiliser une version de Python au moins égale à celle-ci. Pour mettre toutes les
chances de votre côté, utilisez la version qui a été intégrée à Anaconda 3 en version 5.2.0. Les
versions plus anciennes ne vont pas disposer de toutes les fonctions requises et les versions
plus récentes pourraient ne plus être compatibles avec nos exemples. Certains d’entre vous
préféreront un autre outil que Jupyter Notebook. Nous en citerons quelques-uns dans ce
chapitre. Bien sûr, si vous choisissez d’autres outils, les captures d’écran et les descriptions
des chapitres suivants ne correspondront pas. Cela dit, à partir du moment où l’outil choisi
supporte Python en version 3.6.5, les exemples devraient fonctionner de la même façon.
Le fait de télécharger le code source des exemples ne doit pas vous empêcher de vous
entraîner à écrire du code, de le faire analyser par un débogueur, de l’enrichir et de vous
resservir des exemples. Le code vous est fourni déjà saisi pour accélérer votre découverte de
Python et de la datalogie. Une fois que vous avez pris l’habitude de lire ce genre de code
source, vous ne devez pas hésiter à commencer vos propres projets. Vous pouvez même
ressaisir les exemples et comparer avec le code source récupéré afin de repérer vos erreurs
éventuelles. Le code source de la totalité de ce chapitre se trouve dans le fichier nommé
PYDASC_03.ipynb. Le jeu de données correspondant se trouve dans le fichier
PYDASC_03_Dataset_Load. (Nous avons indiqué dans l’introduction comment récupérer le
fichier compressé des exemples.)
https://www.anaconda.com/download
Il est possible qu’il faille se rendre à la page https://repo.anaconda.com/archive pour
récupérer la version 5.2.0 qui n’est plus la plus récente à l’heure où vous lisez ceci. Choisissez
l’un des liens vers une version 3.6 de Python. Le fichier récupéré doit commencer par la
mention Anaconda3-5.2.0- suivie du nom de la plate-forme et d’une mention _32 bits ou _64
(bits). Le fichier s’appellera par exemple Anaconda3-5.2.0-Windows-x86_64.exe pour la
version Windows 64 bits. Voici les systèmes d’exploitation reconnus par Anaconda :
» Windows, en 32 bits et en 64 bits (il est possible que l’installateur ne propose qu’une des
deux versions s’il détecte celle applicable).
» Linux, en 32 bits et en 64 bits ;
» MacOS X, en 64 bits.
La version téléchargée par défaut contient Python en version 3.6, celle que nous avons utilisée
dans le livre. Si vous avez de bonnes raisons de diverger, vous pouvez aussi choisir d’installer
Python dans l’ancienne version 2.7 en utilisant le lien approprié. Aussi bien sous Windows que
sous MacOS X, vous disposez d’un installateur à interface graphique. Sous Linux, vous aurez
recours à l’utilitaire bash sur ligne de commande.
Nous déconseillons bien sûr d’installer Anaconda avec une ancienne version de Python, de la
lignée 2.x.
Vous n’avez pas besoin d’autres produits que ce paquetage gratuit dans ce livre. Lors de votre
navigation sur le site Anaconda, vous allez sans doute voir de nombreux autres produits
complémentaires, qui permettront de créer des applications fiabilisées. Vous pouvez par
exemple ajouter Accelerate pour mieux exploiter des machines multicœurs et activer les
traitements sur le processeur graphique GPU. Nous n’abordons pas ces extensions dans ce
livre, mais vous trouverez tous les conseils appropriés sur le site Anaconda.
Lorsque vous lancez le téléchargement, vous verrez apparaître un formulaire qui vous invite à
fournir des informations à votre sujet. Sachez que le téléchargement fonctionne même si vous
ne saisissez pas vos données.
Un des points forts de Canopy Express est que la société Entought assure un excellent support
aussi bien pour les étudiants que pour les formateurs. Des formations en ligne sont disponibles
pour apprendre à utiliser l’outil (https://training.enthought.com/courses). Vous trouverez
également des formations spécifiques aux activités de science de données.
Le paquetage WinPython
Comme son nom l’indique, ce paquetage n’est disponible que sous Windows. Vous pouvez vous
le procurer à l’adresse http://winpython.github.io. Le produit supporte Python à partir de
la version 3.5. Il s’agit d’une reprise en main du produit Python(x,y), un atelier dont le
développement a été stoppé en 2015.
L’avantage de ce produit est sa grande souplesse, mais c’est au détriment de son ouverture en
termes de plates-formes. Les programmeurs qui ont besoin de jongler avec plusieurs versions
du même atelier trouveront un intérêt à WinPython. Si vous optez pour ce produit, soyez très
vigilant au niveau des configurations, car le code des exemples risque sinon de ne pas
fonctionner.
Figure 3.1 : Première étape de l’installation d’Anaconda qui permet de savoir si vous êtes en 64 bits.
4. Cliquez le bouton d’acceptation pour montrer que vous êtes d’accord avec les
conditions de licence (I agree).
La Figure 3.2 montre qu’il vous est alors demandé quel type d’installation vous voulez
réaliser. En général, il suffit d’installer le produit seulement pour vous. Vous choisirez l’autre
option si d’autres comptes utilisateurs sur la même machine doivent pouvoir accéder à
Anaconda.
Vous pouvez opter pour un autre environnement de développement pour profiter des fichiers de
Jupyter Notebook. Dans le livre, toutes les captures qui montrent une fenêtre d’édition ont été faites
avec Anaconda. Le fait que nous ayons choisi Anaconda ne signifie pas qu’il s’agisse du meilleur
environnement de développement, ni que les auteurs de ce livre le privilégient. Il se trouve
qu’Anaconda fonctionne bien pour ce genre de tutoriel.
Le nom de l’environnement graphique, Jupyter Notebook, est le même quelle que soit la plate-
forme avec laquelle vous travaillez. S’il y a des différences, elles sont négligeables et vous
pourrez les ignorer. Cela dit, les captures d’écran ont été pour la plupart réalisées sous
Windows. De légères différences sont à prévoir si vous travaillez sous Linux ou sous MacOS X.
Si vous ne voyez pas d’icône pour lancer l’outil, vous pouvez démarrer Jupyter Notebook ainsi :
1. Ouvrez une fenêtre de commande Anaconda Prompt, ou bien une fenêtre de
terminal.
Observez l’apparition de la fenêtre en mode texte, dans laquelle vous allez pouvoir saisir
une commande.
2. Naviguez dans les répertoires jusqu’à atteindre celui nommé \Anaconda3\Scripts
(utilisez des barres obliques à droite sous Linux et MacOS).
En général, vous disposez de la commande cd pour changer de répertoire.
3. Saisissez la commande suivante :
Vous devez voir apparaître une page dans votre navigateur avec Jupyter Notebook.
Figure 3.6 : Création d’un dossier pour tout le code des exemples du livre.
Principe du calepin
Un calepin équivaut à un dossier dans lequel vous pouvez réunir plusieurs fichiers d’exemple.
À chaque exemple correspond une cellule. Le dossier peut contenir d’autres types d’éléments
(images, textes explicatifs), comme nous le verrons au fur et à mesure de la lecture du livre.
Figure 3.7 : Aspect d’un calepin de Notebook avec une seule cellule.
4. Donnez à ce calepin le nom suivant sans oublier de valider par la touche Entrée :
PYDASC_03_Test
Le nom proposé vous permet immédiatement de savoir qu’il s’agit du calepin des exemples du
Chapitre 3 de ce livre, avec une mention comme quoi il ne s’agira ici que d’un simple test.
Notez qu’un fichier de calepin va toujours porter l’extension .ipynb mais que nous n’avez pas
besoin de la saisir.
En commençant la saisie par un seul signe #, vous créez un titre de premier niveau ; avec
deux signes #, ce sera un second niveau, etc. Le texte qui suit le signe correspond au
contenu du titre. En utilisant la commande Run, vous faites comprendre ce que vous avez
saisi comme un titre (Figure 3.9). Vous remarquez que l’outil insère automatiquement une
nouvelle cellule pour poursuivre votre travail.
Figure 3.9 : Insertion d’un titre pour documenter le code.
Vous constatez que ce codage a été interprété comme sous-titre, puisque l’affichage utilise
une police légèrement plus petite que celle du titre principal.
4. Procédez de même pour saisir le sous-type de niveau 3 suivant :
Vous avez maintenant inséré une hiérarchie à trois niveaux. Cette structuration permettra
de retrouver facilement les différents blocs de code. Vous remarquez que l’outil crée à
chaque fois une nouvelle cellule, en choisissant d’office le genre de cellule Code, qui est
celui permettant de saisir du code source. Après tout, c’est votre activité principale avec cet
outil.
5. Saisissez donc l’instruction Python suivante puis cliquez la commande Run :
Vous remarquez que le code est en couleur, ce qui confirme que l’outil a reconnu la fonction
print() et l’a distinguée des données que vous lui fournissez (Python est vraiment
sympa). Le résultat est visible dans la Figure 3.10. Notez que le résultat affiché fait partie
de la même cellule que le code qui en est à l’origine. Notebook sépare visuellement le
résultat du code pour plus de confort. Bien sûr, une nouvelle cellule est ajoutée d’office à la
suite.
Il faut penser à refermer correctement votre outil dès que vous avez terminé de travailler dans
un calepin. Pour quitter un calepin, choisissez la commande File/Close and Halt
(Fichier/Fermer et stopper). Vous revenez à la page présentant la liste du contenu du
dossier PYDASC. Elle ne devrait montrer que le calepin que vous venez de créer (Figure 3.11).
Pour sauvegarder l’état actuel du calepin, utilisez la commande File/Save and checkpoint
(Fichier/Enregistrer et checkpoint).
Figure 3.12 : Demande de confirmation avant suppression d’un calepin dans le référentiel.
Figure 3.13 : Sélection des fichiers à télécharger pour les ajouter au référentiel.
La technique de chargement est la même pour tous les jeux de données. Voici par exemple
comment charger les données immobilières de Boston. Le code correspondant se trouve dans
le calepin nommé PYDASC_03_Dataset_Load.
Vous pouvez voir immédiatement fonctionner le code en utilisant la commande Run Cell. La
fonction d’affichage print() indique 506, 13, comme le montre la Figure 3.14. Patientez
quelques secondes que le jeu de données entier soit chargé.
Figure 3.14 : Chargement des données de l’objet Boston.
Chapitre 4
Google Colab
DANS CE CHAPITRE :
» Présentation de Google Colab
Si votre navigateur Web favori est Firefox, il est possible que vous voyiez apparaître un message
d’erreur lors de l’accès à Colab. Le contenu initial du message suggère de modifier les cookies, et en
accédant aux détails, vous apprenez qu’il s’agit d’une opération non sûre. Si vous acceptez le
message par OK, vous aurez l’impression que Colab fonctionne correctement, mais l’exécution du
code ne sera pas possible.
Vérifiez d’abord que votre Firefox est récent. Vous pouvez ensuite résoudre le problème en donnant la
valeur True au paramètre network.websocket. allowInsecureFromHTTPS dans About : Config,
mais cela ne suffit pas toujours. Vérifiez alors que votre Firefox autorise les cookies tiers en activant
tous les cookies tiers ainsi que, si elle est proposée, l’option Données des sites. De plus,
choisissez de conserver les historiques dans la section Historique de la page Vie privée et
sécurité des options. Redémarrez Firefox après chaque modification, puis tentez d’accéder à Colab.
Si rien n’y fait, vous devez utiliser le navigateur Chrome pour travailler avec Colab.
Google Colab ne prétend pas fonctionner avec d’autres navigateurs que Chrome et Firefox.
Vous risquez de rencontrer des erreurs et une absence d’affichage si le navigateur avec lequel
vous voulez accéder à Colab ne convient pas. Si vous utilisez Firefox, revoyez le texte encadré
qui montre comment faire fonctionner Colab. Voyez aussi la section sur le support local
d’exécution un peu plus loin. Les efforts de configuration que vous aurez à déployer dépendent
du niveau de fonctions dont vous avez besoin. La plupart des exemples fonctionnent
correctement dans Firefox sans aucune modification.
Figure 4.2 : Page d’accès aux fonctions principales et à votre disque réseau.
Cliquez dans le nom temporaire attribué au fichier pour le rendre plus suggestif comme nous
l’avons fait avec Notebook. Pour faire exécuter le code d’une cellule, vous cliquez dans l’icône
de flèche pointant vers la droite sur le bord gauche de la cellule. L’exécution ne provoque pas
nécessairement la sélection de la cellule suivante. Dans ce cas, cliquez pour la sélectionner ou
bien utilisez les boutons de navigation de cellule en cellule de la barre d’outils.
Enregistrer un calepin
Les calepins que vous créez et modifiez dans Colab peuvent être enregistrés à plusieurs
emplacements, mais pas sur votre disque local. Une fois qu’un calepin est dans Google Drive
ou dans GitHub, Colab gère le fichier dans le nuage. Pour rapatrier la plus récente version sur
votre disque local, il faut procéder au téléchargement comme nous l’expliquons dans une
section ultérieure. Découvrons les différentes options pour enregistrer un calepin sur un site
distant.
Vous pouvez à tout moment créer une copie de sécurité de votre projet au moyen de la
commande Fichier/Enregistrer une copie dans Drive. La mention « copie » est ajoutée à
la fin du nom du fichier. Vous pourrez changer ce nom ensuite. Colab stocke cette copie dans le
dossier de Google Drive actuel.
Au départ, vous n’avez pas de référentiel disponible. Vous devez soit en créer un, soit accéder
à un référentiel existant. Dès que le fichier est enregistré, il apparaîtra dans le référentiel avec
un lien pour l’ouvrir dans Colab, à moins de ne pas accepter cette option.
Figure 4.9 : Les cellules de code dans Colab possèdent une petite barre d’outils du côté droit et un menu du côté gauche.
Voyons les options du petit menu local (non ouvert dans la Figure 4.9).
» Lien vers la cellule (Link to cell) : affiche une boîte contenant un lien avec
lequel vous pouvez accéder à une cellule du calepin. Vous pouvez copier le lien dans une
page Web ou dans un autre calepin pour offrir un accès direct. Votre destinataire verra la
totalité du calepin, mais n’aura pas besoin de chercher la cellule dont vous voulez discuter.
» Supprimer la cellule (Delete cell) : Supprime la cellule du projet.
» Supprimer l’élément de sortie (Clear output) : efface les informations affichées en
sortie d’exécution. Vous devez relancer cette exécution pour générer une nouvelle sortie.
» Afficher les éléments de sortie en mode plein écran (View Output
Fullscreen) : affiche sur l’écran la totalité de la sortie produite par l’exécution de la
cellule, ce qui est très pratique lorsque le résultat généré est volumineux ou lorsque vous
voulez mieux voir un graphique qui
a été produit. Vous sortez du mode plein écran par la touche Échap.
» Ajouter un commentaire (Add comment) : ajoute une bulle de commentaires du côté
droit de la cellule. Cela n’est pas la même chose qu’un commentaire dans le code source
qui lui est inséré dans le texte et concerne toute la cellule. Ces commentaires peuvent être
modifiés, supprimés ou résolus. Un commentaire est résolu lorsqu’il a été pris en compte et
n’est plus applicable.
» Ajouter un formulaire (Add a form) : insère du côté droit du code dans la même
cellule un formulaire qui permet d’ajouter une section graphique pour des paramètres. Les
formulaires ne sont pas affichés dans le calepin, mais ils ne vont pas empêcher la
réutilisation du même code source dans l’outil Notebook, grâce à la façon dont ils sont
stockés (pour plus de détails à leur sujet, voyez la page
https://colab.research.google.com/notebooks/forms.ipynb).
En marge gauche, vous disposez de l’icône pour exécuter la cellule de code. Dans la marge du
résultat généré, une icône permet d’effacer cette sortie (Figure 4.10).
Figure 4.10 : Une cellule de code Colab comporte des icônes en marge gauche.
L’accélération matérielle
Le code dont vous lancez l’exécution dans Colab est traité sur un serveur Google. Votre
ordinateur ou votre tablette ne fait qu’afficher le code source ainsi que les résultats qui sont
envoyés dans le navigateur. Quelle que soit la puissance matérielle dont vous disposez
localement, elle n’est pas exploitée, à moins d’activer une option à cet effet.
Vous disposez d’une option dans Colab accessible par la commande Modifier/ Paramètres du
notebook (Notebook settings). La boîte de dialogue qui apparaît (Figure 4.13) permet de
choisir le type du moteur d’exécution Python (option que vous ne modifierez pas), mais
également de sélectionner un processeur local, par exemple le processeur graphique GPU. Un
article en anglais fournit des détails à ce sujet (https://medium.com/deep-learning-
turkey/google-colab-free-gpu-tutorial-e113627b9f5d). N’activez pas cette option avant
d’avoir vérifié que votre ordinateur possède un processeur graphique de puissance suffisante.
L’accélération matérielle locale que permet Colab est en effet limitée, et peut ne pas pouvoir
être utilisée au moment où vous croyez en avoir besoin.
La même boîte de paramètres comporte une petite case à cocher qui peut s’avérer bien utile.
Elle permet de ne pas stocker les données correspondant au résultat généré par l’exécution du
code dans le fichier que vous sauvegardez. Lorsque ces résultats sont volumineux, par exemple
lorsqu’il s’agit d’éléments graphiques, la différence en taille et en temps d’enregistrement et
de rechargement peut être énorme. Le seul inconvénient est qu’il faut regénérer les sorties à
chaque rechargement du calepin.
Exécution du code
Le but de l’outil Colab est bien sûr de permettre d’exécuter le code source que vous rédigez.
Nous avons déjà découvert le bouton d’exécution en marge gauche de la cellule. D’autres
options sont disponibles dans le menu Exécution (Runtime). En voici quelques-unes :
» Exécuter la cellule sélectionnée (Running the current cell) : cette commande
a exactement le même effet que le bouton d’exécution de marge gauche.
» Exécuter les autres cellules (Running other cells) : vous disposez de plusieurs
commandes pour exécuter la cellule précédente, la cellule suivante ou l’ensemble des
cellules sélectionnées.
» Tout exécuter (Running all cells) : vous aurez parfois besoin de lancer l’exécution
en séquence de toutes les cellules de code de votre calepin au moyen de cette commande.
L’exécution reprend à partir de la première cellule de code et progresse jusqu’à la fin du
calepin. Vous pouvez interrompre l’exécution au moyen d’une commande du menu
Exécution.
La commande Gérer les sessions du même menu dresse la liste de toutes les sessions en
cours d’exécution pour votre compte Colab. Vous pouvez ainsi savoir quand vous avez exécuté
pour la dernière fois le contenu d’un calepin et combien d’espace mémoire est occupé. Le
bouton ARRÊTER (Terminate) permet d’interrompre l’exécution d’un calepin et le bouton
FERMER de quitter la boîte de dialogue pour revenir à l’édition.
Figure 4.14 : Commandes du menu d’exécution du code.
Dans les deux cas, vous utilisez le bouton Partager dans l’angle supérieur droit de la fenêtre
Colab pour faire apparaître la boîte de la Figure 4.17.
Figure 4.17 : Boîte pour envoyer un message ou générer un lien de partage d’un calepin.
Dès que vous saisissez une adresse de messagerie ou un nom connu de l’annuaire dans le
champ Utilisateurs, un autre champ apparaît pour saisir une note. Cliquez ensuite le bouton
Envoyer. Si vous utilisez l’option Avancé, une autre boîte apparaît pour choisir quelques
options de partage.
L’angle supérieur droit de la boîte de dialogue comporte un lien Obtenir le lien de partage.
Cliquez-le pour faire générer un lien puis cliquez Copier le lien pour l’envoyer dans le
presse-papiers. Vous pouvez alors coller le lien dans un courriel ou un autre support de
communication.
L’aide de Colab
Colab propose un menu Aide qui contient une commande pour accéder aux questions les plus
fréquentes (FAQ). Il n’y a pas de système d’aide spécifique, mais vous trouverez toute l’aide
requise dans la page d’accueil déjà vue
(https://colab.research.google.com/notebooks/welcome.ipynb). Notez qu’il faut avoir
ouvert une session pour y accéder. Le menu permet aussi d’envoyer un rapport de bogue ou de
donner votre avis.
Une des commandes originales est celle nommée Rechercher des extraits de code (code
snippets). Elle donne accès dans le panneau gauche (Figure 4.18) à toute une série de blocs de
code pouvant être réutilisés moyennant quelques retouches. Vous insérez directement l’extrait
sélectionné au moyen du bouton Insérer. Chaque entrée est accompagnée d’un exemple.
Figure 4.18 : Les extraits de code peuvent faire gagner du temps de codage.
Colab bénéficie d’une bonne communauté d’utilisateurs. Vous pouvez par exemple, si vous avez
quelques connaissances en anglais, poser des questions sur le site Stack Overflow, puisque le
menu Aide offre une commande pour y accéder directement.
PARTIE 2
Plongée dans les données
ans la première partie du livre, nous avons installé les outils permettant d’explorer la
D science des données avec le langage Python. L’heure est venue de voir plus en détail les
outils disponibles dans Anaconda afin de bien les maîtriser.
Nous allons passer en revue les deux outils principaux qui sont la console Jupyter et l’éditeur
Jupyter Notebook. Nous découvrirons quelques-unes des actions élémentaires que vous aurez
besoin de réaliser avec ces outils, et nous en découvrirons bien d’autres dans les chapitres
ultérieurs.
Le code source de tous les exemples du chapitre, comme pour tout le livre, est disponible dans
un fichier d’archive compressé. Pour ce chapitre, le fichier porte le nom PYDASC_05.ipynb.
Cependant, la quantité de texte de ce chapitre est tellement réduite que nous vous conseillons
de saisir le code source tout en suivant les explications.
La console Jupyter
Pour mener des expériences en sciences de données de façon interactive, vous disposez d’une
console Python, à laquelle vous accédez par la commande Anaconda Prompt. Dans cette
fenêtre en mode texte, vous pouvez lancer les commandes et voir immédiatement les résultats.
En cas d’erreur, vous refermez la fenêtre et vous en ouvrez une autre. Cette console permet de
faire des essais et de voir ce qui est possible.
Lorsque vous installez l’environnement normal du langage Python, vous disposez d’une console
Python. Celle que propose anaconda, qui correspond à la commande IPython, lui ressemble
énormément, et permet les mêmes opérations, ainsi que quelques autres en supplément. (La
version anaconda de la console Python sera nommée IPython dans la suite du texte.). Si vous
avez déjà utilisé la console standard Python, aucun mal à vous approprier IPython. Certaines
opérations ne se font pas de la même façon, par exemple pour coller un bloc de texte. Il n’est
donc pas inutile de lire la suite de cette section, même si vous connaissez la console Python
classique.
Deux textes apparaissent pour rappeler le numéro de version de Python et d’Anaconda, donner
quelques premières pistes d’accès à l’aide et rappeler les droits d’auteur et la licence. Vous
pouvez par exemple saisir directement la commande credits et valider pour voir apparaître
une liste de contributeurs à ce produit.
Vous remarquez notamment une ligne qui propose de saisir le signe ? pour accéder à l’aide. Si
vous saisissez ce signe, plusieurs commandes sont proposées :
» ? : aide générale pour utiliser Jupyter.
» nom ? : donne accès à des détails concernant le paquetage, l’objet ou la méthode dont
vous mentionnez le nom avant le signe.
» nom ? ? : plus de détails au sujet du paquetage, de l’objet ou de la méthode mentionné.
» %quickref : affiche des informations au sujet d’une fonction magique de Jupyter.
» help : aide générale au sujet du langage de programmation Python. Notez que les
commandes help et ? se distinguent par le fait que la première concerne le langage
Python, alors que la seconde concerne l’outil IPython.
Si votre système d’exploitation le permet, vous pouvez cliquer-droit dans la barre de titre de la
fenêtre Anaconda Prompt pour voir apparaître un menu local. Ce menu propose un sous-menu
Modifier (ou Edit) qui procure un peu plus de confort pour votre travail en mode texte
(Figure 5.2). Ce menu comporte notamment des commandes pour récupérer la saisie et la
coller par exemple dans un calepin de Notebook.
Figure 5.2 : Menu local en mode texte qui permet de copier/ coller du texte.
N. d. T. : Ce menu local apparaîtra sur votre machine soit en anglais, soit en français. Nous
fournissons les deux versions.
Vous accédez au même menu en ouvrant le menu système du coin supérieur gauche de la
fenêtre et en choisissant Modifier. Voici les commandes les plus fréquemment proposées dans
ce menu dynamique :
» Marquer (Mark) : permet de sélectionner le bloc de texte que vous voulez copier.
» Copier (Copy) : copie le bloc marqué dans le presse-papiers. Vous pouvez également
copier en validant directement par Entrée après avoir sélectionné.
» Coller (Paste) : colle le contenu du presse-papiers dans la fenêtre à la position du
curseur. Cette commande ne fonctionne pas très bien avec la console IPython lorsqu’il y a
plusieurs lignes à copier. Mieux vaut utiliser la fonction magique de l’éditeur %paste pour
copier plusieurs lignes.
» Sélectionner tout (Select all) : sélectionne la totalité du texte visible dans la fenêtre.
» Défilement (Scroll) : permet de faire défiler le contenu de la fenêtre avec les flèches du
curseur. Frappez Entrée pour sortir de ce mode.
» Rechercher (Find) : affiche une boîte permettant de rechercher du texte dans la totalité
de la session correspondant à la fenêtre. Cette commande est très utile, car elle permet de
retrouver un mot que vous avez saisi dans la partie qui n’est plus visible de la fenêtre.
La console IPython offre par rapport à la console Python standard plusieurs améliorations, et
notamment une commande pour effacer l’écran, cls. Il suffit de saisir cette commande et de
valider par Entrée. Si vous avez besoin de réinitialiser l’interpréteur dans la fenêtre, comme si
vous deviez redémarrer le moteur de l’outil Notebook, il suffit de saisir les trois commandes
suivantes :
import IPython
app = IPython.Application.instance()
app.shell.reset()
Ce redémarrage fait revenir à zéro la numérotation, ce qui permet mieux de suivre votre
séquence d’exécution. Lorsque vous avez seulement besoin de réinitialiser toutes les variables,
utilisez la fonction magique %reset.
Figure 5.3 : Boîte des propriétés pour contrôler l’aspect de la fenêtre Anaconda Prompt.
Les options sont réparties en plusieurs pages. Les choix sont faits au niveau de la fenêtre
Anaconda Prompt, mais il s’applique à votre session IPython. Voici à quoi sert chaque des
onglets de la Figure 5.3 :
» Options : permet de contrôler la taille du curseur, un curseur plus large étant préférable
sur fond clair. Vous pouvez régler le nombre de commandes mémorisées et le mode
d’édition (mode insertion ou pas).
» Police (Font) : permet de choisir la police d’affichage du texte. Le choix le plus
confortable est en général Raster fonts. Vous pouvez choisir une autre police si elle vous
semble plus lisible en fonction de vos autres choix.
» Configuration (Layout) : permet de choisir la taille de la fenêtre, sa position à l’écran
et la taille du tampon qui va contenir le texte qui disparaît suite au défilement. Vous
pouvez aussi augmenter la taille de la fenêtre. Pour avoir accès à un historique plus long,
augmentez la taille du tampon.
» Couleurs (Colors) : sert à configurer votre palette de couleurs. La configuration
standard consistant à utiliser un fond noir avec du texte gris n’est en général pas très
lisible. Mieux vaut utiliser un fond blanc et du texte noir. C’est à vous de choisir. Voyez
aussi à ce sujet la fonction magique %colors.
Saisissez le nom d’un objet ou d’une commande pour obtenir de l’aide à son sujet, en validant
par Entrée. Les commandes suivantes donnent accès à plusieurs rubriques :
» modules : dresse la liste des modules actuellement chargés. Le contenu de la liste
dépend de la version de Python, de sa configuration et de vos opérations antérieures.
Sachez que la commande peut demander un peu de temps pour présenter ses résultats qui
peuvent être assez volumineux.
» keywords : rappelle la liste des mots-clés du langage Python. Vous pouvez par exemple
saisir assert pour en savoir plus au sujet de cette commande.
» symbols : présente la liste des symboles spécifiques de Python, par exemple * pour la
multiplication, ou << pour le décalage à gauche.
» topics : présente une liste des thèmes généraux de Python, par exemple, les
conversions. Notez que les titres de rubriques apparaissent en lettres capitales.
Aide d’IPython
IPython procure un second niveau d’aide pour vos objets grâce à sa syntaxe basée sur le signe
? . La mention maListe ? affiche pour l’objet son type, son contenu, sa longueur (empreinte
mémoire) et la chaîne documentaire (docomment ou docstring) associée qui en rappelle les
règles d’utilisation.
En faisant suivre le mot recherché de deux signes ? , vous obtenez plus de détails au sujet de
l’élément désigné. La mention maListe ? ? affiche tous les détails disponibles. Dans certains
cas, il n’y a rien de plus que dans la version résumée. Lorsque le code source de l’élément
interrogé est disponible, IPython l’affiche.
Le style nommé Markdown permet d’enrichir l’aspect des cellules contenant des descriptions.
Choisissez par exemple ce type dans la liste, puis saisissez le titre principal en commençant
bien par un signe # suivi d’une espace :
# Utilisation de Jupyter Notebook
Cliquez ensuite le bouton d’exécution (Run) de la barre d’outils. La cellule prend en compte la
demande d’enrichissement du format. Le signe # isolé demande à Notebook de considérer que
la suite est un titre de premier niveau. Vous remarquez que le fait de lancer l’exécution a
automatiquement inséré une nouvelle cellule présélectionnée. Vous pouvez donc directement
ajouter un sous-titre en choisissant à nouveau le type Markdown dans la liste, puis en saisissant
ceci (Figure 5.7) :
Lancez ensuite l’exécution. Vous constatez que le sous-titre est présenté dans une police plus
petite que le titre principal.
Figure 5.7 : L’ajout de codes de titre et de sous-titre rend la lecture du contenu du calepin plus aisée.
Le type de cellule Markdown permet également d’insérer du contenu au format HTML, par
exemple le contenu complet d’une page Web avec ses balises HTML. Nous pouvons donc
obtenir le même titre de niveau un en insérant le texte suivant :
Dès que vous lancez l’exécution, vous voyez le résultat de l’interprétation. Le code HTML va
d’abord vous servir à ajouter des liens vers le Web. Vous pouvez même insérer ainsi une
illustration. Cette possibilité participe à l’intérêt de l’outil Notebook, qui permet bien plus que
saisir du code source.
Vous disposez enfin d’un type de format brut (RawNBConvert) que nous ne décrirons pas dans
ce livre. Il permet d’insérer du texte littéral qui ne sera jamais traité par le convertisseur de
Notebook. Ce type vous permet notamment d’exporter le contenu d’un calepin dans différents
formats (voir aussi la page https://nbconvert.readthedocs.io/en/latest/). Ce style brut
permet d’inclure du contenu dans un format spécial tel que le format Lamport TeX (LaTeX), un
langage de formatage très utilisé dans le monde scientifique pour contrôler le format visuel
des documents.
Redémarrage du noyau
Au fur et à mesure de votre saisie de code source, vous créez de nouvelles variables, vous
importez des modules et vous réalisez différentes opérations qui altèrent l’environnement. Cela
peut vous amener parfois à ne plus savoir si cet environnement est encore correct.
Pour résoudre ce problème, il suffit de redémarrer le noyau au moyen du bouton montrant une
flèche circulaire (Restart the kernel), mais pas avant d’avoir enregistré le document au
moyen de la commande Save and Checkpoint (Enregistrer avec étape), le bouton qui montre
une disquette carrée. Vous n’avez plus ensuite qu’à relancer l’exécution de la totalité du
contenu pour vérifier que tout fonctionne comme prévu.
Il arrive qu’une erreur grave provoque le blocage du noyau. Vous remarquez par exemple que
les mises à jour sont lentes ou que le contenu du calepin réagit lentement. Dans cette situation
aussi, le plus simple est de redémarrer.
Lorsque vous demandez un redémarrage, un message apparaît (Figure 5.8). Lisez bien ce
message pour ne pas perdre vos dernières retouches à la suite du redémarrage. Pensez à
toujours enregistrer le document avant de confirmer.
Figure 5.8 : Pensez à enregistrer vos documents avant de redémarrer.
Pour restaurer le calepin dans un état antérieur, choisissez la commande File/ Revert to
Checkpoint (Fichier/Restaurer une étape). Vous voyez apparaître la liste de tous les points
d’arrêt disponibles. Sélectionnez-en un. Un message (Figure 5.9) vous demande de confirmer.
En effet, dès que vous validez, les informations qui ont été saisies depuis cette sauvegarde sont
perdues.
Figure 5.9 : Pour annuler une erreur, vous pouvez recharger une version antérieure de votre calepin.
%load https://matplotlib.org/_downloads/pyplot_text.py
Lancez l’exécution de la cellule, et l’exemple est chargé dans la cellule. La commande %load
est neutralisée par mise en commentaire. Vous pouvez immédiatement exécuter l’exemple pour
admirer le résultat graphique.
Dans la première ligne, nous demandons d’importer la classe Image puis nous utilisons des
fonctions de cette classe pour définir ce que nous voulons insérer puis pour réaliser cette
insertion. Le résultat est visible en Figure 5.10.
Si vous voulez que l’image soit mise à jour automatiquement, il faut créer un lien actif. Vous
devrez le réactualiser parce que le contenu du calepin a été importé une fois pour toutes. Pour
insérer un tel lien, vous remplacez la dernière instruction de l’exemple précédent, Embed, par
cette commande :
SoftLinked = Image(url=‘http://blog.johnmuellerbooks.com/wp-content/
uploads/2015/04/Layer-Hens.jpg‘)
Si vous comptez insérer souvent des images, vous aurez intérêt à prédéfinir le format. Par
exemple, si vous voulez souvent insérer des éléments au format PDF, vous pouvez ajouter ces
directives au début de votre projet :
De nombreux formats graphiques sont disponibles, les plus communs étant ‘png’, ‘retina’,
‘jpeg’, ‘svg’ et ‘pdf’.
Les possibilités multimédias de Jupyter Notebook sont très riches puisque vous pouvez même
importer une vidéo en l’insérant directement dans le calepin à l’endroit où vous voulez ajouter
une description. Quelques exemples des possibilités de visualisation sont disponibles à
l’adresse suivante :
http://nbviewer.jupyter.org/github/iPython/iPython/blob/1.x/examples/
notebooks/Part%205%20-%20Rich%20Display%20System.ipynb.
E n choses
sciences des données, les données constituent une donnée incontournable (sic). Les
seraient plus simples si vous pouviez aller dans une boutique de données pour
acheter un bloc de données tout prêt ; vous n’auriez plus qu’à écrire l’application pour
l’analyser. En réalité, les données proviennent de sources très diverses, dans des formats très
divers, et leur interprétation est très variable. Chaque entreprise interprète ses données de
façon particulière et les stocke à sa façon. Même lorsqu’une entreprise utilise le même système
pour gérer ses données qu’une autre, il y a peu de chances que les données se présentent dans
le même format et même qu’elles utilisent les mêmes types. Autrement dit, avant de pouvoir
commencer à faire de la datalogie, vous devez d’abord découvrir comment accéder aux
données que vous cherchez, parmi tous les formats possibles. La conformation des données
d’entrée est un véritable travail, mais heureusement, Python sait répondre à vos besoins.
Dans ce chapitre, nous allons découvrir quelques techniques d’accès aux données dans
différents formats et depuis différents gisements. Les flux mémoire sont le mode d’exploitation
de données que tout ordinateur supporte dès le départ. Les fichiers plats existent tant sur vos
disques personnels qu’à distance. Les bases de données, toujours constituées de plusieurs
fichiers, sont situées en général à distance, mais il existe de petits moteurs de bases de
données (SGBD) comme Access, qui peuvent être implantés localement. Enfin, les données
Web sont évidemment accessibles via Internet. D’autres gisements existent, par exemple les
terminaux de point de vente TPV (POS, Point Of Sale), mais nous ne les aborderons pas ici. Un
livre entier pourrait être écrit au sujet des sources de données et de leur format. Les
techniques que nous allons présenter permettent de vous adapter aux formats que vous allez
rencontrer le plus souvent dans votre vie de datalogue.
La librairie d’apprentissage Scikit-learn comporte un certain nombre de jeux de données
d’essai pour l’apprentissage avec lesquels vous pouvez vous entraîner. Ils sont suffisamment
riches pour permettre d’envisager un certain nombre de tâches, et notamment faire des
expérimentations Python dans le domaine de la datalogie. Ces données sont disponibles dès le
départ, et pour ne pas travailler avec des exemples trop complexes, nous allons choisir de les
adopter dans ce livre dans de nombreux exemples. Bien que ce soit des jeux d’essai, nous
sommes en mesure de présenter à travers eux suffisamment de techniques réutilisables avec
des données réelles.
Si vous ne voulez pas saisir le code source des exemples du chapitre, utilisez le calepin
correspondant au fichier PYDASC_06. Nous avons indiqué dans l’introduction comment
récupérer le fichier archive des exemples.
Le répertoire (dossier) de travail des exemples doit contenir également les cinq fichiers de
données suivants :
» Colorblk.jpg
» Colors.txt
» Titanic.csv
» Values.xls
» XMLData.xml
Ces cinq fichiers doivent être placés dans le même sous-répertoire que les fichiers de vos
calepins Notebook. Dans le cas contraire, vous subirez des erreurs d’entrée-sortie en essayant
d’exécuter les exemples. L’emplacement de votre sous-répertoire de travail varie selon la plate-
forme. Sous Windows, vos fichiers d’exemples seront stockés dans votre répertoire de travail, C
: \Users\NomUtil\ PYDASC (si vous avez préparé l’environnement comme demandé dans le
Chapitre 3). Procédez donc à la copie des fichiers de données associées depuis le répertoire
contenant les fichiers décompressés vers votre répertoire de travail.
L’exemple va utiliser des fonctions prédéfinies de Python (fonctions standard). Quel que soit
son type, lorsque vous chargez un fichier en mémoire, toutes les données sont disponibles en
permanence, et le chargement est assez rapide si le fichier n’est pas trop volumineux. Voyons
un premier exemple d’utilisation de cette technique.
Nous utilisons d’abord la méthode standard nommée open() pour récupérer un objet du type
file. Cette méthode attend en paramètres le nom du fichier, puis le mode d’accès désiré qui
est ici le mode lecture, r (Read). À la fin de la deuxième ligne, nous utilisons la méthode de
lecture open_file.read() de notre nouvel objet de type fichier pour lire la totalité du
contenu. Nous aurions pu ajouter un paramètre de taille lors de l’appel à la méthode, par
exemple read(15). Dans ce cas, Python n’aurait lu que les 15 premiers caractères, ou bien
jusqu’à atteindre la fin du fichier EOF (End Of File). L’exécution de ce premier exemple affiche
ceci :
Contenu de Colors.txt:
Couleur Valeur
Rouge 1
Orange 2
Jaune 3
Vert 4
Bleu 5
Pourpre 6
Noir 7
Blanc 8
Cette opération demande de charger la totalité du contenu du fichier en mémoire vive. S’il n’y
a pas assez d’espace disponible en mémoire, le processus va évidemment échouer. Dans ce cas,
vous devrez adopter une autre technique pour accéder aux données, par exemple avec un flux
ou en réalisant un échantillonnage préalable. Autrement dit, n’utilisez pas la présente
technique si vous n’êtes pas sûr que le volume de données puisse être stocké en mémoire vive.
Ce genre de souci n’est pas à craindre avec les jeux d’essai de la librairie Scikit-learn.
Comme dans le précédent exemple, nous partons du fichier local nommé Colors. txt. Ce
fichier contient une ligne d’en-tête puis un certain nombre d’enregistrements qui établissent
une équivalence (mapping) entre un nom de couleur et une valeur numérique. L’objet fichier
nommé open_file contient un pointeur permettant de manipuler le fichier ouvert.
La deuxième et la troisième ligne de code réalisent la lecture des données dans une boucle de
répétition for. Lors de chaque tour de boucle, le pointeur fichier désigne la ligne suivante et
chaque ligne ou enregistrement apparaît seul dans la variable observation. Il ne reste plus
qu’à afficher la valeur qui se trouve dans observation avec un appel à print(). Voici le genre
d’affichage résultant :
Dans cet exemple, Python ne lit qu’un seul enregistrement à la fois depuis un flux. Cela vous
oblige donc à effectuer autant d’actions de lecture qu’il y a d’enregistrements.
Échantillonnage de données
Lorsque vous chargez une source de données sous forme de flux, vous obtenez graduellement
la totalité des enregistrements, mais parfois vous n’en voulez qu’une partie. En filtrant dès
l’entrée, vous économisez du temps et des ressources. Vous pouvez par exemple demander à
ne lire que chaque cinquième enregistrement, ou en lire certains choisis de façon aléatoire.
L’exemple suivant montre comment lire uniquement les enregistrements pairs dans le fichier
d’entrée :
n = 2
with open(“Colors.txt”, ‘r’) as open_file:
for j, observation in enumerate(open_file):
if j % n == 0:
print(‘Ligne lue: ‘ + str(j) +
‘ Contenu: ‘ + observation)
La différence entre cette version et la précédente est que nous utilisons la méthode
enumerate() pour récupérer le numéro de ligne. Un test utilisant la fonction modulo % permet
de vérifier si le reste de la division entre le numéro de ligne et la variable n est égal à zéro (if
j % n == 0). Si c’est le cas, nous voulons récupérer cet enregistrement et nous l’affichons.
L’exécution de l’exemple donne ceci :
Pour utiliser les fonctions aléatoires, il vous faut importer la classe nommée random. Elle
définit une méthode portant le même nom random() qui génère une valeur numérique entre
zéro et un. La valeur exacte est très difficilement prévisible, donc quasiment aléatoire. Notre
variable sample_size contient cette valeur qui permet de définir la taille de l’échantillon. Par
exemple, une valeur égale à 0.25 ne sélectionne que 25 % des lignes du fichier.
Le résultat reste trié dans l’ordre d’origine. Vous ne verrez pas la ligne du vert apparaître
avant celle de l’orange. En revanche, les éléments affichés changent d’une exécution à la
suivante. Voici un exemple de ce que cet exemple va afficher :
image = img.imread(“Colorblk.jpg”)
print(image.shape)
print(image.size)
plt.imshow(image)
plt.show()
Nous procédons ensuite à la préparation de l’image avec imshow(), puis nous appelons
plt.show() pour afficher l’image (revoir la Figure 6.2). Ce n’est qu’une des nombreuses
méthodes disponibles pour manipuler une image dans Python en préparation de son analyse.
import pandas as pd
tabcoul = pd.io.parsers.read_table(“Colors.txt”)
print(tabcoul)
Nous commençons par importer la librairie pandas dont nous avons besoin. Nous appelons
ensuite la méthode read_table() pour charger le contenu du fichier mentionné dans la
variable portant le nom tabcoul. Il ne reste plus qu’à fichier le contenu de cette variable au
moyen de la fonction print(). Voici le résultat qui apparaît :
Couleur Valeur
0 Rouge 1
1 Orange 2
2 Jaune 3
3 Vert 4
4 Bleu 5
5 Pourpre 6
6 Noir 7
7 Blanc 8
Vous constatez que l’analyseur a correctement compris que la première ligne correspondait au
nom des colonnes. Les huit enregistrements sont ensuite numérotés de 0 à 7. La méthode
read_table() accepte d’autres paramètres pour régler le fonctionnement de l’analyseur, mais
la configuration par défaut suffit ici. Pour en savoir plus au sujet de ces paramètres, voyez la
page http://pandas.pydata.org/pandas-
docs/version/0.23.0/generated/pandas.read_table.html.
La Figure 6.3 montre l’aspect de ce format pour le fichier nommé Titanic.CSV. Vous pouvez
afficher ce contenu dans n’importe quel éditeur de texte.
Figure 6.3 : Aspect brut d’un fichier CSV qui reste assez lisible.
Les tableurs comme Excel savent bien sûr importer un fichier au format CSV. La
Figure 6.4 montre le même fichier que la précédente une fois le fichier chargé dans une page
Excel.
Figure 6.4 : Un tableur tel qu’Excel sait interpréter un fichier au format CSV.
Vous constatez dans la première ligne qu’Excel a bien compris que la première ligne était celle
des en-têtes. Cela vous permet directement de lancer un tri sur les données en sélectionnant
une des colonnes dans l’en-tête. La librairie pandas sait correctement exploiter le format CSV,
comme le montre cet exemple :
import pandas as pd
titanic = pd.io.parsers.read_csv(“Titanic.csv”)
X = titanic[[‘age’]]
print(X)
Vous constatez que nous avons choisi comme analyseur read_csv() , fonction appropriée à ce
format qui permet de spécifier de nouvelles options. (Cet analyseur est détaillé à l’adresse
http://pandas.pydata.org/pandas-
docs/version/0.23.0/generated/pandas.read_csv.html). Pour sélectionner un champ, il
suffit d’indiquer son nom. L’exécution de cet exemple donne le résultat suivant en demandant
d’afficher les âges (la partie centrale du résultat a été tronquée pour économiser de la place) :
age
0 29.0000
1 0.9167
2 2.0000
3 30.0000
4 25.0000
5 48.0000
6 63.0000
...
1305 9999.0000
1306 26.5000
1307 27.0000
1308 29.0000
[1309 rows x 1 columns]
Cet affichage est facile à lire pour un humain, mais ce n’est pas le plus intéressant dans un
traitement. Vous pouvez générer une liste mieux exploitable en modifiant la troisième ligne de
l’exemple ainsi :
X = titanic[[‘age’]].values
Nous utilisons maintenant la propriété values. Le résultat devient le suivant, toujours après
suppression d’un grand nombre de lignes (suppression symbolisée par la ligne ...) :
[[29. ]
[ 0.91670001]
[ 2. ]
...
[26.5 ]
[27. ]
[29. ]]
Lorsque vos données d’entrée sont dans un format de bureautique, par exemple Excel de
Microsoft Office, vous devez tenir compte d’une certaine complexité. Vous devez par exemple
dans le cas d’un fichier Excel indiquer à la librairie pandas laquelle des feuilles il faut lire, s’il y
en a plusieurs. Cela dit, vous pouvez même travailler avec plusieurs feuilles. Pour les autres
logiciels Office, vous devez fournir de nombreux détails. Vous ne pouvez pas être approximatif
avec la librairie pandas. Voici comment exploiter le contenu d’un fichier Excel nommé
Values.xls :
import pandas as pd
xls = pd.ExcelFile("Values.xls")
trig_values = xls.parse(‘Sheet1’, index_col=None, na_values=[‘NA’])
print(trig_values)
Nous commençons par demander l’importation de la librairie pandas puis créons un pointeur
vers le fichier Excel au moyen de la méthode constructeur nommée ExcelFile(). Le pointeur
est incarné par la variable xls. Nous pouvons ainsi choisir une feuille, choisir la colonne
servant d’index et préciser quoi faire avec les valeurs vides/absentes. La colonne d’index est
celle qui sert à trier les enregistrements. Si vous fournissez la valeur None, cela signifie que la
librairie doit générer un nouvel index. La méthode parse() sert à récupérer les valeurs
sélectionnées. Les options Excel pour cette méthode sont disponibles à l’adresse
https://pandas.pydata.org/pandas-
docs/stable/generated/pandas.ExcelFile.parse.html.
Il n’est pas obligatoire de travailler en deux étapes (obtention d’un pointeur sur le fichier puis
analyse des données). Voici comment combiner les deux étapes :
En général, mieux vaut travailler en deux étapes, notamment dans des fichiers Excel, car cela
vous évite notamment de fermer et d’ouvrir le fichier pour chaque opération de lecture.
example_file = (“http://upload.wikimedia.org/” +
“wikipedia/commons/7/7d/Dog_face.png”)
image = imread(example_file, as_grey=True)
plt.imshow(image, cmap=cm.gray)
plt.show()
Nous commençons par importer quatre librairies puis créons une chaîne de caractères qui
contient l’adresse du fichier image, chaîne que nous stockons dans la variable example_file.
Nous utilisons la chaîne dans l’appel à la méthode imread() avec le paramètre as_grey forcé
à la valeur True. Ce paramètre demande à Python de convertir l’image en niveaux de gris (si
elle l’est déjà, elle le reste).
L’image est alors chargée et il ne reste qu’à en effectuer le rendu pour pouvoir l’afficher, rendu
réalisé par la fonction imshow() qui exploite une table de couleurs en niveaux de gris cm.gray.
Nous affichons enfin l’image avec la fonction show() (Figure 6.6). Prévoyez de patienter
lorsque vous exécutez l’exemple pour la première fois.
Figure 6.6 : Affichage d’une image après rendu.
L’image est donc disponible en mémoire vive. Vous pouvez en apprendre plus à son sujet au
moyen de la commande suivante :
L’exécution de cette commande vous permet d’apprendre que l’image en mémoire est de type
numpy.ndarray avec des dimensions de 90 pixels sur 90. C’est donc un tableau de pixels que
vous pouvez manipuler. Vous pouvez par exemple changer les dimensions avec le code source
suivant :
image2 = image[5:70,0:70]
plt.imshow(image2, cmap=cm.gray)
plt.show()
Le tableau numpy.ndarray dans image2 est plus petit que celui de image et l’affichage sera
donc plus petit aussi (Figure 6.7).
N. d. T. : rogner une image ne permet plus de la comparer à une autre similaire si cette
dernière n’a pas eu besoin d’être rognée puisque le cadrage n’est plus identique.
Pour une comparaison valable, il faut redimensionner ou retailler l’image et non la rogner.
L’exemple suivant retaille l’image pour lui donner les dimensions requises par une analyse :
L’exécution de l’exemple vous permet d’apprendre que l’image mesure maintenant 30 pixels
sur 30, la rendant comparable à d’autres images ayant ces dimensions.
Une fois que toutes les images possèdent la même taille, il reste à les aplatir, c’est-à-dire à
transformer le tableau en une ligne unique de données. En effet, une ligne de données ne
possède qu’une dimension, sa longueur, alors que l’image est au départ un tableau de lignes et
de colonnes de pixels. Vous ne pouvez donc pas vous en servir directement dans un jeu de
données. L’exemple suivant aplatit image3 pour produire un tableau de 900 éléments de long
sur une seule ligne, tableau stocké dans la variable image_row :
image_row = image3.flatten()
print("Type de données: %s, shape: %s" %(type(image_row), image_row.shape))
Le type n’a pas changé ; il reste numpy.ndarray. Dorénavant, vous pouvez ajouter ce tableau
de 900 éléments à une dimension dans un jeu de données pour lancer une analyse.
Une fois que vous avez accès à la base, vous pouvez piloter le moteur pour qu’il réalise des
actions. Le fruit d’une requête de lecture est un objet de type DataFrame contenant les
données. Pour écrire des données dans la base, vous devez d’abord créer un tel objet
DataFrame ou utiliser celui qui existe déjà. Voici quelques méthodes essentielles du langage
SQL :
» read_sql_table() : lit les données d’une table SQL vers un objet DataFrame.
» read_sql_query() : lit les données d’une base au moyen d’une requête SQL vers un
objet DataFrame.
» read_sql() : lit des données d’une table SQL ou d’une requête vers un objet DataFrame.
» DataFrame.to_sql() : écrit le contenu d’un objet DataFrame dans les tables de la base
active.
La librairie sqlalchemy est capable de faire le lien avec un certain nombre de bases SQL. Voici
les plus connues :
» SQLite
» MySQL
» PostgreSQL
» SQL Server
» Et d’autres bases relationnelles auxquelles vous pouvez vous connecter par le mécanisme
ODBC (Open Data Base Connectivity).
Les techniques que nous allons utiliser dans ce livre se servent de bases d’essai, mais restent
applicables aux bases relationnelles. Pour en savoir plus au sujet des bases relationnelles,
voyez la page https://docs.sqlalchemy.org/en/latest/core/engines.html.
Bases NoSQL
Certains projets de très grande envergure conviennent mal au modèle relationnel, car ils le
rendent trop complexe. Pour répondre à ce besoin, a été inventée l’architecture dite NoSQL
(Not Only SQL, qui n’est pas principalement basée sur le modèle SQL). Vous rencontrerez ce
genre de bases de données moins fréquemment parce qu’elles sont plus difficiles à mettre en
place et utilisent des techniques différentes qui supposent une formation complémentaire.
Elles offrent en revanche des caractéristiques uniques et répondent à des besoins de façon
optimale. L’exploitation d’une telle base reste proche de celle d’une base relationnelle :
1. Importation des fonctions pour accéder à la base de données.
2. Création d’une instance du moteur pour la base de données.
3. Lancement de requêtes grâce à ce moteur et aux fonctions d’accès à la base de
données.
En réalité, les détails vont varier d’un modèle à l’autre, et vous devrez vous procurer les
librairies de fonctions appropriées au produit concerné. Pour utiliser par exemple une base
MongoDB (https://www.mongodb.org/), vous devez vous procurer la librairie PyMongo afin
d’utiliser la classe nommée MongoClient pour créer une instance du moteur de base. Ce
moteur de MongoDB utilise énormément la fonction de recherche find() pour localiser les
données à extraire. Voici un pseudo-code qui donne un aperçu d’une session de travail avec
une base MongoDB. (Vous ne pouvez pas exécuter cet exemple dans le calepin.)
import pymongo
import pandas as pd
from pymongo import Connection
connection = Connection()
db = connection.database_name
input_data = db.collection_name
data = pd.DataFrame(list(input_data.find()))
Les datalogues peuvent avoir besoin d’interfaces de programmation API pour avoir accès et manipuler
des données. Parfois, l’objectif de leur analyse est justement cette interface API. Nous ne présenterons
pas les API en détail dans ce livre parce que chacune est unique et que leur utilisation ne fait pas
partie du quotidien d’un spécialiste de la datalogie. Vous pouvez par exemple recourir à un produit tel
que JQuery (http://jquery.com/) pour accéder et manipuler les données de différentes façons en
relation avec une application Web. Les techniques correspondantes sont plutôt du domaine de la
création d’applications que de celui de la datalogie.
Les interfaces API peuvent constituer des sources de données dont vous pourriez avoir besoin pour
obtenir des données ou les mettre en forme. Il existe d’ailleurs de nombreuses entités correspondant
à des données et ressemblant à des interfaces API, mais qui n’apparaîtront pas dans ce livre. Par
exemple, les développeurs sous Windows peuvent créer des applications de type COM (Component
Object Model) pour générer des données vers le Web, données que vous pouvez utiliser pour vos
analyses. En fait, le nombre de sources de données possibles est énorme ; ce livre se focalise sur les
sources que vous utiliserez le plus fréquemment, et de façon la plus classique. Gardez cependant l’œil
ouvert sur les autres possibilités non décrites dans ce livre.
Une autre source d’alimentation en données correspond aux microservices. À la différence des
services Web, ceux-ci ont des objectifs très précis : chacun ne fournit qu’un seul moyen
d’émettre une requête et d’obtenir un résultat. Les avantages des microservices débordent du
cadre de ce livre, mais vous pouvez les considérer comme des services Web miniatures, et c’est
de cette manière que nous allons les envisager.
Un des formats de données Web les plus utiles à maîtriser est le format XML. Il permet le
stockage de nombreux types de données, y compris des pages Web complètes. Le fait de
travailler avec des services Web ou avec des microservices suppose d’ailleurs de traiter le
format XML en général. Voyons par un exemple comment récupérer les données d’un fichier
XML nommé XMLData.xml (Figure 6.8). Ce fichier reste volontairement simple et ne contient
que deux sous-niveaux hiérarchiques, mais XML peut en réalité en comporter beaucoup plus.
Figure 6.8 : Le format de données hiérarchique XML peut devenir assez complexe.
L’utilisation du format XML, même si le fichier est simple, peut s’avérer plus difficile que les
autres formats. Voici un exemple :
xml = objectify.parse(open(‘XMLData.xml’))
root = xml.getroot()
for i in range(0,4):
obj = root.getchildren()[i].getchildren()
lig = dict(zip([‘Number’, ‘String’, ‘Boolean’],
[obj[0].text, obj[1].text,
obj[2].text]))
lig_s = pd.Series(lig)
lig_s.name = i
df = df.append(lig_s)
print(df)
Après avoir importé les librairies requises, nous analysons les données avec la méthode
objectify.parse(). Un document XML doit toujours posséder un nœud racine qui est ici
incarné par <MyDataSet>. De ce nœud dérivent tous les autres nœuds du contenu, ces autres
nœuds étant des enfants. Pour pouvoir exploiter le document, il faut donc d’abord pouvoir
accéder au nœud racine avec la méthode getroot().
Nous créons ensuite un objet de type DataFrame nommé df qui dès le départ va contenir les
noms des colonnes des entrées : Number, String et Boolean pour les données XML. La
librairie pandas se base comme pour les autres formats sur un objet DataFrame. Nous entrons
ensuite dans une boucle de répétition for pour stocker dans l’objet df les quatre
enregistrements trouvés dans le fichier XML (chacun correspond à un nœud nommé
<Record>).
Le processus semble un peu complexe, mais il suit un ordre logique. La variable nommée obj
reçoit les données de tous les nœuds enfants d’un nœud <Record>. Ces nœuds sont chargés
dans un objet dictionnaire dont les clés sont les valeurs Number, String et Boolean, ce qui
correspond aux colonnes de l’objet DataFrame.
Nous disposons à partir de ce moment d’un objet dictionnaire contenant les données des
lignes. Nous créons alors une ligne avec les données réelles pour DataFrame. La ligne reçoit la
valeur disponible dans le tour de boucle for en cours, puis cette ligne est ajoutée à
DataFrame. Nous demandons d’afficher le résultat pour confirmer que tout s’est passé comme
prévu :
L’ALTERNATIVE JSON
Le format XML est très répandu sur le Web, mais ce n’est pas le seul. Un autre format assez répandu
qu’il faut prévoir dans vos projets est le format JSON ou JavaScript Objet Notation
(http://www.json.org/). Les promoteurs de JSON prétendent qu’il est moins volumineux, plus facile
à utiliser et plus performant que le format XML. Il vous arrivera de recevoir des données au format
JSON en entrée au lieu de XML pour travailler avec certains services Web et microservices.
Vos stratégies d’exploitation des données seront assez simples si vous vous limitez aux deux formats
XML et JSON. Cependant, d’autres propositions de formats de données existent, pour simplifier
l’analyse. De nos jours, les développeurs cherchent à jouir d’une meilleure compréhension des flux de
données, et optent donc pour des formats privilégiant leur lisibilité par les humains. Une des
propositions les plus remarquables correspond au langage YAML
(http://yaml.org/spec/1.2/spec.html). Si vous optez pour une alternative, soyez prêt à un certain
surcroît de travail au départ, le temps de résoudre les besoins particuliers de vos nouveaux projets.
Chapitre 7
Préparation des données
DANS CE CHAPITRE :
» Les librairies NumPy et pandas
» Variables symboliques
L es
particularités de vos données, et notamment leur type, constituent leur profil. C’est le
profil qui détermine quel genre de traitement vous pouvez leur appliquer. Pour que les
données soient compatibles avec un certain type d’analyse, il faut éventuellement les
reprofiler. C’est un peu comme si les données étaient de l’argile et que vous étiez un potier. Au
lieu d’utiliser vos mains pour leur donner un nouveau profil, vous utilisez des fonctions et des
algorithmes. Découvrons dans ce chapitre les outils disponibles pour contrôler le profil des
données et leur impact.
Nous verrons en effet les problèmes liés au profilage. Vous devez par exemple absolument
savoir s’il manque des données dans un jeu. Si les données n’ont pas le profil adéquat,
l’analyse qui en résulte n’aura aucun sens. Certains types de données, et notamment les dates
et les heures, peuvent poser problème. Vous devez donc soigner cette préparation pour être
certain d’obtenir les résultats attendus, et aboutir à un jeu de données sur lequel un grand
nombre d’analyses pourront être menées.
Certaines opérations de profilage ont pour but de créer un jeu de données plus vaste. En
général, les données à traiter ne sont pas toutes dans la même base de données, ni toutes dans
le même format. C’est pourquoi vous devez les profiler et les fusionner pour obtenir au final un
seul jeu de données dans un format connu, et le rendre analysable. Faire converger les
données d’entrée peut ressembler à un art, car les données défient parfois votre sagacité, et
résistent aux retouches trop simples.
Le code source des exemples de ce chapitre se trouve dans le fichier PYDASC_07 du fichier
archive des exemples. Nous avons indiqué comment récupérer ce fichier dans l’introduction.
En plus du fichier de calepin, vous devez pour ce chapitre pouvoir utiliser le fichier de données
d’entrée nommé XMLData2.xml. Le fichier doit se situer dans le même répertoire que le fichier
des exemples. Dans le cas contraire, vous subirez une erreur d’entrée-sortie. Nous avons
expliqué au début du chapitre précédent comment implanter les fichiers de données dans le
répertoire.
Dans les chapitres précédents, nous avons passé beaucoup plus de temps à préparer les données
qu’à effectuer des analyses. Il est vrai que l’essentiel du temps d’un datalogue est consacré à la
préparation des données, car les données d’entrée se présentent rarement dans un format permettant
de leur appliquer immédiatement une analyse. Voici les cinq étapes à prévoir pour cette préparation :
» Obtention des données d’entrée
» Agrégation des données
» Création de sous-ensembles de données
» Nettoyage des données
» Constitution d’un jeu de données unique par fusion de plusieurs jeux
Ne vous découragez pas à l’idée de devoir progresser laborieusement à travers ces étapes. Grâce à
Python et aux différentes librairies, la préparation sera simple et efficace, et c’est pour cette raison
que nous consacrons tant de pages à cette partie initiale du travail. Mieux vous saurez comment
exploiter Python pour parcourir ces tâches initiales très répétitives, plus vite vous pourrez commencer
à vous faire plaisir en lançant toutes sortes d’analyse.
En tant que datalogue, vos données doivent vous inspirer. Vous devez parvenir à les faire parler
en exploitant les possibilités de la librairie pandas, comme le montre cet exemple :
xml = objectify.parse(open(‘XMLData2.xml’))
root = xml.getroot()
df = pd.DataFrame(columns=(‘Number’, ‘String’, ‘Boolean’))
for i in range(0,4):
obj = root.getchildren()[i].getchildren()
row = dict(zip([‘Number’, ‘String’, ‘Boolean’],
[obj[0].text, obj[1].text,
obj[2].text]))
row_s = pd.Series(row)
row_s.name = i
df = df.append(row_s)
search = pd.DataFrame.duplicated(df)
print(df)
print()
print(search[search == True])
L’exemple montre comment trouver les doublons. Il utilise comme données d’entrée une
version modifiée du fichier XML déjà vu, XMLData2.xml. Cette variante contient un
enregistrement en double. En situation réelle, il y a des milliers et des milliers
d’enregistrements, avec éventuellement des centaines de doublons. Cet exemple suffit à
montrer le principe. Il commence par charger le fichier de données en mémoire (comme déjà
vu dans le Chapitre 6), puis il copie les données dans un objet DataFrame.
Au départ, les données peuvent être considérées comme défectueuses, puisqu’elles
contiennent des doublons. Pour les éliminer, il faut d’abord les localiser. Nous commençons par
créer un objet de recherche contenant la liste des lignes en double au moyen d’un appel à
pd.DataFrame.duplicated(). Toutes les lignes en double ont une mention True en dernière
colonne.
Dans cette situation intermédiaire, vous êtes face à une liste de lignes mélangeant doublons et
non-doublons. Pour repérer les lignes en double, nous créons un index qui utilise comme
critère l’expression search == True. Voici le résultat affiché par l’exemple. Vous constatez que
la ligne numéro trois (la quatrième) est un doublon dans la sortie de DataFrame et que cette
ligne est bien affichée par la fonction de recherche :
3 True
dtype: bool
xml = objectify.parse(open(‘XMLData2.xml’))
root = xml.getroot()
df = pd.DataFrame(columns=(‘Number’, ‘String’, ‘Boolean’))
for i in range(0,4):
obj = root.getchildren()[i].getchildren()
row = dict(zip([‘Number’, ‘String’, ‘Boolean’],
[obj[0].text, obj[1].text,
obj[2].text]))
row_s = pd.Series(row)
row_s.name = i
df = df.append(row_s)
print(df.drop_duplicates())
Comme dans l’exemple précédent, nous commençons par créer l’objet cadre de données df
qui contient le doublon. Pour le supprimer, il suffit d’appeler la méthode drop_duplicates().
Voici le résultat :
La recherche de ces problèmes est l’occasion d’établir un plan de données, c’est-à-dire une
liste des tâches à réaliser pour garantir l’intégrité des données. Dans l’exemple suivant, nous
disposons d’une data map avec deux jeux de données B et C :
import pandas as pd
pd.set_option(‘display.width’, 55)
df = pd.DataFrame({‘A’: [0,0,0,0,0,1,1],
‘B’: [1,2,3,5,4,2,5],
‘C’: [5,3,4,1,1,2,3]})
a_group_desc = df.groupby(‘A’).describe()
print(a_group_desc)
La data map utilise des 0 pour les premières séries et des 1 pour les secondes séries. Grâce à
la fonction groupby(), nous stockons les jeux de données B et C dans des groupes. Afin de
savoir si la data map est utilisable, nous demandons des statistiques par describe(). Le
résultat est un jeu de données B avec les séries 0 et 1, ainsi qu’un jeu de données C également
avec les séries 0 et 1.
B \
count mean std min 25% 50% 75% max
A
0 5.0 3.0 1.581139 1.0 2.00 3.0 4.00 5.0
1 2.0 3.5 2.121320 2.0 2.75 3.5 4.25 5.0
C
count mean std min 25% 50% 75% max
A
0 5.0 2.8 1.788854 1.0 1.00 3.0 4.00 5.0
1 2.0 2.5 0.707107 2.0 2.25 2.5 2.75 3.0
Nous disposons ainsi de statistiques au sujet des deux séries de jeux de données. La
distribution des deux jeux grâce aux cas particuliers constitue le plan de données. Si vous
observez les valeurs, vous pouvez déjà prévoir que ce plan n’est peut-être pas exploitable parce
que certaines valeurs sont très éloignées des autres.
Le mode de deux générations par défaut de describe() consiste à présenter les données de
manière non empilée, ce qui risque d’afficher avec des sauts de ligne, rendant le résultat
difficile à lire. Pour éviter cela, il suffit de spécifier la largeur d’affichage à utiliser au minimum
par le genre d’appel suivant :
pd.set_option(‘display.width’, 55)
Un certain nombre de paramètres de pandas peuvent être réglés ainsi (voyez à ce sujet
https://pandas.pydata.org/pandas-docs/stable/generated/pandas.set_option.html).
Dans notre exemple simple, les données restent assez lisibles même non empilées. Voyons ce
qu’il en est si nous demandons leur empilement au moyen du code suivant :
stacked = a_group_desc.stack()
print(stacked)
Le fait d’appeler la méthode stack() reformate la sortie pour la rendre plus compacte :
B C
A
0 count 5.000000 5.000000
mean 3.000000 2.800000
std 1.581139 1.788854
min 1.000000 1.000000
25% 2.000000 1.000000
50% 3.000000 3.000000
75% 4.000000 4.000000
max 5.000000 5.000000
1 count 2.000000 2.000000
mean 3.500000 2.500000
std 2.121320 0.707107
min 2.000000 2.000000
25% 2.750000 2.250000
50% 3.500000 2.500000
75% 4.250000 2.750000
max 5.000000 3.000000
Vous n’aurez pas toujours besoin de la totalité des données fournies par describe(). Par
exemple, vous pouvez vous contenter du nombre d’éléments dans chaque série et de la
moyenne. Voici comment réduire la quantité d’informations affichée :
print(a_group_desc.loc[:,(slice(None),[‘count’,’mean’]),])
En utilisant loc(), vous pouvez spécifier certaines colonnes. Voici le dernier affichage de notre
exemple qui se limite aux informations dont vous avez absolument besoin pour prendre une
décision :
B C
count mean count mean
A
0 5.0 3.0 5.0 2.8
1 2.0 3.5 2.0 2.5
Les exemples concernant les variables catégorielles que nous allons découvrir ne fonctionnent que si
vous disposez au minimum de la version 0.23.0 de la librairie pandas. Cependant, la version
d’Anaconda mise en place peut avoir entraîné l’installation d’une version plus ancienne. Pour en avoir
le cœur net, procédez ainsi :
import pandas as pd
print(pd.__version__)
Le numéro de version de pandas doit apparaître. Vous pouvez également connaître le numéro de
version en ouvrant la fenêtre Anaconda Prompt puis en saisissant la commande pip show pandas
sans oublier de valider par Entrée. Si vous constatez que vous avez une trop ancienne version, dans
la même fenêtre, saisissez cette commande :
La mise à jour est réalisée automatiquement avec un contrôle de tous les paquetages concernés.
Notez que sous Windows, il vous faudra peut-être ouvrir la fenêtre de terminal Anaconda Prompt en
mode administrateur. Pour ce faire, cliquez-droit dans l’entrée du menu Démarrer d’Anaconda Prompt
pour choisir Exécuter en tant qu’administrateur dans le menu local.
import pandas as pd
car_colors = pd.Series([‘Bleu’, ‘Rouge’, ‘Vert’],
dtype=’category’)
car_data = pd.Series(
pd.Categorical(
[‘Jaune’, ‘Vert’, ‘Rouge’, ‘Bleu’, ‘Pourpre’],
categories=car_colors, ordered=False))
find_entries = pd.isnull(car_data)
print(car_colors)
print()
print(car_data)
print()
print(find_entries[find_entries == True])
Nous commençons par créer la variable portant le nom car_colors et contenant les trois
couleurs de base disponibles pour le modèle de voiture concerné. Notez l’obligation d’ajouter
la valeur de la propriété dtype qui vaut category.
Nous définissons ensuite une seconde série correspondant à des données d’entrée. Elle
contient une liste de couleurs car_data, mais toutes les couleurs ne sont pas définies dans la
première série (ce ne sont pas des valeurs acceptables). Lorsque pandas détecte une telle
valeur hors limites, l’affichage de la valeur est remplacé par la mention NaN (Not a Number).
N. d. T. : L’expression NaN est tout à fait justifiée dans le cadre des variables quantitatives,
c’est-à-dire des valeurs numériques. Pour des variables catégorielles, l’expression n’est pas
tout à fait exacte. Considérez que dans ce cas, l’abréviation signifie Non acceptable.
Il est bien préférable de laisser la librairie pandas chercher les couleurs non acceptables que
de le faire à la main. Vous demandez à pandas de chercher les entrées incorrectes par
is_null() et de stocker le résultat dans find_entries. Il suffit ensuite d’afficher la liste des
entrées refusées (valeur NaN). Voici les trois opérations d’affichage :
0 Bleu
1 Rouge
2 Vert
dtype: category
Categories (3, object): [Bleu, Rouge, Vert]
0 NaN
1 Vert
2 Rouge
3 Bleu
4 NaN
dtype: category
Categories (3, object): [Bleu, Rouge, Vert]
0 True
4 True
dtype: bool
Lorsque vous lisez la deuxième liste ci-dessus, vous constatez que les entrées de
rang 0 et 4 valent NaN. L’affichage du contenu de find_entries le prouve. Lorsque le jeu de
données devient volumineux, cette approche permet de repérer facilement puis de corriger les
valeurs aberrantes, avant même de tenter de leur appliquer une analyse.
import pandas as pd
car_colors = pd.Series([‘Bleu’, ‘Rouge’, ‘Vert’],
dtype=’category’)
car_data = pd.Series(
pd.Categorical(
[‘Bleu’, ‘Vert’, ‘Rouge’, ‘Bleu’, ‘Rouge’],
categories=car_colors, ordered=False))
print(car_data)
Il suffit de donner à la propriété nommée cat.categories les nouvelles valeurs désirées. Voici
le résultat affiché :
0 Pourpre
1 Mauve
2 Jaune
3 Pourpre
4 Jaune
dtype: category
Categories (3, object): [Pourpre, Jaune, Mauve]
Combinaisons de niveaux
Il arrive que le niveau de détail offert par une catégorie soit trop important pour donner lieu à
une analyse intéressante. Il peut s’agir d’un nombre de valeurs trop faibles, ce qui ne donne
pas assez de différences au niveau des statistiques. Il est dans ce cas intéressant de combiner
plusieurs petites catégories comme le montre l’exemple suivant :
import pandas as pd
car_data = car_data.cat.set_categories(
["Bleu", "Rouge", "Vert", "Bleu_Rouge"])
print(car_data.loc[car_data.isin([‘Rouge’])])
car_data.loc[car_data.isin([‘Rouge’])] = ‘Bleu_Rouge’
car_data.loc[car_data.isin([‘Bleu’])] = ‘Bleu_Rouge’
car_data = car_data.cat.set_categories(
["Vert", "Bleu_Rouge"])
print()
print(car_data)
Dans l’exemple, le jeu de données d’entrée car_data ne contient qu’une occurrence du bleu et
du rouge, mais trois du vert qui est donc majoritaire. Pour combiner bleu et rouge, il faut
travailler en plusieurs étapes. Tout d’abord, nous ajoutons la nouvelle catégorie Bleu_Rouge
dans car_data puis nous renommons les catégories Rouge et Bleu en Bleu_Rouge afin
d’obtenir la catégorie combinée. Pour terminer, nous supprimons les catégories de départ.
Le problème est qu’il faut d’abord localiser les entrées Rouge pour en faire des entrées
Bleu_Rouge. Pour y parvenir, nous combinons un appel à isin() pour tester si l’entrée est
rouge avec loc() qui récupère l’index de l’entrée. La première instruction d’affichage
print() montre le résultat de la combinaison. La seconde affiche le nouveau contenu de la
catégorie.
2 Rouge
4 Rouge
dtype: category
Categories (4, object): [Bleu, Rouge, Vert, Bleu_Rouge]
0 Bleu_Rouge
1 Vert
2 Bleu_Rouge
3 Vert
4 Bleu_Rouge
5 Vert
dtype: category
Categories (2, object): [Vert, Bleu_Rouge]
Dorénavant, nous avons trois entrées Bleu_Rouge et trois entrées Vert. Les catégories Bleu
et Rouge ne sont plus utilisées. Nous avons donc bien réussi à combiner deux niveaux.
import datetime as dt
now = dt.datetime.now()
print(str(now))
print(now.strftime(‘%a, %d %B %Y’))
Vous constatez que la technique la plus simple est celle basée sur str(). Le résultat affiché
montre néanmoins que ce n’est pas nécessairement le but espéré. Vous disposez de bien plus
de souplesse en utilisant strftime().
2019-09-22 09:46:43.562838
Sun, 22 September 2019
import datetime as dt
now = dt.datetime.now()
timevalue = now + dt.timedelta(hours=2)
print(now.strftime(‘%H:%M:%S’))
print(timevalue.strftime(‘%H:%M:%S’))
print(timevalue - now)
La conversion d’une heure dans une autre est simplifiée par la fonction time-delta(). Voici les
différents paramètres qu’elle accepte pour convertir une valeur de date et heure :
» days
» seconds
» microseconds
» milliseconds
» minutes
» hours
» weeks
Vous pouvez facilement ajouter ou soustraire des heures, par exemple pour savoir combien
d’heures séparent deux dates. Voici le résultat affiché par l’exemple :
09:01:11
11:01:11
2:00:00
Dans l’exemple, la fonction now() permet de récupérer l’heure locale actuelle et la variable
timevalue demande un décalage de deux fuseaux horaires.
import pandas as pd
import numpy as np
print(s.isnull())
print()
print(s[s.isnull()])
Plusieurs conventions existent pour symboliser les données manquantes dans un jeu. Dans
notre exemple, nous gérons le marquage par np.NaN (NumPy NaN) et par la valeur prédéfinie
de Python None.
La recherche des manquants utilise la méthode isnull(). En ajoutant un index au jeu de
données, nous pouvons n’afficher que les entrées manquantes. Voici le résultat des opérations
d’affichage :
False
1 False
2 False
3 True
4 False
5 False
6 True
dtype: bool
3 NaN
6 NaN
dtype: float64
print(s.fillna(int(s.mean())))
print()
print(s.dropna())
Nous exploitons deux méthodes ici : fillna() remplit les trous et dropna() qui supprime
l’entrée manquante. La première méthode oblige à fournir une valeur à insérer dans le trou.
Dans l’exemple, nous partons de la moyenne de toutes les valeurs, mais autre chose est bien
sûr possible. Voici le résultat d’exécution de l’exemple :
0 1.0
1 2.0
2 3.0
3 3.0
4 5.0
5 6.0
6 3.0
dtype: float64
0 1.0
1 2.0
2 3.0
4 5.0
5 6.0
dtype: float64
Lorsque le jeu de données ne comporte qu’une série, les choses restent simples. En revanche,
dès que vous travaillez avec un datagramme, elles se compliquent. Vous pouvez toujours
choisir de supprimer la totalité de la ligne. De même, face à une colonne contenant de
nombreux manquants, vous pouvez la supprimer. Si vous voulez remplir les données
manquantes, les choses sont plus complexes, car il faut considérer le jeu de données de façon
globale, en plus des besoins au niveau de chaque champ individuel.
import pandas as pd
import numpy as np
from sklearn.preprocessing import Imputer
imp = Imputer(missing_values=’NaN’,
strategy=’mean’, axis=0)
imp.fit([[1, 2, 3, 4, 5, 6, 7]])
x = pd.Series(imp.transform(s).tolist()[0])
print(x)
La série s comporte deux trous. Nous créons un objet du type Imputer qui va insérer d’autres
valeurs à leur place. Le paramètre nommé missing_values permet d’indiquer le symbole que
nous recherchons, NaN. Le paramètre axis est ici égal à zéro, ce qui fait progresser
l’imputation par colonnes (la valeur 1 la fait progresser par lignes). Enfin, le paramètre
strategy décide de la façon dont il faut remplacer les manquants.
» mean : remplace les valeurs manquantes par la moyenne selon le même axe.
» median : remplace les valeurs par la médiane le long du même axe.
» most_frequent : remplace les manquants par la valeur la plus fréquente selon le même
axe.
Avant de réaliser une imputation, vous devez alimenter l’objet imputeur avec des statistiques
au moyen d’un appel à fit(). Dans l’exemple, nous appelons ensuite transform() en
l’appliquant à s pour effectuer le remplissage et affichons le résultat sous forme d’une série.
Pour créer cette série, il nous faut convertir la sortie de l’imputeur vers le format liste qui va
devenir l’entrée de Series(). Voici le résultat final, une fois les valeurs remplacées :
0 1.0
1 2.0
2 3.0
3 4.0
4 5.0
5 6.0
6 7.0
dtype: float64
Tranchage de lignes
L’opération de tranchage peut revêtir plusieurs formes ; celle qui nous intéresse ici consiste à
enlever des données depuis des lignes en deux ou en trois dimensions. Un tableau à deux
dimensions (lignes et colonnes) contiendra par exemple des températures sur l’axe horizontal
et le temps sur l’axe vertical. Sélectionner par tranchage revient à conserver les températures
à un moment donné. Vous pouvez parfois combiner les lignes à des cas.
Pour un tableau à trois dimensions, nous envisageons par exemple un axe pour les adresses
(celui des x), un axe pour les produits (celui des y) et un axe pour le temps (celui des z). Cela
permet de voir l’évolution des ventes de produits au cours du temps. Pour savoir si les ventes
d’un produit en particulier sont en augmentation, et où, il s’agit de sélectionner une ligne pour
les ventes d’un produit pour chaque lieu de vente. Voyons comment obtenir cette sélection.
Dans l’exemple, nous commençons par construire un tableau à trois dimensions, puis nous
extrayons la seule ligne 1 (la deuxième) pour obtenir ceci :
Sélection de colonnes
Sélectionner les colonnes revient à appliquer une rotation d’un angle droit par rapport aux
lignes de la sélection précédente. Dans le cas d’un tableau à deux dimensions, cela correspond
par exemple à la sélection des moments correspondant à certaines températures. Pour
l’exemple commercial, cela permet d’obtenir les ventes de tous les produits à un certain
endroit et à un certain moment. Vous pourrez parfois associer des colonnes à des
caractéristiques du jeu de données. L’exemple suivant montre comment réaliser cette sélection
en partant du même tableau d’entrée que le précédent :
array([[ 4, 5, 6],
[14, 15, 16],
[24, 25, 26]])
Rappelons qu’il s’agit ici d’un tableau à trois dimensions. Chacune des colonnes contient tous
les éléments de l’axe z. Le résultat montre toutes les lignes (de 0 à 2) pour la colonne 1 (la
deuxième) avec tous les éléments de l’axe z de 0 à 2 pour cette colonne.
Débitage (dicing)
L’opération de débitage suppose de trancher dans les deux directions pour obtenir un sous-
bloc. Dans le cas d’un tableau en 3D, vous pouvez ainsi récupérer les ventes d’un seul produit à
un seul endroit, mais à tout moment. L’exemple suivant reprend le même tableau que les deux
précédents exemples :
Nous réalisons quatre débitages différents. Dans le premier, nous récupérons la ligne 1,
colonne 1, puis la colonne 1, axe z 1. Dans le troisième débitage, nous récupérons la
colonne 1 pour l’axe z 1. Enfin, nous demandons les lignes 1 et 2 pour les colonnes 1 et 2. Voici
le résultat des quatre opérations :
[14 15 16]
[ 5 15 25]
[12 15 18]
[[[14 15 16]]]
[17 18 19]]
[[24 25 26]
[27 28 29]]]
Concaténation et transformation
Les données d’entrée se présentent très rarement prêtes à l’emploi. Vous aurez souvent besoin
de puiser dans plusieurs bases de données, chacune imposant son format spécifique. Il est
impossible de réaliser des analyses pertinentes à partir de sources divergentes. Vous devez
donc créer un seul jeu de données unifié pour obtenir des données exploitables, ce qui suppose
de combiner les données, opération appelée concaténation.
Il s’agit d’abord de s’assurer que tous les champs portant les mêmes données dans le jeu
destinataire ont les mêmes caractéristiques. Par exemple, un champ contenant un âge peut
être stocké en tant que chaîne dans une première base, mais en tant que valeur numérique
entière dans une autre. Pour que les champs puissent être valablement exploités, ils doivent
tous être du même type.
Découvrons les opérations à prévoir pour concaténer et transformer les données de plusieurs
sources afin d’obtenir un seul jeu pertinent pour une analyse. L’objectif est d’obtenir le jeu
unifié qui représente correctement les données des différents jeux hétérogènes. Mais ces
données ne doivent pas être altérées, au risque de produire des résultats faussés.
import pandas as pd
df = pd.DataFrame({‘A’: [2,3,1],
‘B’: [1,2,3],
‘C’: [5,3,4]})
df = df.append(df1)
df = df.reset_index(drop=True)
print(df)
df.loc[df.last_valid_index() + 1] = [5, 5, 5]
print()
print(df)
df = pd.DataFrame.join(df, df2)
print()
print(df)
Pour ajouter des données à un cadre DataFrame existant, le plus simple consiste à exploiter la
méthode nommée append(). (Nous verrons comment utiliser la méthode concat() décrite
dans le Chapitre 13.) Dans notre exemple, nous avons trois cas dans df qui sont combinés vers
un seul cas dans df1. Il est indispensable que les colonnes de ces deux jeux correspondent
pour que les données soient ajoutées correctement. En effet, lorsque vous ajoutez deux objets
DataFrame l’un à l’autre, le nouvel objet contient les valeurs de l’ancien index. Pour simplifier
l’accès aux différents cas, vous générez un nouvel index au moyen de reset_index().
Pour ajouter un nouveau cas à un jeu DataFrame existant, vous pouvez le créer directement.
Cela se produit dès que vous insérez une nouvelle entrée localisée à une position supérieure
de 1 au dernier index valide par last_valid_index().
Lorsque vous avez besoin d’ajouter une colonne (une nouvelle variable) à l’objet DataFrame,
vous utilisez join(). L’objet résultant met en correspondance les cas ayant la même valeur
d’index. C’est pourquoi l’indexation est essentielle. Enfin, sauf si vous tolérez des valeurs
vides, le nombre de cas des deux objets DataFrame doit être identique. Voici les trois
affichages résultant de cet exemple :
A B C
0 2 1 5
1 3 2 3
2 1 3 4
3 4 4 4
A B C
0 2 1 5
1 3 2 3
2 1 3 4
3 4 4 4
4 5 5 5
A B C D
0 2 1 5 1
1 3 2 3 2
2 1 3 4 3
3 4 4 4 4
4 5 5 5 5
Suppression de données
Lorsque vous avez besoin de supprimer des cas (lignes) ou des variables (colonnes) d’un jeu
car vous n’en avez pas besoin dans l’analyse, vous utilisez la méthode drop(). L’opération va
supprimer soit des cas, soit des variables, en fonction de la façon dont vous fournissez les
paramètres à la méthode, comme le montre l’exemple :
import pandas as pd
df = pd.DataFrame({‘A’: [1,2,3],
‘B’: [4,5,6],
‘C’: [7,8,9]})
df = df.drop(df.index[[1]])
print(df)
df = df.drop(‘B’, 1)
print()
print(df)
Dans l’exemple, nous supprimons d’abord le deuxième cas de df (cette ligne correspond
visuellement à la deuxième colonne dans la définition ci-dessus). Vous constatez qu’il faut
fournir un index. Vous pouvez supprimer un seul cas, comme dans l’exemple, ou bien une plage
de cas en les séparant par des virgules. Vous devez bien sûr être très vigilant au niveau des
numéros des index.
Supprimer une colonne se fait différemment. Vous fournissez son nom ou son index. Dans les
deux cas, il faut fournir un axe qui est normalement 1. Voici le résultat des deux suppressions
de cet exemple :
A B C
0 1 4 7
2 3 6 9
A C
0 1 7
2 3 9
Trier et mélanger
Trier et mélanger sont les deux opérations complémentaires pour maîtriser l’ordre d’apparition
des données. La première opération place les données dans un certain ordre alors que la
seconde supprime tout ordre remarquable. Il faut savoir qu’en général, il n’est pas conseillé de
trier les données d’entrée d’une analyse car cela peut influer sur la pertinence des résultats.
En revanche, vous aurez souvent besoin de trier les données par la suite pour les présenter.
L’exemple suivant effectue un tri puis un mélange :
import pandas as pd
import numpy as np
df = pd.DataFrame({‘A’: [2,1,2,3,3,5,4],
‘B’: [1,2,3,5,4,2,5],
‘C’: [5,3,4,1,1,2,3]})
index = df.index.tolist()
np.random.shuffle(index)
df = df.loc[df.index[index]]
df = df.reset_index(drop=True)
print()
print(df)
La lecture de l’exemple montre que le tri est beaucoup plus simple que le mélange. En effet,
pour trier les données, vous vous servez de la méthode sort_ values() en indiquant la
colonne servant de clé de tri. Vous pouvez choisir de trier en ordre ascendant ou descendant. Il
ne faut pas oublier d’appeler ensuite reset_index() pour que les index se présentent dans
l’ordre correct pour les traitements ultérieurs.
Pour effectuer un mélange, il faut d’abord récupérer l’index actuel avec df.index.tolist() et
le stocker dans index. Vous mélangez l’index par appel à random_shuffle(). Il ne reste plus
qu’à appliquer ce nouvel ordre à df au moyen de loc[]. N’oubliez pas enfin de générer le
nouvel index avec reset_index(). Voici le résultat de ces deux opérations, en sachant que
l’affichage de la seconde ne correspondra sans doute pas au même ordre que lors de votre
exécution :
A B C
0 1 2 3
1 2 1 5
2 2 3 4
3 3 4 1
4 3 5 1
5 4 5 3
6 5 2 2
A B C
0 2 1 5
1 4 5 3
2 1 2 3
3 3 4 1
4 2 3 4
5 3 5 1
6 5 2 2
La principale raison d’être de l’opération d’agrégation des données est d’ajouter de l’anonymat
pour des raisons juridiques ou autres. En effet, même lorsque les données doivent rester
anonymes, elles peuvent permettre d’identifier un individu en employant les bonnes techniques
d’analyse. C’est ainsi que des chercheurs ont pu prouver que l’on pouvait identifier une
personne à partir de trois achats par carte bancaire
(https://www.nature.com/articles/srep01376). L’exemple suivant réalise trois tâches
d’agrégation :
import pandas as pd
df = pd.DataFrame({‘Mappe’: [0,0,0,1,1,2,2],
‘Valeurs’: [1,2,3,5,4,2,5]})
df[‘S’] = df.groupby(‘Mappe’)[‘Valeurs’].transform(np.sum)
df[‘M’] = df.groupby(‘Mappe’)[‘Valeurs’].transform(np.mean)
df[‘V’] = df.groupby(‘Mappe’)[‘Valeurs’].transform(np.var)
print(df)
Nous partons de deux caractéristiques pour cet objet df. Les valeurs dans Mappe définissent
quels éléments dans Valeurs vont ensemble. Pour calculer la somme des éléments de Mappe à
index 0, nous utilisons les éléments de Valeurs, 2 et 3.
Il faut d’abord utiliser groupby() pour regrouper les valeurs de Mappe, puis les indexer dans
Valeurs et appeler transform() pour générer les données agrégées en ayant recours à un
des algorithmes de la librairie NumPy, par exemple np.sum. Voici les résultats de cet exemple :
Mappe Valeurs S M V
0 0 1 6 2.0 1.0
1 0 2 6 2.0 1.0
2 0 3 6 2.0 1.0
3 1 5 9 4.5 0.5
4 1 4 9 4.5 0.5
5 2 2 7 3.5 4.5
6 2 5 7 3.5 4.5
Chapitre 8
Épuration des données
DANS CE CHAPITRE :
» Traitement des données HTML et XML
D ans le Chapitre 6,
nous avons appris à charger différents types de fichiers et dans le Chapitre 7,
nous avons découvert plusieurs techniques pour préparer les données, et notamment les tris et les
sélections. Mais ces opérations de chargement amélioré ne suffisent pas. Il s’agit ensuite d’épurer
les données pour ne conserver que ce qui mérite de subir l’analyse, et c’est l’objectif de ce chapitre.
Nous allons traiter plusieurs types de fichiers, afin d’en récupérer les contenus significatifs, dans un
grand nombre de formats comme le HTML, le XML, le texte brut et d’autres. Nous verrons même
comment intervenir sur un fichier d’image graphique.
Dans la suite du livre, vous allez découvrir que les données peuvent être remodelées de très
nombreuses façons. Pour la machine, ce ne sont que des zéros et des un ; ce sont les humains qui
donnent du sens aux données en intervenant sur leur format, leur stockage et bien sûr leur
interprétation. Une série de zéros et de un peut correspondre à un salaire, à une date ou une lettre
d’amour, tout dépendant de cette interprétation. Les structures contenant les données fournissent
quelques pistes quant à la façon dont elles peuvent être interprétées. Dans ce chapitre, nous allons
voir pourquoi tout datalogue qui utilise Python doit chercher à trouver des motifs remarquables dans
les données. Ceci ainsi qu’un traitement d’épuration permet de découvrir des informations là où vous
ne pensez même pas qu’il y ait quoi que ce soit de remarquable.
Le fichier d’exemple de ce chapitre correspond à PYDASC_08.
xml = objectify.parse(open(‘XMLData.xml’))
root = xml.getroot()
df = pd.DataFrame(columns=(‘Number’, ‘Boolean’))
row_s = pd.Series(row)
row_s.name = obj[1].text
df = df.append(row_s)
print(type(df.loc[‘Primo’][‘Number’]))
print(type(df.loc[‘Primo’][‘Boolean’]))
L’objet df de type DataFrame est créé vide, puis rempli dans la boucle de répétition for qui traverse
les nœuds enfants de la racine de la structure. La liste produite contient les éléments suivants :
» un élément <Number> (int) ;
» un élément ordinal qui est une chaîne (string) ;
» un élément <Boolean> de type chaîne aussi.
Le booléen sert à incrémenter l’objet df. Le code se base sur l’élément ordinal comme label d’index et
construit chaque nouvelle ligne à insérer dans l’objet df. L’idée est de convertir les données trouvées
dans l’arborescence XML vers le bon type de données à stocker dans df. Les éléments numériques
sont directement disponibles comme valeurs numériques entières int. En revanche, pour l’élément
booléen : il faut convertir la chaîne en une valeur numérique avec la fonction strtobool() de
distutils.util. On obtient ainsi la valeur numérique 0 pour la valeur booléenne False et la
valeur 1 pour la valeur True. Ce ne sont pas encore des valeurs du type booléen, mais des valeurs
numériques entières. Il reste donc à convertir ces chiffres 0 et 1 au moyen de bool().
L’exemple montre en outre comment accéder aux valeurs individuelles dans l’objet DataFrame. La
propriété name se sert de l’élément de type chaîne <String> pour se simplifier l’accès. Vous lui
fournissez un index avec loc[] puis accédez à la caractéristique désirée grâce à un second index.
Voici le résultat qu’affiche cet exemple :
<class ‘int’>
<class ‘bool’>
df = pd.DataFrame(data,
columns=(‘Number’, ‘Boolean’),
index = list(map_string))
print(df)
print(type(df.loc[‘Primo’][‘Number’]))
print(type(df.loc[‘Primo’][‘Boolean’]))
La partie préparatoire ne change pas par rapport à la version précédente : importation des données et
obtention du nœud racine. L’exemple crée ensuite un objet de données dans lequel sont placées des
paires regroupant numéro d’enregistrement et valeur booléenne. Dans le fichier XML, les données
sont toutes de type chaîne de caractères. Nous utilisons donc la fonction map() pour convertir les
chaînes correctement. Pour le numéro d’enregistrement, il suffit de convertir vers le type entier int.
La fonction xpath() reçoit en paramètre un chemin depuis la racine jusqu’aux données désirées, ici,
‘Record/Number’.
Pour la valeur booléenne, les choses sont un peu plus ardues. Nous utilisons toujours la fonction
strtobool() pour convertir la valeur chaîne vers un numérique qui est ensuite converti en booléen
par bool(). Vous pouvez cependant éviter la double conversion, au risque de recevoir une erreur
prétendant que les listes ne possèdent pas la fonction tolower(). Nous contournons ce souci en
effectuant une triple conversion, c’est-à-dire en effectuant d’abord une conversion vers chaîne au
moyen de la fonction str().
L’objet de données DataFrame n’est plus créé de la même façon. Toutes les lignes sont insérées en
même temps au moyen de data. Les noms des colonnes sont obtenus comme dans l’exemple
précédent, mais il nous faut maintenant pouvoir ajouter les noms des lignes. Nous utilisons le
paramètre index appliqué à une version associée (mappée) de la sortie de xpath(), pour le chemin
menant à ‘Record/String’. Voici l’affichage résultant :
Number Boolean
Primo 1 True
Secundo 2 False
Tercio 3 True
Quarto 4 False
<class ‘numpy.int64’>
<class ‘numpy.bool_’>
Gestion d’Unicode
Un fichier texte ne contient par définition que du texte, mais les valeurs numériques qui
correspondent aux différentes lettres et chiffres de ce texte varient en fonction du codage utilisé. On
peut en effet coder un caractère alphabétique sur 7 bits, 8 bits, ou, de plus en plus souvent, sur
plusieurs octets. De même, les caractères non américains sont souvent codés de façon variable. Il est
donc indispensable de connaître la norme utilisée pour coder les signes. Plusieurs encodages sont
visibles dans la page http://www.i18nguy.com/unicode/code-pages.html.
La lignée 3.x de Python a résolument opté pour le standard de codage UTF-8 aussi bien pour la
lecture que pour l’écriture des fichiers. Pour exploiter un autre encodage, par exemple celui nommé
ASCII, vous devrez utiliser une solution de contournement. Dans le cas contraire, vous obtiendrez une
erreur. Un article en anglais donne quelques détails au sujet de ce contournement éventuel
(https://docs.python.org/3/howto/unicode.html).
Si vous tentez de traiter un fichier qui n’est pas généré avec le bon encodage, vous ne pourrez
évidemment pas réaliser vos analyses, ni importer des modules. Vous devez donc bien tester votre
code et vérifier dès le départ qu’il n’y a pas un problème de codage qui empêcherait votre projet de
s’exécuter. Vous trouverez des informations supplémentaires à ce sujet dans les deux pages
suivantes :
» http://blog.notdot.net/2010/07/Getting-unicode-right-in-Python
» http://web.archive.org/web/20120722170929/http://boodebr.org/main/python/all-
about-python-and-unicode
Voyons donc comment réaliser une racinisation et une purge des mots vides dans une phrase. Dans la
première phase, nous apprenons un à un à l’algorithme comment analyser à partir d’une phrase test.
Dans une seconde phase, nous traitons une autre phrase en cherchant les mots qui se trouvent dans
la première.
stemmer = PorterStemmer()
def tokenize(text):
tokens = word_tokenize(text)
stems = stem_tokens(tokens, stemmer)
return stems
print(vec.get_feature_names())
print(sentence1.toarray())
L’exemple crée un vocabulaire (une liste de mots) à partir d’une phrase de test, et stocke celui-ci dans
la variable vocab. Nous créons ensuite un objet de type CountVectorizer nommé vect qui va
recevoir la liste des mots pleins, une fois les mots vides éliminés. Le paramètre tokenizer indique
quelle fonction utiliser pour la racinisation. Le paramètre stop_words désigne un fichier texte
contenant la liste des mots vides du langage concerné. Dans notre cas, c’est la langue anglaise. Il
existe des fichiers de mots vides pour un certain nombre d’autres langues, et notamment le français
et l’allemand. Tous les autres paramètres de CountVectorizer sont décrits dans la page
https://scikit-
learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html.
Le vocabulaire est ensuite inséré dans une autre variable CountVectorizer nommée vec avec
laquelle nous réalisons le traitement de la phrase de test au moyen de la fonction transform(). Voici
le résultat affiché par cet exemple :
Expressions régulières
Les expressions régulières offrent au datalogue un intéressant ensemble d’outils pour analyser du
texte brut. Lors de vos premiers pas, vous pourriez être effrayé par l’apparence de ces expressions et
leur fonctionnement exact. Pour vous aider, vous pouvez vous servir d’un site tel que
https://regexr.com/ pour tester des expressions régulières afin de voir comment formuler celle
dont vous avez besoin. Le principe consiste à considérer certains caractères et symboles comme ayant
une valeur spéciale (contexte dans lequel on parle de métacaractères). Le grand Tableau 8.1 qui suit
donne une liste d’expressions régulières avec une description de l’effet de chacune. Dans la première
colonne, la mention xr signifie expression régulière.
Caractère Description
(xr) Regroupe des expressions régulières et mémorise le texte trouvé
(? : xr) Regroupe des expressions régulières sans mémoriser le texte trouvé
(?#...) Commentaire non pris en compte
xr? Trouve 0 ou 1 occurrence de l’expression (mais pas plus)
xr* Trouve 0 ou plus occurrences de l’expression
xr+ Trouve 1 ou plus occurrences de l’expression
(?> xr) Trouve un motif indépendant sans backtracking
. Trouve un caractère à part Newline (\n) (sauf si ajout de l’option m)
[^...] Trouve un caractère ou une plage de caractères absents du masque
[...] Trouve un caractère ou une plage de caractères présents dans le masque
xr{ n, m} Trouve entre n et m occurrences de l’expression
\n, \t, etc. Trouve les caractères de contrôle comme Newline (\n), Retour Chariot (\r) et Tab (\t)
\d Trouve les chiffres (équivalent à [0-9])
a|b Trouve soit a, soit b
xr{ n} Trouve le nombre exact n d’occurrences
xr{ n,} Trouve n ou plus occurrences
\D Trouve les non-chiffres
\S Trouve les non-espaces
\B Trouve en dehors des débuts et/ou des fins des mots
\W Trouve les caractères hors des mots
\1...\9 Trouve la sous-expression de rang n
\10 Trouve la sous-expression de rang n si elle a déjà réussi
\A Trouve le début d’une chaîne
^ Trouve le début d’une ligne
\z Trouve la fin d’une chaîne
\Z Trouve la fin d’une chaîne (si saut de ligne, trouve juste avant)
$ Trouve la fin d’une ligne
\G Trouve le point de fin du dernier motif trouvé
\s Trouve les espaces (équivaut à [\t\n\r\f])
\b Trouve les extrémités des mots si hors des crochets et trouve Backspace (0x08) dans les crochets
\w Trouve des caractères de mots
(?= xr) Stipule une position avec un motif (sans plage)
(? ! xr) Stipule la négation d’une position avec un motif (sans plage)
(?-imx) Désactive temporairement l’état des options i, m ou x dans une xr (si entre parenthèses,
seulement dans la zone entre parenthèses)
(?imx) Active temporairement l’état des options i, m ou x dans une xr (si entre parenthèses, seulement
dans la zone entre parenthèses)
(?-imx: xr) Désactive temporairement l’état des options i, m ou x dans la zone entre parenthèses
(?imx: xr) Active temporairement l’état des options i, m ou x dans la zone entre parenthèses
Avec les expressions régulières, vous allez pouvoir effectuer des traitements préalables, avant ceux
présentés dans la suite du chapitre. Voyons par exemple comment extraire les numéros de téléphone
de deux phrases dans lesquelles ils sont présentés de façon différente. Ce genre de capture de
données vous sera très utile dès que vous devrez prendre en charge des fichiers qui ne sont pas
formatés de la même façon. En étudiant le masque défini dans cet exemple, vous aurez une bonne
idée du principe des expressions régulières, et pourrez l’appliquer à toutes sortes de projets.
import re
pattern = re.compile(r’(\d{3})-(\d{3})-(\d{4})’)
dmatch1 = pattern.search(data1).groups()
dmatch2 = pattern.search(data2).groups()
print(dmatch1)
print(dmatch2)
Dans nos deux phrases de test, le nouveau téléphone n’apparaît pas au même endroit. Notre masque
doit se lire de gauche à droite. Dès qu’il est défini, nous cherchons cinq groupes de deux chiffres
séparés par un tiret.
Pour que la recherche soit plus rapide, nous appelons la fonction compile() qui va produire une
version compilée du masque. Cela évite à Python de le réinterpréter à chaque utilisation. Le résultat
de cette compilation est stocké dans pattern.
La fonction search() applique le masque à chacune des deux phrases, puis stocke ce qu’elle a trouvé
dans des groupes puis produit une structure de type tuple dans deux variables. Voici ce qui est
affiché :
Les exemples de cette section se basent sur un jeu de données standard nommé 20Newsgroups
(http://qwone.com/⁓jason/20Newsgroups/) qui fait partie de l’installation Scikit-learn. C’est un jeu de
données bien adapté à la présentation de plusieurs genres d’analyse de texte, et le site de référence donne
quelques détails à ce sujet.
Vous n’avez rien à faire pour pouvoir utiliser ce jeu parce que la librairie Scikit-learn le connaît déjà.
Cependant, lors de la première exécution de l’exemple, vous verrez apparaître un message d’avertissement
assez long. Rien de grave. Il suffit de patienter que les données soient téléchargées depuis le site. Observez
bien le côté gauche de la cellule de code dans IPython Notebook. L’indicateur va montrer un témoin [*].
Lorsque cette mention est remplacée par une valeur numérique, c’est que le téléchargement est terminé.
Notez que le message d’avertissement reste affiché jusqu’à la prochaine exécution.
Principe du sac de mots
Dans un sac de nœuds, des valeurs numériques représentent les mots, les fréquences des mots et les
positions des mots, ce qui permet d’effectuer des manipulations mathématiques pour trouver des
motifs remarquables dans la structure. Les sacs de mots ne tiennent pas compte de la grammaire, ni
même de l’ordre dans lequel les mots apparaissent. L’objectif est de simplifier le texte pour en
permettre une analyse aisée.
La création d’un sac de mots utilise les technologies NLP de traitement du langage naturel (Natural
Language Processing) et de recherche d’informations IR (Information Retrieval). Avant d’appliquer
ces deux traitements, vous devez normalement supprimer les caractères spéciaux, par exemple les
balises de format HTML, les mots vides et éventuellement appliquer une racinisation (comme décrit
en début de chapitre). Pour notre exemple, nous allons partir du jeu de données 20 Newsgroups. Voici
donc comment accéder à ce jeu pour produire un sac de mots :
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(twenty_train.data)
Soyez vigilant si vous consultez des exemples disponibles en ligne, car nombreux sont ceux qui ne
disent pas clairement d’où proviennent leurs listes de catégories. Une telle liste, que vous pouvez
utiliser, est fournie par exemple par le site http://qwone.com/⁓jason/20Newsgroups/. Faites
confiance au site de référence dès que vous avez besoin de répondre à une question concernant les
catégories du jeu de données.
Dans notre exemple, nous appelons fetch_20newsgroups() pour récupérer le jeu de données et le
charger en mémoire. L’objet d’entraînement nommé twenty_train correspond à un banc (bunch). Cet
objet contient une liste de catégories et les données associées, mais ces données ne sont pas encore
distribuées en éléments tokens individuels. De plus, l’algorithme qui doit traiter les données n’est pas
encore entraîné.
C’est à partir de ce banc de données que nous allons pouvoir créer notre sac. Nous commençons par
donner une valeur entière qui sert d’index à chaque mot unique du jeu d’entraînement. Chaque
document reçoit lui aussi une valeur entière. Nous comptons ensuite le nombre d’occurrences des
mots dans chaque document, puis produisons la liste de documents et comptons les paires, ce qui
permet de savoir quels mots apparaissent à quelle fréquence dans chacun des documents.
Certains des mots de la liste principale n’apparaissent pas dans certains documents, ce qui
correspond à un jeu de données creux à haute dimension. La matrice scipy.sparse est une structure
dans laquelle vous allez pouvoir stocker tous les éléments différents de zéro de la liste afin
d’économiser l’espace mémoire. Avec l’appel à count_vect.fit_transform(), nous stockons le sac
de mots produits ainsi dans X_train_counts. Nous pouvons donc ensuite afficher le nombre d’entrées
créées grâce à la propriété shape ainsi que le nombre d’occurrences du mot « Caltech » dans le
premier document :
categories = [‘sci.space’]
twenty_train = fetch_20newsgroups(subset=’train’,
categories=categories,
remove=(‘headers’,
‘footers’,
‘quotes’),
shuffle=True,
random_state=42)
count_chars = CountVectorizer(analyzer=’char_wb’,
ngram_range=(3,3),
max_features=10)
count_chars.fit(twenty_train[‘data’])
count_words = CountVectorizer(analyzer=’word’,
ngram_range=(2,2),
max_features=10,
stop_words=’english’)
count_words.fit(twenty_train[‘data’])
X = count_chars.transform(twenty_train.data)
print(count_words.get_feature_names())
print(X[1].todense())
print(count_words.get_feature_names())
Comme dans le précédent exemple, nous commençons par récupérer le jeu de données pour le placer
dans un banc. En revanche, la vectorisation est réalisée différemment en raison des paramètres.
Ici, le paramètre nommé analyzer décide de la façon dont l’application doit créer les n-grammes.
Vous avez le choix entre des mots (word), des caractères (char) ou les caractères des extrémités des
mots, word boundaries (char_wb). Le paramètre nommé ngram_range attend d’abord deux entrées
sous forme d’un tuple : la taille minimale du n-gramme et sa taille maximale. Le troisième paramètre
max_features stipule combien de caractéristiques doivent être envoyées par la vectorisation. Dans le
second appel à la vectorisation, nous utilisons le paramètre stop_words pour supprimer les mots
vides (nous avons vu ce processus en début de chapitre). Après cette étape, les données sont adaptées
à l’algorithme de transformation.
L’exemple affiche trois types de résultats. Le premier montre les dix trigrammes de caractères
principaux du document. Le deuxième montre les détails du n-gramme du premier document avec la
fréquence des dix principaux n-grammes. Le troisième affichage montre les dix trigrammes principaux
pour les mots.
Transformations TF-IDF
La technique TF-IDF, de l’anglais Term Frequency times Inverse Document Frequency est une
opération de pondération : elle permet de distinguer plus aisément un document parmi plusieurs à
partir d’un terme détecté souvent dans toute la série de documents. En théorie, plus un terme est
fréquent dans un document, plus il est important pour ce document (si l’on exempte les mots vides).
Mais la mesure est faussée d’une part par la taille du document relativement aux autres, et d’autre
part par la fréquence du même terme dans les autres documents.
En effet, ce n’est pas parce qu’un mot apparaît souvent dans un document que ce terme est crucial
pour comprendre ou deviner la nature du document. Les mots vides sont très fréquents par définition,
avec à peu près la même fréquence dans tous les documents. Si vous analysez par exemple une série
de documents sur la science-fiction, comme celui de la série 20 Newsgroups, vous constaterez que la
plupart utilisent le terme UFO pour parler des OVNI. L’acronyme UFO ne permet donc pas de
différencier les documents. De plus, les documents plus longs auront un plus grand nombre de fois le
même terme sans que cela ait un sens intéressant.
En fait, un mot assez rare mais dans un seul document ou seulement quelques-uns pourront mieux
servir de discriminant de la nature du document. Si vous traitez par exemple des documents qui
parlent de science-fiction et aussi de voitures, l’acronyme UFO sera bien plus discriminant parce qu’il
n’appartient (pour le moment) qu’à un seul des deux sujets abordés.
Les moteurs de recherche ont sans cesse besoin de soupeser les mots des documents pour savoir s’ils
sont importants. Les mots ayant le plus grand poids vont servir à indexer chaque document, ce qui
permettra de le trouver aisément lorsque vous ferez une recherche avec ces mots. Vous devinez
pourquoi la transformation TF-IDF est assez souvent utilisée dans les moteurs de recherche.
Entrons dans les détails. La partie TF cherche à connaître la fréquence d’un terme dans un document
et la partie IDF détermine l’importance, le poids, du terme en calculant l’inverse de la fréquence de
ce terme dans tous les documents. Une valeur IDF élevée dénote un terme rare, la valeur TF-IDF
finale sera donc grande. Une valeur IDF faible correspond à un mot fréquent, ce qui donnera un score
TF-IDF faible. Plusieurs calculs sont disponibles à l’adresse http://www.tfidf.com/. Voici un
exemple de calcul de TF-IDF en Python :
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(
twenty_train.data)
tfidf = TfidfTransformer().fit(X_train_counts)
X_train_tfidf = tfidf.transform(X_train_counts)
caltech_idx = count_vect.vocabulary_[‘caltech’]
print(‘Score de "Caltech" dans un BOW:’)
print(‘count: %0.3f’ % X_train_counts[0, caltech_idx])
print(‘TF-IDF: %0.3f’ % X_train_tfidf[0, caltech_idx])
Comme dans les deux exemples précédents, nous commençons par récupérer le jeu de données, puis
créons un sac de mots (nous savons maintenant exploiter un tel sac).
Nous appelons TfidfTransformer() pour convertir les documents bruts en une matrice de
caractéristiques TF-IDF. Il n’est pas visible dans l’exemple, mais nous profitons de la valeur par défaut
du paramètre use_idf qui active une repondération de la fréquence de document inverse. Les
données une fois vectorisées sont adaptées à l’algorithme de transformation. Nous appelons ensuite
tfidf. transform() pour déclencher la transformation. Voici le résultat affiché par l’exemple :
Pour saisir la relation entre comptage d’occurrences et TF-IDF, il suffit de calculer le nombre moyen
d’occurrences et la moyenne de la valeur TF-IDF :
import numpy as np
count = np.mean(X_train_counts[X_train_counts>0])
tfif = np.mean(X_train_tfidf[X_train_tfidf>0])
print(‘moyenne de count : %0.3f’ % np.mean(count))
print(‘moyenne de TF-IDF: %0.3f’ % np.mean(tfif))
Les résultats montrent que quelle que soit votre manière de compter, par occurrences dans le premier
document ou par TF-IDF, la valeur sera toujours le double de la moyenne des mots. Cela permet de
comprendre qu’il s’agit d’un mot-clé pour modéliser le texte.
La technique TF-IDF permet donc de repérer les termes ou n-grammes les plus importants et
d’exclure les moins importants. C’est également un traitement d’entrée très utile pour les modèles
linéaires, qui fonctionnent mieux avec des scores TF-IDF qu’avec des comptages de mots. Passée cette
étape, vous pouvez procéder à l’entraînement d’un classificateur pour lancer différentes analyses.
Mais ne vous inquiétez pas de cette partie du processus pour l’instant. Nous présenterons les
classificateurs dans les Chapitres 12 et 15, et verrons un exemple complet dans le Chapitre 17.
Matrices d’adjacence
Une matrice d’adjacence incarne les connexions entre les nœuds d’un graphe. Chaque lien entre deux
nœuds correspond à une valeur supérieure à 0 dans la matrice. La représentation exacte des
connexions dépend du fait que le graphe est de type orienté (le sens des connexions est géré) ou non
orienté.
Le problème de la plupart des exemples en ligne est que les auteurs utilisent un petit nombre de
données pour pouvoir simplifier les explications. Dans le monde réel, vous allez faire face à des
graphes beaucoup plus complexes et vous ne pourrez pas en effectuer une analyse simplement par
visualisation. Songez seulement au nombre de nœuds correspondant aux carrefours dans une ville de
taille moyenne, les rues étant les liens. Dès qu’un graphe devient dense, il est impossible de découvrir
des motifs remarquables par simple visualisation. Cela correspond au problème de la touffe de poils
(hairball).
Une approche pour analyser une matrice adjacente consiste à les trier d’une façon appropriée. Vous
pouvez par exemple trier les données en fonction d’une propriété autre que leurs liens. Le graphe des
rues d’une ville pourrait être classé dans l’ordre chronologique de dernière réfection de chaque rue,
ce qui permettrait de créer un trajet tenant compte de l’état des chaussées. Autrement dit, pour bien
bénéficier des données d’un graphe, il s’agit de savoir manipuler l’organisation interne des données.
Découverte de NetworkX
L’exploration des graphes serait fastidieuse s’il vous fallait écrire la totalité du code source. Fort
heureusement, vous disposez du paquetage NetworkX pour Python. Il simplifie la création, la
manipulation et l’étude de la structure, de la dynamique et des fonctions d’un réseau (d’un graphe)
complexe. Dans ce livre, nous ne verrons que les graphes, mais le paquetage sait aussi gérer les
digraphes et les multigraphes.
L’objectif principal de NetworkX est d’éviter l’apparition de cette touffe de poils (plat de nouilles)
inexploitable. Grâce à de simples appels de fonction, vous restez à l’abri de la complexité du
traitement d’un graphe et d’une matrice d’adjacence. L’exemple suivant crée une matrice élémentaire
à partir d’un des graphes fournis avec le paquetage NetworkX :
import networkx as nx
G = nx.cycle_graph(10)
A = nx.adjacency_matrix(G)
print(A.todense())
Nous commençons bien sûr par importer le paquetage puis créons un graphe à partir du modèle
cycle_graph(). Il comporte 10 nœuds. Nous appelons adja-cency_matrix() pour créer la matrice,
puis affichons son contenu :
[[0 1 0 0 0 0 0 0 0 1]
[1 0 1 0 0 0 0 0 0 0]
[0 1 0 1 0 0 0 0 0 0]
[0 0 1 0 1 0 0 0 0 0]
[0 0 0 1 0 1 0 0 0 0]
[0 0 0 0 1 0 1 0 0 0]
[0 0 0 0 0 1 0 1 0 0]
[0 0 0 0 0 0 1 0 1 0]
[0 0 0 0 0 0 0 1 0 1]
[1 0 0 0 0 0 0 0 1 0]]
Vous pouvez faire des tests sans devoir créer vos propres graphes car le site de NetworkX propose un
certain nombre de graphes standard, tous utilisables dans IPython. Voyez la page
https://networkx.github.io/documentation/latest/reference/generators.html.
Il n’est pas inutile de générer une représentation graphique de notre graphe, comme le fait l’exemple
suivant. La Figure 8.1 montre le résultat.
Nous pouvons ajouter un lien entre les nœuds 1 et 5 en utilisant la fonction add_ edge(). Le résultat
est visible dans la Figure 8.2.
G.add_edge(1,5)
nx.draw_networkx(G)
plt.show()
» Traitement de tableaux
es chapitres précédents constituent une partie préparatoire. Vous savez maintenant réaliser
L quelques tâches indispensables avec Python et avez pris le temps d’utiliser plusieurs outils
utiles en datalogie. Après cette première phase, prenons un peu de recul pour découvrir ce
qu’il faut prendre en compte pour résoudre un problème de science des données.
Ce chapitre ne représente pas la fin de votre voyage. Si on compare les chapitres précédents à
la préparation des valises, à la création des réservations et au choix de l’itinéraire, ce chapitre
représente le trajet jusqu’à l’aéroport. Vous commencez à voir globalement les choses se
mettre en place.
Nous allons d’abord passer en revue les points à prendre en compte pour résoudre un
problème de datalogie. Vous ne pouvez en effet pas plonger la tête la première dans une
analyse. Il faut d’abord comprendre le problème et considérer les ressources disponibles en
termes de données d’entrée, d’algorithmes et de ressources informatiques. En considérant le
contexte du problème, vous allez mieux le comprendre et mieux définir pourquoi les données
dont vous disposez le concerne. En effet, le contact est essentiel parce que comme dans les
langues humaines, il change la signification aussi bien du problème que des données. Par
exemple, si vous dites que vous avez des roses rouges, le sens ne sera pas le même selon que
vous vous adressiez à votre compagne ou compagnon, ou bien à un collègue qui, comme vous,
aime jardiner. La rose rouge est la donnée, le contexte est la personne à laquelle vous en
parlez. Déclarer « J’ai une rose rouge » hors contexte n’a pas de sens pratique. Les données
n’ont en elles-mêmes pas de signification car elles ne permettent de répondre à une question
que lorsque vous connaissez leur contexte. Ainsi, lorsque vous dites « J’ai des données » , se
pose tout de suite la question « Qu’est-ce que signifient ces données ? » .
Vous travaillerez à partir d’un ou de plusieurs jeux de données, par exemple des tables en deux
dimensions constituées de lignes horizontales qui sont les cas ou observations et de colonnes
qui sont les caractéristiques, ou variables en statistiques. Ce sont ces variables utilisées dans
vos analyses qui vont déterminer vos résultats. Il est donc essentiel dans la phase de
conception de votre solution de bien déterminer quelles caractéristiques vous voulez pouvoir
obtenir à partir des données source et comment vous voulez les transformer.
Dans la dernière section du chapitre, nous verrons quelques cas pratiques. Il est souvent
possible d’utiliser plusieurs approches, mais lorsque vous avez un grand volume de données,
privilégiez la rapidité. En utilisant des tableaux et des matrices pour certaines opérations, vous
remarquerez qu’il faut parfois patienter, tant que vous n’avez pas découvert certaines
techniques et astuces. Il n’est d’ailleurs jamais trop tôt pour au moins connaître l’existence de
certaines de ces techniques ; vous les retrouverez dans les chapitres ultérieurs, lorsque vous
commencerez à prendre en main l’ensemble du processus, et verrez que c’est une véritable
magie qui va permettre de faire parler les données d’une manière que vous ne soupçonnez pas
encore.
Établissement du contexte
Nous avons dit qu’il fallait trouver le contexte pour déployer une solution de datalogie viable. Il
s’agit d’une science appliquée et une approche abstraite ne conviendra sans doute pas. Bien
sûr, vous pourriez par vanité adopter un cluster Hadoop ou mettre en place un réseau neuronal
complexe et en faire la démonstration devant vos collègues. Il n’est pas sûr que ce genre de
solution ambitieuse résolve votre problème. Vous devez vérifier si un algorithme est bien
approprié, ou si la transformation des données que vous prévoyez convient effectivement. C’est
tout un art que d’examiner le problème de façon critique ainsi que les ressources, puis de créer
un environnement permettant d’obtenir une solution désirable.
L’expression solution désirable souligne le fait que vous pourriez tout à fait aboutir à des
solutions qui ne le sont pas, du fait qu’elles ne répondent pas à la demande, ou bien qu’elles y
répondent, mais en consommant trop de temps et de ressources. Voyons les grandes lignes du
processus permettant de créer le contexte du problème et des données.
Les humains sont habitués à considérer les données pour ce qu’elles sont, la plupart du temps : de
simples opinions. D’ailleurs, il arrive que les gens tordent la vérité des données au point de les rendre
inutilisables ; elles deviennent des incertitudes. Un ordinateur n’est pas capable de distinguer le vrai
du faux : il ne voit que des données. Lors de vos analyses, vous devez donc évaluer le degré de
véracité des données. Une chose que vous ferez dans tous les cas est de traquer les données
aberrantes pour les supprimer, mais cette technique ne suffit pas toujours à résoudre le problème. Un
humain qui exploite ces données d’entrée risque de déduire une vérité à partir de ces incertitudes.
Voici les cinq cas de données non fiables que vous pouvez rencontrer, en utilisant un accident de la
circulation en guise d’illustration :
» Embellissement : cette catégorie de dévoiement de la vérité est le fruit d’une tentative
volontaire de remplacer une information fiable par une information non fiable. Par exemple, lors
de la rédaction du constat amiable d’accident, un individu peut prétendre qu’il a été ébloui par le
soleil, ce qui l’a empêché d’éviter la collision. En réalité, cette personne était peut-être distraite,
par exemple en pensant à ce qu’elle allait faire en rentrant chez elle. Personne ne pourra la
contredire, et elle ne sera pas autant sanctionnée. Le résultat est que les données d’entrée sont
contaminées.
» Omission : l’introduction d’incertitude par omission se produit lorsqu’une personne ne ment sur
aucun point soulevé, mais oublie de mentionner un point essentiel pouvant changer la
perception globale de la situation. Si nous reprenons notre accident, supposons que la personne
ait embouti un sanglier, ce qui a endommagé sa voiture. Elle déclare à juste titre que la
chaussée était humide, que cela s’était passé au crépuscule et qu’elle avait un peu tardé à
freiner, sans compter que le sanglier a surgi d’un bosquet. La conclusion serait qu’il s’agit d’un
simple accident non responsable. Mais la personne a oublié de mentionner qu’elle était en train
d’envoyer un SMS. Si les services de police avaient été informés de cela, les causes de l’accident
auraient été différentes. Le conducteur aurait sans doute écopé d’une contravention et
l’assurance aurait renâclé à rembourser les réparations. Les données saisies dans la base de la
police n’auraient pas été les mêmes.
» Perspective : ce genre d’altération se produit lorsque plusieurs témoins voient le même
incident. Prenons l’exemple d’un accident impliquant un piéton et une voiture. Nous aurons trois
perspectives différentes : celle du conducteur, celle du piéton et celle du passant qui a tout vu.
L’officier de police obtiendrait trois versions différentes, en supposant bien sûr que chacune des
trois personnes dise la vérité. Ces divergences sont presque toujours la règle et le rapport que va
dresser l’officier va constituer une moyenne des trois points de vue, enrichie de son expérience
personnelle. Ce rapport sera donc proche de la vérité, mais proche seulement. Il y a lieu de
considérer les différents points de vue. Celui du conducteur lui permet de dire ce qu’il voyait sur
son tableau de bord et de dire qu’elle était la condition du véhicule à ce moment. Les deux
autres personnes ne connaissent pas ces informations. Le piéton qui a été heurté peut indiquer
l’expression du visage du conducteur au moment du choc. Enfin, le témoin est le mieux placé
pour savoir si le conducteur a tenté de l’éviter. Chacune des personnes doit faire sa déclaration
sans pouvoir tirer avantage des données qu’elle ne connaît pas.
» Biais : on parle de biais d’interprétation lorsqu’une personne peut voir la vérité mais que des
convictions et/ou des croyances personnelles l’obligent à la déformer. Toujours dans l’exemple de
l’accident, un conducteur peut tellement se concentrer sur sa trajectoire qu’il ne parvient même
plus à voir surgir le sanglier et n’a plus le temps de réagir. Cette catégorie de contre-vérités est
difficile à appréhender. Il est possible que le conducteur n’ait vraiment pas pu voir le sanglier car
il était tapi dans un bosquet. Il peut aussi ne pas l’avoir vu par inattention, alors qu’il arrivait en
terrain découvert. Autrement dit, la question n’est pas de savoir si le conducteur a vu le sanglier,
mais pourquoi il ne l’a pas vu. Il arrive que la vérification des sources d’une incertitude de biais
prenne de l’importance lorsqu’il s’agit de créer un algorithme qui doit éviter ce genre de sources
de données imprécises.
» Cadre de référence : cette catégorie ne correspond normalement pas à une erreur volontaire
ou pas, mais à un problème de compréhension. Une incertitude de cadre de référence est celle
qui se produit lorsqu’une personne décrit un événement ou un accident, mais que son
interlocuteur n’a aucune expérience avec ce genre d’événements, ce qui l’empêche de
comprendre la situation. Les quiproquos qui s’ensuivent sont abondamment exploités dans les
comédies. Il n’est vraiment pas facile de faire comprendre quelque chose à une personne
lorsqu’elle n’a pas de connaissances et d’expérience dans le domaine, c’est-à-dire de cadre de
référence.
Recherche de solutions
La datalogie est un domaine de connaissances complexes à l’intersection entre sciences
informatiques, mathématiques, statistiques et commerce. Rares sont ceux qui peuvent
maîtriser tous ces domaines à la fois. Vous devez chercher si quelqu’un d’autre n’a pas déjà eu
à faire face au même problème pour ne pas réinventer la roue. À partir du moment où vous
avez créé le contexte de votre projet, vous savez mieux ce que vous devez chercher et pouvez
puiser dans différentes sources.
» Lisez la documentation de Python. Vous trouverez peut-être des exemples qui vous
mettront sur une piste. Vous trouverez beaucoup de documentations en anglais avec des
exemples de datalogie sur les sites des librairies NumPy, SciPy, pandas et surtout Scikit-
learn. Voici les adresses de ces sites :
https://docs.scipy.org/doc/numpy/user/
https://docs.scipy.org/doc/
https://pandas.pydata.org/pandas-docs/version/0.23.2/
https://scikit-learn.org/stable/user_guide.html
» Cherchez sur le Web comment d’autres ont résolu le même genre de problème.
Cherchez par exemple des sites tels que Quora, Stack Overflow ou Cross Validated.
» Lisez les études des chercheurs. Vous pouvez chercher, par exemple, Google Scholar
ou Microsoft Academics. Vous y trouverez des articles qui expliquent comment préparer les
données ou donnent les détails des algorithmes les plus adaptés à chaque problème.
Bien que ce soit évident, votre solution doit d’abord répondre au problème. Lors de votre
recherche de solutions existantes, vous en trouverez qui semblent prometteuses, mais que
vous ne parviendrez pas à bien appliquer à votre situation car le contexte n’est pas exactement
le même. Votre jeu de données est peut-être incomplet ou pas suffisamment vaste. L’approche
d’analyse que vous choisissez peut ne pas bien répondre à la question posée, ou de manière
imprécise. N’hésitez pas à repartir dans vos recherches tout en progressant, au fur et à mesure
de vos tests, afin d’évaluer d’autres solutions compatibles et vos contraintes précises.
Combinaison de variables
Vos données d’entrée se présentent très rarement dans le format approprié à un algorithme.
Prenons une situation concrète : l’installation de plans de travail de cuisine. Le poids d’un plan
oblige à planifier l’affectation de 1, 2 ou 3 installateurs sur chaque chantier. Vous disposez de
deux tables d’entrée. La première contient les dimensions et les matériaux des différents plans
disponibles. La seconde contient les densités par matériau. Une seule personne ne suffit que
pour installer un plan de travail dont le poids ne dépasse pas certaines valeurs, et de plus, dont
la longueur ne dépasse pas une certaine limite. Votre objectif est de produire des prédictions
pour que l’entreprise organise au mieux la distribution de ses installateurs sur les différents
chantiers.
Vous allez créer un jeu de données à deux dimensions par combinaison de variables. Il n’y aura
que deux caractéristiques dans le jeu résultant. La première va informer sur les longueurs des
plans de travail. Une seule personne peut transporter un plan de 2 m au maximum, dans
certains matériaux. Il faudra deux personnes pour installer un plan plus long. L’autre
caractéristique va correspondre au poids (à la masse) du plan. Un plan de 2 m de long avec
une épaisseur de 28 cm peut être installé par une seule personne dans certains matériaux,
mais pas dans d’autres. Dans ce cas, il vous faudra deux personnes et si le plan est plus long,
trois.
La première caractéristique est facile à générer. Il suffit de récupérer les longueurs des
différents plans de travail du catalogue. La seconde caractéristique suppose de réaliser un
calcul à partir des variables des deux tables :
Votre jeu de données résultant permettra de connaître le poids pour chaque longueur de plan
dans chacun des matériaux disponibles. Cela vous permet de produire un modèle pour prédire,
donc planifier, le nombre d’installateurs.
Variables indicatrices
Une variable indicatrice est une variable booléenne, une caractéristique ne pouvant prendre
que la valeur 0 ou 1. C’est une sorte de pseudo-variable simplifiant l’analyse. Prenons comme
exemple un jeu de données qui distingue les individus majeurs des mineurs. Vous pouvez
remplacer la caractéristique correspondant à l’âge par une telle variable, et ainsi remplacer les
valeurs de 0 à 17 ans par la caractéristique booléenne 0 et les valeurs à partir de 18 ans par la
caractéristique 1.
Les variables indicatrices accélèrent les analyses et offre une plus grande précision pour la
création de catégories. En quelque sorte, elle remplace des niveaux de gris par du noir et
blanc. Une personne est soit majeure, soit mineure, après tout. Il s’agit donc d’une
simplification des données.
Distribution transformatrice
Une distribution consiste à organiser les valeurs d’une variable ou caractéristique permettant
de connaître la fréquence d’apparition des différentes valeurs pour mieux comprendre les
données. Il existe un grand nombre de distributions possibles et la plupart des algorithmes
savent les traiter (vous trouverez une galerie à l’adresse
https://www.itl.nist.gov/div898/handbook/eda/section3/eda366.htm). Il faut cependant
veiller à ce que l’algorithme soit compatible avec la distribution désirée.
Soyez particulièrement attentif devant les distributions trop uniformes ou biaisées car elles
sont difficiles à traiter pour plusieurs raisons. Au contraire, c’est la courbe en forme de cloche
qui sera votre amie. Dès que vous voyez qu’une distribution s’éloigne de cette distribution en
cloche, songez à lui appliquer une transformation.
Lorsque vous traitez une distribution, vous constaterez de temps à autre que les valeurs
semblent faussées, et l’algorithme que vous y appliquez ne produira pas un résultat conforme à
vos attentes. En transformant la distribution, vous appliquez une fonction à ces valeurs, par
exemple pour corriger leur dérive. La transformation rend la distribution plus exploitable,
faisant de votre jeu d’entrée une distribution normale. Voici quelques transformations qu’il est
conseillé de toujours appliquer à vos caractéristiques numériques :
» logarithmes np.log(x) et exponentielles np.exp(x) ;
» inverse 1/X, racine carrée np.sqrt(x) et racine cubique x**(1.0/3.0) ;
» transformations polynomiales telles que x**2, x**3, etc.
Vectorisation
Le processeur de votre ordinateur est capable de réaliser de façon très efficace certains
calculs. Vous allez en tirer profit si les données lui sont présentées dans un bon format. La
structure de stockage de données de NumPy nommée ndarray permet de mettre en place une
table de données à plusieurs dimensions. Vous pouvez ainsi construire un cube, voire un
hypercube lorsqu’il y a plus de trois dimensions.
Les calculs deviennent simples et rapides avec ce genre de tableau. Dans l’exemple suivant,
nous créons un jeu de données comprenant trois observations avec sept caractéristiques
(attributs) pour chacune. L’exemple calcule la valeur maximale de chaque observation puis la
soustrait à la valeur minimale pour produire la plage de valeurs.
import numpy as np
dataset = np.array([[2, 4, 6, 8, 3, 2, 5],
[7, 5, 3, 1, 6, 8, 0],
[1, 3, 2, 1, 0, 0, 8]])
print(np.max(dataset, axis=1) - np.min(dataset, axis=1))
L’instruction d’affichage obtient le maximum avec np.max() puis soustrait le minimum obtenu
par np.min(). Dans l’exemple, la valeur maximale de chaque observation vaut [8 8 8]et la
valeur minimale vaut [0 0 0]. Voici le résultat :
[6 8 8]
import numpy as np
a = np.array([15.0, 20.0, 22.0, 75.0, 40.0, 35.0])
a = a*.01
print(a)
Cet exemple commence par créer un tableau puis le remplit avec les valeurs entières qui
doivent devenir des pourcentages. Nous appliquons ensuite une multiplication par 0.01, ce qui
permet ensuite d’utiliser ces valeurs fractionnaires avec d’autres nombres, pour vous en servir
réellement en tant que pourcentages. Voici le résultat affiché :
import numpy as np
a = np.array([2, 4, 6, 8])
b = np.array([[1, 2, 3, 4],
[2, 3, 4, 5],
[3, 4, 5, 6],
[4, 5, 6, 7]])
c = np.dot(a, b)
print(c)
Le tableau défini en tant que vecteur doit l’être avant celui correspondant à la matrice, si vous
ne voulez pas provoquer d’erreur. Voici le résultat :
[ 60 80 100 120]
L’opération consiste à multiplier chacune des valeurs du premier tableau par la valeur de la
colonne correspondante dans la matrice. Autrement dit, la première valeur du tableau est
multipliée par la valeur de la première colonne en première ligne de la matrice. C’est ainsi que
la première valeur calculée est égale à 60, soit 2*1 + 4*2 + 6*3 + 8*4.
Multiplication de matrices
Vous pouvez également multiplier une matrice par une autre, le résultat correspondant à la
multiplication des lignes de la première matrice avec les colonnes de la seconde. Voici
comment procéder avec NumPy :
import numpy as np
a = np.array([[2, 4, 6, 8],
[1, 3, 5, 7]])
b = np.array ([[1, 2],
[2, 3],
[3, 4],
[4, 5]])
c = np.dot(a, b)
print(c)
Le résultat est une matrice de 2 sur 2. Dans l’exemple, cette matrice contiendra les valeurs
suivantes :
[[60 80]
[50 66]]
Chaque ligne de la première matrice est multipliée par chacune des colonnes de la seconde.
Par exemple, la valeur 50 de la seconde ligne des résultats correspond à la multiplication des
valeurs de la ligne 2 de la matrice a par celles de la colonne 1 de la matrice b, comme ceci :
1*1 + 3*2 + 5*3 + 7*4.
PARTIE 3
Visualisation des informations
L a plupart des gens parviennent mieux à digérer des informations sous forme graphique que
sous forme textuelle. Un graphique ou diagramme aide à voir les relations entre les éléments
et à faire des comparaisons, car il s’agit d’être efficace dans vos moyens de communication.
Rien ne sert de passer des heures à préparer, analyser et restituer des données si vous êtes le
seul à pouvoir en tirer profit. Python facilite la création de diagrammes à partir de vos données
texte grâce à la librairie MatPlotLib, une émulation de l’application complète Matlab. Une
comparaison des deux est disponible à l’adresse
https://phillipmfeldman.org/Python/Advantages_of_Python_Over_Matlab.html.
Si vous connaissez déjà l’application MATLAB, vous maîtriserez rapidement MatPlotLib, car
cette librairie utilise le même concept de machine à changement d’état, et demande de définir
les éléments des diagrammes de la même façon. Certains considèrent même que MatPlotLib
est supérieur à MATLAB du fait que le nombre de lignes de code source à écrire est inférieur.
D’autres ont remarqué que le passage de MATLAB à MatPlotLib était facile. Vous pouvez tout à
fait combiner les deux outils : faire des expérimentations sur vos données avec MATLAB puis
créer votre application avec Python et MatPlotLib. C’est une question de goût.
Dans ce chapitre, nous allons découvrir ce qu’il est possible de faire avec MatPlotLib. Nous
nous servirons de cette librairie plusieurs fois dans la suite du livre, aussi ne réaliserons-nous
ici qu’une visite rapide mais cependant essentielle. De ce fait, si vous connaissez déjà MATLAB,
vous pourrez certainement parcourir rapidement ce chapitre, et même en ignorer certaines
sections. Dans tous les cas, la connaissance de ce chapitre est un prérequis pour l’utilisation de
MatPlotLib dans les chapitres suivants.
Le fichier archive des exemples de ce chapitre correspond à PYDASC_10. Nous avons expliqué
dans l’introduction comment récupérer cette archive.
Dans cet exemple, nous utilisons la fonction plt.plot() pour créer un tracé avec des valeurs
selon l’axe x entre 1 et 11 ; pour l’axe vertical y, nous puisons dans les variables du jeu de
données. En appelant plot.show(), nous faisons afficher le diagramme dans un encadré sous
la cellule de code (parfois dans une fenêtre indépendante). Voyez la Figure 10.1. Par défaut, le
style graphique est un diagramme à lignes. Nous verrons d’autres types de diagrammes dans
le Chapitre 11.
Lorsque vous exécutez cet exemple, vous voyez deux lignes (Figure 10.2). Dans la version
papier du livre, les lignes sont représentées dans deux niveaux de gris différents, mais en
réalité, elles sont en couleurs.
Vous devez fournir deux paramètres au moins. Le premier est bien sûr le nom du fichier
destinataire, éventuellement précédé d’un chemin d’accès. Le second paramètre décide du
format du fichier. Dans l’exemple, nous choisissons le format très répandu PNG (Portable
Network Graphic). Plusieurs autres formats sont disponibles : PDF, PostScript (PS), Postscript
encapsulé (EPS) et SVG (Scalable Vector Graphics).
Vous aurez noté, à la deuxième ligne, que la fonction magique %matplotlib est dotée du
paramètre auto au lieu du paramètre inline des deux premiers exemples. Cette instruction
permet de choisir le moteur de rendu (d’affichage) en coulisses (backend). Dans cet exemple,
l’affichage n’est plus réalisé dans le calepin. Différentes options sont possibles pour le choix du
moteur de rendu, en fonction de la version de Python et de MatPlotLib en vigueur. Certains
programmeurs préfèrent le paramètre notebook à la place de inline. Pour utiliser Notebook,
il faut redémarrer le noyau, et le résultat n’est pas toujours conforme aux attentes. La liste des
différents moteurs disponibles peut être affichée au moyen de %matplotlib -l. De plus, vous
désactivez les interactions avec le tracé par un appel à plt.ioff().
Nous avons stocké l’objet incarnant les axes dans une variable ax, ce qui est préférable à la
manipulation directe, car le code sera plus simple. Par exemple, nous activons les axes par
défaut en appelant plt.axes(). Nous récupérons un identifiant de ces axes dans la variable
ax. Rappelons qu’un identifiant est une sorte de poignée de préhension permettant de
manipuler un élément du programme. En programmation, un tel identifiant correspond à un
pointeur qui contient une adresse mémoire (handle).
Nous intervenons sur les limites des axes au moyen d’un appel à set_xlim() puis à
set_ylim(), c’est-à-dire sur la longueur de chaque axe. Nous contrôlons les repères le long de
chaque axe avec set_xticks() et set_yticks(). Vous avez une liberté totale quant aux
légendes de chaque repère sur l’axe. La Figure 10.3 montre le résultat de cet exemple.
Comparez-le avec celui de la Figure 10.2.
L’instruction %matlplotlib notebook donne un aspect final assez différent. Des boutons de
contrôle sous le diagramme permettent de faire défiler et de zoomer, passer d’une vue à une
autre et sauvegarder le diagramme sur disque. Au-dessus du diagramme, un bouton sur le bord
droit de la barre permet de quitter le mode interactif ou mode d’édition. Vos retouches sont
mémorisées et le diagramme se présentera tel que vous l’avez prévu. Vous sortez aussi du
mode interactif dès que vous demandez l’affichage d’un autre diagramme.
N. d. T. : En cas de souci avec le mode notebook, rebasculez en mode de rendu inline ou
auto.
Il suffit d’appeler la fonction grid(). Comme la plupart des autres fonctions de MatPlotLib,
vous pouvez lui fournir d’autres paramètres pour adapter la grille à vos convenances, par
exemple n’ajouter des lignes d’instance que sur l’axe x plutôt que sur les deux. La
Figure 10.4 montre le résultat du précédent exemple.
DIAGRAMMES ET HANDICAP
Il est essentiel de penser aux personnes souffrant d’un handicap visuel, par exemple concernant la
discrimination des couleurs. Un daltonien ne pourra pas distinguer une ligne verte d’une ligne rouge.
Un malvoyant ne distinguera pas une ligne en pointillé d’une ligne tiretée. Cherchez donc à combiner
plusieurs modes pour aider à distinguer les tracés afin d’augmenter les chances d’être compris du
plus grand nombre.
Paramètre Style
‘-’ Trait plein
‘--’ Tireté
‘-.’ Tiret-point
‘:’ Pointillé
Le style de tracé doit être le troisième paramètre de la fonction plot(). Il suffit de fournir la
chaîne de caractères contenant le symbole désiré, comme le montre l’exemple suivant.
Dans l’exemple, la première série est tracée avec un style tireté et la seconde avec un style
pointillé (Figure 10.5).
Paramètre Couleur
‘b’ Bleu
‘g’ Vert
‘r’ Rouge
‘c’ Cyan
‘m’ Magenta
‘y’ Jaune
‘k’ Noir
‘w’ Blanc
Comme pour les styles de tracé, vous fournissez votre valeur dans le troisième paramètre de la
fonction plot(). Dans notre exemple, il y aura une ligne rouge et une magenta. Le résultat
reste celui de la Figure 10.2, mais avec deux couleurs de tracé différentes. Dans la version
imprimée du livre, vous ne voyez que des niveaux de gris différents.
Ajout de marqueurs
Les marqueurs sont des symboles spéciaux qui peuvent être associés à chaque point de
données d’un diagramme en lignes. Vous avez bien moins de souci à vous faire avec les
marqueurs qu’avec les lignes et les couleurs au niveau de l’accessibilité. Même lorsque le
marqueur n’est pas facile à voir, il reste en général facile à distinguer des autres marqueurs.
Le tableau suivant montre les marqueurs disponibles dans MatPlotLib.
Tableau 10.3 : Marqueurs de MatPlotLib.
Comme pour les tracés et les couleurs, votre paramètre de marqueur entre dans le troisième
paramètre de la fonction plot(). L’exemple suivant combine justement un style de ligne et un
de marqueur pour personnaliser chacun des deux tracés.
La Figure 10.6 montre clairement que l’ajout de ces marqueurs combiné à la différence de
styles de trait rend le résultat beaucoup plus lisible, même sur une imprimante monochrome.
Figure 10.6 : Les marqueurs ajoutent encore à la lisibilité des valeurs individuelles.
Ajout de labels, d’annotations et de légendes
Pour rendre vos diagrammes encore plus accessibles, vous devez y ajouter des labels d’axes,
des annotations et des cartouches de légendes. Voici à quoi servent ces différents
compléments :
» Labels d’axes : permettent d’identifier un groupe ou une série de données, afin que le
lecteur sache exactement quel type d’information lui est proposé.
» Annotations : servent à fournir une information complémentaire au sujet des données.
Les annotations assurent la bonne compréhension de certains points du diagramme.
» Cartouche de légendes : fournit les noms des groupes ou série de données tracées en
indiquant en général, le type ou la couleur de trait utilisé.
Découvrons les compléments que MatPlotLib permet d’ajouter à votre diagramme. Tous les
diagrammes n’ont pas besoin de tous ces compléments, mais n’hésitez pas à ajouter ceux qui
semblent nécessaires pour transmettre le plus clairement possible le fruit de votre travail
d’analyse.
L’appel à xlabel() documente l’axe x et celui à ylabel() documente l’autre (Figure 10.7).
Ajout d’annotations
Une annotation sert à mettre l’accent sur un endroit en particulier dans le diagramme, par
exemple un point de données dont la valeur déborde de la plage prévue. L’exemple suivant
ajoute une annotation.
Nous appelons la fonction legend() après avoir réalisé les deux tracés, et surtout pas avant
(alors que c’était le contraire pour d’autres fonctions de ce chapitre). Notez que vous devez
prévoir une variable pour recevoir l’identifiant de chacun des deux tracés. C’est ainsi que
line1 reçoit l’identifiant renvoyé par le premier appel à legend() et line2 reçoit celui du
second appel.
Par défaut, le cartouche est positionné dans le coin supérieur droit, ce qui n’est pas idéal dans
notre exemple. Grâce au paramètre loc, nous pouvons choisir une autre localisation, ici, le
coin inférieur droit correspondant à la valeur 4. Pour les autres emplacements, voyez la
documentation de la fonction
(https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend). Le résultat
est visible dans la Figure 10.9.
Figure 10.9 : Ajout d’un cartouche de légende pour identifier les deux tracés.
Chapitre 11
Visualisation des données
DANS CE CHAPITRE :
» Choix du type de diagramme approprié
Nous allons bien sûr nous intéresser aux représentations les plus utiles en datalogie :
histogrammes et camemberts, mais également nuages et boîtes à moustaches qui permettent
de repérer plus aisément les motifs remarquables. Du fait que vous allez souvent travailler
avec des données temporelles ou géographiques, nous dédions une section à chacun de ces
deux sujets. Nous verrons enfin comment produire des graphes orientés et non orientés, très
prisés dans les analyses sociologiques des médias en ligne.
Le calepin des exemples de ce chapitre correspond au fichier PYDASC_11.
Le paramètre indispensable correspond bien sûr aux valeurs, et vous pourriez produire un
camembert minimal en ne fournissant que ce paramètre.
Le paramètre couleurs permet de choisir la couleur de chaque tranche et le paramètre
legaxe d’identifier chacune d’elles. Vous aurez souvent besoin de faire ressortir une des
valeurs, ce que permet le paramètre explose avec sa liste de valeurs d’écartement. La
valeur 0 maintient la part collée aux autres.
Parmi les informations qui peuvent être associées à chaque part, la plus importante est le
pourcentage numérique, que fait afficher le paramètre autopct. Notez que vous devez lui
fournir une chaîne de formatage.
N.d.T. : Dans cette chaîne, les valeurs numériques indiquent le nombre minimal de chiffres
avant la virgule (le point décimal) et le nombre maximal après ce séparateur. La lettre f
signifie qu’il faut afficher les valeurs en tant que numériques à virgule flottante
(fractionnaires) et le redoublement du second signe pourcentage permet d’afficher ce signe au
lieu de laisser sa fonction habituelle de métacaractère marquant une position.
Parmi les autres paramètres, counterclock qui est un booléen permet de choisir de répartir
les parts en sens horaire ou antihoraire. Le paramètre shadow demande d’afficher une ombre
sous le camembert pour donner un effet de relief. D’autres paramètres sont décrits à l’adresse
https://matplotlib.org/api/pyplot_api.html.
Il est bien sûr souvent indispensable d’ajouter un titre, en utilisant la fonction title(). Le
résultat est visible en Figure 11.1.
plt.show()
Vous devez au minimum fournir la série de coordonnées x et les hauteurs des barres. Notre
exemple utilise la fonction range() pour créer les coordonnées en x et la variable valeur
pour les hauteurs.
En guise de personnalisation, nous utilisons le paramètre de largeur width qui permet de
décider des largeurs de chaque barre. Dans l’exemple, la deuxième barre est rendue un peu
plus large pour la distinguer même dans une impression monochrome. Nous utilisons le
paramètre color pour modifier la couleur de cette barre qui apparaît en rouge, les autres
restant en bleu.
Comme les autres types de diagrammes, un certain nombre d’options sont possibles. Dans
l’exemple, nous utilisons le paramètre align pour centrer les données selon l’axe x au lieu de
les laisser alignées à gauche. Parmi les autres paramètres, citons hatch qui améliore le rendu
visuel. La Figure 11.2 montre l’affichage résultant.
Ce chapitre ne montre que les grands principes de MatPlotLib pour créer des diagrammes.
Vous trouverez d’autres exemples sur le site de référence
(https://matplotlib.org/1.2.1/examples/index.html). Vous remarquerez quelques
exemples très sophistiqués, notamment ceux qui incorporent de l’animation. Avec un peu de
pratique, vous pourrez vous aussi les exploiter pour représenter vos données.
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
x = 20 * np.random.randn(10000)
Les données d’entrée sont choisies au hasard et la distribution doit produire une courbe de
type cloche. Vous devez fournir la série de valeurs x. Le deuxième paramètre de la fonction
hist() spécifie le nombre de groupes (bins). La valeur implicite est égale à 10. Le paramètre
range limite la représentation aux valeurs principales, en ignorant les valeurs aberrantes.
Plusieurs types d’histogrammes sont disponibles. La valeur par défaut crée un histogramme de
type diagramme en barres simples. Vous pouvez également créer des barres empilées, étagées
ou étagées et remplies (type de l’exemple). Vous pouvez enfin choisir l’orientation (verticale
par défaut).
Comme la plupart des autres diagrammes du chapitre, vous disposez de certaines options. Le
paramètre align détermine la position de chaque barre par rapport à la ligne de base. Le
paramètre color sert à choisir les couleurs des barres. Enfin, le paramètre label ne sera
visible que si vous demandez également le cartouche de légendes (comme dans l’exemple de la
Figure 11.3).
Figure 11.3 : Histogramme visualisant la distribution des valeurs.
Le diagramme sera légèrement différent à chaque exécution, puisqu’il part de valeurs choisies
de façon pseudo-aléatoire.
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Pour cet exemple, il nous faut préparer un jeu de données d’entrée en combinant plusieurs
techniques de production de valeurs. Voici comment nous utilisons ces techniques :
» spread : contient un jeu de valeurs aléatoires entre 0 et 100 ;
» center : génère 50 valeurs directement au centre de la plage de 50 ;
» flier_high : simule des valeurs aberrantes entre 100 et 200 ;
» flier_low: simule des valeurs aberrantes entre 0 et 100.
Ces différents lots de valeurs sont réunis en un seul jeu de données au moyen de la fonction
concatenate(). Ce jeu est généré aléatoirement, mais il possède des caractéristiques
prédéfinies, et notamment un grand nombre de valeurs vers le milieu. Il variera donc d’une
exécution à l’autre, mais conviendra pour notre exemple.
L’appel à la fonction de tracé boxplot() n’a besoin que du paramètre data pour les valeurs.
Les autres paramètres ont des valeurs par défaut. Dans l’exemple, les valeurs aberrantes sont
des X verts grâce au paramètre sym. Le paramètre width contrôle la taille de la boîte (que
nous avons volontairement énormément étirée en largeur pour la rendre plus visible). Enfin, le
paramètre notch permet d’ajouter une encoche sur les flancs de la boîte (ce paramètre vaut
false par défaut). La Figure 11.4 montre un résultat possible.
Figure 11.4 : Boîte à moustaches visualisant des groupes de valeurs.
Dans la boîte, la ligne rouge au milieu correspond aux valeurs médianes. Les deux lignes
horizontales séparées de la boîte par des traits indiquent les limites supérieure et inférieure
pour les quatre quartiles. Les valeurs aberrantes sont visibles au-dessus et en dessous sous
forme de signes X.
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
x1 = 5 * np.random.rand(40)
x2 = 5 * np.random.rand(40) + 25
x3 = 25 * np.random.rand(20)
x = np.concatenate((x1, x2, x3))
y1 = 5 * np.random.rand(40)
y2 = 5 * np.random.rand(40) + 25
y3 = 25 * np.random.rand(20)
y = np.concatenate((y1, y2, y3))
L’exemple commence par générer des coordonnées x, y aléatoires. Pour chaque coordonnée x,
il faut disposer d’une coordonnée y. Vous pouvez tout à fait produire un tel diagramme
uniquement avec ces jeux de coordonnées.
Pour personnaliser le diagramme, servez-vous par exemple du paramètre s qui décide de la
taille de chaque point de données. Le paramètre marker contrôle la forme du point et le
paramètre c définit les couleurs de tous les points (mais vous pouvez aussi choisir les couleurs
individuellement). Le résultat est visible dans la Figure 11.5.
Figure 11.5 : Diagramme en nuage montrant des groupes de données et donc des motifs.
Visualisation de groupes
Dans un diagramme en nuage, la troisième série de données (le troisième axe) est incarnée par
différentes couleurs. Grâce à ces couleurs, vous allez pouvoir mettre en valeur certains
groupes pour les distinguer des autres. L’exemple qui suit montre comment profiter de cette
possibilité dans un nuage de points :
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
x1 = 5 * np.random.rand(50)
x2 = 5 * np.random.rand(50) + 25
x3 = 30 * np.random.rand(25)
x = np.concatenate((x1, x2, x3))
y1 = 5 * np.random.rand(50)
y2 = 5 * np.random.rand(50) + 25
y3 = 30 * np.random.rand(25)
y = np.concatenate((y1, y2, y3))
L’exemple ressemble beaucoup au précédent, hormis des valeurs un peu différentes, et surtout
une nouvelle ligne pour définir un tableau de couleurs. Dans la version imprimée du livre, vous
distinguerez dans le résultat les valeurs par des niveaux de gris (Figure 11.6). Le premier
groupe est en bleu, le second en vert et le dernier en rouge.
Figure 11.6 : Tableau de couleurs pour distinguer les groupes.
Visualisation de corrélations
Vous aurez parfois besoin de visualiser une tendance générale, d’après les données du nuage.
En effet, même si les différents groupes sont faciles à distinguer, cela ne permet pas toujours
d’en déduire une direction, une orientation globale. Mais vous pouvez ajouter une ligne de
tendance comme le fait l’exemple suivant. Nous avons modifié le jeu de données d’entrée pour
que les groupes soient moins faciles à distinguer, justifiant ainsi l’ajout de la ligne de tendance
(Figure 11.7).
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.pylab as plb
%matplotlib inline
x1 = 15 * np.random.rand(50)
x2 = 15 * np.random.rand(50) + 15
x3 = 30 * np.random.rand(25)
x = np.concatenate((x1, x2, x3))
y1 = 15 * np.random.rand(50)
y2 = 15 * np.random.rand(50) + 15
y3 = 30 * np.random.rand(25)
y = np.concatenate((y1, y2, y3))
plt.show()
Le code source reste très proche des deux précédents, sauf que les données empêchent de
bien distinguer les groupes. Pour ajouter la ligne, nous appelons la fonction polyfit() en lui
fournissant les données. La fonction renvoie un vecteur de coefficients p destiné à réduire
l’erreur des moindres carrés. (La régression par moindres carrés est une méthode permettant
de trouver une ligne qui fait la somme des relations entre deux variables x et y, au moins dans
le domaine de la variable explicative x. Le troisième paramètre de polyfit() détermine le
degré d’adaptation polynomiale.
Le vecteur produit par polyfit() est utilisé en entrée de polyid() pour calculer les points de
données réels selon l’axe y. Il ne reste qu’à créer la ligne de tendance avec un appel à plot()
(Figure 11.7).
Figure 11.7 : Nuage de points avec une ligne de tendance générale.
import pandas as pd
import matplotlib.pyplot as plt
import datetime as dt
%matplotlib inline
Nous commençons par générer notre source de données à la volée vers une structure
DataFrame (mais n’importe quelle autre source peut bien sûr convenir). Vous remarquez que
nous créons une plage de dates dans la variable date_plage pour mémoriser les bornes
inférieure et supérieure des dates, ce qui permettra un traitement plus efficace dans une
boucle for. L’exemple crée le cadre de données dans la variable df.
Figure 11.8 : Diagramme d’évolution des données dans le temps.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import datetime as dt
%matplotlib inline
plt.xlabel(‘Date ventes’)
plt.ylabel(‘Volume ventes’)
plt.title(‘Jours’)
plt.legend([‘Ventes’, ‘Tendance’])
plt.show()
La section précédente qui abordait les corrélations montrait la technique généralement utilisée
pour ajouter une tendance. Dans certains cas, le rendu visuel n’est pas le plus lisible. Le
présent exemple adopte une autre approche : il ajoute directement la ligne de tendance dans la
structure DataFrame. Si vous demandez l’affichage de df après la ligne df[‘trend’] =
tendance, vous verrez apparaître une série de données ressemblant à la suivante :
Ventes Tendance
2020-07-29 37 9.454545
2020-07-30 8 12.242424
2020-07-31 7 15.030303
2020-08-01 0 17.818182
2020-08-02 3 20.606061
2020-08-03 24 23.393939
2020-08-04 40 26.181818
2020-08-05 19 28.969697
2020-08-06 45 31.757576
2020-08-07 37 34.545455
Avec cette approche, les tracés deviennent plus simples. Vous n’avez à appeler plot() qu’une
fois et n’avez plus besoin de recourir à la partie pylab de MatPlotLib (revoyez si nécessaire la
partie décrivant les corrélations). Le code source est plus lisible et le résultat risque moins
d’entraîner les désagréments que l’on voit fréquemment dans les diagrammes publiés sur le
Web.
Dans le premier exemple de corrélations, l’appel à plot() faisait générer automatiquement
une légende, mais MatPlotLib n’ajoute pas automatiquement de ligne de tendance. Voilà
pourquoi il faut créer une nouvelle légende pour le tracé. La Figure 11.9 montre le résultat
produit à partir de données générées aléatoirement.
Vous demandez ainsi la création d’un nouvel environnement Basemap utilisant Python en
version 3 et Anaconda en version 5.2. (Il est possible que ce soit les mêmes numéros de
version que votre configuration de base.)
3. Si vous êtes sous MacOS X ou sous Linux, saisissez la commande suivante :
Une fois que le kit est installé, les programmes qui veulent l’utiliser doivent absolument
commencer par les quatre directives suivantes :
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
%matplotlib inline
Gestion des librairies dépréciées
Le langage Python possède, parmi ses nombreux avantages, celui d’être entouré d’un très
grand nombre de paquetages complémentaires. Cependant, tous ces paquetages ne sont pas
mis à jour suffisamment vite pour éviter de voir apparaître des fonctions dépréciées dans
d’autres paquetages qui en dépendent. Une fonction dépréciée est une fonction qui existe dans
le paquetage concerné, mais que les auteurs du paquetage se préparent à supprimer ou à
remplacer. C’est la raison pour laquelle vous voyez des messages vous avertissant de cette
obsolescence. Ces messages n’empêchent pas le code d’être exécuté, mais les utilisateurs de
votre programme peuvent s’alarmer. Personne n’aime voir apparaître un message d’erreur au
milieu des résultats, d’autant plus que l’application Notebook affiche les messages en rouge
clair par défaut.
Il est hélas possible que le paquetage Basemap que vous avez installé soit à l’origine d’une
fonction dépréciée et donc d’un message. Cette section vise à corriger cette situation. Pour en
savoir plus au sujet des problèmes éventuels, vous pouvez consulter la page
https://github.com/matplotlib/basemap/issues/382. Voici l’allure générale d’un tel
message :
C:\Users\Luca\Anaconda3\lib\site-packages\mpl_toolkits
\basemap\__init__.py:1708: MatplotlibDeprecationWarning:
The axesPatch function was deprecated in version 2.1.
Use Axes.patch instead.
limb = ax.axesPatch
C:\Users\Luca\Anaconda3\lib\site-packages\mpl_toolkits
\basemap\__init__.py:1711: MatplotlibDeprecationWarning:
The axesPatch function was deprecated in version 2.1. Use
Axes.patch instead.
if limb is not ax.axesPatch:
Ce texte peut sembler terrifiant, mais il désigne simplement deux problèmes. Le premier
concerne la librairie MapPlotLib et notamment l’appel à axesPatch(). Les messages indiquent
en outre que cet appel a été déprécié depuis la version 2.1. Saisissez donc les deux commandes
suivantes pour vérifier la version de MatPlotLib :
import matplotlib
print(matplotlib.__version__)
Si vous avez bien installé Anaconda comme indiqué dans le Chapitre 3, vous devriez avoir au
minimum une version 2.2.2 de MatPlotLib. Il s’agit donc de revenir en arrière volontairement
sur la version de MatPlotLib en utilisant la commande suivante dans la fenêtre Anaconda
Prompt :
Bien sûr, ce rétrogradage peut créer des problèmes dans un autre code source qui utilise une
des nouveautés de MatPlotLib. La solution n’est pas idéale, mais elle peut vous sauver si vous
utilisez intensivement Basemap dans votre projet.
Une bonne solution consiste à simplement admettre qu’il y a un souci en documentant cela
dans le code source. Vous serez ainsi prêt à réagir au problème lors de la prochaine mise à jour
d’un paquetage. Il suffit d’ajouter les deux lignes de code source suivantes :
import warnings
warnings.filterwarnings(“ignore”)
L’appel à la fonction filterwarnings() réalise l’action demandée qui consiste ici à ignorer les
messages d’avertissement. Pour annuler ce masquage, vous utilisez la fonction
resetwarnings(). L’attribut nommé module de cette fonction est le même que celui qui est la
source des problèmes. Vous pouvez définir un masque plus vaste avec l’attribut category.
Dans notre exemple, le masque est restreint à un seul module.
m = Basemap(projection=’merc’,llcrnrlat=10,urcrnrlat=50,
llcrnrlon=-160,urcrnrlon=-60)
m.drawcoastlines()
m.fillcontinents(color=’lightgray’,lake_color=’lightblue’)
m.drawparallels(np.arange(-90.,91.,30.))
m.drawmeridians(np.arange(-180.,181.,60.))
m.drawmapboundary(fill_color=’aqua’)
m.drawcountries()
plt.title(“Projection de Mercator”)
plt.show()
Si nécessaire, ajoutez en début de ce code source les quatre lignes de test quelques pages en
arrière (trois directives d’import et une directive magique %matplotlib).
L’exemple commence par définir les coordonnées de cinq villes puis crée le fond de carte. Le
paramètre projection définit l’aspect général, les quatre paramètres llcrnrlat, urcrnrlat,
llcrnrlon et urcrnrlon règlent les bords de la carte. D’autres paramètres sont possibles en
option.
Les actions suivantes servent à personnaliser la carte. La fonction drawcoastlines() rend les
lignes côtières mieux visibles. Pour mieux distinguer la terre ferme de l’eau, vous appelez
fillcontinents() en choisissant vos couleurs. La fonction drawcountries() permet de
montrer les limites des états, ce dont nous avons besoin ici. Vous disposez à partir de ce
moment d’une carte prête à recevoir vos données.
Dans l’exemple, nous créons les coordonnées en x et en y à partir de celles définies pour les
cinq villes un peu plus haut. Nous affichons ensuite les positions sur la carte dans une couleur
différente. Il ne reste plus ensuite qu’à afficher le résultat (Figure 11.10).
Figure 11.10 : Insertion d’un jeu de données sur un fond de carte géographique.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G = nx.Graph()
H = nx.Graph()
G.add_node(1)
G.add_nodes_from([2, 3])
G.add_nodes_from(range(4, 7))
H.add_node(7)
G.add_nodes_from(H)
G.add_edge(1, 2)
G.add_edge(1, 1)
G.add_edges_from([(2,3), (3,6), (4,6), (5,6)])
H.add_edges_from([(4,7), (5,7), (6,7)])
G.add_edges_from(H.edges())
nx.draw_networkx(G)
plt.show()
Dans le Chapitre 8, nous avions utilisé la librairie NetworkX pour construire un exemple. Ici,
nous utilisons plusieurs techniques. Nous commençons par importer cette librairie NetworkX
puis nous appelons le constructeur nommé Graph() qui attend plusieurs paramètres d’entrée
qui vont servir d’attributs. Notez que vous pouvez créer un graphe tout à fait exploitable sans
mentionner aucun de ces attributs (c’est d’ailleurs ce que nous faisons).
Pour ajouter un nœud, nous appelons add_node() en indiquant le numéro de nœud. Rien ne
vous empêche d’indiquer une liste, un dictionnaire ou une plage de nœuds avec range(), en
utilisant add_nodes_from(). Vous pouvez même importer des nœuds depuis un autre graphe.
Dans l’exemple, nous utilisons des nombres pour les nœuds, mais vous pouvez tout à fait
indiquer des lettres, des chaînes de caractères ou même des dates. En revanche, vous ne
pouvez pas utiliser de valeurs booléennes pour un nœud.
Au départ, il n’y a pas de lien autour d’un nouveau nœud. Pour en ajouter, vous utilisez la
fonction add_edge() en précisant les numéros des nœuds à relier. Comme pour les nœuds,
vous pouvez en ajouter plusieurs en un geste, au moyen de la fonction add_edges_from(). Vous
spécifiez alors une liste, un dictionnaire ou un autre graphe. Le résultat de l’exemple est
montré en Figure 11.11. (Ce résultat varie d’une exécution à l’autre, mais les connexions sont
les mêmes.)
Figure 11.11 : Graphe non orienté avec liens.
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline
G = nx.DiGraph()
G.add_node(1)
G.add_nodes_from([2, 3])
G.add_nodes_from(range(4, 6))
G.add_path([6, 7, 8])
G.add_edge(1, 2)
G.add_edges_from([(1,4), (4,5), (2,3), (3,6), (5,6)])
Nous commençons par créer un graphe orienté par un appel au constructeur DiGraph(). Notez
au passage que le paquetage NetworkX sait également gérer les types MultiGraph() et
MultiDiGraph(). Tous les types sont présentés dans la page https://mriduls-
networkx.readthedocs.io/en/latest/_modules/index.html.
L’ajout des nœuds se réalise de la même façon que pour un graphe non orienté. Pour en ajouter
un seul, vous utilisez add_node() et pour en ajouter plusieurs, add_nodes_from(). Pour
ajouter des nœuds et des liens en même temps, vous utilisez add_path(). Notez que l’ordre
des nœuds est essentiel. La circulation d’un nœud à l’autre va de gauche à droite dans la liste
fournie en paramètre.
L’ajout des liens se fait, comme dans un graphe non orienté, avec add_edge() pour un seul, et
add_edges_from() pour en ajouter plusieurs. L’ordre dans lequel les numéros des nœuds sont
fournis est critique. Le flux va de gauche à droite dans chaque paire de nœuds.
L’exemple ajoute des couleurs particulières pour les nœuds, des labels et utilise une seule
forme de point (avec contrôle de leur taille). La création proprement dite passe par un appel à
draw_networkx(). Notez qu’il faut donner la valeur True à with_labels pour que les légendes
du paramètre labels soient visibles. Le résultat est donné dans la Figure 11.12.
L es chapitres précédents nous ont permis d’apprendre les techniques de base pour charger
les données, puis les manipuler en langage Python. Nous pouvons maintenant nous
intéresser à des outils plus complexes pour manipuler les données et faire de l’apprentissage
machine. Le but final de la plupart des projets de datalogie est, rappelons-le, la construction
d’un outil capable automatiquement de résumer, de prédire et de recommander des décisions
par simple analyse des données.
Avant d’en arriver là, il reste encore à retravailler les données en y appliquant des
transformations encore plus radicales. Nous entrons là dans le domaine de la reformulation
des données ; il s’agit de transformations sophistiquées suivies d’une analyse visuelle et
statistique, suivie à son tour d’autres transformations. Nous allons voir comment exploiter
d’énormes flux de texte, nous allons découvrir les caractéristiques fondamentales d’un jeu de
données, optimiser la vitesse des expériences, compresser les données et créer de nouvelles
caractéristiques de synthèse, de nouveaux groupes et classifications et enfin détecter des cas
inattendus ou exceptionnels qui pourraient créer du tort à la bonne conclusion de votre projet.
À partir de maintenant, nous allons utiliser beaucoup plus le paquetage nommé Scikit-learn (la
documentation complète est à l’adresse
https://scikitlearn.org/stable/documentation.html). Ce paquetage propose un
référentiel unique qui réunit quasiment tous les outils dont un datalogue a besoin. Nous
verrons dans ce chapitre les caractéristiques essentielles de Scikit-learn, qui est structuré en
modules, classes et fonctions. Nous verrons également quelques techniques pour gagner du
temps dans Python en améliorant ses performances. Elles se basent sur des données
volumineuses non structurées et utilisent des calculs très gourmands en puissance.
Le fichier calepin contenant le code source des exemples de ce chapitre porte le nom
PYDASC_12.
Découverte de Scikit-learn
Parfois, le moyen le plus efficace pour découvrir un nouvel outil consiste à jouer avec, d’autant
plus si l’outil est complexe. Il n’est donc pas inutile de commencer par essayer le paquetage
Scikit-learn et les opérations mathématiques complexes qu’il permet. C’est ce que nous vous
proposons de faire dans les sections qui suivent, tout en découvrant au passage les concepts
fondamentaux de l’outil, et son grand intérêt dans le domaine de la datalogie.
Chaque classe de base définit ses propres méthodes et attributs. Les possibilités
fondamentales de traitement des données d’apprentissage sont en revanche garanties par
quelques séries de méthodes et attributs correspondant aux interfaces. Ces interfaces
constituent un guichet d’accès par programmation, c’est-à-dire une API (Application
Programming Interface). Ce sont ces interfaces qui garantissent l’homogénéité des méthodes
et attributs entre les différents algorithmes du paquetage. Ces interfaces orientées objet de
Scikit-learn sont au nombre de quatre :
» estimator : pour ajuster les paramètres en les apprenant à partir des données grâces à
l’algorithme ;
» predictor : pour générer des prédictions à partir des paramètres ajustés ;
» transformator : pour transformer les données en utilisant les paramètres ajustés ;
» model : pour rendre compte de la qualité d’ajustement ou d’autres points de mesure.
Dans le paquetage, les algorithmes sont groupés selon la classe de base avec plusieurs objets
d’interface dans des modules. Chaque module constitue une spécialisation pour la résolution
d’un certain genre d’apprentissage machine. C’est ainsi que le module linear_model sert à la
modélisation linéaire alors que le module metrics sert à la métrologie et au suivi des pertes.
Lorsque vous cherchez un algorithme dans Scikit-learn, il faut d’abord trouver le module
contenant le genre d’algorithmes dont vous avez besoin puis sélectionner celui-ci dans la liste
du contenu du module. En général, l’algorithme correspond à une classe dont les méthodes et
attributs sont déjà déclarés parce que ce sont les mêmes que les autres algorithmes de Scikit-
learn.
Prévoyez un peu de temps pour prendre en main les classes de Scikit-learn tout en sachant que
l’interface API est la même pour tous les outils du paquetage. Il suffit d’apprendre une classe
pour savoir utiliser les autres. La meilleure approche consiste donc à apprendre entièrement
une classe pour réutiliser vos connaissances avec les autres.
Dans ce cas précis, il s’agit d’utiliser la méthode fit(X, y), X correspondant au tableau
bidimensionnel de prédicteurs (donc le jeu d’observation à apprendre) alors que y est le
résultat, soit un tableau à une dimension.
En appliquant la méthode fit(), vous mettez en relation l’information dans X avec y. Une
nouvelle donnée ayant les mêmes caractéristiques que X permet de déduire y correctement.
Certains des paramètres sont estimés par la méthode fit() en interne. Vous pouvez ainsi
distinguer les paramètres qui sont appris des hyperparamètres qui sont définis par vous au
moment de créer l’instance de l’apprenant.
Cette instanciation consiste à associer une classe Scikit-learn à une variable Python. En plus
des hyperparamètres, vous pouvez stipuler des paramètres de travail, par exemple, la
normalisation demandée ou la semence de valeurs aléatoires afin d’obtenir les mêmes résultats
dans tous les appels travaillant sur les mêmes données d’entrée.
Voyons un exemple de régression linéaire, une opération d’apprentissage machine élémentaire
très fréquente. Nous allons utiliser des données d’entrée fournies avec le paquetage Scikit-
learn. Le jeu nommé Boston, déjà rencontré, contient des variables prédicteurs que nous
pouvons confronter au prix des maisons, afin de générer un prédicteur qui va pouvoir estimer
une nouvelle maison à partir de ses caractéristiques.
Les dimensions renvoyées pour les deux variables X et y sont les suivantes :
L’affichage nous apprend que les deux tableaux ont le même nombre de lignes et que X
comporte 13 caractéristiques. La méthode shape() analyse un tableau et renvoie sa
dimension.
Le nombre de lignes doit être le même pour X et pour y. Vous devez également vous assurer
que X et y correspondent parce que l’algorithme apprend en confrontant les lignes de X avec
l’élément correspondant de y. Si vous naviguez au hasard dans les tableaux, rien ne peut être
appris.
Les caractéristiques de X, c’est-à-dire les colonnes, s’appellent également des variables dans le
monde des statistiques, ou des caractéristiques dans le monde du mécapprentissage ou
apprentissage machine.
Une fois que vous avez importé la classe LinearRegression, vous pouvez instancier une
variable nommée hypothesis en choisissant le mode de normalisation. Ici, nous paramétrons
le zéro de moyenne et une déviation standard unitaire pour toutes les variables, opération
statistique permettant que toutes les variables soient au même niveau. Nous pouvons ensuite
estimer les paramètres à apprendre.
Après ajustement (fitting), la variable hypothesis contient les paramètres appris, et vous
pouvez les visualiser au moyen des méthodes coef_, comme c’est habituel dans tous les
modèles linéaires dans lesquels la sortie est une sommation des variables pondérées par
coefficients. Cette activité d’ajustement peut également être considérée comme
l’apprentissage d’ajustement, c’est-à-dire l’apprentissage d’un algorithme machine.
Une hypothèse est une façon de décrire un algorithme qui a été entraîné avec des données.
L’hypothèse définit une représentation possible de y en fonction de X que vous testez au
niveau de la validité. C’est donc une hypothèse tant en termes scientifiques, qu’en termes
d’apprentissage machine.
Deux autres classes importantes de l’objet en dehors de la classe de l’estimateur sont celles du
prédicteur et du modèle. La classe predictor sert à prédire la probabilité d’un certain
résultat, en obtenant ce résultat pour les nouvelles observations avec ses méthodes predict()
et predict_proba(), comme dans cet extrait :
import numpy as np
new_observation = np.array([1, 0, 1, 0, 0.5, 7, 59,
6, 3, 200, 20, 350, 4],
dtype=float).reshape(1, -1)
print(hypothesis.predict(new_observation))
[25.8972784]
Pour que la prédiction soit valide, il est nécessaire que les nouvelles observations aient le
même nombre de caractéristiques que l’élément X en entrée, et dans le même ordre.
Le modèle de classe vous informe au sujet de la qualité de l’ajustement grâce à sa méthode
score() :
hypothesis.score(X, y)
0.7406077428649428
help(LinearRegression)
Ici, nous appliquons les valeurs min et max qui ont été apprises auprès de X à la nouvelle
variable new_observation, puis renvoyons la transformation.
La technique de hachage
Scikit-learn réunit la plupart des structures de données et fonctions dont vous aurez besoin
pour réaliser vos projets de datalogie. Vous y trouverez même des classes pour les problèmes
les plus épineux.
Si vous devez par exemple traiter des objets texte, une des solutions les plus pratiques offertes
par Scikit-learn se fonde sur la technique du hachage (hashing). Nous avons vu comment
préparer du texte avec la technique du sac de mots dans le Chapitre 8 et pondérer des mots
avec la transformation TF-IDF. Ces transformations sont puissantes, mais elles ne sont
applicables que si la totalité du texte est connue et accessible dans la mémoire de la machine.
Il est beaucoup plus ardu de traiter des flux de texte générés en ligne, par exemple ceux
obtenus dans les réseaux sociaux ou puisés dans d’énormes référentiels en ligne. Il devient
vraiment difficile de convertir ce genre de volume de texte dans une matrice de données pour
les analyser. Face à un tel problème, vous tirerez grand bénéfice de la technique du hachage,
qui procure quelques avantages :
» le hachage aide à gérer d’énormes matrices de données alimentées par du texte à la
volée ;
» vous corrigez plus facilement des valeurs ou des variables inattendues dans les données
texte ;
» vous pouvez construire des algorithmes adaptatifs pour de vastes collections de
documents.
print(hash(‘Python’))
745803682812927152
Sur votre machine, la valeur produite sera sans doute différente. Ne vous en inquiétez pas. Les
fonctions de hachage peuvent donner des résultats différents d’un ordinateur à l’autre. Si vous
avez vraiment besoin de résultats constants, utilisez seulement les fonctions de hachage Scikit-
learn ; vous serez assuré que les valeurs produites seront les mêmes sur toutes les machines.
Une fonction de hachage de Scikit-learn peut également renvoyer un index dans les limites
d’une plage positive spécifiée. Vous pouvez obtenir le même style de résultat manuellement
dans Python en utilisant l’opérateur modulo :
print(abs(hash(‘Python’)) % 1000)
Le résultat est alors une valeur de hachage entière beaucoup moins longue :
152
print(oh_enconder.vocabulary_)
L’exécution montre que le programme produit un dictionnaire qui contient des paires
constituées des mots et de leur codage :
Cette stratégie n’est plus applicable et échoue lorsque le projet reçoit en entrée des éléments
variant beaucoup. Cela est souvent le cas en datalogie lorsqu’il s’agit de travailler du texte ou
des caractéristiques symboliques. Le flux Internet ou un autre environnement en ligne peut de
façon soudaine produire de nouvelles données. Dans ce cas, les fonctions de hachage sont
mieux adaptées pour prendre en charge des données d’entrée imprévisibles :
1. Définition d’une plage de sortie de la fonction de hachage. Tous les vecteurs de
caractéristiques vont l’utiliser. Dans l’exemple, la plage de valeurs va de 0 à 24.
2. Calcul d’index pour chaque mot de la chaîne au moyen de la fonction de
hachage.
3. Affectation d’une valeur unitaire aux positions des vecteurs en fonction des
index des mots.
Vous pouvez définir un mécanisme de hachage en Python très simplement : il suffit de créer
une fonction puis de vérifier les résultats à partir des deux chaînes servant de test :
print(hashing_trick(
input_string=’Python for data science’,
vector_size=20))
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]
Si vous faites des essais, les résultats ne seront peut-être pas les mêmes que ceux imprimés
dans ce livre, en raison des différences de fonctionnement du hachage. Vous pouvez ensuite
afficher la seconde chaîne une fois codée :
print(hashing_trick(
input_string=’Python for machine learning’,
vector_size=20))
[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0]
Dans l’exemple, la plupart des vecteurs correspondent à des entrées valant zéro, ce qui est un
gaspillage d’espace mémoire, en comparaison de la technique un sur N. Une façon de résoudre
ce problème consiste à adopter les matrices creuses ; c’est le sujet de la prochaine section.
[1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0]
(0, 0) 1
(0, 5) 1
(0, 16) 1
(0, 18) 1
La première colonne montre qu’il s’agit de coordonnées (des tuples d’indices ligne, colonne) et
l’autre colonne est la valeur de la cellule.
Plusieurs matrices et plusieurs structures de type matrice creuse sont disponibles dans le
paquetage SciPy. Chacune propose de stocker les données d’une manière différente et
fonctionne de façon spécifique. Certaines sont très efficaces pour les opérations de tranchage
alors que d’autres sont meilleures pour les calculs. Un bon choix est souvent csc_matrix()
qui est une matrice compressée se fondant sur les lignes. La plupart des algorithmes de Scikit-
learn l’acceptent en entrée, et elle est optimale pour les opérations sur les matrices.
En tant que datalogue, vous n’aurez pas à programmer votre propre version de la technique de
hachage, sauf si vous avez une idée originale à ce niveau. Scikit-learn propose la classe
HashingVectorizer() qui permet rapidement de transformer une collection de textes en une
matrice creuse en utilisant la technique de hachage. Voici un script qui réplique l’exemple
précédent :
Dès qu’un nouveau texte arrive, CountVectorizer() (dans oh_enconder) peut le transformer
en se basant sur le schéma de codage précédent, dans lequel les nouveaux mots n’existaient
pas ; le résultat est de ce fait un vecteur de zéro vide. Pour le vérifier, il suffit de transformer la
matrice creuse en une matrice pleine avec todense() :
matrix([[1., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
0., 0., 0., 1.]])
Dans le pire des cas, un nouveau mot va se placer à une position déjà occupée. Deux mots
différents seront traités de la même façon par l’algorithme, mais cela ne va pas sensiblement
modifier ses performances.
La fonction HashingVectorizer() est idéale dès que les données ne peuvent pas toutes être
stockées en mémoire et que les caractéristiques ne sont pas fixées. Dans tous les autres cas,
vous utilisez la fonction CountVectorizer() qui est plus intuitive.
Les avantages de chaque approche en termes de gestion des données étant clairs, vous vous
demanderez sans doute ensuite quel est l’impact de chacune sur la vitesse de traitement et
l’empreinte mémoire.
Au niveau de la vitesse, Jupyter vous propose une solution prête à l’emploi consistant à
combiner deux commandes magiques, celle en ligne %timeit et celle de cellule %%timeit :
» %timeit : calcule le meilleur temps d’exécution d’une instruction ;
» %%timeit : calcule le meilleur temps d’exécution de la séquence d’instructions d’une
cellule de calepin, compte non tenu de celle de la ligne dans laquelle se trouve la
commande magique (ce qui permet de s’en servir pour initialiser l’opération).
Les deux commandes procèdent à un nombre d’essais exprimé par r et répètent ce train
d’essais n fois. Si vous spécifiez les deux paramètres –r et –n, l’application cherche à favoriser
la réponse la plus rapide.
Voici par exemple comment chronométrer l’opération d’affectation d’une liste de 10**6 valeurs
ordinales par compréhension de liste :
111 ms ± 1.77 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
111 ms ± 1.45 ms per loop (mean ± std. dev. of 5 runs, 20 loops each)
%%timeit
l = list()
for k in range(10**6):
l.append(k)
164 ms ± 2.24 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Nous en déduisons que la compréhension de liste est 50 % plus rapide que la boucle for.
Refaisons maintenant le même test en comparant deux stratégies de codage du texte.
Commençons par préparer les décors :
N. d. T. : Dans la version française, nous conservons les phrases de test en anglais qui
conviennent tout autant à la démonstration.
Nous avons ainsi chargé les classes et nous les avons instanciées. Testons la première des deux
approches :
719 µs ± 14.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Les performances sont environ six fois meilleures (1 000 µs valent une milliseconde) :
113 µs ± 644 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Conclusion : le hachage est plus rapide que le codage un sur N (one hot). Il faut dire que le
hachage utilise un algorithme optimisé qui garde trace de la façon dont les mots ont été codés,
ce que ne fait pas l’autre algorithme.
L’outil Jupyter est un excellent environnement pour mesurer les performances d’une solution
de datalogie. Vous pouvez faire vos mesures sur ligne de commande ou bien dans un script
lancé depuis l’environnement d’édition en important la classe timeit puis en vous servant de
la fonction timeit(), en fournissant les paramètres d’entrée sous forme d’une chaîne.
Si votre commande à chronométrer a besoin de variables, de classes ou de fonctions qui ne
sont pas disponibles dans la livraison Python standard, par exemple les classes de Scikit-learn,
vous pouvez les indiquer en tant que second paramètre d’entrée. Il suffit de construire une
chaîne décrivant à Python comment importer tous les objets requis depuis l’environnement
principal, comme le montre cet exemple :
import timeit
cumulative_time = timeit.timeit(
“hashing = htrick.transform(texts)”,
“from __main__ import htrick, texts”,
number=10000)
print(cumulative_time / 10000.0)
0.0001185207694947394
INSTALLATION AVEC PIP OU AVEC CONDA
Un grand nombre de paquetages peuvent être ajoutés à Python. La plupart se présentent sous la
forme de modules à télécharger à part. Certains proposent un installateur pour Windows, ce qui peut
apparemment simplifier l’installation, mais la plupart des paquetages comptent sur l’outil PIP
(Preferred Installer Program), accessible directement depuis la ligne de commande Python.
Pour vous en servir, ouvrez une fenêtre Anaconda Prompt. Si vous avez par exemple besoin d’installer
NumPy, saisissez directement la commande pip install numpy. Le logiciel sera téléchargé avec tous
les paquetages dont il dépend puis installé. Vous pouvez même choisir la version à installer en
écrivant par exemple pip install -U numpy==1.14.5. ou plus couramment demander la mise en
place de la plus récente version d’un paquetage déjà présent en écrivant ceci :
Puisque vous avez installé Anaconda, vous pouvez remplacer l’outil PIP par conda qui est encore plus
efficace parce qu’il réaligne les numéros de versions de tous les autres paquetages. Il est donc
capable de mettre à jour et même de rétrograder un paquetage existant pour le rendre compatible.
Pour utiliser conda, vous travaillez depuis la même fenêtre Anaconda Prompt. Saisissez par exemple
conda install numpy. L’outil va analyser le système, indiquer les changements puis demander s’il
doit s’exécuter. Vous frappez la touche Y si vous êtes d’accord. L’outil permet bien sûr de mettre à
jour les paquetages existants avec conda update numpy ou tout votre système avec conda update -
all.
Procéder à une installation ou une mise à jour tout en utilisant Jupyter est un peu plus complexe. Jake
Vanderplas de l’université de Washington a publié un article intéressant à ce sujet (https://jake-
vdp.github.io/blog/2017/12/05/installing-python-packages-from-jupyter/). Il propose
plusieurs astuces pour installer et mettre à jour des paquetages depuis l’interface Jupyter. Mais tant
que vous n’avez pas l’outil bien en main, mieux vaut procéder aux installations et aux mises à jour
avant de démarrer Jupyter.
import sys
!{sys.executable} -m pip install memory_profiler
Au début de chaque démarrage d’une session Jupyter Notebook que vous désirez chronométrer
et surveiller, vous exécutez la commande suivante :
%load_ext memory_profiler
Une fois ces préparations réalisées, vous pouvez facilement inspecter l’empreinte mémoire au
moyen de la commande magique %memit :
hashing = htrick.transform(texts)
%memit dense_hashing = hashing.toarray()
L’affichage vous informe sur le volume de mémoire occupé et sa variation par rapport à
l’instruction précédente ( « increment » ) :
Pour une étude plus détaillée de l’utilisation mémoire, il faut stocker le code source d’une
cellule sur disque, puis lancer le profilage au moyen de la commande magique locale %mprun
en l’appliquant à une fonction définie dans ce fichier externe. En effet, cette commande
magique ne peut traiter qu’un script Python externe. Vous obtenez ainsi un rapport détaillé,
ligne par ligne, comme le montre cet exemple :
%%writefile example_code.py
def comparison_test(text):
import sklearn.feature_extraction.text as txt
htrick = txt.HashingVectorizer(n_features=20,
binary=True,
norm=None)
oh_enconder = txt.CountVectorizer()
oh_enconded = oh_enconder.fit_transform(text)
hashing = htrick.transform(text)
return oh_enconded, hashing
Vos structures de données sont surtout des tableaux (array) de NumPy ou des DataFrame de pandas.
Ce sont des structures différentes, l’une se contentant de stocker les données dans des matrices alors
que l’autre propose des structures de stockage complexes sous différents formats. Cependant, le type
DataFrame se fonde sur le type array de NumPy. De ce fait, vous pouvez réduire l’empreinte
mémoire et augmenter les performances en étudiant le fonctionnement des tableaux NumPy et leur
utilisation par pandas.
Les tableaux de NumPy stockent les valeurs dans des blocs mémoire voisins. Ce voisinage permet à
Python d’accéder plus vite aux données, par exemple pour effectuer un tranchage. Le souci est le
même qu’avec le phénomène de fragmentation disque : si les données sont disséminées en différents
endroits du disque, elles occupent plus d’espace et sont moins rapidement accessibles.
En fonction de vos besoins, vous pouvez demander de faire stocker les données des tableaux par
lignes (c’est le choix standard aussi bien dans NumPy que dans les langages C et C++), ou par
colonnes. Les contenus des cellules sont stockés en mémoire les uns après les autres de façon
contiguë. Vous pouvez donc enregistrer votre tableau soit ligne par ligne, pour traiter plus rapidement
dans ce sens, soit colonne par colonne. Ces détails de fonctionnement sont masqués, mais ils sont
essentiels car d’eux dépend l’efficacité des tableaux NumPy en datalogie, domaine dans lequel on
utilise beaucoup les matrices numériques pour traiter l’information. C’est pour cette raison que tous
les algorithmes de Scikit-learn attendent en entrée des structures du type tableau NumPy, et ce style
de tableau est à type fixe ; un tel tableau ne peut être que du même type que la séquence de
données et ne peut pas en varier).
La structure DataFrame de pandas revient à une collection bien organisée de tableaux NumPY. Les
variables définies dans votre cadre DataFrame sont regroupées et compactées dans des tableaux
pour les différents types. Toutes les variables de type entier sont regroupées dans un bloc IntBlock,
toutes les variables flottantes dans un tableau FloatBlock et toutes les autres dans ObjectBlock. De ce
fait, lorsque vous voulez traiter une seule variable, vous accédez à tout son groupe. Première
conséquence : il est préférable d’appliquer vos opérations à toutes les variables du même type en
même temps. Une autre conséquence est que les variables de type chaîne sont beaucoup plus
pénalisantes en empreinte mémoire et en temps de calcul. Même une série très légère telle qu’une
liste de noms de couleurs déclenche le stockage dans une chaîne complète d’au moins 50 octets, dont
la manipulation est fastidieuse avec le moteur de NumPy. Comme nous l’avons indiqué dans le
Chapitre 7, il est utile de tenter de transformer les données de type chaîne en variables catégorielles ;
vous chaînes deviennent ainsi des valeurs numériques, ce qui réduit énormément l’empreinte
mémoire et améliore les performances.
Le compte rendu détaillé apparaît dans un panneau dans le bas de la fenêtre de Notebook par
défaut :
Filename: C:\Users\Sergent\PYDASC\example_code.py
L’usage mémoire est celui constaté après exécution de la ligne mentionnée ; la colonne
increment indique la variation par rapport à la ligne précédente.
Parallélisme multicœur
Pour profiter du parallélisme multicœur dans Python, vous intégrez le paquetage Scikit-learn
avec le paquetage nommé joblib qui se charge des opérations gourmandes en temps ; c’est
par exemple le cas de la réplication de modèles pour valider des résultats ou de la recherche
des meilleurs hyperparamètres. Scikit-learn permet en particulier de réaliser du
multitraitement pour les opérations suivantes :
» Cross-validation : test de résultat d’une hypothèse d’apprentissage au moyen de
données d’apprentissage et de test différentes.
» Recherche grille : modification systématique des hyperparamètres d’une hypothèse
d’apprentissage et test des résultats.
» Prédiction multilabel : exécution répétée d’un algorithme avec plusieurs cibles, lorsque
de nombreux résultats différents doivent être prévus en même temps.
» Méthodes d’apprentissage machine d’ensemble : modélisation d’une grande palette
de classificateurs, tous indépendants, comme dans la modélisation basée sur
RandomForest.
if __name__==’__main__’:
Cette instruction teste si le programme a été lancé directement, ou s’il a été démarré par une
console Python active. Vous évitez ainsi toute confusion ou erreur du processus multiparallèle,
par exemple un appel répété en boucle du parallélisme.
Démonstration du multitraitement
Jupyter Notebook permet aisément de vérifier à quel point le multitraitement peut faire gagner
du temps dans les projets de datalogie. Il vous permet en effet d’utiliser la commande magique
%timeit pour le chronométrage. Vous commencez par charger trois éléments : un jeu de
données multiclasse, un algorithme d’apprentissage complexe SVC (Support Vector Classifier)
et une procédure de cross-validation pour obtenir des résultats fiables auprès de toutes les
procédures. Nous reviendrons sur ces différents outils dans la suite du livre. Pour l’instant,
sachez que les procédures deviennent assez intensives, parce que l’approche SVC
produit 10 modèles qu’elle appelle 10 fois avec la cross-validation, ce qui donne 100 modèles
au total.
Voici le chronométrage pour un seul cœur (vous avez le temps de faire une pause) :
21.6 s ± 188.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
13.1 s ± 101 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Notre essai montre un avantage clair, alors que nous n’utilisons qu’un petit jeu de données,
pour lequel Python passe l’essentiel de son temps à démarrer une console puis à exécuter une
partie du code de celle-ci. Cette surcharge représente plusieurs secondes, ce qui constitue une
proportion importante du temps total. Vous imaginez donc le gain que vous pouvez espérer
avec un jeu de données plus volumineux. Le temps d’exécution peut facilement être divisé par
deux ou trois.
Ce code source fonctionne bien dans Jupyter, mais si vous enregistrez dans un script, puis
demandez à Python de l’exécuter depuis une console, ou si vous le chargez dans un
environnement EDI, vous risquez de rencontrer des erreurs à cause des opérations internes à
une tâche multicœur. Comme indiqué plus tôt, la solution consiste à faire venir tout le code
dans la dépendance d’une instruction if qui va permettre de vérifier comment le programme
a été démarré. Voici un exemple de cette solution :
L a détecter
datalogie se fonde sur des algorithmes complexes afin de créer des prédictions et de
des signaux significatifs dans le flot de données. Chaque algorithme a des
avantages et des inconvénients. Dans les grandes lignes, le processus est celui-ci : vous
sélectionnez plusieurs algorithmes, vous les faites travailler sur les données, vous réglez leurs
paramètres le plus précisément possible et vous décidez finalement lequel permettra le mieux
de produire le résultat espéré.
Il y a beaucoup d’automatisation dans ce processus, et cela est possible grâce à la puissance
du langage de script Python et aux librairies d’analyse. Les algorithmes d’apprentissage sont
complexes ; les détails de leur manière de travailler peuvent vous sembler obscurs. Même
lorsque vous considérez ces outils comme des sortes de boîtes noires magiques, il ne faut
jamais oublier cet acronyme très simple : GIGO qui signifie Garbage In, Garbage Out (Déchets
en Entrée, Déchets en Sortie, DEDS). Cet axiome est connu depuis toujours en statistiques et
en informatique : quelle que soit la puissance des algorithmes, vous n’obtiendrez de bons
résultats que si des données d’entrée sont de bonne qualité.
L’analyse de données exploratoire EDA (Exploratory Data Analysis) est une approche globale
d’exploration de jeux de données qui utilise des statistiques générales et des visualisations
graphiques pour mieux apprécier la pertinence de ces données. L’analyse EDA rend les étapes
ultérieures d’analyse et de modélisation plus efficaces. Nous allons découvrir au cours de ce
chapitre les descriptions des données indispensables, et verrons en quoi elles peuvent vous
aider à choisir les opérations de transformation de données les plus adéquates pour trouver
vos solutions.
Le fichier contenant le calepin des exemples de ce chapitre correspond à PYDASC_13. Nous
avons expliqué dans l’introduction comment récupérer et décompresser ce fichier archive.
L’approche EDA
L’analyse de type EDA a été conçue dans les laboratoires Bell par le mathématicien et
statisticien John Tukey. Il cherchait à faire apparaître de nouvelles questions et à susciter de
nouvelles actions en étudiant en détail les données, ce qui justifie le terme exploratoire.
Cette approche s’opposait à celle en vigueur à l’époque, qui cherchait plutôt à confirmer des
hypothèses en fin de traitement. Cette stratégie par confirmation cherche à appliquer une
théorie par une procédure ; les données ne servent qu’à faire des tests puis à mettre la théorie
en pratique. La technique d’analyse EDA est apparue à la fin des années 1970, donc bien avant
que se constitue le flot immense des métadonnées (Big Data). Tukey avait deviné que les
activités de test et de modélisation pouvaient facilement être automatisées. Voici ce qu’il avait
dit dans une de ses notes les plus célèbres :
« Le seul moyen pour les humains d’être meilleurs que les ordinateurs consiste à prendre le
risque de faire pire qu’eux. »
Ce genre de phrase vous invite à ne pas vous limiter à l’application de l’algorithme
d’apprentissage automatique, mais à envisager également des actions exploratoires manuelles
avec créativité. Les ordinateurs sont imbattables dès qu’il faut optimiser, mais les humains
restent bien meilleurs pour découvrir, en empruntant des routes surprenantes et en essayant,
apparemment avec peu d’espoir, pour à la fin dégager des solutions très efficaces.
Si vous avez pratiqué les exemples des chapitres précédents, vous avez déjà réalisé quelques
opérations sur les données. L’analyse de données exploratoire EDA est un peu différente : elle
va faire des tests qui vont plus loin que les présupposés élémentaires concernant la qualité des
données, opération qui correspond à l’analyse de données initiales IDA (Initial Data Analysis).
Récapitulons ce que nous avons vu jusqu’à présent :
» Finalisation des observations et marquage des cas manquants.
» Transformation de variables texte ou catégorielles.
» Création de nouvelles caractéristiques grâce aux connaissances du domaine des données.
» Constitution d’un jeu de données numériques dans lequel les lignes sont des observations
et les colonnes des variables ou caractéristiques.
L’analyse EDA va plus loin que l’analyse initiale IDA. C’est un changement d’attitude qui va au-
delà des hypothèses de base. Voici les activités d’une telle exploration :
» description des données ;
» exploration minutieuse des distributions ;
» compréhension des relations entre les variables ;
» repérage des situations inhabituelles ou inattendues ;
» distribution des données dans des groupes ;
» repérage des motifs inhabituels dans les groupes ;
» estimation des différences entre les groupes.
Dans les pages suivantes, nous allons voir en quoi l’analyse EDA vous aide au niveau de la
distribution des variables dans votre jeu. Cette distribution des variables désigne la liste des
valeurs que peut prendre chaque variable en comparaison de leur fréquence d’apparition.
Lorsque vous parvenez à déterminer la distribution d’une variable, vous en savez beaucoup sur
la façon dont elle peut varier et influer sur un algorithme d’apprentissage, ce qui vous aide à
faire en sorte que cet algorithme soit exploité au mieux dans votre projet.
Ce jeu de test est maintenant chargé dans une variable d’une classe spécifique de Scikit-learn.
Nous pouvons donc produire à partir d’elle un tableau nparray de NumPy et une structure
DataFrame de pandas :
import pandas as pd
import numpy as np
Les librairies NumPy, Scikit-learn et, plus encore, pandas sont en développement constant.
Avant de vous lancer dans les analyses EDA, il est utile d’en vérifier les numéros de versions.
Si vous utilisez une version trop ancienne ou trop récente, les résultats seront différents de
ceux montrés dans ce livre, ou parfois vont échouer. Comparez votre affichage avec celui de
l’exemple précédent. (N.d.T. : S’il y a une différence dans le numéro mineur (sous-version, le
dernier), cela ne porte normalement pas à conséquence.)
Nous allons découvrir dans ce chapitre plusieurs commandes pandas et NumPy, pour explorer
de plus en plus la structure des données. Cette approche progressive donne toute liberté dans
l’analyse, mais il n’est pas inutile de savoir que lorsque vous êtes pressé, vous pouvez obtenir
tout un lot de statistiques en un seul geste par la méthode describe() que vous appliquez à
votre cadre DataFrame. Vous écrirez par exemple print iris_dataframe.describe().
print(iris_dataframe.mean(numeric_only=True))
print(iris_dataframe.median(numeric_only=True))
Cette valeur médiane permet de localiser la position de la séparation entre les deux moitiés de
valeurs. La médiane est moins influencée par les cas anormaux ou par une distribution
déséquilibrée des valeurs autour de la moyenne. Notez que dans l’exemple, les moyennes ne
sont pas centrées (aucune variable n’a de valeur nulle) et que la médiane des longueurs de
pétales est assez différente de la moyenne correspondante. Cela invite à pousser
l’investigation.
Voici ce qu’il faut vérifier au niveau des mesures de tendance centrale :
» Est-ce qu’il y a des moyennes égales à zéro ?
» Est-ce qu’elles sont différentes les unes des autres ?
» Est-ce que la médiane est différente de la moyenne correspondante ?
print(iris_dataframe.std())
Il y a lieu ensuite de mesurer l’étendue (range) qui est tout simplement la différence entre la
valeur minimale et la valeur maximale pour chaque variable quantitative. L’étendue permet
d’en apprendre plus au sujet des différences d’échelle entre les variables :
print(iris_dataframe.max(numeric_only=True)
- iris_dataframe.min(numeric_only=True))
Vous pouvez ensuite confronter vos mesures d’écart type et d’étendue aux moyennes et
médianes. Si un écart type ou une étendue semble trop élevé par rapport à vos mesures de
centralité, c’est peut-être l’indice qu’il y a un problème. Peut-être existe-t-il des valeurs très
inhabituelles qui perturbent les calculs ou une distribution inattendue des valeurs autour de la
moyenne.
print(iris_dataframe.quantile([0,.25,.50,.75,1]))
La Figure 13.1 montre le résultat qui apparaît dans un panneau inférieur de votre fenêtre. Les
lignes du jeu de données sont réparties entre les différents quartiles, avec les variables dans
les colonnes. Le quartile des 25 % de la longueur des sépales vaut 5.1 ; cela signifie que 25 %
des valeurs du jeu de données pour cette mesure sont inférieures à 5.1.
Ces deux mesures vous aident à estimer la forme des données et de réaliser un test formel
pour localiser les variables qui mériteraient quelques ajustements ou transformations, afin de
s’approcher mieux d’une distribution de type gaussien. N’oubliez pas que vous allez visualiser
les données par la suite ; ce n’est ici que la première étape d’un processus assez long.
La distribution normale ou gaussienne est la plus utilisée en statistiques, de par ses propriétés
mathématiques spéciales. Elle représente la pierre angulaire de nombreux tests et modèles
statistiques. Certains d’entre eux, comme la régression linéaire, sont très utilisés en datalogie.
Dans une distribution gaussienne, la moyenne et la médiane ont la même valeur et les valeurs
sont distribuées de façon symétrique autour de la moyenne, ce qui donne un profil en cloche ;
l’écart type désigne la distance depuis la moyenne et le point où la courbe de distribution
change de concave en convexe (point d’inflexion). Ces paramètres font de la distribution
gaussienne une distribution remarquable qui est mise à profit dans les calculs statistiques.
Dans les données réelles, vous rencontrerez rarement une distribution gaussienne, aussi
importante soit-elle de par ses propriétés statistiques. Vous devrez faire face à des
distributions très différentes. C’est pour cette raison qu’il faut faire une analyse exploratoire
EDA et réaliser les mesures d’asymétrie et de kurtosis.
Dans un exemple antérieur du chapitre qui mesurait la variance, nous avions remarqué que la
longueur des pétales montrait une différence entre moyenne et médiane. Nous reprenons le
même exemple pour évaluer l’asymétrie et l’acuité afin de savoir s’il faut intervenir sur la
variable.
Pour réaliser ces deux mesures, il s’agit de déterminer si la p-valeur est inférieure ou égale
à 0.05. Si c’est le cas, cela vous oblige à rejeter la normalité (votre variable distribuée en tant
que distribution gaussienne). Il en découle que vous pourrez obtenir de meilleurs résultats en
tentant de transformer la variable en distribution normale. Voici comment réaliser le test
concerné :
Les résultats nous apprennent que les données sont légèrement asymétriques vers la gauche,
sans l’être assez pour les rendre inexploitables. Le vrai problème est que la courbe est bien
trop plate pour ressembler à une cloche, ce qui nous oblige à pousser plus loin.
Il est d’usage de tester automatiquement l’asymétrie et l’acuité de toute variable. Cela vous
permet ensuite d’inspecter celles dont les valeurs sont les plus fortes au simple regard. La
normalité d’une distribution peut cacher différents soucis, par exemple, l’existence de données
aberrantes par rapport au groupe, ce qui n’est possible de voir qu’en produisant un
diagramme.
Cet exemple utilise le regroupement, binning. Il permet aussi d’explorer la situation dans
laquelle une variable est située sous ou au-dessus d’une charnière, qui est en général la
moyenne ou la médiane. Dans ce cas, vous réglez la fonction pd.qcut() au centile 0.5 ou
pd.cut() à la valeur moyenne de la variable.
Le regroupement transforme des variables numériques en variables catégorielles, afin
d’améliorer votre compréhension des données ainsi que la phase d’apprentissage qui suit en
réduisant le bruit causé par les valeurs aberrantes et les non-linéarités de la variable
transformée.
print(iris_dataframe[‘group’].value_counts())
Dans cet exemple, les fréquences affichées montrent que tous les groupes ont la même taille :
virginica 50
versicolor 50
setosa 50
Name: group, dtype: int64
Vous pouvez même demander le calcul des fréquences, par exemple pour la longueur des
pétales regroupés suite à l’action précédente :
(0.9, 1.6] 44
(4.4, 5.1] 41
(5.1, 6.9] 34
(1.6, 4.4] 31
Name: petal length (cm), dtype: int64
Nous pouvons également extraire des informations de base sur les fréquences, et notamment
le nombre de valeurs uniques pour chaque variable et le mode de la fréquence (qui
correspondent aux lignes top et freq dans le résultat).
print(iris_binned.describe())
print(pd.crosstab(iris_dataframe[‘group’],
iris_binned[‘petal length (cm)’]))
boxplots = iris_dataframe.boxplot(fontsize=9)
La Figure 13.4 montre la structure fondamentale de la solution de chacune des variables, par
la position des deux centiles 25 et 75 qui correspondent aux extrémités de la boîte. Vous y
voyez également la médiane au centre de la boîte. Les segments de droite qui forment les
moustaches représentent 1,5 fois l’écart interquartile IQR par rapport aux extrémités de la
boîte (ou par rapport à la distance à la valeur extrême si elle reste dans la limite de 1,5 fois
l’écart IQR). Les valeurs inhabituelles sont toutes visibles en tant que signes au-delà des
moustaches.
Figure 13.5 : Boîte à moustaches des longueurs de pétales présentées par groupe.
Réalisation d’un test T
Vous pouvez obtenir une vérification statistique de ce que signifie la différence entre les
moyennes de plusieurs groupes une fois que vous avez détecté une différence d’un groupe par
rapport à une variable. Pour ce faire, vous pouvez réaliser un test T (approprié lorsque la
population échantillonnée montre une distribution normale exacte) ou une analyse de variance
à un seul facteur (ANOVA).
Le test T va comparer deux groupes en même temps ; c’est à vous de décider si les groupes ont
ou pas une variance similaire. Cela suppose de calculer d’abord cette variance, comme ceci :
La p-valeur correspond à la probabilité que la différence statistique t qui a été calculée ne soit
que le fruit du hasard. En général, lorsque la valeur est inférieure à 0.05, vous pouvez
confirmer que les moyennes de groupes sont suffisamment différentes.
Vous pouvez tester plus de deux groupes au moyen d’une analyse de variance à un seul facteur
ANOVA unidirectionnelle. Dans ce cas, la p-valeur s’interprète de façon proche au test T :
Dans la Figure 13.6, l’axe des abscisses montre toutes les variables quantitatives alignées.
L’axe des ordonnées montre les observations sous forme de lignes parallèles de couleurs
différentes en fonction du groupe propriétaire.
Figure 13.6 : Les coordonnées parallèles aident à déduire qu’un groupe est facile à séparer des autres.
Lorsque les lignes parallèles d’un groupe convergent toutes à l’écart des autres, c’est un
groupe aisément séparable. Cette visualisation permet également d’estimer la possibilité pour
certaines caractéristiques de discriminer les groupes.
cols = iris_dataframe.columns[:4]
densityplot = iris_dataframe[cols].plot(kind=’density’)
Le diagramme est représenté en Figure 13.9. Il compare les longueurs et les largeurs des
pétales. Nous distinguons plusieurs groupes avec des couleurs différentes. La forme étirée de
la distribution montre une forte corrélation entre les deux variables observées. La répartition
du nuage en groupes laisse espérer la possibilité de les séparer.
Figure 13.9 : Diagramme en nuage révélant les relations entre deux variables.
Lorsque le nombre de variables n’est pas trop important, comme ici, vous pouvez même
générer automatiquement les points pour toutes les combinaisons de variables. Vous obtenez
ainsi une matrice de nuages. L’exemple suivant montre comment créer ce genre de
diagramme :
La Figure 13.10 montre le résultat pour le jeu de données Iris. La diagonale qui représente
l’estimation de densité peut être remplacée par un histogramme en fournissant le paramètre
diagonal=hist.
Figure 13.10 : Une matrice de diagramme en nuage affichant plusieurs informations à la fois.
Principe de la corrélation
Nous pouvons représenter les relations entre les variables de façon graphique, mais également
en réalisant des estimations statistiques. Lorsque vous traitez des variables numériques,
l’estimation est une corrélation, la plus fameuse étant la corrélation de Pearson. Elle constitue
le fondement des modèles d’estimation linéaire complexes. Lorsque vous travaillez avec des
variables catégorielles, votre estimation est une association, et l’outil le plus utilisé pour
mesurer des associations entre caractéristiques est la statistique du Khi 2 (chi-square).
Covariance et corrélation
La première mesure des relations entre deux variables est la covariance. Elle permet de
savoir si les deux variables se comportent de la même façon par rapport à leur moyenne.
L’association est dite positive lorsque les valeurs isolées des deux variables sont la plupart au-
dessus ou la plupart en dessous de la moyenne correspondante. Les deux tendent donc à
converger et vous pouvez prédire le comportement de l’une à partir de l’autre. La covariance
est dans ce cas une valeur positive et plus elle est importante, plus elle est forte.
En revanche, si une variable est en général au-dessus et l’autre en général au-dessous de la
moyenne correspondante, l’association est négative. Même si cela signe une divergence, la
situation permet de faire des prédictions. En observant l’état de l’une, vous pouvez deviner
l’état probable de l’autre, bien qu’elles soient en sens opposé. La covariance est dans ce cas
une valeur négative.
La troisième possibilité correspond au cas où les deux variables divergent ou convergent de
temps à autre. La covariance va tendre vers zéro, ce qui prouve que les variables ne partagent
que peu de causes et ont donc des comportements indépendants.
En théorie, si votre variable cible est numérique, vous voudrez qu’elle soit fortement
covariante (positivement ou négativement) par rapport aux variables prédictives. En cas de
forte covariance positive ou négative entre les variables prédictives, cela signe une redondance
d’informations, c’est-à-dire que les variables désignent les mêmes données et ne font que
transmettre la même signification en termes légèrement différents.
Vous pouvez facilement calculer une matrice de covariance au moyen de cov() de la librairie
pandas. Appliquons-la immédiatement à la structure DataFrame du jeu de données Iris :
iris_dataframe.cov()
La Figure 13.11 montre une matrice avec des variables présentes dans les lignes et dans les
colonnes. Cette observation, parmi plusieurs combinaisons de lignes et de colonnes, permet de
connaître le niveau de covariance entre les variables choisies. En lisant ces résultats, vous
déduisez immédiatement qu’il y a peu de relations entre les longueurs et les épaisseurs des
sépales, ce qui prouve que ce sont des informations différentes. Il semble en revanche y avoir
une relation particulière entre les longueurs et les épaisseurs des pétales ; l’exemple ne
permet pas d’en savoir plus sur cette relation parce que la mesure n’est pas facile à
interpréter.
Vous devez utiliser une mesure standard, mais différente, car les échelles des variables
observées ont un impact sur la covariance. Pour y parvenir, vous adoptez la corrélation, qui est
une estimation de la covariance après standardisation des variables. Voici un calcul de
corrélation au moyen de la méthode corr() de pandas :
iris_dataframe.corr()
Les choses deviennent encore plus intéressantes, puisque les valeurs de corrélation sont
bornées entre -1 et +1. La relation entre longueur et épaisseur de pétales est positive, puisque
la valeur 0.96 est proche du maximum.
Vous pouvez faire calculer les matrices de covariance et de corrélation au moyen de certaines
commandes de NumPy :
Dans le domaine des statistiques, ce genre de corrélation est une corrélation de Pearson ; son
coefficient est le r de Pearson ou PCC.
Une autre technique intéressante consiste à élever la corrélation au carré. Cela fait perdre le
signe de la relation et la nouvelle valeur permet de connaître le pourcentage d’informations
partagées entre deux variables. Dans notre exemple, une valeur de 0.96 signifie qu’il y a 96 %
d’informations partagées. Pour obtenir une matrice de corrélation au carré de l’exemple, nous
utiliserions l’instruction suivante :
iris_dataframe.corr)**2
Rappelons que la covariance et la corrélation sont toutes deux basées sur les moyennes ; elles
ont donc tendance à incarner des relations qui peuvent être exprimées au moyen de deux
formulations linéaires. Dans les jeux de données réels, les variables montrent rarement une
telle formulation linéaire, et sont plutôt très non linéaires, avec des courbes et des
rebroussements. Heureusement, vous pouvez rendre ces relations linéaires en appliquant des
transformations mathématiques. Souvenez-vous qu’il ne faut utiliser les corrélations que pour
étudier les relations entre les variables, pas pour en exclure une partie.
La p-valeur indique la probabilité que la différence de Khi carré soit due au simple hasard. La
valeur élevée pour Khi carré et la valeur remarquable pour la p-valeur nous confirment que la
variable de longueur de pétale permet effectivement de distinguer les différents groupes d’iris.
Plus la valeur du Khi carré est élevée, plus il est probable que les deux variables soient liées.
Cependant, cette mesure est dépendante du nombre de cellules dans la table. Ne l’utilisez pas
pour comparer des tests de Khi carré différents, sauf si vous êtes certain que les tables ainsi
confrontées ont la même forme.
La loi du Khi carré est particulièrement intéressante pour évaluer les relations entre les
variables numériques après regroupement, même si une importante non-linéarité risque de
perturber le calcul du r de Pearson. En effet, et à la différence des mesures de corrélation,
vous pouvez ainsi être informé d’une association éventuelle, sans pour autant obtenir des
détails quant à la direction ou à la magnitude absolue.
Pendant votre analyse exploratoire, vous ne devez pas négliger l’importance des
transformations de données dans la préparation de la phase d’apprentissage. Cela signifie qu’il
faudra appliquer quelques formules mathématiques. La plupart des algorithmes
d’apprentissage fonctionnent au mieux lorsque le coefficient corrélation de Pearson est
maximal entre les variables à prédire et celles servant à la prédiction. Dans la section suivante,
nous allons découvrir les procédures les plus usitées qui permettent d’enrichir les relations
entre variables. La transformation que vous allez choisir va dépendre de la distribution réelle
des données. Vous ne pouvez donc pas le décider au départ. La découverte se fera pendant
l’analyse exploratoire et de nombreux tests. Dans cette section, nous tenons également à
souligner la nécessité de faire correspondre le processus de transformation à la formule
mathématique adoptée.
Certains des algorithmes vont adopter un comportement surprenant si vous ne remettez pas
vos variables à l’échelle par une standardisation. En guise de règle, soyez toujours vigilant
avec les modèles linéaires, les analyses de groupes clusters et tout algorithme qui prétend se
fonder sur des mesures statistiques.
Lors de l’exploration des différentes transformations possibles, une boucle de répétition for
pourrait montrer qu’une transformation de puissance augmenterait la corrélation entre deux
variables. Ceci augmenterait les performances d’un algorithme d’apprentissage linéaire. Vous
pouvez même tenter d’autres transformations (racine carrée np.sqrt(x) et exponentielle
np.exp(x)), voire combiner des transformations, comme avec le log inverse np.log(1 : x).
Dans certains cas, le fait de transformer une variable par une action logarithmique va poser
problème parce que cela ne fonctionne pas avec les valeurs négatives et nulles. Vous devrez
d’abord rééchelonner les valeurs pour que la plus petite soit égale à 1. Vous y arrivez aisément
en combinant quelques fonctions du paquetage NumPy :
np.log(x + np.abs(np.min(x)) + 1)
Chapitre 14
Réduction de dimensionnalité
DANS CE CHAPITRE :
» La magie de la décomposition en valeurs singulières
e que ce que l’on appelle Big Data (métadonnées) est une collection de jeux de données
C tellement volumineuse que ces données deviennent difficiles à traiter avec les techniques
habituelles. Les problèmes de statistiques exploitent de petites quantités d’échantillons. Vous
utilisez des techniques de statistiques traditionnelles pour les petits volumes et des techniques
de datalogie pour les gros volumes.
L’énormité peut désigner l’existence d’un très grand nombre d’exemples, d’observations (de
lignes). C’est dans ce sens que l’on pense tout d’abord aux métadonnées. Il est par exemple
assez difficile d’exploiter une base de données contenant des millions de clients en appliquant
systématiquement les mêmes traitements à la totalité de la base. Cette « grande longueur » n’est
pourtant pas la seule manière de caractériser un gisement de mégadonnées. Les données
peuvent en effet être volumineuses dans le sens horizontal, c’est-à-dire par le nombre de cas ou
caractéristiques (de colonnes). On parle dans ce cas de dimensionnalité des données. Lorsqu’une
base comporte beaucoup de colonnes, beaucoup de variables, des centaines de milliers, un vrai
problème se pose. Même si vous ne pensez traiter que quelques cas à la fois, l’obligation de
gérer un très grand nombre de caractéristiques peut rendre les analyses impossibles.
Cette grande quantité de dimensions oblige à chercher des techniques pour filtrer les
informations, en ne conservant que celles qui peuvent le mieux aider à résoudre le problème. Ce
genre de filtre va réduire les dimensions en supprimant d’abord toutes les informations
redondantes. Dans ce chapitre, nous allons voir comment réduire les dimensions en détectant les
répétitions d’information. Ce travail peut être comparé à une compression d’informations, un
peu comme la compression des fichiers sur disque dur pour économiser l’espace de stockage.
N.d.T. : Le même genre d’épuration sémantique est appliqué dans le processus de normalisation
lors de la conception des structures des bases de données relationnelles (formes normales,
https://fr.wikipedia.org/wiki/Forme_normale_(bases_de_donn%C3%A9es_relationnelles)).
Le fichier calepin des exemples de ce chapitre correspond au nom PYDASC_14. Nous avons
expliqué dans l’introduction comment récupérer les exemples.
M = U * s * Vh
En sachant que :
» U : contient toutes les informations à propos des lignes ou observations ;
» VH : contient toutes les informations concernant les colonnes ou caractéristiques ;
» S : contient la description du processus SVD (une sorte de journal).
Lorsque l’objectif est de réduire les dimensions, le fait de créer trois matrices à partir d’une
semble partir dans le mauvais sens. Effectivement, vous pourriez penser que cette technique va
générer encore plus de données. Mais la magie est au cœur de la décomposition en valeurs
singulières SVD. En construisant les nouvelles matrices, nous séparons les informations
concernant les lignes de celles concernant les colonnes (qui étaient liées dans la première
matrice). Toutes les informations utiles sont ramenées dans les premières colonnes des nouvelles
matrices.
La matrice résultante s permet de savoir comment la compression a été réalisée. La somme de
toutes les valeurs dans s indique combien d’informations étaient présentes dans la matrice de
départ. De plus, chaque valeur dans s indique la quantité de données qui a été accumulée dans
chacune des colonnes respectives de U et de Vh.
Pour mieux comprendre, il faut s’intéresser aux valeurs individuelles. Si, par exemple, la somme
de s vaut 100 et que la première valeur de s vaut 99, cela signifie que 99 % de l’information est
dorénavant ramenée dans la première colonne de U et de Vh. Autrement dit, vous pouvez sans
souci abandonner toutes les autres colonnes après la première sans perdre d’informations
importantes pour la réussite de votre projet d’exploration des données.
import numpy as np
A = np.array([[1, 3, 4], [2, 3, 5], [1, 2, 3], [5, 4, 6]])
print(A)
[[1 3 4]
[2 3 5]
[1 2 3]
[5 4 6]]
Cette matrice contient les données que nous voulons réduire, sous forme de quatre observations
avec trois caractéristiques chacune. Nous pouvons utiliser le module linalg de NumPy pour
appeler la fonction svd() qui va répartir cette matrice en trois variables U, s et Vh.
U, s, Vh = np.linalg.svd(A, full_matrices=False)
print(np.shape(U), np.shape(s), np.shape(Vh))
print(s)
Le résultat affiche les profils (shape) des trois matrices produites puis affiche le contenu de s :
La matrice U qui incarne les lignes possède quatre valeurs. La matrice Vh est une matrice
carrée ; ses trois lignes correspondent aux colonnes de la matrice de départ. Enfin, la matrice s
est diagonale, c’est-à-dire que tous ses éléments sont à zéro, sauf les diagonales. La longueur de
la diagonale correspond à celle des trois colonnes de départ. Pour obtenir des pourcentages,
vous additionnez les trois valeurs, ce qui donne 14,75 puis vous divisez cette valeur par celle de
la colonne individuelle. Par exemple, 12.26 / 14.75 donne 0.83 soit environ 83 %. Nous
constatons donc que la plupart des valeurs sont bien dans le premier élément, ce qui confirme
que la première colonne contient le plus d’informations (environ 83 %). La deuxième colonne en
contient 14 % et la troisième le reste.
Vous pouvez ainsi vérifier que la décomposition SVD tient ses promesses en étudiant directement
son résultat. L’exemple suivant reconstruit la matrice de départ avec la fonction de NumPy
nommée dot(). Elle multiplie U, s (en diagonale) et Vh. Cette fonction réalise des multiplications
matricielles, et non arithmétiques. Voici une reconstruction de matrice :
[[1. 3. 4.]
[2. 3. 5.]
[1. 2. 3.]
[5. 4. 6.]]
La reconstruction est parfaite, mais cela n’a rien d’étonnant, dans la mesure où pour l’instant,
nous n’avons supprimé aucune colonne dans la matrice U. Nous n’avons fait que restructurer les
données afin de décorréler les nouvelles variables, ce qui sera également utile pour les
algorithmes de regroupement clustering que nous verrons dans le Chapitre 15.
Lorsque vous utilisez une décomposition SVD, vous vous intéressez surtout à la matrice
résultante U qui incarne les lignes, car c’est elle qui va remplacer le jeu de données de départ.
Voyons maintenant comment faire réellement un peu de réduction des données. Commençons
par exclure la troisième colonne de la matrice, celle qui a le moins de poids :
print(np.round(np.dot(np.dot(U[:,:2], np.diag(s[:2])),
Vh[:2,:]),1)) # reconstruction k=2
Les valeurs sont quasiment les bonnes. Cela confirme que vous pouvez oublier la dernière
composante et vous servir de la matrice U en remplacement de celle de départ. Les valeurs ne
divergent que d’un ou deux dixièmes. Poursuivons la réduction en supprimant maintenant
également la deuxième colonne de la matrice U :
print(np.round(np.dot(np.dot(U[:,:1], np.diag(s[:1])),
Vh[:1,:]),1)) # reconstruction k=1
Nous retrouvons beaucoup moins bien les valeurs de départ. Certaines erreurs sont de presque
un point de différence. En revanche, l’essentiel des informations numériques reste intact. Vous
pouvez donc utiliser la matrice U à la place des données initiales. Imaginez le potentiel de cette
technique sur une matrice comptant des centaines de colonnes ! Vous allez pouvoir les
transformer en une matrice compacte en supprimant la majorité des colonnes.
La partie délicate consiste bien sûr à choisir combien de colonnes conserver. Vous pouvez
surveiller la quantité d’informations restant disponible et dans quelles colonnes en faisant
produire une somme cumulée de la matrice diagonale s au moyen de la fonction cumsum() de
NumPy. Une bonne règle consiste à faire en sorte de conserver de 70 à 85 % des informations de
départ (mais cette règle n’est pas figée). Tout dépend de l’importance qu’il y a à pouvoir
reconstruire le jeu de données initial.
Une méthode qui se base sur la décomposition SVD correspond à l’indexation sémantique latente
LSI (Latent Semantic Indexation). Elle parvient assez bien à réunir des documents et des mots
en se basant sur le fait que des mots différents tendent à avoir la même signification dans des
contextes similaires. Ce genre d’analyse va plus loin que les simples synonymes, en traitant des
concepts de groupement sémantique. C’est ainsi qu’une analyse LSI appliquée à des comptes-
rendus sportifs permettra de retrouver les équipes de football d’un championnat uniquement par
détection simultanée de plusieurs noms d’équipes dans les articles, sans avoir une connaissance
préalable de ce qu’est une équipe de football ou un championnat.
Il s’agit ensuite bien sûr de chercher les causes de cette variance partagée. Pour répondre à
cette question, et pour exploiter les variances uniques et partagées, sont apparues deux
nouvelles techniques d’analyse : l’analyse de facteurs et l’analyse par composantes principales
PCA (Principal Components Analysis, aussi connue sous le sigle français ACP).
Le modèle psychométrique
Bien avant l’invention des premiers algorithmes d’apprentissage machine, la discipline qui
s’intéresse aux mesures psychologiques, la psychométrique, a tenté de chercher des solutions
statistiques aux problèmes de grandes dimensions dans l’étude des personnalités. Comme
d’autres aspects de nos êtres, notre personnalité ne peut pas être mesurée directement. Il
n’existe ainsi pas de mesure quantitative du degré d’intelligence ou d’introversion d’une
personne. Les tests psychologiques et les questionnaires ne fournissent que des indices de
tendance.
Les chercheurs en psychologie connaissaient l’outil de décomposition en valeurs singulières et
ont tenté de l’utiliser. Ils ont alors été attirés par les variances partagées : lorsque deux variables
varient de concert, on peut en conclure que la cause des variations est la même. Les
psychologues ont ainsi créé l’analyse de facteurs. Au lieu d’appliquer la décomposition SVD aux
données d’entrée, ils vont l’appliquer à une matrice contenant les variances partagées, espérant
ainsi compresser toutes les informations et produire de nouvelles caractéristiques qu’ils ont
décidé d’appeler facteurs.
Nous chargeons les données puis stockons toutes les caractéristiques prédictives. Nous
initialisons ensuite la classe FactorAnalysis en la paramétrant pour qu’elle recherche quatre
facteurs, puis ajustons les données. Les résultats sont visibles par l’attribut components_ qui
fournit un tableau contenant les mesures des relations entre les facteurs venant d’être créés
dans les lignes, avec les caractéristiques de départ dans les colonnes :
import pandas as pd
print(pd.DataFrame(factor.components_, columns=cols))
L’affichage des résultats d’analyse montre les relations entre les lignes de facteurs et les
variables initiales en colonnes. Les valeurs peuvent être interprétées comme des corrélations :
Une valeur positive à l’intersection entre un facteur et une caractéristique signe une relation de
proportion positive entre les deux éléments. Une valeur négative signifie que les éléments
divergent et évoluent en sens contraire. Dans l’exemple avec le jeu d’iris, il ne faut conserver
que deux facteurs et non quatre, car deux seulement sont reliés de façon significative aux
caractéristiques. Vous pouvez vous servir de ces deux facteurs comme nouvelles variables parce
qu’elles rendent accessibles une caractéristique importante qui était cachée, alors que les
données d’entrée ne fournissaient que des indices.
Vous devez tester plusieurs valeurs pour n_components parce que vous ne savez pas au départ
combien de facteurs sont cachés dans les données. Vous saurez que vous demandez trop de
facteurs pour l’algorithme lorsqu’il va générer des facteurs avec des valeurs très faibles ou
nulles dans le tableau components_.
Le résultat permet de voir comment la variance initiale du jeu est distribuée parmi les
composantes : la première composante compte par exemple pour 92,5 % de la variance présente
au départ. La matrice de composantes produite montre chacune des composantes dans les lignes
en relation avec chacune des variables de départ dans les colonnes :
Après chargement du jeu de données, nous pouvons lancer l’algorithme t-SNE pour extraire les
données :
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
plt.xticks([], [])
plt.yticks([], [])
for target in np.unique(ground_truth):
selection = ground_truth==target
X1, X2 = Tx[selection, 0], Tx[selection, 1]
c1, c2 = np.median(X1), np.median(X2)
plt.plot(X1, X2, ‘o’, ms=5)
plt.text(c1, c2, target, fontsize=18)
La Figure 14.1 montre le résultat. Nous pouvons constater que certains chiffres, notamment le 0,
le 6 et le 4, se distinguent aisément des autres, alors que le 3 et le 9, voire le 5 et le 8 risquent
plus d’être confondus.
Figure 14.1 : Projection produite par l’algorithme t-SNE sur des chiffres manuscrits.
Nous commençons par importer un jeu de données de visages nommé Olivetti, disponible dans
Scikit-learn. Pour l’exemple, nous divisons le jeu d’images identifiées en un jeu d’entraînement et
un jeu de test. Vous allez supposer que vous connaissez les noms dans le jeu d’entraînement,
mais pas ceux du jeu qui servira à tester. L’objectif est donc d’associer des images du jeu de test
aux images les plus ressemblantes du jeu d’entraînement.
Le jeu Olivetti contient 400 photos prises de 40 personnes, donc 10 photos par personne. Les
photos qui représentent la même personne sont prises à des moments différents, avec un
éclairage différent et des modifications dans l’expression du visage ou la présence d’accessoires
tels que des lunettes. Chaque image mesure 64 sur 64 pixels. La sérialisation de ces pixels pour
obtenir des caractéristiques aboutit à un jeu comportant 400 cas et 4 096 variables. Pour en
savoir plus au sujet de ce jeu de données, vous pouvez utiliser la commande
print(dataset.DESCR) (nous le faisons dans l’exemple). Pour d’autres détails au sujet du jeu,
voyez le site Web de AT&T Laboratories Cambridge
(https://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html). Commençons
par transformer puis réduire les images en appliquant l’algorithme PCA de Scikit-learn :
Nous avons adopté la classe RandomizedPCA, une version approximative de l’analyse PCA qui
fonctionne mieux lorsque le jeu comporte beaucoup de lignes et de variables. La décomposition
produit 25 nouvelles variables (paramètre n_com-ponents). Le paramètre whiten_True
provoque un blanchiment pour supprimer un peu de bruit de fond causé par la granularité du
texte et des photos. Nous obtenons bien 25 composantes, soit environ 80 % des informations
trouvées dans 4 096 caractéristiques.
photo = 17
print(‘La personne représentée est le sujet %i’
% test_answers[photo])
plt.subplot(1, 2, 1)
plt.axis(‘off’)
plt.title(‘Photo ‘+str(photo)+’ inconnue dans le jeu de test’)
plt.imshow(test_faces[photo].reshape(64,64),
cmap=plt.cm.gray, interpolation=’nearest’)
plt.show()
La Figure 14.2 montre que la recherche du sujet 34 fait proposer la photo 17 dans le jeu de test.
Figure 14.2 : Application cherchant des photos similaires.
En sortie de décomposition du jeu de test, l’exemple ne conserve que les données concernant la
photo 17, puis soustrait ces données de la décomposition du jeu d’entraînement. En
conséquence, le jeu d’entraînement contient les différences par rapport à la photo d’exemple.
Nous élevons les valeurs au carré pour supprimer les valeurs négatives puis nous les
additionnons par ligne, ce qui produit une série d’erreurs cumulées. Les photos les plus
ressemblantes sont celles pour lesquelles les erreurs de moindres carrés, donc celles dont les
différences sont les plus faibles.
mask = compressed_test_faces[photo,]
squared_errors = np.sum((compressed_train_faces - mask)**2,
axis=1)
minimum_error_face = np.argmin(squared_errors)
most_resembling = list(np.where(squared_errors < 20)[0])
print(«Sujet le plus ressemblant dans le jeu d’entraînement: %i»
% train_answers[minimum_error_face])
L’extrait précédent indique le numéro de l’image de la personne identifiée qui ressemble le plus,
ce qui est bien en accord avec le sujet choisi dans le jeu de test :
Nous pouvons vérifier le traitement réalisé en affichant la photo 17 du jeu de test avec les trois
images les mieux classées du jeu d’entraînement, celles qui y ressemblent le plus (Figure 14.3) :
L’image la plus ressemblante n’est qu’une version avec une résolution différente de celle à
identifier, mais les deux autres montrent une pose différente de la même personne que la
photo 17. Nous constatons donc que l’analyse PCA peut se fonder sur une image échantillon pour
trouver d’autres photos de la même personne.
Posts: 585
Nous pouvons alors importer la classe TfidfVectorizer et la paramétrer pour qu’elle élimine
les mots vides et ne garder que les mots significatifs. Nous obtenons une matrice dont toutes les
colonnes pointent vers des mots différents.
Nous avons déjà rencontré la technique TF-IDF dans le Chapitre 8. Rappelons qu’elle s’intéresse
à la fréquence des mots dans un document. Elle établit un poids selon la rareté de chaque mot,
ce qui permet d’écarter les mots qui ne sont d’aucun intérêt pour classer ou identifier les
documents. Cela permet notamment d’éliminer les mots du langage commun rencontrés dans les
discussions.
Comme avec d’autres algorithmes du module sklearn.decomposition, le paramètre
n_components sert à choisir le nombre de composantes désirées. Vous augmentez cette valeur
pour rechercher plus de sujets. Plus cette valeur est élevée, plus va diminuer le taux d’erreur
renvoyé par la méthode reconstruction_err_. Vous déciderez quand arrêter cet ajustement en
arbitrant entre le temps de traitement supplémentaire et le supplément de sujets obtenu.
La dernière partie du script informe sur les cinq sujets trouvés. La lecture des mots vous permet
de choisir quelle signification donner à ces sujets, en tenant compte des caractéristiques
correspondantes. Par exemple, les mots anglais drive, hard, card et floppy ressortent tous au
domaine de l’informatique. Bien sûr, le mot peut également n’avoir aucune ambiguïté, par
exemple, comics, car, stereo ou games.
feature_names = vectorizer.get_feature_names()
n_top_words = 15
for topic_idx, topic in enumerate(nmf.components_):
print(‘Topic #%d:’ % (topic_idx+1),)
topics = topic.argsort()[:-n_top_words - 1:-1]
print(‘ ‘.join([feature_names[i] for i in topics]))
Les sujets sont présentés dans l’ordre avec leurs mots-clés les plus représentatifs. Pour explorer
le modèle qui en résulte, vous pouvez étudier l’attribut nommé components_ du modèle NMF
entraîné. Il s’agit d’un tableau numérique de type ndarray qui contient des valeurs positives
pour tous les mots associés au sujet. Vous pouvez récupérer les index des associations les plus
fortes au moyen de la méthode argsort() ; les valeurs les plus élevées correspondent aux mots
les plus représentatifs. L’instruction suivante extrait les index des mots les plus représentatifs
pour le sujet 1 :
print(nmf.components_[0,:].argsort()[:-n_top_words-1:-1])
[1075 1459 632 2463 740 888 2476 2415 2987 10 2305 1 3349 923 2680]
Vous utilisez ces accès index pour obtenir des chaînes de texte en les appelant depuis un tableau
produit par la méthode get_feature_names() que vous appliquez à l’objet TfidfVectorizer
qui a été préparé auparavant. L’extrait suivant montre comment extraire le mot qui correspond à
l’index du mot le plus significatif pour ce sujet :
word_index = 1075
print(vectorizer.get_feature_names()[word_index])
Nous apprenons ainsi que ce mot est « condition » , et concerne donc l’état d’un objet mis en
vente :
condition
Suggestions cinématographiques
Un autre domaine applicatif de réduction des données correspond au système qui génère des
recommandations d’achat ou d’activités qui pourraient vous intéresser. Vous avez certainement
vu les résultats de ces applications sur les sites de commerce électronique, à partir du moment
où vous vous êtes identifié et avez visité au moins quelques pages. Vous montrez votre intérêt
pour des produits en naviguant de page en page, et encore mieux en passant à la caisse. Votre
comportement et celui d’autres visiteurs permettent de vous proposer de nouvelles opportunités,
ce qui correspond au mécanisme de filtrage collaboratif.
Avec la décomposition SVD, vous pouvez réaliser ce système de suggestion collaborative, en vous
basant par exemple sur des fréquences calculées à partir des achats d’autres clients, ou à partir
de notations. Vous pouvez ainsi générer des suggestions pertinentes, même pour des produits
qui se vendent peu ou sont nouveaux. En guise d’exemple, nous allons exploiter une base de
données très connue produite par le site Web MovieLens à partir des notes attribuées par des
cinéphiles à propos de films. Puisqu’il s’agit de données externes, vous devez d’abord
télécharger le fichier compressé ml-m1.zip à l’adresse suivante :
http://files.grouplens.org/datasets/movielens/ml-1m.zip
Une fois que le fichier compressé a été récupéré, vous devez le déplacer dans votre répertoire de
travail PYDASC puis le décompresser pour obtenir un sous-répertoire ml-1m. Voici comment
vérifier que vous êtes bien dans le répertoire de travail :
import os
print(os.getcwd())
os.chdir(«.») # Si vous avez besoin d’en changer
print(os.getcwd())
import pandas as pd
from scipy.sparse import csr_matrix
users = pd.read_table(‘ml-1m/users.dat’, sep=’: :’,
header=None, names=[‘user_id’, ‘gender’,
‘age’, ‘occupation’, ‘zip’], engine=’python’)
ratings = pd.read_table(‘ml-1m/ratings.dat’, sep=’: :’,
header=None, names=[‘user_id’, ‘movie_id’,
‘rating’, ‘timestamp’], engine=’python’)
movies = pd.read_table(‘ml-1m/movies.dat’, sep=’: :’,
header=None, names=[‘movie_id’, ‘title’,
‘genres’], engine=’python’)
MovieLens = pd.merge(pd.merge(ratings, users), movies)
Nous utilisons la librairie pandas pour charger les différentes tables, puis les fusionner en nous
basant sur les caractéristiques portant le même nom (les deux variables user_id et movie_id) :
ratings_mtx_df = MovieLens.pivot_table(values=’rating’,
index=’user_id’, columns=’title’, fill_value=0)
movie_index = ratings_mtx_df.columns
Toujours grâce à pandas, nous créons une table croisée qui associe les utilisateurs dans les
lignes aux titres de films dans les colonnes. Un index des films permettra de savoir à quel film
correspond chaque colonne :
Grâce à la classe TruncatedSVD, nous réduisons la table de données pour qu’elle n’ait plus
que 10 composantes. Cette classe propose un algorithme plus souple que celui de Scipy nommé
linalg.svd qui nous a servi dans les exemples précédents. TruncatedSVD produit une matrice
résultante ayant exactement la forme que vous désirez grâce au paramètre n_components, ce qui
produit un résultat plus rapidement en consommant moins de mémoire (les matrices résultantes
complètes ne sont pas calculées).
En calculant la matrice Vh, vous réduisez le nombre de notations des utilisateurs similaires (les
scores sont exprimés par lignes) afin d’aboutir à des dimensions compressées qui expriment des
goûts et des préférences généraux. Prenez bien note du fait que puisque vous cherchez la
matrice Vh (la réduction en colonnes des titres de films), alors que l’algorithme ne fournit que la
matrice U (la décomposition en lignes), il est nécessaire d’injecter la transposition de la table de
données (cette transposition intervertit lignes et colonnes, et vous obtenez la sortie de
TruncatedSVD qui est la matrice Vh). Vous pouvez ensuite interroger pour un film en particulier :
Cette opération fournit l’index d’un épisode de Star Wars avec ses coordonnées SVD :
Le titre du film permet de trouver la colonne correspondante, qui est celle de l’index 3154 ici.
Nous pouvons alors afficher les valeurs de dix composantes, ce qui donne le profil du film. Nous
pouvons ensuite chercher tous les films dont les scores ressemblent à ceux du film étudié (tous
ceux ayant une forte corrélation avec lui). Pour y parvenir, une bonne technique consiste à
calculer la matrice de corrélation de tous les films, d’obtenir la tranche qui correspond à celui
qui vous intéresse, puis à chercher dans ses données tous les titres de films les plus apparentés
par indexation (montrant une corrélation positive forte, d’au moins 0.98 par exemple). C’est ce
que fait le code source suivant :
import numpy as np
correlation_matrix = np.corrcoef(R)
P = correlation_matrix[movie_idx]
print(list(movie_index[(P > 0.95) & (P < 1.0)]))
Nous obtenons ainsi les noms des films qui ressemblent à celui proposé, ce qui constitue bien un
générateur de suggestions basées sur des préférences cinématographiques :
[‘Raiders of the Lost Ark (1981)’, ‘Star Wars: Episode IV - A New Hope
(1977)’, ‘Star Wars: Episode VI - Return of the Jedi (1983)’, ‘Terminator,
The (1984)’]
Les amateurs de Star Wars apprécieraient plusieurs autres films, et notamment les
épisodes 4 et 6 de la saga. Ils aimeraient sans doute également Les Aventuriers de l’arche
perdue (Raiders of the Lost Ark) puisque l’acteur principal est le même dans tous ces films
(Harrison Ford).
La décomposition SVD trouve nécessairement la meilleure approche pour associer une ligne ou
une colonne de données et pour trouver des interactions complexes ou des relations que vous ne
soupçonnez même pas. Vous n’avez pas besoin d’émettre des hypothèses de départ : c’est une
approche entièrement pilotée par les données.
N. d. T. : Dans les deux précédents exemples, nous n’avons pas traduit les données puisqu’elles
proviennent de jeux de données disponibles uniquement en anglais.
Chapitre 15
Regroupements (clustering)
DANS CE CHAPITRE :
» Exploration des possibilités du regroupement non supervisé
» L’alternative DBScan
’une des compétences que les êtres humains pratiquent depuis l’aube de notre histoire
L consiste à classer les choses du monde en catégories, à partir de caractéristiques que
partagent différents objets du réel, selon des critères qui dépendent de celui qui réalise ce
classement. Les hommes des cavernes ont classé le monde de la nature en distinguant d’abord
les plantes des animaux, puis entre ceux qui leur étaient utiles et ceux qui pouvaient leur
nuire. De nos jours, les départements de marketing classent les clients et prospects en
différents segments cibles puis les visent en les attirant dans des plans marketing sophistiqués.
La classification est un processus indispensable pour construire de nouvelles connaissances, en
permettant de regrouper les objets similaires, par exemple :
» désigner tous les éléments d’une classe par un seul nom ;
» produire une synthèse des caractéristiques remarquables au moyen d’un type de classe
servant d’exemple ;
» associer des actions spécifiques à une classe ou mémoriser des connaissances
particulières, de façon automatique.
L’exploitation des énormes flux de données actuels réclame cette même compétence de
classement, mais à une tout autre échelle. Pour pouvoir détecter des groupes de signaux
inconnus masqués dans les données, il faut utiliser des algorithmes capables d’apprendre
comment associer des exemples à certaines classes (apprentissage supervisé) mais également
capables de découvrir de nouvelles classes intéressantes qui étaient passées sous silence
(apprentissage non supervisé).
Vos activités de datalogue vont surtout consister à mettre en pratique vos compétences de
prédiction, mais vous devrez également pouvoir déceler de nouvelles informations présentes
dans les données. Vous devrez par exemple souvent repérer de nouvelles caractéristiques pour
renforcer le pouvoir de prédiction de vos modèles, trouver des solutions praticables pour
comparer les données de façon complexe et repérer des points communs dans les flux des
réseaux sociaux.
L’approche de classification pilotée par les données correspond au regroupement ou clustering.
Elle vous sera d’un grand secours dans vos projets dès que vous aurez besoin d’obtenir de
nouvelles perspectives alors qu’il vous manque des labels (étiquettes) de données au départ, ou
lorsque vous aurez besoin d’en créer de nouvelles.
Les techniques de regroupement englobent tout un ensemble de méthodes de classification
non supervisée. Elles permettent de créer des classes pertinentes par traitement direct des
données sans connaissances préalables, ni hypothèses au sujet des groupes qui peuvent être
présents. Les algorithmes supervisés ont besoin d’exemples avec des labels de classes ; ceux
qui sont non supervisés trouvent en toute autonomie quels seraient les labels les plus
appropriés.
Le regroupement permet de synthétiser d’énormes volumes de données. C’est une excellente
technique pour fournir des données à un auditoire non technique, et pour alimenter un
algorithme supervisé en variables de groupes, ce qui revient à lui transmettre des informations
concentrées et significatives.
Il existe plusieurs techniques de regroupement que nous distinguons en adoptant les règles
suivantes :
» affectation de chaque exemple à un groupe unique (partitionnement) ou à plusieurs
groupes (regroupement flou, fuzzy) ;
» choix de la règle heuristique adoptée par la technique pour décider si un exemple fait ou
non partie d’un groupe ;
» spécification de la façon dont les techniques quantifient les différences entre les
observations, ce qui revient à mesurer des distances.
Vous allez la plupart du temps utiliser les techniques de partitionnement. Chaque point de
données ne peut faire partie que d’un groupe et les groupes ne se chevauchent pas ; il n’y a
pas d’appartenance multiple. Parmi les méthodes de partitionnement, vous utiliserez la plupart
du temps celle de la k-moyenne. Nous verrons dans ce chapitre d’autres méthodes qui se
basent sur des méthodes d’agglomération et de densité de données.
Les méthodes d’agglomération regroupent les données en fonction d’une distance. L’approche
de densité de données exploite l’idée que les groupes sont très denses et continus. Lorsque
vous détectez une diminution de densité en explorant une partie d’un groupe ou une série de
points, cela peut indiquer que vous êtes arrivé à une frontière de groupe.
Puisque vous ne savez pas a priori ce que vous cherchez, vous utiliserez plusieurs méthodes
pour obtenir plusieurs points de vue sur les données. La réussite d’un regroupement tient à
l’utilisation du maximum de recettes différentes, en comparant les résultats puis en essayant
de trouver une raison pour regrouper certaines observations et pas d’autres.
Le fichier calepin du code source des exemples de ce chapitre correspond à PYDASC_15.ipynb.
import numpy as np
A = np.array([165, 55, 70])
B = np.array([185, 60, 30])
Les lignes suivantes calculent les différences entre les trois éléments, procèdent à une
élévation au carré puis cherchent la racine carrée du total de ces valeurs au carré :
D = (A - B)
D = D**2
D = np.sqrt(np.sum(D))
print(D)
Voici le résultat :
45.0
La distance euclidienne n’est donc qu’une grosse addition. Le problème est que lorsque les
variables qui servent à construire le vecteur de différences sont exprimées dans des échelles
assez différentes (dans notre exemple, la taille pourrait avoir été exprimée en mètres), le
résultat est une distance anormalement influencée par les éléments ayant la plus grande
échelle. Il est donc indispensable de normaliser les échelles des variables avant d’appliquer un
algorithme à k-moyennes. Vous pouvez opter pour une plage fixe ou bien pour une
normalisation statistique avec une moyenne à zéro et une variance unitaire.
Un autre souci tient à la corrélation entre les variables qui entraîne une redondance
d’informations. En effet, lorsque deux variables sont très corrélées, une partie de ce qu’elle
signifie fait doublon. L’information est prise en compte deux fois dans le calcul de la somme qui
sert à produire la distance. Si vous n’êtes pas informé de cette corrélation, certaines variables
vont dominer le calcul de la distance de façon anormale et vous risquez de ne pas trouver les
groupes dont vous pourrez tirer profit. La solution consiste ici à éliminer cette corrélation en
appliquant d’abord un algorithme de réduction de dimensions tel que l’algorithme par
composantes principales PCA (vu dans le chapitre précédent). Il vous incombe de vérifier les
échelles et les corrélations avant d’appliquer une technique de regroupement par k-moyennes
ou autre qui produit des distances euclidiennes.
Après avoir importé les chiffres depuis Scikit-learn, nous affectons les données à une variable
puis stockons les labels dans une autre variable pour pouvoir vérifier plus tard. Nous pouvons
ensuite traiter les données par une technique d’analyse PCA. Voici le résultat affiché :
Voici le résultat :
Nous éliminons les problèmes d’échelle et de corrélation en appliquant une analyse PCA aux
données rééchelonnées. En théorie, la PCA peut produire le même nombre de variables qu’au
départ, mais l’exemple en élimine quelques-unes au moyen du paramètre n_components. Le
choix de n’utiliser que 30 composants, à comparer aux 64 variables initiales, permet de
préserver l’essentiel de l’information, environ 90 %, tout en simplifiant le jeu de données en
supprimant les corrélations et réduisant la redondance et le bruit conséquent.
Toute la suite du chapitre utilise le jeu de données stocké dans Cx qui a été défini dans le
précédent extrait de code source ci-dessus. Si vous avez besoin de faire exécuter un des
exemples ultérieurs directement, pensez à faire réexécuter d’abord le bloc ci-dessus pour que
la variable soit connue.
Les données issues de la transformation PCA se trouvent dans la variable nommée Cx. Nous
pouvons maintenant demander l’import de la classe KMeans puis définir les paramètres de
travail :
» n_clusters est le nombre K de centroïdes que nous voulons trouver.
» n_init est le nombre d’itérations de la technique à k-moyennes avec des centroïdes
initiaux différents. Nous avons besoin d’un nombre d’essais suffisant, par exemple 10.
Une fois le paramétrage réalisé, notre classe de regroupement est prête à l’emploi. Nous avons
utilisé la méthode fit() en l’appliquant au jeu de données Cx pour obtenir un jeu normalisé
au niveau des échelles et avec moins de dimensions.
Nous convertissons la solution, qui est stockée dans la variable interne nommée labels de la
classe clustering, vers une structure de données de pandas du type DataFrame pour pouvoir
appliquer un traitement de tableau croisé afin de comparer les labels initiaux à ceux déduits du
regroupement. Les résultats sont visibles dans la Figure 15.1. Les lignes correspondent aux
valeurs vraies, et il suffit de chercher les valeurs de regroupement pour lesquelles la majorité
des observations sont réparties dans plusieurs groupes. Elles correspondent aux images de
chiffres qui sont les plus difficiles à distinguer des autres par la méthode des k-moyennes.
Figure 15.1 : Tableau croisé des valeurs vraies et des groupes de k-moyennes.
Vous remarquez d’abord que les valeurs vraies pour 0 et pour 6 sont regroupées quasiment
dans un groupe alors que d’autres, telles que les valeurs vraies pour 3, 9 et quelques autres
entre 5 et 8 sont moins centralisées. Cette première analyse permet de deviner que certains
chiffres seront plus faciles à reconnaître que d’autres.
La technique du tableau croisé est assez pratique dans notre exemple parce qu’elle permet de
comparer le résultat du regroupement aux valeurs vraies. Cependant, il vous arrivera rarement
de disposer des valeurs vraies dans vos travaux de regroupement. Vous utiliserez dans ce cas
les centroïdes des groupes pour représenter les valeurs des variables. Vous les obtiendrez au
moyen de statistiques descriptives, en exploitant la moyenne et la médiane comme décrit dans
le Chapitre 13, pour chaque groupe. Vous pourrez ensuite comparer les statistiques
descriptives entre les groupes.
Nous pouvons par ailleurs remarquer que bien qu’il n’y ait que dix valeurs dans l’exemple, il
existe bien d’autres formes d’écritures manuscrites, ce qui demande de chercher d’autres
groupes. Le seul problème est de savoir au bout de combien de groupes différents il faut
s’arrêter.
Pour mesurer la viabilité d’un groupe, vous vous servez du concept d’inertie. L’inertie
correspond à la somme de toutes les différences entre chaque membre du groupe et son
centroïde. L’inertie est faible lorsque les exemples du groupe sont similaires aux centroïdes.
Mais cette mesure ne suffit pas seule. Vous remarquerez que l’inertie diminue lorsque vous
travaillez avec de plus en plus de groupes. Il s’agit de comparer l’inertie d’une opération de
regroupement à la précédente pour en tirer un taux de variation qui est plus facilement
mesurable et interprétable. Pour obtenir ce taux de variation d’inertie dans Python, il suffit de
mettre en place une boucle de répétition qui réalise des regroupements progressifs en
mémorisant les valeurs. Voici le code source d’une telle boucle pour notre problème de chiffres
manuscrits :
import numpy as np
inertie = list()
for k in range(1,21):
clustering = KMeans(n_clusters=k,
n_init=10, random_state=1)
clustering.fit(Cx)
inertie.append(clustering.inertia_)
delta_inertie = np.diff(inertie) * -1
Nous définissons la variable inertie dans la classe de regroupement après avoir ajusté le
regroupement. Cette variable incarne une liste qui accumule les taux de variation d’inertie
entre deux solutions. L’extrait suivant affiche un graphe en ligne de ce taux (Figure 15.2).
Figure 15.2 : Taux de variation de l’inertie pour des solutions jusqu’à k=20.
Il s’agit de chercher les sauts brusques dans l’examen de ce taux d’inertie. Si le taux monte,
cela signifie que le regroupement apporte plus de bénéfices que prévu par rapport au
précédent. S’il chute fortement, c’est que vous réalisez un regroupement de trop. Ainsi, toutes
les solutions avant la chute sont de bonnes candidates, si l’on s’en tient au principe d’économie
de moyen. En effet, un saut correspond à une augmentation de complexité, et les bonnes
solutions sont en général les plus simples (N. d. T. : loi du rasoir d’Occam). Nous pouvons
remarquer plusieurs sauts pour k=7, 9, 11 et 14, parmi lesquels k=14 semble le saut le plus
prometteur de par son important changement d’orientation par rapport à la tendance
descendante.
L’étude du taux de variation d’inertie apporte quelques indices pour trouver les bonnes
solutions de regroupement. C’est à vous de prendre la décision finale si vous avez besoin d’en
savoir encore plus au sujet des données. En revanche, si le regroupement n’est qu’une étape
dans une séquence de traitements complexe, il est inutile de passer trop de temps à
l’optimisation du nombre de regroupements. Cherchez une solution qui apporte suffisamment
de groupes pour alimenter l’algorithme d’apprentissage suivant qui va procéder à la sélection.
k = 10
clustering = KMeans(n_clusters=k,
n_init=10, random_state=1)
clustering.fit(Cx)
inertie_kmoy = clustering.inertia_
print(«Inertie de K-moyenne: %0.1f» % inertie_kmoy)
Vous apprenez que l’inertie de k-moyennes est d’environ 58250.0. Testons les mêmes données
avec le même nombre de groupes en préparant un regroupement MiniBatchKMeans pour
réaliser des lots successifs de 100 exemples :
Le script balaye les index du jeu de données (Cx) qui a été préalablement mis à l’échelle et
analysé en PCA afin de produire des lots de 100 observations. Avec la méthode partial_fit(),
nous ajustons un regroupement par k-moyennes pour chaque lot en nous basant sur les
centroïdes trouvés lors du tour précédent. L’algorithme s’arrête lorsqu’il n’a plus de données.
Grâce à la méthode score() appliquée à toutes les données disponibles, le programme peut
évaluer l’inertie pour une solution basée sur dix groupes, qui s’élève maintenant à
environ 64600.0. Cette technique basée sur MiniBatchKmeans présente donc une inertie
supérieure à l’algorithme standard. Cette solution est donc moins bonne et vous la réserverez
aux cas dans lesquels vous ne pouvez vraiment pas charger le jeu de données en entier en
mémoire.
Regroupements hiérarchiques
L’algorithme par k-moyennes gère des centroïdes ; le regroupement hiérarchique tente de
relier chaque point de données à son plus proche voisin en se basant sur une mesure de
distance, réalisant une agrégation (agglomération) qui produit un groupe. En répétant
l’algorithme au moyen de différentes méthodes de liaison, nous collectons tous les points de
données vers un nombre de groupes de moins en moins grand jusqu’à ce que tous les points
finissent par aboutir dans un seul groupe.
La visualisation de ce genre de processus fait penser à la classification du vivant en biologie. Il
s’agit d’une sorte d’arbre inversé dont toutes les branches convergent vers un tronc commun.
Une bonne représentation est le dendrogramme, et vous en rencontrez dans la recherche
médicale et biologique. L’implémentation proposée par Scikit-learn du regroupement par
agrégation ne permet pas de produire un tel dendrogramme à partir des données parce que
cette technique ne donne de bons résultats que dans de rares cas alors que vous avez besoin
que cela fonctionne pour de nombreux exemples.
Les algorithmes par agrégation sont moins faciles à utiliser que ceux par k-moyennes et ne
s’ajustent pas bien aux jeux de données volumineux. Ils conviennent mieux aux études
statistiques et on les rencontre souvent dans les sciences de la nature, l’archéologie (ainsi que
parfois en psychologie et économie). L’avantage est d’offrir une panoplie complète de solutions
de regroupement imbriquées : il ne vous reste plus qu’à choisir celle qui convient le mieux à
votre besoin.
Pour bien utiliser le regroupement par agrégation, il faut connaître les différentes méthodes de
liaison qui correspondent au processus de recherche, ainsi que les mesures de distance. Vous
disposez de trois méthodes de liaison (dissimilarités interclasses) :
» Distance de Ward : privilégie la recherche de groupes sphériques, à forte cohérence
interne et très différents des autres groupes. La méthode cherche en outre à trouver des
groupes de tailles similaires, ce qui est un avantage. Ne peut fonctionner qu’avec la
distance euclidienne.
» Saut maximum ou Complete : agrège les groupes à partir des observations les plus
éloignées, c’est-à-dire les points de données les moins similaires. Les groupes ainsi créés
ont tendance à contenir des observations très similaires, et sont donc assez compacts.
» Lien moyen ou Average : agrège les groupes en fonction de leurs centroïdes en ignorant
leur frontière. Produit des groupes plus vastes qu’avec la méthode précédente. Les groupes
peuvent avoir des tailles et des formes variées, à la différence de la méthode Ward. Cette
approche convient de ce fait bien en biologie, en capturant la diversité de la nature.
Hclustering = AgglomerativeClustering(n_clusters=10,
affinity=’euclidean’,
linkage=’ward’)
Hclustering.fit(Cx)
ms = np.column_stack((ground_truth,Hclustering.labels_))
df = pd.DataFrame(ms, columns = [‘Données réelles’,’Clusters’])
pd.crosstab(df[‘Données réelles’],
df[‘Clusters’], margins=True)
Figure 15.3 : Tableau croisé entre valeurs réelles et groupes par agrégation de Ward.
Pour notre exemple, les résultats sont un peu meilleurs qu’avec l’approche par k-moyennes.
Bien sûr, vous aurez remarqué qu’il faut plus de temps pour réaliser ce genre d’analyse. Si le
nombre d’observations est important, les calculs d’un regroupement hiérarchique peuvent
prendre des heures, et rendre la solution non viable. Pour contourner ce problème du temps de
traitement, vous pouvez adopter un regroupement en deux phases qui fonctionne plus vite tout
en apportant une solution hiérarchique même pour un jeu de données volumineux.
Le problème à régler à ce moment est la mémorisation des liens entre les cas et les groupes
provenant du traitement par k-moyennes. Nous choisissons d’utiliser une structure de type
dictionnaire.
Kx = clustering.cluster_centers_
Kx_mapping = {case:cluster for case,
cluster in enumerate(clustering.labels_)}
Notre nouveau jeu de données correspond à la variable Kx qui contient les centroïdes des
groupes qui ont été détectés par l’algorithme à k-moyennes. Chaque groupe peut être vu
comme une synthèse correcte des données de départ. Si vous demandez un regroupement de
cette synthèse, vous devriez obtenir quasiment la même chose qu’en faisant un regroupement
des données initiales.
Vous pouvez alors connecter les résultats aux centroïdes dont vous vous êtes servis au départ,
ce qui permet facilement de décider si un groupe hiérarchique a bien été constitué à partir de
certains centroïdes issus de k-moyennes. Le résultat auquel vous parvenez incarne les
observations qui correspondent aux groupes de k-moyennes qui possèdent ces centroïdes ou
centres de masse.
H_mapping = {case:cluster for case,
cluster in enumerate(Hclustering.labels_)}
final_mapping = {case:H_mapping[Kx_mapping[case]]
for case in Kx_mapping}
Il ne reste plus qu’à évaluer la solution en produisant un tableau croisé comme nous l’avions
fait pour les deux algorithmes précédents par k-moyennes et par hiérarchie directe
(Figure 15.4).
ms = np.column_stack((ground_truth,
[final_mapping[n] for n in range(max(final_mapping)+1)]))
df = pd.DataFrame(ms,
columns = [‘Données réelles’,’Clusters’])
pd.crosstab(df[‘Données réelles’],
df[‘Clusters’], margins=True)
Figure 15.4 : Tableau croisé entre valeurs réelles et regroupement en deux phases.
Cette solution reste assez proche des deux précédentes. Cela prouve que l’approche est une
bonne méthode pour gérer un jeu de données volumineux, voire un jeu de mégadonnées. Vous
pouvez le réduire vers une représentation plus compacte afin de le traiter ensuite par un
regroupement moins capable de s’ajuster à un gros volume, mais offrant des techniques plus
précises et plus variées.
L’approche en deux phases possède un autre avantage, celui de bien se comporter face à des
données bruitées ou aberrantes. En effet, la première phase basée sur les k-moyennes élimine
bien ces problèmes en les renvoyant pour traitement par d’autres solutions de regroupement.
Avec DBScan, vous n’avez pas besoin de fournir un paramètre K pour le nombre de groupes
espérés car l’algorithme les trouve par lui-même. Cela semble simplifier l’utilisation de
DBScan, mais en réalité, l’algorithme vous demande en échange de définir deux paramètres
fondamentaux pour bien travailler : eps et min_sample :
» eps : distance maximale entre deux observations permettant de les considérer comme
étant du même voisinage.
» min_sample : nombre minimal d’observations d’un voisinage permettant de les convertir
en un point central (core point).
Le principe de l’algorithme est de circuler dans les données en construisant des groupes par
association de toutes les observations qui forment un voisinage. Par voisinage, on entend un
petit groupe de points de données situés les uns des autres à une distance d’au maximum eps.
Si ce nombre de points ainsi regroupés est inférieur à la valeur min_sample, DBScan ne crée
pas cet îlot de voisinage.
DBScan parvient à produire tous les voisinages quelle que soit la forme des groupes, la seule
condition étant la proximité gérée par le paramètre eps. S’il ne trouve plus de voisins dans la
portée, DBScan cherche à créer des groupes même avec seulement deux points de données
s’ils sont dans les limites de eps. Les points qui ne sont associés à aucun groupe sont
considérés comme du bruit, car trop différents pour appartenir à aucun groupe.
Vous devrez faire de nombreux essais en faisant varier les valeurs de eps et de min_sample.
Les groupes produits vont changer énormément en fonction des valeurs des deux paramètres.
Vous commencerez par un petit nombre pour min_samples afin de favoriser des
regroupements sous forme de nombreux hameaux ; choisissez par exemple la valeur 5. Faites
alors varier la valeur de eps en commençant par 0.1. Ne soyez pas déçu si vous n’obtenez pas
de résultats intéressants au départ. Continuez à essayer différentes combinaisons.
Revenons à notre exemple après cette petite description de DBScan. Lançons une exploration
des données pour obtenir une nouvelle perspective sur celles-ci. Nous commençons par
dénombrer les groupes :
ms = np.column_stack((ground_truth, DB.labels_))
df = pd.DataFrame(ms,
columns = [‘Données réelles’, ‘Clusters’])
pd.crosstab(df[‘Données réelles’],
df[‘Clusters’], margins=True)
Quasiment la moitié des observations sont regroupées dans le groupe -1 qui correspond hélas
au bruit, des valeurs que l’on peut considérer comme « asociales » . Quand on tient compte du
nombre de dimensions (ici, 30 variables décorrélées suite à une analyse PCA) et de la grande
variété des échantillons (puisque ce sont des manuscrits), il est normal que de nombreux cas
n’entrent pas naturellement dans le même groupe. Le résultat de l’exemple est visible dans la
Figure 15.5.
Nombre de clusters: 12
Counter({-1: 836, 6: 182, 0: 172, 2: 159, 1: 156, 4: 119,
5: 77, 3: 28, 10: 21, 7: 18, 8: 16, 9: 13})
Figure 15.5 : Tableau croisé du traitement par DBScan.
La force de DBScan est de fournir des groupes fiables et cohérents. En effet, DBScan n’est pas
forcé de trouver une solution avec un nombre déterminé de groupes lorsque cette solution
n’existe pas, alors que c’est le cas avec les k-moyennes et les agglomérations ou agrégations.
Chapitre 16
Détection des données aberrantes
DANS CE CHAPITRE :
» Définition d’une donnée aberrante
Parmi toutes ces erreurs, les plus ardues à corriger sont les données aberrantes parce qu’il
n’existe pas une définition unique de cet état, ni des raisons claires à leur présence au milieu
de vos données. Il vous faut donc lancer des investigations et des évaluations.
Le fichier de calepin de tous les exemples de ce chapitre correspond à P4DS4D2_16.
Nous utilisons le générateur aléatoire de NumPy pour créer une variable portant le nom
normal qui contient 1 000 observations provenant d’une distribution standard.
Les statistiques descriptives principales que sont la moyenne, la médiane et la variance ne
laissent rien présager de gênant. Voici ces trois valeurs :
Nous allons maintenant modifier une seule valeur afin d’insérer un aberrant :
aberrant = normal.copy()
aberrant[0] = 50.0
print(‘Moyenne mean: %0.3f Médiane: %0.3f Variance: %0.3f’ %
(np.mean(aberrant),
np.median(aberrant),
np.var(aberrant)))
Vous constatez que la moyenne a triplé, et la variance a subi le même sort. La médiane, qui se
base sur les positions, ne varie pas puisqu’elle cherche quelle valeur occupe la position
médiane entre toutes les observations une fois celles-ci triées.
Plus grave encore, la corrélation entre variable initiale et variable aberrante est très éloignée
de +1.0 (la valeur de corrélation d’une variable par rapport à elle-même). Cela montre que la
mesure de relation linéaire entre les deux variables a été sérieusement altérée.
Ces deux situations vont empêcher un algorithme d’apprentissage de bien généraliser pour de
nouvelles données. En d’autres termes, les aberrants forcent l’apprentissage à se sur-ajuster
au jeu de données actuel.
Plusieurs remèdes sont possibles face à des aberrants ; certains vous demandent de modifier
les données actuelles alors que d’autres vont vous faire choisir une fonction d’erreur
appropriée à votre algorithme d’apprentissage. (En effet, certains algorithmes permettent de
choisir une fonction d’erreur différente en paramètre lors de la configuration de la procédure
d’apprentissage.)
La plupart des algorithmes d’apprentissage savent exploiter plusieurs fonctions d’erreur. Ce
genre de fonction va aider l’algorithme à travailler en comprenant les erreurs et en appliquant
des ajustements dans son processus. Cela dit, certaines fonctions d’erreur sont très sensibles
aux données aberrantes alors que d’autres y résistent beaucoup mieux. C’est ainsi qu’une
mesure d’erreur élevée au carré va amplifier les aberrants du fait que les erreurs qui
découlent des exemples ayant de grandes valeurs sont élevées au carré, ce qui augmente
encore leur impact.
En combinant ces deux techniques, vous déterminez aisément s’il y a des aberrants et où il faut
les chercher. Le jeu de données de test sur le diabète tiré de Scikit-learn est un excellent
exemple pour commencer :
La troisième commande permet de rassembler toutes les données dans la variable nommée X
qui est un tableau du type ndarray de NumPy. Nous pouvons ensuite transformer cela en une
structure DataFrame et réclamer quelques statistiques descriptives (Figure 16.1) :
import pandas as pd
pd.options.display.float_format = ‘{:.2f}’.format
df = pd.DataFrame(X)
df.describe()
Vous repérez les variables douteuses en observant les extrémités de la distribution (la valeur
maximale d’une variable). Vous vérifiez notamment que les valeurs minimale et maximale sont
éloignées du quartile 25 et du quartile 75. Le tableau affiché laisse voir que de nombreuses
variables ont des valeurs maximales indûment élevées. La production d’une boîte à moustaches
le confirme. Le code source suivant crée cette boîte pour toutes les variables (Figure 16.2).
La boîte à moustaches possède des moustaches positionnées à plus ou moins 1,5 fois la plage
interquartile IQR (InterQuartile Range), soit la distance entre les quartiles inférieur et
supérieur par rapport aux bords inférieur et supérieur de la boîte (les quartiles supérieur et
inférieur). Ce style de boîte à moustaches est appelé boîte de Tukey d’après le statisticien John
Tukey qui l’a inventée et diffusée parmi d’autres techniques d’exploration des données. Elle
permet de bien montrer l’existence de cas en dehors des moustaches, tous ces points peuvent
être considérés comme des données aberrantes.
La Figure 16.3 montre les lignes du jeu de données avec quelques probables valeurs
aberrantes.
L’inégalité de Tchebychev est prudente. Une valeur située à au moins sept écarts types de la
moyenne a d’énormes probabilités d’être aberrante. Vous opterez donc pour cette règle
lorsque vous aurez impérieusement besoin de ne pas écarter une valeur en la considérant à
tort comme aberrante. Dans les autres cas, vous pouvez vous contenter de la règle habituelle
des 68-95-99.7.
Xs_capped = Xs.copy()
o_idx = np.where(np.abs(Xs)>3)
Xs_capped[o_idx] = np.sign(Xs[o_idx]) * 3
Dans cet extrait, la fonction de NumPy nommée sign() permet de récupérer le signe (+1 ou –
1) de l’aberrant. La valeur est alors multipliée par trois puis affectée au point de données qui a
été récupéré par un index booléen dans le tableau standardisé.
Cette technique souffre d’une contrainte, car l’écart type sert pour les valeurs hautes et les
valeurs basses, ce qui suppose que la distribution des données est symétrique, alors que cela
se présente rarement en réalité. Vous pouvez dans ce cas utiliser une technique un peu
complexe qui réalise un plafonnement et porte le nom du chercheur Charles P. Winsor (la
« winsorisation » ). Selon cette technique, toutes les valeurs en dépassement sont forcées à
une valeur spécifiée en tant que plafond, qui est en général celle correspondant aux 5 % de la
limite inférieure et aux 95 % de la limite supérieure :
Vous disposez ainsi de valeurs butées pour les valeurs très grandes et très petites, ce qui vous
assure de préserver l’asymétrie éventuelle de la distribution. Dans les deux cas, par
multiplication d’écarts types ou par winsorisation, les données sont ensuite prêtes à subir
d’autres traitements et analyses.
Vous disposez enfin d’une solution automatique qui consiste à laisser la librairie Scikit-learn
transformer les données en plafonnant les aberrants. Il suffit d’utiliser l’outil échelonneur
nommé RobustScaler, outil basé sur l’écart interquartile (IQR, déjà utilisé dans les boîtes à
moustaches de ce chapitre) :
Approche multivariée
L’étude univariée des variables une à une permet de détecter un grand nombre d’observations
aberrantes, mais tous les aberrants ne s’éloignent pas nécessairement beaucoup de la norme.
Certains sont le produit d’une combinaison inhabituelle de valeurs de plusieurs variables.
Cette situation est rare, mais son impact peut perturber sérieusement les algorithmes
d’apprentissage.
Il ne suffit plus dans ce cas d’inspecter en détail chacune des variables pour éliminer les cas
anormaux du jeu de données. Vous ne détecterez ce genre de souci qu’avec quelques
techniques capables de considérer plusieurs variables en même temps.
Ces techniques abordent le problème avec un point de vue différent pour chacune :
» réduction des dimensions ;
» regroupement par densité ;
» modélisation de distribution non linéaire.
Vous appliquerez chacune de ces techniques pour pouvoir comparer leurs résultats et détecter
des indices particuliers pour certains cas. Parfois, vous les aurez déjà aperçus par votre
analyse univariée, mais d’autres seront de nouveaux cas à résoudre.
Utilisation de l’analyse PCA
L’analyse par composantes principales PCA sait restructurer entièrement les données en
supprimant les doublons et en reclassant les nouvelles composantes en fonction de la quantité
de variance initiale exprimée. Ce genre d’analyse procure donc une vue complète et
synthétique de la distribution des données, ce qui permet de faire ressortir les aberrants
multivariés.
La distribution générale des données est rendue visible d’abord par les deux premières
composantes qui sont les plus informatives au niveau des variances. Le résultat permet
d’obtenir de bons indices des aberrants candidats.
Parallèlement, les deux dernières composantes (les résiduelles ou ultimes) regroupent toutes
les informations que la méthode PCA n’a pas pu positionner ailleurs. Elles fournissent aussi des
suggestions de candidats à l’aberrance, moins évidents à traquer.
first_2 = sum(pca.explained_variance_ratio_[:2]*100)
last_2 = sum(pca.explained_variance_ratio_[-2:]*100)
plt.show()
La Figure 16.4 présente deux nuages de points pour les deux premières composantes. Le
programme présente également la variance telle qu’expliquée par les deux premières
composantes (soit la moitié du contenu d’informations du jeu) ainsi que par les deux dernières
composantes :
Figure 16.4 : Les deux premières et les deux dernières composantes en sortie d’analyse PCA.
Étudiez particulièrement les points de données selon les axes, l’axe des x correspondant à la
variable indépendante et l’axe des y à la variable dépendante. Vous pourriez ainsi détecter un
seuil qui permettra de distinguer les données correctes de celles qui sont douteuses.
En utilisant les deux composantes ultimes, vous allez pouvoir repérer quelques points à étudier
précisément en utilisant un seuil de –0.3 pour la dixième composante et un seuil de –1.0 pour
la neuvième. Tous les cas situés entre ces deux valeurs sont des candidats à l’aberrance
(Figure 16.5).
Figure 16.5 : Les cas aberrants éventuels détectés par l’analyse PCA.
df[DB.labels_==-1]
Comme déjà vu dans le chapitre précédent, DBScan a besoin des deux paramètres eps et
min_samples. Pour trouver les bonnes valeurs, vous allez devoir faire des essais en réajustant
les deux paramètres, un travail assez délicat.
Vous commencez par une petite valeur pour min_samples en augmentant progressivement la
valeur de eps à partir de 0.1. Après chaque essai, vous évaluez la situation en comptant le
nombre d’observations qui se trouvent dans la classe –1 pour l’attribut labels. Vous arrêtez
d’itérer lorsque le nombre d’aberrants ainsi détectés semble suffisant pour vous lancer dans
une inspection visuelle.
Vous trouverez toujours des points de données situés sur les bordures des parties denses et il
est donc difficile de fournir un seuil pour le nombre de cas candidats à faire partie de la classe
des –1. En règle générale, les aberrants ne doivent pas constituer plus de 5 % des cas, mais ce
n’est qu’une règle générale.
Les résultats du précédent exemple vous apprennent combien d’exemples font partie du
groupe des –1, c’est-à-dire ceux que l’algorithme considère comme extérieurs au groupe
principal. Vous découvrez à cette occasion la liste des cas qui font partie de ce groupe.
Bien que ce soit moins automatique, vous pouvez également appliquer l’algorithme de
regroupement par k-moyennes pour détecter les aberrants. Vous commencez par une analyse
avec regroupement pour obtenir un nombre de groupes suffisamment important. (Si vous
n’êtes pas sûr, essayez plusieurs solutions.) Vous pouvez ensuite chercher les groupes qui ne
contiennent que quelques cas, voire un seul ; ce sont sans doute des aberrants parce que ce
sont de petits groupes à l’écart des grands groupes contenant la majorité des observations.
evaluation = auto_detection.predict(Xc)
df[evaluation==-1]
Le résultat permet d’obtenir la liste des cas suspects. L’algorithme est entraîné à reconnaître
les cas normaux. Lorsque vous injectez de nouveaux cas dans le jeu puis les évaluez avec un
algorithme IsolationForest déjà entraîné, vous pouvez rapidement voir si ces nouvelles
données posent problème et où.
L’algorithme IsolationForest est gourmand en puissance de traitement. Si vous lancez une
analyse sur un vaste jeu de données, prévoyez beaucoup de temps et beaucoup d’espace
mémoire.
PARTIE 5
Apprendre des données
ette partie applicative du livre vous propose de découvrir les algorithmes et outils qui vont
C permettre d’apprendre grâce aux données, c’est-à-dire d’entraîner un modèle, pour ensuite
prédire des estimations numériques (par exemple des prix dans l’immobilier) ou des
estimations catégorielles de classe (par exemple différentes espèces de fleurs) en partant de
nouveaux exemples. Nous allons commencer par l’algorithme le plus simple pour aller vers les
plus sophistiqués. Les quatre algorithmes présentés ici constituent un point de départ
classique pour toute personne intéressée par la datalogie.
Le fichier calepin des exemples de ce chapitre correspond à PYDASC_17.
y = bx + a
Cette formule théorique peut être réexprimée de façon plus parlante en fonction du domaine
d’application. Par exemple, vous aurez besoin d’estimer les ventes futures en fonction des
ventes passées et du budget consacré à la publicité pour ces produits. Dans ce cas, la formule
devient celle-ci :
VentesFut = B*(BudgetPub) + A
La classe de Scikit-learn qui permet les régressions fait partie du module linear_ model. Une
fois que vous avez ajusté l’échelle de la variable X, vous n’avez pas d’autres paramètres ni de
préparatifs à prévoir pour profiter de cet algorithme.
Une fois que l’algorithme a été ajusté, vous vous servez de la méthode nommée score() pour
obtenir la valeur de R2. Cette mesure entre 0 et 1 permet de savoir si un modèle de régression
en particulier réussit mieux à prédire y qu’une simple moyenne. L’opération d’ajustement
produit une ligne ou une courbe qui épouse au mieux les points de données fournis en entrée ;
vous ajustez cette ligne ou cette courbe pour pouvoir réaliser d’autres opérations, et
notamment des prédictions basées sur les tendances ou les motifs obtenus grâce à ces
données. La valeur de R2 peut également être considérée comme la quantité d’informations
cibles expliquée par le modèle (comme dans une corrélation élevée au carré) ; une valeur
proche de 1 prouve que le modèle parvient à expliquer l’essentiel de la variable y.
print(regression.score(X, y))
0.740607742865
Dans l’exemple, la valeur de R2 est d’environ 0.74, ce qui est un bon résultat pour un modèle
simple. Ce score peut être compris comme le pourcentage d’informations encore présentes
dans la variable cible, tout en étant explicables par le modèle avec ses prédicteurs. Ce score
signifie que le modèle couvre une grande partie de l’information que vous vouliez prédire et
qu’il ne reste que 26 % inexpliqués.
En statistiques, lorsque vous utilisez des modèles linéaires, il est considéré comme acceptable
et raisonnable de calculer R2 sur le même jeu de données que celui servant à l’entraînement.
En datalogie et en apprentissage machine, il est toujours conseillé de tester les scores sur des
données qui n’ont pas servi à l’entraînement. Les algorithmes plus complexes réussissent
mieux à mémoriser les données de mécapprentissage, mais cette remarque s’avère parfois
vraie aussi pour les modèles plus simples tels que la régression linéaire.
Pour comprendre ce qui pilote les estimations dans le modèle de régression multiple, il faut
s’intéresser à l’attribut nommé coef_ ; c’est un tableau contenant les coefficients de
régression bêta. Ces coefficients sont les nombres qui ont été estimés par le modèle de
régression pour transformer en réalité les variables d’entrée de la formule vers la cible de
prédiction y. L’attribut boston.DESCR est affiché en même temps pour vous aider à savoir
quelle variable est référencée par les coefficients. La fonction zip() génère une liste itérable
des deux attributs et vous pouvez l’afficher :
Les variables indiquées avec les coefficients arrondis (les valeurs de b qui correspondent aux
pentes comme indiqué un peu plus haut) sont les suivantes :
DIS indique les distances pondérées menant à cinq agences pour l’emploi. C’est le coefficient
qui a le plus d’impact absolu. Par exemple, dans un contexte immobilier, un logement trop
éloigné des centres d’intérêt tels que les bassins d’emploi va faire baisser la valeur. En
revanche, AGE et INDUS qui décrivent l’âge du bâtiment et la disponibilité d’activités dans la
région autres que le commerce de détail n’ont pas autant d’influence sur le résultat parce que
la valeur absolue de leur coefficient bêta est inférieure à DIS.
Pour simplifier l’utilisation de l’exemple, nous éliminons une valeur, ce qui nous permet ensuite
de l’utiliser pour tester l’efficacité du modèle de régression logistique.
La régression logistique ne se contente pas d’afficher la classe résultante qui est ici la
classe 2 ; elle procède en outre à une estimation de la probabilité que l’observation fasse partie
des trois classes. En se basant sur l’observation qui a servi à la prédiction, la régression
logistique estime une probabilité de 71 % d’appartenance à la classe 2. C’est une probabilité
forte, mais cela laisse une marge d’incertitude.
Les probabilités permettent d’estimer la classe la plus probable, mais vous pouvez également
trier les prédictions selon leur probabilité de faire partie de cette classe. Cela vous sera
particulièrement utile dans le domaine médical : le classement d’une prédiction en termes
d’éventualités par rapport à d’autres permet d’apprendre quelle maladie les patients risquent
le plus de contracter ou s’ils sont déjà atteints.
Stratégies multiclasses
La technique de régression logistique que nous venons de découvrir sait gérer
automatiquement les problèmes à classes multiples (nous avions travaillé avec trois espèces
d’iris à deviner au départ). La plupart des algorithmes de la librairie Scikit-learn qui prédisent
des probabilités ou des scores de classes savent d’office gérer des problèmes à plusieurs
classes, au moyen de deux stratégies différentes :
» Une contre toutes : l’algorithme compare chaque classe à toutes les autres, en
construisant un modèle par classe. S’il y a dix classes à trouver, vous obtenez dix modèles.
Cette stratégie se fonde sur la classe de Scikit-learn nommée OneVsRestClassifier.
» Une contre une : l’algorithme compare chaque classe contre chacune des autres
classes, afin de construire un nombre de modèles égal à n * (n-1) / 2, n correspondant
au nombre de classes. Si vous avez dix classes, vous obtenez 45 modèles puisque 10 *
(10 - 1) / 2. Cette technique se fonde sur la classe OneVsOneClassifier.
La stratégie multiclasse utilisée par défaut dans la régression logistique est Une contre toutes.
Découvrons par un exemple comment utiliser les deux stratégies avec le jeu de chiffres
manuscrits, avec des classes pour les nombres entre 0 et 9. Commençons par charger les
données et les stocker dans des variables :
Lorsque vous lisez des segments de la formule, par exemple P(A¦B), il faut procéder ainsi : il
s’agit de la probabilité de A sachant B. Le symbole ¦ correspond à sachant. Il s’agit dans ce
cas d’une probabilité conditionnelle parce que c’est la probabilité de survenue de A qui dépend
d’une évidence B. Dans l’exemple, si nous injectons les valeurs numériques dans la formule,
nous obtenons : 60 % * 50 % / 35 % = 85,7 %.
Dans l’exemple, même s’il n’y a que 50 % de chances qu’une personne soit de sexe féminin, le
fait de connaître l’évidence de la chevelure permet d’atteindre 85,7 %, ce qui est bien plus
favorable. Vous aurez beaucoup plus de chances de faire une bonne prédiction qu’une
personne ayant les cheveux longs soit une femme parce que vous avez un peu moins de 15 %
de risques de vous tromper.
L’approche naïve bayésienne est très prisée parce qu’elle ne requiert pas trop de données pour
fonctionner. Elle gère naturellement les classes multiples. Elle sait même gérer les variables
numériques, à condition d’appliquer quelques modifications aux variables pour les transformer
en classes. Scikit-learn propose trois classes naïves bayésiennes dans son module
sklearn.naive_bayes :
» MultinomialNB : se base sur les probabilités dérivées de la présence d’une
caractéristique. Lorsque c’est le cas, la classe affecte une certaine probabilité au résultat
indiqué par les données textuelles pour la prédiction.
» BernoulliNB : fournit la fonctionnalité multinomiale naïve bayésienne, mais en
pénalisant l’absence d’une caractéristique. La probabilité est différente selon que la
caractéristique est présente ou pas. Toutes les caractéristiques sont considérées comme
des variables dichotomes (la distribution d’une variable dichotome est une des options de
Bernoulli). Vous pouvez l’utiliser avec des données de type texte.
» GaussianNB : cette version de l’algorithme naïf bayésien suppose que toutes les
caractéristiques sont dans une distribution normale. Cette classe n’est vraiment pas idéale
pour les données textuelles contenant des mots peu denses ou dans lesquels la densité
des mots est faible (dans ce cas, mieux vaut utiliser une distribution multinomiale ou de
Bernoulli). C’est le meilleur choix en revanche si les variables ont des valeurs positives et
négatives.
Une fois les deux jeux chargés en mémoire, nous importons les deux modèles bayésiens pour
les instancier. Nous commençons par définir les valeurs alpha qui vont éviter que les
caractéristiques rares obtiennent une probabilité nulle, ce qui les exclurait de l’analyse. Nous
choisissons une toute petite valeur alpha, comme vous pouvez le voir ci-dessous :
Nous avions utilisé la technique de hachage dans le Chapitre 12 pour modéliser des données
textuelles sans craindre l’apparition de nouveaux mots après la phase d’entraînement. Deux
techniques de hachage différentes sont possibles : la première consiste à compter les mots
dans l’approche multinomiale, et l’autre se limite à noter qu’un mot est apparu ou pas dans
une variable binaire, ce qui est l’approche binomiale. Nous allons bien sûr nettoyer le jeu en
supprimant les mots vides (N. d. T. : comme dans les exemples précédents, nous nous en
tenons aux mots vides de la langue anglaise, mais revoyez le Chapitre 12 si nécessaire).
Nous pouvons maintenant lancer l’entraînement des deux classifieurs puis les tester sur le jeu
de test qui correspond à une série d’articles apparaissant à la suite de ceux du jeu
d’entraînement. Nous mesurons la précision sous forme d’un pourcentage de prédiction
correcte.
import numpy as np
target = newsgroups_train.target
target_test = newsgroups_test.target
multi_X = np.abs(
multinomial.transform(newsgroups_train.data))
multi_Xt = np.abs(
multinomial.transform(newsgroups_test.data))
bin_X = binary.transform(newsgroups_train.data)
bin_Xt = binary.transform(newsgroups_test.data)
Multinomial.fit(multi_X, target)
Bernoulli.fit(bin_X, target)
Vous aurez remarqué que l’entraînement des deux modèles et la génération de leurs
prédictions sur le jeu test ne prennent que peu de temps. Cela est remarquable lorsque vous
songez que le jeu d’entraînement contient plus de 11 000 articles avec 300 000 mots et que le
jeu de test en contient encore 7500 autres.
L’exécution de ce code nous permet d’obtenir des statistiques intéressantes sur le texte :
Soyez averti que KNN est assez sensible aux données aberrantes. Vous devez également
ajuster les échelles des variables et supprimer les données redondantes. Dans l’exemple, nous
nous servons de l’analyse par composantes principales PCA. Ici, il n’est pas nécessaire de
rééchelonner, parce que les données correspondent à des pixels qui sont tous au même format.
Pour éviter le problème des aberrants, vous maintenez un critère de voisinage faible, c’est-à-
dire que vous n’allez pas chercher les similarités à une trop grande distance.
Vous pourrez gagner beaucoup de temps et éviter bien des erreurs si vous réussissez à
connaître le type des données. Dans l’exemple, nous savons que ce sont des matrices de pixels.
Vous appliquerez toujours comme première étape une analyse exploratoire EDA (Chapitre 13)
pour obtenir quelques informations intéressantes. Dans tous les cas, il est de bonne pratique
de toujours chercher à obtenir des informations complémentaires au sujet de la façon dont les
données ont été générées et de ce qu’elles représentent.
Pour passer au test, nous stockons nos cas dans tX, puis essayons quelques cas que KNN ne va
pas considérer comme voisins :
KNN se base sur une distance pour décider qu’une observation est voisine du cas cible ou pas.
Vous réglez cette distance prédéfinie au moyen du paramètre p :
» Lorsque p vaut 2, nous utilisons la distance euclidienne qui a été décrite dans la partie sur
les regroupements du Chapitre 15.
» Lorsque p vaut 1, nous utilisons la distance de Manhattan qui est une distance absolue
entre les observations dans une grille orthonormée. Vous allez d’un point à un autre par
angles droits, comme lorsque vous marchez dans une ville moderne. La distance
euclidienne est la distance en diagonale. La distance de Manhattan n’est pas le chemin le
plus court, mais elle est plus réaliste et surtout moins sensible aux bruits et au fléau des
dimensions.
En général, la distance euclidienne est la bonne, mais elle peut donner parfois des résultats
pires que l’autre, notamment s’il y a beaucoup de variables corrélées. Notre code d’exemple
montre que l’analyse l’accepte bien.
Nous obtenons la précision ainsi qu’un échantillon des prédictions, ce qui permet de les
comparer avec les valeurs réelles afin de repérer les différences :
Précision: 0.990
Prédiction: [2 2 5 7 9 5 4 8 1 4 9 0 8 9 8]
Réalité : [2 2 5 7 9 5 4 8 8 4 9 0 8 9 8]
L’exécution de ce code permet de voir quel est l’effet du changement de valeur de k, ce qui
permet de décider de la valeur optimale par rapport aux données disponibles :
Quelques tests vous permettent de conclure que le paramètre n_neighbors qui est celui
correspondant à k doit être réglé à la valeur 5 qui donne la meilleure précision. Vous pouvez le
régler au voisinage le plus proche avec n_neighbors=1, pour obtenir des résultats acceptables.
En revanche, toute valeur supérieure à 5 donne des résultats de moins en moins bons au
niveau classification.
Donnez-vous la règle générale suivante : si votre jeu de données ne contient pas trop
d’observations, donnez à k une valeur proche du nombre d’observations disponible élevé au
carré. Cette règle n’est cependant pas rigide. Il est toujours conseillé d’essayer plusieurs
valeurs de k pour optimiser les performances de la technique KNN. Commencez toujours par
une petite valeur et augmentez-la progressivement.
Chapitre18
Validation croisée, sélectionet optimisation
DANS CE CHAPITRE :
» Problèmes de sur- et de sous-ajustement
L es
algorithmes de mécapprentissage (apprentissage machine) apprennent en étudiant les
données. Les quatre algorithmes que nous avons vus dans le précédent chapitre sont assez
simples, mais permettent néanmoins d’estimer efficacement une classe ou une valeur
lorsque vous leur fournissez des exemples avec des valeurs cibles. Un apprentissage par
induction, consistant à trouver une règle générale à partir d’exemples particuliers. Les
humains procèdent dès l’enfance à ce genre d’apprentissage par étude de quelques exemples
pour en tirer des règles générales des concepts. Il applique ensuite ces règles à de nouvelles
situations. Lorsque vous voyez quelqu’un se brûler en touchant une plaque chaude, vous
comprenez immédiatement que l’action est dangereuse, et vous évitez d’y toucher, sans avoir
besoin d’en faire l’expérience.
L’apprentissage par les exemples des algorithmes souffre de quelques pièges. Voici quelques-
uns des problèmes qui peuvent se présenter :
» Il n’y a pas assez d’exemples pour en tirer une règle, quel que soit l’algorithme machine
utilisé.
» L’application d’apprentissage est alimentée avec de mauvais exemples et ne peut donc
pas fonctionner correctement.
» Même lorsque l’application obtient des exemples corrects et suffisants, elle ne parvient
pas en produire des règles à cause d’une trop grande complexité. L’inventeur de la loi de la
gravitation, Isaac Newton, père de la physique moderne, avait expliqué que son idée de la
gravitation était venue en voyant tomber une pomme. Le problème c’est qu’il n’est pas
donné à tout monde de tirer une loi universelle à partir d’une série d’observations ; les
algorithmes ne sont pas mieux lotis que nous.
Il est crucial de garder ces problèmes à l’esprit lorsque vous abordez l’apprentissage machine.
Une telle application va généraliser plus ou moins en fonction de la quantité de données, de
leur qualité et des caractéristiques de l’application, pour faire face à de nouveaux cas. S’il y a
le moindre souci à un niveau ou à un autre, vous allez subir des contraintes sérieuses. Vous
devez reconnaître et apprendre à éviter ce genre de pièges dans vos activités de datalogie.
Le fichier contenant le code source des exemples de ce chapitre correspond à PYDASC_18.
Le problème peut être lié à une classe trop fréquente ou prépondérante. Par exemple dans la
détection des fraudes, la plupart des transactions sont légitimes et seules quelques-unes sont
criminelles. Un algorithme optimisé pour la précision va favoriser la classe prépondérante et
sera donc fausse dans ses prédictions pour les classes mineures, ce qui est absolument
inacceptable pour un algorithme qui doit deviner toutes les classes correctement, et non
seulement certaines.
Les problèmes que vous ne pouvez pas gérer par la justesse peuvent l’être par les mesures de
précision precision et de rappel recall ainsi que par leur optimisation conjointe avec f1. La
précision concernée ici est celle des prédictions. La mesure compte le nombre de fois où
l’algorithme a bien prédit la bonne classe. Vous vous servirez de la mesure de précision pour
diagnostiquer des cancers après avoir évalué les données des examens. La précision
correspondant au pourcentage de patients qui ont réellement un cancer, par rapport à ceux qui
ont été diagnostiqués avec un cancer. Si vous avez diagnostiqué 10 patients malades alors
que 9 seulement sont vraiment atteints, la précision est de 90 %.
N. d. T. : Nous avons traduit accuracy par justesse et conservé précision pour precision.
Les conséquences ne sont bien sûr pas les mêmes selon que vous diagnostiquez un cancer chez
un patient qui n’en a pas (faux positif) ou manquez l’évaluation d’un patient qui a un cancer
(faux négatif). La précision ne constitue qu’une partie de l’histoire. Diagnostiquer comme sain
un patient qui doit en réalité être au plus vite soigné pour un cancer est un vrai problème. La
seconde partie de l’histoire correspond à la mesure de rappel recall. Pour toute une classe,
elle indique le pourcentage de prédictions correctes. En conservant le même exemple, la
mesure de rappel indique le pourcentage de patients qui ont été correctement diagnostiqués
comme atteints. S’il y avait 20 patients cancéreux et que vous en avez diagnostiqué 9, le rappel
est de 45 %.
Il peut arriver que vous ayez une bonne précision avec un faible niveau de rappel ou un fort
niveau de rappel tout en perdant de la précision. Vous pouvez tenter de maximiser les deux
mesures (précision et rappel) au moyen du score nommé f1 qui se base sur la formule
suivante :
Grâce au score f1, vous êtes certain de toujours disposer de la meilleure combinaison de
précision et de rappel.
La mesure de performances nommée ROC AUC qui est une fonction d’efficacité de récepteur
avec aire sous la courbe (Receiver Operating Characteristic Area Under Curve) permet de trier
les classifications en fonction de leur probabilité de vraisemblance. Dans notre exemple
précédent, l’algorithme d’apprentissage optimise la mesure ROC AUC en cherchant à trier les
patients en commençant par ceux qui sont le plus susceptibles d’être malades. La mesure ROC
AUC donne une valeur élevée lorsque l’ordre de tri est correct, et une valeur faible dans le cas
contraire. Lorsque votre modèle possède une valeur ROC AUC élevée, vous pouvez
immédiatement vous concentrer sur les patients les plus susceptibles d’être affectés. Dans un
projet de détection de fraude financière, vous voudrez trier les clients dans l’ordre décroissant
de leur risque de commettre une fraude. Si votre modèle possède un ROC AUC correct, vous
pouvez immédiatement vous concentrer sur les clients les plus risqués.
Le jeu comporte plus de 500 observations avec 13 caractéristiques. La cible est un prix ; nous
optons donc pour une régression linéaire et optimisons le résultat par la méthode des moindres
carrés. Il reste à vérifier que la régression linéaire convient bien au jeu de données Boston puis
à quantifier son niveau d’adéquation avec la méthode des moindres carrés (nous pourrons ainsi
comparer à d’autres modèles) :
Nous avons ajusté le modèle aux données par la méthode fit(). (Ce sont les données
d’entraînement qui alimentent le modèle en exemples.) Nous utilisons ensuite la fonction
mean_squared_error() pour calculer l’erreur de prédiction. La valeur obtenue, de l’ordre
de 21.90, semble correcte, mais elle a été calculée sur le jeu d’entraînement. Nous ne savons
donc pas si les choses se passeront aussi bien avec de nouvelles données (les algorithmes
d’apprentissage savent bien apprendre et bien mémoriser à partir d’exemples).
Il nous faut donc lancer un test avec des données que l’algorithme n’a jamais traitées pour
écarter toute possibilité de mémorisation. Ce n’est qu’ainsi que nous pourrons savoir si
l’algorithme sait traiter n’importe quelles nouvelles données. Nous allons donc attendre de
nouvelles données, faire des prédictions puis les confronter à la réalité. L’opération peut
prendre un certain temps, et donc représenter des risques et des coûts, tout dépendant du
type de problème à résoudre. (Certaines applications dans le domaine médical sont très
sensibles parce que des vies dépendent des conclusions.)
Par bonheur, nous avons une autre solution pour atteindre le même objectif : nous pouvons
simuler de nouvelles données en divisant en deux les observations, avec un sous-jeu
d’entraînement et un sous-jeu de test. Une pratique courante consiste à réserver de 25 à 30 %
du volume de données au jeu qui va servir à tester l’algorithme, le reste correspondant au jeu
d’entraînement. Voici comment réaliser un tel partitionnement avec Python :
Nous obtenons en résultat le profil des deux jeux obtenus suite à la séparation. Le jeu
d’entraînement comporte 70 % des données initiales et le jeu de test en compte 30 % :
Les deux jeux sont stockés dans les variables nommées X et y grâce à la fonction
train_test_split(). La proportion réservée au jeu de test est indiquée par le paramètre
test_size. Le contenu du jeu de test est toujours choisi au hasard par la fonction. Nous
pouvons ensuite nous servir du jeu d’entraînement :
regression.fit(X_train,y_train)
print(‘Erreur Entraînement moindres carrés: %.2f’ % mean_squared_error(
y_true=y_train, y_pred=regression.predict(X_train)))
Après ajustement, nous arrivons à une valeur de 19.07, inférieure à celle du précédent essai.
Mais ce qui nous intéresse est le taux d’erreur du jeu de test :
Notre mesure d’erreur est de 30.70 ! L’estimation sur le jeu d’entraînement était trop
optimiste. La valeur pour le jeu de test est plus réaliste, mais nous travaillons avec un volume
de données inférieur (moins de la moitié). Le résultat va évidemment changer si vous modifiez
les proportions. Voyons l’impact d’une retouche :
Ce petit exercice vous donne une idée des problèmes à prendre en compte avec les
algorithmes d’apprentissage. Chaque algorithme se caractérise par un certain niveau de biais
ou de variance dans sa prédiction des résultats. Votre problème est que vous ne pouvez pas en
estimer précisément l’impact. Lorsque vous devez choisir l’algorithme, vous ne pouvez pas être
certain de pouvoir prendre la décision la plus efficace.
Il est toujours déconseillé d’utiliser les données d’entraînement pour évaluer les performances
d’un algorithme parce qu’il va naturellement mieux réussir avec ces données qu’il connaît déjà.
C’est encore plus le cas avec les algorithmes qui ont un biais faible, en raison de la complexité.
Vous pouvez vous attendre à un faible taux d’erreur sur les données d’entraînement, ce qui
donne un avantage incongru à cet algorithme lorsque vous allez le comparer à d’autres qui ont
un profil biais/variance différent. En travaillant avec les données de test, vous réduisez le
volume d’exemples de l’entraînement, ce qui peut faire baisser les performances de
l’algorithme, mais vous obtenez en échange une estimation d’erreur plus fiable et plus
pertinente pour vos comparaisons.
Validation croisée
Lorsque le jeu de test produit à répétition des résultats instables à cause de la façon dont les
valeurs sont échantillonnées, une solution consiste à prélever plusieurs jeux de test pour en
produire une moyenne. Il s’agit d’une approche statistique qui est à la base de la technique de
validation croisée. En voici la recette simple :
1. Vous segmentez vos données en plusieurs plis (folds), chacun contenant la
même distribution de cas. En général, prévoyez 10 cas par pli ; mais vous pouvez choisir
une valeur de 3, 5 ou même 20.
2. Gardez un des plis comme jeu de test, tous les autres serviront de jeux
d’entraînement.
3. Lancez l’entraînement puis notez le résultat de votre premier jeu de test. Si vous
n’avez pas beaucoup de données, utilisez un grand nombre de plis, car le volume de
données et le nombre de plis ont tous deux un effet positif sur la qualité de l’entraînement.
4. Répétez les étapes 2 et 3 en choisissant à chaque fois un autre pli comme jeu de
test.
5. Calculez la moyenne et l’écart type de tous les résultats de tests. Vous aurez ainsi
une bonne estimation de la qualité du prédicteur. L’écart type vous donnera sa fiabilité. (Si
la valeur est trop élevée, une erreur de validation croisée pourrait se montrer imprécise.)
Attendez-vous à ce que les prédicteurs à variance élevée montrent un écart type de
validation croisée élevé.
Cette technique peut sembler un peu complexe, mais Scikit-learn vous facilite le travail grâce
aux fonctions de son module sklearn.model_selection.
%matplotlib inline
import pandas as pd
df = pd.DataFrame(X, columns=boston.feature_names)
df[‘target’] = y
df.boxplot(‘target’, by=’CHAS’, return_type=’axes’);
La boîte à moustaches de la Figure 18.1 confirme que les maisons situées le long de la rivière
se vendent plus cher que les autres. Bien sûr, il y a des maisons à prix élevé dans tout Boston,
mais il est important de surveiller combien de maisons en bordure de rivière font partie de
votre analyse parce que le modèle doit être général pour la totalité de la ville de Boston, et non
limité au cas particulier des biens situés le long de la Charles River.
Lorsqu’une caractéristique est rare ou à fort impact, vous ne pouvez pas a priori savoir si elle
est présente dans un échantillon puisque les plis sont créés de façon aléatoire. Si la
caractéristique est présente en trop forte ou trop faible quantité dans le pli, l’algorithme risque
de déduire des règles incorrectes.
Pour vous éviter de faire travailler vos algorithmes avec des échantillons malformés lors de vos
validations croisées, exploitez la classe nommée StratifiedKFold. Elle sait contrôler
l’échantillonnage de sorte que certaines caractéristiques, ou même certains résultats (lorsque
les classes cibles sont très déséquilibrées) seront toujours présents dans les plis et dans la
bonne proportion. Vous vous contentez d’indiquer quelles variables vous voulez contrôler au
moyen du paramètre y, ce que montre l’exemple suivant :
L’erreur de validation reste la même, mais en contrôlant la variable CHAR, l’écart type
diminue, ce qui vous confirme que la variable avait un impact sur les résultats de validation
croisée précédents.
Lorsque votre sélection concerne un problème de classification, les deux approches f_classif
et chi2 ont tendance à produire le même jeu de variables prépondérantes. Il reste néanmoins
conseillé de tester les deux métriques pour réaliser vos sélections.
Dans ce résultat, un F-score (ou F-mesure) élevé indique une association plus forte entre une
caractéristique et la variable cible. La lecture de ces valeurs permet de sélectionner les
variables les plus importantes pour le modèle d’apprentissage. Soyez cependant à l’affût pour
les aspects suivants :
» Une variable à forte association peut aussi être fortement corrélée, ce qui indique une
duplication d’informations, qui a l’effet d’un bruit dans l’apprentissage.
» Certaines variables peuvent être injustement pénalisées, notamment les variables
binaires qui valent 1 ou 0. Vous remarquez par exemple que nos résultats indiquent que la
variable binaire CHAS est la moins bien associée à la variable cible, alors que nous savons
par nos essais précédents qu’elle a eu un impact lors de la phase de validation croisée.
Ce processus de sélection univariée offre un vrai avantage lorsque le nombre de variables est
important et que toutes les autres méthodes s’avèrent inutilisables en pratique. La meilleure
approche consiste à réduire la valeur de SelectPercentile d’au moins la moitié du nombre de
variables disponibles puis de réduire le nombre de variables jusqu’à une quantité gérable afin
de permettre l’utilisation d’une méthode plus sophistiquée, plus précise, telle que la sélection
avide que nous découvrons maintenant.
Vous pouvez récupérer un index vers ce jeu de variables optimal en utilisant l’attribut
support_ de la classe RFECV après ajustement :
print(boston.feature_names[selector.support_])
Vous constatez avec joie que CHAS apparaît dorénavant parmi les caractéristiques les plus
influentes, ce qui n’était pas le cas de la recherche univariée par l’approche précédente. RFECV
peut détecter qu’une variable est importante, qu’elle soit binaire, catégorielle ou numérique.
C’est tout simplement parce que cette méthode évalue directement le rôle que joue la
caractéristique dans la prédiction.
La méthode RFECV est certainement plus efficace que l’approche univariée puisqu’elle tient
compte des caractéristiques à forte corrélation ; elle est en outre réglée pour optimiser la
mesure de l’évaluation (qui n’est en général, ni chi2, ni F-score). Mais c’est un processus avide,
qui réclame donc beaucoup de puissance de traitement, et ne peut donner qu’une
approximation du meilleur ensemble de prédicteurs.
Comme tous les algorithmes d’apprentissage, RFECV risque de sur-ajuster la sélection. Vous
devez tester la méthode RFECV sur différents échantillons du jeu d’entraînement pour
confirmer quelles sont les meilleures variables à sélectionner.
Votre prédiction sera encore plus apte à être généralisée et vos résultats bonifiés si vous
peaufinez vos hyperparamètres, notamment avec les algorithmes complexes dont le
fonctionnement n’est pas formidable si vous vous en tenez au paramétrage par défaut.
Le terme hyperparamètre désigne un paramètre que vous devez vous-même choisir et régler,
car l’algorithme ne peut pas les ajuster par lui-même à partir des données. Comme toutes les
autres décisions qui doivent être prises par le datalogue dans un processus d’apprentissage, il
vous faut choisir avec tact, après avoir étudié les résultats de la validation croisée.
Le module de Scikit-learn nommé sklearn.grid_search est conçu pour optimiser les
hyperparamètres. Il réunit plusieurs utilitaires qui automatisent et simplifient la recherche des
meilleures valeurs pour chacun d’eux. Le code source des prochains paragraphes présente les
procédures appropriées, et nous commençons par charger en mémoire le jeu de données des
fleurs Iris :
import numpy as np
from sklearn.datasets import load_iris
iris = load_iris()
X, y = iris.data, iris.target
Une fois le jeu d’iris et la librairie NumPy en place, nous pouvons passer à l’optimisation de
l’algorithme pour bien prédire les espèces d’iris.
En considérant une plage de valeurs pour chacun des paramètres, vous devinez aisément que
cela correspond au test d’un assez grand nombre de modèles, 40 dans notre cas :
Pour préparer vos recherches, il vous faut construire un dictionnaire Python dont les clés sont
les noms des paramètres et les valeurs les listes de valeurs à tester. Notre exemple définit une
plage entre 1 et 10 pour l’hyperparamètre n_neighbors en spécifiant l’itérateur range(1,11)
qui va produire la séquence de nombres pendant la recherche.
Avant de démarrer, il reste à évaluer le score de la validation croisée à partir d’un modèle
témoin qui possède les paramètres par défaut suivants :
Vous devez noter le résultat pour pouvoir juger de l’amélioration apportée par le réglage des
paramètres :
L’exemple commence par tester le témoin grâce à la mesure de précision (le pourcentage de
réponses exactes). Ce témoin incarne les hyperparamètres par défaut de l’algorithme (tel
qu’expliqué lors de l’instanciation de la variable classifier dans sa classe). Il n’est pas aisé
d’améliorer la précision qui est déjà ici de 0.967 (96,7 %). Nous avons trouvé la solution en
passant par une validation croisée à dix plis :
L’instanciation est faite avec l’algorithme, le dictionnaire de recherche, la mesure et les plis de
validation croisée. La classe GridSearch travaille ensuite avec la méthode fit(). La
recherche étant terminée, il est possible de réajuster le modèle avec la meilleure combinaison
de paramètres trouvée, si vous spécifiez refit=True. Vous pouvez ainsi immédiatement lancer
une prédiction en utilisant directement la classe GridSearch. Nous allons bien sûr afficher les
meilleurs paramètres et le score que procure la meilleure combinaison :
Les résultats sont fournis par les attributs best_params_ et best_score_. La précision atteinte
vaut 0.973, ce qui est une amélioration par rapport au témoin. Nous pouvons aussi inspecter la
séquence complète de scores de validation croisée ainsi que l’écart type :
print(search.cv_results_)
Le grand nombre de combinaisons testées montre que quelques-unes ont obtenu le score
de 0.973, lorsque les combinaisons avaient neuf ou dix voisins. Vous pouvez profiter d’une
classe de visualisation de Scikit afin de mieux comprendre comment l’optimisation a travaillé
en relation avec le nombre de voisins. La méthode validation_curve() donne tous détails au
sujet du comportement des méthodes train() et validation() en fonction des
hyperparamètres différents dans n_neighbors.
La classe validation_curve propose deux tableaux qui contiennent les résultats avec les
valeurs des paramètres en ligne et les plis de validation croisée en colonne.
L’action de projeter les lignes génère une visualisation graphique (Figure 18.2). Vous
comprenez ainsi mieux comment se déroule le processus d’apprentissage.
Figure 18.2 : Affichage des courbes de validation.
Recherches aléatoires
La recherche combinatoire est exhaustive mais prend beaucoup de temps. Elle a tendance à
sur-ajuster les plis de validation croisée si vous n’avez pas assez d’observations dans vos
données, et cherchez de façon extensive une optimisation. Notre approche correspond à la
recherche aléatoire dans laquelle vous définissez une recherche combinatoire, mais ne testez
que certaines des combinaisons choisies au hasard.
Vous pourriez croire que cela revient à tâtonner dans le noir. En fait, ce genre de recherche est
assez utile de par son inefficacité ! Si vous choisissez un assez grand nombre de combinaisons
aléatoires, vous aurez statistiquement beaucoup de chances de trouver une combinaison de
paramètres optimale, sans cette fois-ci risquer un sur-ajustement. Dans l’exemple précédent,
nous avons fait tester 40 modèles systématiquement. Avec une recherche aléatoire, nous
pouvons réduire de 75 % ce volume (10 modèles seulement) et atteindre le même niveau
d’optimisation.
La recherche aléatoire est simple à mettre en place. Vous commencez par importer la classe
depuis le module grid_search, plus vous réutilisez les paramètres de suite GridSearchCV.
Vous y ajoutez un paramètre n_iter qui décide du nom de combinaisons à essayer. Vous
pouvez en règle générale conserver un quart à un tiers du nombre total de combinaisons
d’hyperparamètres :
L’affichage nous permet d’apprendre que la recherche aléatoire fournit des résultats similaires
à ceux d’une recherche systématique réclamant beaucoup de puissance machine.
Chapitre 19
Vers plus de complexité linéaire et non linéaire
DANS CE CHAPITRE :
» Renforcement polynomial des caractéristiques
D ans les précédents chapitres, nous avons découvert certains des algorithmes
d’apprentissage les plus simples, mais assez efficaces, parmi lesquels la régression linéaire
et la régression logistique, la méthode naïve bayésienne et la technique des plus proches
voisins KNN. La connaissance de ces techniques suffit pour mener à bien un projet de
régression ou de classification. Nous pouvons maintenant partir à la découverte de techniques
encore plus complexes et puissantes : renforcement des caractéristiques des données,
amélioration des estimations par une régularisation et exploitation des mégadonnées en les
répartissant en partitions plus facilement gérables.
Nous verrons également une famille d’algorithmes très puissante pour la classification et la
régression : les séparateurs à vastes marges ou machines à vecteurs de supports SVM. Nous
ne négligerons pas une présentation des réseaux neuronaux. Ces deux techniques permettent
de résoudre les problèmes de datalogie les plus ardus. Les machines SVM avaient pris
récemment le dessus sur les réseaux neuronaux, mais ces derniers ainsi que les ensembles
arborescents ont repris l’avantage en tant qu’outils de prédiction incontournables (nous y
reviendrons dans le Chapitre 20). Les réseaux neuronaux existent depuis longtemps, mais ce
n’est que depuis quelques années qu’ils se sont améliorés au point de devenir des outils
incontournables pour les prédictions de données de type image ou texte. Nous consacrons un
certain nombre de pages aux machines SVM, en raison de la complexité des opérations de
régression et de classification avec des techniques évoluées et nous en consacrons quelques-
unes aux réseaux neuronaux. Vous n’hésiterez pas à approfondir vos connaissances dans ces
deux domaines ; vos efforts seront largement récompensés.
Le fichier calepin contenant les exemples de ce chapitre correspond à PYDASC_19. Deux
représentations graphiques concernant les algorithmes des machines SVM correspondent au
fichier PYDASC_19_SVM.
boston = load_boston()
random.seed(0) # Creates a replicable shuffling
new_index = list(range(boston.data.shape[0]))
shuffle(new_index) # shuffling the index
X, y = boston.data[new_index], boston.target[new_index]
print(X.shape, y.shape, boston.feature_names)
Pour faciliter l’exploration et le traitement des données qui vont suivre, nous convertissons le
tableau de prédicteurs et la variable cible dans une structure de données DataFrame de
pandas. Normalement, Scikit-learn doit être alimenté par une structure de tableau ndarray,
mais les objets DataFrame sont acceptés également :
import pandas as pd
df = pd.DataFrame(X,columns=boston.feature_names)
df[‘target’] = y
La façon la plus efficace de détecter les transformations consiste à générer une représentation
graphique en nuage de points. Cela permet de bien voir évoluer deux variables. L’objectif est
de rendre les plus linéaires possible les relations entre prédicteurs et résultat. Nous allons
donc tenter plusieurs combinaisons, par exemple, celle-ci :
La Figure 19.1 montre le nuage de points correspondant. Nous constatons que nous pouvons
appliquer une courbe comme approximation du nuage de points, et non une ligne droite.
Lorsque LSTAT vaut environ 5, il semble que la cible prenne des valeurs entre 20 et 50. Les
excursions diminuent jusqu’à environ 10 lorsque LSTAT augmente.
Figure 19.1 : Relations non linéaires entre la variable LSTAT et les prix cibles.
Une transformation logarithmique pourrait nous aider dans un tel contexte, mais vos valeurs
doivent se situer entre 0 et 1, comme des pourcentages, ce que montre l’exemple. Dans les
autres cas, vous pouvez appliquer quelques autres transformations à votre variable x, et
notamment : x**2, x**3, 1/x, 1/x**2, 1/x**3 et sqrt(x). Il s’agit de les essayer et de
tester le résultat. Pour ce test, vous pouvez vous inspirer du script suivant :
import numpy as np
from sklearn.feature_selection import f_regression
single_variable = df[‘LSTAT’].values.reshape(-1, 1)
F, pval = f_regression(single_variable, y)
print(‘F score de la caractéristique originale : %.1f’ % F)
F, pval = f_regression(np.log(single_variable),y)
print(‘F score de la caractéristique transformée: %.1f’ % F)
Cet exemple affiche la F-mesure (score) qui permet de juger à quel point une caractéristique
prédit une solution d’un problème d’apprentissage, aussi bien une caractéristique d’origine
qu’une ayant été transformée. Nous constatons que le score de la caractéristique transformée
apporte une vraie amélioration.
Cette mesure F est très utile pour sélectionner les variables. Elle permet aussi de vérifier
l’intérêt de faire une transformation. En effet, aussi bien f_regres-sion que f_classif se
basent sur un modèle linéaire et sont donc sensibles à toute transformation réelle qui permet
de rendre les relations d’une variable plus linéaires.
Voyons donc comment tester et détecter les interactions dans notre jeu de test Boston. Nous
commençons par charger quelques classes de travail :
df = pd.DataFrame(X,columns=boston.feature_names)
baseline = np.mean(cross_val_score(regression, df, y,
scoring=’r2’,
cv=crossvalidation))
interactions = list()
for var_A in boston.feature_names:
for var_B in boston.feature_names:
if var_A > var_B:
df[‘interaction’] = df[var_A] * df[var_B]
cv = cross_val_score(regression, df, y,
scoring=’r2’,
cv=crossvalidation)
score = round(np.mean(cv), 3)
if score > baseline:
interactions.append((var_A, var_B, score))
print(‘Témoin (baseline) R2: %.3f’ % baseline)
print(‘Top 10 des interactions: %s’ % sorted(interactions,
key=lambda x :x[2],
reverse=True)[:10])
Cet exemple teste l’effet de l’addition de chaque interaction au modèle en utilisant une
validation croisée à 10 plis. (Nous avons vu dans le Chapitre 18 comment exploiter les
validations croisées et les plis.) Le code mémorise les changements de la mesure de R2 dans
une pile qui est une simple liste, permettant à l’application de trier et d’analyser ces résultats.
Le score du témoin (baseline) est de 0.699 ; la pile d’interaction montre une amélioration
sensible avec une valeur de 0.782. Il est essentiel de savoir ce qui est à l’origine de cette
amélioration. Les deux variables concernées sont RM (le nombre de pièces moyens, rooms) et
LSTAT (le pourcentage de la population à faibles revenus, lower). Un nuage de points va
permettre de visualiser l’impact de ces deux variables :
La Figure 19.2 clarifie l’amélioration. Une partie des biens situés au centre du diagramme
oblige à connaître les deux variables LSTAT et RM afin de pouvoir correctement distinguer les
biens à prix élevés des biens bon marché. C’est pourquoi une interaction est indispensable.
Figure 19.2 : La combinaison des variables LSTAT et RM aide à distinguer les biens chers des biens bon marché.
Le suivi des améliorations au fur et à mesure de l’ajout de nouveaux termes complexes est
réalisé par le stockage de valeurs dans la liste nommée improvements. La visualisation
graphique des résultats est disponible dans la Figure 19.3. Nous constatons que certains ajouts
sont intéressants parce que l’erreur quadratique diminue alors que d’autres additions sont
inacceptables, puisqu’elles augmentent l’erreur.
Figure 19.3 : Augmentation de la puissance de prédiction par ajout de caractéristiques polynomiales.
Au lieu d’ajouter systématiquement toutes les variables générées, vous pourriez réaliser des
tests avant de choisir d’ajouter un terme quadratique ou une interaction. Il suffit de faire une
validation croisée pour vérifier si chaque addition apporte vraiment quelque chose à la
puissance de prédiction. Notre exemple constitue une bonne base de départ pour envisager
d’autres moyens de contrôler la complexité actuelle des jeux de données ou celle que vous avez
générée par transformation et création de caractéristiques au cours de votre phase
d’exploration des données. Avant d’aller plus loin, il est bon de vérifier le profil du jeu de
données actuel, ainsi que l’erreur quadratique moyenne de la validation croisée.
L’erreur quadratique moyenne est bonne, mais le ratio entre 506 observations
et 104 caractéristiques n’est pas excellent parce que le nombre d’observations risque de ne
pas suffire pour bien estimer les coefficients :
La plage dans laquelle il est conseillé de chercher la bonne valeur de alpha correspond à
np.logspace(-5,2,8). Bien sûr, si la valeur optimale que vous trouvez se trouve reléguée à
l’une des extrémités de la plage de test, il y a lieu d’agrandir la plage et de relancer le test.
Nous utilisons dans l’exemple de ce chapitre et le suivant les deux variables nommées polyX
et y. Elles ont été créées dans l’exemple de la création d’interaction entre les variables plus
haut dans ce chapitre. Si vous n’avez pas pratiqué cette section antérieure, vous ne pourrez
pas réussir les exemples de la présente section.
Pour préparer la régression Lasso, nous choisissons un algorithme moins sensible (tol=0.05)
et une approche d’optimisation aléatoire (selection=‘random’). Nous obtenons une valeur
d’erreurs carrées moyennes supérieure à celle obtenue avec la régularisation de type L2 :
Application de la régularisation
Vous pouvez profiter d’une procédure de sélection des caractéristiques à partir des coefficients
peu denses issus d’une régression L1. Vous pouvez donc réellement vous servir de la classe
Lasso pour sélectionner les variables les plus importantes. Vous réglez la quantité de variables
avec le paramètre alpha. Dans l’exemple, nous choisissons une valeur de 0.01 pour alpha, ce
qui permet d’obtenir une solution très simplifiée :
Cette sélection de variables par L1 peut être appliquée automatiquement à une régression
comme à une classification au moyen des deux classes Randomized-Lasso et
RandomizedLogisticRegression. Dans les deux cas, le résultat est une série de modèles
régularisés et aléatoires L1. Le code conserve une trace des coefficients produits. Il s’agit de
tous les coefficients qui n’ont pas été forcés à zéro. Vous pouvez entraîner les deux classes au
moyen de la méthode fit(), mais il n’y a pas de méthode predict(). Elles disposent en
revanche d’une méthode transform() qui permet de réduire le jeu de données comme la
plupart des classes du module sklearn.preprocessing.
Combinaison de L1 et de L2 : ElasticNet
La régularisation de type L2 réduit l’impact des caractéristiques corrélées. La régularisation
de type L1 a tendance à sélectionner des caractéristiques. Une bonne stratégie consiste à
combiner les deux types en utilisant une somme pondérée, ce qui correspond à la classe
ElasticNet. Vous contrôlez les effets de L1 et de L2 par le paramètre alpha déjà connu ; pour
contrôler la proportion d’effets de L1 sur le résultat global, vous disposez du paramètre
l1_ratio. S’il est à zéro, vous utilisez en fait une régression Ridge. S’il est à 1, vous avez une
régression Lasso.
SGD = SGDRegressor(loss=’squared_loss’,
penalty=’l2’,
alpha=0.0001,
l1_ratio=0.15,
max_iter=2000,
random_state=1)
scaling = StandardScaler()
scaling.fit(polyX)
scaled_X = scaling.transform(polyX)
cv = cross_val_score(SGD, scaled_X, y,
scoring=’neg_mean_squared_error’,
cv=crossvalidation)
score = abs(np.mean(cv))
print(‘CV MSE: %.3f’ % score)
CV MSE: 12.179
N. d. T. : Rappelons que MSE signifie Mean Squared Error ou erreur carrée moyenne.
Dans ce premier exemple, nous avons utilisé la méthode fit(), ce qui suppose que vous
pouvez charger toutes les données d’entraînement en mémoire. Pour entraîner le modèle en
plusieurs étapes, vous utilisez à la place la méthode par-tial_fit(). Elle ne lance qu’une
itération sur les données que vous fournissez puis les conserve en mémoire pour les ajuster en
recevant de nouvelles données. Le code demande dans ce cas un plus grand nombre
d’itérations :
La trace des améliorations partielles de l’algorithme ayant été conservée pour les
10 000 itérations sur les mêmes données, nous pouvons produire un diagramme pour voir
l’effet de ces améliorations, comme le montre le code suivant. Notez que vous auriez pu utiliser
des données différentes pour chaque étape.
Un beau programme, n’est-il pas ? Il faut également prendre en compte les quelques
inconvénients avant de se jeter sur l’importation du premier module SVM venu :
» SVM donne le meilleur de lui-même avec une classification binaire, qui était l’objectif
initial de cette technique. SVM n’est donc pas aussi bon avec les autres problèmes de
prédictions.
» SVM est moins efficace lorsqu’il y a plus de variables que d’exemples ; il faut dans ce cas
chercher une autre solution, par exemple SGD.
» SVM ne fournit qu’un résultat prédit ; pour obtenir des estimations avec probabilités pour
chaque réponse, il faut prévoir des calculs coûteux en temps.
» Le fonctionnement est correct dès le départ, mais il faut passer du temps à faire des
essais pour bien régler les nombreux paramètres et obtenir les meilleurs résultats.
Lorsque les deux groupes se distinguent l’un de l’autre, vous résolvez le problème en
choisissant une ligne séparatrice à une position choisie. Il faut bien étudier les détails et
mesurer de façon précise. Au départ, l’opération de marquage de la séparation peut sembler
simple, mais il faut imaginer l’effet de l’arrivée de nouvelles données, plus tard. Vous ne
pourrez jamais être certain d’avoir choisi la bonne ligne séparatrice au départ.
Le diagramme de droite dans la Figure 19.5 montre deux solutions possibles, mais il y en a
d’autres. Ces deux solutions sont trop proches des observations (regardez la distance entre les
lignes et les premiers points de données). Rien ne permet de penser que les nouvelles
observations vont apparaître dans les mêmes régions que celles délimitées dans la figure. La
technique SVM limite le risque de mal choisir la ligne séparatrice (comme les lignes A et B de
la Figure 19.6). Elle cherche la solution en trouvant la distance la plus grande entre les points
frontières de chacun des groupes. Il est évident que lorsque vous cherchez l’espace maximal
entre les groupes, vous réduisez les chances de définir une ligne qui va devenir caduque en
tant que frontière !
Figure 19.6 : Une solution SVM valable pour la séparation entre deux groupes ou plus.
La plus grande distance entre deux groupes correspond à la marge. Si elle est suffisamment
grande, vous pouvez avoir de bonnes assurances que l’algorithme fonctionnera bien avec des
données actuellement inconnues. La marge est positionnée à partir des points présents en
limite de marge, points qui correspondent aux vecteurs de supports (la technique SVM tire son
nom de ces points).
Nous voyons une solution SVM dans le diagramme de gauche de la Figure 19.6. La marge est
une ligne tiretée, le séparateur une ligne continue et les vecteurs de support sont les points
cerclés.
Dans les problèmes réels, il n’est pas toujours simple de séparer les classes comme dans cet
exemple. Une machine SVM bien paramétrée peut résoudre certaines ambiguïtés
correspondant à des points mal classés. Un algorithme à SVM bien réglé peut faire des
miracles.
Pendant votre phase de prise en main de la machine SVC avec des exemples, il faut d’abord
chercher des solutions permettant aux points de données de bien expliquer le travail de
l’algorithme, afin de comprendre les concepts qui le régissent. Dans la réalité, vous devrez
vous contenter d’approximations acceptables ; vous observerez rarement des marges
importantes et univoques.
La technique SVM ne se limite pas aux classifications binaires en deux dimensions ; elle sait
traiter les données complexes, autrement dit, dès qu’il y a plus de dimensions ou dans des
situations telles que celles du deuxième diagramme de la Figure 19.6 : il est impossible ici de
séparer les groupes par une ligne droite.
Lorsqu’elle doit traiter de nombreuses variables, la machine SVM peut utiliser un plan de
séparation complexe que l’on appelle un hyperplan. SVM s’en sort également très bien lorsqu’il
n’est pas possible de séparer les classes par une ligne droite ou un plan ; en effet, cette
technique peut explorer des solutions non linéaires dans un espace à plusieurs dimensions
grâce à une technique de calcul correspondant à l’astuce de noyau (kernel trick).
Il faut d’abord compter le nombre d’exemples dans les données. S’il y en a de l’ordre
de 10 000, les calculs vont être longs, mais cela n’empêche pas d’utiliser SVM pour des
problèmes de classification en optant pour la variante sklearn. svm.LinearSVC. Dans le cas
d’un problème de régression, vous remarquerez que LinearSVC n’est pas assez rapide ; dans
ce cas, vous vous tournerez vers une solution stochastique que nous décrivons dans une
section ultérieure.
Le module SVM de la librairie Scikit-learn englobe deux puissantes librairies écrites en
langage C : libsvm et liblinear. Un flux de données est échangé entre Python et les deux
librairies externes pendant l’ajustement du modèle. Les échanges de données sont lissés grâce
à un cache mémoire. Hélas, si le cache est trop petit pour le nombre de points de données à
gérer, il va constituer un goulet d’étranglement ! Si vous avez assez de mémoire, n’hésitez pas
à augmenter la taille de cache qui n’offre que 200 Mo en réglage usine. Vous pouvez aller si
possible jusqu’à 1000 Mo au moyen du paramètre de la classe SVM nommé cache_size. Si la
quantité d’exemples n’est pas importante, vous pouvez vous contenter de choisir entre
classification et régression.
Dans tous les cas, vous avez deux algorithmes. Pour une classification, vous pouvez choisir
entre sklearn.svm.SVC et sklearn.svm.NuSVC. La variante Nu ne se distingue que par ses
paramètres d’entrée et un algorithme légèrement différent. Vous obtenez quasiment les mêmes
résultats. En général, vous choisirez donc la version non-Nu.
Une fois l’algorithme exact choisi, vous allez vous intéresser aux paramètres, et notamment au
paramètre C qui stipule à quel point l’algorithme doit s’adapter aux points de données de
l’entraînement. Lorsque la valeur de C est faible, SVM s’adapte moins en cherchant à
emprunter une voie moyenne, car il n’utilise que quelques points et variables parmi ceux
disponibles. Avec une valeur importante pour C, le processus d’apprentissage est forcé de
travailler plus sur les points disponibles et prendre en compte de nombreuses variables.
En général, vous choisirez une valeur intermédiaire pour C après avoir fait quelques essais. Si
C est trop important, vous risquez un sur-ajustement, résultat d’une profonde adaptation du
SVM à vos données, qui provoquera une incapacité partielle à gérer les nouveaux problèmes.
Avec une valeur de C trop faible, vous aurez des prédictions imprécises, ce qui correspond à
un sous-ajustement : le modèle est trop simple pour le problème qu’il doit résoudre.
Après le paramètre C, vous vous intéresserez aux trois paramètres kernel, degree et gamma
qui sont interdépendants et dont la valeur dépend de la spécification de kernel. Par exemple,
un kernel valant linear n’a pas besoin des paramètres degree et gamma. La spécification du
paramètre kernel sert à décider si le modèle SVM va utiliser une ligne ou une courbe pour
deviner la mesure d’une classe ou d’un point. Puisque les modèles linéaires sont plus simples
et réussissent bien avec les nouvelles données, ils semblent intéressants, mais hélas, ils sont
moins efficaces dès que les variables données sont interconnectées de façon complexe. Vous
avez intérêt à commencer avec un noyau linéaire parce que vous ne savez pas d’avance si un
modèle linéaire va fonctionner avec votre projet. Vous réglez ensuite le paramètre C, puis vous
vous servez du modèle et des performances constatées que vous stockez dans un témoin pour
pouvoir tester ensuite des solutions non linéaires.
Après chargement des jeux de données, nous demandons l’importation des données avec
load.digits, ce qui permet d’en extraire les prédicteurs (digits. data) pour les stocker dans
X et les classes prédites (digits.target) dans y.
Pour jeter un œil sur le contenu du jeu de données, vous pouvez vous servir des deux fonctions
de matplotlib subplot(), qui crée un tableau de dessins sur deux lignes de cinq colonnes et
imshow(), qui crée les valeurs des pixels en niveaux sur une grille de 8 sur 8. Le code source
organise les informations dans digits. images sous forme d’une série de matrices, chacune
contenant les pixels d’un chiffre.
En observant ces données, vous pouvez en déduire que la technique SVM va pouvoir deviner
chaque chiffre en associant des probabilités avec les valeurs des pixels individuels dans chaque
grille. Le chiffre 2 n’offre pas les mêmes pixels noirs que le chiffre 1, ni même les mêmes
groupes de pixels. Il est de règle en datalogie d’essayer plusieurs approches et algorithmes
avant d’aboutir à un résultat acceptable. Il n’est jamais inutile de se montrer imaginatif et
intuitif pour tenter de déterminer au plus tôt quelle sera l’approche la plus fructueuse. Ici, si
vous explorez X, vous constatez qu’il y a exactement 64 variables, chacune indiquant le niveau
de grille d’un pixel. Au total, vous avez un grand nombre d’exemples, exactement 1797.
print(X[0])
Si vous reformatez l’affichage du même vecteur en tant que matrice de huit lignes sur huit
colonnes, vous pouvez même apercevoir la silhouette du chiffre zéro :
print(X[0].reshape(8, 8))
Les valeurs à zéro correspondent aux blancs et les valeurs supérieures à des nuances de gris :
[[ 0. 0. 5. 13. 9. 1. 0. 0.]
[ 0. 0. 13. 15. 10. 15. 5. 0.]
[ 0. 3. 15. 2. 0. 11. 8. 0.]
[ 0. 4. 12. 0. 0. 8. 8. 0.]
[ 0. 5. 8. 0. 0. 9. 8. 0.]
[ 0. 4. 11. 0. 1. 12. 7. 0.]
[ 0. 2. 14. 5. 10. 12. 0. 0.]
[ 0. 0. 6. 13. 10. 0. 0. 0.]]
Arrivé en ce point, vous vous demandez peut-être quoi faire au niveau des labels ou étiquettes.
Le paquetage NumPy propose à cet effet une fonction nommée unique() qui permet de
compter ces labels :
np.unique(y, return_counts=True)
Le résultat associe le label de classe qui est le premier nombre à sa fréquence, qu’il est
intéressant d’observer dans la deuxième ligne :
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
array([178, 182, 177, 183, 181, 182, 181, 179, 174, 180],
dtype=int64))
Quasiment tous les labels correspondent au même nombre d’exemples à peu près. Vous pouvez
en déduire que les classes sont équilibrées ; la machine SVM ne va pas être influencée à croire
qu’une classe est plus probable que les autres.
S’il y avait un nombre de cas vraiment différents pour une ou plusieurs classes, vous seriez
face à un problème de déséquilibre, qui vous obligerait à réaliser le genre d’évaluation
suivante :
» conservation de la place déséquilibrée et recherche de prédiction biaisée en faveur des
classes les plus fréquentes ;
» rééquilibrage des classes en utilisant des pondérations, autrement dit, en donnant plus
de poids à certaines observations ;
» sélection pour éliminer certains cas dans les classes qui sont surpeuplées.
Une fois les données réparties entre entraînement et test, il est conseillé de rééchelonner les
valeurs numériques, tout d’abord en récupérant les paramètres d’échelle auprès du jeu
d’entraînement puis en les appliquant via une transformation aux deux jeux d’entraînement et
de test.
Une dernière action de préparation à réaliser avant d’injecter les données dans la SVM est le
rééchelonnement qui va transformer toutes les valeurs pour qu’elles tiennent dans la plage
entre –1 et +1 (ou entre 0 et +1, si vous préférez). Cela vous évite de voir certaines variables
influencer l’algorithme de façon démesurée en lui faisant croire qu’elles sont réellement
prépondérantes simplement parce qu’elles ont de grandes valeurs. Les calculs seront ainsi
exacts, réguliers et rapides.
Le code source qui suit adapte les données d’entraînement à une classe SVC utilisant un noyau
linéaire. Nous effectuons une validation croisée puis testons les résultats au niveau de leur
précision (le pourcentage de chiffres correctement devinés) :
Nous demandons à la machine SVC d’utiliser le noyau linear et de rééquilibrer les classes
automatiquement. Cette opération garantit qu’elles auront la même taille une fois que le jeu de
données aura été réparti en jeu d’entraînement et en jeu de test.
Nous déclarons ensuite deux variables. Les performances de la validation croisée vont être
mémorisées par la fonction cross_val_score() qui renvoie une liste de 10 scores après la
validation croisée à 10 plis (cv=10). Nous obtenons le résultat du test en appelant en séquence
deux méthodes sur l’algorithme d’apprentissage. Tout d’abord fit() pour ajuster le modèle,
puis score() pour évaluer le résultat sur le jeu de test en appliquant une précision moyenne,
c’est-à-dire un pourcentage moyen de résultats corrects parmi toutes les classes prédites.
Il ne reste plus qu’à afficher les valeurs des deux variables et à évaluer le résultat. Il est assez
bon, puisque nous atteignons 97,4 % de prédictions correctes sur le jeu de test :
gridsearch = GridSearchCV(svc,
param_grid=search_space,
scoring=’accuracy’,
refit=True, cv=10)
gridsearch.fit(X_tr,y_tr)
La recherche GridSearchCV est un peu plus difficile à utiliser, mais elle permet de vérifier de
nombreux modèles en séquence. Il y a lieu de définir d’abord une variable pour l’espace de
recherche en utilisant un dictionnaire Python qui contiendra le planning d’exploration de la
procédure. Pour définir cet espace, vous créez le dictionnaire, ou s’il y en a plus d’un, une liste
de dictionnaire, pour chacun des groupes de paramètres à tester. Dans ce dictionnaire, les clés
correspondent aux noms des paramètres et les valeurs à une liste ou à une fonction qui génère
la liste contenant les valeurs que vous voulez tester :
La fonction de NumPy nommée logspace() génère une liste de sept valeurs pour C,
entre 10^–3 et 10^3. Le nombre de valeurs à tester devient considérable, mais le test est
exhaustif. En utilisant une telle plage, vous êtes certain que lorsque vous testez C et les autres
paramètres de SVM, vous avez testé toutes les possibilités.
Nous initialisons GridSearchCV en choisissant l’algorithme d’apprentissage, l’espace de
recherche, la fonction de notation et le nombre de plis de la validation croisée. Nous
demandons ensuite à la procédure, une fois la meilleure solution trouvée, d’ajuster la meilleure
combinaison de paramètres, afin que vous obteniez un modèle prédictif prêt à l’emploi :
cv = gridsearch.best_score_
test_score = gridsearch.score(X_t, y_t)
best_c = gridsearch.best_params_[‘C’]
Nous procédons à l’extraction des données de validation croisée et des scores de test puis nous
affichons la valeur de C correspondant au meilleur score :
Le choix est vaste, mais vous utiliserez en général le noyau à fonction rbf. Il est plus rapide que
les autres et permet de trouver une approximation de quasiment toutes les fonctions non
linéaires.
Voici en quelques mots comment fonctionne un noyau rbf : il distribue les données en de très
nombreux groupes, ce qui lui permet plus facilement d’associer une réponse à chaque groupe.
Pour utiliser le noyau rbf, vous devez régler les paramètres degree et gamma en plus de C. Ils
sont faciles à paramétrer et une bonne recherche systématique trouvera toujours les valeurs
adéquates.
Le paramètre degree accepte une valeur à partir de deux. Elle sert à déterminer la complexité
de la fonction non linéaire qui va séparer les différents points. En pratique, vous pouvez
utiliser les valeurs 2, 3 ou 4 pour le paramètre degree dans une recherche systématique. Si
vous remarquez que votre meilleur résultat correspond à 4, vous pouvez tâter le terrain un peu
plus haut, en testant les valeurs 3, 4 et 5. Continuez à grimper, en sachant que vous irez
rarement plus haut que 5.
Le rôle du paramètre gamma dans notre algorithme est proche de celui de C en réalisant un
arbitrage entre sur- et sous-ajustement. Il n’existe que pour le noyau rbf. Une grande valeur
de gamma demande à l’algorithme de créer des fonctions non linéaires avec des profils
irréguliers, ce qui leur permet de mieux épouser les données. Une valeur faible génère des
fonctions plus régulières, quasiment sphériques, qui ignorent la plupart des irrégularités dans
les données.
Puisque vous connaissez maintenant les détails de cette approche non linéaire, nous pouvons
essayer rbf en conservant l’exemple précédent. Sachez dès maintenant que le traitement
risque d’être assez long (songez au grand nombre de combinaisons à tester). Tout dépend au
fond des performances de votre machine :
La différence essentielle par rapport au script précédent est un espace de recherche plus
sophistiqué. En utilisant une liste, nous exploitons deux dictionnaires : un contenant les
paramètres à tester pour le noyau linéaire et un autre pour le noyau rbf. C’est ce qui nous
permet de comparer les performances des deux approches. Soyez patient pendant l’exécution
du code. Voici ce que vous allez enfin obtenir :
Les résultats montrent clairement que rbf est meilleur. La marge est cependant réduite par
rapport à un modèle linéaire, quand on pense au supplément de complexité et de temps de
traitement. Le meilleur modèle serait plus facile à déterminer en travaillant sur un plus grand
volume de données. Mais obtenir plus de données est coûteux en temps et en argent. Lorsque
vous n’avez pas vraiment de modèle qui se distingue, choisissez toujours le plus simple. Ici, le
noyau linéaire est évidemment plus simple que le noyau rbf.
boston = datasets.load_boston()
X,y = boston.data, boston.target
X_tr, X_t, y_tr, y_t = train_test_split(X, y,
test_size=0.3,
random_state=0)
scaling = MinMaxScaler(feature_range=(-1, 1)).fit(X_tr)
X_tr = scaling.transform(X_tr)
X_t = scaling.transform(X_t)
Notre objectif est d’estimer la valeur médiane des biens occupés. Nous allons essayer de
l’inférer au moyen d’une régression SVR (à vecteurs de support epsilon). Cette machine utilise
les paramètres C, kernel, degree et gamma déjà connus auxquels elle ajoute epsilon. Cette
mesure permet de connaître le taux d’erreur considéré comme acceptable par l’algorithme.
Une valeur forte pour epsilon implique un nombre de points de support faible alors qu’une
petite valeur pour epsilon nécessite un grand nombre de points de support. Autrement dit,
epsilon constitue un autre paramètre participant à l’arbitrage entre sur- et sous-ajustement.
L’expérience montre que pour l’espace de recherche de ce paramètre, nous pouvons nous
baser sur la séquence [0, 0.01, 0.1, 0.5, 1, 2, 4]. Vous commencez par la valeur 0 dans
laquelle l’algorithme n’accepte aucune erreur pour aller au maximum jusqu’à 4. Vous irez plus
loin dans l’espace de recherche seulement si vous constatez qu’une valeur d’epsilon élevée
apporte encore de meilleures performances.
Une fois l’espace de recherche de epsilon défini et SVR choisi comme algorithme, il ne reste
plus qu’à terminer le script. Notez qu’une fois de plus, les calculs peuvent prendre un certain
temps, car il y a beaucoup de combinaisons à évaluer.
svr = SVR()
search_space = [{‘kernel’: [‘linear’],
‘C’: np.logspace(-3, 2, 6),
‘epsilon’: [0, 0.01, 0.1, 0.5, 1, 2, 4]},
{‘kernel’: [‘rbf’],
‘degree’:[2,3],
‘C’:np.logspace(-3, 3, 7),
‘gamma’: np.logspace(-3, 2, 6),
‘epsilon’: [0, 0.01, 0.1, 0.5, 1, 2, 4]}]
gridsearch = GridSearchCV(svr,
param_grid=search_space,
refit=True,
scoring= ‘r2’,
cv=10, n_jobs=-1)
gridsearch.fit(X_tr, y_tr)
cv = gridsearch.best_score_
test_score = gridsearch.score(X_t, y_t)
print(‘Score R2 CV : %0.3f’ % cv)
print(‘Score R2 Test: %0.3f’ % test_score)
print(‘Meilleurs paramètres: %s’ % gridsearch.best_params_)
La recherche systématique (grid search) peut notamment prendre beaucoup de temps, même
si comme dans l’exemple nous utilisons toute la puissance disponible avec le paramètre
n_jobs=-1. Pour chaque noyau, le nombre de modèles à produire consiste à multiplier par les
combinaisons de valeurs des paramètres. Pour un noyau rbf, nous avons deux valeurs pour
degree, sept pour C, six pour gamma et sept pour epsilon, ce qui donne 588 modèles et chacun
doit être répliqué dix fois parce que cv=10. Il y a donc 5 880 modèles uniquement pour le
noyau rbf. Le code doit aussi tester le modèle linéaire, qui réclame 420 tests. Vous finissez
tout de même par obtenir les résultats :
Score R2 CV : 0.868
Score R2 Test: 0.834
Meilleurs paramètres: {‘C’: 1000.0, ‘degree’: 2, ‘epsilon’: 2, ‘gamma’: 0.1,
‘kernel’: ‘rbf’}
Précisons que l’erreur est calculée en utilisant R au carré, ce qui donne une mesure
entre 0 et 1 pour indiquer les performances du modèle, la valeur 1 étant la meilleure possible.
Les trois paramètres loss, penalty et dual sont liés par des contraintes réciproques.
Consultez le Tableau 19.2 pour décider quelle combinaison vous voulez utiliser.
Notez que l’algorithme n’accepte pas la combinaison des valeurs penalty=’l1’ et loss=’l1’.
En revanche, vous simulez parfaitement l’approche d’optimisation SVC avec la combinaison
penalty=’l2’ et loss=’l1’.
La machine LinearSVC est donc assez rapide, ce que prouve un test en comparant avec SVC :
svc.fit(X_tr, y_tr)
linear.fit(X_tr, y_tr)
svc_score = svc.score(X_t, y_t)
libsvc_score = linear.score(X_t, y_t)
print(‘Score de précision de test SVC: %0.3f’ % svc_score)
print(‘Précision de test LinearSVC: %0.3f’ % libsvc_score)
import timeit
X,y = make_classification(n_samples=10**4,
n_features=15,
n_informative=10,
random_state=101)
t_svc = timeit.timeit(‘svc.fit(X, y)’,
‘from __main__ import svc, X, y’,
number=1)
t_libsvc = timeit.timeit(‘linear.fit(X, y)’,
‘from __main__ import linear, X, y’,
number=1)
print(‘Meilleur temps moyen (sec.) SVC: %0.1f’ % np.mean(t_svc))
print(‘Meilleur temps moyen (sec.) LinearSVC: %0.1f’ % np.mean(t_libsvc))
Le système servant d’exemple produit les résultats suivants, résultats pouvant légèrement
diverger sur votre équipement :
Avec le même volume à traiter, LinearSVC est clairement plus rapide que SVC. Pour être
précis, 11.3 / 0.2 = 55 fois plus rapide. Voyons ce qui se passe lorsque l’on augmente le
volume, par exemple en le triplant :
Nous constatons que le temps requis par SVC augmente plus vite (9,3 fois) que celui requis par
LinearSVC (8 fois). En effet, SVC va demander de plus en plus de temps, et les choses vont
empirer au fur et à mesure de l’augmentation de volume. Voici les résultats avec cinq fois plus
de données :
L’algorithme SVC devient rapidement inutilisable. Vous choisirez donc LinearSVC dès que vous
avez de gros volumes à traiter. Mais lorsque vous avez des millions d’exemples à classifier ou à
régresser, il reste à vérifier que LinearSVC reste dans la course. Nous avons vu comment la
classe SGD, avec SGDClassifier et SGDRegressor, permettait d’utiliser un algorithme du type
SVM lorsqu’il y avait des millions de lignes de données, sans trop mettre à genoux la machine.
Pour SGDClassifier, il suffit de régler le paramètre loss à la valeur ‘hinge’. Pour
SGDRegressor, il faut régler le même paramètre à ‘epsilon_insensi-tive’ et paramétrer le
paramètre epsilon lui-même.
Réalisons un autre test de performances pour voir les avantages et inconvénients des deux
approches LinearSVC et SGDClassifier :
Nous avons environ 100 000 cas à traiter. Si votre machine dispose d’assez de mémoire et si
vous avez la patience, vous pouvez encore augmenter le nombre de cas de l’entraînement ou le
nombre de caractéristiques afin de tester les deux algorithmes de façon très intensive.
linear = LinearSVC(penalty=’l2’,
loss=’hinge’,
dual=True,
random_state=1)
linear.fit(X_tr, y_tr)
score = linear.score(X_t, y_t)
t = timeit.timeit(«linear.fit(X_tr, y_tr)»,
“from __main__ import linear, X_tr, y_tr”,
number=1)
print(‘Précision test LinearSVC: %0.3f’ % score)
print(‘Temps moyen LinearSVC: %0.1f secs’ % np.mean(t))
Sur notre machine de test, LinearSVC a fait son travail pour toutes les lignes en un peu plus
de quatre secondes :
sgd = SGDClassifier(loss=’hinge’,
max_iter=100,
shuffle=True,
random_state=101)
sgd.fit(X_tr, y_tr)
score = sgd.score(X_t, y_t)
t = timeit.timeit(«sgd.fit(X_tr, y_tr)»,
“from __main__ import sgd, X_tr, y_tr”,
number=1)
Nous constatons que SGDClassifier n’a eu besoin que d’environ une seconde pour traiter les
mêmes données et fournir le même score en sortie :
Une présentation approfondie des réseaux neuronaux n’entre pas dans le cadre de ce livre ;
cela dit, nous allons présenter une implémentation simple, disponible dans la librairie Scikit-
learn. Nous allons ainsi créer rapidement un réseau neuronal pour pouvoir le comparer à
d’autres algorithmes de mécapprentissage.
Les deux fonctions ont exactement les mêmes paramètres. Nous pouvons donc n’étudier qu’un
exemple pour la classification. Nous repartons du jeu de données des chiffres manuscrits
auxquels nous allons appliquer une classification MLP. Nous commençons comme d’habitude
par importer les paquetages, charger le jeu de données en mémoire puis le répartir en un jeu
d’entraînement et un jeu de test (de la même façon que pour les machines à vecteurs de
support SVM) :
digits = datasets.load_digits()
X, y = digits.data, digits.target
X_tr, X_t, y_tr, y_t = train_test_split(X, y,
test_size=0.3,
random_state=0)
Il est indispensable de bien prétraiter les données en entrée du réseau neuronal car les
réseaux neuronaux sont sensibles aux problèmes d’échelle et à la distribution des données. Il
est donc conseillé de d’abord normaliser les données en forçant la moyenne à zéro et la
variance à un ou à redimensionner celle-ci en choisissant comme minimum et maximum les
valeurs –1 et +1 ou 0 et +1. Vous verrez quelle transformation fonctionne le mieux par
expérimentation. L’expérience acquise montre que le redimensionnement pour la plage –1 à
+1 semble la meilleure. C’est ce que nous réalisons dans notre exemple :
Nous avons déjà dit qu’il était conseillé de définir la transformation du prétraitement
seulement sur les données d’entraînement, puis d’appliquer la procédure ainsi apprise aux
données de test. C’est le seul moyen de tester correctement à quel point le modèle réagira bien
à des données différentes.
La configuration de votre perceptron MLP requiert le réglage de quelques paramètres. Si vous
négligez d’y consacrer le soin approprié, vous serez déçu par les résultats. En effet, MLP n’est
pas un algorithme prêt à l’emploi dès le départ. Vous devez d’abord définir l’architecture des
neurones, choisir combien il en faut dans chaque couche et combien il faut de couches. Le
nombre de neurones dans chaque couche est défini avec le paramètre hidden_layer_sizes. Il
reste ensuite à choisir le bon solveur :
» L-BFGS : destiné aux petits jeux de données ;
» Adam : destiné aux grands jeux de données ;
» SGD (Descente de Gradient Stochastique, DGS) : résout la plupart des problèmes si
certains paramètres spéciaux sont bien configurés. Il s’agit notamment du taux
d’apprentissage qui correspond à la vitesse à laquelle le système apprend et au « moment
de Nesterov », valeurs qui aident le réseau à éviter d’utiliser les solutions les moins
intéressantes. Pour le taux d’apprentissage, vous devez choisir la valeur initiale
learning_rate_init qui est en général de 0.001 (mais peut encore être inférieure) et la
vitesse de changement pendant l’entraînement (paramètre learning_rate qui peut
correspondre à une des trois valeurs ‘constant’, ‘invscaling’ ou ‘adaptive’).
Le paramétrage fin d’un solveur SGD est assez complexe. Vous ne pouvez évaluer l’impact des
paramètres sur les données qu’en les testant dans le cadre d’une optimisation des
hyperparamètres. La plupart des gens préfèrent donc démarrer avec un solveur L-BFGS ou
Adam.
Un autre hyperparamètre critique correspond à max_iter qui règle le nombre d’itérations et
peut entraîner des résultats très différents si la valeur est trop élevée ou trop faible. Par
défaut, elle est égale à 200, mais il est toujours préférable d’augmenter ou de diminuer la
valeur après avoir fixé les autres paramètres. Vous devez enfin demander un rebattage ou
mélange des données avec shuffle=True et choisir un état aléatoire initial avec random_state
pour garantir que les résultats pourront être reproduits. Notre exemple choisit de travailler
avec 512 nœuds dans une seule couche, d’utiliser le solveur Adam et le nombre d’itérations
standard de 200 :
nn = MLPClassifier(hidden_layer_sizes=(512, ),
activation=’relu’,
solver=’adam’,
shuffle=True,
tol=1e-4,
random_state=1)
Cet exemple réussit bien à classifier les chiffres manuscrits à partir d’un perceptron MLP, en
arborant des scores CV et de test corrects :
Ces résultats sont un peu meilleurs que ceux d’une machine SVC, mais cela suppose un
paramétrage fin de quelques valeurs. Ne vous attendez jamais à une approche prête à l’emploi
avec un algorithme non linéaire, sauf à adopter une des solutions basées sur un arbre de
décision, ce qui est justement le sujet du prochain chapitre
Chapitre 20
Plus forts à plusieurs
DANS CE CHAPITRE :
» Principe des arbres de décisions
Ce chapitre est l’occasion d’aller plus loin que les modèles d’apprentissage individuels qui nous
ont occupés jusqu’ici. Nous allons découvrir la puissance des ensembles, des groupes de
modèles laissant présager des performances bien supérieures. Un ensemble apporte une
intelligence collective en partageant des informations pour générer de meilleures prédictions.
L’idée de base est qu’un groupe d’algorithmes peu performants parvient à produire de
meilleurs résultats qu’un seul modèle très bien entraîné.
Il vous est peut-être arrivé de participer à un jeu dans lequel il fallait deviner le nombre de
billes dans un vase. Chaque participant a peu de chances de trouver le bon nombre, mais des
expérimentations ont montré qu’en collectant un grand nombre de fausses réponses puis en
produisant leur moyenne, on parvient à s’approcher de la bonne réponse ! Cette sagesse des
foules s’explique par le fait que les mauvaises réponses tendent à se répartir autour de la
bonne. Voilà pourquoi vous parvenez presque à la bonne réponse en faisant la moyenne des
mauvaises réponses.
Lorsque vous devez faire des prédictions complexes dans un projet de datalogie, vous pouvez
tirer profit des compétences diverses de plusieurs algorithmes d’apprentissage pour obtenir
des prédictions plus précises qu’avec un seul. Nous allons dans ce chapitre mettre en place un
processus qui va permettre de tirer avantage de plusieurs algorithmes pour obtenir de
meilleures réponses.
Le fichier calepin du code source de ce chapitre correspond à PYDASC_20.
Ces statistiques permettent de construire facilement un arbre tel que celui qui est illustré dans
la Figure 20.1. Pour obtenir ce genre de graphique ainsi que celui du jeu de données Iris un
peu plus loin dans ce chapitre, nous avons profité du paquetage nommé dtreeviz créé par le
professeur Terence Parr et Prince Grover de l’université de San Francisco
(https://parrt.cs.usfca.edu). Si vous avez besoin de créer des visualisations à partir de vos
arbres de décision, récupérez le paquetage et les conseils d’installation à l’adresse
https://github.com/parrt/dtreeviz. Des explications concernant la création et l’utilisation
du paquetage sont disponibles sur le blog du professeur Parr
(https://explained.ai/decision-tree-viz).
Dans la figure, l’arbre est inversé avec la racine en haut et les branches qui en descendent.
Tout en haut est représenté le jeu de données complet. La première fourche utilise le sexe
comme critère pour créer deux branches dont une donne directement sur une feuille (un
segment terminal). Les cas dans les feuilles sont classifiés selon la classe la plus fréquente, ou
par un calcul de la probabilité élémentaire des cas ayant les mêmes caractéristiques. Cela
donne la probabilité dans la feuille. L’autre branche est subdivisée par âge.
Procédons à une lecture des nœuds de l’arbre. Le premier nœud rend compte de la règle qui
régit la première fourche et mène à tous les autres nœuds. L’arbre de la Figure 20.1 considère
que le sexe est le meilleur prédicteur. Dans le nœud du haut, la variable is_female est
représentée sous forme de barres verticales. La barre de gauche correspond au sexe masculin
et celle de droite au sexe féminin. Vous constatez à première vue que les femmes ont un taux
de survie supérieur (la zone grisée dans la version imprimée ou vert clair sur écran).
Au deuxième niveau, les hommes sont séparés des femmes. Du côté droit du deuxième niveau,
nous trouvons un nœud qui n’est peuplé que par des femmes avec un histogramme qui montre
que quasiment toutes les femmes de première et de deuxième classe ont survécu et qu’environ
la moitié de celles de la troisième classe ont péri. Nous pouvons en déduire une première
règle : les femmes des première et deuxième classes peuvent être classifiées comme
survivantes parce que la probabilité est forte pour elles. Ce n’est pas le cas pour celles de la
troisième classe. Pour en savoir plus à ce niveau, il faudrait créer un nouvel embranchement
pour extraire d’autres détails.
Du côté des hommes, le deuxième niveau montre que c’est l’âge qui est le critère parce que
quasiment tous ceux de sexe masculin ayant moins de 10 ans environ ont survécu, alors que
ceux plus âgés ont presque tous péri. L’arbre s’arrête à ce niveau, mais nous pourrions
exploiter d’autres critères pour connaître plus en détail les règles de partitionnement et la
probabilité de survie en fonction d’autres caractéristiques. En descendant dans l’arbre, nous
confirmons que la plupart des survivants étaient des femmes avec leurs enfants, ce qui
coïncide avec la règle en cas de naufrage « Les femmes et les enfants d’abord ! » . Cette
répartition correspond bien à la situation du Titanic dans laquelle il n’y avait pas assez de
canots de sauvetage (parce que l’armateur croyait son navire insubmersible). Vous en saurez
plus sur ce drame lié aux canots de sauvetage insuffisants en cherchant sur le Web
(https://www.historyonthenet.com/the-titanic-lifeboats/).
Dans cet arbre, chaque fourche est binaire, mais il est possible d’utiliser des fourches à
plusieurs branches, si l’algorithme le permet. Dans le paquetage Scikit-learn, les deux classes
DecisionTreeClassifier et DecisionTreeRegressor dans le module sklearn.tree sont
toutes deux des arbres binaires. Voici dans quelles conditions un arbre de décision arrête de
segmenter les données :
» Il n’y a plus de cas à distribuer : toutes les données sont devenues des feuilles de l’arbre.
» La règle de segmentation utilise un nombre de cas inférieur à un plancher prédéfini. Cette
précaution permet d’empêcher l’algorithme de travailler avec des feuilles qui sont peu
représentatives ou trop spécifiques par rapport aux données analysées. Vous évitez ainsi
un sur-ajustement et une variance dans les estimations (voir aussi le Chapitre 18).
» Une des feuilles obtenues possède un nombre de cas inférieur à un plancher, ce qui
constitue une autre précaution pour empêcher la production de règles générales à partir
d’un échantillon trop peu peuplé.
Les arbres de décision ont naturellement tendance à souffrir de sur-ajustement. Vous pouvez
limiter les effets de la variance des estimations en choisissant bien le nombre de segments et
de feuilles terminales. Il est en général conseillé de se limiter à 30 cas (mais cela dépend de la
taille de l’échantillon de départ).
Les arbres de décision sont intuitifs, faciles à comprendre et à visualiser (tout dépendant
encore du nombre de branches et de feuilles). Ils offrent un autre avantage aux datalogues :
aucun traitement ni transformation des données n’est nécessaire, parce que les arbres savent
modéliser les non-linéarités en faisant des approximations. Ils acceptent même tous les types
de variables, même les variables catégorielles (une fois encodées avec des codes pour les
différentes classes). Enfin, les arbres de décision savent gérer les données absentes. Il suffit
d’affecter aux cas absents une valeur inhabituelle, très grande ou négative (selon la façon dont
les cas absents sont distribués). Les arbres de décision sont également peu impactés par les
données aberrantes.
Nous chargeons les données dans X, qui contient les prédicteurs, et dans y, qui contient les
classifications. Nous demandons alors une validation croisée pour vérifier les résultats au
moyen d’arbres de décision :
import numpy as np
from sklearn import tree
for depth in range(1,10):
tree_classifier = tree.DecisionTreeClassifier(
max_depth=depth, random_state=0)
if tree_classifier.fit(X,y).tree_.max_depth < depth:
break
score = np.mean(cross_val_score(tree_classifier,
X, y,
scoring=’accuracy’,
cv=crossvalidation))
print(‘Profondeur (depth): %i Précision (accuracy): %.3f’ % (depth,score))
Le code va descendre dans les niveaux de profondeur de l’arborescence jusqu’à ce que l’arbre
ne puisse plus grandir. Le code va alors indiquer le score de validation croisée en termes de
précision :
Nous constatons qu’une profondeur de 4 est idéale parce que l’arbre va commencer à sur-
ajuster si nous continuons à descendre. La Figure 20.2 montre à quel point l’arbre résultant
est devenu complexe. C’est une autre illustration de l’avantage du paquetage de visualisation
dtreeviz. Nous voyons immédiatement que l’espèce Setosa se distingue des deux autres. Pour
distinguer entre Versicolor et Virginica, il a fallu segmenter plus en fonction de la largeur et de
la longueur des pétales.
Figure 20.2 : Arbre de classification du jeu de données Iris sur quatre niveaux de profondeur.
Pour obtenir une bonne réduction, donc une simplification, vous donnez au paramètre
min_samples_split la valeur 30. Pour vous épargner les feuilles terminales trop petites, vous
donnez à min_samples_leaf la valeur 10. Les petites feuilles sont ainsi supprimées de l’arbre
résultant, ce qui diminue un peu la précision de la validation croisée mais augmente la
simplicité et la capacité de généralisation de la solution.
tree_classifier = tree.DecisionTreeClassifier(
min_samples_split=30, min_samples_leaf=10,
random_state=0)
tree_classifier.fit(X,y)
score = np.mean(cross_val_score(tree_classifier, X, y,
scoring=’accuracy’,
cv=crossvalidation))
print(‘Précision: %.3f’ % score)
Précision: 0.913
L’erreur carrée absolue de validation croisée pour l’immobilier à Boston vaut alors :
Voici la précision de validation croisée pour un ensachage appliqué aux chiffres manuscrits :
Précision: 0.964
Aussi bien pour l’ensachage que pour la forêt aléatoire, plus il y a de modèles dans l’ensemble,
mieux c’est. Vous ne craignez pas de sur-ajustement parce que chacun des modèles diffère des
autres et les erreurs vont se répartir autour de la valeur réelle. Plus de modèles signifie plus de
stabilité du résultat.
Un autre aspect de cet algorithme est qu’il permet d’estimer l’importance des variables, en
prenant en compte la présence de tous les autres prédicteurs. Vous pouvez ainsi repérer les
caractéristiques importantes dans la prédiction de la cible, compte-tenu de la palette de
caractéristiques disponibles. Vous pouvez vous servir de cette estimation comme règle pour
sélectionner les variables.
À la différence des arbres de décisions isolés, vous ne pouvez pas facilement visualiser ni
décortiquer le fonctionnement d’une forêt aléatoire qui ressemble plutôt à une boîte noire.
Vous ne connaissez que les entrées et les sorties. En raison de cette opacité, le seul moyen de
comprendre comment l’algorithme travaille par rapport aux caractéristiques est cette
estimation de l’importance des variables.
Vous obtenez l’estimation d’importance dans une forêt aléatoire d’une façon très simple : après
avoir construit chacun des arbres, le code charge dans chacune des variables des données
factices, puis évalue à quel point le pouvoir de prédiction décroît. Si la variable est très
influente, cette opération a un effet négatif sensible sur la prédiction. Si l’opération ne change
quasiment rien au résultat, c’est que la variable n’est pas influente.
Classifieurs à forêt aléatoire
Nous conservons notre jeu de chiffres manuscrits pour tester un classifieur à forêt aléatoire :
X, y = digit.data, digit.target
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold
crossvalidation = KFold(n_splits=5, shuffle=True,
random_state=1)
RF_cls = RandomForestClassifier(n_estimators=300,
random_state=1)
score = np.mean(cross_val_score(RF_cls, X, y,
scoring=’accuracy’,
cv=crossvalidation))
print(‘Précision: %.3f’ % score)
Précision: 0.977
Dans la plupart de vos projets, il vous suffira de bien choisir le nombre d’estimateurs, en visant
la plus grande valeur possible compte tenu des possibilités de traitement de la machine.
Voyons cela en calculant puis en visualisant une courbe de validation pour l’algorithme :
La Figure 20.3 montre les résultats de ce traitement. Plus nous avons d’estimateurs, meilleurs
sont les résultats. Vous remarquez qu’à partir d’un certain point, le gain devient vraiment
minimal.
Figure 20.3 : Vérification de l’impact du nombre d’estimateurs dans une forêt aléatoire.
X, y = boston.data, boston.target
from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import KFold
RF_rg = RandomForestRegressor (n_estimators=300,
random_state=1)
crossvalidation = KFold(n_splits=5, shuffle=True,
random_state=1)
score = np.mean(cross_val_score(RF_rg, X, y,
scoring=’neg_mean_squared_error’,
cv=crossvalidation))
print(‘Erreur carrée moyenne: %.3f’ % abs(score))
Voici les erreurs carrées moyennes de validation croisée que nous obtenons :
L’approche de forêt aléatoire se base sur des arbres de décision. Un arbre de décision
segmente le jeu de données en petits segments correspondant à des feuilles, et ceci pour
estimer les valeurs de régression. La forêt aléatoire exploite la moyenne des valeurs de chaque
feuille pour produire une prédiction. Cette technique fait disparaître des prédictions. Les
valeurs extrêmes ou très élevées disparaissent suite à la création de la moyenne dans chaque
feuille. Le résultat est une série de valeurs amorties à la place des valeurs très élevées ou très
basses.
Nous utilisons toujours le jeu de chiffres manuscrits et démarrons avec un classifieur par
défaut. Nous optimisons max_features et min_samples_leaf. Pour max_features, nous allons
sélectionner des options préconfigurées (auto pour avoir toutes les caractéristiques, sqrt ou
log2 pour appliquer une fonction au nombre de caractéristiques). Nous procéderons à une
réintégration avec un petit nombre de caractéristiques en utilisant un tiers de celles-ci. En
limitant le nombre de caractéristiques échantillonnées, nous réduisons le nombre
d’occurrences de sélection simultanée de deux variables corrélées similaires, ce qui améliore
les performances de prédiction.
Une raison statistique invite à optimiser min_samples_leaf. En laissant apparaître des feuilles
avec peu de cas, vous favorisez un sur-ajustement pour des combinaisons données très
particulières. Pour bénéficier d’un minimum de confiance statistique de fidélité des motifs de
données à des règles générales, il est conseillé de demander au moins 30 observations par
feuille. Voici donc le code source de notre optimisation :
Nous pouvons ainsi découvrir les meilleurs paramètres et la meilleure précision. Le paramètre
à régler encore correspond au nombre d’arbres :
X, y = digit.data, digit.target
Précision: 0.754
Notre exemple utilise l’estimateur par défaut, qui correspond à un arbre de décision complet.
Si vous voulez essayer une simple souche (qui n’a pas besoin d’autres estimateurs), vous devez
instancier la classe AdaBoostClassifier ainsi :
base_estimator=DecisionTreeClassifier(max_depth=1)
X, y = digit.data, digit.target
crossvalidation = KFold(n_splits=5,
shuffle=True,
random_state=1)
Nous définissons bien sûr ici aussi le taux d’apprentissage et le nombre d’estimateurs (ce sont
des paramètres essentiels pour un apprentissage sans sur-ajustement). Nous devons en outre
fournir les valeurs pour les paramètres subsample et max_depth. Le paramètre subsample
réalise un sous-échantillonnage pendant l’entraînement, pour qu’il soit effectué sur un jeu de
données différent à chaque tour, comme c’est le cas dans la technique d’ensachage bagging. Le
paramètre max_depth stipule le nombre maximal de niveaux des arbres. En pratique, vous
pouvez commencer par trois niveaux, mais il faudra peut-être augmenter cette valeur dans le
cas d’une modélisation de données complexes :
Précision: 0.972
Nous obtenons une erreur carrée moyenne de régression bien meilleure que celle produite par
une forêt aléatoire :
Hyperparamètres de GBM
Les modèles à machine GBM sont assez sensibles aux sur-ajustements s’il y a trop
d’estimateurs en séquence et si le modèle commence à ajuster sur le bruit des données. Il faut
donc vérifier l’efficacité des valeurs couplées pour le nombre d’estimateurs et le taux
d’apprentissage. L’exemple suivant part du jeu de données immobilières de Boston :
X, y = boston.data, boston.target
from sklearn.model_selection import KFold
crossvalidation = KFold(n_splits=5, shuffle=True,
random_state=1)
GBR = GradientBoostingRegressor(n_estimators=1000,
subsample=1.0,
max_depth=3,
learning_rate=0.01,
random_state=1)
Sachez que l’optimisation peut prendre un certain temps parce que les algorithmes GBM
sollicitent beaucoup la machine, surtout si vous choisissez une valeur élevée pour max_depth.
Une bonne technique consiste à ne pas modifier le taux d’apprentissage tout en cherchant à
optimiser subsample et max_depth par rapport à n_estimators (rappelons que de grandes
valeurs pour max_depth supposent en général un moins grand nombre d’estimateurs). Une fois
que vous avez trouvé les meilleures valeurs pour subsample et pour max_depth, vous pouvez
alors pousser plus loin l’optimisation avec n_estimators et learning_rate.
best_params = search_func.best_params_
best_score = abs(search_func.best_score_)
print(‘Meilleurs param.: %s’ % best_params)
print(‘Meilleure erreur carrée moyenne: %.3f’ % best_score)
Une fois l’optimisation réalisée, nous étudions l’erreur carrée moyenne et constatons
l’amélioration par rapport au paramétrage par défaut. Les machines GBM ont toujours besoin
d’un paramétrage fin pour produire leurs meilleurs résultats :
» Sites pédagogiques
» Ressources
N. d. T. : Nous fournissons avec le fichier archive des exemples un fichier au format texte
contenant les liens mentionnés dans les deux chapitres de cette partie.
Arrivé à la fin de ce livre, vous avez découvert un certain nombre de sujets concernant la
science des données avec Python. Mais ce n’est que la partie émergée de l’iceberg. Une
quantité phénoménale de ressources sont offertes à vos quêtes.
Certains sites présentés se résument à une liste de liens sélectionnés. D’autres présentent des
exposés complets, et d’autres encore proposent des outils de datalogie.
Tout bouge sans cesse sur Internet, et certains liens pourront devenir caducs lorsque vous lirez
ceci. C’est ainsi. Nous les revérifierons lors de la prochaine édition.
N. d. T. : Nous avons ajouté quelques sites français par rapport à la version originale.
La datalogie à Paris-Saclay
(en français) Le plateau de Saclay à 20 km au sud-ouest de Paris devient un pôle d’excellence
en réunissant établissements publics de recherche, entreprises technologiques, grandes écoles
et université. Deux liens pour accéder au plateau :
https://www.universite-paris-saclay.fr/fr/bdbc/data-sciences-cartographie-des-
45-formations
https://www.datascience-paris-saclay.fr/
Tout est organisé en catégories, thèmes et sujets, facilitant la navigation. Par exemple, la
catégorie Data Pipeline & Tools, donne sur Python, qui donne sur un lien pour les débutants
Anyone Can Code.
Vous y trouverez aussi les liens vers les grands sites américains de diffusion de cours en ligne
MOOC que sont Coursera, Udacity et Edx.
Pour les MOOC en français, voyez ces sites :
» https://www.fun-mooc.fr/
» https://www.my-mooc.com/fr/categorie/data-science
ar définition, la datalogie (science des données) s’intéresse aux données. Tout au long de ce
P livre, nous avons utilisé un certain nombre de jeux de données, et notamment ceux fournis
pour expérimentation dans la librairie Scikit-learn. Ces jeux sont parfaits pour débuter, mais
puisque vous voulez certainement poursuivre votre aventure, il vous faut trouver de nouvelles
sources de données plus vastes.
Les pages qui suivent montrent comment accéder à un certain nombre de défis liés à des jeux
de données, et conçus pour faire de vous un datalogue de classe mondiale. En combinant les
compétences acquises dans ce livre avec ces jeux de données, vous allez réaliser des projets
incroyables. Vous passerez peut-être même pour une sorte de magicien en extrayant des
informations étonnantes dans ces jeux de données. Tout ce qui suit permet d’acquérir des
compétences particulières et de réussir dans les projets les plus divers.
&e réseau Internet regorge de jeux de données, mais tous ne sont pas du même niveau.
Choisissez vos défis avec soin. Ceux que nous présentons ici sont souvent accompagnés de
tutoriels et sont liés à des publications scientifiques. Les compétitions proposées sont
considérées comme de haut niveau. En relevant ces défis, vous pourrez par exemple ensuite
vous attaquer à l’extraction d’informations depuis une base de données d’entreprise.
Cette base permet de s’entraîner à traiter des données produites par des utilisateurs, aussi
bien en mode supervisé que non supervisé. Nous avons abordé ces deux modes dans les
Chapitres 15 et 19. Cet énorme volume de données vous donne l’occasion de vous confronter
réellement à l’exploitation des mégadonnées.
Écriture manuscrite
La reconnaissance de motifs (patterns), notamment ceux de l’écriture manuscrite, est un
domaine de datalogie majeur. Le jeu de données des chiffres manuscrits de l’institut NIST est
disponible sur le site du chercheur français Yann Le Cun (parti vivre aux USA) à l’adresse
suivante :
http://yann.lecun.com/exdb/mnist/
Il s’agit d’un sous-ensemble du jeu complet, ne contenant que 60 000 exemples avec un jeu de
test de 10 000. Le jeu complet du NIST est disponible par un lien sur la même page. Le jeu
réduit donne l’occasion de traiter les données manuscrites sans avoir à réaliser d’abord de
lourds prétraitements.
Le jeu est constitué de quatre fichiers, pour l’entraînement et les tests, avec les images et les
labels. Vous devez bien sûr récupérer ces quatre fichiers. Un problème de ce jeu NIST est que
les fichiers d’images ne sont pas dans un format unique (le format est présenté en bas de la
page Web). Plutôt que d’écrire vous-même le code Python permettant de charger ces images,
n’hésitez pas à récupérer les solutions publiées par d’autres. Voici trois sites permettant de
récupérer le code pour lire ce jeu de données en Python :
https://cs.indstate.edu/⁓jkinne/cs475-f2011/code/mnistHandwriting.py
https://martin-thoma.com/classify-mnist-with-pybrain/
https://gist.github.com/akesling/5358964
La page consacrée à cette base contient une liste de méthodes à appliquer aux deux jeux. Vous
pourrez y découvrir un nombre étonnant de classificateurs qui vous donneront des idées pour
résoudre le problème. Ce jeu de données se montrera utile dans toutes sortes d’activités.
Vous avons utilisé le jeu d’expérimentation de chiffres manuscrits fourni dans Scikit-learn tout
au long de ce livre. Ce jeu de données est beaucoup moins volumineux, ce qui nous a permis
d’avancer vite parmi les exemples des chapitres concernés.
Analyse de photographies
L’institut de recherche canadien CIFAR propose des jeux de données graphiques à l’adresse
https://www.cs.toronto.edu/⁓kriz/cifar.html sous forme de sous-ensembles du jeu
complet (qui réunit plus de 80 millions de petites images). Le jeu CIFAR-10 ne retient
que 60 000 images en couleurs de 32 sur 32 réparties en dix classes (6 000 images dans
chacune). Ces dix classes correspondent à quatre classes de mode de transport (avions,
automobiles, bateaux, camions) et six classes d’animaux (oiseaux, chats, cervidés, chiens,
grenouilles, chevaux).
Le jeu CIFAR-100 contient 100 classes au lieu de 10, avec le même volume, donc 600 images
par classe. La classification est hiérarchique et les classes sont réparties dans 20 superclasses.
Par exemple, la superclasse des mammifères aquatiques réunit les cinq classes castors,
dauphins, loutres, morses et baleines.
La page mentionnée propose le téléchargement en trois versions : Python, Matlab et binaire.
Prenez soin de bien lire les instructions fournies avec le fichier archive. Vous opterez
évidemment pour la version Python.
Ce défi est tout à fait intéressant à relever juste après celui des chiffres manuscrits. Il donne
l’occasion d’apprendre à gérer des images complexes en couleurs. Si vous avez réalisé les
exemples du Chapitre 14, vous avez un peu d’expérience dans le traitement d’images à partir
du jeu d’expérimentation Olivetti Faces.
Dans la page d’accueil, ouvrez le menu THE DATA et choisissez Get started pour commencer
(https://commoncrawl.org/the-data/get-started/). Une description détaillée du contenu
est fournie à l’adresse suivante :
http://webdatacommons.org/hyperlinkgraph/
Notez que le téléchargement de la base complète (qui grossit jour après jour), représente
notamment 190 Go uniquement pour le fichier des index et, tenez-vous bien, 55 990 Go
(56 téraoctets) pour le seul fichier WARC.
Mais que ces volumes ne vous fassent pas peur. Si vous avez réalisé les exemples du
Chapitre 7, vous savez déjà travailler avec des données de liens de graphes. Le volume à
traiter a bien sûr son importance, mais vous connaissez déjà certaines des techniques requises.
Et maintenant, bon courage dans la suite de vos aventures de datalogue !
Sommaire
Couverture
Introduction
Contenu du livre
Trajectoire de lecture
Les auteurs
Terminologie française
Considérations de performances
Capacités de visualisation
L’accélération matérielle
Exécution du code
L’aide de Colab
PARTIE 2. Plongée dans les données
Chapitre 5. Découverte des outils
La console Jupyter
Bases NoSQL
Concaténation et transformation
Expressions régulières
La technique de hachage
Principe de la corrélation
Regroupements hiérarchiques
Approche multivariée
Validation croisée
La datalogie à Paris-Saclay
Écriture manuscrite
Analyse de photographies