Tanagra Data Mining
1 Objectif
Un cas d’étude pour montrer les pistes d’optimisation d’un programme sous Python. Outils de
débogage, d’analyse et de profilage de code sous l’éditeur SPYDER. Programmation de la procédure
‘’leave-one-out’’ pour l’évaluation des modèles en analyse prédictive.
J’avais écrit récemment un document à propos de l’optimisation des programmes sous R
(‘’Programmer efficacement sous R’’, février 2019). Dans ce tutoriel, nous étudierons cette fois-ci
comment déboguer, analyse et optimiser du code en Python, via l’EDI (Environnement de
Développement Intégré) SPYDER livré avec la distribution ANACONDA.
D’autres environnements de développements existent pour Python (‘’Here are the most popular
Python IDEs / Editors’’, KDnuggets, Décembre 2018) mais, pour ma part, SPYDER me convient très
bien au jour le jour. Je le conseille souvent à mes étudiants, en partie à cause de sa similitude avec
RStudio. L’interface leur étant familière, le passage d’un langage à l’autre est moins abrupt.
Tout comme pour R, nous prétexterons de l’implémentation du leave-one-out (LOOCV – Leave-One-
Out Cross-Validation) en modélisation prédictive (analyse discriminante linéaire) pour explorer les
fonctionnalités proposées par SPYDER.
2 Procédure ‘’leave-one-out’’
Le ‘’leave-one-out’’ (LOOCV, leave-one-out cross-validation) est une procédure de rééchantillonnage
qui permet d’estimer les performances prédictives d’un modèle élaboré sur un dataset de taille n.
Cette technique est un cas particulier de la validation croisée, qui s’impose lorsque nous disposons
d’une base de données de taille réduite et qu’il n’est pas réaliste de la subdiviser en échantillons
d’apprentissage et de test.
L’algorithme est le suivant :
v = vecteur des prédictions
Pour i allant de 1 à n (taille de la base de données)
Construire le modèle sur toutes les observations sauf le n°i (n-1 observations)
Prédire sur l’individu n°i, collecter la valeur dans v
Croiser l’attribut cible et le vecteur des prédictions v
En déduire la proportion de mauvaises prédictions, il s’agit du taux d’erreur en LOOCV
17 avril 2019 Page 1/14
Tanagra Data Mining
On va construire n fois un modèle prédictif, mieux vaut avoir une bonne machine et/ou savoir
programmer astucieusement. C’est ce que nous allons essayer de faire dans ce tutoriel, en nous
aidant des outils de profiling.
Nous utiliserons l’analyse discriminante (LinearDiscriminantAnalysis du package ‘’scikit-learn’’) en
guise d’algorithme de machine learning. L’information n’est pas anodine. En effet, elle est stable
(faible variance), peu propice au surapprentissage, l’utilisation de la LOOCV pour mesurer l’erreur en
généralisation se justifie pleinement dans son contexte. C’est un peu moins vrai lorsque nous avons
affaire à des algorithmes qui ont tendance à trop coller aux données.
3 Données
Nous utilisons une variante de la base ‘’waveform’’ avec 1000 observations. La variable cible ‘’onde’’
est à 3 modalités, nous disposons de 21 descripteurs (V1…V21). De fait, nous aurons à réaliser n =
1000 cycles d’apprentissage sur (n – 1) = 999 observations durant la LOOCV. Il faut espérer que la
classe LinearDiscriminantAnalysis de ‘scikit-learn’’ soit programmée efficacement, sinon nous
risquons d’attendre un moment devant notre PC.
Voici le code pour importer et vérifier l’intégrité des données.
#changement de répertoire
import os
[Link]("... votre dossier ...")
#chargement des données
import pandas
wave = pandas.read_csv("[Link]", sep="\t", header=0)
print([Link]())
<class '[Link]'>
RangeIndex: 1000 entries, 0 to 999
Data columns (total 22 columns):
onde 1000 non-null object
V01 1000 non-null float64
V02 1000 non-null float64
V03 1000 non-null float64
V04 1000 non-null float64
V05 1000 non-null float64
V06 1000 non-null float64
V07 1000 non-null float64
V08 1000 non-null float64
V09 1000 non-null float64
V10 1000 non-null float64
V11 1000 non-null float64
17 avril 2019 Page 2/14
Tanagra Data Mining
V12 1000 non-null float64
V13 1000 non-null float64
V14 1000 non-null float64
V15 1000 non-null float64
V16 1000 non-null float64
V17 1000 non-null float64
V18 1000 non-null float64
V19 1000 non-null float64
V20 1000 non-null float64
V21 1000 non-null float64
dtypes: float64(21), object(1)
memory usage: 172.0+ KB
4 Première version du leave-one-out en Python
4.1 Implémentation et mesure du temps d’exécution
Plusieurs éléments sont nécessaires pour implémenter la LOOCV. Détaillons-les.
Compteur de durée. Tout d’abord, pour mettre en place un compteur de durée d’exécution, nous
récupérons la valeur courante de l’horloge de la machine (en durée de secondes depuis la date
d’initialisation du compteur).
#pour mesurer le temps
import time
debut = [Link]()
Vecteur de prédiction. Pour les collecter les prédictions sur les ième individus successifs de la LOOCV,
nous initialisons un vecteur vide.
#vecteur de prédiction
import numpy
prediction = [Link]([], dtype='object')
Outil pour la LOOCV. Pour définir les sous-échantillons de la LOOCV à chaque itération, nous
instancions un objet de la classe LeaveOneOut de ‘’scikit-learn’’. Elle se chargera de produire les
indices des individus concernés par l’apprentissage et la prédiction au moment voulu.
#classe pour leave one out
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
Classe pour la modélisation. Enfin, nous importons la classe LinearDiscriminantAnalysis de ‘’scikit-
learn’’ pour la modélisation à l’aide l’analyse discriminante. Nous nous appuierons sur les paramètres
par défaut de l’implémentation proposée (version 0.20.3 de sklearn).
17 avril 2019 Page 3/14
Tanagra Data Mining
#algo. pour modélisation
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
Boucle de calcul de la LOOCV. Voici la boucle de calcul de la LOOCV.
#itérer sur les observations en apprentissage et en test
for train_index, test_index in [Link]([Link][:,1:]):
#instancier l'algo
lda = LinearDiscriminantAnalysis()
#modéliser
[Link](X=[Link][train_index,1:], y=[Link][train_index,0])
#prédire
p = [Link](X=[Link][test_index,1:])
#ajouter dans les résultats
prediction = [Link](prediction, p)
Nous distinguons :
− La boucle for qui itère sur les index des observations en apprentissage (train_index, une liste
avec 999 valeurs) et en test (test_index, une liste avec une seule valeur) fournis par [Link]().
− Puis, à chaque itération, nous créons une instance de l’analyse discriminante en faisant appel au
constructeur de LinearDiscriminantAnalysis().
− Les coefficients de l’analyse discriminante sont calculés à l’aide de la fonction fit() appliquée au
données d’apprentissage c.-à-d. au [Link] indexé par les individus appartenant à
l’échantillon d’apprentissage.
− Nous effectuons la prédiction sur l’unique observation en test avec predict().
− Enfin, nous récupérons cette prédiction en l’ajoutant append() dans le vecteur que nous avions
préparé à cet effet plus haut.
L’opération la plus coûteuse est la phase de modélisation à partir de données avec fit() qui sera
répétée n = 1000 fois et, malheureusement, il n’est pas possible de jouer dessus, sauf à modifier les
paramètres de l’instance lors de son initialisation (lors de l’appel du constructeur). Mais ceci est un
autre sujet qui dépasse le cadre de l’optimisation du programme Python puisque nous touchons aux
propriétés mêmes de l’algorithme d’apprentissage.
Calcul du taux de reconnaissance. Nous calculons le taux de succès en faisant appel à la fonction
accuracy_score() de ‘’scikit-learn’’.
#afficher le taux de bonnes prédictions
from [Link] import accuracy_score
print('Acc. score = ' + str(accuracy_score([Link], prediction)))
17 avril 2019 Page 4/14
Tanagra Data Mining
Durée de calcul. Enfin, nous calculons le temps de traitements en faisant de nouveau appel à time().
Nous déduisons la durée via l’écart avec la valeur obtenue lors de l’appel précédent.
#time à la fin
fin = [Link]()
#durée d'exécution
print('Duree = '+str(fin-debut)+ ' sec.')
Pour cette première version de notre implémentation, la durée d’exécution de la validation croisée
est d’approximativement 7.25 secondes. Je dis bien approximativement parce que nous obtenons
une valeur légèrement différente à chaque lancement du programme.
4.2 Analyse statique du code
‘’Est-ce que mon programme est bien écrit ?’’ est une interrogation qui taraude toujours tout bon
programmeur. SPYDER propose un analyseur de code. Nous y avons accès via le menu SOURCE /
DEMARRER L’ANALYSE DE CODE STATIQUE.
17 avril 2019 Page 5/14
Tanagra Data Mining
J’ai la note de 0.00/10 (en rouge dans la fenêtre de droite). Bien ! C’est encourageant. Il semble
surtout que je ne sois pas très attentif aux conventions d’écriture : il manque l’espace derrière
certaines virgules (lignes 28, 32, etc.), les importations de modules (import) ne sont pas regroupées
dans la partie haute du programme (ex. pandas, time, numpy, etc.), je ne respecte pas les règles de
nommage des variables, etc. Rien de bien méchant à vrai dire. Il n’y a ni warning, ni erreurs. On s’en
contentera. En tous les cas, ce type d’outil permet d’acquérir les bons réflexes. Les règles de
nommage des variables par exemple ne sont pas très importants tant que l’on travaille tout seul, ils
le deviennent dès lors que l’on travaille en équipe et qu’il faut partager du code avec d’autres
programmeurs.
4.3 Débogage du programme
Suivre pas-à-pas le déroulement du programme est essentiel pour en comprendre le
fonctionnement, en surveillant les valeurs prises par les différentes variables, en suivant le chemin
suivi par l’exécution selon le contexte d’utilisation. Truffer son code de print() est une solution simple
et immédiate, mais ingérable à mesure que notre code augmente de volume. Il faut alors passer par
le débogueur intégré de l’EDI.
L’outil DEBOGUEUR de SPYDER est très intéressant dans cette optique.
17 avril 2019 Page 6/14
Tanagra Data Mining
Par exemple, nous pouvons ajouter un point d’arrêt à la ligne 30 de notre programme (rond rouge
dans l’EDI à la ligne 30 ci-dessous), juste avant l’instanciation de l’algorithme de machine learning.
A l’exécution (menu DEBOGUER / DEBOGUER), nous observons dans l’EDI (le programme est arrêté à
la ligne 30 dans la copie d’écran ci-dessous, elle est légèrement surlignée) :
Nous visualisons dans l’explorateur de variables
les valeurs courantes des objets. Et dans la console,
nous pouvons approfondir nos investigations. Par
exemple, je demande ici les dimensions du vecteur
train_index. Il comporte 999 valeurs.
Nous pouvons par la suite poursuivre l’exécution
ligne à ligne du programme (DEBOGUER / PAS) et
surveiller l’état des variables. Après 5 itérations,
voici ci-contre ce que nous observons dans le
vecteur des prédictions via la console.
Nous pouvons terminer l’exécution du programme
17 avril 2019 Page 7/14
Tanagra Data Mining
en retirant le point d’arrêt et en cliquant sur le menu DEBOGUER / CONTINUER.
4.4 Profilage du code
Venons-en maintenant au thème principal de ce tutoriel, l’optimisation de notre programme via
l’outil de profilage fourni par SPYDER. Nous cliquons sur le menu EXECUTION / PROFILER. Voici ce
que nous avons dans la fenêtre PROFILEUR dans un premier temps.
Plutôt que par ligne de code, les temps de traitements sont regroupés par fonctions appelées,
lesquelles étant décomposées en appels de sous-fonctions. Elles sont triées par durée de
traitements. Nous disposons également du nombre d’appels. Cette information est très importante.
Gagner ne serait-ce qu’une infime fraction de secondes sur une instruction qui est appelée un très
grand nombre de fois fera gagner un temps considérable à l‘heure du bilan.
Il est possible de sauvegarder ces informations en cliquant sur le bouton SAVE DATA, nous créons le
fichier ‘’lvo – [Link]’’ (c’est un fichier binaire qu’on ne peut pas lire dans un éditeur de texte).
La description est trop détaillée dans cette présentation. Nous revenons au niveau le plus élevé en
cliquant sur l’icone la plus à gauche (les 4 flèches qui se rejoignent).
17 avril 2019 Page 8/14
Tanagra Data Mining
Revoyons notre code source pour identifier chaque item de cette liste.
17 avril 2019 Page 9/14
Tanagra Data Mining
Si l’on se focalise sur les fonctions les plus gourmandes :
− Les instructions fit() (ligne 32, 5.09 sec.), predict() (ligne 34, 764.17 ms.) et _init_ (ligne 30,
1.21 ms.), répétées 1000 fois, sont faciles à identifier. Gagner du temps sur les deux
premières instructions n’est pas trop possibles, sur la troisième, nous verrons plus loin qu’il
est possible de la sortir de la boucle mais le gain est négligeable.
− Le _getitem_ appelé 4001 fois paraît mystérieux (1.76 sec.). En scrutant le code, on se rend
compte qu’il s’agit des accès au [Link] ‘’wave’’ (objet ‘’pandas’’) que l’on observe dans
les lignes n°28, 32 (2 fois) et 34.
− _find_and_load (1.23 sec., 1478 appels) correspond apparemment aux mécanismes d’accès
internes aux objets. J’ai un peu (beaucoup) regardé. Nous n’avons pas vraiment prise là-
dessus. Les opportunités d’optimisation ne sont pas évidentes.
Dans les sections suivantes, nous essayons de réduire le temps de calcul en jouant sur la
réorganisation des commandes et les choix de structure de données.
5 Seconde version – Passage aux matrices Numpy
Tout comme sous R, je suspecte que les [Link] (‘’pandas’’ ici, basé sur des listes) sont moins
performantes que les matrices (‘’numpy’’ sous Python). Nous créons deux structures intermédiaires
pour la matrice des descripteurs (Xdata) et le vecteur de la variable cible (Ydata).
Voici la nouvelle version du code source :
#pour mesurer le temps
import time
debut = [Link]()
#vecteur de prédiction
import numpy
prediction = [Link]([], dtype='object')
#classe pour leave one out
from sklearn.model_selection import LeaveOneOut
loo = LeaveOneOut()
#algo. pour modélisation
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
#matrice et vecteur de données
Xdata = [Link][:,1:].values
Ydata = [Link][:,0].values
17 avril 2019 Page 10/14
Tanagra Data Mining
#itérer
for train_index, test_index in [Link](Xdata):
#instancier l'algo
lda = LinearDiscriminantAnalysis()
#modéliser
[Link](X=Xdata[train_index,:], y=Ydata[train_index])
#prédire
p = [Link](X=Xdata[test_index,:])
#ajouter dans les résultats
prediction = [Link](prediction, p)
#afficher le taux de bonnes prédictions
from [Link] import accuracy_score
print('Acc. score = ' + str(accuracy_score([Link], prediction)))
#fin
fin = [Link]()
#durée
print('Duree = '+str(fin-debut)+ ' sec.')
Le temps de calcul passe à 4.6 sec. en moyenne. Le gain est énorme (nous étions à 7.25 sec. dans la
version précédente) mais il ne m’étonne pas. Nous avions observé le même phénomène sous R.
Un nouvel appel du profileur nous donne le tableau suivant :
17 avril 2019 Page 11/14
Tanagra Data Mining
L’accès à la structure ‘’pandas’’ matérialisée par _getitem_ répétés a disparu de la liste des
instructions (il en reste 2, lorsqu’on crée les variables Xdata et Ydata).
Pour mieux comparer les différences, nous pouvons charger les informations de la précédente
version ‘’lvo- [Link]’’ avec le bouton LOAD DATA.
Nous constatons, entre autres, que fit() et predict() bénéficient également de ce changement de
structure de données.
6 Troisième version – Vecteur de booléens
On peut s’amuser longtemps comme cela. Dans cette dernière version, j’ai essayé de réduire encore
ce qui était répétitif dans la boucle, à savoir les instructions split(), append() et l’instanciation de
l’objet de calcul.
− Pour ce dernier la solution est simple, il suffit de la sortit de la boucle simplement.
− Pour split(), plutôt que les index d’observations fournis par la classe LeaveOneOut de ‘’scikit-
learn’’, nous utilisons un vecteur de booléens (index) pour désigner les individus en
apprentissage et en test. A chaque itération, la valeur de l’individu exclu de l’apprentissage
est passé à False.
− Pour éviter append() et son cortège de petites allocations mémoire successives, nous
initialisons le vecteur de prédictions à la bonne taille dès le départ, puis nous la remplissons
par accès indicé.
17 avril 2019 Page 12/14
Tanagra Data Mining
Voici la nouvelle version du code :
#pour mesurer le temps
import time
debut = [Link]()
#vecteur de prédiction -- initialisation à la bonne taille
import numpy
prediction = [Link]([Link][0]," ", dtype='object')
#vecteur indiquant l'individu à traiter
index = [Link]([Link][0],True)
#algo. pour modélisation
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
#matrices
Xdata = [Link][:,1:].values
Ydata = [Link][:,0].values
#nombre d'observations
n = [Link][0]
#instancier l'algo – sortie de la boucle
lda = LinearDiscriminantAnalysis()
#itérer
for i in range(n):
#modifier l'indicateur pour i pour l'exclure de l'apprentissage
index[i] = False
#modéliser
[Link](X=Xdata[index,:], y=Ydata[index])
#prédire
p = [Link](X=Xdata[[i],:])
#ajouter dans le vecteur des résultats par accès indicé
prediction[i] = p[0]
#remettre la bonne valeur pour l'individu n°i
index[i] = True
#afficher le taux de bonnes prédicions
from [Link] import accuracy_score
print('Acc. score = ' + str(accuracy_score([Link], prediction)))
#fin
fin = [Link]()
#durée
print('Duree = '+str(fin-debut)+ ' sec.')
17 avril 2019 Page 13/14
Tanagra Data Mining
Hé bien… on n’a pas gagné grand-chose finalement. Le temps de traitement moyen est resté autour
de 4.6 sec. C’est assez étonnant je trouve. Les créations de vecteurs d’index et les allocations
mémoires répétées ne sont pas si pénalisantes que cela il faut croire.
En tous les cas, le profileur nous montre que split() et append() ont bel et bien disparu de la liste des
instructions.
7 Conclusion
Vérifier et optimiser son code font partie du quotidien du programmeur. Dans ce tutoriel, nous avons
étudié comment faire dans l’environnement de développement SPYDER pour Python. Nous
constatons que nous disposons d’un bel outil qui nous permet de travailler dans d’excellentes
conditions, c’est une très bonne chose.
8 Références
Spyder : The Scientific Python Development Environnement -- [Link]
17 avril 2019 Page 14/14