Modélisation de données cliniques en : Diagnostic coronarien

Résumé

Modélisation de la variable binaire de présence d'une pathologie coronarienne. Comparaison de la plupart des méthodes et algorithmes; intérêt, ou non, d'ajouter des variables ou caractéristiques (features) obtenues par analyse de correspondances de toutes les variables rendues qualitatives. Utilisation de caret pour l'optimisation des valeurs des paramètres et introducton à l'implémentation de xgboost en R.

Introduction

Des données publiques disponibles sur le site UCI repository décrivent des facteurs de risque et résultats cliniques: 13 parmi 75 de l’étude originale de Detrano et al. (1989), liés à une maladie coronarienne (athérosclérose). Celle-ci est jugée présente lorsque tous les vaisseaux coronariens sont obstrués à plus de 50% par des athéromes. Les variables étudiées sont observées sur un échantillon de 270 patients admis dans une clinique de Cleveland (Ohio) à la suite de douleurs thoraciques pouvant être dues à une angine de poitrine. Elles sont décrites dans le tableau ci-dessous:

Num Code Libellé Valeurs
1 Age
2 Sexe sxF, sxM
3 Douleur Thoracique dlA (angine typique), dlb(atypique) dlc(différent) dlD(asymptom.)
4 Tension Systolique mmHg à l’admission et au repos
5 Cholest Taux mg/dl (préférable<200, limite entre 200 et 240, risqué au-delà)
6 Sucre Taux à jeun scN (<120mg/dl), scO (>120mg/dl)
7 Cardio ECG au repos cdA (Normal) cdB (ST/T anormal) cdC (hypertrophie ventr. gauche)
8 FreqM Fréquence cardiaque maximum lors du test d’effort
9 AngInd Angine induite par l’effort : tmA (oui), tmB (non)
10 PicInd Dépression ST Induite par effort / repos
11 PentInd Segment ST Induit à l’effort piA(ascendante), piB(plate), piC(descendante)
12 Nvais Nombre de vaisseaux fl0, fl1, fl2, fl3 majeurs colorés par fluoroscopie
13 Thal Scintigraphie thN(normal) thF(défaut fixé) thR(défaut révers.) avec effort
14 Coro Coronaropathie CoA(Absence), CoP(Présence)

Certaines sont associées à des risques potentiels et d’autres sont résultats d’examens cliniques au repos ou à la suite d’un test d’effort. Les variables 1, 4, 5, 8, 10 sont quantitatives, les autres sont qualitatives dont certaines binaires : 2, 6, 9, 14. Le diagnostic (variable Coro) a été établi par une angiographie permettant de mesurer l’obstruction des artères coronariennes.

Après la phase exploratoire l’objectif sur ces données est de construire un modèle de prévision de la variable Coro à partir de l’observation des autres, pas ou peu invasives, car l’angiographie est un examen invasif comportant des risques. C'est en fait plus qu'un examen puisqu'il permet simultanément de poser par exemple des stents pour faciliter le débit sanguin.

Répondre aux questions en s'aidant des résultats des exécutions.

1 Préparation des données

1.1 Variables disponibles

Cette étape résume rapidement la suite des commandes qui découlent de la phase phase exploratoire des données qu'il est vivement conseillé d'avoir pratiquée pour se familiariser avec celles-ci.

Lecture des données et préparation pour construire le dataFrame. Les données sont lues et les codes des modalités (niveaux des facteurs) sont renommés de façon explicite.


In [ ]:
path="http://www.math.univ-toulouse.fr/~besse/Wikistat/data/"
#path=""
heart=read.table(paste(path,"heart.dat",sep=""))
# recodage des classes et nom des variables
heart=data.frame(
    Age=heart[,1],
    Sexe=factor(as.factor(heart[,2]),labels=c("sxF","sxM")),
    Douleur=factor(as.factor(heart[,3]),labels=c("dlA","dlB","dlC","dlD")),
    Tension=heart[,4],
    Cholest=heart[,5],
    Sucre=factor(as.factor(heart[,6]),labels=c("scN","scO")),
    Cardio=factor(as.factor(heart[,7]),labels=c("cdA","cdB","cdC")),
    FreqM=heart[,8],
    AngInd=factor(as.factor(heart[,9]),labels=c("tmA","tmB")),
    PicInd=heart[,10],
    PenteInd=factor(as.factor(heart[,11]),labels=c("piA","piB","piC")),
    Nvais=factor(as.factor(heart[,12]),labels=c("fl0","fl1","fl2","fl3")),
    Thal=factor(as.factor(heart[,13]),labels=c("thN","thF","thR")),
    Coro=factor(as.factor(heart[,14]),labels=c("CoA","CoP")))
summary(heart)

Découpage des variables quantitatives en trois classes pour permettre une analyse factorielle multiple des correspondances.


In [ ]:
AgeQ=cut(heart$Age,breaks=quantile(heart$Age,c(0,.33,.66,1)),labels=c("AgeA","AgeB","AgeC"),include.lowest = TRUE)
TensQ=cut(heart$Tension,breaks=quantile(heart$Tension,c(0,.33,.66,1)),labels=c("TensA","TensB","TensC"),include.lowest = TRUE)
CholQ=cut(heart$Cholest,breaks=quantile(heart$Cholest,c(0,.33,.66,1)),labels=c("CholA","CholB","CholC"),include.lowest = TRUE)
FreQ=cut(heart$FreqM,breaks=quantile(heart$FreqM,c(0,.33,.66,1)),labels=c("FreqA","FreqB","FreqC"),include.lowest = TRUE)
PicQ=cut(heart$PicInd,breaks=quantile(heart$PicInd,c(0,.33,.66,1)),labels=c("PicA","PicB","PIcC"),include.lowest = TRUE)

Suppression d'une modalité trop rare.


In [ ]:
heart[heart$Cardio=="cdB","Cardio"]="cdA"
heart$Cardio=factor(heart$Cardio,exclude=NULL)

Création finale du dataFrame


In [ ]:
heartT=data.frame(heart,AgeQ,TensQ,CholQ,FreQ,PicQ)
summary(heartT)

1.2 Gestion, analyse des variables rendues qualitatives

L'étude exploratoire montre l'importance des variables qualitatives. en particulier, l'âge n'intervient pas comme risque de la même façon en fonction du genre des patients. Aussi, une nouvelle variable croisant sexe et âge est construite et une version simplifiée, avec 3 modalités au lieu de 6, est ajoutée à la base.


In [ ]:
# Sélection des seules variables qualitatives
heartQ=heartT[,-c(1,4,5,8,10)]
summary(heartQ)

In [ ]:
# Nouvelle variable pour intégrer l'interaction Sexe x Age
SexAge=heartQ$Sexe:heartQ$AgeQ
heartQ=data.frame(heartQ,"SexAge"=SexAge)

In [ ]:
# Simplifier la variable en regroupant les jeunes hommes et femmes dans la même classe
SxAg=as.factor(rep(c("sHFageA","sFageBC","sHageBC"),90))
SxAg[heartQ$AgeQ=="AgeA"]="sHFageA"
SxAg[heartQ$SexAge=="sxF:AgeB" | heartQ$SexAge=="sxF:AgeC"]="sFageBC"
SxAg[heartQ$SexAge=="sxM:AgeB" | heartQ$SexAge=="sxM:AgeC"]="sHageBC"

In [ ]:
# Complétion du data frame
heartQ2=data.frame(heartQ[,-15],SxAg)
summary(heartQ2)

1.2 Ajout de caractéristiques (features)

Cette étude teste la stratégie suivante: ajouter des nouvelles variables ou features sous la forme des composantes principales issues de l'AFCM du tableau disjoinctif complet. Si toutes les variables étaient quantitatives, cela reviendrait à ajouter les composantes principales d'une analyse en composantes principales mais en prenant toutes celles qualitatives plus les quantitatives découpées en classes, c'est une AFCM qui convient.

Cette approche a un autre avantage, elle permet de considérer des méthodes de classification supervisée qui n'autorisent que des variables quantitatives comme variables prédictives tout en conservant l'information apportée par les variables qualitatives.

Q Quelles sont les principales méthodes de classifications supervisée concernées?

Q Quelle autre approche est mise en oeuvre pour contourner ce problème?

R :L'autre approche, souvent utilisée, consiste à remplacer toutes les variables qualitatives par les paquets de leurs indicatrices (dummy variables).

Construction des composantes principales d'une AFCM

Les nouvelles variables (composantes principales) sont construites à l'aide d'une AFCM disponible dans la librairie FactoMineR. Elles sont encore les composantes principales de l'ACP, avec la métrique dite du Chi2, du tableau disjonctif complet.

Une première exécution fournit la représentation graphique de toutes les modalités dans le premier plan factoriel.


In [ ]:
library(FactoMineR)
afc=MCA(heartQ2,quali.sup=c(1,9,10),graph=F)
options(repr.plot.width=5, repr.plot.height=5)
par(mar=c(2.1, 2.1, .1, .1))
plot.MCA(afc,invisible=c("ind"),cex=0.8,habillage="quali",palette=palette(rainbow(14)),title="")

L'interprétation de ce graphique était un des objectifs de la phase exploratoire. Remarquer simplement la position des modalités liées à la présence ou non de la pathologie: CoP, CoA, et celles représentant les facteurs de risque, dont sHageBC (hommes plus âgés), alors que le cholestérol (CholA, B, C) augmente avec l'âge (tous genres confondus) sans être nécessairement lié avec l'aggravation du risque.


In [ ]:
# Regrouper toiutes les variables dans un même dataFrame
heartT=data.frame(heart,heartQ2[,c(10:15)])
summary(heartT)

Séparation des échantillons apprentissage et test

Q Qu'est ce qui nécessite cette démarche?


In [ ]:
# séparation apprentissage / test
set.seed(11)
npop=nrow(heartT)

ntest=100
testi=sample(1:npop,ntest)
appri=setdiff(1:npop,testi)

datapp=heartT[appri,]
datest=heartT[testi,]

Les étapes suivantes calculent les composantes principales de l'AFCM ou coordonnées des individus dans la base des vecteurs propres.

Q Les individus, patients, de l'étude appartenant à l'échantillon teste sont considérés comme individus supplémentaires. Pourquoi?

R: Il faut calculer également les nouvelles coordonnées des individus du test dans cette base mais ils ne doivent pas participer au calcul des vecteurs et valeurs propres et donc à la définition des axes factoriels. Pour ce faire, ils sont déclarés comme individus en supplémentaires.


In [ ]:
library(FactoMineR)
summary(heartQ)  
afc=MCA(heartQ2[,-9],ind.sup=testi,ncp=9,graph=F)

In [ ]:
summary(afc$ind$coord)

In [ ]:
summary(afc$ind.sup$coord)

Construction des des bases de données échantillon et test


In [ ]:
# Ajout des nouvelles variables
datapp=data.frame(datapp,afc$ind$coord)
datest=data.frame(datest,afc$ind.sup$coord)

In [ ]:
summary(datapp)

2 Panoplie des méthodes de classification supervisée

Le problème à résoudre est la prévision de la présence d'une coronopathie. C'est donc un problème de discriminaiton binaire que beaucoup de méthodes peuvent aborder.

L'objectif est de détrerminer si les composantes principales ont un intérêt ou non pour l'atteinte de l'objectif.

2.1 Régression logistique

La sélection de variables et donc la complexité du modèle sont optimisées par minimisation du critère AIC avant de calculer l'erreur de prévision sur l'échantillon test.

Q Quels sont principaux les algorithmes de sélection de variable utilisables pour cet objectif?

Sur les seules variables initiales quantitatives et qualitatives


In [ ]:
res.log=glm(Coro~1,data=datapp,family="binomial")
res.log.step=step(res.log,scope=list(lower=~1,upper=~(Age+Sexe+Douleur+Tension+Cholest+
                  AngInd+PicInd+PenteInd+Nvais+Thal+Sucre+Cardio+FreqM+SxAg),
                  direction="both"),trace=0)
anova(res.log.step, test="Chisq")
table(predict(res.log.step,datest,type="response")>0.5,datest[,14]=="CoP")

Sur l'ensemble des variables


In [ ]:
res.log=glm(Coro~1,data=datapp,family="binomial")
res.log.step=step(res.log,scope=list(lower=~1,upper=~(Age+Sexe+Douleur+Tension+Cholest+
                AngInd+PicInd+PenteInd+Nvais+Thal+Sucre+Cardio+FreqM+SxAg+Dim.1+Dim.2+Dim.3+
                                    Dim.4+Dim.5+Dim.6+Dim.8+Dim.9)),direction="both",trace=0)
anova(res.log.step, test="Chisq")
table(predict(res.log.step,datest,type="response")>0.5,datest[,14]=="CoP")

Sur les seules composantes de l'AFCM


In [ ]:
res2.log=glm(Coro~1,data=datapp,family="binomial")
res2.log.step=step(res2.log,scope=list(lower=~1,upper=~(Dim.1+Dim.2+Dim.3+Dim.4+
                            Dim.5+Dim.6+Dim.8+Dim.9)),direction="both",trace=0)
anova(res2.log.step, test="Chisq")
table(predict(res2.log.step,datest,type="response")>0.5,datest[,14]=="CoP")

Q Que pensez de la capacité des composantes pricipales de l'AFCM à résumer l'information pertinente apportée par les variables? Justifier.

2.2 Arbre de décision binaire

Comme précédemment la méthode est comparée sur les deux ensembles de variables.

Sur les données initiales


In [ ]:
library(rpart)
library(partykit)
res.tree=rpart(Coro~.,data=datapp[,1:20],cp=0)
par(mar=c(2.1, 2.1, .1, .1))
plot(as.party(res.tree),gp = gpar(fontsize = 11))

In [ ]:
table(predict(res.tree,datest,type="class"),datest[,14])

Q Que dire des facteurs de risque identifiés par ce modèle? de la qualité de la prévision? Que signifie Cp? Que dire du choix de la valeur?

Sur toutes les données


In [ ]:
res.tree=rpart(Coro~.,data=datapp,cp=0)
options(repr.plot.width=6, repr.plot.height=5)
par(mar=c(2.1, 2.1, .1, .1))
library(partykit) # si java est bien installé
plot(as.party(res.tree),gp = gpar(fontsize = 10))

In [ ]:
table(predict(res.tree,datest,type="class"),datest[,14])

Sur les composantes de l'AFCM


In [ ]:
res2.tree=rpart(Coro~.,data=datapp[,c(14,21:29)],cp=0)
table(predict(res2.tree,datest,type="class"),datest[,14])

2.3 Réseaux de neurones

Même démarche avec cet algorithme.

Variables initiales


In [ ]:
### nnet
library(MASS)
library(nnet)
res.net=nnet(Coro~.,data=datapp[,1:20],size=5,decay=1,maxit=500)
table(predict(res.net,datest,type="class"),datest[,14]) #21

Ensemble des variables


In [ ]:
res2.net=nnet(Coro~.,data=datapp,size=5,decay=1,maxit=500)
table(predict(res.net,datest,type="class"),datest[,14])

Composantes principales


In [ ]:
res2.net=nnet(Coro~.,data=datapp[,c(14,21:29)],size=5,decay=1,maxit=500)
table(predict(res.net,datest,type="class"),datest[,14])

Q Quelle stratégie vous semble raisonnable? Quel est le rôle du paramètre decay? Que manque-t-il à cette étape?

2.4 Séparateur à vaste marge

Variables initiales


In [ ]:
library(e1071)
res.svm=svm(Coro~.,data=datapp[,1:20],cost=0.5,kernel="radial")     
par(mar=c(2.1, 2.1, .1, .1))
options(repr.plot.width=2, repr.plot.height=2)
plot(tune.svm(Coro~.,data=datapp,cost=c(.5,1,1.25,1.5,1.75,2)),main="")

In [ ]:
table(predict(res.svm,datest,type="class"),datest[,14])

Toutes les variables


In [ ]:
res.svm=svm(Coro~.,data=datapp,cost=0.5,kernel="radial")     
table(predict(res.svm,datest,type="class"),datest[,14])

Composantes principales


In [ ]:
res2.svm=svm(Coro~.,data=datapp[,c(14,21:29)],cost=0.5,kernel="radial")     
table(predict(res2.svm,datest,type="class"),datest[,14])

Q Quel est le paramètre cost? Commenter les résultats.

2.5 K plus proches voisins

Q Quelle contrainte pour cette méthode sur le choix des variables? Commenter les résultats (choix de k, qualité).


In [ ]:
library(class)
options(repr.plot.width=2.5, repr.plot.height=2.5)
par(mar=c(2.1, 2.1, .1, .1))
plot(tune.knn(as.matrix(datapp[,c(1,4,5,8,10,21:28)]),as.factor(datapp[,c(14)]),k=2:20),main="")

In [ ]:
pred.knn=knn(datapp[,c(1,4,5,8,10,21:28)],datest[,c(1,4,5,8,10,21:28)],datapp[,c(14)],k=12,prob=T)
table(pred.knn,datest[,14])  #15

2.6 Forêts aléatoires

Variables initiales


In [ ]:
library(randomForest)
xapp=datapp[,-c(14,21:29)];yapp=datapp[,14];xtest=datest[,-c(14,21:29)];ytest=datest[,14]
res.rf=randomForest(x=xapp, y=yapp, xtest=xtest, ytest=ytest, mtry=2,do.trace=50,
                    importance=TRUE)
sort(res.rf$importance[,3],decreasing=TRUE)

Q Quel paramètres faudrait-il optimiser? A quoi sert importance=TRUE dans la fonction de cette méthode? Comment en interpréter le résultat?

Toutes les variables


In [ ]:
xapp=datapp[,-14];yapp=datapp[,14];xtest=datest[,-14];ytest=datest[,14]
res.rf=randomForest(x=xapp, y=yapp, xtest=xtest, ytest=ytest, mtry=2,do.trace=50,importance=TRUE)
sort(res.rf$importance[,3],decreasing=TRUE)

Composantes principales


In [ ]:
xapp=datapp[,c(21:29)];yapp=datapp[,14];xtest=datest[,c(21:29)];ytest=datest[,14]
res2.rf=randomForest(x=xapp, y=yapp, xtest=xtest, ytest=ytest, mtry=2,do.trace=50)

Q Que signifie OOB ? Comparer les qualité de prévision.

2.7 Gradient boosting

Cette implémentation inclut une optimisation par validation croisée.

Variables initiales


In [ ]:
library(gbm)
boost=gbm(as.numeric(datapp$Coro)-1~.,
          data=datapp[,-c(21:28)],distribution="adaboost",
          n.trees=500, cv.folds=10,n.minobsinnode = 5,
          shrinkage=0.01,verbose=FALSE)

In [ ]:
options(repr.plot.width=3, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
best.iter=gbm.perf(boost,method="cv")
best.iter # 381
pred.boost=predict(boost,newdata=datest,n.trees=best.iter)
table(as.factor(sign(pred.boost)),datest[,14]) #19
mean(sign(pred.boost) != (2*as.numeric(datest[,14])-3))

Q Comment ce graphe est-il obtenu? Quelle interprétation en tirer?

Toutes les variables


In [ ]:
boost=gbm(as.numeric(datapp$Coro)-1~.,data=datapp,distribution="adaboost",
          n.trees=500, cv.folds=10,n.minobsinnode = 5,shrinkage=0.01,verbose=FALSE)

In [ ]:
options(repr.plot.width=3, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
best.iter=gbm.perf(boost,method="cv")
best.iter # 381
pred.boost=predict(boost,newdata=datest,n.trees=best.iter)
table(as.factor(sign(pred.boost)),datest[,14]) #19
mean(sign(pred.boost) != (2*as.numeric(datest[,14])-3))

2.8 Comparaison des courbes ROC


In [ ]:
library(ROCR)
roclogit=predict(res.log.step,newdata=datest,type="response")
predlogit=prediction(roclogit,datest[,14])
perflogit=performance(predlogit,"tpr","fpr")

prob=attr(pred.knn,"prob")
prob <- ifelse(pred.knn == "CoA", 1-prob, prob)

predknn=prediction(prob,datest[,14])
perfknn=performance(predknn,"tpr","fpr")

roctree=predict(res.tree,newdata=datest,type="prob")[,2]
predtree=prediction(roctree,datest[,14])
perftree=performance(predtree,"tpr","fpr")

rocrn=predict(res.net,datest)
predrn=prediction(rocrn,datest[,14])
perfrn=performance(predrn,"tpr","fpr")

res.svm=svm(Coro~.,data=datapp[,c(14,20:28)],cost=3,kernel="radial",probability=T)

rocsvm=predict(res.svm,datest,probability=T)
predsvm=prediction(attr(rocsvm,"probabilities")[,2],datest[,14])
perfsvm=performance(predsvm,"tpr","fpr")

rocrf=res.rf$test$vote[,2]
predrf=prediction(rocrf,datest[,14])
perfrf=performance(predrf,"tpr","fpr")

predboost=prediction(pred.boost,datest[,14])
perfboost=performance(predboost,"tpr","fpr")


options(repr.plot.width=4, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
palette("default")
plot(perflogit,col=1,lwd=2)
plot(perfknn,col=2,lwd=3,add=T)
plot(perftree,col=3,lwd=2,add=T)
plot(perfrn,col=4,lwd=2,add=T)
plot(perfsvm,col=5,lwd=2,add=T)
plot(perfboost,col=6,lwd=2,add=TRUE)
plot(perfrf,col=7,lwd=2,add=T,)

legend("bottomright",legend=c("logit","knn","rpart","nnet","svm","gbm","rf"),
       y.intersp=2,col=c(1:7),lty=1,lwd=2,text.width=0.15,ncol=2)

Q A la vue de ce graphique, quelles méthodes abandonner à ce niveau? Pourquoi? Que dire de façon générale sur le choix de la meilleure stratégie pour déterminer le paquet de variables à utiliser? Distinguer selon l'objectif de prévision ou d'interprétation.

La suite dépend des choix qui sont opérés en privilégiant la pévision "brute".

3 Automatisation des optimisations

La librairie caret permet d'automatiser les valeurs des paramètres de complexité des modèles pour chaque méthodes. Les échantillons apprentissage et test sont normalisés.

Q Que signifie le paramètre tunelength de la fonction train de cette librairie?


In [ ]:
library(caret)
# Extraction des échantillons
trainDescr=datapp[,-14]
testDescr=datest[,-14]
trainY=datapp[,14]
testY=datest[,14]

# Normalisation
xTrans=preProcess(trainDescr)
trainDescr=predict(xTrans,trainDescr)
testDescr=predict(xTrans,testDescr)
# Choix de la validation croisée
cvControl=trainControl(method="cv",number=10)

3.1 Régression logistique

L'option tunelength est inutile. L'algorithme opère une sélection de variables par algorithme forward minimisant l'AIC.

Q POurqupoi ré-exécuter la commande set.seed avant l'optimisation du ou des paramètres pour chaque méthode?


In [ ]:
set.seed(2)
rlogFit = train(trainDescr, trainY,method = "glmStepAIC", tuneLength = 8, trControl = cvControl)
rlogFit

3.2 Forêts aléatoires


In [ ]:
set.seed(2)
rfFit = train(trainDescr, trainY,method = "rf", tuneLength = 8,trControl = cvControl)
rfFit
options(repr.plot.width=3, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
plot(rfFit)

3.3 Gradient boosting


In [ ]:
set.seed(2)
gbmFit = train(trainDescr, trainY,method = "gbm", tuneLength = 8, 
               trControl = cvControl,verbose=FALSE)

In [ ]:
# gbmFit commande trop bavarde

In [ ]:
options(repr.plot.width=4, repr.plot.height=4)
par(mar=c(2.1, 2.1, .1, .1))
plot(gbmFit)

Q Comment interpréter ce graphique?

3.3 Séparateur à vaste marge


In [ ]:
set.seed(2)
svmgFit = train(trainDescr[,c(1,4,5,8,10,20:28)], trainY,method = "svmRadial", tuneLength = 8,trControl = cvControl)
svmgFit

In [ ]:
options(repr.plot.width=3, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
plot(svmgFit)

3.4 Réseau de neurones

Q Quel autre paramètre permettrait de faire varier et prendre en compte l'implémentation du perceptron dans la librairie Scikit-learn de Python?


In [ ]:
set.seed(2)
nnetFit = train(trainDescr, trainY,method = "nnet", tuneLength = 6,trControl = cvControl)

In [ ]:
# nnetFit # commande trop bavarde

In [ ]:
plot(nnetFit)

4 Validation croisée Monte Carlo

Q Que met en oeuvre cette procédure de validation croisée Monte Carlo (fonction pred.autom) ? Pourquoi?

Attention, exécuter au préalable la fonction pred.autom définie en annexe à la fin de ce calepin.

4.1 Base avec toutes les variables


In [ ]:
models=c("gbm","rf","nnet")
noptim=c(6,6,6)
Niter=30 ; Init=3
predH=pred.autom(trainDescr, trainY,methodes=models,N=Niter,xinit=Init,size=noptim,type="prob")

In [ ]:
# Calcul des taux de bien classés
obs=predH$obs
prevH=predH$pred
resH=lapply(prevH,function(x)apply((x>0.5)==(obs==1),2,mean))
# Moyennes des taux de bien classés par méthode
lapply(resH,mean)

In [ ]:
# distributions des taux de bien classés
options(repr.plot.width=2.2, repr.plot.height=2.2)
par(mar=c(2.1, 2.1, .1, .1))
boxplot(data.frame(resH))

In [ ]:
predrocH=lapply(prevH,function(x)prediction(x,obs==1))
perfrocH=lapply(predrocH,function(x)performance(x,"tpr","fpr"))
options(repr.plot.width=4, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
plot(perfrocH$gbm,col=1,lwd=3,avg="threshold")
plot(perfrocH$nnet,col=3,add=TRUE,lwd=3,avg="threshold")
plot(perfrocH$rf,col=2,add=TRUE,lwd=3,avg="threshold",
     print.cutoffs.at=c(0.1, 0.25, 0.5, 0.75, 0.9))
legend("bottomright",legend=c("gbm","nnet","rf"),y.intersp=3,
       col=c(1,3,2),lty=1,lwd=2,text.width=0.15)

Q Que signifient les valeurs présentes sur les courbes ROC? Quelle conclusion tirer de ce graphique en fonction du taux de faux positif acceptable?

Variables initiales


In [ ]:
models=c("gbm","rf")
noptim=c(10,10)
Niter=30 ; Init=3 
predH2=pred.autom(trainDescr[,-c(20:28)], trainY,methodes=models,N=Niter,xinit=Init,size=noptim,type="prob")

In [ ]:
# Calcul des taux de bien classés
obs=predH2$obs
prevH2=predH2$pred
resH2=lapply(prevH2,function(x)apply((x>0.5)==(obs==1),2,mean))
# Moyennes des taux de bien classés par méthode
lapply(resH2,mean)

In [ ]:
# distributions des taux de bien classés
options(repr.plot.width=2.2, repr.plot.height=2.2)
par(mar=c(2.1, 2.1, .1, .1))
boxplot(data.frame(resH2))

In [ ]:
predrocH2=lapply(prevH2,function(x)prediction(x,obs==1))
perfrocH2=lapply(predrocH2,function(x)performance(x,"tpr","fpr"))
options(repr.plot.width=4, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
plot(perfrocH2$gbm,col=1,lwd=3,avg="threshold")
plot(perfrocH2$rf,col=2,add=TRUE,lwd=3,avg="threshold",
     print.cutoffs.at=c(0.1, 0.25, 0.5, 0.75, 0.9))
legend("bottomright",legend=c("gbm","rf"),y.intersp=3,
       col=c(1,2),lty=1,lwd=2,text.width=0.15)

Q Quelle méthode ? Quelle stratégie de choix de base de données finalement choisir? Les différences vous semble-t-il significatives?

4 XGBoost

Comme le boosting conduit à des résultats encourageants, la librairie xgboost, utilisée dans la plupart des concours de prévision (kaggle), est testée dans l'espoir de les améliorer encore.

4.1 Utilisation élémentaire


In [ ]:
TrainD=as.matrix(data.frame(tab.disjonctif(trainDescr[,-c(1,4,5,8,10,20:28)]),trainDescr[,c(1,4,5,8,10,20:28)]))
TestD=as.matrix(data.frame(tab.disjonctif(testDescr[,-c(1,4,5,8,10,20:28)]),testDescr[,c(1,4,5,8,10,20:28)]))

In [ ]:
library(xgboost)
xbst = xgboost(data = TrainD, label = as.numeric(trainY)-1, max.depth = 2,
               eta = 1, nround = 30, objective = "binary:logistic")
pred = predict(xbst, as.matrix(TestD))

In [ ]:
prediction = as.numeric(pred > 0.5)
err = mean(as.numeric(pred > 0.5) == as.numeric(testY))
print(paste("test-error=", err))

In [ ]:
xbst = xgboost(data = as.matrix(trainDescr[,20:28]), label = as.numeric(trainY)-1, max.depth = 2,
               eta = 1, nround = 30, objective = "binary:logistic")
pred = predict(xbst, as.matrix(testDescr[,20:28]))

In [ ]:
err = mean(as.numeric(pred > 0.5) == as.numeric(testY))
print(paste("test-error=", err))

4.2 Optimisation des paramètres

Le principal souci de cette librairie est lié au nombre de paramètres qui peuvent être optimisés. La librairie caret est utilisée pour facilité cette optimisation en première approche. Le calcul est long et d'autres moyens (e.g. GPU) seraient nécessaires pour espérer optimiser les valeurs de tous les paramètres.


In [ ]:
library(xgboost)
set.seed(2)
xgbmFit = train(as.matrix(TrainD), trainY,method = "xgbTree",tuneLength = 4,trControl = cvControl,verbose=FALSE)

In [ ]:
xgbmFit
Tuning parameter 'gamma' was held constant at a value of 0
Tuning parameter 'min_child_weight' was held constant at a value of 1
Accuracy was used to select the optimal model using  the largest value.
The final values used for the model were nrounds = 50, max_depth = 1, 
eta = 0.3, gamma = 0, colsample_bytree = 0.8, min_child_weight = 1 and 
subsample  = 0.5.  

Q Ci-dessus la synthèse de résultats fournis par l'exécution de la commande train de caret. Que retenir de cette implémentation du boosting?


In [ ]:
options(repr.plot.width=5, repr.plot.height=5)
par(mar=c(2.1, 2.1, .1, .1))
plot(xgbmFit)

4.3 Automatisation


In [ ]:
models=c("xgbTree")
noptim=c(4)
Niter=30 ; Init=3 
predH3=pred.autom(TrainD,trainY,methodes=models,N=Niter,xinit=Init,size=noptim,type="prob")

In [ ]:
# Calcul des taux de bien classés
obs=predH3$obs
prevH3=predH3$pred
resH3=lapply(prevH3,function(x)apply((x>0.5)==(obs==1),2,mean))
# Moyennes des taux de bien classés par méthode
lapply(data.frame(resH2,resH3),mean)

In [ ]:
# distributions des taux de bien classés
options(repr.plot.width=2.2, repr.plot.height=2.2)
par(mar=c(2.1, 2.1, .1, .1))
boxplot(data.frame(resH2,resH3))

In [ ]:
predrocH3=lapply(prevH3,function(x)prediction(x,obs==1))
perfrocH3=lapply(predrocH3,function(x)performance(x,"tpr","fpr"))
options(repr.plot.width=4, repr.plot.height=3)
par(mar=c(2.1, 2.1, .1, .1))
plot(perfrocH3$xgbTree,col=1,lwd=3,avg="threshold")
plot(perfrocH2$gbm,col=3,add=TRUE,lwd=3,avg="threshold")
plot(perfrocH2$rf,col=2,add=TRUE,lwd=3,avg="threshold",
     print.cutoffs.at=c(0.1, 0.25, 0.5, 0.75, 0.9))
legend("bottomright",legend=c("xgboost","gbm","rf"),y.intersp=3,
       col=c(1,3,2),lty=1,lwd=2,text.width=0.15)

Q Commenter ces derniers résultats

Annexe: fonction de validation croisée Monte Carlo


In [ ]:
pred.autom=function(X,Y,p=1/2,methodes=c("knn",
   "rf"),size=c(10,2),xinit=11,N=10,typerr="cv",
   number=4,type="raw")
# Fonction de prévision de N échantillons tests
# par une liste de méthodes de régression 
# ou classification (uniquement 2 classes)
# Optimisation des paramètres par validation 
# croisée (défaut) ou bootstrap ou... (cf. caret)
# X : matrice ou frame des variables explicatives
# Y : variable cible quantitative ou qualitative 
# p : proportion entre apprentissage et test
# methodes : liste des méthodes de rdiscrimination
# size : e grille des paramètres à optimiser
# xinit : générateur de nombres aléatoires
# N : nombre de réplications apprentissage / test
# typerr : "cv" ou "boo" ou "oob"
# number : nombre de répétitions CV ou bootstrap
# pred : liste des matrices de prévision
{
# type d'erreur
Control=trainControl(method=typerr,number=number)
# initialisation du générateur 
set.seed(xinit)
# liste de matrices stockant les prévisions
# une par méthode, une ligne par observation, 
# une colonne par échantillon test
inTrain=createDataPartition(Y,p=p,list=FALSE)
ntest=length(Y[-inTrain])
pred=vector("list",length(methodes))
names(pred)=methodes
pred=lapply(pred,function(x)x=matrix(0,
   nrow=ntest,ncol=N))
# variable cible des échantillons test
obs=matrix(0,ntest,N)

set.seed(xinit)
# N itérations, une par échantillon test
for(i in 1:N)  
{
# indices de l'échantillon d'apprentissage 
inTrain=createDataPartition(Y,p=p,list=FALSE)
# Extraction des échantillons
trainDescr=X[inTrain,]
testDescr=X[-inTrain,]
trainY=Y[inTrain]
testY=Y[-inTrain]
# stockage des observés de testY
obs[,i]=testY
# centrage et réduction des variables
xTrans=preProcess(trainDescr)
trainDescr=predict(xTrans,trainDescr)
testDescr=predict(xTrans,testDescr)
# estimation et optimisation des modèles 
# pour chaque méthode de la liste
for(j in 1:length(methodes))
{
# modélisation 
modFit = train(trainDescr, trainY,
   method = methodes[j], tuneLength = size[j],
   trControl = Control)
# prévisions de l'échantillon test
if (type=="prob")  pred[[j]][,i]=predict(modFit,
    newdata = testDescr,type=type)[,1]
else pred[[j]][,i]=predict(modFit, 
   newdata = testDescr)
}}
list(pred=pred,obs=obs) # résultats
}

In [ ]: