Avant-propos

  • Options d’édition: syntaxe ‘tango’, le thème ‘readable’.
  • Fragments et résultats.
  • Source: ‘Statistiques en sciences humaines avec R, Éditions de Boeck, 2014’.
  • Il est fortement recommandé d’utiliser Linux ou Mac avec des textes comportant des caractères internationaux comme le français; cela évite bien des problèmes de caractères.
  • Ce rapport est principalement généré sur Linux/Ubuntu.


Le traitement du langage naturel (mieux connu sous l’acronyme anglais de NLP) permet d’analyser des discours, des résolutions, des procès-verbaux, des éditoriaux, des courriels, des textos, etc. Il permet aussi d’analyser des verbatims (les résultats de transcriptions d’émissions de radio ou de comparutions par exemple).

Chargement et prétraitement

Préparation

Installons les fonctions pour le forage de textes (forer un texte consiste à en extraire des termes d’un texte comme on extrait du minerai de la roche).

library(tm)
library(RCurl)
library(RColorBrewer)
library(wordcloud)
library(XML)
library(xml2)

Chargement

Récupérons les textes qui constitueront le corpus.

Il s’agit de deux discours du trône de l’Assemblée Nationale du Québec, donc de deux premiers ministres. Le premier texte, celui de Philippe Couillard, provient du site La Société du patrimoine politique du Québec, alors que le second texte, celui de Pauline Marois, provient du site du gouvernement du Québec.

# Récupérer des texte du Web
doc1 <- getURL("http://dimension.usherbrooke.ca/voute/discours/2014Couillard.html")
doc2 <- getURL('http://dimension.usherbrooke.ca/voute/discours/2012Marois.html')

Prétraitement

Il y a un problème.

Comme le minerai, il faut raffiner l’extraction brute, car le texte html comporte des déchets. Bien qu’il existe plusieurs degrés de raffinage, nous n’allons qu’utiliser un raffinage superficiel.

Il existe une fonction personnalisée que l’on peut récupérer d’un dépositaire GitHub.

# Nettoyer le html avec une fonction
htmlToText = function(input, ...) {
    require(RCurl)
    require(XML)
    evaluate_input <- function(input) {
        if(file.exists(input)) {
            char.vec <- readLines(input, warn = FALSE)
            return(paste(char.vec, collapse = ""))
        }
        if(grepl("", input, fixed = TRUE)) return(input)
        if(!grepl(" ", input)) {
            #if(!file.exists("cacert.perm")) download.file(url="http://curl.haxx.se/ca/cacert.pem", destfile="cacert.perm")
            #return(getURL(input, followlocation = TRUE, cainfo = "cacert.perm"))
        }
        return(NULL)
    }
    convert_html_to_text = function(html) {
        doc <- htmlParse(html, asText = TRUE, encoding="Latin-1")
        text <- xpathSApply(doc, "//text()[not(ancestor::script)][not(ancestor::style)][not(ancestor::noscript)][not(ancestor::form)]", xmlValue)
        return(text)
    }
    collapse_text = function(txt) {
        return(paste(txt, collapse = " "))
    }
    html.list <- lapply(input, evaluate_input)
    text.list <- lapply(html.list, convert_html_to_text)
    text.vector <- sapply(text.list, collapse_text)
    return(text.vector)
} 

Exécutons le raffinage simple sur les textes bruts.

# Nettoyer
txtdoc1 <- htmlToText(doc1)
txtdoc2 <- htmlToText(doc2)

Idéalement, on sauvegarde les textes nettoyés à l’aide de la fonction write. On archive tous les fichiers du dossier, txt et Rmd, ensemble (nous avons préalablement précisé le répertoire de travail avec setwd).

# Copier dans sur l'ordi, dans un sous-répertoire du répertoire de travail (créer ce sous-répertoire avant d'exécuter les commandes)
write(txtdoc1, file = "textes/2014Couillard.txt")
write(txtdoc2, file = "textes/2012Marois.txt")

Ensuite, on peut recharger les fichiers txt avec scan ou read.ftable (fonctions idéales pour des fichiers txt ou plats (flat files)).

scan("textes/2014Couillard.txt", skip = 1, nlines = 10, what = "raw")
##  [1] "Monsieur"        "le"              "Président,"     
##  [4] "Monsieur"        "le"              "Chef"           
##  [7] "de"              "l’opposition"    "officielle,"    
## [10] "Monsieur"        "le"              "Chef"           
## [13] "du"              "deuxième"        "groupe"         
## [16] "de"              "l’opposition,"   "Mesdames,"      
## [19] "Monsieur,"       "représentants"   "de"             
## [22] "Québec"          "solidaire,"      "Chers"          
## [25] "collègues"       "parlementaires," "Distingués"     
## [28] "invités"         "dans"            "les"            
## [31] "tribunes,"       "Chères"          "Québécoises,"   
## [34] "chers"           "Québécois,"      "Monsieur"       
## [37] "le"              "Président,"

Les deux textes ont été filtrés et les termes ont été segmentés. Le travail d’analyse peut commencer.



Approche qdap

Cette section 2 est générée sur Windows, car Linux ne permet pas le chargement du package qdap.

library(qdap)
library(qdapTools)

texte <- scan("textes/2014Couillard.txt", skip = 1, nlines = 10, what = "raw", encoding = "UTF-8")

# Print and plot text
frequent_terms <- freq_terms(texte, top = 20, stopwords = tm::stopwords("french"))

plot(frequent_terms)

Le package qdap est très sommaire. Il permet un aperçu, une exploration. Il se limite à des analyses de fréquences simples sur un texte ou des documents.

La prochaine approche permet d’analyser un corpus de documents et de pousser plus loin l’exploration de texte et la visualisation des résultats.



Approche tm

Création du corpus

Création de la matrice du corpus

Qu’il s’agisse de deux longs documents (des livres par exemple) ou de milliers de courriels ou de millions de textos, la procédure de création est la même.

Sur Windows.

# Garder les parenthèses
# Garder paramètre encoding
( base <- Corpus(DirSource("textes/", encoding = "UTF-8"), readerControl = list(language = "french",  load = TRUE)) )

dtm <- DocumentTermMatrix(base)
dtm

Sur Linux.

# Enlever paramètre encoding
( base <- Corpus(DirSource("textes/"), readerControl = list(language = "french",  load = TRUE)) )

dtm <- DocumentTermMatrix(base)
dtm
## <<SimpleCorpus>>
## Metadata:  corpus specific: 1, document level (indexed): 0
## Content:  documents: 2
## <<DocumentTermMatrix (documents: 2, terms: 3493)>>
## Non-/sparse entries: 4405/2581
## Sparsity           : 37%
## Maximal term length: 21
## Weighting          : term frequency (tf)

À noter qu’un corpus peut avoir deux formes:

  • Dans une matrice de corpus DocumentTermMatrix, les lignes de la matrice sont les documents, les colonnes sont les termes.
  • Dans une matrice TermDocumentMatrix, c’est l’inverse.

Voice la taille de la matrice de corpus (ligne x colonne).

dim(dtm)
## [1]    2 3493

Analyses primaires

Analysons le contenu de la matrice du corpus. Voici quelques fonctions.

# document 1
inspect(dtm[1,])
## <<DocumentTermMatrix (documents: 1, terms: 3493)>>
## Non-/sparse entries: 2009/1484
## Sparsity           : 42%
## Maximal term length: 21
## Weighting          : term frequency (tf)
## Sample             :
##                 Terms
## Docs             dans des gouvernement les notre nous pour que sur une
##   2012Marois.txt   74 174           91 187    75  184  104  84  62  70
# document 2
inspect(dtm[2,])
## <<DocumentTermMatrix (documents: 1, terms: 3493)>>
## Non-/sparse entries: 2396/1097
## Sparsity           : 31%
## Maximal term length: 21
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                dans des les nos notre nous pour que qui une
##   2014Couillard.txt   79 192 221  71   114  218   81  99 106  57
# document 1, 10 premiers termes
inspect(dtm[1,1:10])
## <<DocumentTermMatrix (documents: 1, terms: 10)>>
## Non-/sparse entries: 10/0
## Sparsity           : 0%
## Maximal term length: 4
## Weighting          : term frequency (tf)
## Sample             :
##                 Terms
## Docs             000 170 1812 1990 1er 2000 2001 2002 2011 2012
##   2012Marois.txt   1   1    1    1   1    1    1    1    1    1
# 500e terme
inspect(dtm[,500])
## <<DocumentTermMatrix (documents: 2, terms: 1)>>
## Non-/sparse entries: 1/1
## Sparsity           : 50%
## Maximal term length: 11
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                déclaration
##   2012Marois.txt              1
##   2014Couillard.txt           0
# Éviter
#inspect(base[2]) # document 2

# les termes les moins fréquents
findFreqTerms(dtm, lowfreq = 200)
## [1] "des"  "les"  "nous"
# idem, dans le document 1
findFreqTerms(dtm[1,], lowfreq = 100)
## [1] "des"  "les"  "nous" "pour"

Transformation

Transformons la matrice du corpus en une matrice simple (une matrice comme en algèbre, à l’exception que le contenu n’est pas numérique). Examinons cette matrice simple.

# Imprimer la classe
class(dtm)
## [1] "DocumentTermMatrix"    "simple_triplet_matrix"
# Dimensions
dim(dtm)
## [1]    2 3493
# Transformer
dtm2 <- inspect(dtm)
## <<DocumentTermMatrix (documents: 2, terms: 3493)>>
## Non-/sparse entries: 4405/2581
## Sparsity           : 37%
## Maximal term length: 21
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                dans des gouvernement les notre nous pour que qui une
##   2012Marois.txt      74 174           91 187    75  184  104  84  60  70
##   2014Couillard.txt   79 192           38 221   114  218   81  99 106  57
# Imprimer la classe
class(dtm2)
## [1] "matrix"
# Dimensions
dim(dtm2)
## [1]  2 10
# Calculer le nombre de mots (la somme des lignes sur chaque colonne)
nbmots <- rowSums(dtm2)
nbmots
##    2012Marois.txt 2014Couillard.txt 
##              1103              1205

Visualisation – Sommaire de corpus

Traçons les résultats sur un graphique.

# Changer les paramètres
par(mfrow = c(1,1),  oma = c(0,.5,.5,.5))

# Tracer
dotchart(nbmots, row.names(dtm), main = "Nombre de mots dans les discours")

abline(v = median(nbmots), lty = 1, lwd = 2, col = 'blue3')
abline(v = mean(nbmots), lty = 2, lwd = 2, col = 'red3')

legend("bottomright", inset = 0.02, c("Médiane", "Moyenne"), lty = c(1,2), lwd = 2, col = c('blue3', 'red3'))

# Rechanger les paramètres
par(mfrow = c(1,1),  oma = c(0,0,0,0))

Évidemment, avec plus de textes (pas seulement deux), ce graphique devient plus intéressant.

Filtrage

Filtres

Nous pouvons raffiner le corpus:

  • Mettre en minuscules.
  • Enlever la ponctuation.
  • Enlever les chiffres ou les nombres.
  • Enlever les espaces superflues1 (les ‘blancs’ de trop).
  • Enlever les mots communs appelés ‘stopwords’ qui sont spécifiques à chaque langue.
  • Enlever des mots supplémentaires ou choisis.

Opération de filtrage

En partant du corpus base, nous reprenons la procédure de création de la matrice du corpus (juste avant l’étape dtm <- DocumentTermMatrix(base)), mais avec des étapes supplémentaires de raffinage.

# Transformation complète
base2 <- tm_map(base, content_transformer(tolower)) # minuscule
base2 <- tm_map(base2,content_transformer(removePunctuation)) # ponctuation
base2 <- tm_map(base2,content_transformer(removeNumbers)) # nombres
base2 <- tm_map(base2,content_transformer(stripWhitespace)) # espaces
base2 <- tm_map(base2,content_transformer(removeWords), stopwords("french")) # stopwords

motssupprimes <- c("président", "qui", "est", "par", "sont", "pour", "cette", "que", "des", "comme", "les", "cest", "avec", "aussi", "allons", "bien", "plus", "tous", "ailleurs", "elles", "ils", "parce", "encore", "être", "entre", "faut", "très", "ainsi", "certains", "certaines", "the", "autres", "autre", "hui", "ans", "celles", "celle", "quelque", "quelques", "chaque", "depuis", "beaucoup", "celui", "lorsqu", "fois", "donc", "chez", "rien", "mal", "bien", "prendre", "dès", "face", "dit", "fois", "pourtant", "face", "dire", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix", "plusieurs", "chacun", "chacune", "car", "sous", "sur", "vers", "quoi", "grâce", "non", "oui", "cependant", "avoir", "ceux", "surtout", "toute", "toutes", "tout", "uvre", "sorte", "premier", "dernier", "moins", "plus", "dont", "monsieur")

base2 <- tm_map(base2,content_transformer(removeWords), motssupprimes) # mots choisis

Création de la deuxième matrice du corpus

Au lieu de base, nous travaillons avec base2.

Créons et inspectons la matrice du corpus (assurance-qualité).

# Créer une matrice de corpus
dtm2 <- DocumentTermMatrix(base2)
dtm2
## <<DocumentTermMatrix (documents: 2, terms: 3284)>>
## Non-/sparse entries: 4073/2495
## Sparsity           : 38%
## Maximal term length: 25
## Weighting          : term frequency (tf)
dim(dtm2)
## [1]    2 3284
# A/Q
findFreqTerms(dtm2, lowfreq = 100)
## [1] "gouvernement" "québec"
# Créer une matrice simple
dtm2_2 <-  inspect(dtm2)
## <<DocumentTermMatrix (documents: 2, terms: 3284)>>
## Non-/sparse entries: 4073/2495
## Sparsity           : 38%
## Maximal term length: 25
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                développement dune faire gouvernement létat loi
##   2012Marois.txt               21   18    24           91     9  15
##   2014Couillard.txt            16   15    23           38    18  13
##                    Terms
## Docs                ministre québec québécois services
##   2012Marois.txt           9     48        28        9
##   2014Couillard.txt       25     52        21       21
dtm2_2
##                    Terms
## Docs                développement dune faire gouvernement létat loi
##   2012Marois.txt               21   18    24           91     9  15
##   2014Couillard.txt            16   15    23           38    18  13
##                    Terms
## Docs                ministre québec québécois services
##   2012Marois.txt           9     48        28        9
##   2014Couillard.txt       25     52        21       21
dim(dtm2_2)
## [1]  2 10
# Calculer le nombre de mots
nbmots2_2 <- rowSums(dtm2_2)
nbmots2_2
##    2012Marois.txt 2014Couillard.txt 
##               272               242

Nous obtenons dtm2_2; plus épurée que dtm2.

Visualisation – Nuages de mots

Créons des nuages de mots. Les mots en gros caractères sont les plus fréquents.

Cette fois, nous allons extraires les documents de base2 pour en faire deux matrices de corpus indépendantes: dtmUnText1 et dtmUnText2. Les nuages de mots sont tirées des ces deux matrices indépendantes.

par(mfrow = c(1,2), mar = c(5,2,3,2), oma = c(1,0,1,0))

# Créer une matrice de corpus
dtmUnTexte1 <- DocumentTermMatrix(base2[1])

# Transformer en matrice simple, filtrer les mots le plus fréquents
freq1 <- sort(colSums(as.matrix(dtmUnTexte1)), decreasing = T)

# Dessiner le nuage
wordcloud(names(freq1), freq1, max.words = 20)

title("Marois \n2012", "sur 272 mots")

# ---

dtmUnTexte2 <- DocumentTermMatrix(base2[2])

freq2 <- sort(colSums(as.matrix(dtmUnTexte2)), decreasing = T)

wordcloud(names(freq2), freq2, max.words = 20)

title("Couillard \n2014", "sur 242 mots")

par(mfrow = c(1,1))

par(mfrow = c(1,2), mar = c(4,2,4,2), oma = c(0,0,0,0))

Lorsqu’un nuage apparait plus gros qu’un autre, il démontre non seulement la fréquence des mots les plus usités, mais la concentration du vocabulaire. Le texte de gauche est plus riche en vocabulaire et les fréquences de mots récurrents sont moins élevées. Il faut aussi relever que le texte de gauche est plus long et que les mots les plus usités sont plus dilués dans l’ensemble (pourcentage plus bas).

En couleur

Ajoutons de la couleur et quelques paramètres.

En passant, nous sauvegardons nos résultats préliminaires dans des fichers CSV. Ils nous serons utiles plus loin.

par(mfrow = c(1,2), mar = c(5,2,3,2), oma = c(1,0,1,0))

dtmUnTexte1 <- DocumentTermMatrix(base2[1])
freq1 <- sort(colSums(as.matrix(dtmUnTexte1)), decreasing = T)

wordcloud(names(freq1), freq1, max.words = 30, random.order = F, colors = brewer.pal(8, "Set1"))

# Autres thèmes de couleur...
# Dark2 8, Paired 12, Set1 9, Set2 8, Set3 12, etc.

title("Marois \n2012", "sur 272 mots")

# Sauvegarder
freq_1 <- as.matrix(freq1)

write.table(freq_1, file = "freq1.csv", sep = ";", row.names = T, append = F, col.names = F)

# ---

dtmUnTexte2 <- DocumentTermMatrix(base2[2])
freq2 <- sort(colSums(as.matrix(dtmUnTexte2)), decreasing = T)

wordcloud(names(freq2), freq2, max.words = 30, random.order = F, colors = brewer.pal(8, "Dark2"))

title("Couillard \n2014", "sur 242 mots")

# Sauvegarder
freq_2 <- as.matrix(freq2)

write.table(freq_2, file = "freq2.csv", sep = ";", row.names = T, append = T, col.names = F)

par(mfrow = c(1,2), mar = c(4,2,4,2), oma = c(0,0,0,0))

Les paramètres ont permis de concentrer les mots plus usités au milieu des nuages. La fréquence se reflète non seulement sur la taille des caractères, mais aussi sur la couleur des caractères.

Quoi faire avec les CSV

Voici un aperçu des fichiers sauvegardés. Il s’agit d’un classement décroissant des mots et de leur fréquence.

# Charger le fichier
freq1 <- read.csv("freq1.csv", header = FALSE, sep = ";")

# Lire la tête du fichier
head(freq1)
##              V1 V2
## 1  gouvernement 91
## 2        québec 48
## 3     québécois 28
## 4         faire 24
## 5 développement 21
## 6          dune 18

Nous avons sauvegardé les résultats préliminaires pour les utiliser avec d’autres programmes.

Pour générer de meilleurs nuages de mots, il existe bien d’autres options comme ce site.

On peut ensuite télécharger resultat, le nuage, en format SVG et l’appliquer dans un rapport ou un site web.

Au lieu de sauvegarder les résultats en fichier CSV, il est possible de les copier-coller dans une feuille de calcul ou un traitement de texte quelconque.

# Exporter dans un objet
V1 <- as.vector(names(freq1))
V2 <- as.numeric(freq1)
freq_1 <- cbind(V1, V2)

# Ne pas sauvegarder
#write.table(freq_1, file = "freq1.csv", sep = ";", row.names = T, append = F, col.names = F)

Sur Windows.

# Copier
library(questionr)

clipcopy(freq_1, digits = 3)

Il ne reste qu’à coller la capture dans un document externe et formater le résultat brut.

Sur Linux.

Il faut fignoler un peu:

  • Installer xclip dans un terminal: sudo apt-get install xclip.
  • Exécuter une fonction personnalisée.
# Rouler cette fonction
clipboard <- function(x, sep = "\t", row.names = FALSE, col.names = TRUE){ con <- pipe("xclip -selection clipboard -i", open = "w")
     write.table(x, con, sep=sep, row.names = row.names, col.names = col.names)
     close(con)}

# Utiliser cette fonction pour copier
clipboard(freq_1)

Il ne reste qu’à coller à l’extérieur et formater le tout.

Visualisation – Graphique à barres

Un nuage de mot c’est bien, mais nous aurions pu tracer un simple graphique à barres à propos des 10 mots les plus fréquents.

# Reprendre la matrice simple filtrant les mots le plus fréquents
# Transformer
freq1_df <- as.data.frame(freq1)

# Lire les 10 mots le plus fréquents
head(freq1_df, 10)
##               V1 V2
## 1   gouvernement 91
## 2         québec 48
## 3      québécois 28
## 4          faire 24
## 5  développement 21
## 6           dune 18
## 7      politique 18
## 8      également 16
## 9            loi 15
## 10    québécoise 14
par(mar = c(5,2,4,2), oma = c(4,1,0,0))

# Tracer un graphique à barres
barplot(freq1_df[1:10,2], names.arg = freq1_df[1:10,1], col = "tan", las = 2, ylim = c(0, 100))
grid()

# las = 0, parallèle
# las = 2, perpendiculaire

par(mar = c(4,2,4,2), oma = c(0,0,0,0))

Cette solution est simple, mais elle révèle beaucoup plus en ordonnant les résultats.

Regroupement & visualisation

Répertoire

Répertorions les mots selon certaines catégories. Il faut d’abord construire un dictionnaire thématique.

dictionnaire <- c("québec", "québécois", "québécoises", "canada", "canadien", "canadiens", "canadiennes")

dtmDictionnaire <- DocumentTermMatrix(base2,list(dictionary = dictionnaire))
inspect(dtmDictionnaire)
## <<DocumentTermMatrix (documents: 2, terms: 7)>>
## Non-/sparse entries: 9/5
## Sparsity           : 36%
## Maximal term length: 11
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                canada canadien canadiennes canadiens québec québécois
##   2012Marois.txt         3        0           0         0      2        48
##   2014Couillard.txt     10        4           0         1      0        52
##                    Terms
## Docs                québécoises
##   2012Marois.txt             28
##   2014Couillard.txt          21
dtmDictionnaireP <- DocumentTermMatrix(base2,list(dictionary = dictionnaire)) / nbmots2_2
inspect(dtmDictionnaireP)
## <<DocumentTermMatrix (documents: 2, terms: 7)>>
## Non-/sparse entries: 9/5
## Sparsity           : 36%
## Maximal term length: 11
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                    canada   canadien canadiennes   canadiens
##   2012Marois.txt    0.01102941 0.00000000           0 0.000000000
##   2014Couillard.txt 0.04132231 0.01652893           0 0.004132231
##                    Terms
## Docs                     québec québécois québécoises
##   2012Marois.txt    0.007352941 0.1764706  0.10294118
##   2014Couillard.txt 0.000000000 0.2148760  0.08677686
dtmDictionnairePDF <- data.frame(inspect(dtmDictionnaireP))
## <<DocumentTermMatrix (documents: 2, terms: 7)>>
## Non-/sparse entries: 9/5
## Sparsity           : 36%
## Maximal term length: 11
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                    canada   canadien canadiennes   canadiens
##   2012Marois.txt    0.01102941 0.00000000           0 0.000000000
##   2014Couillard.txt 0.04132231 0.01652893           0 0.004132231
##                    Terms
## Docs                     québec québécois québécoises
##   2012Marois.txt    0.007352941 0.1764706  0.10294118
##   2014Couillard.txt 0.000000000 0.2148760  0.08677686
dtmDictionnaireDF = data.frame(inspect(dtmDictionnaire))
## <<DocumentTermMatrix (documents: 2, terms: 7)>>
## Non-/sparse entries: 9/5
## Sparsity           : 36%
## Maximal term length: 11
## Weighting          : term frequency (tf)
## Sample             :
##                    Terms
## Docs                canada canadien canadiennes canadiens québec québécois
##   2012Marois.txt         3        0           0         0      2        48
##   2014Couillard.txt     10        4           0         1      0        52
##                    Terms
## Docs                québécoises
##   2012Marois.txt             28
##   2014Couillard.txt          21

Tout ce code est bien lourd et ne nous dit rien…

Visualisation

En fonction de mots clés, nous obtenons un décompte…

par(mfrow = c(2, 1), mar = c(4,5,4,2), oma = c(0,4,1,0), las = 2)

# 1
qc <- dtmDictionnaireDF$québécoise + dtmDictionnaireDF$québécoises + dtmDictionnaireDF$québécois + dtmDictionnaireDF$québec

barplot((qc), horiz = T, names.arg = (row.names(dtmDictionnaireDF)), xlim = c(0,200))

title(main = list("Fréquences absolues\n du mot Québec et ses déclinaisons", cex = 1))

# 2
qc <- dtmDictionnaireDF$canadienne + dtmDictionnaireDF$canadiennes + dtmDictionnaireDF$canadiens + dtmDictionnaireDF$canadien + dtmDictionnaireDF$canada

barplot((qc), horiz = T, names.arg = (row.names(dtmDictionnaireDF)), xlim = c(0,20))

title(main = list("Fréquences absolues\n du mot Canada et ses déclinaisons", cex = 1))

…ou un ratio.

par(mfrow = c(2, 1), mar = c(4,5,4,2), oma = c(0,4,1,0), las = 2)

qc <- dtmDictionnairePDF$québécoise + dtmDictionnairePDF$québécoises + dtmDictionnairePDF$québécois + dtmDictionnairePDF$québec

barplot((qc), horiz = T, names.arg = (row.names(dtmDictionnairePDF)), xlim = c(0,0.5))

title(main = list("Fréquences pondérées\n du mot Québec et ses déclinaisons", cex = 1))

qc <- dtmDictionnairePDF$canadienne + dtmDictionnairePDF$canadiennes + dtmDictionnairePDF$canadiens + dtmDictionnairePDF$canadien + dtmDictionnairePDF$canada

barplot((qc), horiz = T, names.arg = (row.names(dtmDictionnairePDF)), xlim = c(0,0.5))

title(main = list("Fréquences pondérées\n du mot Canada et ses déclinaisons", cex = 1))

par(mfrow = c(1, 1), mar = c(4,2,4,2), oma = c(0,0,0,0), las = 0)

Mots communs et différents, en nuage

Croisons les documents pour retrouver des mots communs et différents: commonality.cloud et comparison.cloud. Ces fonctions sont graphiques.

D’abord, il faut générer une matrice de corpus inversée (TDM plutôt que DTM).

tdm3 <- TermDocumentMatrix(base2)

Ensuite nous transformons cette matrice de corpus en matrice simple et nous trançons les graphiques.

par(mfrow = c(1,2), mar = c(4,2,3,2), oma = c(0,0,0,0))

tdm3_m <- as.matrix(tdm3)

# Changer le nom des colonnes (facultatif)
#colnames(tdm3_m) <- c("A", "B")

par(mfrow = c(1, 2))

# Commun
commonality.cloud(tdm3_m, colors = "steelblue1", max.words = 10, random.order = FALSE)

title("Mots les plus communs \ndans les documents")

# ---

# Différent
comparison.cloud(tdm3_m, colors = "steelblue1", max.words = 10, random.order = FALSE, scale = c(1,2), title.size = 1.5)

title("Mots différents \ndans les documents")

par(mfrow = c(1,1), mar = c(4,2,4,2), oma = c(0,0,0,0))
  • Le nuage de mots communs est un nuage de mot où la taille décrit la fréquence du mot.
    • Le mot le plus commun est ‘québec’, suivi de ‘gouvernement’. ‘québec’ apparait 103 fois dans les deux documents par exemple.
  • Le nuage de mots différents rapporte à la fois la fréquence par la taille du mot et l’appartenance par la position et la couleur du mot.
    • Le mot ‘québécoise’ apparait plus souvent dans le document du haut (45 fois vs 43 fois en bas).
    • Le mot ‘ministre’ apparait nettement plus souvent dans le document du bas (29 fois vs 9 fois en haut).

Mots communs et différents, en pyramide

La visualisation par nuage donne une idée d’ensemble, mais c’est une visualisation floue.

Si l’on veut mieux comparer (les mots communs ou différents), il faut un graphique qui ressemble au graphique à barres.

par(mfrow = c(2,1), mar = c(4,5,4,2), oma = c(0,2,0,0))

barplot(freq1_df[1:10,2], names.arg = freq1_df[1:10,1], col = "tan", las = 2, ylim = c(0, 100))
grid()

barplot(freq1_df[1:10,2], names.arg = freq1_df[1:10,1], col = "tan", las = 2, xlim = c(0, 100), horiz = TRUE, las = 1)
grid()

par(mfrow = c(1,1), mar = c(4,2,4,2), oma = c(0,0,0,0))

Le pyramid.plot() du package plotrix permet de comparer du graphiques à barres horizontales.

La matrice simple (dérivée de la matrice du corpus) comporte des cellules vides. Extrayons uniquement les cellules non nulles communes aux deux textes (car un texte peut comporter un mot qu’un autre texte ne possède pas). Nous appliquons la double contrainte (non nul, commun) avec le & (AND).

meme_mots <- subset(tdm3_m, tdm3_m[, 1] > 0 & tdm3_m[, 2] > 0)

head(meme_mots)
##                 Docs
## Terms            2012Marois.txt 2014Couillard.txt
##   accommodements              1                 1
##   accès                       5                 2
##   accélérer                   5                 2
##   action                      1                 1
##   adopter                     1                 1
##   affaires                    1                 3

Il faut ensuite ordonner les mots en ordre décroissant. Créons d’abord une nouvelle colonne: la moyenne de fréquence par ligne des deux colonnes.

meme_mots_ <- cbind(meme_mots, apply(meme_mots[,1:2], 1, mean))

head(meme_mots_)
##                2012Marois.txt 2014Couillard.txt    
## accommodements              1                 1 1.0
## accès                       5                 2 3.5
## accélérer                   5                 2 3.5
## action                      1                 1 1.0
## adopter                     1                 1 1.0
## affaires                    1                 3 2.0
meme_mots_ <- meme_mots_[order(meme_mots_[, 3], decreasing = TRUE), ]

head(meme_mots_)
##               2012Marois.txt 2014Couillard.txt     
## gouvernement              91                38 64.5
## québec                    48                52 50.0
## québécois                 28                21 24.5
## faire                     24                23 23.5
## développement             21                16 18.5
## ministre                   9                25 17.0

Créons un palmarès des 20 premiers mots.

top_mots <- data.frame(
x = meme_mots_[1:20, 1],
y = meme_mots_[1:20, 2],
labels = rownames(meme_mots_[1:20, ])
)

head(top_mots)
##                x  y        labels
## gouvernement  91 38  gouvernement
## québec        48 52        québec
## québécois     28 21     québécois
## faire         24 23         faire
## développement 21 16 développement
## ministre       9 25      ministre

Traçons le graphique pyramide.

library(plotrix)

pyramid.plot(top_mots$x, top_mots$y, labels = top_mots$labels, gap = 30, top.labels = c("2012Marois", "terme", "2014Couillard"), main = "Mots en commun", laxlab = NULL, raxlab = NULL, unit = NULL)

## [1] 5.1 4.1 4.1 2.1

Visualization – N-grammes avec dendrogrammes

Un dendrogramme permet d’observer les associations de mots.

D’abord, il faut nettoyer la matrice du corpus. Créons un corpus plus dense.

Nous l’ordonnons.

Puis, nous calculons une distance ‘euclidienne’. C’est un principe de géométrie.

Dans une immense matrice (un plan donc), on cherche à connaitre la ‘distance’ entre chaque mot: mesurée avec la fréquence de chaque paire de mots ou bigrammes. Une paire de mots représente deux mots qui se suivent dans les textes. Au lieu d’analyser des mots uniques, on analyse des bigrammes. Les paires de mots les plus fréquentes sont les bigrammes les plus fréquents.

On pourrait répéter l’exercice pour des trigrammes et plus.

# Créer
dense_tdm3 <- removeSparseTerms(tdm3, sparse = 0.9) # sparsity remains high
dense_tdm3 <- removeSparseTerms(tdm3, sparse = 0.1) # picky, sparsity decreases

# Simplifions
dense_tdm3_m <- as.matrix(dense_tdm3)

# Fréquences non nulles et mots communs dans les documents
meme_mots <- subset(dense_tdm3_m, dense_tdm3_m[, 1] > 0 & dense_tdm3_m[, 2] > 0)

head(meme_mots)
##                 Docs
## Terms            2012Marois.txt 2014Couillard.txt
##   accommodements              1                 1
##   accès                       5                 2
##   accélérer                   5                 2
##   action                      1                 1
##   adopter                     1                 1
##   affaires                    1                 3
# Ordonner en ordre décroissant
meme_mots_ <- cbind(meme_mots, apply(meme_mots[,1:2], 1, mean))

head(meme_mots_)
##                2012Marois.txt 2014Couillard.txt    
## accommodements              1                 1 1.0
## accès                       5                 2 3.5
## accélérer                   5                 2 3.5
## action                      1                 1 1.0
## adopter                     1                 1 1.0
## affaires                    1                 3 2.0
meme_mots_ <- meme_mots[order(meme_mots_[, 3], decreasing = TRUE), ]

head(meme_mots_)
##                Docs
## Terms           2012Marois.txt 2014Couillard.txt
##   gouvernement              91                38
##   québec                    48                52
##   québécois                 28                21
##   faire                     24                23
##   développement             21                16
##   ministre                   9                25
top_mots <- data.frame(
  x1 = meme_mots_[1:20, 1],
  x2 = meme_mots_[1:20, 2])

head(top_mots)
##               x1 x2
## gouvernement  91 38
## québec        48 52
## québécois     28 21
## faire         24 23
## développement 21 16
## ministre       9 25
# Créer un d.f
top_mots_df <- as.data.frame(top_mots)

# Calculer la distance euclidienne
top_mots_dist <- dist(top_mots_df)

# Créer des grappes
hc <- hclust(top_mots_dist)

# Transformer en dendrogramme
hcd <- as.dendrogram(hc)

# Ajouter des étiquettes de champs
labels(hcd)
##  [1] "ministre"      "services"      "létat"         "projet"       
##  [5] "santé"         "travail"       "loi"           "doit"         
##  [9] "léconomie"     "place"         "projets"       "québécois"    
## [13] "faire"         "développement" "dune"          "politique"    
## [17] "également"     "société"       "gouvernement"  "québec"
par(mar = c(8,2,4,2), oma = c(0,1,0,0))

# Tracer le dendrogramme
plot(hcd, main = "Dendrogramme 1")

par(mar = c(4,2,4,2), oma = c(0,0,0,0))

Il faut lire que la paire ‘gouvernement-québec’, à droite du graphique, revient plus de 40 fois.

Ensuite, nous avons beaucoup de bigrammes peu fréquents qui reviennent moins de 20 fois. Mais on peut les regrouper en familles et remonter dans l’arbre.

On doit comprendre que le bigramme ‘ministre-services’ partage une plus grande parenté (frère-soeur) dans les textes avec le bigramme ‘santé-travail’ qu’avec le bigramme ‘québecois-faire’ (cousin-cousine).

Il est possible de constituer des grappes de bigrammes.

En couleur

Ajoutons de la couleur à un bigramme donné (“gouvernement-québec”) et des boites de division (allons-y pour 5 grappes).

library(dendextend)

hcd2 <- branches_attr_by_labels(hcd, c("gouvernement", "québec"), col = 'red3')

par(mar = c(8,2,4,2), oma = c(0,1,0,0))

plot(hcd2, main = "Dendrogramme 2")

# Ajouter 4 grappes
rect.dendrogram(hcd2, k = 5, border = 'grey50')

par(mar = c(4,2,4,2), oma = c(0,0,0,0))

Nous nous arrêtons ici.

Le sujet de l’analyse du langage naturel est tellement vaste qu’il est comme le contenu d’une bibliothèque: on lit les classiques, mais il est impossible de tout étudier.




  1. en typographie et en imprimerie, le mot ‘espace’ est féminin.