La loi de Zipf illustrée avec gutenbergr en R 🇫🇷¶

Xiaoou WANG

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¶

  1. 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)
  1. 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
A tibble: 3 Ă— 8
gutenberg_idtitleauthorgutenberg_author_idlanguagegutenberg_bookshelfrightshas_text
<int><chr><chr><int><chr><chr><chr><lgl>
768Wuthering Heights Brontë, Emily 405enGothic Fiction/Movie Books/Best Books Ever Listings Public domain in the USA.TRUE
14155Madame Bovary Flaubert, Gustave 574frBest Books Ever Listings/FR Littérature/Banned Books from Anne Haight's listPublic domain in the USA.TRUE
2229Faust: Der Tragödie erster TeilGoethe, Johann Wolfgang von586deHarvard Classics/Best Books Ever Listings/DE Drama Public domain in the USA.TRUE

Explication de la fonction main¶

  • Dans main, on exĂ©cute la fonction welcome_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).”
../images/linguistique_informatique_01_zipf_fr_18_2.png

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

\[f(n)=\frac{K}{n}\]

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