M1 BBS - TP Maths - Analyse en Composantes Principales
1 ACP : Premier exemple illustrant le changement de repère et la variance portée par les axes
1.1 Jeu de données et préliminaires
Jeu de données utilisé en TDB TP2 pour la régression linéaire :
library(tidyverse)
## ── Attaching packages ───────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.2 ──
## ✔ ggplot2 3.4.0 ✔ purrr 0.3.5
## ✔ tibble 3.1.8 ✔ dplyr 1.0.10
## ✔ tidyr 1.2.1 ✔ stringr 1.4.1
## ✔ readr 2.1.3 ✔ forcats 0.5.2
## ── Conflicts ──────────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
=read.table("http://silico.biotoul.fr/site/images/d/de/Bacterial_genomes.txt", sep="\t", header=TRUE) genomes
Exploration du contenu
summary(genomes)
## Genome_size ORF_number
## Min. : 920 Min. : 744
## 1st Qu.: 2250 1st Qu.:2310
## Median : 3608 Median :3475
## Mean : 4023 Mean :3831
## 3rd Qu.: 4842 3rd Qu.:4634
## Max. :10467 Max. :9501
La taille des génomes en kilobases et le nombre de gènes/ORF sont du même ordre. De manière très grossière, on observe généralement 1 gène par kilobase. Variances :
apply(genomes, 2, var)
## Genome_size ORF_number
## 4842536 3621794
Visualisation sous forme de boites à moustaches:
par(mfrow=c(1,2))
boxplot(genomes$Genome_size, ylim=c(0,10**4), main='Genome size')
boxplot(genomes$ORF_number, ylim=c(0,10**4), main='ORF number')
Les variances sont-elles homogènes ?
var.test(genomes$Genome_size, genomes$ORF_number)
##
## F test to compare two variances
##
## data: genomes$Genome_size and genomes$ORF_number
## F = 1.3371, num df = 98, denom df = 98, p-value = 0.1523
## alternative hypothesis: true ratio of variances is not equal to 1
## 95 percent confidence interval:
## 0.8977926 1.9912333
## sample estimates:
## ratio of variances
## 1.337054
Dans ce cas, on pourrait réaliser une ACP non normée. En pratique, pour ce jeu de données, ça ne fait presque aucune différence. On va faire avec la normalisation pour décomposer l’ensemble des opérations.
1.2 ACP normée avec
princomp
Réalisation de l’ACP normée avec la fonction princomp
:
= princomp(genomes, cor=T) acp
Visualisation et comparaison du repère original vs.
projection dans celui des 2 premières composantes de l’ACP (les
coordonnées des individus dans ce repère se trouvent dans
scores
) :
par(mfrow=c(1,2))
plot(genomes, pch="", main="Scatterplot + régression linéaire\n + centre du nuage")
abline(lm(genomes$ORF_number ~ genomes$Genome_size), lty='dotted', col='blue')
points(mean(genomes$Genome_size), mean(genomes$ORF_number), pch='×', col='red', cex=3)
text(genomes$Genome_size, genomes$ORF_number, labels= 1:nrow(genomes), cex=.5)
#plot(acp$scores[,1], -acp$scores[,2], xlab='PC1', ylab='PC2', pch="", ylim=2*c(-2000,2000), main='ACP')
plot(acp$scores[,1], -acp$scores[,2], xlab='PC1', ylab='PC2', pch="", ylim=c(-2,3), main='ACP')
text(acp$scores[,1], -acp$scores[,2], labels=1:nrow(genomes), cex=.5)
abline(h=0, lty="dashed")
abline(v=0, lty="dashed")
Variances portées par les axes via summary
summary(acp)
## Importance of components:
## Comp.1 Comp.2
## Standard deviation 1.3987612 0.20848734
## Proportion of Variance 0.9782665 0.02173348
## Cumulative Proportion 0.9782665 1.00000000
Discuter de la variance portée sur les deux premières composantes principales.
De manière visuelle avec la fonction screeplot
:
screeplot(acp)
1.3 Analyse du résultat
de princomp
Il est toujours conseillé de consulter l’aide (même si parfois, elle n’aide pas vraiment) pour savoir ce qui est renvoyé par une fonction :
?princomp
Pour ce qui est renvoyé, on s’intéressera à la section Value :
Value
princomp returns a list with class "princomp" containing the following components:
sdev the standard deviations of the principal components.
loadings the matrix of variable loadings (i.e., a matrix whose columns contain the eigenvectors). This is of class "loadings": see loadings for its print method.
center the means that were subtracted.
scale the scalings applied to each variable.
n.obs the number of observations.
scores if scores = TRUE, the scores of the supplied data on the principal components. These are non-null only if x was supplied, and if covmat was also supplied if it was a covariance list. For the formula method, napredict() is applied to handle the treatment of values omitted by the na.action.
call the matched call.
na.action If relevant.
Pour les initié·e·s, on pourra noter :
sdev
: les écart-types des composantes principales (correspondent également aux valeurs propres de la matrice de covariance)loadings
: les vecteurs propres de la matrice de covariancescores
: les coordonnées individus (déjà utilisé plus tôt)center
etscale
: valeurs utilisées pour centrer et réduire les variables
1.3.1 Rappel sur les étapes d’une ACP
- Centrage des individus (changement d’origine pour la placer au centre du nuage de points)
- Optionnel : Normalisation des variables
- Calcul de la matrice de variance-covariance
- Calcul des vecteurs et valeurs propres associés de la matrice de covariance
Les vecteurs propres (composantes principales) correspondent à la combinaison linéaire des variables de départ. Les valeurs propres associées correspondent aux variances de ces nouveaux axes.
1.3.2 Centrage
apply(genomes, 2, mean)
## Genome_size ORF_number
## 4023.051 3831.273
$center acp
## Genome_size ORF_number
## 4023.051 3831.273
Ainsi, la matrice centrée s’obtient :
= apply(genomes, 2, function(x) x-mean(x)) %>%
Uc
as.matrixhead(Uc)
## Genome_size ORF_number
## Aaci -1005.0505 -799.272727
## AbauD01 -119.0505 9.727273
## Acap 103.9495 -339.272727
## Acel -1580.0505 -1565.272727
## Acfe -1865.0505 -1739.272727
## Amir 4224.9495 3345.727273
Les moyennes sont à 0 :
apply(Uc, 2, mean)
## Genome_size ORF_number
## 1.609487e-13 -2.251392e-13
1.3.3 Réduction
Pour la normalisation (des variables), on calcule les écart-types (au niveau de la population) soit la formule \(\sigma = \sqrt{ \frac{\sum_{i=1}^n (x_i - \overline{x})^2}{n}}\) avec ici, les \(\overline{x}=0\), on obtient \(\sigma = \sqrt{ \frac{\sum_{i=1}^n (x_i \cancel{- \overline{x}})^2}{n}} = \sqrt{ \frac{\sum_{i=1}^n x_i^2}{n}}\) que l’on peut écrire sous forme matricielle : \(\sqrt{\frac{1}{n}X^tX}\) :
= function(x) sqrt( 1/length(x) * t(x) %*% x) sd.n
Appliqué à chacune des variables
apply(Uc, 2, sd.n )
## Genome_size ORF_number
## 2189.434 1893.465
$scale acp
## Genome_size ORF_number
## 2189.434 1893.465
Ainsi, la matrice (déjà centrée) réduite s’obtient :
= t( t(Uc) / acp$scale )
Ucn head(Ucn)
## Genome_size ORF_number
## Aaci -0.45904579 -0.422121681
## AbauD01 -0.05437501 0.005137286
## Acap 0.04747779 -0.179180859
## Acel -0.72167074 -0.826670964
## Acfe -0.85184136 -0.918565970
## Amir 1.92969934 1.766986379
Les écarts-types sont de 1 :
apply(Ucn, 2, sd.n )
## Genome_size ORF_number
## 1 1
1.3.4 Variance-covariance
= 1/nrow(Ucn) * t(Ucn) %*% Ucn
mcov mcov
## Genome_size ORF_number
## Genome_size 1.000000 0.956533
## ORF_number 0.956533 1.000000
1.3.5 Vecteurs propres et valeurs propres
= eigen(mcov) e
Les vecteurs propres sont dans loadings
:
$vectors e
## [,1] [,2]
## [1,] 0.7071068 -0.7071068
## [2,] 0.7071068 0.7071068
$loadings acp
##
## Loadings:
## Comp.1 Comp.2
## Genome_size 0.707 0.707
## ORF_number 0.707 -0.707
##
## Comp.1 Comp.2
## SS loadings 1.0 1.0
## Proportion Var 0.5 0.5
## Cumulative Var 0.5 1.0
Et les valeurs propres dans sdev
(à mettre au carré pour
avoir les variances) :
$values e
## [1] 1.95653303 0.04346697
$sdev**2 acp
## Comp.1 Comp.2
## 1.95653303 0.04346697
Les proportions de variances :
$values / sum(e$values) e
## [1] 0.97826652 0.02173348
$sdev**2 / sum(acp$sdev**2) acp
## Comp.1 Comp.2
## 0.97826652 0.02173348
summary(acp)
## Importance of components:
## Comp.1 Comp.2
## Standard deviation 1.3987612 0.20848734
## Proportion of Variance 0.9782665 0.02173348
## Cumulative Proportion 0.9782665 1.00000000
1.3.6 Projection dans le nouveau repère
Il reste à calculer les coordonnées des individus dans le nouveau repère en utilisant les combinaisons linéaires (des variables de départ) fournie par les vecteurs propres.
Pour le 1er individu surla 1ère composante :
1,] Ucn[
## Genome_size ORF_number
## -0.4590458 -0.4221217
Avec le 1er vecteur :
$vectors[,1] e
## [1] 0.7071068 0.7071068
Sa coordonnée sur le 1er axe est donc : -0.4590458 * 0.7071068 + -0.4221217 * 0.7071068 = -0.6230795
Et donc pour tous les individus :
= Ucn %*% e$vectors
Ucnp head(Ucnp)
## [,1] [,2]
## Aaci -0.62307950 0.02610929
## AbauD01 -0.03481633 0.04208155
## Acap -0.09312813 -0.16027187
## Acel -1.09484292 -0.07424637
## Acfe -1.25186703 -0.04718142
## Amir 2.61395154 -0.11505544
head(acp$scores)
## Comp.1 Comp.2
## Aaci -0.62307950 -0.02610929
## AbauD01 -0.03481633 -0.04208155
## Acap -0.09312813 0.16027187
## Acel -1.09484292 0.07424637
## Acfe -1.25186703 0.04718142
## Amir 2.61395154 0.11505544
Visualisation
par(mfrow=c(1,2))
plot(acp$scores[,1], -acp$scores[,2], xlab='PC1', ylab='PC2', pch="", main='ACP princomp')
text(acp$scores[,1], -acp$scores[,2], labels=1:nrow(genomes), cex=.4)
abline(h=0, lty="dashed")
abline(v=0, lty="dashed")
plot(Ucnp, , xlab='PC1', ylab='PC2', pch="", main='ACP pas à pas')
text(Ucnp, labels=1:nrow(genomes), cex=.4)
abline(h=0, lty="dashed")
abline(v=0, lty="dashed")
2 Application sur le jeu de données iris
Dans ce TP, nous utilisons le jeu de données sur les iris. Description sur wikipedia http://en.wikipedia.org/wiki/Iris_flower_data_set
Et dans R, avec la commande
?iris
Chargement des données
%>%
iris as_tibble
Bien sûr, on se posera certainement les questions habituelles :
combien de variables ? de quelles natures ? combien d’individus ? pour
chaque variétés, … Pour cela, ggpairs
permet un apperçu
rapide (s’il n’y a pas trop de variables) :
library(GGally)
ggpairs(iris, progress=F, aes(color=Species, alpha=.2))
Réaliser une ACP sur le jeu de données iris pas à
pas (sans utiliser princomp
) et discuter les résultats
obtenus.
On factorise le code précédent sous forme de fonction :
= function(df, normalize=FALSE) {
pca = nrow(df)
n # centered
= as.matrix( apply(df, 2, function(x) x-mean(x)) )
U # normalized if asked
= rep(1, ncol(df))
scale if (normalize) {
= apply(U, 2, function(x) sqrt( 1/length(x) * t(x) %*% x))
scale = t( t(U)/scale )
U
}# variance-covariance & eigen
= 1/n * t(U) %*% U
mcov = eigen(mcov)
e names(e$values) = paste0('Comp.', 1:length(e$values))
colnames(e$vectors) = paste0('Comp.', 1:length(e$values))
rownames(e$vectors) = colnames(df)
# projection
= U %*% e$vectors
Up colnames(Up) = paste0('Comp.', 1:length(e$values))
# format results as a list for returning a princomp list
= list(
res sdev = sqrt(e$values),
loadings = e$vectors,
center = apply(df, 2, mean),
scale = scale,
n.obs = n,
scores = Up,
call = sys.call()
)class(res) = c('princomp')
return(res)
}
= pca(iris[,-5], normalize = T) acp.iris
On devait obtenir ceci pour une ACP normée:
summary(acp.iris)
## Importance of components:
## Comp.1 Comp.2 Comp.3 Comp.4
## Standard deviation 1.7083611 0.9560494 0.38308860 0.143926497
## Proportion of Variance 0.7296245 0.2285076 0.03668922 0.005178709
## Cumulative Proportion 0.7296245 0.9581321 0.99482129 1.000000000
Discuter de la variance portée par les axes.
Illustrations
library(factoextra)
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
fviz_screeplot(acp.iris, addlabels=T)
fviz_pca_biplot(acp.iris, habillage = iris$Species, addEllipses = T, label='var')
Même chose mais sans normalisation
= pca(iris[,-5], normalize=F) acp.iris
On devait obtenir ceci :
summary(acp.iris)
## Importance of components:
## Comp.1 Comp.2 Comp.3 Comp.4
## Standard deviation 2.0494032 0.49097143 0.27872586 0.153870700
## Proportion of Variance 0.9246187 0.05306648 0.01710261 0.005212184
## Cumulative Proportion 0.9246187 0.97768521 0.99478782 1.000000000
Discuter de la variance portée par les axes.
Illustrations
fviz_screeplot(acp.iris, addlabels=T)
fviz_pca_biplot(acp.iris, habillage = iris$Species, addEllipses = T, label='var')
Vous pouvez factoriser le code de la partie précédente sous forme de fonction qui prendra en paramètre la matrice de données quantitatives et un paramètre indiquant si la normalisation doit être effectuée.
Pour que summary
( et les fonctions
screeplot
biplot
, …) fonctionne comme sur le
résultat de princomp
, il faut que cette fonction renvoie
une variable ayant le même type de structures de données (et de même
structure) :
class(acp)
## [1] "princomp"
names(acp)
## [1] "sdev" "loadings" "center" "scale" "n.obs" "scores" "call"
= function(df, normalize=F) {
pca
...# formattage du résultat comme princomp
= list(
results
... ,call = sys.call()
)class(results) = "princomp"
return(results)
}
3 MDS : Multi-dimensional scaling
L’ACP s’utilise sur une matrice individus-variables (quantitatives).
Il arrive que l’on ne dispose pas de cette matrice mais seulement de la distance/similitude entre les individus.
On a alors une matrice dont les individus sont à la fois les lignes et les colonnes, et les valeurs correspondent à la distance/similitude entre une paire d’individus.
Simulons cette situation avec le jeu de données iris. On
utilise dist
qui par défaut utilise la distance euclidienne
:
= dist(iris[,-5])
m as.matrix(m)[1:10, 1:10] %>% round(3)
## 1 2 3 4 5 6 7 8 9 10
## 1 0.000 0.539 0.510 0.648 0.141 0.616 0.520 0.173 0.922 0.469
## 2 0.539 0.000 0.300 0.332 0.608 1.091 0.510 0.424 0.510 0.173
## 3 0.510 0.300 0.000 0.245 0.510 1.086 0.265 0.412 0.436 0.316
## 4 0.648 0.332 0.245 0.000 0.648 1.166 0.332 0.500 0.300 0.316
## 5 0.141 0.608 0.510 0.648 0.000 0.616 0.458 0.224 0.922 0.529
## 6 0.616 1.091 1.086 1.166 0.616 0.000 0.995 0.700 1.459 1.010
## 7 0.520 0.510 0.265 0.332 0.458 0.995 0.000 0.424 0.548 0.480
## 8 0.173 0.424 0.412 0.500 0.224 0.700 0.424 0.000 0.787 0.332
## 9 0.922 0.510 0.436 0.300 0.922 1.459 0.548 0.787 0.000 0.557
## 10 0.469 0.173 0.316 0.316 0.529 1.010 0.480 0.332 0.557 0.000
MDS avec la commande cmdscale
:
par(mfrow=c(1,2))
= cmdscale(m)
mds head(mds)
## [,1] [,2]
## [1,] -2.684126 0.3193972
## [2,] -2.714142 -0.1770012
## [3,] -2.888991 -0.1449494
## [4,] -2.745343 -0.3182990
## [5,] -2.728717 0.3267545
## [6,] -2.280860 0.7413304
plot(mds, col=iris$Species, pch=19, main='MDS')
plot(acp.iris$scores[,1], -acp.iris$scores[,2], col=iris$Species, pch=19, main='ACP')
Visualisation
library(gridExtra)
##
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
##
## combine
= tibble(Dim1=mds[,1], Dim2= mds[,2], Species = iris$Species) %>%
mds.p ggplot(aes(x=Dim1, y=Dim2, color = Species, shape=Species)) +
geom_point(show.legend=F) +
theme_light() +
ggtitle('MDS')
= tibble(PC1=acp.iris$scores[,1], PC2= -acp.iris$scores[,2], Species = iris$Species) %>%
acp.p ggplot(aes(x=PC1, y=PC2, color = Species, shape=Species)) +
geom_point() +
theme_light() +
ggtitle('ACP')
grid.arrange(mds.p, acp.p, ncol=2)
4 Pour aller plus loin
Pour la réduction de dimensions et la visualisation, on pourra s’intéresser à d’autres méthodes selon les données et leur nature
- qualitatives : Analyse factorielle des correspondances (AFC), et analyse des correspondances multiples (ACM)
- UMAP : données volumimneuses, pseudo-distances locales