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:
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:
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:
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:
e, al solito, head() per avere i primi risultati:
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:
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:
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:
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().
che non e di mi è il a la un
359 295 272 244 214 186 178 164 159 158
Possiamo scegliere quanti termini visualizzare:
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"):
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"):
# 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.
Date le dimensioni della tabella, potrebbe essere preferibile salvare i risultati in un oggetto da esplorare con calma:
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.
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:
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:
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:
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:
[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:
[1] "snowball" "stopwords-iso" "misc" "smart"
[5] "marimo" "ancient" "nltk" "perseus"
Per avere l’elenco dei termini inclusi in una lista specifica:
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”:
[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:
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():
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:
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") 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
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:
# 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:
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)
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:
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)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
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:
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:
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:
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:
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)")
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.
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).
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”:
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():
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:

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

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:
[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.
[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,
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.
Figura 18: 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:
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 esempiosize = 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:
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:
(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
(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
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)| 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 | ÿ tr
| 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”.
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
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):
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:
ovvero:
Dunque, a partire dal dataframe del testo:
5.8 La matrice testuale
| 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() :
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:
$fonte
[1] "angolotesti.it"
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
| 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. § ??):
# 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.dfmDocument-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.
data frame con 0 colonne e 3 righe
Laddove servano per le analisi successive, esse potranno essere aggiunte, come per gli oggetti tokens (§ 4.4.4):
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')
Vedi anche: https://www.agnesevardanega.eu/wiki/r/gestione_dei_dati/dplyr_count_tally↩︎
Esistono anche altri pacchetti per la creazione di wordcloud.↩︎
Si tratta di una espressione multilessicale; cfr. oltre, §5.7↩︎
Categorie e colori sono stati attributi alle barre “a mano”. Il grafico è stato prodotto in fase esplorativa, con un minimo di trattamento del testo.↩︎
Di conseguenza, è possibile usare anche Tidytext (cfr. § 4.5.6), dove però si pone il problema di come estrarre gli n-grammi senza stopword.↩︎
Vedi: https://www.agnesevardanega.eu/wiki/r/analisi_bivariata/regressione_logistica.↩︎