Code
library(wordcloud2)
library(tidytext)
library(tidyverse)
library(scales)
library(tm)
library(syuzhet)
library(ggthemes)
Il ne faut pas toucher aux idoles : la dorure en reste aux mains.
Flaubert, Gustave. Madame Bovary (partie 3, chapitre 6)
Un nuage de mots permet de visualiser la diversité et la fréquence lexicale dans des données textuelles. Il y a plusieurs outils en ligne qui nous donnent des nuages de mots automatiques. Ici, on utilise une extension en R : c’est moins automatique, mais on aura plus de contrôle sur la figure.
Après la création de notre nuage de mots, on fera une analyse de sentiment sur la troisième partie du roman. On termine le tutoriel avec une figure qui compare les tendances de différents sentiments — c’est une opportunité de discuter quelques caractéristiques esthétiques importantes pour maximiser la clarté des tendances en question.
Voici les extensions nécessaires pour reproduire les codes ci-dessous.
Voici la séquence d’étapes : on commence par le fichier txt
(téléchargé à partir du projet Gutenberg, qui doit être nettoyé (on enlève toutes les lignes qui ne font pas partie du roman). Ensuite, on transforme le texte vers un tibble
, on renomme une colonne, et on extrait les trois parties et ses chapitres. Finalement, on compte le nombre des mots dans chaque chapitre (cette information sera utile pour le graphique plus tard). La deuxième étape ici consiste à tokeniser le texte avec la fonction unnest_tokens()
.
# La séquence des chapitres
ch = seq(1:15) |>
as.roman() |>
str_c("$")
ch = str_c("^", ch, collapse = "|")
mb = read_lines("bovary.txt") |>
as_tibble() |>
rename(ligne = value) |>
mutate(partie = str_extract(string = ligne, pattern = "\\w+\\sPARTIE") |>
str_remove_all(pattern = "\\sPARTIE")) |>
fill(partie, .direction = "down") |>
mutate(chapitre = str_extract(string = ligne, pattern = ch)) |>
fill(chapitre, .direction = "down") |>
filter(ligne != "") |>
filter(!str_detect(string = ligne, pattern = "PARTIE")) |>
filter(!str_detect(string = ligne, pattern = ch)) |>
mutate(ligne_n = row_number(),
nMots = str_count(ligne, pattern = " ") + 1) |>
mutate(totalMots = cumsum(nMots), .by = c(partie, chapitre)) |>
mutate(chapitre = chapitre |> as.roman() |> as.numeric()) |>
unnest_tokens(mot, ligne)
partie | chapitre | ligne_n | nMots | totalMots | mot |
---|---|---|---|---|---|
DEUXIÈME | 8 | 4683 | 12 | 2042 | à |
TROISIÈME | 1 | 7979 | 9 | 718 | solitude |
DEUXIÈME | 9 | 5633 | 9 | 3213 | aurait |
DEUXIÈME | 5 | 3600 | 10 | 2041 | n'embarrassent |
PREMIÈRE | 7 | 1525 | 10 | 2200 | un |
Pour notre nuage de mots, on crée un objet pour les stopwords (fr_stop
) à partir de l’extension tm
.1 Finalement, on prépare notre tableau (tibble
) pour la fonction wordcloud2()
.
fr_stop = stopwords(kind = "fr") |>
as_tibble() |>
rename(mot = value)
nuage = mb |>
select(mot) |>
group_by(mot) |>
count() |>
arrange(desc(n)) |>
filter(!mot %in% fr_stop$mot) |>
droplevels()
wordcloud2(data = nuage, size = .45,
shape = "oval",
rotateRatio = 0.5,
ellipticity = 1, color = "black",
backgroundColor = "white")
Notre nuage de mots
L’objectif ici sera d’analyser les sentiments (et ses tendances) dans la troisième partie du roman. On se concentre sur cinq sentiments déjà listés dans une base de données pour le français (dans l’extension syuzhet
). Comme d’habitude, il faut préparer les données pour qu’on soit capable de générer la figure en question.
Voici le code utilisé. Lisez l’annotation au-dessous du code pour mieux comprendre quelques éléments importants.
1chapterLength = mb |>
group_by(chapitre) |>
summarize(mots = sum(nMots))
2sents0 = get_sentiment_dictionary(dictionary = "nrc", language = "french") |>
select(word, sentiment) |>
rename(mot = word) |>
as_tibble()
3sents1 = mb |>
inner_join(sents0, by = "mot") |>
mutate(across(where(is_character), as_factor))
sents2 = sents1 |>
group_by(partie, chapitre, sentiment) |>
count() |>
group_by(partie, chapitre) |>
4 mutate(prop = n / sum(n)) |>
left_join(chapterLength, by = "chapitre") |>
ungroup() |>
filter(sentiment %in%
c("negative", "positive", "joy", "sadness", "disgust"),
5 partie == "TROISIÈME") |>
mutate(sentiment = factor(sentiment,
levels = rev(c("disgust", "sadness", "negative", "positive", "joy")),
labels = rev(c("dégoût", "tristesse", "négatif", "positif", "joie")))) |>
droplevels() |>
mutate(prop = prop |> round(2)) |>
select(-partie)
get_sentiment_dictionary()
, de l’extension syuzhet
chapitre | sentiment | n | prop | mots |
---|---|---|---|---|
5 | négatif | 274 | 0.19 | 98740 |
3 | tristesse | 17 | 0.11 | 69213 |
2 | négatif | 169 | 0.19 | 84767 |
11 | positif | 139 | 0.17 | 72253 |
7 | dégoût | 70 | 0.06 | 94474 |
La figure exige une étape additionnelle : pour ancrer les étiquettes des cinq sentiments examinés (en utilisant la fonction geom_text()
), on prévoit ses proportions à partir des régressions linéaires. Dans les lignes ci-dessous, on groupe les données en fonction de chaque sentiment et on exécute une régression. Ensuite, on calcule les prévisions des modèles en utilisant la fonction map_dbl()
. Finalement, on crée un tibble
avec nos prévisions ainsi qu’une colonne (chapitre
) dont les valeurs sont 11
. C’est une façon d’ancrer les étiquettes dans l’axe x
.
do()
nous aide à exécuter un modèle linéaire pour chaque niveau du facteur sentiment
, utilisé pour grouper nos données dans la ligne précédente. Le résultat sera un tableau de deux colonnes. La classe de la deuxième colonne sera list
: elle contiendra nos cinq modèles
map_dbl()
pour exécuter la fonction predict()
. Ici, on pose la question « Quelle sera la proportion prévue par chacun des cinq modèles par rapport à la proportion du sentiment x
dans le chapitre 11? »
geom_text()
). Le tableau final est affiché ci-dessous
sentiment | pred | chapitre |
---|---|---|
dégoût | 0.054 | 11 |
négatif | 0.207 | 11 |
positif | 0.145 | 11 |
tristesse | 0.119 | 11 |
joie | 0.068 | 11 |
Veuillez noter que les modèles sont utilisés ici uniquement pour positionner les étiquettes dans la figure. Autrement dit, il ne s’agit pas d’une analyse statistique.
Finalement, on arrive à la figure. On observe ici des tendances intéressantes : dans la première partie du roman, les sentiments négatifs augmentent tandis que les sentiments positifs diminuent. Bien que les lignes de tendance (régression linéaire) ne soient pas justifiées, étant données les variables en question (y
= proportion, x
= chapitres), elles facilitent la communication visuelle des changements à travers les chapitres. En plus, il est intéressant de noter l’étonnante linéarité des tendances en général (spécialement pour le sentiment négatif
).
ggplot(data = sents2,
aes(x = chapitre, y = prop, color = sentiment)) +
stat_smooth(method = "lm", alpha = 0.1,
aes(fill = sentiment)) +
geom_point(aes(size = mots),
alpha = 0.5, show.legend = FALSE) +
theme_tufte(base_size = 11, base_family = "Futura") +
coord_cartesian(xlim = c(1, 13)) +
scale_x_continuous(breaks = seq(1, 11, 1)) +
scale_y_continuous(labels = percent_format()) +
scale_fill_manual(values = rev(c("#DC143C",
"#E97451",
"#CD853F",
"#5F9EA0",
"#4682B4"))) +
scale_color_manual(values = rev(c("#DC143C",
"#E97451",
"#CD853F",
"#5F9EA0",
"#4682B4"))) +
geom_text(data = predictions, aes(x = chapitre,
y = pred,
label = sentiment),
position = position_nudge(x = 0.25),
hjust = 0,
family = "Futura",
size = 4) +
theme(axis.ticks = element_blank(),
legend.position = "none") +
labs(x = "\nChapitre",
y = NULL,
title = "Tendences des sentiments : Madame Bovary, partie III",
caption = "La taille de chaque cercle représente le nombre de mots dans le chapitre")
Lorsqu’on examine (visuellement) les données en question, il est important de considérer comment la figure affichera l’information textuelle, c’est-à-dire les niveaux du facteur sentiment
. Naturellement, c’est une question toujours pertinente, mais la nature des données textuelles amplifie son importance. Ici, je place chaque sentiment dans la figure, ce qui rend l’interprétation de chaque ligne plus intuitive que l’option traditionnelle, démontrée ci-dessous.
C’est beaucoup plus difficile à interpréter les tendances avec une légende quand il y a plusieurs niveaux dans le facteur d’intérêt. En plus, c’est une solution peu accessible, vu que l’interprétation précise dépend de la perception des couleurs utilisées.
Il y a certainement d’autres solutions. Par exemple, on pourrait ajouter un titre ou un sous-titre avec les sentiments en utilisant leurs couleurs respectives (consultez un exemple ici). Toutefois, ce type de solution n’est pas idéale, étant donné le nombre de sentiments ici. Finalement, on pourrait ajouter les sentiments manuellement en utilisant la fonction annotate()
. C’est la pire solution, vu qu’un petit changement dans les données ou dans les dimensions de la figure changera la position correcte pour les étiquettes. La solution suggérée ci-dessus est, à mon avis, la solution la plus précise et la plus généralisable.
Voici un autre exemple d’analyse textuelle dans la version anglaise de ce site web (Moby Dick).
Copyright © 2024 Guilherme Duarte Garcia
Veuillez noter que la liste des stopwords en question n’est pas exhaustive. Donc, ne soyez pas surpris si notre nuage contient des mots fonctionnels.↩︎