Capitolo 5 L’esplorazione del corpus

Testi e dati degli esempi (cartella Proton Drive, in aggiornamento). Gli script che seguono presuppongono che testi e dati siano in una sottocartella “dati”.

5.1 Introduzione

Gli strumenti e le funzioni presentati di seguito sono fra i primi ad essere usati dopo il parsing del testo, in quanto servono:

  • individuare particolarità dei testi, quali termini, nomi di luoghi o di persone (entità), omografie da disambiguare ed errori sfuggiti nella prima fase di preanalisi dei testi (Capitolo 2);

  • scegliere le parole vuote (stopwords);

  • individuare espressioni multilessicali, ovvero parole composte da più termini (come “New York” o “capo di Stato”);

  • costruire liste e dizionari da applicare ai fini della definitiva selezione e normalizzazione delle forme, per la costruzione di una matrice testuale adatta alle analisi successive.

5.2 L’analisi delle frequenze

A partire da uno qualunque dei dataframe, possiamo ottenere facilmente statistiche e grafici utilizzando le funzioni base di R, o quelle del tidyverse, in particolare dplyr e tidyr.

È il caso dei dataframe dei token di Tidytext, o di quelli prodotti da spaCy o TreeTagger. Si tratta infatti di long tables in cui ogni riga corrisponde ad un token, e che possono contenere altre variabili di cui potremmo studiare le distribuzioni.

Svariati output descrittivi di *Quanteda” sono – o sono facilmente trasformabili in – dataframe.

Partiamo dunque con alcuni esempi di distribuzioni di frequenze a partire da informazioni organizzate in tabella.

5.2.1 Da un dataframe: le funzioni di base e del tidyverse

La funzione table

La funzione base di R per ottenere il conteggio delle frequenze è table().

Guardiamo le variabili presenti nel dataframe dei token prodotto con Tidytext:

head(tidy.sep.toks, 10)
readtext object consisting of 10 documents and 1 docvar.
# A data frame: 10 × 4
  doc_id       seg_id word     text     
* <chr>         <int> <chr>    <chr>    
1 sepolcri.txt      1 all      "\"\"..."
2 sepolcri.txt      1 ombra    "\"\"..."
3 sepolcri.txt      1 de       "\"\"..."
4 sepolcri.txt      1 cipressi "\"\"..."
5 sepolcri.txt      1 e        "\"\"..."
6 sepolcri.txt      1 dentro   "\"\"..."
# ℹ 4 more rows

Per avere il conteggio delle frequenze della variabile word, possiamo ad esempio usare table(), in questo modo:

table(tidy.sep.toks$word) %>% 
  head(10)

         a  abbraccia abbracciar    abduani      abita  accendono 
        38          1          1          1          1          1 
accennando accogliean    accolte accorrenti 
         1          1          1          1 

Il risultato è il vettore dei valori di frequenza (38, 1, 1, 1, …) con un nome (“a”, “abbraccia”, …): si tratta di un named vector.

Si noti che che le parole sono state tutte portate all’iniziale minuscola, e che i valori seguono l’ordine alfabetico dei termini. Per avere i primi 15 termini in ordine decrescente di frequenza, useremo la funzione sort(), con l’argomento decreasing = TRUE:

table(tidy.sep.toks$word) %>% 
  # ordine decrescente
  sort(decreasing = T) %>% 
  head(15)

  e  il   l  di  le   a che  la   i   d  de del ove non per 
124  49  48  43  41  38  36  35  28  26  16  16  15  14  14 

Il tidyverse: la funzione count

Per ottenere tabelle riassuntive con le funzioni del tidyverse, è per lo più sufficiente combinare le funzioni count(), summarise(), e, per introdurre una variabile di raggruppamento, group_by24:

Ad esempio, sempre a partire dal dataframe visto sopra, per avere le frequenze della variabile word useremo count() in questo modo:

tidy.sep.toks %>% 
  count(word)

e, al solito, head() per avere i primi risultati:

tidy.sep.toks %>% 
  count(word) %>% 
  head()
readtext object consisting of 6 documents and 0 docvars.
# A data frame: 6 × 3
  word           n text     
* <chr>      <int> <chr>    
1 a             38 "\"\"..."
2 abbraccia      1 "\"\"..."
3 abbracciar     1 "\"\"..."
4 abduani        1 "\"\"..."
5 abita          1 "\"\"..."
6 accendono      1 "\"\"..."

Anche qui i risultati seguono l’ordine alfabetico delle parole, e, per avere le 15 parole più frequenti, useremo l’argomento sort:

tidy.sep.toks %>% 
  count(word, sort = T) %>% 
  head(15)
readtext object consisting of 15 documents and 0 docvars.
# A data frame: 15 × 3
  word      n text     
* <chr> <int> <chr>    
1 e       124 "\"\"..."
2 il       49 "\"\"..."
3 l        48 "\"\"..."
4 di       43 "\"\"..."
5 le       41 "\"\"..."
6 a        38 "\"\"..."
# ℹ 9 more rows

Il tidyverse: la funzione group_by

Volendo conoscere il numero di types per segmento, dobbiamo prima indicare seg_id come variabile di raggruppamento (con group_by()), e poi calcolare il totale dei valori unici per ciascun gruppo (con summarise()).

tidy.sep.toks %>% 
  # rinominiamo seg_id
  group_by(segmento = seg_id) %>% 
  # rinominiamo la colonna delle frequenze
  summarise(tokens = n())
# A tibble: 56 × 2
   segmento tokens
      <int>  <int>
 1        1     19
 2        2     90
 3        3      4
 4        4     49
 5        5     21
 6        6     24
 7        7     77
 8        8     68
 9        9     17
10       10     64
# ℹ 46 more rows

Nota: Altri esempi di tabelle e grafici verranno resi disponibili in Appendice.

5.2.2 Quanteda: statistiche del corpus

Come si è visto, dal summary del corpus, possiamo ricavare un dataframe:

library(quanteda)
quanteda_options(language_stemmer ="italian")
# tabella di summary
summary(sanremo_21) %>% as.data.frame() %>% head()
                 Text Types Tokens Sentences           cantante
1                 Ora   119    304         1             Aiello
2               Dieci   148    416         2  Annalisa Scarrone
3  Potevi_Fare_Di_Più   194    404         1              Arisa
4         E_Invece_Sì   132    297         1               Bugo
5 Musica_Leggerissima   129    270         1 COLAPESCEDIMARTINO
6  Fiamme_Negli_Occhi   115    245         1          Coma Cose
               titolo
1                 Ora
2               Dieci
3  Potevi Fare Di Più
4         E Invece Sì
5 Musica Leggerissima
6  Fiamme Negli Occhi

e un grafico analogo a quello in Figura 5:

summary(sanremo_21) %>% 
  ggplot(aes(x = Text, y = Tokens, group = 1)) + 
  geom_line(aes(lty = "Tokens")) + 
  geom_line(aes(y = Types, lty = "Types")) +
  scale_x_discrete (labels = NULL) +
  labs(x = "Sanremo 2021: brani", y = "", lty = NULL)

La funzione textstat_summary() restituisce direttamente un dataframe con statistiche più dettagliate, per corpora e oggetti di classe tokens:

library(quanteda.textstats)
# corpus
textstat_summary(sanremo_21) %>% head()
             document chars sents tokens types puncts numbers symbols
1                 Ora  1515     1    304   110     15       0       0
2               Dieci  1997     2    416   133     22       0       0
3  Potevi_Fare_Di_Più  2017     1    404   184      4       0       0
4         E_Invece_Sì  1529     1    297   123     11       0       0
5 Musica_Leggerissima  1448     1    270   120     10       0       0
6  Fiamme_Negli_Occhi  1209     1    245   108      5       0       0
  urls tags emojis
1    0    0      0
2    0    0      0
3    0    0      0
4    0    0      0
5    0    0      0
6    0    0      0

la rappresentazione grafica di alcune di queste informazioni è presentata in Figura ??.

5.2.3 Quanteda: dalla matrice testuale

In Quanteda, possiamo ottenre le frequenze delle forme solo a partire da matrici testuali (§ 5.8.1).

Dal momento che però, nelle fasi iniziali dell’analisi (e non solo), si lavora sui token, sarà bene iniziare a familiarizzare con la funzione dfm() — che crea la matrice —, applicata sull’oggetto tokens su cui stiamo lavorando.

Una volta costruito il nostro oggetto tokens “sanremo_21.toks”:

# estraiamo i token
sanremo_21.toks <- tokens(sanremo_21,
                          remove_punct = T,
                          remove_numbers = T,
                          remove_symbols = T)

per iniziare ad esplorare le statistiche relative alle forme, senza voler costruire una matrice, useremo l’espressione dfm(sanremo_21.toks)

topfeatures

La funzione topfeatures(), applicata alla matrice (dfm(sanremo_21.toks)) produce una distribuzione delle frequenze dei termini, in ordine decrescente, analoga al risultato di table().

dfm(sanremo_21.toks) %>% 
  topfeatures() 
che non   e  di  mi   è  il   a  la  un 
359 295 272 244 214 186 178 164 159 158 

Possiamo scegliere quanti termini visualizzare:

dfm(sanremo_21.toks) %>% 
  topfeatures(n = 15) 
che non   e  di  mi   è  il   a  la  un  ma  ti  se  in  ho 
359 295 272 244 214 186 178 164 159 158 150 117 115  92  88 

ed avere una distribuzione di frequenza dei documenti in cui appare ciascun termine (scheme = "docfreq"):

dfm(sanremo_21.toks) %>% 
  topfeatures(n = 15, scheme = "docfreq") 
non   e che  di   a  mi  un  il  la   è  ma  in   l  le  ci 
 31  31  31  30  29  28  28  28  28  27  26  25  25  24  24 

Per trasformare questo vettore numerico in dataframe (ed utilizzarlo in ggplot, ad esempio) il metodo più semplice consiste nell’usare as_tibble(), che prevede un argomento per trasformare i nomi degli elementi in una colonna ed assegnare loro il nome(rownames = "feature"):

dfm(sanremo_21.toks) %>% 
  topfeatures(n = 10) %>% 
  as_tibble(rownames = "feature")
# A tibble: 10 × 2
   feature value
   <chr>   <dbl>
 1 che       359
 2 non       295
 3 e         272
 4 di        244
 5 mi        214
 6 è         186
 7 il        178
 8 a         164
 9 la        159
10 un        158

textstat_frequency

La funzione textstat_frequency() (del pacchetto quanteda.textstats), produce un dataframe di tutti i termini della matrice, indicando: frequenze semplici, frequenze per documento e ranking.

dfm(sanremo_21.toks) %>%
  textstat_frequency() 

Date le dimensioni della tabella, potrebbe essere preferibile salvare i risultati in un oggetto da esplorare con calma:

# tabella di tutte le frequenze
df.feat <- dfm(sanremo_21.toks) %>%
  textstat_frequency() 
# prime venti
head(df.feat, 20)
   feature frequency rank docfreq group
1      che       359    1      31   all
2      non       295    2      31   all
3        e       272    3      31   all
4       di       244    4      30   all
5       mi       214    5      28   all
6        è       186    6      27   all
7       il       178    7      28   all
8        a       164    8      29   all
9       la       159    9      28   all
10      un       158   10      28   all
11      ma       150   11      26   all
12      ti       117   12      22   all
13      se       115   13      23   all
14      in        92   14      25   all
15      ho        88   15      24   all
16     per        86   16      22   all
17      da        85   17      21   all
18    come        84   18      23   all
19      te        83   19      21   all
20       l        82   20      25   all

Variabili di raggruppamento

Nella tabella precedente, notiamo la presenza della colonna “group”, che indicherebbe la modalità di una eventuale variabile di raggruppamento.

Consideriamo l’esempio dell’articolo di Sanremo, segmentato.

art.toks <- art.corpus %>% 
  tokens(remove_punct = T, 
         remove_symbols = T, 
         remove_numbers = T)

La funzione docvars() consente — anche per le matrici testuali — di accedere alle variabili relative ai documenti (metadati), che vengono conservate durante la costruzione dell’oggetto dfm. Controlliamo le variabili utilizzabili:

docvars(dfm(art.toks))
  fonte       data     pattern
1  ansa 2020-02-05      Titolo
2  ansa 2020-02-05 Sottotitolo
3  ansa 2020-02-05    Articolo

Indichiamo pattern come variabile di raggruppamento:

dfm(art.toks) %>% 
  dfm_group(groups = pattern)
Document-feature matrix of: 3 documents, 175 features (63.43% sparse) and 3 docvars.
             features
docs          sanremo fiorello al festival mai più ospite magari in
  Articolo          2        2  3        3   0   2      1      1  5
  Sottotitolo       0        0  0        0   0   0      0      0  0
  Titolo            1        1  1        1   1   1      1      1  1
             features
docs          gara
  Articolo       1
  Sottotitolo    0
  Titolo         1
[ reached max_nfeat ... 165 more features ]

La matrice viene trasformata in modo da avere per riga i gruppi, anziché i documenti.

Allo stesso modo, possiamo indicare la variabile di raggruppamento all’interno della funzione:

dfm(art.toks) %>% 
  textstat_frequency(groups = pattern)  %>% 
  head()
  feature frequency rank docfreq    group
1      di         8    1       1 Articolo
2      mi         7    2       1 Articolo
3      ho         7    2       1 Articolo
4     che         7    2       1 Articolo
5      il         6    5       1 Articolo
6       e         6    5       1 Articolo

5.3 Le stopword, o parole vuote

Come si è visto nelle tabelle precedenti, le parole con le frequenze più alte sono prive di un significato sostativo.

Per studiare le frequenze, è dunque opportuno iniziare ad affrontare già in questa fase il secondo fondamentale step della normalizzazione, dopo il trattamento della punteggiatura (ed eventualmente delle maiuscole; cfr. § 4.4.2), ovvero quello delle stopword.

Le stopword sono le parole più comuni del linguaggio, e vengono dette “vuote” in quanto non veicolano un contenuto semantico autonomo (cfr. § ??). Tali liste, in linea di massima, comprendono:

  • punteggiatura
  • articoli, congiunzioni, interiezioni
  • verbi ausiliari
  • verbi fraseologici
  • pronomi personali (possono essere utili in alcuni casi).
  • alcuni avverbi

Premesso che «In analisi del testo volte non a una semplice ricerca di informazioni ma a un’analisi di contenuto, ogni stop list predefinita è tutta da verificare»(Bolasco 2013, 95), certamente la presenza di questi termini con elevate frequenze rende complicata l’analisi della distribuzione dei termini.

Nel processo di esplorazione del corpus e poi ancora di analisi dei testi, potranno essere identificati — ricursivamente — altri termini vuoti. La possibilità di gestire le liste e di applicarle in fase esplorativa senza modificare definitivamente il testo è di grande aiuto, in quanto altri interventi (identificazione delle espressioni multilessicali, disambiguazione degli omografi, lemmatizzazione) richiederanno di tornare al testo così com’è.

5.3.1 Le stopword in Quanteda

Un’ampia lista di stopword in varie lingue e proveniente da varie fonti è presente nel pacchetto stopwords (Benoit, Muhr, e Watanabe 2021), importato con Quanteda, e gestito dallo stesso sviluppatore:

stopwords::stopwords('it') %>% 
  head(20)
 [1] "ad"    "al"    "allo"  "ai"    "agli"  "all"   "agl"   "alla" 
 [9] "alle"  "con"   "col"   "coi"   "da"    "dal"   "dallo" "dai"  
[17] "dagli" "dall"  "dagl"  "dalla"

Nota: Le preposizioni articolate, in questa lista, non comprendono l’apostrofo. I parser di spacyr e di TreeTagger, invece, li lasciano.

Per conoscere le fonti:

stopwords::stopwords_getsources()
[1] "snowball"      "stopwords-iso" "misc"          "smart"        
[5] "marimo"        "ancient"       "nltk"          "perseus"      

Per avere l’elenco dei termini inclusi in una lista specifica:

stopwords::stopwords('it', 
                     source = "snowball") %>% 
  head(20)

Per l’italiano, sono disponibili le fonti “snowball”, “nltk” (equivalente a snowball), e “stopwords-iso”, che contiene però termini quantomeno problematici, quali ad esempio, fra le prime 20, “accidenti”:

stopwords::stopwords('it', 
                     source = "stopwords-iso") %>% 
  head(20)
 [1] "a"          "abbastanza" "abbia"      "abbiamo"    "abbiano"   
 [6] "abbiate"    "accidenti"  "ad"         "adesso"     "affinche"  
[11] "agl"        "agli"       "ahime"      "ahimã¨"     "ahimè"     
[16] "ai"         "al"         "alcuna"     "alcuni"     "alcuno"    

Le stopword possono essere richiamate con la funzione stopwords(), e rimosse dagli oggetti tokens con la funzione tokens_remove():

art.toks <- tokens_remove(art.toks, 
                          stopwords('it'))

Singoli termini che dovessero emergere come problematici nel seguito dell’analisi possono essere eliminati singolarmente con la stessa funzione:

art.toks <- tokens_remove(art.toks, 
                          c("Ve", "d"))
                          

Si tratta di una forma semplificata di tokens_select(), funzione che in questa fase potrebbe rivelarsi utile anche per altre operazioni, e a cui è bene quindi accennare:

art.toks <- tokens_select(art.toks, 
                          pattern = stopwords('it'), 
                          selection = 'remove')
                          

Ecco come cambia la distribuzione delle frequenze delle prime 15 parole nel corpus di Sanremo 2021, senza parole vuote:

sanremo_21.toks %>% 
  tokens_remove(stopwords('it')) %>% 
  dfm() %>%
  textstat_frequency() %>% head(10)
   feature frequency rank docfreq group
1       te        83    1      21   all
2      mai        82    2      15   all
3       me        67    3      15   all
4      ora        57    4      15   all
5     dire        55    5       9   all
6   quando        50    6      16   all
7     solo        44    7      15   all
8   sempre        44    7      10   all
9    fuori        43    9      12   all
10  niente        43    9      13   all

Notare che abbiamo escluso le stopword “al volo”, ovvero senza intervenire sull’oggetto token.

Volendo lavorare com dfm(sanremo_21.toks), possiamo usare allo stesso modo la funzione dfm_remove():

dfm(sanremo_21.toks) %>% 
  dfm_remove(stopwords('it')) %>% 
  textstat_frequency() %>% head(10)

Osservermo come “me” e “te”, pronomi personali, non siano presenti nella lista, diversamente da altri (io, tu, noi, ecc.).

5.3.2 Personalizzare le liste di stopword

A maggior ragione, dal momento che le espressioni arcaiche non sono incluse nella lista, troveremo numerose parole vuote nel testo dei Sepolcri, e non nella lista:

sep.toks %>% 
  tokens_remove(stopwords('it')) %>% 
  dfm() %>%
  textstat_frequency() %>% head(15)
   feature frequency rank docfreq group
1        d        26    1      18   all
2       de        16    2      16   all
3      ove        15    3      15   all
4       né         8    4       4   all
5       te         6    5       5   all
6    sotto         6    5       6   all
7    forse         5    7       5   all
8    morte         5    7       5   all
9     sole         5    7       4   all
10   terra         5    7       4   all
11  quando         5    7       5   all
12   ombre         5    7       5   all
13    quel         5    7       5   all
14    urne         4   14       4   all
15      me         4   14       2   all

Le liste usate in Quanteda sono vettori carattere, facilmente modificabili, aggiungendo o togliendo termini.

Costruiamo una lista personale (sw), a partire da una di quelle disponibili nel pacchetto:

sw <- stopwords::stopwords('it')

Ad essa si potranno via via aggiungere altri termini, in questo modo:

sw <- c(sw, "de", "ove", "né", "te", "me")

oppure toglierne:

sw <- sw[-c(633)]

Per rimettere le parole in ordine alfabetico, dopo le modifiche:

sw <- (str_sort(sw))

Per eliminare le parole ripetute:

sw <- str_unique(sw)

Lo studio delle frequenze semplici consente di individuare i termini da inserire in una lista personalizzata di stopwords. Ad esempio:

sep.toks %>%
  tokens_remove(stopwords('it')) %>%
  dfm() %>% 
  # controlliamo con una soglia di frequenza
  dfm_trim(min_termfreq = 4) %>% 
  textstat_frequency()
   feature frequency rank docfreq group
1        d        26    1      18   all
2       de        16    2      16   all
3      ove        15    3      15   all
4       né         8    4       4   all
5       te         6    5       5   all
6    sotto         6    5       6   all
7    forse         5    7       5   all
8    morte         5    7       5   all
9     sole         5    7       4   all
10   terra         5    7       4   all
11  quando         5    7       5   all
12   ombre         5    7       5   all
13    quel         5    7       5   all
14    urne         4   14       4   all
15      me         4   14       2   all
16   amore         4   14       4   all
17      dì         4   14       4   all
18    ossa         4   14       4   all
19   tombe         4   14       4   all
20     fra         4   14       4   all
21   lungo         4   14       4   all
22    capo         4   14       4   all
23   umane         4   14       3   all
24     lor         4   14       4   all
25    onde         4   14       4   all
26  patria         4   14       4   all

Nota: la funzione dfm_trim() riduce la matrice in base a criteri. In questo caso, abbiamo indicato la soglia di frequenza con min_termfreq = 4.

Volendo, i token formati da un solo carattere possono anche essere eliminati in blocco, con tokens_select(min_nchar=2L).

sep.toks %>% 
  tokens_remove(stopwords('it')) %>% 
  # conservare i token composti da almeno 2 caratteri
  tokens_select(min_nchar=2) %>% 
  dfm() %>%
  textstat_frequency() %>% head(5)
  feature frequency rank docfreq group
1      de        16    1      16   all
2     ove        15    2      15   all
3      né         8    3       4   all
4      te         6    4       5   all
5   sotto         6    4       6   all

Dal momento che in genere si adotta una soglia di frequenza delle forme pari a 4, e che le parole vuote hanno frequenze alte, dovrebbe essere sufficiente aggiungere alla nostra lista le seguenti forme: “d”, “de”, “ove”, “né”, “te”, “quel”, “me”, “fra”, e “lor” .

sw_2 <- stopwords::stopwords('it')

sw_2 <- c(sw_2, "d", "de", "ove", "né", "te", "quel", "me", "fra", "lor")
sep.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  textstat_frequency() %>% head(15)
   feature frequency rank docfreq group
1    sotto         6    1       6   all
2    forse         5    2       5   all
3    morte         5    2       5   all
4     sole         5    2       4   all
5    terra         5    2       4   all
6   quando         5    2       5   all
7    ombre         5    2       5   all
8     urne         4    8       4   all
9    amore         4    8       4   all
10      dì         4    8       4   all
11    ossa         4    8       4   all
12   tombe         4    8       4   all
13   lungo         4    8       4   all
14    capo         4    8       4   all
15   umane         4    8       3   all

Si tenga conto, che, ai fini della gran parte delle analisi, l’obiettivo non è individuare ed eliminare “tutte” le stopword, e che alcune possono anche essere trattate come “parole piene”, in funzione delle dimensioni semantiche o pragmatiche che si vogliano trattare. Nel caso riportato in § 5.5.3, ad esempio, abbiamo utilizzato l’espressione “secondo_me” come parola piena.

L’obiettivo è individuare le parole “vuote” rispetto agli obiettivi dell’analisi e con frequenze alte o medie, per poi concentrarsi semmai nella selezione di parole chiave.

  • frequenze semplici

  • frequenze dei termini + frequenze nei documenti (o idf)

5.3.3 Le stopword in Tidytext

library(tidytext)

Per eliminare le parole vuote da un dataframe di token, Tidytext ricorre alla funzione anti_join() del pacchetto dplyr: questa funzione filtra le righe di un dataframe in base ai termini contenuti in un secondo dataframe. I due dataframe devono avere una colonna in comune (con lo stesso nome). In pratica, per eliminare le righe delle parole vuote, serve non un vettore carattere, ma un dataframe di stopword.

Tidytext ne fornisce uno solo per la lingua inglese. Per costruirne uno con la nostra lista, controlliamo la struttura di quello base:

stop_words %>% head()
# A tibble: 6 × 2
  word      lexicon
  <chr>     <chr>  
1 a         SMART  
2 a's       SMART  
3 able      SMART  
4 about     SMART  
5 above     SMART  
6 according SMART  

La seconda colonna contiene le informazioni sulle fonti delle liste, e non è strettamente necessaria.

Possiamo costruire il dataframe delle stopword a partire dalla lista standard:

sw_df <- data.frame(word = quanteda::stopwords("it"))
head(sw_df)
  word
1   ad
2   al
3 allo
4   ai
5 agli
6  all

anti_join() confronta le righe di x e y in base ad un campo comune che possiamo indicare noi (by) o che individua automaticamente, ed elimina da x quelle presenti in y.

anti_join(x = tidy.sep.toks,
          y = sw_df,
          by = word) 
tidy.sep.toks %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) %>% head(10)
readtext object consisting of 10 documents and 0 docvars.
# A data frame: 10 × 3
  word      n text     
* <chr> <int> <chr>    
1 d        26 "\"\"..."
2 de       16 "\"\"..."
3 ove      15 "\"\"..."
4 né        8 "\"\"..."
5 sotto     6 "\"\"..."
6 te        6 "\"\"..."
# ℹ 4 more rows

Anche qui, possiamo individuare le stopword rimanenti in base alle frequenze:

tidy.sep.toks %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) %>% 
  filter(n > 3)
readtext object consisting of 26 documents and 0 docvars.
# A data frame: 26 × 3
  word      n text     
  <chr> <int> <chr>    
1 d        26 "\"\"..."
2 de       16 "\"\"..."
3 ove      15 "\"\"..."
4 né        8 "\"\"..."
5 sotto     6 "\"\"..."
6 te        6 "\"\"..."
# ℹ 20 more rows

e possiamo integrare il nostro dataframe, unendolo per riga (rbind()) a quello che andiamo a creare con i termini da aggiungere:

sw_df <- data.frame(word = c("d", "de", "ove", "né", "te", 
                    "quel", "me", "fra", "lor")) %>% 
  rbind(sw_df)
tidy.sep.toks %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) %>% head(10)
readtext object consisting of 10 documents and 0 docvars.
# A data frame: 10 × 3
  word       n text     
* <chr>  <int> <chr>    
1 sotto      6 "\"\"..."
2 forse      5 "\"\"..."
3 morte      5 "\"\"..."
4 ombre      5 "\"\"..."
5 quando     5 "\"\"..."
6 sole       5 "\"\"..."
# ℹ 4 more rows

5.3.4 Le stopword in spacyr

library(spacyr)
spacy_initialize(model = "it_core_news_sm")

Come si è visto nel capitolo precedente (§ 4.7.1), uno degli argomenti della funzione spacy_parse() serve ad estrarre alcuni attributi dei token previsti dalla libreria spaCy, ma non esplicitamente dalla funzioni. Fra questi, rientra anche il loro essere o meno stopword:

spacy_parse(sepolcri,
            additional_attributes = c("is_stop")) %>% 
  filter(pos != 'PUNCT' & pos != 'SPACE') %>% 
  select(c(4:6,8)) %>% 
  head(10)
        token      lemma   pos is_stop
1        All'       a il   ADP   FALSE
2       ombra      ombra  NOUN   FALSE
3          de         de   ADP   FALSE
4    cipressi   cipresso  NOUN   FALSE
5           e          e CCONJ    TRUE
6      dentro     dentro   ADV    TRUE
7          l'         il   DET    TRUE
8        urne       urne PROPN   FALSE
9  Confortate confortate PROPN   FALSE
10         di         di   ADP    TRUE

La lista è molto ampia, ma – oltre a non includere alcune parole vuote – contiene alcune parole piene (ad es.: “lasciato”), come si può constatare all’indirizzo https://github.com/explosion/spaCy/blob/master/spacy/lang/it/stop_words.py, oppure eseguendo il comando:

# per eseguire una riga di Python in R
reticulate::py_run_string("print(nlp.Defaults.stop_words)")

Alternativamente, una volta identificati le parti del discorso e i lemmi, possiamo portare il dataframe in Quanteda (come visto in § 4.7.1), o usarlo con un dataframe di stopword e la funzione anti_join() (cfr. § 5.3.3).

df <- spacy_parse(sepolcri) %>% 
  unnest_tokens(word, token) 
df %>% 
  # riordiniamo le colonne per controllare il risultato
  select(1:3,7, everything()) %>% 
  # prime righe per controllare
  head()
        doc_id sentence_id token_id     word    lemma   pos entity
1 sepolcri.txt           1        1      all     a il   ADP       
2 sepolcri.txt           1        2    ombra    ombra  NOUN       
3 sepolcri.txt           1        3       de       de   ADP       
4 sepolcri.txt           1        5 cipressi cipresso  NOUN       
5 sepolcri.txt           1        6        e        e CCONJ       
6 sepolcri.txt           1        7   dentro   dentro   ADV       

I token sono stati portati in minuscolo, e gli apostrofi sono stati eliminati. Usiamo il dataframe delle stopword, creato prima:

df %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) %>% head(10)
     word n
1   sotto 6
2   forse 5
3   morte 5
4   ombre 5
5  quando 5
6    sole 5
7   terra 5
8   amore 4
9    capo 4
10     dì 4

oppure, con i lemmi:

df %>% 
  anti_join(sw_df, by='word') %>% 
  count(lemma, sort = T) %>% head(10)
     lemma n
1    tomba 7
2    terra 6
3     Sole 5
4    amico 5
5    forse 5
6    ombra 5
7   quando 5
8    sotto 5
9    umano 5
10 amoroso 4

5.4 I grafici delle frequenze

I pacchetti sin qui considerati non offrono funzioni specifiche per i grafici di base, che sono abbastanza facilmente realizzabili con altre funzioni.

La distribuzione delle frequenze dei token (di solito, si tratta di parole) pone però problemi specifici, rispetto ad altri tipi di variabili e fenomeni, in quanto si tratta di rappresentare in uno spazio limitato e in maniera leggibile una grande quantità di informazioni .

5.4.1 Grafici a barre

I grafici a barre sono quelli naturalmente indicati per le frequenze di variabili categoriali. Nel caso dell’analisi testuale, essi richiedono però una notevole riduzione degli elementi da rappresentare.

Ad esempio, abbiamo rappresentato le quindici forme più frequenti nei Sepolcri (prima della normalizzazione), in Figura 6, a partire dal risultato della funzione topfeatures().

Dal momento che il risultato di questa funzione consiste in un named vector, per creare un grafico trasformiamo prima il risultato in un dataframe. Per l’esattezza, in questo caso lo trasformiamo in una tibble, per poter attribuire un nome alla colonna dei termini:

library(quanteda)
quanteda_options(language_stemmer ="italian")
sep.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  topfeatures() %>% 
  as_tibble(rownames = "Forme") %>% 
  ggplot(aes(reorder(Forme, -value), value)) +
  geom_col(fill = "darkorange") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
  labs(x = NULL, y = NULL,
       caption = "\"I Sepolcri\" (Quanteda)")

Altra questione che si pone con i grafici dei termini è la leggibilità dei testi. In questo caso, il problema è risolto ruotando le etichette dell’asse delle x, con theme(axis.text.x = element_text(angle = 90, hjust = 1)).

Ma le barre possono anche essere poste in orizzontale, e anzi, quando sono numerose, il grafico risulterà più leggibile. In questo caso, rappresentiamo 30 termini.

Creiamo il dataframe dei token, eliminando anche i numeri:

tidy.art.toks <- tidy.art %>%   
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  unnest_tokens(word, text,
                strip_numeric = TRUE)

Per eliminare le stopword, possiamo usare il dataframe creato e modificato sopra:

tidy.art.toks %>% 
  # eliminiamo le stopword
  anti_join(sw_df, by='word') %>% 
  count(word, sort=T) %>% 
  head(30) %>% 
  # grafico
  ggplot(aes(n, reorder(word, n))) +
  geom_col(fill = "darkorange") +
  labs(y = NULL, x = NULL, 
       caption = "Articolo su Sanremo (Tidytext)")
Barplot dei tokens

Figura 16: Barplot dei tokens

5.4.2 wordcloud

Da qualche anno si è diffuso l’uso delle wordcloud, grafici in cui i termini vengono disposti nello spazio in ordine casuale, con dimensioni proporzionali alle frequenze (semplici o ponderate).

Il pacchetto quanteda.textplots prevede la funzione textplot_wordcloud()25, che lavora sulla matrice testuale.

library(quanteda.textplots)

Il principale problema posto dalle wordcloud è la leggibilità dei risultati. A parte il fatto che la nuvola è composta casualmente, mentre la prossimità dei termini può indurre qualche tipo di distorsione cognitiva, si pone la questione del numero e della rilevanza dei termini da rappresentare.

Iniziamo con il porre una soglia di frequenza a 4 (argomento min_count = 4):

set.seed(345)
sanremo_21.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  # wordcloud
  textplot_wordcloud(min_count = 4,
                     # scegliere i colori
                     color = brewer.pal(8, "Dark2"),
                     # colori in ordine casuale
                     random_color = T)

Notiamo qui alcune parole vuote (avverbi), che hanno frequenze alte e sono diffuse in quasi tutti i documenti, e che potrebbero essere inserite nella nostra lista.

Un’alternativa è inserire una soglia sia per i termini, che per i documenti, con dfm_trim(), ad esempio scegliendo termini che abbiamo una frequenza minima di 4, e che siano presenti in almeno 3 testi: questo rende la nuvola meno fedele alla distribuzione delle frequenze semplici, ma forse più indicativa dei termini (e, forse, dei temi) che si ripetono nei testi.

set.seed(345)
sanremo_21.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  dfm_trim(min_termfreq = 4, min_docfreq = 3) %>% 
  textplot_wordcloud(color = brewer.pal(8, "Dark2"))

5.5 Analisi delle concordanze con Quanteda

Come si è detto nel capitolo terzo, per alcune attività è importante avere la possibilità di poter tornare ai testi originali, in qualunque momento. L’analisi delle concordanze rientra fra queste.

Sebbene possa sembrare complicato, si tratta di semplicemente di conservare il dataframe dei testi e/o il corpus (eventualmente segmentato, ed eventualmente corretto: si tratta di oggetti character x), e, in Quanteda, applicare le funzioni a:

  • tokens(x)
  • dfm(tokens(x))

Tidytext non prevede funzioni specifiche per l’esplorazione del testo.

5.5.1 Keywords in context (Kwic)

La funzione kwic() serve a ricercare stringhe in un oggetto tokens, e a visualizzarle nel loro contesto.

# una parola nel contesto
tokens(sanremo_21) %>% 
  kwic("Amore", 
       # termine così com'è
       valuetype = "fixed")
Keyword-in-context with 13 matches.                                                                     
                     [Dieci, 82]                  così Non fanno l' |
                    [Dieci, 219]                  così Non fanno l' |
                   [Arnica, 254]                una tempesta Ché l' |
                [Combat_Pop, 44] male Questo funerale Credevi fosse |
                [Combat_Pop, 80]                 no Alle canzoni d' |
               [Combat_Pop, 198]                 no Alle canzoni d' |
                      [Voce, 89]              da me Dove sei finita |
                     [Voce, 262]               ' io Dove sei finita |
                     [Voce, 390]      rendevo conto Dove sei finita |
             [Il_Farmacista, 10]                     ! ) Polvere d' |
             [Il_Farmacista, 42]    Son tutte soluzioni al naturale |
             [Il_Farmacista, 97]     c' è neppure controindicazione |
 [Quando_Ti_Sei_Innamorato, 208]               che brucia dentro L' |
                                 
 amore | nei film E forse non    
 amore | nei film E forse non    
 amore | si scopre solo in mezzo 
 amore | E invece era un coglione
 amore | , alle lezioni di stile 
 amore | , alle lezioni di stile 
 amore | Come non ci sei più     
 amore | Come non ci sei più     
 amore | Come non ci sei più     
 amore | , Té verde due bustine  
 Amore | mio, vedrai che male    
 Amore | mio, ti dirò come       
 amore | che mi dai è quello     

I risultati della ricerca non tengono conto di maiuscole e minuscole (case_insensitive = TRUE).

Per sfruttare al meglio le potenzialità di questo strumento, l’oggetto tokens dovrebbe essere costruito modificando al minimo i testi, ad esempio usando tokens(sanremo_21).

tokens(articolo$text) %>% 
  kwic("Fiorello", 
       valuetype = "fixed")
Keyword-in-context with 3 matches.                                               
   [text1, 5]    ##TITOLO Sanremo: | Fiorello |
  [text1, 73] il serio e il faceto | Fiorello |
 [text1, 142]  fatto morire", dice | Fiorello |
                                
 al festival mai più ospite     
 apre alla possibilità in un    
 nella sua incursione a sorpresa

Gli argomenti principali sono:

  window = 5,
  valuetype = c("glob", "regex", "fixed"),
  case_insensitive = TRUE

Con window possiamo specificare il numero di parole che precedono e seguono il pattern di ricerca (la finestra), mentre con valuetype specifichiamo in che modo debba essere interpretata la stringa di ricerca.

# solo i primi risultati (in totale sono 23)
tokens(sanremo_21) %>% 
  kwic("*amor*", window = 3,
       # glob-style wildcard (*)
       valuetype = "glob") %>% 
  head()
Keyword-in-context with 6 matches.                                                                   
           [Dieci, 82]      fanno l' |    amore    | nei film E    
          [Dieci, 219]      fanno l' |    amore    | nei film E    
    [E_Invece_Sì, 104]  dittatore S' |  innamora   | , vomita e    
    [E_Invece_Sì, 202]  dittatore S' |  innamora   | , vomita e    
    [E_Invece_Sì, 278]  dittatore S' |  innamora   | , vomita e    
 [Santa_Marinella, 56] niente di cui | innamorarsi | per sempre Per

Questa funzione è utile già in fase di organizzazione del corpus, ad esempio per mettere a punto un pattern di ricerca. Poniamo di voler cercare tutti i termini che abbiamo a che fare con “amore” e con “amare”:

tokens(sanremo_21) %>% 
  kwic("amar", window = 3,
       valuetype = "regex")

La stringa “amar” seleziona anche la parola “amaro”, mentre “amo” e “ama” sono esclusi dai risultati. Se cercassimo “amo”, troveremmo anche tutti i verbi alla seconda persona plurale.

Conviene qui usare un vettore carattere, invece che una sola stringa (e quindi eventualmente un dizionario per l’analisi del contenuto; vedi paragrafo ??) per affinare i risultati di ricerca:

# con un vettore
# solo i primi risultati (in totale sono 43)
parole <- c("*amor*", "amo", "ama", "amare")
tokens(sanremo_21) %>% 
  kwic(parole, window = 3,
       valuetype = "glob")

Per indicare che un pattern, o un vettore di pattern, è composto di più termini (frase o espressione multilessicale), usiamo la funzione phrase():

# con una frase
kwic(tokens(sanremo_21), phrase("ti am*"), 
                   valuetype = "glob")
kwic(tokens(sanremo_21), phrase("ti am*"), 
                   valuetype = "glob") %>% 
  as.data.frame()
                    docname from  to                        pre
1        Potevi_Fare_Di_Più  266 267   E chissà quanto tempo io
2                   Glicine  112 113 a non saper fingere Dentro
3                   Glicine  207 208 a non saper fingere Dentro
4                   Glicine  290 291 a non saper fingere Dentro
5  Quando_Ti_Sei_Innamorato  100 101      Quando mi hai detto “
6  Quando_Ti_Sei_Innamorato  183 184      Quando mi hai detto “
7                  Un_Rider   78  79        qui a dire ancora “
8                  Un_Rider  185 186        qui a dire ancora “
9                  Un_Rider  268 269   facciamo a dire ancora “
10                 Un_Rider  295 296        qui a dire ancora “
11                 Un_Rider  373 374        di un palazzo ed io
    keyword                       post pattern
1  ti amerò     ancora A che serve una  ti am*
2    ti amo e fuori tremo Come glicine  ti am*
3    ti amo e fuori tremo Come glicine  ti am*
4    ti amo e fuori tremo Come glicine  ti am*
5    ti amo    ” , confuso Dicesti non  ti am*
6    ti amo    ” , confuso Dicesti non  ti am*
7    Ti amo        ” Ho provato a fare  ti am*
8    Ti amo        ” Ho provato a fare  ti am*
9    Ti amo          ” ? A dire ancora  ti am*
10   Ti amo        ” Ho provato a fare  ti am*
11   ti amo      te lo giuro infatti a  ti am*

Visualizzando il risultato come dataframe (in questo caso, tibble), abbiamo accesso ad alcune informazioni aggiuntive, come la posizione del termine all’interno del documento (il numero indicato nelle colonne ‘from’ e ‘to’ è l’indice dei token).

Ad esempio, in Glicine i token “ti amo” sono il 112 e il successivo 113, il 207 e il 208, e ancora il 290 e il 291.

5.5.2 Grafici dei termini nel contesto (textplot xray)

I risultati della ricerca kwic possono quindi essere rappresentati graficamente in vari modi a partire dal dataframe dei risultati.

Nel pacchetto quanteda.texplots troviamo la funzione textplot_xray() che crea un grafico a dispersione dei termini nei documenti, in base a tali indici.

Questo è il grafico della distribuzione dei termini corrispondenti al pattern “amor”, nell’intero testo dei Sepolcri:

# un testo
textplot_xray(kwic(tokens(sep.corpus), "amor", valuetype = "regex"))

Se il corpus è composto di tanti documenti, o di un testo segmentato, otterremo un grafico che li mette a confronto:

# più testi
tokens(sanremo_21) %>% 
  kwic("amor", valuetype = "regex") %>% 
textplot_xray()

In questo caso, l’asse delle x rappresenta la posizione relativa dei token, in quanto i testi possono avere lunghezze diverse.

Possiamo infine confrontare più termini e più testi. Ad esempio, i pattern “amor” e “patr” nelle frasi dei Sepolcri:

textplot_xray(kwic(sep.toks, "amor", valuetype = "regex"),
              kwic(sep.toks, "patr", valuetype = "regex")) +
  labs(title = "Grafico di dispersione",
       y = "Segmento") 

Leggiamo i segmenti 31 e 34, in cui appaiono entrambi:

sep.corpus.s[[31]]

[1] “Lieta dell’ aer tuo veste la Luna Di luce limpidissima i tuoi colli Per vendemmia festanti, e le convalli Popolate di case e d’ oliveti Mille di fiori al ciel mandano incensi: E tu prima, Firenze, udivi il carme Che allegrò l’ ira al Ghibellin fuggiasco, E tu i cari parenti e l’ idioma Dèsti a quel dolce di Calliope labbro, Che Amore in Grecia nudo e nudo in Roma D’ un velo candidissimo adornando, Rendea nel grembo a Venere Celeste; Ma più beata che in un tempio accolte Serbi l’ Itale glorie, uniche forse Da che le mal vietate Alpi e l’ alterna Onnipotenza delle umane sorti, Armi e sostanze t’ invadeano, ed are E patria, e, tranne la memoria, tutto.”

Qui troviamo il termine “Amore” a metà (nel grafico la posizione è 0,5) e il termine “patria” alla fine.

sep.corpus.s[[34]]

[1] “Con questi grandi abita eterno: e l’ ossa Fremono amor di patria.”

Nel segmento 34, invece, li troviamo entrambi alla fine (0.8 e 1 circa).

5.5.3 Esempio: kwic nelle interviste libere

Torniamo all’esempio del paragrafo 3.3.5. Nelle interviste, avevamo dato mandato agli intervistati di parlarci liberamente della loro esperienza di democrazia.

Una volta distinte le domande e le risposte, sono stati trattati solo i testi di queste ultime. Dal momento che, anche escludendo le stopword, le primissime parole in ordine di frequenza sono quelle trasversalmente associate al tema in oggetto, ovvero in pratica ciò di cui si parla in tutti i testi esaminati,

I 20 termini più diffusi nelle interviste, per categoria

Figura 17: I 20 termini più diffusi nelle interviste, per categoria

Non stupisce quindi che i primi termini che troviamo sono dunque democrazia”,”secondo_me“26, e ”dire” (Figura 1727). La parola più frequente in assoluto è stata però “persona/persone”, dato che, in prima ipotesi, poteva essere indicativo di un diffuso uso di categorie non politiche nel discorso degli intervistati.

Lo strumento delle keywords in context è stato usato per mettere a confronto la diffusione e soprattutto la distribuzione dei termini “person*” (persona e persone) e “democrazia” nei documenti.

Nella Figura 18 l’estensione delle interviste è rappresentata dalle barre bianche, e le linee nere mostrano in quali punti occorrono, rispettivamente, i termini. Nella Figura 19, invece, gli stessi dati sono rappresentati in scala relativa.

Grafico dei termini 'person*' e  'democrazia' nei testi

Figura 18: Grafico dei termini ’person*’ e ‘democrazia’ nei testi

Grafico dei termini 'person*' e  'democrazia' nei testi

Figura 19: Grafico dei termini ’person*’ e ‘democrazia’ nei testi

Si noterà come “democrazia” ricorra prevalentemente all’inizio e alla fine delle interviste, sollecitata dall’intervistatore, mentre “person*” si distribuisce lungo l’intera intervista. Le tre interviste in cui il termine “democrazia” ricorre lungo tutto il colloquio sono state realizzate a giovani attivi in politica.

5.6 Collocations

5.6.1 Con Quanteda

Un altro strumento per esplorare le concordanze fra parole, valutando in particolare le espressioni multilessicali, è rappresentato dalla funzione textstat_collocations().

Prendiamo in considerazione un nuovo esempio, quello del discorso dell’allora Presidente del Consiglio Giuseppe Conte del 2 novembre 2020, per la presentazione alle Camere delle misure di contenimento della seconda ondata del Covid-19 in Italia. Il testo è quello riportato dall’Ansa.

library(readtext)
# importiamo il testo
disc_conte <-readtext("dati/disc_conte_20201102.txt")
# costruiamo il corpus
d_conte.corpus <- disc_conte %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()

Come per l’analisi delle kwic, conviene iniziare da un oggetto tokens “grezzo”: in questo caso i token senza punteggiatura:

tokens(d_conte.corpus, remove_punct = T) %>% 
  textstat_collocations() %>% 
  head()
           collocation count count_nested length   lambda        z
1        rispetto alla     6            0      2 5.746459 9.226004
2 territorio nazionale     6            0      2 7.389956 9.082399
3           sulla base     4            0      2 7.359468 8.007471
4           alla prima     4            0      2 5.230314 7.838024
5              ad oggi     4            0      2 4.998989 7.828706
6             su tutto     5            0      2 8.322533 7.813797

La funzione restituisce un dataframe con le seguenti informazioni:

  • collocation, l’espressione multilessicale;
  • length: dimensioni dell’espressione; per default la funzione ricerca espressioni di 2 termini, ma è infatti possibile indicare un valore con l’argomento size, e anche cercare collocations di dimensioni diverse, ad esempio size = 2:3
  • count, frequenza (minimo 2 occorrenze, argomento min_count).

In termini di frequenze, il risultato è lo stesso di tokens_ngrams() (cfr. § 4.4.3)28:

tokens(d_conte.corpus, remove_punct = T) %>% 
  tokens_ngrams() %>% dfm() %>% topfeatures(n=5)
            di_un            con_il          tutto_il 
               10                 9                 8 
terapia_intensiva        di_rischio 
                8                 8 
tokens(d_conte.corpus, remove_punct = T) %>% 
  textstat_collocations() %>% 
  arrange(desc(count)) %>% 
  head(5)
          collocation count count_nested length    lambda        z
172             di un    10            0      2  1.783330 4.950439
21             con il     9            0      2  2.652615 6.929496
68         di rischio     8            0      2  3.238210 6.152879
85  terapia intensiva     8            0      2 11.802628 5.816219
203          tutto il     8            0      2  6.880156 4.711197

Nell’output della funzione, però, troviamo anche alcune importanti statistiche per la valutazione degli n-grammi:

  • count_nested: numero delle eventuali occorrenze dell’espressione all’interno di altre, più lunghe;
  • lambda e z (test di Wald), che indicano la significatività dell’espressione, rispetto alle possibili combinazioni alternative (si vedano i dettagli nella pagina di aiuto della funzione e in Blaheta e Johnson 2001);

Sul significato di lambda

Per size = 2, lambda è il log odds ratio della tabella 2 x 2 che considera le diverse combinazioni fra i due termini.

Si consideri ad esempio il caso di”terapia” e “intensiva”, termini che ricorrono 8 volte, e sempre assieme. Costruiamo la tabella delle combinazioni possibili, nel corpus senza punteggiatura e stopwords (lista sw_2; vedi oltre):

# totale tokens 2245
# tabella
tab <- matrix(c(8.5,0.5,0.5,(2245-8.5-1)), 2, 2, byrow = T) %>% 
  as.table()
dimnames(tab) <- list("terapia" = list("pres", "ass"), 
                      "intensiva" = list("pres", "ass"))
tab
       intensiva
terapia   pres    ass
   pres    8.5    0.5
   ass     0.5 2235.5

Abbiamo aggiunto alle frequenze osservate una correzione pari a 0,5 (argomento smoothing; suggerito in primo luogo per evitare le celle vuote). Eseguiamo la regressione logistica, ed estraiamo i coefficienti29:

glm(terapia ~ intensiva, data = tab, 
    weights = Freq,                 # pesi
    family = binomial)$coefficients                
 (Intercept) intensivaass 
   -2.833213    11.238581 

Il valore lambda calcolato con la procedura di Blaheta e Johnson, corretto dagli sviluppatori di Quanteda, è pari a 11.239028 (cfr. Tabella 11).

Consideriamo il caso di due termini che compaiono sia insieme che da soli: “territorio” (9 occorrenze in totale), “nazionale” (10),“territorio nazionale” (6).

tab <- matrix(c(6.5,3.5,4.5,(2245-6.5-3.5-4.5)), 2, 2, byrow = T) %>% 
  as.table()
dimnames(tab) <- list("territorio" = list("pres", "ass"), 
                      "nazionale" = list("pres", "ass"))

tab
          nazionale
territorio   pres    ass
      pres    6.5    3.5
      ass     4.5 2230.5
glm(territorio ~ nazionale, data = tab, weights = Freq,
    family = binomial)$coefficients
 (Intercept) nazionaleass 
  -0.3677248    6.8249429 

Qui il log odd ratio è 6.8249429, mentre lambda è pari 6.825391 (cfr. Tabella 11).

In questo caso, possiamo anche calcolare l’odds ratio, in quanto non ci sono celle vuote:

tab <- matrix(c(6,3,4,(2245-6-3-4)), 2, 2, byrow = T) %>% 
  as.table()
# odds ratio
fisher.test(tab)$estimate 
odds ratio 
  976.7698 
# log odds ratio
log(fisher.test(tab)$estimate)
odds ratio 
  6.884251 

5.7 Individuazione delle multiword

Possiamo usare lo strumento delle collocations per individuare espressioni multilessicali. A partire dai testi originali, potranno essere individuate anche espressioni multilessicali “vuote”, come “rispetto alla” (a, all, ecc.), o “sulla base di” (e “in base a”).

Trattandosi di un dataframe, possiamo ordinare e riorganizzare i risultati nei modi consueti:.

tokens(d_conte.corpus, remove_punct = T) %>% 
  textstat_collocations(min_count = 3) %>% 
  # ordinate per frequenza
  arrange(desc(count)) %>% 
  head(10)
            collocation count count_nested length    lambda        z
68                di un    10            0      2  1.783330 4.950439
21               con il     9            0      2  2.652615 6.929496
39           di rischio     8            0      2  3.238210 6.152879
48    terapia intensiva     8            0      2 11.802628 5.816219
80             tutto il     8            0      2  6.880156 4.711197
7          della salute     7            0      2  5.768107 7.701170
23            in questo     7            0      2  3.179700 6.795101
1         rispetto alla     6            0      2  5.746459 9.226004
2  territorio nazionale     6            0      2  7.389956 9.082399
43          posti letto     6            0      2  9.336630 6.056719

Di locuzioni quali “per l’appunto”, “in tutti i modi”, ecc. — e in funzione del tipo di analisi che si voglia condurre — potrebbe essere bene avere o costruire una lista, per rimuoverle e disambiguare le altre (“parte” e “modo”, ad esempio).

Usando la funzione dopo aver eliminato le parole vuote, possiamo trovare espressioni multilessicali “piene”, e anche entità. In questo caso, ordiniamo i risultati in base al valore di lambda:

tokens(d_conte.corpus, remove_punct = T) %>% 
  tokens_remove(sw_2) %>% 
textstat_collocations(min_count = 3) %>% 
  arrange(desc(lambda)) %>% 
  head(12)
ÿtr
Tabella 11: Quanteda: collocations
collocation count count_nested length lambda z
15 terapia intensiva 8 0 2 11,239028 5,538353
17 province autonome 3 0 2 10,353958 5,001176
9 istituto superiore 5 0 2 9,194719 5,721871
10 superiore sanità 5 0 2 9,194719 5,721871
14 inizio emergenza 4 0 2 8,994495 5,553990
13 posti letto 6 0 2 8,772645 5,690628
16 quadro epidemiologico 4 0 2 8,204697 5,291164
18 ministero salute 3 0 2 7,786328 4,984678
6 poco meno 3 0 2 7,644568 6,572517
5 prima ondata 4 0 2 6,938135 6,793015
19 regioni province 3 0 2 6,850287 4,471709
1 territorio nazionale 6 0 2 6,825391 8,387313

Un caso particolare è dato dall’uso di questo strumento per riconoscere le “entità”.

  • “istituto” e “sanità” hanno frequenze pari a 5, come “istituto superiore”, mentre “superiore” ricorre 7 volte, cioè due volte con il significato di “maggiore”.
kwic(tokens(d_conte.corpus), "superiore")
Keyword-in-context with 7 matches.                                                                  
  [disc_conte_20201102.txt, 230]         , curato dall' Istituto |
  [disc_conte_20201102.txt, 645] incremento dei casi di Covid-19 |
  [disc_conte_20201102.txt, 747]           Regioni e l' Istituto |
  [disc_conte_20201102.txt, 988]           i dati dell' Istituto |
 [disc_conte_20201102.txt, 1949]          ", redatto da Istituto |
 [disc_conte_20201102.txt, 2148]               invece, il dato è |
 [disc_conte_20201102.txt, 2505] ripeto elaborati dall' Istituto |
                                        
 Superiore | di Sanità, ha costretto    
 superiore | ai 150 contagi per ogni    
 superiore | di sanità, viene elaborato 
 Superiore | di Sanità, oltre il        
 Superiore | di Sanità e Ministero della
 superiore | alla media nazionale. Nella
 Superiore | di Sanità, dal ministero   
  • “ministero” occorre sempre insieme a “salute”, mentre non è vero il viceversa: creare una multiword permetterà di disambiguare i termini.

Troviamo inoltre: commissario Arcuri, presidente Fico, Regno Unito, Conferenza (delle) Regioni

kwic(tokens(d_conte.corpus), "salute")
Keyword-in-context with 9 matches.                                
  [disc_conte_20201102.txt, 739]
 [disc_conte_20201102.txt, 1955]
 [disc_conte_20201102.txt, 2512]
 [disc_conte_20201102.txt, 3167]
 [disc_conte_20201102.txt, 3208]
 [disc_conte_20201102.txt, 3432]
 [disc_conte_20201102.txt, 3469]
 [disc_conte_20201102.txt, 3640]
 [disc_conte_20201102.txt, 3741]
                                                 
 sono rappresentati il Ministero della | salute |
           di Sanità e Ministero della | Salute |
           Sanità, dal ministero della | Salute |
      con ordinanza del ministro della | Salute |
      con ordinanza del ministro della | Salute |
                  , motivi di studio o | salute |
                  , motivi di studio o | salute |
              della vita umana e della | salute |
       dilemma fra la protezione della | salute |
                                            
 , le Regioni e l                           
 e condiviso in sede di                     
 , dalla Conferenza delle Regioni           
 e dipenderà esclusivamente e oggettivamente
 sarà possibile poi uscire da               
 , situazioni di necessità.                 
 , situazioni di necessità.                 
 , che costituisce una precondizione        
 individuale e collettiva e la              

5.7.1 Trattamento in Quanteda: tokens_compound

Funzione tokens_compound()

tokens(d_conte.corpus) %>% 
  tokens_compound(phrase(c("ministero della salute", 
                           "ministro della salute"))) %>% 
kwic("salute")
Keyword-in-context with 4 matches.                                                                  
 [disc_conte_20201102.txt, 3422]            , motivi di studio o |
 [disc_conte_20201102.txt, 3459]            , motivi di studio o |
 [disc_conte_20201102.txt, 3630]        della vita umana e della |
 [disc_conte_20201102.txt, 3731] dilemma fra la protezione della |
                                             
 salute | , situazioni di necessità.         
 salute | , situazioni di necessità.         
 salute | , che costituisce una precondizione
 salute | individuale e collettiva e la      

Possiamo creare una lista di multiword, ad esempio:

my_mw <- c("terapia intensiva", "istituto superiore di sanità",
  "ministero della salute", "ministro della salute")

E poi usare la funzione. In questo caso, le stopword sono state tolte dopo, in quanto usiamo le preposizioni articolate nel pattern (anche se nulla vieta di usare “istituto superiore sanità” senza preposizione):

tokens(d_conte.corpus,
       remove_punct = T) %>% 
  # prima le multiword
  tokens_compound(phrase(my_mw)) %>% 
  # poi le stopword
  tokens_remove(sw_2) 

5.7.2 Le multiword in Tidytext

Se si vogliono trattare le multiword in una analisi che utilizza Tidytext, è necessario intervenire direttamente sul testo, e dunque far precedere questo lavoro alla tokenizzazione (nello script a fine capitolo è infatti inserita prima, in maniera che la procedura sia esposta in modo lineare).

Utilizzeremo la funzione str_replace_all(), con un vettore delle sostituzioni, tenendo conto che, di base, la funzione è case-sensitive.

Il vettore delle sostituzioni

Nel caso dei nomi di persona, costruiamo il vettore delle sostituzioni:

repl <- c(
  "Achille Lauro" = "Achille_Lauro",
  "Lorenzo Jovanotti" = "Lorenzo_Jovanotti",
  "De Filippi" = "De_Filippi",
  "Giuliano Sangiorgi" = "Giuliano_Sangiorgi"
)

repl
       Achille Lauro    Lorenzo Jovanotti           De Filippi 
     "Achille_Lauro"  "Lorenzo_Jovanotti"         "De_Filippi" 
  Giuliano Sangiorgi 
"Giuliano_Sangiorgi" 

Riprendendo l’esempio del paragrafo precedente, possiamo anche creare un named vector da un vettore carattere my_mw, in questo modo:

# vettore delle sostituzioni
my_mw2 <- my_mw %>% 
  str_replace_all(" ", "_")

# nomi 
names(my_mw2) <- my_mw

my_mw2
             terapia intensiva   istituto superiore di sanità 
           "terapia_intensiva" "istituto_superiore_di_sanità" 
        ministero della salute          ministro della salute 
      "ministero_della_salute"        "ministro_della_salute" 

Ricerca e sostituzione nel testo

Nel primo esempio, usiamo il vettore per le sostituzioni direttamente in str_replace_all:

articolo %>% 
  # correzione degli apostrofi
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  # multiwords
  mutate(text = str_replace_all(text, repl))

Nel caso del discorso di Conte, invece, è più sicuro effettuare la sostituzione ignorando maiuscole e minuscole (troviamo “Istituto Superiore di Sanità”, e “Istituto superiore di Sanità”).

Il vettore con i pattern di ricerca e sostituzione andrà usato — sempre in str_replace_all() — con la funzione fixed(), che prevede l’argomento ignore_case=TRUE:

str_replace_all(text, fixed(my_mw2, ignore_case=T))

ovvero:

mutate(text = str_replace_all(text, fixed(my_mw2,
                                          ignore_case=T)))

Dunque, a partire dal dataframe del testo:

disc_conte %>% 
  # correzione degli apostrofi
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  # multiword
  mutate(text = str_replace_all(text, 
                                fixed(my_mw2, ignore_case=T))) %>% 
  # tokenizzazione
  unnest_tokens(word, text, 
                strip_numeric = T) %>% 
  # stopword
  anti_join(sw_df, by='word') 

5.8 La matrice testuale

Tabella 12: Matrici testuali
pacchetto matrice funzione
Quanteda documenti termini (features) dfm()
co-occorrenze (feature co-occurrence) fcm()
tm documenti termini (dt) DocumentTermMatrix()
termini documenti (td) TermDocumentMatrix()
Tidytext Quanteda documenti termini cast_dfm()
tm documenti termini cast_dtm()
tm termini documenti cast_tdm()
koRpus documenti termini docTermMatrix()

5.8.1 Quanteda: La matrice documenti-termini

Per costruire la matrice documenti-termini (o documenti-forme) a partire da un oggetto tokens — generalmente senza punteggiatura, che nelle fasi successive dell’analisi non dovrebbe servire più — usiamo la funzione dfm() :

tokens(sep.corpus.s, remove_punct = T) %>% 
  dfm()
Document-feature matrix of: 56 documents, 1,014 features (96.93% sparse) and 0 docvars.
                features
docs             all ombra de cipressi e dentro l urne confortate di
  sepolcri.txt.1   1     1  1        1 1      1 1    1          1  1
  sepolcri.txt.2   0     0  0        0 5      0 1    0          0  1
  sepolcri.txt.3   0     0  0        0 0      0 0    0          0  0
  sepolcri.txt.4   0     0  0        0 7      0 3    0          0  1
  sepolcri.txt.5   0     0  0        0 0      0 1    0          0  1
  sepolcri.txt.6   0     0  1        0 0      0 1    0          0  0
[ reached max_ndoc ... 50 more documents, reached max_nfeat ... 1,004 more features ]

I termini sono ridotti a 1.014 in quanto — indipendentemente da come abbiamo costruito l’oggetto tokens — vengono normalizzati in minuscolo.

Gli argomenti della funzione sono infatti:

dfm(
  x,
  tolower = TRUE,
  remove_padding = FALSE
)

tolower = TRUE significa appunto che tutti i token vengono ridotti a minuscole, mentre remove_padding = FALSE indica che gli spazi vuoti lasciati da eventuali token rimossi verranno mantenuti.

La matrice porterà con sé i metadati dell’oggetto tokens. Ad esempio:

tokens(sanremo_21, remove_punct = T) %>% 
  dfm() %>% meta()
$fonte
[1] "angolotesti.it"
tokens(sanremo_21, remove_punct = T) %>% 
  dfm() %>% docvars() %>% head()
            cantante              titolo
1             Aiello                 Ora
2  Annalisa Scarrone               Dieci
3              Arisa  Potevi Fare Di Più
4               Bugo         E Invece Sì
5 COLAPESCEDIMARTINO Musica Leggerissima
6          Coma Cose  Fiamme Negli Occhi
Tabella 13: Alcune funzioni per la trasformazione delle matrici testuali (Quanteda)
Funzione
dfm_group() raggruppa i documenti in base a una variabile di raggruppamento
dfm_subset() riduce la matrice in base a condizioni (per riga: riduce i documenti)
dfm_trim() riduce la matrice in base ad una soglia di frequenza (per colonna: riduce i termini)

Le matrici di Quanteda possono essere trasformate in altri formati, con (https://www.rdocumentation.org/packages/quanteda/topics/convert)[convert()].

5.8.2 Tidytext: dai token alle matrici testuali

Un dataframe di token è già di per sé facilmente trasformabile in una matrice testuale con le funzioni del tidyverse.

Tidytext include funzioni specifiche per esportare un dataframe con il conteggio delle frequenze delle parole per documento in una matrice testuale del formato richiesto da Quanteda e tm.

Per le matrici di Quanteda, ad esempio, troviamo la funzione (https://www.rdocumentation.org/packages/tidytext/topics/cast_tdm)[cast_dfm()].

Il primo passo è dunque quello di calcolare le frequenze delle parole (count(word)) per ciascun documento o, in questo caso, segmento (group_by(seg_id); cfr. § ??):

tidy.art.toks %>% 
  group_by(seg_id) %>% 
  count(word) %>%
  head()
# A tibble: 6 × 3
# Groups:   seg_id [1]
  seg_id   word        n
  <chr>    <chr>   <int>
1 Articolo a           5
2 Articolo accordo     1
3 Articolo achille     1
4 Articolo agli        1
5 Articolo al          3
6 Articolo albergo     1

Poi usiamo la funzione indicando, nell’ordine: il campo dell’identificativo di documento (seg_id), quello dei termini (word) e quello dei valori, ovvero delle occorrenze (n):

tidy.art.dfm <- tidy.art.toks %>% 
  group_by(seg_id) %>% 
  count(word) %>% 
  cast_dfm(seg_id, word, n)
tidy.art.dfm
Document-feature matrix of: 3 documents, 175 features (63.43% sparse) and 0 docvars.
             features
docs          a accordo achille agli al albergo alla amadeus amici
  Articolo    5       1       1    1  3       1    1       2     1
  Sottotitolo 0       0       1    0  0       0    0       1     0
  Titolo      0       0       0    0  1       0    0       0     0
             features
docs          anche
  Articolo        3
  Sottotitolo     0
  Titolo          0
[ reached max_nfeat ... 165 more features ]

Le altre variabili, naturalmente, non sono state “portate” nella matrice come metadati.

docvars(tidy.art.dfm)
data frame con 0 colonne e 3 righe

Laddove servano per le analisi successive, esse potranno essere aggiunte, come per gli oggetti tokens4.4.4):

docvars(tidy.art.dfm) <- tidy.art[,c(1,2,4,5)]
docvars(tidy.art.dfm)
               doc_id      seg_id fonte       data
1 ansa_2020-02-05.txt      Titolo  ansa 2020-02-05
2 ansa_2020-02-05.txt Sottotitolo  ansa 2020-02-05
3 ansa_2020-02-05.txt    Articolo  ansa 2020-02-05

Allo stesso modo, possiamo passare dai dataframe di spaCy e TreeTagger ad una matrice testuale di Quanteda, eventualmente usando i lemmi.

tagged.sep@tokens %>% 
  # per elimare la punteggiatura e portare a minuscolo
  unnest_tokens(token, token) %>% 
  count(sntc, token) %>% 
  cast_dfm(sntc, token, n) 
Document-feature matrix of: 56 documents, 1,010 features (96.93% sparse) and 0 docvars.
    features
docs all cipressi confortate de della dentro di duro e forse
   1   1        1          1  1     1      1  1    1 1     1
   2   0        0          0  0     0      0  1    0 5     0
   3   0        0          0  0     0      0  0    0 0     0
   4   0        0          0  0     1      0  1    0 7     0
   5   0        0          0  0     0      0  1    0 0     0
   6   0        0          0  1     0      0  0    0 0     1
[ reached max_ndoc ... 50 more documents, reached max_nfeat ... 1,000 more features ]

La Tabella 12 mostra le funzioni di Tidytext per esportare i dataframe in matrici testuali.

5.9 Funzioni usate

Funzioni pacchetto
table() tabelle di frequenza e di contingenza base
sort() ordinamento dei valori base
count() conteggio di valori (frequenze) dplyr
group_by() definire una variabile di raggruppamento dplyr
summarise() sintetizzare un dataset in base a statistiche dplyr
ggplot(), aes() grafici ggplot2
geom_col() grafici a barre ggplot2
labs() titoli dei grafici ggplot2
theme() tema dei grafici ggplot2

5.10 Codice del capitolo

## Strumenti per l'analisi testuale e il text mining con R
## Agnese Vardanega avardanega@unite.it

## Capitolo 5 - Esplorazione del corpus

library(tidyverse)
library(quanteda)
quanteda_options(language_stemmer ="italian")

library(tidytext)

# DATI --------------------------------------------------------------------

# esempio Sanremo '21: tokens
sanremo_21.toks <- tokens(sanremo_21,
                          remove_punct = T,
                          remove_numbers = T,
                          remove_symbols = T)

# esempio articolo: tokens (Quanteda)
art.toks <- art.corpus %>% 
  tokens(remove_punct = T, 
         remove_symbols = T, 
         remove_numbers = T)

# esempio articolo: tokens (Tidytext)
tidy.art.toks <- tidy.art %>%   
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  unnest_tokens(word, text,
                strip_numeric = TRUE)

# esempio discorso Conte
# importiamo il testo
library(readtext)
disc_conte <-readtext("dati/disc_conte_20201102.txt")
# costruiamo il corpus
d_conte.corpus <- disc_conte %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()
# costruiamo l'oggetto tokens
d_conte.toks <- d_conte.corpus %>% 
  tokens(remove_punct = T,
         remove_numbers = T,
         remove_symbols = T) 

# ANALISI DELLE FREQUENZE -------------------------------------------------

# table
table(tidy.sep.toks$word) 
table(tidy.sep.toks$word) %>% 
  # ordine decrescente
  sort(decreasing = T) 

## tidyverse
# count (dplyr)
tidy.sep.toks %>% 
  count(word)

tidy.sep.toks %>% 
  count(word, sort = T)

# tokens per segmento: group_by e summarise
tidy.sep.toks %>% 
  # rinominiamo seg_id
  group_by(segmento = seg_id) %>% 
  # rinominiamo la colonna delle frequenze
  summarise(tokens = n())

## Quanteda

# summary
summary(sanremo_21) %>% as.data.frame()

library(quanteda.textstats)

# corpus: textstat_summary
textstat_summary(sanremo_21)

## dfm: topfeatures
dfm(sanremo_21.toks) %>% 
  topfeatures() 

dfm(sanremo_21.toks) %>% 
  topfeatures(n = 15) 

dfm(sanremo_21.toks) %>% 
  topfeatures(n = 15, scheme = "docfreq") 

## dfm: textstat_frequency
dfm(sanremo_21.toks) %>%
   textstat_frequency()

# tabella di tutte le frequenze
df.feat <- dfm(sanremo_21.toks) %>%
  textstat_frequency() 

# prime venti
head(df.feat, 20)

# groups
dfm(art.toks) %>% 
  dfm_group(groups = pattern)

dfm(art.toks) %>% 
  textstat_frequency(groups = pattern)  %>% 
  head()

# STOPWORD ----------------------------------------------------------------

## Quanteda
# pacchetto stopwords
stopwords::stopwords('it') %>% 
  head(20)

stopwords::stopwords_getsources()

stopwords::stopwords('it',
                      source = "snowball") %>%
   head(20)

stopwords::stopwords('it', 
                     source = "stopwords-iso") %>% 
  head(20)

# tokens_remove
sanremo_21.toks %>% 
  # eliminiamo le stopwords
  tokens_remove(stopwords('it')) %>% 
  dfm() %>%
  textstat_frequency() %>% head(10)

sep.toks %>% 
  tokens_remove(stopwords('it')) %>% 
  dfm() %>%
  textstat_frequency() %>% head(15)

## liste personalizzate di stopwords
sep.toks %>%
  tokens_remove(stopwords('it')) %>%
  dfm() %>% 
  # controllare con una soglia di frequenza
  dfm_trim(min_termfreq = 4) %>% 
  textstat_frequency()

sep.toks %>% 
  tokens_remove(stopwords('it')) %>% 
  # conservare i token composti da almeno 2 caratteri
  tokens_select(min_nchar=2) %>% 
  dfm() %>%
  textstat_frequency() %>% head(5)

# costruire una lista a partire da quella standard
sw_2 <- stopwords::stopwords('it')
# aggiungere altre stopword
sw_2 <- c(sw_2, "d", "de", "ove", "né", "te", "quel", "me", "fra", "lor")

sep.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  textstat_frequency() %>% head(15)

## Tidytext

# dataframe delle stopword
stop_words %>% head()

# costruire il dataframe con le stopword italiane
sw_df <- data.frame(word = quanteda::stopwords("it"))
head(sw_df)

# anti_join
tidy.sep.toks %>% 
  # eliminare le stopwords
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T)

tidy.sep.toks %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) %>% 
  # soglia di frequenza con filter
  filter(n > 3)

# aggiungiamo altre stopword al dataframe costruito
sw_df <- data.frame(word = c("d", "de", "ove", "né", "te", 
                    "quel", "me", "fra", "lor")) %>% 
  rbind(sw_df)

tidy.sep.toks %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T)

## spacyr
library(spacyr)
spacy_initialize(model = "it_core_news_sm")

# spacy_parse
spacy_parse(sepolcri,
            # crea una colonna che indica le stopword
            additional_attributes = c("is_stop")) 

# spacy_parse e anti_join
df <- spacy_parse(sepolcri) %>% 
  unnest_tokens(word, token) 

df %>% 
  anti_join(sw_df, by='word') %>% 
  count(word, sort = T) 

## koRpus e TreeTagger
detach("package:quanteda")
library(koRpus.lang.it)

tagged.sep.2 <- treetag(
  sepolcri$text, 
  format = "obj",                 
  lang="it",
  doc_id = "sepolcri",            
  sentc.end = c(".", "!", "?"),   
  stopwords = sw_2,                 # stopwords
  treetagger="manual",
  TT.options=list(
    path="C:/TreeTagger",   
    preset="it"
  )
)

# disattiviamo i pacchetti
detach("package:koRpus.lang.it")
detach("package:koRpus")


# GRAFICI DELLE FREQUENZE -------------------------------------------------

library(quanteda)
quanteda_options(language_stemmer ="italian")

# grafico delle forme (features) più frequenti: Quanteda
sep.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  topfeatures() %>% 
  as_tibble(rownames = "Forme") %>% 
  ggplot(aes(reorder(Forme, -value), value)) +
  geom_col(fill = "darkorange") +
  theme(axis.text.x = element_text(angle = 90, hjust = 1)) +
  labs(x = NULL, y = NULL,
       caption = "\"I Sepolcri\" (Quanteda)")

# grafico delle forme più frequenti: Tidytext
tidy.art.toks %>% 
  # eliminiamo le stopword
  anti_join(sw_df, by='word') %>% 
  count(word, sort=T) %>% 
  head(30) %>% 
  # grafico
  ggplot(aes(n, reorder(word, n))) +
  geom_col(fill = "darkorange") +
  labs(y = NULL, x = NULL, 
       caption = "Articolo su Sanremo (Tidytext)")

## Quanteda: wordcloud
library(quanteda.textplots)

set.seed(345)
sanremo_21.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  # wordcloud
  textplot_wordcloud(min_count = 4,
                     # scegliere i colori
                     color = brewer.pal(8, "Dark2"),
                     # colori in ordine casuale
                     random_color = T)
# oppure
set.seed(345)
sanremo_21.toks %>% 
  tokens_remove(sw_2) %>% 
  dfm() %>%
  # soglia di frequenza anche per i documenti
  dfm_trim(min_termfreq = 4, min_docfreq = 3) %>% 
  textplot_wordcloud(color = brewer.pal(8, "Dark2"))


# ANALISI DELLE CONCORDANZE (Quanteda) ------------------------------------

## kwic
# una parola nel contesto: corpus
tokens(sanremo_21) %>% 
  kwic("Amore", 
       # termine così com'è
       valuetype = "fixed")

# una parola nel contesto: testo
tokens(articolo$text) %>% 
  kwic("Fiorello", 
       valuetype = "fixed")

tokens(sanremo_21) %>% 
  kwic("*amor*", window = 3,
       # glob-style wildcard (*)
       valuetype = "glob") 

tokens(sanremo_21) %>%
 kwic("amar", window = 3,
      # regex, espressioni regolari
      valuetype = "regex")

# con un vettore
parole <- c("*amor*", "amo", "ama", "amare")
tokens(sanremo_21) %>%
 kwic(parole, window = 3,
      valuetype = "glob")

# con una frase
kwic(tokens(sanremo_21), phrase("ti am*"),
                  valuetype = "glob")

# risultato come dataframe
kwic(tokens(sanremo_21), phrase("ti am*"), 
                   valuetype = "glob") %>% 
  as.data.frame()

## grafici delle kwic
# un testo
textplot_xray(kwic(tokens(sep.corpus), "amor", valuetype = "regex"))

# più testi
tokens(sanremo_21) %>% 
  kwic("amor", valuetype = "regex") %>% 
textplot_xray()

# confronto fra parole chiave
textplot_xray(kwic(sep.toks, "amor", valuetype = "regex"),
              kwic(sep.toks, "patr", valuetype = "regex")) +
  labs(title = "Grafico di dispersione",
       y = "Segmento") 

## Collocations
# textstat_collocations
tokens(d_conte.corpus, remove_punct = T) %>% 
  textstat_collocations()

# individuare multiword "vuote"
# con le stopword, ordine di frequenza
tokens(d_conte.corpus, remove_punct = T) %>% 
  # soglia di frequenza
  textstat_collocations(min_count = 3) %>% 
  # ordinate per frequenza
  arrange(desc(count)) 

# individuare multiword
# senza stopword, valore di lambda
tokens(d_conte.corpus, remove_punct = T) %>%
  tokens_remove(sw_2) %>%
  textstat_collocations(min_count = 3) %>%
  # ordinate per valore di lambda
  arrange(desc(lambda))


# LE MULTIWORD ------------------------------------------------------------

## Quanteda
# tokens_compound
tokens(d_conte.corpus) %>% 
  tokens_compound(phrase(c("ministero della salute", 
                           "ministro della salute")))

# con una lista
my_mw <- c("terapia intensiva", "istituto superiore di sanità",
           "ministero della salute", "ministro della salute")

tokens(d_conte.corpus,
       remove_punct = T) %>% 
  # prima le multiword
  tokens_compound(phrase(my_mw)) %>% 
  # poi le stopword
  tokens_remove(sw_2) 

## Tidytext

# named vector delle sostituzioni
repl <- c(
  "Achille Lauro" = "Achille_Lauro",
  "Lorenzo Jovanotti" = "Lorenzo_Jovanotti",
  "De Filippi" = "De_Filippi",
  "Giuliano Sangiorgi" = "Giuliano_Sangiorgi"
)

# oppure, da un vettore carattere
my_mw2 <- my_mw %>% 
  str_replace_all(" ", "_")
# nomi 
names(my_mw2) <- my_mw

# sostituzione
articolo %>% 
  # correzione degli apostrofi
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  # multiwords
  mutate(text = str_replace_all(text, repl))

# sostituzione con fixed(..., ignore_case=T)
disc_conte %>% 
  # correzione degli apostrofi
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  # multiword
  mutate(text = str_replace_all(text, 
                                fixed(my_mw2, ignore_case=T))) %>% 
  # tokenizzazione
  unnest_tokens(word, text, 
                strip_numeric = T) %>% 
  # stopword
  anti_join(sw_df, by='word') 

  1. Vedi anche: https://www.agnesevardanega.eu/wiki/r/gestione_dei_dati/dplyr_count_tally↩︎

  2. Esistono anche altri pacchetti per la creazione di wordcloud.↩︎

  3. Si tratta di una espressione multilessicale; cfr. oltre, §5.7↩︎

  4. Categorie e colori sono stati attributi alle barre “a mano”. Il grafico è stato prodotto in fase esplorativa, con un minimo di trattamento del testo.↩︎

  5. Di conseguenza, è possibile usare anche Tidytext (cfr. § 4.5.6), dove però si pone il problema di come estrarre gli n-grammi senza stopword.↩︎

  6. Vedi: https://www.agnesevardanega.eu/wiki/r/analisi_bivariata/regressione_logistica.↩︎