Travaux pratiques - Echantillonnage. Analyse en composantes principales

(la nouvelle version de cette séance, utilisant l’API DataFrame)

Références externes utiles :

Les exemples ci-dessous reprennent, en partie, ceux des documentations en ligne.

Echantillonnage

Nous travaillerons lors de cette séance sur des données Spambase Data Set issues de l’archive de l’UCI. Vous trouverez sur le site de l’UCI des explications plus détaillées concernant ces données, regardez-les, notamment la signification des variables et les classes auxquelles appartiennent les données. Pour charger ces données, ouvrez une fenêtre terminal et entrez les commandes suivantes :

[cloudera@quickstart ~]$ mkdir -p tpacp/data
[cloudera@quickstart ~]$ cd tpacp/data
[cloudera@quickstart data]$ wget https://archive.ics.uci.edu/ml/
         machine-learning-databases/spambase/spambase.data --no-check-certificate

Dans le fichier spambase.data, chaque ligne contient un vecteur de 57 valeurs float et ensuite une étiquette de classe (0 ou 1). Pour lire ce fichier et créer la structure de données qui sera utilisée dans la suite, lancez spark-shell dans une nouvelle fenêtre terminal. Entrez

[cloudera@quickstart ~]$ export PATH="$PATH:/usr/lib/spark/bin"
[cloudera@quickstart ~]$ cd tpacp
[cloudera@quickstart tpacp]$ spark-shell

et ensuite, dans l’interpréteur de commandes Spark :

scala> import org.apache.spark.mllib.util.MLUtils
scala> import org.apache.spark.mllib.linalg.Vectors
scala> import org.apache.spark.rdd.RDD

scala> val spambase = sc.textFile("file:///home/cloudera/tpacp/data/spambase.data")
scala> val lignes = spambase.map(l => l.split(",").map(_.toDouble)).map(l =>
                    (l.last, Vectors.dense(l.take(57)))).cache()
scala> lignes.count()        // pour vérifier la création de ce RDD de 4601 lignes

lignes est un RDD de type RDD[K,V] qui contiendra les données dont nous nous servirons dans la suite, nous le rendons donc persistant avec cache().

Question :

Expliquez comment le RDD lignes est obtenu à partir du RDD spambase.

Echantillonnage simple

Nous obtiendrons d’abord deux RDD, lignesEch1 et lignesEch2, qui sont des échantillons du RDD lignes avec deux valeurs très différentes pour fraction (taux de sélection) :

scala> val lignesEch1 = lignes.sample(false, 0.5)
scala> lignesEch1.count()
scala> val lignesEch2 = lignes.sample(false, 0.1)
scala> lignesEch2.count()

Question :

Que pouvez-vous constater en comparant le nombre de lignes de lignesEch1 ou lignesEch2 et celui de lignes multiplié par la valeur de fraction correspondante ? Expliquez.

Nous examinerons maintenant des statistiques simples calculées sur les colonnes des 3 RDD :

scala> import org.apache.spark.mllib.linalg.Vector
scala> import org.apache.spark.mllib.stat.{MultivariateStatisticalSummary, Statistics}

scala> val summary = Statistics.colStats(lignes.values)
scala> val summary1 = Statistics.colStats(lignesEch1.values)
scala> val summary2 = Statistics.colStats(lignesEch2.values)
scala> println(summary.mean)
scala> println(summary1.mean)
scala> println(summary2.mean)

Question :

Quel constat faites-vous en comparant les moyennes calculées pour une même variable (colonne) sur lignes et sur les deux échantillons ? Expliquez.

Echantillonnage stratifié

Pour un RDD de type (clé,valeur), l’échantillonnage stratifié permet de fixer le taux d’échantillonnage pour chaque valeur de la clé. La méthode sampleByKey (voir PairRDDFunctions dans la documentation API Spark en Scala) respecte de façon approximative les taux d’échantillonnage indiqués. La méthode sampleByKeyExact respecte de façon plus stricte ces taux de sélection mais est plus coûteuse.

scala> import org.apache.spark.rdd.PairRDDFunctions

scala> val fractions = Map(0.0 -> 0.5, 1.0 -> 0.5)    // Map[K, Double]

scala> val lignesEchStratApprox = lignes.sampleByKey(false, fractions)
scala> val lignesEchStratExact = lignes.sampleByKeyExact(false, fractions)

scala> lignes.countByKey()
scala> lignesEch1.countByKey()
scala> lignesEchStratApprox.countByKey()
scala> lignesEchStratExact.countByKey()

Question :

Que constatez-vous concernant le rapport entre les nombres de valeurs pour chaque clé (chaque « strate ») ? Expliquez.

Analyse en composantes principales (ACP)

MLlib donne la possibilité de calculer directement les k premières composantes principales pour un ensemble d’observations. Les N observations sont représentées par une matrice distribuée de type RowMatrix dont le nombre de lignes est égal au nombre d’observations (N) et le nombre colonnes au nombre de variables (noté ici par n). Ces k premières composantes principales sont les vecteurs propres d’une ACP centrée mais non réduite appliquée aux observations. Les composantes principales sont retournées dans une matrice locale DenseMatrix de n lignes (le nombre de variables initiales) et k colonnes (le nombre de composantes principales demandées). Les composantes principales sont triées en ordre décroissant des valeurs propres correspondantes mais ces valeurs propres ne sont pas retournées.

Dans la suite, nous réaliserons des ACP sur les données Spambase Data Set, ainsi que sur un échantillon de ces données et sur des données aléatoires générées suivant une loi normale multidimensionnelle isotrope (même variance dans toutes les directions). Nous visualiserons les résultats.

Réaliser l’ACP

L’ACP centrée et normée sur les données lues dans le RDD lignes :

scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
scala> import org.apache.spark.mllib.feature.StandardScaler

// Obtenir un RDD avec les colonnes centrées (moyenne=0) et réduites (variance=1)
scala> val centRed = new StandardScaler(withMean = true, withStd = true).fit(lignes.values)
scala> val lignesCR = centRed.transform(lignes.values)

// Obtenir la RowMatrix à partir du RDD lignesCR
scala> val matLignes: RowMatrix = new RowMatrix(lignesCR)

// Calculer les 3 premières composantes principales
scala> val matCompPrincipales = matLignes.computePrincipalComponents(3)

// Projeter les données sur les 3 premières composantes principales
scala> val projections: RowMatrix = matLignes.multiply(matCompPrincipales)

// Déterminer (a posteriori) les valeurs propres correspondantes
scala> val matSummary = projections.computeColumnSummaryStatistics()

Question :

Quelles sont les trois plus grandes valeurs propres ? Appliquez une ACP centrée mais non normée sur les données de lignes, comparez les trois plus grandes valeurs propres.

L’ACP centrée et normée sur l’échantillon simple lignesEch2 de lignes :

scala> val centRedEch2 = new StandardScaler(withMean = true, withStd = true).fit(lignesEch2.values)
scala> val lignesCREch2 = centRedEch2.transform(lignesEch2.values)
scala> val matLignesEch2 = new RowMatrix(lignesCREch2)
scala> val matCompPrincEch2 = matLignesEch2.computePrincipalComponents(3)
scala> val projectionsEch2 = matLignesEch2.multiply(matCompPrincEch2)
scala> val matSummaryEch2 = projectionsEch2.computeColumnSummaryStatistics()

L’ACP sur des données tridimensionnelles obtenues par tirage aléatoire suivant une loi normale isotrope :

scala> import org.apache.spark.mllib.random.RandomRDDs
scala> import org.apache.spark.mllib.random.RandomRDDs

scala> val normalRnd = RandomRDDs.normalVectorRDD(sc, 4601L, 3)
scala> val matRnd = new RowMatrix(normalRnd)

scala> val matCompPrincRnd = matRnd.computePrincipalComponents(3)
scala> val projectionsRnd = matRnd.multiply(matCompPrincRnd)
scala> val matSummaryRnd = projectionsRnd.computeColumnSummaryStatistics()

Question :

Comparez entre elles les trois premières valeurs propres pour chacune des analyses effectuées. Commentez le résultat obtenu.

Visualiser les résultats

Dans cette séance de travaux pratiques nous visualiserons les données à l’aide d’un outil simple qu’est gnuplot. Vous pouvez vous servir d’autres outils (comme matplotlib ou ggplot2) si vous les maîtrisez déjà ; il vous faudra toutefois les installer.

Vérifiez si gnuplot est présent : entrez gnuplot dans une fenêtre terminal, si le programme est installé alors vous aurez un prompt gnuplot>, quittez en entrant quit. Si gnuplot n’est pas présent sur le système il est nécessaire de l’installer. Pour cela, entrez la commande suivante dans une fenêtre terminal :

[cloudera@quickstart ~]$ sudo yum install gnuplot

Gnuplot peut être utilisé avec des commandes en ligne ou avec des scripts. Pour écrire le script de base qui permettra la visualisation, ouvrez un éditeur (par ex. KWrite) et copiez dans la fenêtre de l’éditeur le texte suivant :

set datafile separator ','
set term x11 0
set title "Projections donnees spambase"
set xlabel "CP 1"
set ylabel "CP 2"
set zlabel "CP 3"
splot "/home/cloudera/tpacp/data/acpspam/part-00000" using 1:2:3
pause -1
#
set term x11 1
set title "Projections donnees echantillon spambase"
splot "/home/cloudera/tpacp/data/acpech2/part-00000" using 1:2:3
pause -1
#
set term x11 2
set title "Projections donnees aleatoires"
splot "/home/cloudera/tpacp/data/acprnd/part-00000" using 1:2:3
pause -1

Enregistrez-le dans un fichier visualisation.gp dans le répertoire /home/cloudera/tpacp

Il est maintenant nécessaire d’enregistrer sur disque les données projetées, dans un format adapté à la lecture par gnuplot : un vecteur par lignes, composantes séparées par un espace (ou une tabulation). Pour cela, entrez les commandes suivantes :

// Enregistrement des projections des données spambase sur les 3 premières CP
scala> val projectionsTxt = projections.rows.map(l => l.toString.filter(c => c != '[' & c != ']'))
scala> projectionsTxt.saveAsTextFile("file:///home/cloudera/tpacp/data/acpspam")

// Enregistrement des projections des données de l'échantillon
scala> val projectionsEch2Txt = projectionsEch2.rows.map(l => l.toString.filter(c => c != '[' & c != ']'))
scala> projectionsEch2Txt.saveAsTextFile("file:///home/cloudera/tpacp/data/acpech2")

// Enregistrement des projections des données aléatoires
scala> val projectionsRndTxt = projectionsRnd.rows.map(l => l.toString.filter(c => c != '[' & c != ']'))
scala> projectionsRndTxt.saveAsTextFile("file:///home/cloudera/tpacp/data/acprnd")

Ces commandes ont créé des sous-répertoires acpspam, acpech2 et acprnd dans le répertoire /home/cloudera/tpacp/data. Chacun de ces sous-répertoires contient un fichier avec les données correspondantes, portant le nom part-00000.

Si on supprime la ligne set datafile separator ',' du fichier visualisation.gp ci-dessus, alors il faut ajouter .replace(',', ' ') comme dernière opération du map pour obtenir le bon format dans projectionsTxt, projectionsEch2Txt et projectionsRndTxt.

Pour visualiser les résultats, entrez les commandes suivantes dans une fenêtre terminal :

[cloudera@quickstart ~]$ gnuplot
gnuplot> load "/home/cloudera/tpacp/visualisation.gp"

En appuyant sur Entrée, vous ouvrez successivement chacun des trois fichiers, avec la possibilité de changer de point de vue avec la souris (clic gauche).