IX. Analyse factorielle des correspondances

Auteur·rice

Guillaume Guex

Dans ce TP, nous allons effectuer une Analyse factorielle des correspondances (AFC) dans un cadre d’analyse des données textuelles, c’est-à-dire en utilisant la matrice documents-termes.

L’AFC est très utilisée dans ce cadre, en particulier dans le monde francophone grâce à l’impulsion de Jean-Paul Benzécri. Elle permet de produire une visualisation pertinente des proximités entre les différents documents d’un corpus (vis-à-vis de leur utilisation du vocabulaire) et permet l’affichage simultané des documents et des mots, ce qui aide dans l’interprétation des positions des documents.

Nous allons utiliser ici la librairie FactoMineR (http://factominer.free.fr/index_fr.html), qui possède de nombreuses méthodes d’analyses factorielles. La librairie factoextra possède plusieurs fonctions utiles dans l’affichage des résultats de FactoMineR.

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readtext)
library(quanteda)
Package version: 4.4
Unicode version: 14.0
ICU version: 71.1
Parallel computing: disabled
See https://quanteda.io for tutorials and examples.

Attachement du package : 'quanteda'

L'objet suivant est masqué depuis 'package:readtext':

    texts
library(FactoMineR)
library(factoextra)
Welcome to factoextra!
Want to learn more? See two factoextra-related books at https://www.datanovia.com/en/product/practical-guide-to-principal-component-methods-in-r/
library(plotly)
Warning: le package 'plotly' a été compilé avec la version R 4.6.1

Attachement du package : 'plotly'

L'objet suivant est masqué depuis 'package:ggplot2':

    last_plot

L'objet suivant est masqué depuis 'package:stats':

    filter

L'objet suivant est masqué depuis 'package:graphics':

    layout

1 Pré-traitements

1.1 Chargement et création de l’objet tokens

Encore une fois, nous allons charger notre corpus avec readtext() :

texts_df = readtext("data/manifesto/*.txt")

Nous allons créer l’objet tokens (à partir de l’objet corpus) quanteda en enlevant la ponctuation, les symboles, etc. :

corpus_obj = corpus(texts_df)
tokens_obj = tokens(corpus_obj, remove_punct=T, remove_symbols=T, 
                    remove_numbers=T, remove_url=T, 
                    remove_separators=T)

On sépare les tokens par apostrophes :

tokens_obj = tokens_obj %>%
  tokens_split("'") %>%
  tokens_split("’")

Et on enlève les stopwords :

stopwords = readLines("data/stopwords/frenchST.txt")
tokens_obj = tokens_obj %>%
  tokens_remove(stopwords)

1.2 Création de la matrice document-terme

L’objet de type document-feature (document-terme) se crée avec la fonction dfm() :

dfm_obj = dfm(tokens_obj)

On convertit cet objet en dataframe R :

dfm_df = convert(dfm_obj, to="data.frame")

La première colonne contient les noms des documents, on l’utilise pour créer les noms des lignes (et enlever cette colonne) :

dfm_df = dfm_df %>%
  column_to_rownames("doc_id")

Cette matrice est maintenant purement numérique et peut être utilisée dans les fonctions de FactoMineR.

2 L’AFC

2.1 La fonction CA()

L’analyse factorielle des correspondances s’effectue avec la fonction CA() (“correspondence analysis”) de FactoMineR. Par défaut, cette fonction va afficher automatiquement les graphiques résultants. On peut désactiver cette option avec graph=F :

ca_res = CA(dfm_df, graph=F)

Le retour de cette fonction possède plusieurs éléments, que l’on peut voir avec names(), ou même en affichant la variable du retour :

ca_res
**Results of the Correspondence Analysis (CA)**
The row variable has  9  categories; the column variable has 7674 categories
The chi square of independence between the two variables is equal to 72144.29 (p-value =  3.094457e-186 ).
*The results are available in the following objects:

   name              description                   
1  "$eig"            "eigenvalues"                 
2  "$col"            "results for the columns"     
3  "$col$coord"      "coord. for the columns"      
4  "$col$cos2"       "cos2 for the columns"        
5  "$col$contrib"    "contributions of the columns"
6  "$row"            "results for the rows"        
7  "$row$coord"      "coord. for the rows"         
8  "$row$cos2"       "cos2 for the rows"           
9  "$row$contrib"    "contributions of the rows"   
10 "$call"           "summary called parameters"   
11 "$call$marge.col" "weights of the columns"      
12 "$call$marge.row" "weights of the rows"         

Nous verrons plus loin comment utiliser ces différentes valeurs. Pour le moment voyons comment afficher le biplot à partir de cette sortie.

2.2 Le biplot

Pour tracer le biplot, il suffit d’appliquer la fonction plot() sur notre objet de sortie :

plot(ca_res)

Le problème est qu’il y a beaucoup trop de mots pour y voir quelque chose. On utilise les paramètres selectCol="contrib 30" (sélectionne les 30 mots avec la plus grande contribution aux axes affichés) et unselect=1 (rend invisibles les points non-sélectionnés) pour afficher un sous-ensemble de points (on désactive également l’auto-positionnement des labels avec autoLab="no" et on diminue la taille de la police avec cex=0.8) :

plot(ca_res, selectCol="contrib 30", unselect=1, autoLab="no", cex=0.8)

Notez que plotly peut être utilisé pour explorer le graphique :

ca_plot = plot(ca_res, selectCol="contrib 30", unselect=1, 
               autoLab="no", cex=0.8)
ggplotly(ca_plot) %>%
  style(marker=list(opacity=0))

On peut aussi utiliser la fonction fviz_ca_biplot() de factoextra :

fviz_ca_biplot(ca_res, repel=T, select.col=list(contrib=30))

D’autres dimensions peuvent également être affichées :

fviz_ca_biplot(ca_res, axes=c(3, 4), repel=T, select.col=list(contrib=30))

Mais ici, le document du Parti communiste français, par son particularisme, semble absorber toute l’inertie.

2.3 Lignes ou colonnes supplémentaires

Comme le document du Parti communiste français est si particulier, on peut éventuellement refaire une AFC en le mettant comme ligne supplémentaire. Attention, on doit ajouter une petite quantité à la matrice pour éviter des colonnes vides :

ca_noPCF_res =  CA(dfm_df + 1e-50, graph=F, row.sup=6)

On peut alors refaire nos graphiques. La ligne correspondant au Parti communiste sera toujours affichée, mais n’a pas été utilisée pour définir les axes :

fviz_ca_biplot(ca_noPCF_res, repel=T, select.col=list(contrib=30))

Dimensions 3-4 :

fviz_ca_biplot(ca_noPCF_res, axes=c(3, 4), repel=T, select.col=list(contrib=30))

2.4 Extraction des mesures

Plusieurs mesures peuvent être obtenues à partir de la sortie de CA. Par exemple, le pourcentage de variance expliquée par chaque axe :

ca_noPCF_res$eig
      eigenvalue percentage of variance cumulative percentage of variance
dim 1  0.3801665              19.008089                          19.00809
dim 2  0.3432619              17.162885                          36.17097
dim 3  0.2948976              14.744699                          50.91567
dim 4  0.2740065              13.700155                          64.61583
dim 5  0.2648177              13.240720                          77.85655
dim 6  0.2558857              12.794127                          90.65068
dim 7  0.1869888               9.349324                         100.00000

Le même résultat avec fviz_eig() :

fviz_eig(ca_noPCF_res)

La qualité de la représentation des lignes ou colonnes sur chaque axe s’exprime avec les cosinus carrés :

round(ca_noPCF_res$row$cos2, 2)
                                   Dim 1 Dim 2 Dim 3 Dim 4 Dim 5
en_marche_2017.txt                  0.00  0.50  0.39  0.07  0.00
europe_ecologie_les_verts_2017.txt  0.02  0.00  0.04  0.12  0.81
france_insoumise_2017.txt           0.59  0.29  0.08  0.02  0.01
front_national_2017.txt             0.00  0.04  0.22  0.10  0.01
mouvement_democratique_2017.txt     0.01  0.02  0.32  0.36  0.18
parti_radical_de_gauche_2017.txt    0.79  0.14  0.05  0.01  0.00
parti_socialiste_2017.txt           0.00  0.00  0.00  0.00  0.00
udi_republicains_2017.txt           0.00  0.28  0.04  0.40  0.03

On peut les afficher avec fviz_cos2() :

fviz_cos2(ca_noPCF_res, choice="row", axes=c(1, 2)) 

Les contributions des lignes ou des colonnes pour chaque axes s’obtiennent avec :

head(round(ca_noPCF_res$col$contrib, 4), 20)
             Dim 1  Dim 2  Dim 3  Dim 4  Dim 5
emmanuel    0.0001 0.0967 0.0024 0.0499 0.0040
macron      0.0063 0.0299 0.0061 0.0386 0.0009
président   0.0095 0.0014 0.0142 0.0013 0.0101
retrouver   0.0053 0.0362 0.0038 0.0028 0.0006
esprit      0.0184 0.0601 0.0359 0.0021 0.0042
conquête    0.0231 0.0191 0.0736 0.0064 0.0002
bâtir       0.0005 0.0339 0.0059 0.0618 0.0413
france      0.0596 0.1806 0.2121 0.0017 0.4797
nouvelle    0.0121 0.0111 0.0000 0.0018 0.0033
contrat     0.0015 0.1938 0.0862 0.1957 0.0014
nation      0.0273 0.0189 0.0119 0.0035 0.0303
programme   0.0003 0.0546 0.0262 0.0088 0.0001
a-t-il      0.0137 0.0013 0.0320 0.0008 0.0016
construit   0.0001 0.0458 0.0485 0.0102 0.0006
vivre       0.0104 0.0214 0.0333 0.0001 0.0018
travail     0.0400 0.0987 0.0343 0.0054 0.0084
inventer    0.0001 0.0915 0.0969 0.0204 0.0012
nouvelles   0.0234 0.0040 0.0029 0.0034 0.0302
protections 0.0029 0.0301 0.0130 0.0095 0.0001
sommes      0.0034 0.1642 0.0301 0.0029 0.0038

Ou graphiquement avec fviz_contrib() :

fviz_contrib(ca_noPCF_res, choice="col", top=20, axes=1:3)

D’ailleurs, utilisons ces dernières pour extraire les 50 mots avec la contribution la plus grande sur les trois premiers axes :

top50_words = ca_noPCF_res$col$contrib %>%
  as_tibble(rownames=NA) %>%
  rownames_to_column() %>%
  mutate(contrib123=`Dim 1` + `Dim 2` + `Dim 3`) %>%
  top_n(50, contrib123) %>%
  .$rowname
top50_words
 [1] "france"            "contrat"           "savons"           
 [4] "écologique"        "employeurs"        "réduirons"        
 [7] "euros"             "nets"              "mois"             
[10] "augmenterons"      "ouvrirons"         "assurance-chômage"
[13] "ferons"            "développerons"     "français"         
[16] "changer"           "réussissent"       "voulons"          
[19] "entrepreneurs"     "supprimerons"      "mettrons"         
[22] "créerons"          "paieront"          "tpe"              
[25] "créer"             "devront"           "dispositif"       
[28] "donnerons"         "construirons"      "proposerons"      
[31] "renforcerons"      "réaliser"          "étendrons"        
[34] "europe"            "devons"            "erreur"           
[37] "parce"             "oeuvre"            "mesures"          
[40] "proposons"         "onu"               "suivantes"        
[43] "refuser"           "renforcement"      "soutien"          
[46] "propose"           "immigration"       "maintenir"        
[49] "majorité"          "radicaux"         

Il nous est aussi possible d’extraire les coordonnées, afin de construire notre graphique manuellement :

doc_coord = rbind(ca_noPCF_res$row$coord, 
                  ca_noPCF_res$row.sup$coord) # Les coordonnées supplémentaires
word_coord = ca_noPCF_res$col$coord
top50_coord = word_coord[rownames(word_coord) %in% top50_words, ]

Ici, nous allons faire un graphique en 3D avec plotly :

plt_3d = plot_ly(type = "scatter3d",  mode = "markers") %>%
  add_trace(x=doc_coord[, 1], y=doc_coord[, 2], z=doc_coord[, 3],
            mode="text", text=rownames(doc_coord), 
            textfont=list(color="blue", size=15)) %>%
  add_trace(x=top50_coord[, 1], y=top50_coord[, 2], z=top50_coord[, 3],
            mode="text", text=rownames(top50_coord), 
            textfont=list(color="red"))
plt_3d