La loi de Zipf illustrée avec gutenbergr en R 🇫🇷¶
Il s’agit de montrer quelques aspects de la loi de Zipf avec 3 textes (Wuthering Heights, Madame Bovary et Faust) récupérés du site Gutenberg.
Packages utilisés¶
Les trois packages/ensemble de packages utilisés sont gutenbergr
,tidyverse
et gridExtra
(pour afficher des multi-plots).
[24]:
options(tidyverse.quiet = TRUE) # omettre les warnings pour ne pas encombrer le document
library(gutenbergr)
library(tidyverse)
library(gridExtra)
options(gridExtra.quiet = TRUE)
# center plot title
theme_update(plot.title = element_text(hjust = 0.5))
Workflow général¶
On prédéfinit les titres des 3 œuvres qui seront proposées à l’utiliseur et produit par la suite un dataframe contenant les métadatas
eng.title <- "Wuthering Heights"
fr.title <- "Madame Bovary"
de.title <- "Faust: Der Tragödie erster Teil"
# Get meta infos
metas <- get_books_meta(eng.title, fr.title, de.title)
On définit une fonction main qui comprend toutes les étapes restantes allant de la saisie de l’utilisateur jusqu’à la génération et de la sauvegarde des graphes.
main(metas)
Explication de la fonction get_books_meta¶
La fonction get_books_meta
prend les titres des 3 oeuvres comme entrées et produit un dataframe des informations des 3 œuvres en question en filtrant les métadatas du package guternbergr
sur les colonnes titre
et language
.
[7]:
get_books_meta <- function(eng, fr, de) {
# This function takes titles of 3 books in english
# french and german and returns a dataframe of meta information
english.meta <- gutenberg_metadata %>%
filter(title == eng) %>%
filter(language == "en")
french.meta <- gutenberg_metadata %>%
filter(title == fr) %>%
filter(language == "fr")
german.meta <- gutenberg_metadata %>%
filter(title == de) %>%
filter(language == "de")
return(rbind(english.meta, french.meta, german.meta))
}
eng.title <- "Wuthering Heights"
fr.title <- "Madame Bovary"
de.title <- "Faust: Der Tragödie erster Teil"
# Get meta infos
metas <- get_books_meta(eng.title, fr.title, de.title)
metas
gutenberg_id | title | author | gutenberg_author_id | language | gutenberg_bookshelf | rights | has_text |
---|---|---|---|---|---|---|---|
<int> | <chr> | <chr> | <int> | <chr> | <chr> | <chr> | <lgl> |
768 | Wuthering Heights | Brontë, Emily | 405 | en | Gothic Fiction/Movie Books/Best Books Ever Listings | Public domain in the USA. | TRUE |
14155 | Madame Bovary | Flaubert, Gustave | 574 | fr | Best Books Ever Listings/FR Littérature/Banned Books from Anne Haight's list | Public domain in the USA. | TRUE |
2229 | Faust: Der Tragödie erster Teil | Goethe, Johann Wolfgang von | 586 | de | Harvard Classics/Best Books Ever Listings/DE Drama | Public domain in the USA. | TRUE |
Explication de la fonction main¶
Dans
main
, on exécute la fonctionwelcome_message
qui affiche les informations des 3 oeuvres.
[10]:
welcome_message <- function(metas) {
# This functions takes a df of metainfo and prints the information in a human-friendly fashion #
cat("Here are the books in our database.\n\n")
print(sprintf("%-10s%-30s%10s", "book id", "book title", "language"))
print(sprintf("%-10d%-30s%10s", metas$gutenberg_id, metas$title, metas$language))
cat("\n\n\nTell us the book id on which you want to test the zipf's law.\n\n\n")
}
welcome_message(metas)
Here are the books in our database.
[1] "book id book title language"
[1] "768 Wuthering Heights en"
[2] "14155 Madame Bovary fr"
[3] "2229 Faust: Der Tragödie erster Teil de"
Tell us the book id on which you want to test the zipf's law.
Ensuite on demande à l’utilisateur de saisir le book id. Notez que si le book id ne figure pas dans la base de données on affiche un avertissement et relance la fonction
main
.
book.id <- readline()
# convert the book id into integer
book.id <- as.integer(book.id)
# check id validity
if (!(book.id %in% metas$gutenberg_id)) {
cat("\n\n\U0001f609!Book id not found!\U0001f609 Please type an id existing in our database.\n\n")
main(metas)
}
Si le book id figure dans la base de donnée on stocke le titre du livre et normalise le texte.
book.chosen <- metas %>% filter(gutenberg_id == book.id)
book.title <- book.chosen$title
# normalize the long text and convert to words
text <- normalize_text(book.id)
La fonction
normalize_text
fait notamment les choses suivantes :Normalisation proprement dite : transformer toutes lettres en minuscules, remplacer les non caractères (ponctuation) et les whitespaces par l’espace, supprimer les espaces au début et à la fin des lignes de textes, supprimer les espaces multiples, supprimer les lignes vides etc.
Coller les éléments du vecteur texte en un vecteur de caractères de longueur 1.
Découper le vecteur de caractères de longueur 1 en mots et retourner les mots.
[30]:
normalize_text <- function(id) {
# This function takes a book id and returns normalized words vector #
text <- gutenberg_download(id, mirror = "http://mirrors.xmission.com/gutenberg/")
body <- text$text
# tackle non english texts
body <- iconv(body, "latin1", "UTF-8")
body.lower <- tolower(body)
# replace whitespace characters/non words by space for easy split
body.lower <- gsub("[^a-zA-Z\\s]", " ", body.lower)
# remove spaces at the beginning and end of texts
body.lower <- gsub("(^ | +$)", "", body.lower)
# remove multiple spaces
body.lower <- gsub(" +", " ", body.lower)
# remove empty lines
body.lower <- body.lower[which(body.lower != "")]
# make the whole text a vector of length 1
body.v <- paste(body.lower, collapse = " ")
# split the text to words
body.words <- unlist(strsplit(body.v, "\\W+"))
return(body.words)
}
Ensuite on passe Ă la fonction
get_freq
qui retourne un dataframe dont les colonnes sont :mot
fréquence absolue
fréquence relative
rang
[12]:
get_freq <- function(text) {
# This function takes a words vector and returns a dataframe
# with word, count, relative freq, rank as columns
# get frequency table and convert to tibble
text.freq <- table(text)
sorted_text.freq <- sort(text.freq, decreasing = TRUE)
df <- as_tibble(sorted_text.freq)
# reaarange the tibble to get rank info and relative frequency
df <- df %>%
rename(word = text) %>% # rename the default text column to word
mutate(total = sum(n)) %>% # create a total n column
mutate(
rank = row_number(), # create rank column
`term frequency` = n / total # create the relative frequency column
)
return(df)
}
Ensuite la fonction
make_plots
génère un objet liste comprenant 3 graphes :Le premier affiche les 20 mots les plus fréquents du texte.
Le deuxième affiche la distribution des fréquences relatives.
Le troisième affiche sur l’axe X le logarithme du rang de chaque mot et l’axe Y le logarithme de sa fréquence relative.
[36]:
make_plots <- function(df, title) {
# This function takes a df and title
# with 3 plots related to Zipf's law as output
# Plot scatterplot of top20 words (word vs freq)
p.top20 <- ggplot(df[1:20, ], aes(x = reorder(word, -n),
y = n, group = 1, label = word)) +
geom_text(check_overlap = TRUE) + # declare labels
ggtitle(paste("Top 20 most common words in", title)) +
xlab("Word") +
ylab("Absolute frequency")
# plot the non-log relative frequency distribution
p.nonlog <- ggplot(df, aes(`term frequency`)) +
geom_histogram(show.legend = FALSE) +
xlim(NA, 0.0005) +
ggtitle(paste("Relative frequency distribution of words in", title)) +
xlab("Relative frequency") +
ylab("Percentage")
# plot the log zip's distribution
p.log <- ggplot(df, aes(rank, `term frequency`)) +
geom_line(size = 1.1, alpha = 0.8, show.legend = FALSE) +
scale_x_log10() +
scale_y_log10() +
ggtitle(paste("Standard plot of Zipf's law using words in", title)) +
xlab("Relative frequency (log)") +
ylab("Rank")
return(list(p.top20, p.nonlog, p.log))
}
Ensemble de la fonction main¶
[37]:
main <- function(metas) {
# This function asks the user to choose a book id
# displays top 20 most common words
# plot and save 3 plots illustrating the zipf's law
welcome_message(metas)
book.id <- readline()
# convert the book id into integer
book.id <- as.integer(book.id)
# check id validity
if (!(book.id %in% metas$gutenberg_id)) {
cat("\n\n\U0001f609!Book id not found!\U0001f609
Please type an id existing in our database.\n\n")
main(metas)
} else {
book.chosen <- metas %>% filter(gutenberg_id == book.id)
book.title <- book.chosen$title
# normalize the long text and convert to words
text <- normalize_text(book.id)
# get frequency table as dataframe
df.freq <- get_freq(text)
# show top 20 most common words
cat(paste0("\n\n\nThe book you chose was ", book.title, ".\n\n\n"))
cat("Here is the list of the top 20 most frequent words in this text.\n\n\n")
print(df.freq[1:20, ])
# make plots
plots <- make_plots(df.freq, book.title)
# combine and show all 3 graphes
grid.arrange(plots[[1]], plots[[2]], plots[[3]], nrow = 3)
# save the plot we don't use it for the jupyter notebook illustrated on website
# g <- arrangeGrob(plots[[1]], plots[[2]], plots[[3]], nrow = 3)
# ggsave(file = paste0("zipf_", book.title, "_xiaoouWang.png"), g)
}
}
Interprétations des graphes¶
On utilisel ici le livre Faust
comme exemple principal.
[38]:
main(metas)
2229
Here are the books in our database.
[1] "book id book title language"
[1] "768 Wuthering Heights en"
[2] "14155 Madame Bovary fr"
[3] "2229 Faust: Der Tragödie erster Teil de"
Tell us the book id on which you want to test the zipf's law.
The book you chose was Faust: Der Tragödie erster Teil.
Here is the list of the top 20 most frequent words in this text.
# A tibble: 20 x 5
word n total rank `term frequency`
<chr> <int> <int> <int> <dbl>
1 und 919 33225 1 0.0277
2 ich 701 33225 2 0.0211
3 die 670 33225 3 0.0202
4 der 607 33225 4 0.0183
5 nicht 426 33225 5 0.0128
6 das 402 33225 6 0.0121
7 ein 399 33225 7 0.0120
8 ist 390 33225 8 0.0117
9 zu 380 33225 9 0.0114
10 du 319 33225 10 0.00960
11 in 309 33225 11 0.00930
12 sie 304 33225 12 0.00915
13 es 302 33225 13 0.00909
14 so 292 33225 14 0.00879
15 mephistopheles 283 33225 15 0.00852
16 den 279 33225 16 0.00840
17 mit 274 33225 17 0.00825
18 sich 267 33225 18 0.00804
19 faust 266 33225 19 0.00801
20 mir 264 33225 20 0.00795
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Warning message:
“Removed 256 rows containing non-finite values (stat_bin).”
Warning message:
“Removed 1 rows containing missing values (geom_bar).”
6.1 Graphe des 20 mots les plus courants¶
On remarquera surtout que les tokens les plus fréquents sont des mots fonctionnels, excepté faust
et mephistopheles
qui sont les deux personnages principaux de l’histoire.
6.2 Gaphe de la distribution des fréquences relatives¶
On remarquera surtout que les tokens fréquents (à gauche) et les tokens rares (à droite) coexistent dans le même texte.
La distribution n’est pas du tout une distribution normale. Elle a une longue queue (beaucoup de mots rares). Les mots les plus fréquents occupent déjà une grande partie du texte.
Il est Ă noter que le choix de binwidth
est important pour rendre le graph plus lisse, comme on peut remarquer sur le mĂŞme graphe de Wuthering Heights
(plus bas). Cela montre l’intérêt du graphe 3 qui illustre mieux et en termes plus concrets la loi de Zipf.
6.3 Graphe des logarithmes des rangs et des fréquences relatives¶
En effet, la loi de Zipf (d’abord formulée par Jean-Baptiste Estoup) stipule que dans un texte donné, la fréquence d’occurrence f(n)
(nous prenons ici la fréquence relative) d’un mot est liée à son rang n
dans l’ordre des fréquences par une loi de la forme
oĂą K est une constante.
Si l’on prend les logarithmes de chaque côté, la relation devient linéaire et théoriquement on doit observer une ligne droite, ce qui n’est pas le cas ici. Cela montre que bien que la formule de Zipf donne l’allure générale des courbes, elle en représente très mal les détails
(Mandelbrot (1954: 12)).
Références¶
Mandelbrot, Benoit. 1954. Structure formelle des textes et communcation. Word 10:1–27.
Cours de Guillaume Desagulier intitulé linguistique outillée et traitements statistiques : https://corpling.modyco.fr/workshops/M2TAL/2.descriptive.stats.html