19  Analyseren: frequenties en TF-IDF

19.1 Frequenties

De simpelste analyse-methode is het simpelweg tellen hoe vaak een term - vaak een woord of een combinatie van woorden, zoals een N-gram - in een of meerdere documenten voorkomt: de frequentie.

De frequentie kunnen we makkelijk bepalen door met de dplyr::group_by()-functie op de kolom met term te groeperen en vervolgens met de functie n() te bepalen wat de frequentie is.

Hieronder laten we zien hoe je dit voor de data in tidytext_woord en in tidytext_ngram:

19.1.1 Woord

freq_tidytext_woord <- tidytext_woord %>% 
  group_by(text) %>% 
  summarise(aantal = n()) %>% 
  arrange(desc(aantal)) # sorteren van hoog naar laag

Hieronder zie je de top 10 van meest voorkomende woorden:

# A tibble: 10 × 2
   text    aantal
   <chr>    <int>
 1 de       12695
 2 het       9461
 3 van       7606
 4 en        4209
 5 in        3884
 6 college   3801
 7 is        3532
 8 een       3070
 9 voor      2697
10 te        2668

Wat direct opvalt is dat er woorden tussen staan die niet nuttig lijken voor een tekstanalyse. Deze woorden worden vaak uit een dataset gefilterd met een zogenoemde lijst stopwoorden.1 Je kunt daarvoor zelf een stopwoordenlijst opstellen, maar voor het gemak gebruiken we een bestaande stopwoordenlijst. Met de anti_join()-functie kun je deze woorden vervolgens uit de lijst filteren. Ook staan er een aantal getallen en losse letters in de dataset. Deze filteren we er óók uit met de str_detect() en nchar()-functies:

stopwoorden <- read_csv("https://raw.githubusercontent.com/stopwords-iso/stopwords-nl/master/stopwords-nl.txt", col_names = F)

freq_tidytext_woord_filter <- freq_tidytext_woord %>% 
  anti_join(stopwoorden, by = join_by(text == X1)) %>% 
  filter(!str_detect(text, "[:digit:]|[:punct:]")) %>% 
  filter(nchar(text) > 1)

Na het filteren van de stopwoorden, de getallen en losse letters, zie je hieronder de 10 meest voorkomende woorden:

# A tibble: 10 × 2
   text         aantal
   <chr>         <int>
 1 college        3801
 2 haag           1290
 3 den            1278
 4 vragen         1075
 5 aangeven        750
 6 haagse          745
 7 gemeente        682
 8 gemeenteraad    664
 9 pagina          591
10 hart            490

Deze gefilterde dataset ziet er al veel bruikbaarder uit, maar het zijn nog steeds veel niet-inhoudelijke woorden waarvan je verwacht dat ze in raadsvragen voorkomen. Gegeven deze context zou je de stopwoordenlijst eventueel aan kunnen vullen met deze veel voorkomende maar niet-inhoudelijke woorden. In het geval van stukken uit de Haagse gemeenteraad zul je er mogelijk de namen van politieke partijen en termen als college, den, haag en gemeenteraad uit willen filteren.

19.1.2 N-gram

Ook voor N-grams geldt dat we deze willen filteren op stopwoorden, getallen en losse letters. Dit willen we echter pas doen nádat de N-grams zijn gegenereerd, omdat je anders N-grams maakt van woorden die niet in die volgorde in de tekst voorkomen.2 Hiervoor doorlopen we de volgende stappen:

  1. de N-gram opsplitsen in aparte variabelen;
  2. de afzonderlijke variabelen filteren op de aanwezigheid van stopwoorden;

Daarnaast wordt de dataset ook gefilterd op;

  1. woorden die getallen of losse leestekens bevatten;
  2. woorden van 1 karakter (losse letters);
  3. wanneer de verschillende woorden in de n-gram hetzelfde zijn;
  4. samenvoegen van de opgesplitste woorden in een 2-gram.
freq_tidytext_ngram <- tidytext_ngrams %>% 
  separate(text, into = c("woord_1", "woord_2"), sep = " ") %>% # stap 1
  anti_join(stopwoorden, by = join_by(woord_1 == X1)) %>% # stap 2
  anti_join(stopwoorden, by = join_by(woord_2 == X1)) %>% # stap 2
  filter(!if_any(c("woord_1", "woord_2"), # stap 3
                 ~ str_detect(., "[:digit:]|[:punct:]"))) %>%
  filter(if_any(c("woord_1", "woord_2"), ~ nchar(.) > 1)) %>% # stap 4 
  filter(woord_1 != woord_2) %>% # staop 5
  unite("text", woord_1, woord_2, sep = " ") %>% # stap 6
  group_by(text) %>%
  summarise(aantal = n()) %>%
  arrange(desc(aantal)) # sorteren van hoog naar laag

Hieronder zie je de top 10 van meest voorkomende 2-grams of bi-gram:

# A tibble: 10 × 2
   text                 aantal
   <chr>                 <int>
 1 den haag               1273
 2 college aangeven        694
 3 schriftelijke vragen    463
 4 volgende vragen         373
 5 ris nummer              343
 6 college bereid          323
 7 overeenkomstig art      271
 8 college bekend          260
 9 orde stelt              255
10 gemeenteraad ris        211

Óók bij deze 2-grams zie je voornamelijk woordcombinaties die waarschijnlijk in veel raadsvragen voorkomen en zou je ook hier de stopwoordenlijst kunnen aanvullen om deze veel voorkomende 2-grams uit de dataset te halen.

19.1.3 Het visualiseren met een tag-cloud

Een veelgebruikte methode om woordfrequenties of n-gram-frequenties te visualiseren is een zogenoemde tag-cloud. Hier zijn verschillende packages voor, maar de twee die het meest gebruikt worden zijn wordcloud en wordcloud2.

Met de package wordcloud maak je een statische tag-cloud en is daarmee geschikt voor statische publicatieformaten, zoals Word-bestanden en PDF. Je maakt een tag-cloud met de volgende code, waarbij we het object freq_tidytext_woord_filter met woordfrequenties gebruiken:

library(wordcloud)

freq_tidytext_woord_filter %>% 
  with(wordcloud(text, aantal, max.words = 100))

Met wordcloud2 maak je een interactieve tag-cloud. Als je met je muis over een woord of N-gram gaat, komt er bij te staan hoe vaak deze voorkomt en is daarmee geschikt voor interactieve publicaties, zoals een HTML-bestand of een RevealJS-presentatie. Je maakt een tag-cloud dan met de volgende code, waarbij we in dit voorbeeld de 2-gram in het object freq_tidytext_ngram gebruiken:

# Package installeren met install.packages("wordcloud2")
library(wordcloud2)

# freq_tidytext_woord %>% 
freq_tidytext_ngram %>% 
  arrange(desc(aantal)) %>% 
  head(100) %>% 
  wordcloud2()

19.2 TF-IDF

Frequenties van woorden of n-grams zijn erg interessant om gevoel bij de inhoud van 1 of meerdere teksten te krijgen. Wat zijn thema’s die in het geheel van de teksten veel voorkomen? Maar wat als je wilt weten waarin een specifieke tekst juist verschilt van de rest? Wat zijn de onderwerpen die voor dié specifieke tekst uniek zijn? Dat is waar TF-IDF van pas komt.

TF-IDF staat voor Term Frequency - Inverse Document Frequency en is een maat voor het belang van een term - vaak een woord, maar het kan net zo goed een N-gram zijn - voor een specifieke tekst gegeven het totaal van de teksten.(Tfidf, 2023) Hoe hoger de TF-IDF-score, hoe belangrijker de term.

Term Frequency (TF)

Het aantal keer dat een term in een specifieke tekst voorkomt, gedeeld door het totaal aantal termen in die specifieke tekst.

\[ Term\ Frequency = \frac{Aantal\ keer\ dat\ term\ in\ tekst\ voorkomt}{Totaal\ aantal\ termen\ in\ tekst} \]

Inverse Document Frequency (IDF)

\[ Inverse\ Document\ Frequency\ = \ln\ \left( \frac{Aantal\ documenten}{Aantal\ documenten\ waar\ de\ term\ in\ voorkomt} \right) \]

Term Frequency - Inverse Document Frequency (TF-IDF)

\[ TF-IDF = \frac{Term\ Frequency}{Inverse\ Document\ Frequency} \]

We gaan van elk woord in elke tekst in het object haagse_raadsvragen met de functie bind_tf_idf() de TF-IDF vaststellen. Deze functie vraagt een frequentie van het aantal keer dat een term in een specifiek document voorkomt, dus we groeperen elke tekst op een term en voegen daar een kolom aantal aan toe vóórdat we de functie toepassen:

tf_idf <- haagse_raadsvragen %>% 
  unnest_tokens(input = text, 
                output = text,
                token = "words") %>% 
  group_by(bestandsnaam, text) %>% 
  summarise(aantal = n()) %>% 
  ungroup() %>%
  bind_tf_idf(term = text, document = bestandsnaam, n = aantal)

Het resultaat van deze code is een object tf_idf met daarin per term per document de variabelen:

  • aantal = het aantal keer dat een term in een tekst voorkomt
  • tf = de Term Frequency
  • idf = de Inverse Document Frequency
  • tf_idf = de Term Frequency - Inverse Document Frequency

Met de zojuist berekende tf_idf-score kun je nu per tekst vaststellen wat de meest belangrijke woorden zijn gegeven het totaal van teksten. Voor de raadsvragen in het document 315211_RIS315211 Haags voedselbeleid.pdf zijn in vergelijking met alle andere raadsvragen uit 2023 de 10 belangrijkste woorden bijvoorbeeld:

tf_idf %>% 
  filter(bestandsnaam == "315211_RIS315211 Haags voedselbeleid.pdf") %>% 
  select(-bestandsnaam) %>% 
  arrange(desc(tf_idf)) %>% 
  head(10)
# A tibble: 10 × 5
   text           aantal      tf   idf tf_idf
   <chr>           <int>   <dbl> <dbl>  <dbl>
 1 voedsel             7 0.0143   4.62 0.0663
 2 voedselraad         4 0.00820  5.31 0.0436
 3 discussiepaper      3 0.00615  6.01 0.0369
 4 voedselbeleid       3 0.00615  6.01 0.0369
 5 actualisering       2 0.00410  6.01 0.0246
 6 programmabrief      2 0.00410  6.01 0.0246
 7 tiny                2 0.00410  6.01 0.0246
 8 werkagenda’s        2 0.00410  6.01 0.0246
 9 duurzaam            3 0.00615  3.70 0.0228
10 aanbod              3 0.00615  3.30 0.0203

Het kan natuurlijk óók interessant zijn om een verandering in het gebruik van termen door de tijd heen te meten. Hiervoor kun je bijvoorbeeld de TF-IDF per maand berekenen in plaats van per document. Hiervoor hebben we de maand waarin een vraag is gesteld aan het object haagse_raadsvragen gekoppeld in een nieuw object haagse_raadsvragen_maand.

tf_idf_maand <- haagse_raadsvragen_maand %>% 
  unnest_tokens(input = text, 
                output = text,
                token = "words") %>% 
  group_by(maand, text) %>% 
  summarise(aantal = n()) %>% 
  ungroup() %>%
  bind_tf_idf(term = text, document = maand, n = aantal)

Je kunt nu per maand kijken welke termen in díe maand belangrijk waren. Hieronder plotten we van de maanden januari tot men met april de 10 termen met de hoogste TF-IDF-score:3

tf_idf_maand %>% 
  filter(maand %in% c(1:4)) %>% 
  group_by(maand) %>% 
  slice_max(tf_idf, n = 10) %>% 
  ungroup() %>%
  mutate(text = reorder(text, tf_idf)) %>%
  ggplot(aes(tf_idf, text, fill = maand)) +
  geom_col(show.legend = FALSE) +
  labs(x = "tf-idf", y = NULL) +
  facet_wrap(~maand, ncol =2, scales = "free")

Het karakter van raadsvragen - ze gaan vaak in op de dagelijkse actualiteit - maakt dat een vergelijking over maanden heen misschien niet veel inzichten oplevert. Maar de groepering had ook op het niveau van politieke partijen gemaakt kunnen worden. Of de vraagsteller. Of je zou ook een analyse op andere documenten kunnen doen, bijvoorbeeld op jaarverslagen van scholen. Zie je verschil tussen provincies? Tussen openbaar en bijzonder onderwijs? Tussen basis en voortgezet onderwijs? De mogelijkheden zijn eindeloos!


  1. Lees op Text Mining with R meer over het gebruik van een stopwoordenlijst.↩︎

  2. Lees meer over het filteren van N-grams in het boek Tidy Text Mining with R.↩︎

  3. Je ziet dat sommige maanden méér dan 10 termen met de hoogste TF-IDF-score hebben. Dit komt omdat termen soms dezelfde TF-IDF-score hebben.↩︎