[fr] Utilisation de rainette

Julien Barnier

2020-05-09

Installation

Le package n’étant pas encore disponible sur le CRAN, il s’installe depuis GitHub via l’extension remotes :

remotes::install_github("juba/rainette")

Préparation du corpus

Importation

La première étape consiste à importer votre corpus dans R. Vous pouvez utiliser un corpus au format tm ou quanteda par exemple, ou bien utiliser la fonction import_corpus_iramuteq pour importer directement un corpus depuis un fichier texte au format Iramuteq. Dans ce cas vous obtiendrez un objet de type corpus du package quanteda. Les métadonnées éventuelles présentes dans le fichier (variables étoilées) sont importées en tant que métadonnées du corpus (variables de docvars(corpus)).

Pour ce document, on va importer le texte du Manifeste du parti communiste (Karl Marx et Friedrich Engels, 1848, source wikisource). Celui-ci est placé dans un fichier texte au format Iramuteq fourni avec rainette. Le corpus est composé de quatre documents, un document par partie de l’ouvrage.

library(rainette)
library(quanteda)
## Import du corpus
fichier <- system.file("extdata", "manifeste_pc.txt", package = "rainette")
corpus <- import_corpus_iramuteq(fichier)

On peut vérifier que notre corpus est bien constitué de 4 documents (l’introduction et les trois parties principales), et d’une variable de métadonnée :

corpus
## Corpus consisting of 4 documents and 1 docvar.
## text1 :
## "Un spectre hante l'Europe, le spectre du communisme. Toutes ..."
## 
## text2 :
## "L'histoire de toute société jusqu'à nos jours n'a été que l'..."
## 
## text3 :
## "Quelle est la position des communistes vis-à-vis des proléta..."
## 
## text4 :
## "Par leur position historique, les aristocraties françaises e..."

Cette variable de métadonnée est justement la partie correspondant au texte :

docvars(corpus)
##   partie
## 1  intro
## 2      I
## 3     II
## 4    III

Découpage en segments

La méthode Reinert de classification s’applique en général à des segments de texte relativement courts (appelés uce, unités de contexte élémentaires), et non à des textes longs. Une première étape consiste donc à découper chaque texte du corpus en segments via la fonction split_segments. Ici on découpe en segments d’environ 40 mots (l’algorithme essaie de tenir compte de la ponctuation pour, par exemple, placer les césures entre des phrases ou au niveau d’une virgule).

corpus <- split_segments(corpus, segment_size = 40)

Notre corpus est désormais constitué de 318 segments et 2 variables de métadonnées :

corpus
## Corpus consisting of 278 documents and 2 docvars.
## text1_1 :
## "Un spectre hante l'Europe, le spectre du communisme. Toutes ..."
## 
## text1_2 :
## "Quelle est l'opposition qui, à son tour, n'a pas relancé à s..."
## 
## text1_3 :
## "à la face du monde entier, leur manière de voir, leurs buts ..."
## 
## text2_1 :
## "L'histoire de toute société jusqu'à nos jours n'a été que l'..."
## 
## text2_2 :
## "une guerre qui finissait toujours, ou par une transformation..."
## 
## text2_3 :
## "Dans la Rome antique, nous trouvons des patriciens, des chev..."
## 
## [ reached max_ndoc ... 272 more documents ]

Si on regarde les nouvelles métadonnées, on peut voir que la variable partie a été dupliquée pour chaque segment correspondant, et une nouvelle variable segment_source indique le document d’origine du segment.

head(docvars(corpus))
##   partie segment_source
## 1  intro          text1
## 2  intro          text1
## 3  intro          text1
## 4      I          text2
## 5      I          text2
## 6      I          text2

On peut aussi visualiser les premiers segments de texte calculés :

texts(corpus)[1:2]
##                                                                                                                                                                                                                                                                                                                                                   text1_1 
## "Un spectre hante l'Europe, le spectre du communisme. Toutes les puissances de la vieille Europe se sont unies en une Sainte-Alliance pour traquer ce spectre : le Pape et le Czar, Metternich et Guizot, les radicaux de France et les policiers d'Allemagne.\nQuelle est l'opposition que n'ont pas accusée de communisme ses adversaires au pouvoir ?" 
##                                                                                                                                                                                                                                                                                                                                                   text1_2 
##                       "Quelle est l'opposition qui, à son tour, n'a pas relancé à ses adversaires de droite ou de gauche l'épithète flétrissante de communiste ?\nDeux choses ressortent de ces faits :\n1° Déjà le communisme est reconnu par toutes les puissances d'Europe comme une puissance ;\n2° Il est grand temps que les communistes exposent,"

Calcul et traitement de la matrice termes-documents

L’étape suivante est de calculer la matrice termes-documents (dtm), grand tableau numérique avec les documents en lignes, les mots en colonnes, et le nombre d’occurrences de chaque mot dans chaque document comme valeurs.

Notre corpus étant au format quanteda, on va utiliser les fonction de cette extension.

D’abord on calcule la dtm en convertissant le texte en minuscules, et en supprimant ponctuation, nombres, et les mots-outils français les plus courants :

dtm <- dfm(corpus, remove = stopwords("fr"), tolower = TRUE, remove_punct = TRUE, remove_numbers = TRUE)

On va ensuite procéder à la racinisation des termes, puis on va supprimer les termes apparaissant dans moins de 3 segments :

dtm <- dfm_wordstem(dtm, language = "french")
dtm <- dfm_trim(dtm, min_termfreq = 3)

De nombreux autres traitements seraient possibles ou préférables, mais on se contentera de cette matrice pour cet exemple.

Classification simple

Une fois notre matrice prête, on peut procéder à une première forme de classification : une classification descendante hiérarchique simple, calculée avec la fonction rainette. Ici on va lui passer plusiurs arguments : le nombre maximal de classes souhaitées (k = 5) et le nombre minimal de termes pour qu’une classe soit découpée en deux à l’étape suivante de la classification (min_split_members = 10).

L’argument min_uc_size, lui, indique le nombre minimal de mots par segment. En effet, lors du calcul de la dtm, certaines formes (mots-outils, mots trop peu fréquents) ont été supprimées, nos segments peuvent donc varier en taille (entendue comme le nombre de mots encore présents). Avec min_uc_size = 10, les segments comportant moins de 10 formes sont regroupés avec le segment suivant jusqu’à atteindre la taille minimale souhaitée.

res <- rainette(dtm, k = 5, min_uc_size = 10, min_split_members = 10)

L’objet résultat ne nous dit pas grand chose en lui-même :

res
## 
## Call:
## rainette(dtm = dtm, k = 5, min_uc_size = 10, min_split_members = 10)
## 
## Cluster method   : reinert 
## Number of objects: 5

Pour faciliter l’exploration des résultats, rainette propose une interface interactive sous la forme d’un gadget shiny, qui peut être lancée avec la fonction rainette_explor :

rainette_explor(res, dtm)

L’interface devrait ressembler à quelque chose comme ça :

Vous pouvez modifier le nombre de classes, la statistique utilisée pour le calcul des termes caractéristiques, etc. Par défaut, les graphiques sous chaque classe vous indiquent les termes les plus caractéristiques du groupe positivement (en bleu) ou négativement (en rouge) selon la statistique considérée.

À noter que vous pouvez aussi opter pour des graphiques sous forme de nuages de mots :

Cette interface vous permet d’expérimenter librement sur le nombre de classes et leur interprétation. Vous pouvez à tout moment cliquer sur le bouton Get R code pour obtenir le code R correspondant au graphique actuellement affiché, ainsi que la commande cutree_rainette qui vous permet de récupérer les groupes d’appartenance de chaque document du corpus, là aussi selon le nombre de groupes actuellement affichés.

## Clustering description plot
rainette_plot(res, dtm, k = 5, type = "bar", n_terms = 20, free_scales = FALSE,
    measure = "chi2", show_negative = "TRUE", text_size = 11)
## Groups
cutree_rainette(res, k = 5)

Vous pouvez par exemple utiliser l’appel de cutree_rainette pour ajouter comme nouvelle métadonnée du corpus le groupe d’appartenance pour la classification en 5 classes :

corpus$group <- cutree_rainette(res, k = 5)
head(docvars(corpus))
##   partie segment_source group
## 1  intro          text1     2
## 2  intro          text1     4
## 3  intro          text1     5
## 4      I          text2     4
## 5      I          text2     4
## 6      I          text2     3

Vous pouvez aussi récupérer directement les statistiques de spécificité de chaque groupe à l’aide de rainette_stats :

rainette_stats(corpus$group, dtm, n_terms = 5)
## [[1]]
## # A tibble: 5 x 6
##   feature       chi2        p n_target n_reference sign    
##   <chr>        <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 libert        42.2 8.28e-11       11           3 positive
## 2 l'abolit      41.3 1.32e-10       10           2 positive
## 3 propriet      39.1 4.13e-10       24          31 positive
## 4 intellectuel  29.3 6.12e- 8        6           0 positive
## 5 matériel      21.0 4.58e- 6        7           3 positive
## 
## [[2]]
## # A tibble: 5 x 6
##   feature    chi2        p n_target n_reference sign    
##   <chr>     <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 femm       50.6 1.12e-12       11           0 positive
## 2 famill     44.7 2.31e-11       11           1 positive
## 3 travail    38.6 5.09e-10       22          21 positive
## 4 salari     35.2 2.94e- 9        8           0 positive
## 5 communaut  30.1 4.09e- 8        7           0 positive
## 
## [[3]]
## # A tibble: 5 x 6
##   feature     chi2             p n_target n_reference sign    
##   <chr>      <dbl>         <dbl>    <dbl>       <dbl> <fct>   
## 1 moyen       35.7 0.00000000227       23          13 positive
## 2 l'industr   28.6 0.0000000880        14           4 positive
## 3 petit       28.5 0.0000000915        13           3 positive
## 4 industriel  22.2 0.00000240          11           3 positive
## 5 commerc     19.5 0.00000999          11           4 positive
## 
## [[4]]
## # A tibble: 5 x 6
##   feature      chi2        p n_target n_reference sign    
##   <chr>       <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 class        43.0 5.47e-11       61          43 positive
## 2 prolétariat  32.9 9.75e- 9       37          21 positive
## 3 lutt         31.6 1.92e- 8       26          10 positive
## 4 propr        18.0 2.26e- 5       13           4 positive
## 5 propriet    -15.6 7.85e- 5        3          52 negative
## 
## [[5]]
## # A tibble: 5 x 6
##   feature     chi2            p n_target n_reference sign    
##   <chr>      <dbl>        <dbl>    <dbl>       <dbl> <fct>   
## 1 contr       31.7 0.0000000180       21          21 positive
## 2 français    29.2 0.0000000643       11           4 positive
## 3 littératur  24.1 0.000000909         9           3 positive
## 4 social      21.2 0.00000418         29          50 positive
## 5 allemand    19.2 0.0000116          11           8 positive

Classification double

Le deuxième type de classification proposé est une classification double : selon la méthode proposée par Max Reinert, on effectue deux classifications simples en faisant varier la taille minimale des segments, puis on “croise” les résultats de ces deux classifications pour déterminer de nouvelles classes, potentiellement plus robustes.

Une classification double utilise la fonction rainette2. Celle-ci peut se faire de deux manières. On peut d’abord effectuer les deux classifications simples, ici une avec une taille de segment minimale à 10, et une autre à 15 :

res1 <- rainette(dtm, k = 7, min_uc_size = 10, min_split_members = 10)
res2 <- rainette(dtm, k = 7, min_uc_size = 15, min_split_members = 10)

Puis on utilise rainette2 sur ces deux objets résultats, en lui indiquant le nombre maximal de classes à calculer (argument max_k) et le nombre minimal de segments par classe (argument min_members) :

res <- rainette2(res1, res2, max_k = 7, min_members = 10)

L’autre manière est d’appeler directement rainette2 sur notre matrice dtm, en lui indiquant avec les arguments uc_size1 et uc_size2 les deux tailles de segments souhaitées :

res <- rainette2(dtm, uc_size1 = 10, uc_size2 = 15, max_k = 7, min_members = 10)

L’objet résultat est un tibble contenant, pour chaque valeur de k, les partitions optimales trouvées et leurs caractéristiques. Là encore, une interface interactive est proposée pour visualiser et explorer ces résultats. Elle se lance via la fonction rainette2_explor :

rainette2_explor(res, dtm)

L’interface est très semblable à la précédente, sauf qu’il n’y a plus de dendrogramme mais à la place un diagramme en barre des effectifs des groupes. Soyez attentifs aux NA, qui représentent les segments non classés : contrairement à la classification simple, ils peuvent être assez nombreux ici.

Là encore, vous pouvez utiliser le bouton Get R code pour récupérer et copier/coller le code R permettant de reproduire le graphique affiché, et récupérer les groupes d’appartenance des segments du corpus.

Vous pouvez également utiliser rainette_stats pour récupérer les statistiques de spécificité des groupes :

groups <- cutree(res, 3)
rainette_stats(groups, dtm, n_terms = 5)
## [[1]]
## # A tibble: 5 x 6
##   feature      chi2        p n_target n_reference sign    
##   <chr>       <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 travail     -58.1 2.51e-14        4          39 negative
## 2 a           -54.0 2.00e-13        8          43 negative
## 3 propriet    -44.6 2.42e-11       12          43 negative
## 4 prolétariat  26.2 3.09e- 7       56           2 positive
## 5 famill      -19.2 1.16e- 5        0          12 negative
## 
## [[2]]
## # A tibble: 5 x 6
##   feature       chi2        p n_target n_reference sign    
##   <chr>        <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 intellectuel  47.8 4.69e-12        6           0 positive
## 2 propriet      47.6 5.19e-12       20          35 positive
## 3 l'abolit      39.9 2.68e-10        8           4 positive
## 4 libert        32.2 1.37e- 8        8           6 positive
## 5 l'éduc        30.4 3.51e- 8        5           1 positive
## 
## [[3]]
## # A tibble: 5 x 6
##   feature    chi2        p n_target n_reference sign    
##   <chr>     <dbl>    <dbl>    <dbl>       <dbl> <fct>   
## 1 femm       57.3 3.80e-14       11           0 positive
## 2 salari     39.9 2.69e-10        8           0 positive
## 3 travail    35.0 3.36e- 9       20          23 positive
## 4 communaut  34.1 5.21e- 9        7           0 positive
## 5 famill     30.2 3.92e- 8        9           3 positive

Si certains points n’ont pas été affecté à un groupe, vous pouvez utiliser rainette2_complete_groups pour les assigner au groupe le plus proche selon une méthode k-nearest neighbors :

groups_completed <- rainette2_complete_groups(dtm, groups)