Analyse textuelle de Madame Bovary

Il ne faut pas toucher aux idoles : la dorure en reste aux mains.


Flaubert, Gustave. Madame Bovary (partie 3, chapitre 6)

Nuage de mots

Source : Wikipedia 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.


Les étapes

1. Charger les extensions

Voici les extensions nécessaires pour reproduire les codes ci-dessous.

Code
library(wordcloud2)
library(tidytext)
library(tidyverse)
library(scales)
library(tm)
library(syuzhet)
library(ggthemes)

2. Charger et préparer le texte

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().

Code
# 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)
Exemples de mots dans le tableau
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

3. Préparer des mots

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().

Code
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


Analyse de sentiment

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.

1. Préparer les données

Voici le code utilisé. Lisez l’annotation au-dessous du code pour mieux comprendre quelques éléments importants.

Code
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)
1
Compter le nombre des mots pour chaque chapitre, ce qui permettra d’ajuster la taille de chaque cercle dans la figure
2
Extraire le dictionnaire des sentiments pour le français à partir de la fonction get_sentiment_dictionary(), de l’extension syuzhet
3
Trouver les sentiments pour les mots dans le roman
4
Calculer la proportion de chaque sentiment dans chaque chapitre de chaque partie
5
Sélectionner les cinq sentiments d’intérêt (en anglais) et les traduire vers le français
Exemples de proportions
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

2. Créer la figure

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.

Code
models = sents2 |> 
    group_by(sentiment) |> 
1    do(model = lm(prop ~ chapitre, data = .))

2preds = map_dbl(models$model, ~predict(., newdata = tibble(chapitre = 11)))

3predictions = tibble(sentiment = models$sentiment,
                     pred = preds |> round(3),
                     chapitre = 11)
1
La fonction 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
2
Ensuite, on utilise 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? »
3
Finalement, on combine les sentiments et les prévisions des modèles, ce qui nous permettra d’ancrer les étiquettes dans la figure (geom_text()). Le tableau final est affiché ci-dessous
Prévisions à partir des modèles pour ancrer les étiquettes dans la figure
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).

Code
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")

La figure finale

La figure finale

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.

Une légende traditionnelle pour les sentiments

Une légende traditionnelle pour les sentiments

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.

Plus d’exemples?

Voici un autre exemple d’analyse textuelle dans la version anglaise de ce site web (Moby Dick).


Copyright © 2024 Guilherme Duarte Garcia

Notes de bas de page

  1. 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.↩︎