Capitolo 3 Documenti e corpora

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

3.1 Introduzione

L’insieme dei testi oggetto di analisi viene definito corpus (pl. corpora), espressione che, negli studi linguistici, indica «la riunione di più opere, raccolte e pubblicate con lo scopo di fornire serie ordinate e complete degli scritti di uno o più autori o intorno a determinate materie» (Vocabolario Treccani).

Per corpus intenderemo qui, in primo luogo, i testi importati e organizzati ai fini dell’analisi.

Corpus è però anche una classe di oggetti di R, che:

  • conservano i testi nella loro forma originaria;
  • le informazioni relative ai testi (metadati dei documenti);
  • e quelle sull’insieme dei documenti (metadati del corpus, come data, fonte, ecc.).

Il corpus consente «l’interazione fra il livello paradigmatico di studio del lessico e quello sintagmatico di analisi del discorso» (Bolasco 2013, 83): esso è dunque uno strumento che serve non solo ad organizzare i testi, ma anche a facilitare lo studio delle relazioni fra parole, testi ed eventualmente frammenti di testi.

Tabella 3: Caratteristiche dei corpora nei pacchetti
Quanteda tidytext tm
corpus no
metadati del corpus
metadati dei documenti (variabili)
segmentazione no

Un esempio di possibile uso dei corpora per la gestione e la trasformazione dei testi è illustrato nel § 3.3.5. Nel caso di testi molto brevi, come i messaggi di Twitter, invece, questo tipo di organizzazione potrebbe non essere necessaria (vedi § ??).

Di conseguenza, dei pacchetti che stiamo qui considerando:

  • tm lavora esclusivamente con oggetti corpus;

  • Quanteda li prevede, ma consente di lavorare anche direttamente con i token;

  • tidytext usa esclusivamente dataframe di token, costruiti a partire da dataframe di testi (3.2);

  • koRpus, nonostante il nome, non usa questa classe di oggetti, in quanto è dedicato all’analisi di singoli testi nella prospettiva della linguistica dei corpora (ma può essere integrato in tm: cfr. § 4.7.3).

Gli oggetti corpus di Quanteda e tm sono largamente compatibili fra di loro, benché sia necessario procedere ad una “conversione” per passare dall’uno all’altro.

Figura 15: Organizzazione dei documenti

3.2 L’importazione dei testi

L’organizzazione dei documenti ai fini del progetto di analisi inizia con le scelte relative all’importazione, ovvero quelle relative al formato dei dati — da cui dipendono i pacchetti e le funzioni che si potranno utilizzare.

Con il pacchetto readtext (Benoit e Obeng 2024), i documenti vengono importati in un dataframe (funzione readtext()), e in un formato compatibile con tutti i pacchetti qui considerati.

In particolare, di default, vengono attribuiti i seguenti nomi alle colonne:

  • doc_id per l’identificativo dei documenti;
  • text per il campo del testo da analizzare.

La funzione permette di importare file in diversi formati: txt, ma anche doc, docx, pdf, html, csv, tab, tsv, e altri.

3.2.1 Importare un solo testo

# chiamiamo il pacchetto
library(tidyverse)
library(readtext)

Importiamo il testo de I Sepolcri (file “sepolcri.txt”) dalla sottocartella “dati”:

# sepolcri
sepolcri <- readtext("dati/sepolcri.txt")

Nello spazio di lavoro, abbiamo ora un dataframe di classe “readtext”, composto di una riga (il testo), e le due variabili doc_id e text:

sepolcri
readtext object consisting of 1 document and 0 docvars.
# A data frame: 1 × 2
  doc_id       text               
  <chr>        <chr>              
1 sepolcri.txt "\"All'ombra \"..."

Importiamo anche il file “ansa_2020-02-05.txt”:

# articolo
articolo <- readtext("dati/ansa_2020-02-05.txt")
articolo
readtext object consisting of 1 document and 0 docvars.
# A data frame: 1 × 2
  doc_id              text               
  <chr>               <chr>              
1 ansa_2020-02-05.txt "\"##TITOLO S\"..."

3.2.2 Importare più testi organizzati in una cartella

Per importare un insieme di documenti contenuti in una cartella, è sufficiente indicare il percorso, e usare l’asterisco * come carattere jolly:

canzoni <- readtext('dati/canzoni/*.txt')
canzoni
readtext object consisting of 2 documents and 0 docvars.
# A data frame: 2 × 2
  doc_id               text               
  <chr>                <chr>              
1 01_Achille Lauro.txt "\"Achille La\"..."
2 02_Alberto Urso.txt  "\"Alberto Ur\"..."

In questo caso, il dataframe sarà composto di tante righe quanti sono i documenti contenuti nella cartella (nel nostro esempio, 2), e verrà assegnato come identificativo dei testi, nel campo doc_id, il nome del file (“01_Achille Lauro.txt”; “02_Alberto Urso.txt”).

3.2.3 Metadati dei documenti (variabili)

Le eventuali variabili relative ai singoli documenti (data, autore, intervistato, luogo, ecc.) possono essere importate da un file csv che le contiene (con un campo identificativo comune), o essere estrapolate dal nome o dal percorso del file7.

Ad esempio, possiamo ricavare le variabili del documento da nomi di file strutturati, ad esempio:

EU_euro_2004_de_PSE.txt

In questo caso, specificheremo che le variabili vanno estrapolate dal nome del file (docvarsfrom), e indicheremo il nome delle variabili (docvarnames):

readtext('dir',
          docvarsfrom = "filenames", 
          docvarnames = c("unit", "context", "year", "language", "party"))

Dove “unit” sarà il campo che conterrà il valore “EU”, “context” quello per “euro”, e così via.

Tornando all’esempio delle canzoni:

canzoni <- readtext('dati/canzoni/*.txt',
                    docvarsfrom = "filenames",
                    docvarnames = c("ID",
                                    "cantante"))

canzoni
readtext object consisting of 2 documents and 2 docvars.
# A data frame: 2 × 4
  doc_id               text                   ID cantante     
  <chr>                <chr>               <int> <chr>        
1 01_Achille Lauro.txt "\"Achille La\"..."     1 Achille Lauro
2 02_Alberto Urso.txt  "\"Alberto Ur\"..."     2 Alberto Urso 

3.2.4 La gestione dei documenti

Il fatto che i testi siano organizzati in un dataframe permette di usare le funzioni per la gestione dei dati, più note agli utilizzatori di R, dopo l’importazione.

Ad esempio, possiamo cambiare il nome dell’identificativo con rename():

canzoni %>% 
  rename('file' = doc_id)
readtext object consisting of 2 documents and 2 docvars.
# A data frame: 2 × 4
  file                 text                   ID cantante     
  <chr>                <chr>               <int> <chr>        
1 01_Achille Lauro.txt "\"Achille La\"..."     1 Achille Lauro
2 02_Alberto Urso.txt  "\"Alberto Ur\"..."     2 Alberto Urso 

Oppure, tornando all’esempio dell’articolo di giornale, una volta effettuata l’importazione e strutturate le variabili dei documenti, trasformiamo la variabile “data” nel più appropriato formato Date:

articolo <- readtext('dati/ansa_2020-02-05.txt',
                    docvarsfrom = "filenames",
                    docvarnames = c("fonte",
                                    "data"))

articolo$data <- as.Date(articolo$data)

articolo
readtext object consisting of 1 document and 2 docvars.
# A data frame: 1 × 4
  doc_id              text                fonte data      
  <chr>               <chr>               <chr> <date>    
1 ansa_2020-02-05.txt "\"##TITOLO S\"..." ansa  2020-02-05

3.2.5 Importare i testi da un dataset

Nel caso di testi brevi o molto brevi, come le risposte aperte a un questionario, o i messaggi di Twitter, i documenti sono generalmente raccolti e organizzati in un dataframe. In questo caso, potremmo voler usare le funzioni del pacchetto readr (o importare il dataset anche dal menu di RStudio).

Importiamo il dataset delle canzoni di Sanremo 2021 (“sanremo2021.csv”), presente, in formato tab-delimited (tsv, anche se con estensione .csv), nella cartella dati:

sanremo21 <- read_tsv("dati/sanremo2021.csv")
sanremo21 %>% head()
# A tibble: 6 × 3
  cantante           titolo              testo                        
  <chr>              <chr>               <chr>                        
1 Aiello             Ora                 "Ora ora ora ora\nMi parli c…
2 Annalisa Scarrone  Dieci               "Cos'è che ti ho promesso\nN…
3 Arisa              Potevi Fare Di Più  "Lasciarsi adesso non fa più…
4 Bugo               E Invece Sì         "Le metropolitane vanno molt…
5 COLAPESCEDIMARTINO Musica Leggerissima "Se fosse un'orchestra a par…
6 Coma Cose          Fiamme Negli Occhi  "Quando ti sto vicino sento\…

Nota. Nel seguito verrà spesso usata la funzione head() (di solito nella forma %>% head()) al solo scopo di ridurre la lunghezza dell’output stampato. La funzione consente di stampare infatti solo le prime righe degli oggetti, e non svolge alcun altro ruolo nell’analisi.

Dal momento che vorremo usare questo dataset con i pacchetti per l’analisi testuale, trasformiamo i campi secondo gli standard visti sopra, ovvero:

  • costruiamo una variabile doc_id con il titolo (mutate("doc_id" = str_replace_all(titolo, " ", "_")))8;

  • rinominiamo la colonna che contiene i testi delle canzoni, attribuendole il nome text .

sanremo21 <- sanremo21 %>% 
  # titolo come doc_id
  mutate("doc_id" = str_replace_all(titolo, " ", "_")) %>% 
  # rinominiamo il campo "testo"
  select(doc_id, "text" = testo, 
         cantante, titolo)
head(sanremo21)
# A tibble: 6 × 4
  doc_id              text                             cantante titolo
  <chr>               <chr>                            <chr>    <chr> 
1 Ora                 "Ora ora ora ora\nMi parli come… Aiello   Ora   
2 Dieci               "Cos'è che ti ho promesso\nNon … Annalis… Dieci 
3 Potevi_Fare_Di_Più  "Lasciarsi adesso non fa più ma… Arisa    Potev…
4 E_Invece_Sì         "Le metropolitane vanno molto v… Bugo     E Inv…
5 Musica_Leggerissima "Se fosse un'orchestra a parlar… COLAPES… Music…
6 Fiamme_Negli_Occhi  "Quando ti sto vicino sento\nCh… Coma Co… Fiamm…

Nota. Per aggiungere o modificare una variabile, usiamo qui mutate(). Vedi anche https://www.agnesevardanega.eu/wiki/r/tidyverse/dplyr_mutate:

Per rinominare la variabile e nello stesso tempo riorganizzare l’ordine delle variabili, usiamo la funzione select(), che serve a scegliere alcune variabili di un dataset, eventualmente rinominandole. Vedi anche: https://www.agnesevardanega.eu/wiki/r/tidyverse/dplyr_select

3.3 I corpora in Quanteda

3.3.1 La funzione corpus

Per costruire il corpus di Quanteda a partire dal dataframe dei testi importati, usiamo la funzione corpus():

library(quanteda)
# impostiamo l'italiano per il parsing dei testi
quanteda_options(language_stemmer ="italian") 
# e/o controlliamo che le opzioni siano corrette
quanteda_options()
# corpus
sanremo <- corpus(canzoni)

Volendo intervenire in questa fase sugli apostrofi, non riconosciuti come confini di parole (vedi § 4.2.1), inseriamo uno spazio dopo gli apostrofi non seguiti da uno spazio:

sanremo <- canzoni %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()
class(sanremo)
[1] "corpus"    "character"
typeof(sanremo)
[1] "character"
Tabella 4: Tipi di fonti per i corpora in Quanteda
oggetti importati metadati
dataframe doc_id, e altre variabili
vettore carattere nomi degli elementi del vettore
corpus, tm corpus metadati presenti

3.3.2 Codifica dei documenti

3.3.3 I metadati

I metadati — del corpus e dei documenti — sono conservati come attributi dell’oggetto: names, docvars e meta.

str(sanremo)
 'corpus' Named chr [1:2] "Achille Lauro\n\nMe Ne Frego\n\nSì\nNoi
 sì\nNoi che qui\nSiamo soli qui\nNoi sì\nSoli qui\nFai di me quel 
 che v"| __truncated__ ...
 - attr(*, "names")= chr [1:2] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
 - attr(*, "docvars")='data.frame': 2 obs. of  5 variables:
  ..$ docname_: chr [1:2] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
  ...
 - attr(*, "meta")=List of 3
  ..$ system:List of 6
  ...
  ..$ object:List of 2
  ...
  ..$ user  : list()

Metadati dei documenti

L’identificativo del documento (doc_id) diventa l’attributo names degli elementi contenuti nell’oggetto (come per i named vector, le liste e i dataframe).

 'corpus' Named chr [1:2] "Achille Lauro\n\nMe Ne Frego\n\nSì\nNoi
 sì\nNoi che qui\nSiamo soli qui\nNoi sì\nSoli qui\nFai di me quel 
 che v"| __truncated__ ...
 - attr(*, "names")= chr [1:2] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
 ...

Con la funzione docnames() — oppure con la funzione di base names() — possiamo accedere ed eventualmente modificare i nomi dei documenti che compongono il corpus:

docnames(sanremo)
[1] "01_Achille Lauro.txt" "02_Alberto Urso.txt" 
# non eseguire
docnames(sanremo) <- c("testo1", "testo2")
sanremo

Le altre variabili relative ai documenti costituiscono i veri e propri metadati di documento, e, qualora siano presenti nel dataframe dei testi, vengono importate come dataframe (attributo docvars).

 'corpus' Named chr [1:2] "Achille Lauro\n\nMe Ne Frego\n\nSì\nNoi
 sì\nNoi che qui\nSiamo soli qui\nNoi sì\nSoli qui\nFai di me quel 
 che v"| __truncated__ ...
 ...
 - attr(*, "docvars")='data.frame': 2 obs. of  5 variables:
  ..$ docname_: chr [1:2] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
  ...

Costruiamo, ad esempio, il corpus delle canzoni di Sanremo 2021 (sempre correggendo i confini delle parole):

sanremo_21 <- sanremo21 %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()

La funzione docvars() permette di accedere a questo dataframe, e dunque alle variabili, eventualmente modificandole:

# variabili
docvars(sanremo_21) %>% 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

Questo significa che, se le variabili dei documenti sono contenute in un dataset diverso da quello che contiene i testi, esse possono essere aggiunte o unite successivamente, purché naturalmente abbiano il campo “doc_id” o un’altra variabile in comune (si veda l’esempio nel §3.3.5).

Per accedere ad una singola variabile, possiamo scrivere:

docvars(sanremo_21)$cantante
[1] "Aiello"             "Annalisa Scarrone"  "Arisa"             
[4] "Bugo"               "COLAPESCEDIMARTINO" "Coma Cose"         

O anche, direttamente:

sanremo_21$cantante

Metadati del corpus

La funzione per accedere e modificare i metadati del corpus è meta(), la cui sintassi è.

meta(x, field = NULL, type = c("user", "object", "system", "all"))

Possiamo accedere a tutti i metadati (meta(sanremo_21, type = "all")), oppure, separatamente, alle informazioni di sistema:

meta(sanremo_21, type = "system")
$`package-version`
[1] ‘3.2.0’

$`r-version`
[1] ‘4.1.1’

$system
  sysname   machine      user 
"Windows"  "x86-64"     "..." 

$directory
[1] "C:/Users/..."

$created
[1] "2022-01-11"

$source
[1] "data.frame"

e a quelle relative all’oggetto:

meta(sanremo_21, type = "object")
$unit
[1] "documents"

$summary
$summary$hash
character(0)

$summary$data
NULL

Possiamo aggiungere altri metadati (che saranno di tipo “user”) dopo l’importazione. Registriamo ad esempio la fonte dei testi.

meta(sanremo_21, 
     field = "fonte") <- "angolotesti.it"
meta(sanremo_21)
$fonte
[1] "angolotesti.it"

Ma possiamo anche impostarli in fase di importazione, come lista in cui definiamo il nome del campo (ad esempio “fonte”), e il valore del campo (“angolotesti.it”) (named list)9:

sanremo <- corpus(canzoni, meta = 
                    list("fonte" = "angolotesti.it"))
meta(sanremo)
$fonte
[1] "angolotesti.it"

equivale a

meta(sanremo_21, type = "user")
$fonte
[1] "angolotesti.it"

3.3.4 Informazioni sul corpus

La generica funzione summary(), applicata al corpus, restituisce — come abbiamo visto — una veloce descrizione del corpus, compresi il numero di token e di type:

summary(sanremo)
Corpus consisting of 2 documents, showing 2 documents:

                 Text Types Tokens Sentences ID      cantante
 01_Achille Lauro.txt   151    367         1  1 Achille Lauro
  02_Alberto Urso.txt    95    207         1  2  Alberto Urso

Altra funzione utile per descrivere il corpus è ndoc(), con la quale otteniamo il numero dei documenti:

ndoc(sanremo)
[1] 2

Con nsentence(), infine, possiamo conoscere il numero di frasi (in questo caso, i testi delle canzoni sono privi di punteggiatura):

nsentence(sanremo)
01_Achille Lauro.txt  02_Alberto Urso.txt 
                   1                    1 
Tabella 5: Quanteda: Funzioni per modificare i corpora
corpus_reshape() Segmentare i documenti (reversibile)
corpus_segment() Segmentare i documenti
corpus_subset() Subset del corpus
corpus_group() Raggruppare documenti in base a una variabile
corpus_sample() Selezionare un campione di documenti all’interno del corpus
corpus_trim() Rimuovere elementi del corpus (frasi, paragrafi, documenti) in base alla lunghezza.

3.3.5 Esempio: unire testi e metadati da file diversi

Vogliamo importare le trascrizioni di alcune interviste in profondità in Word, contenute in una sottocartella della cartella “dati”, e associarle alle informazioni sulle persone intervistate, contenute in un foglio di calcolo di Google (google sheet).

A quel punto si importeranno le interviste e il foglio di lavoro, e li si unirà.

  1. Importiamo le trascrizioni (vedi § 3.2.2):
library(tidyverse)
library(readtext)
interviste <- readtext("dati/interviste/*") %>% 
  mutate(doc_id = str_remove_all(doc_id, ".docx"))
interviste
readtext object consisting of 10 documents and 0 docvars.
# Description: df [10 x 2]
  doc_id text               
  <chr>  <chr>              
1 DCMSP0 "\"#RIC Buon \"..."
2 DSA0P0 "\"#RIC Buong\"..."
3 DSMSP1 "\"#RIC Allor\"..."
...   

Il nome del file è importato come identificativo dell’intervista, ma con l’estensione del file. Questa è stata rimossa (mutate(doc_id = str_remove_all(doc_id, ".docx")).

Eliminiamo tutto il testo contenuto fra parentesi quadre (\[(.*?)\]), ovvero tutti i commenti sull’interazione, ad esempio “[ride]”:

# elimina il testo fra parentesi quadre
interviste <- interviste %>%
  mutate(text = str_remove_all(text, "\\[(.*?)\\]"))

Notare che fare questa operazione in questa fase, è molto più semplice che effettuarla su Word, o sul corpus.

  1. Importiamo il file con i metadati delle interviste, con il pacchetto googlesheets4 [Bryan (2023)]10: lo chiamiamo metadocs, e trasformiamo le variabili in fattore dove serve:
library(googlesheets4)
# url del foglio
ss <- "https://docs.google.com/spreadsheets/d/..."
metadocs <- read_sheet(ss, sheet = "dati")   %>% 
  # indichiamo quali variabili trasformare in fattori
  mutate_at(vars(c(1:5,7:9)), list(factor))
metadocs
# A tibble: 29 x 24
   ID     doc_id Genere Partecipa Studia Area   Comune 
   <chr>  <chr>  <fct>  <fct>     <fct>  <fct>  <fct>   
 1 UNASP1 UNASP1 Uomo   si        si     Nord   altro~ 
 2 UCMSP1 UCMSP1 Uomo   si        si     Centro metro~ 
 ...

Si noterà che:

  • Ci sono più righe delle interviste trascritte, perché analizziamo le interviste man mano che procedono: usando lo script per l’analisi, possiamo ripetere tutta la procedura in pochi secondi;
  • Abbiamo due variabili uguali (doc_id e ID). La prima serve ad unire i dataframe e verrà modificata in fase di segmentazione; la seconda non verrà modificata e ci permetterà quindi, volendolo, di raggruppare tutti i segmenti o tutti i token appartenenti alla stessa intervista.
  1. Uniamo i dataframe, per aggiungere le variabili dei documenti ai testi, con left_join() di dplyr, che unisce le variabili del secondo dataset indicato al primo, in base ad un campo comune (in questo caso, doc_id):
interviste <- left_join(interviste, metadocs)
  1. Costruiamo il corpus (§ 3.3):
library(quanteda)
quanteda_options(language_stemmer ="italian") 

demo_corpus <- interviste %>% 
  # correzione degli apostrofi
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()
demo_corpus
Corpus consisting of 10 documents and 9 docvars.
"#RIC Buon pomeriggio. #INT Salve, buon pomeriggio. #RIC Buon..."

DSA0P0 :
"#RIC Buongiorno Alessia, mi sente #INT Sì, la sento, ma non ..."
...

Le variabili sono state correttamente importate e associate ai testi:

docvars(demo_corpus)
      ID Genere Partecipa Studia   Area       Comune ...
1 DCMSP0  Donna        no     si Centro    metropoli ...
2 DSA0P0  Donna        no     no    Sud altro comune ...
3 DSMSP1  Donna        no     si    Sud    metropoli ...
...

3.4 Statistiche riassuntive del corpus

3.4.1 Quanteda: summary

La funzione summary() restituisce — come abbiamo visto — una veloce descrizione del corpus:

summary(sanremo)
Corpus consisting of 2 documents, showing 2 documents:

                 Text Types Tokens Sentences ID      cantante
 01_Achille Lauro.txt   151    367         1  1 Achille Lauro
  02_Alberto Urso.txt    95    207         1  2  Alberto Urso
tokens(sanremo, remove_punct = T) %>% 
  summary()
                     Length Class  Mode     
01_Achille Lauro.txt 362    -none- character
02_Alberto Urso.txt  207    -none- character

Dal summary, possiamo ricavare 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)

Oppure una tabella (dataframe):

# tabella di summary
summary(sanremo) %>% as.data.frame() %>% head()
                  Text Types Tokens Sentences ID      cantante
1 01_Achille Lauro.txt   151    367         1  1 Achille Lauro
2  02_Alberto Urso.txt    95    207         1  2  Alberto Urso

che potrà essere formattata, e resa adatta alla pubblicazione in un report, con la funzione kable():

# tabella di summary
summary(sanremo) %>% as.data.frame() %>% 
  # selezione delle colonne di interesse
  select(1:4) %>% 
  knitr::kable()
Text Types Tokens Sentences
01_Achille Lauro.txt 151 367 1
02_Alberto Urso.txt 95 207 1

3.4.2 Quanteda: quanteda.textstats

Esistono funzioni specifiche per esplorare questi oggetti, inserite, nelle versioni più recenti, in pacchetti (moduli) separati:

library(quanteda.textstats)

La funzione textstat_summary() restituisce un dataframe con statistiche più dettagliate, per corpora e oggetti token:

# corpus
textstat_summary(sanremo)
              document chars sents tokens types puncts numbers
1 01_Achille Lauro.txt  1697     1    367   129      5       0
2  02_Alberto Urso.txt   996     1    207    83      0       0
  symbols urls tags emojis
1       0    0    0      0
2       0    0    0      0

Da questi dataframe, possiamo creare tabelle e grafici, in modo del tutto simile a quanto visto sopra.

Vediamo ad esempio il codice per un grafico a linee e punti che rappresenta l’estensione dei testi (n. token), analogo a quello in Figura 5.

# grafico a punti e linee
textstat_summary(sanremo_21) %>% 
  ggplot(aes(x = document, y = tokens, group = 1)) + 
  geom_line(col = palette()[1]) + 
  geom_point(col = palette()[2]) + 
  scale_x_discrete (labels = NULL) +
  labs(x = "Sanremo 2021: brani", y = "Tokens")

# per compatibilità con le funzioni di altri pacchetti
detach("package:quanteda")

Nota: Per creare un grafico con ggplot, dobbiamo usare — di base — due funzioni: ggplot() e una funzione geom_(). Nella prima, indicheremo un dataframe come origine dei dati (ggplot(data = summary(sanremo_21), ...), e con la funzione aes() le variabili da rappresentare sugli assi (ggplot(data = ..., aes(x = ..., y = ...))). Infine, si aggiunge la funzione che indica il tipo di rappresentazione desiderata (+ geom_line()).

Vedi https://www.agnesevardanega.eu/wiki/r/ggplot2/ggplot

3.5 I corpora in tm

In tm lo strumento principale, tanto per la gestione dei testi quanto per la normalizzazione, è il corpus.

I corpora sono implementati allo scopo di poter utilizzare direttamente alcune funzioni previste dal pacchetto NLP. Essi sono dunque alquanto diversi da quelli utilizzati da Quanteda.

Dal momento che non ci occuperemo specificamente delle funzioni di tm, prenderemo in considerazione solo la struttura e i metadati del corpus, per valutare le possibilità di esportazione/importazione dei dati da un formato all’altro.

3.5.1 La costruzione del corpus

La creazione del corpus richiede due funzioni. La prima (in questo caso DirSource()) serve ad indicare l’origine dei dati (una cartella, o directory; Tabella 6).

La seconda funzione (VCorpus()) costruisce il corpus e consente di specificare, attraverso l’argomento readerControl, la lingua dei testi e, eventualmente la funzione per l’importazione (Tabella 7).

Tabella 6: Tipi di fonti per i corpora in tm
DataframeSource() Dataframe
DirSource() Cartella (percorso)
URISource() uri
VectorSource() Vettore di testi
Tabella 7: I corpora in tm<
Corpus() readerControl = list()
VCorpus(): database non permanente readerControl = list()
PCorpus(): database permanente readerControl = list()
SimpleCorpus() control = list()
library(tm)

Per importare i due documenti contenuti nella cartella “dati/canzoni”, scriveremo dunque DirSource("dati/canzoni") e poi useremo la funzione VCorpus().

# da una directory
sanremo.tm <- DirSource("dati/canzoni") %>% 
  VCorpus(readerControl = list(language = "it-IT"))

Per costruire un corpus da un dataframe, useremo invece DataframeSource():

# da un dataframe
sanremo21.tm <- DataframeSource(sanremo21) %>% 
  VCorpus(readerControl = list(language = "it-IT"))
class(sanremo21.tm)
[1] "VCorpus" "Corpus" 
typeof(sanremo21.tm)
[1] "list"

Per correggere gli apostrofi prima della costruzione del corpus, correggeremo il campo “text” del dataframe dei documenti:

sanremo21.tm <- sanremo21 %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  DataframeSource() %>% 
  VCorpus(readerControl = list(language = "it-IT"))

Controlliamo i risultati, con le funzioni summary() e inspect():

sanremo21.tm %>% 
  summary() %>% head()
                    Length Class             Mode
Ora                 2      PlainTextDocument list
Dieci               2      PlainTextDocument list
Potevi_Fare_Di_Più  2      PlainTextDocument list
E_Invece_Sì         2      PlainTextDocument list
Musica_Leggerissima 2      PlainTextDocument list
Fiamme_Negli_Occhi  2      PlainTextDocument list
inspect(sanremo21.tm)

Controlliamo i risultati dell’importazione dei primi 3 documenti:

inspect(sanremo21.tm[1:3])
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 2
Content:  documents: 3

[[1]]
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 1515

[[2]]
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 1997

[[3]]
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 2017

3.5.2 I metadati

I metadati del corpus e dei documenti vengono gestiti in maniera analoga a quanto abbiamo visto per Quanteda, anche se con funzioni diverse.

Esplorando il corpus nel pannello Environment, o con il comando str(sanremo21.tm), possiamo osservare infatti che il corpus si compone di tre elementi (organizzati in una lista):

  • content: il contenuto, ovvero i documenti;
  • meta: i metadati del corpus;
  • dmeta: i metadati dei documenti, organizzati in un dataframe.
str(sanremo21.tm)
Classes 'VCorpus', 'Corpus'  hidden list of 3
 $ content:List of 31
  ..$ :List of 2
  .. ..$ content: chr "Ora ora ora ora\nMi parli come allora\nQua
  ndo ancora non mi conoscevi\nPensavi le cose peggiori\nQuella 
  notte i"| __truncated__
  .. ..$ meta   :List of 7
  .. .. ..$ author       : chr(0) 
  .. .. ..$ datetimestamp: POSIXlt[1:1], format: "2021-09-19 15:23:13"
  .. .. ..$ description  : chr(0) 
  .. .. ..$ heading      : chr(0) 
  .. .. ..$ id           : chr "Ora"
  .. .. ..$ language     : chr "it-IT"
  .. .. ..$ origin       : chr(0) 
  .. .. ..- attr(*, "class")= chr "TextDocumentMeta"
  .. ..- attr(*, "class")= chr [1:2] "PlainTextDocument" "TextDocument"
...
 $ meta   : list()
  ..- attr(*, "class")= chr "CorpusMeta"
 $ dmeta  : tibble [31 x 2] (S3: tbl_df/tbl/data.frame)
  ..$ cantante: chr [1:31] "Aiello" "Annalisa Scarrone"  ...
  ..$ titolo  : chr [1:31] "Ora" "Dieci" "Potevi Fare Di Più" ...

Il content è a sua volta una lista di 31 elementi (i documenti), ciascuno dei quali è una lista di due elementi: content (il testo delle canzoni, in questo caso) e meta (una lista di sette elementi).

Metadati del corpus

I metadati del corpus sono accessibili con la funzione meta(x, type=“corpus”).

meta(sanremo21.tm, type="corpus")

oppure

sanremo21.tm$meta
list()
attr(,"class")
[1] "CorpusMeta"

Per modificarli o inserirne di personalizzati, usiamo l’argomento tag11.

meta(sanremo21.tm, type="corpus", tag = "fonte") <- "angolotesti.it"

oppure

sanremo21.tm$meta$fonte <- "angolotesti.it"
sanremo21.tm$meta$fonte
[1] "angolotesti.it"
meta(sanremo21.tm, type="corpus")
$fonte
[1] "angolotesti.it"

attr(,"class")
[1] "CorpusMeta"

Metadati dei documenti

Le variabili del dataframe importato, sono accessibili scrivendo sanremo21.tm$dmeta, oppure meta(sanremo21.tm, type = "indexed"), oppure, più semplicemente:

meta(sanremo21.tm) %>% 
  head()
# A tibble: 6 × 2
  cantante           titolo             
  <chr>              <chr>              
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 

Per richiamare o modificare le variabili, usiamo la notazione delle liste, in questo modo

meta(sanremo21.tm)$titolo %>% head()
[1] "Ora"                 "Dieci"               "Potevi Fare Di Più" 
[4] "E Invece Sì"         "Musica Leggerissima" "Fiamme Negli Occhi" 

o

meta(sanremo21.tm)['titolo'] 

L’identificativo dei documenti è conservato insieme al content.

Per ciascun documento, è inoltre prevista la possibilità di inserire i metadati nel formato conforme al Dublin Core Metadata Initiative.

DublinCore(sanremo21.tm[[1]])
  contributor: character(0)
  coverage   : character(0)
  creator    : character(0)
  date       : 2021-09-17 12:19:13
  description: character(0)
  format     : character(0)
  identifier : Ora
  language   : it-IT
  publisher  : character(0)
  relation   : character(0)
  rights     : character(0)
  source     : character(0)
  subject    : character(0)
  title      : character(0)
  type       : character(0)
detach("package:tm")

3.6 Convertire i formati

3.6.1 Quanteda e tm

Possiamo importare direttamente un corpus costruito con tm in Quanteda, con la funzione corpus().

library(quanteda)
quanteda_options(language_stemmer ="italian")
corpus(sanremo21.tm)
Corpus consisting of 31 documents and 7 docvars.
text1 :
"Ora ora ora ora Mi parli come allora Quando ancora non mi co..."

text2 :
"Cos' è che ti ho promesso Non so Non mi ricordo adesso Me lo..."

text3 :
"Lasciarsi adesso non fa più male non è importante Cosa ci im..."

text4 :
"Le metropolitane vanno molto veloci II giornali gratis La ra..."

text5 :
"Se fosse un' orchestra a parlare per noi Sarebbe più facile ..."

text6 :
"Quando ti sto vicino sento Che a volte perdo il baricentro E..."

[ reached max_ndoc ... 25 more documents ]

Le 7 variabili sono i metadati dei documenti (compreso l’identificativo), sebbene alcuni campi siano vuoti:

docvars(corpus(sanremo21.tm)) %>% head()
  author       datetimestamp description heading                  id
1     NA 2024-03-16 18:07:55          NA      NA                 Ora
2     NA 2024-03-16 18:07:55          NA      NA               Dieci
3     NA 2024-03-16 18:07:55          NA      NA  Potevi_Fare_Di_Più
4     NA 2024-03-16 18:07:55          NA      NA         E_Invece_Sì
5     NA 2024-03-16 18:07:55          NA      NA Musica_Leggerissima
6     NA 2024-03-16 18:07:55          NA      NA  Fiamme_Negli_Occhi
  language origin
1    it-IT     NA
2    it-IT     NA
3    it-IT     NA
4    it-IT     NA
5    it-IT     NA
6    it-IT     NA

Non è invece possibile importare in tm un corpus di Quanteda. È però possibile convertire una matrice testuale di Quanteda (dfm) in una matrice di tm (dtm); Tabella 12).

detach("package:quanteda")

3.6.2 Tidytext: la funzione tidy()

La funzione tidy() di Tidytext consente di trasformare corpora e matrici testuali in un dataframe (una parole per riga):

# corpus di tm
tidytext::tidy(sanremo21.tm) %>% head()
# A tibble: 6 × 8
  author datetimestamp       description heading id    language origin
  <lgl>  <dttm>              <lgl>       <lgl>   <chr> <chr>    <lgl> 
1 NA     2024-03-16 18:07:55 NA          NA      Ora   it-IT    NA    
2 NA     2024-03-16 18:07:55 NA          NA      Dieci it-IT    NA    
3 NA     2024-03-16 18:07:55 NA          NA      Pote… it-IT    NA    
4 NA     2024-03-16 18:07:55 NA          NA      E_In… it-IT    NA    
5 NA     2024-03-16 18:07:55 NA          NA      Musi… it-IT    NA    
6 NA     2024-03-16 18:07:55 NA          NA      Fiam… it-IT    NA    
# ℹ 1 more variable: text <chr>

Vengono importati però solo i metadati dei documenti, e non quelli del corpus. Come si vede, infatti, manca il tag ‘fonte’ (angolotesti.it), e l’autore e il titolo (tm.corpus$dmeta). Informazioni, che, volendo, possono essere aggiunte con bind_cols():

bind_cols(tidytext::tidy(sanremo21.tm),
      sanremo21.tm$dmeta,
      'fonte' = sanremo21.tm$meta$fonte) %>% 
   head()
# A tibble: 6 × 11
  author datetimestamp       description heading id    language origin
  <lgl>  <dttm>              <lgl>       <lgl>   <chr> <chr>    <lgl> 
1 NA     2024-03-16 18:07:55 NA          NA      Ora   it-IT    NA    
2 NA     2024-03-16 18:07:55 NA          NA      Dieci it-IT    NA    
3 NA     2024-03-16 18:07:55 NA          NA      Pote… it-IT    NA    
4 NA     2024-03-16 18:07:55 NA          NA      E_In… it-IT    NA    
5 NA     2024-03-16 18:07:55 NA          NA      Musi… it-IT    NA    
6 NA     2024-03-16 18:07:55 NA          NA      Fiam… it-IT    NA    
# ℹ 4 more variables: text <chr>, cantante <chr>, titolo <chr>,
#   fonte <chr>
# corpus di quanteda
tidytext::tidy(sanremo_21) %>% head()
# A tibble: 6 × 3
  text                                                 cantante titolo
  <chr>                                                <chr>    <chr> 
1 "Ora ora ora ora\nMi parli come allora\nQuando anco… Aiello   Ora   
2 "Cos' è che ti ho promesso\nNon so\nNon mi ricordo … Annalis… Dieci 
3 "Lasciarsi adesso non fa più male non è importante\… Arisa    Potev…
4 "Le metropolitane vanno molto veloci\nII giornali g… Bugo     E Inv…
5 "Se fosse un' orchestra a parlare per noi\nSarebbe … COLAPES… Music…
6 "Quando ti sto vicino sento\nChe a volte perdo il b… Coma Co… Fiamm…

In questo caso, non sono stati importati gli identificativi dei documenti, che possono però essere recuperati con quanteda::docnames().

I metadati del corpus possono invece essere visualizzati con glance()12:

tidytext::glance(sanremo_21)
# A tibble: 1 × 1
  fonte         
  <chr>         
1 angolotesti.it

Possiamo recuperare tutte le informazioni in questo modo:

bind_cols('doc_id' = quanteda::docnames(sanremo_21),
      tidytext::tidy(sanremo_21),
      tidytext::glance(sanremo_21)) %>% 
  head()
# A tibble: 6 × 5
  doc_id              text                       cantante titolo fonte
  <chr>               <chr>                      <chr>    <chr>  <chr>
1 Ora                 "Ora ora ora ora\nMi parl… Aiello   Ora    ango…
2 Dieci               "Cos' è che ti ho promess… Annalis… Dieci  ango…
3 Potevi_Fare_Di_Più  "Lasciarsi adesso non fa … Arisa    Potev… ango…
4 E_Invece_Sì         "Le metropolitane vanno m… Bugo     E Inv… ango…
5 Musica_Leggerissima "Se fosse un' orchestra a… COLAPES… Music… ango…
6 Fiamme_Negli_Occhi  "Quando ti sto vicino sen… Coma Co… Fiamm… ango…

3.7 Funzioni usate

Per l’importazione e la gestione dei testi, le competenze di base necessarie riguardano principalmente la gestione dei diversi oggetti, o strutture di dati: i vettori carattere, i dataframe, e liste.

In fase di importazione e organizzazione dei dati, in particolare, può essere necessario trasformare i dataframe, le variabili — eventualmente creandone di nuove — e i valori delle variabili. In particolare, in questo capitolo abbiamo usato le seguenti funzioni:

Funzioni pacchetto
as.Date() trasformare un campo in formato data base
read_csv(),
read_tsv()
importare un file in formato csv o tab delimited readr
bind_cols(),
bind_rows()
unire due tabelle, per colonna o per riga dplyr
mutate() modificare i valori di una variabile dplyr
select(),
rename()
selezionare (e/o rinominare) le variabili di un dataset dplyr
str_replace(),
str_replace_all()
sostituire testo stringr

Le caratteristiche degli oggetti di classe Corpus utilizzati da Quanteda e tm sono state dettagliate nei paragrafi ad essi dedicati.

Per i grafici, che vedremo meglio nel capitolo 5, abbiamo usato le funzioni del pacchetto ggplot2:

Funzioni pacchetto
ggplot(), aes() ggplot
geom_line() grafico a linee ggplot
geom_point() grafico a punti ggplot
labs() titoli ed etichette di un grafico ggplot
scale_x_discrete() modifica l’asse delle x di un grafico ggplot

3.8 Codice del capitolo

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

## Capitolo 3 - Documenti e corpora

## Pacchetti --------------------------------------------------------------

library(tidyverse)

# IMPORTAZIONE ------------------------------------------------------------

library(readtext)

# sepolcri
sepolcri <- readtext("dati/sepolcri.txt")

# articolo
articolo <- readtext('dati/ansa_2020-02-05.txt',
                     docvarsfrom = "filenames",
                     docvarnames = c("fonte",
                                     "data"))
# modificare la data
articolo$data <- as.Date(articolo$data)

# canzoni
canzoni <- readtext('dati/canzoni/*.txt',
                    # impostare le variabili
                    docvarsfrom = "filenames",
                    docvarnames = c("ID",
                                    "cantante"))

# dataset
sanremo21 <- read_tsv("dati/sanremo2021.csv")
sanremo21 <- sanremo21 %>% 
  # titolo come doc_id
  mutate("doc_id" = str_replace_all(titolo, " ", "_")) %>% 
  # rinominiamo il campo "testo" e riorganizziamo le colonne
  select(doc_id, "text" = testo, 
         cantante, titolo)


# QUANTEDA ----------------------------------------------------------------

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

# CORPUS
corpus(canzoni)
corpus(sanremo21)

# correggere gli apostrofi
sanremo <- canzoni %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()

sanremo_21 <- sanremo21 %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()

sep.corpus <- sepolcri %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus()

# METADATI

docnames(sanremo)

# metadati dei documenti
docvars(sanremo_21)
docvars(sanremo_21)$cantante
sanremo_21$cantante

# metadati del corpus
meta(sanremo_21, type = "all")
meta(sanremo_21, type = "system")
meta(sanremo_21, type = "object")
# modificare i metadati
meta(sanremo_21, 
     field = "fonte") <- "angolotesti.it"
# in fase di costruzione del corpus
sanremo <- corpus(canzoni, meta = 
                    list("fonte" = "angolotesti.it"))
meta(sanremo)
meta(sanremo_21, type = "user")

# informazioni sul corpus
summary(sanremo)
ndoc(sanremo)
nsentence(sanremo)

# statistiche del corpus --------------------------------------------------

# Quanteda 
library(quanteda.textstats)
library(quanteda.textplots)

summary(sanremo)
# tabella di summary
summary(sanremo) %>% as.data.frame()
# textstat_summary
textstat_summary(sanremo)

# grafico a punti e linee
textstat_summary(sanremo_21) %>% 
  ggplot(aes(x = document, y = tokens, group = 1)) + 
  geom_line(col = palette()[1]) + 
  geom_point(col = palette()[2]) + 
  scale_x_discrete (labels = NULL) +
  labs(x = "Sanremo 2021: brani", y = "Tokens")


detach("package:quanteda")

# TM ----------------------------------------------------------------------

library(tm)

# CORPUS
# da una directory
sanremo.tm <- DirSource("dati/canzoni") %>% 
  VCorpus(readerControl = list(language = "it-IT"))
# da un dataframe
sanremo21.tm <- DataframeSource(sanremo21) %>% 
  VCorpus(readerControl = list(language = "it-IT"))

# correggere gli apostrofi prima
sanremo21.tm <- sanremo21 %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  DataframeSource() %>% 
  VCorpus(readerControl = list(language = "it-IT"))

summary(sanremo21.tm)

# inspect
inspect(sanremo21.tm)
# primi tre documenti
inspect(sanremo21.tm[1:3])


# METADATI
# visualizzare i metadati (del corpus)
meta(sanremo21.tm, type="corpus")
sanremo21.tm$meta
# modificare i metadati
meta(sanremo21.tm, type="corpus", tag = "fonte") <- "angolotesti.it"
# oppure
sanremo21.tm$meta$fonte <- "angolotesti.it"
# controllare
meta(sanremo21.tm, type="corpus")
# tag
sanremo21.tm$meta$fonte
# metadati dei documenti
meta(sanremo21.tm, type = "indexed")
# o
meta(sanremo21.tm)
# singole variabili
meta(sanremo21.tm)$titolo
meta(sanremo21.tm)['titolo']

detach("package:tm")

# Conversione dei formati -------------------------------------------------

# QUANTEDA e TM
library(quanteda)
quanteda_options(language_stemmer ="italian")

# corpus tm -> corpus quanteda
corpus(sanremo21.tm)

detach("package:quanteda")

# TIDYTEXT
# corpus tm -> tibble
tidytext::tidy(sanremo21.tm)
# aggiungere le altre informazioni
bind_cols(tidytext::tidy(sanremo21.tm),
          sanremo21.tm$dmeta,
          'fonte' = sanremo21.tm$meta$fonte) 

# corpus Quanteda -> tibble
tidytext::tidy(sanremo_21)

# metadati del corpus
tidytext::glance(sanremo_21)

bind_cols('doc_id' = quanteda::docnames(sanremo_21),
          tidytext::tidy(sanremo_21),
          tidytext::glance(sanremo_21))

  1. Si vedano gli esempi nell’help della funzione.↩︎

  2. Vedi: https://www.agnesevardanega.eu/wiki/r/gestione_dei_dati/grep_ricerca_sostituzione_testo↩︎

  3. Vedi voce «Liste»↩︎

  4. Per l’uso vedi la voce Importare i dati dai fogli Google↩︎

  5. tm usa il linguaggio del NLP, mentre Quanteda usa il linguaggio delle matrici. I database hanno campi, a differenza delle unità linguistiche e delle liste.↩︎

  6. Una funzione che tidytext mutua dal pacchetto broom (vedi voce: broom)↩︎