Capitolo 1 Esempio Sepolcri
1.1 Introduzione
Questo capitolo introduce alcuni concetti di base e le principali definizioni utili a comprendere le procedure dell’analisi testuale con R, seguendo l’esempio di un testo noto, complesso, e relativamente breve, “I sepolcri” di Ugo Foscolo.
In realtà, non è semplice presentare la procedura di analisi, perché i campi di applicazione dell’analisi quantitativa dei testi sono vari e numerosi, così come le tecniche e le procedure di analisi: una sintesi di massima delle possibili opzioni è presentata in Figura 3.
Figura 3: La costruzione della base dati: workflow
Nei capitoli successivi, verranno presentati alcuni degli strumenti per l’analisi testuale e del contenuto che si basano sulle matrici documenti-termini (Figura 4) e su quelle delle co-occorrenze.

Figura 4: Matrice documenti-termini, o documenti-forme
Per costruire la matrice testuale, e dunque procedere all’analisi, è necessario definire righe e colonne, ovvero le unità di contesto (i “documenti”; § 1.3) e le unità di analisi di testo (le “forme”; § 1.4), attraverso la riduzione/normalizzazione dei testi.
1.2 Testo e corpus
1.2.1 Il testo
Con il termine “testo” indichiamo l’insieme delle parole (inclusi punteggiatura e altri segni; cfr. § 1.4) che compongono uno scritto (documento).
Il testo dell’esempio illustrato in questo capitolo è quello dell’ode I Sepolcri, così come disponibile in WikiSource, e di cui riportiamo solo la prima strofa.
All’ ombra de’ cipressi e dentro l’ urne
Confortate di pianto è forse il sonno
Della morte men duro? Ove più il Sole
Per me alla terra non fecondi questa
Bella d’ erbe famiglia e d’ animali,
E quando vaghe di lusinghe innanzi
A me non danzeran l’ ore future,
Né da te, dolce amico, udrò più il verso
E la mesta armonia che lo governa,
Né più nel cor mi parlerà lo spirto
Delle vergini Muse e dell’ Amore,
Unico spirto a mia vita raminga,
Qual fia ristoro a’ dì perduti un sasso
Che distingua le mie dalle infinite
I testi possono essere importati in vari modi, che verranno illustrati nel terzo capitolo. In questo caso, il file del formato txt presente in WikiSource è stato scaricato, controllato e corretto, e infine importato con la funzione del pacchetto readtext [Benoit e Obeng (2024)]1.
1.2.2 Il corpus
Un corpus è l’insieme dei testi oggetto dell’analisi. Il nostro esempio consta di un solo testo, ma volessimo analizzare tutte le opere di Ugo Foscolo, sarebbe preferibile organizzare tutti i testi in un corpus.
Nel campo del NLP, si tratta di uno dei formati più comuni di organizzazione dei dati, ed è utilizzato da diversi pacchetti di R. Il vantaggio principale di questo formato, è che i testi vengono organizzati insieme ai relativi metadati, e ai metadati del corpus.
In Quanteda (Benoit et al. 2023), il corpus è infatti pensato per restare il riferimento stabile dei testi originari, non per il pre-trattamento dei testi (capitolo 2; solitamente necessario almeno per correggere errori ortografici), né per le successive trasformazioni dei testi.
1.3 Unità di contesto: documenti e frammenti
La statistica testuale si basa fondamentalmente su frequenze e co-occorrenze delle parole. Ma cosa si intende per “co-occorrenza” o “vicinanza” di due parole? Nel nostro esempio, le parole “ombra” del primo verso e “risplenderà” dell’ultimo co-occorrono? Sono vicine o sono lontane?
Per dare una risposta sensata a domande di questo genere, è necessario prendere alcune decisioni sul contesto delle parole: l’ode, la strofa, le opere di Foscolo, ecc.
I testi potranno dunque essere segmentati in frammenti, che saranno le unità di contesto rispetto alle quali saranno valutate molte delle misure statistiche utilizzate.
Nel caso del nostro esempio, abbiamo diviso il testo originale in frammenti corrispondenti alle frasi (divise da segni di interpunzione forti). Si considerino i primi tre:
Corpus consisting of 3 documents.
sepolcri.txt.1 :
"All' ombra de' cipressi e dentro l' urne Confortate di pianto è forse il sonno ..."
sepolcri.txt.2 :
"Ove più il Sole Per me alla terra non fecondi questa Bella d' erbe famiglia e d'..."
sepolcri.txt.3 :
"Vero è ben, Pindemonte!"
Ogni frammento — sono in tutto 56 — viene ora indicizzato come un documento separato (sepolcri.txt.1
, sepolcri.txt.2
, sepolcri.txt.3
, ecc.).
1.4 Unità di analisi dei testi: parole, token e types
Le prime domande che solitamente si pongono a livello descrittivono sono: quanto è lungo il testo? Quante parole contiene? Quali sono le parole più frequenti?
Una prima risposta può essere data contando i caratteri del testo, ad esempio con la funzione nchar()
:
sepolcri.txt
10929
Il numero di caratteri — come forse si avrà avuto modo di vedere utilizzando le statistiche dei documenti di Word — può però essere conteggiato tenendo conto o meno degli spazi (il risultato di nchar
li include).
Ancora meno immediato è definire cosa significhi contare le “parole”.
1.4.1 Definizioni
Bolasco (2013) definisce “parola” come il termine convenzionale generico utilizzato per identificare l’unità di analisi del testo.
I 10.793 caratteri possono essere distinti in 2.363 occorrenze (o tokens) di 1.096 forme o types (inclusi numeri, segni di punteggiatura e altri caratteri),
Corpus consisting of 1 document, showing 1 document:
Text Types Tokens Sentences
sepolcri.txt 1096 2363 56
dove — riprendendo ancora le definizioni del glossario di Bolasco (2013):
- il token è la singola occorrenza o replica di un type;
- il type è il tipo di occorrenza scandita dal parsing del testo … “. I type vengono anche chiamati forme (grafiche) o grafie.
Le parole, comunemente intese, sono i token esclusi i segni di punteggiatura, mentre lafrequenza di una parola, è il numero di occorrenze del type.
Chiariamo meglio con un esempio tratto dal testo della canzone di Achille Lauro presentata a Sanremo 2020.
Sì Noi sì Noi che qui Siamo soli qui Noi sì Soli qui Fai di me quel che vuoi sono qui
In questa frase — scelta proprio perché contiene molte ripetizioni, ed è priva di punteggiatura — i tokens sono 212 (notare che il corpus in questo caso non serve e non viene costruito):
# creiamo un vettore carattere con il testo
testo <- "Sì Noi sì Noi che qui Siamo soli
qui Noi sì Soli qui Fai di me
quel che vuoi sono qui"
# tokenizziamo il corpus
testo %>%
tokens()
Tokens consisting of 1 document.
text1 :
[1] "Sì" "Noi" "sì" "Noi" "che" "qui" "Siamo" "soli"
[9] "qui" "Noi" "sì" "Soli"
[ ... and 9 more ]
Le forme (i types) sono invece 14:
[1] "Sì" "Noi" "sì" "che" "qui" "Siamo" "soli" "Soli"
[9] "Fai" "di" "me" "quel" "vuoi" "sono"
ulteriormente riducibili a 12 operando una prima normalizzazione, ovvero eliminando le maiuscole:
[1] "sì" "noi" "che" "qui" "siamo" "soli" "fai" "di"
[9] "me" "quel" "vuoi" "sono"
Per estensione o ampiezza in occorrenze di questo testo intendiamo l’insieme dei token (21 “parole”, \(N\)), mentre i 12 type rappresentano l’ampiezza del suo vocabolario (\(V\)).
La distribuzione di frequenza delle parole (il numero di occorrenze dei types) è infine quella rapprentata in Tabella 1.
N | |
---|---|
qui | 4 |
sì | 3 |
noi | 3 |
che | 2 |
soli | 2 |
siamo | 1 |
fai | 1 |
di | 1 |
me | 1 |
quel | 1 |
vuoi | 1 |
sono | 1 |
Tornando ai Sepolcri, possiamo confrontare l’ampiezza del testo (\(N\), 2.363) — ciò che comunemente si intende per “lunghezza” — con quella del vocabolario (\(V\), 1.096)
Corpus consisting of 1 document, showing 1 document:
Text Types Tokens Sentences
sepolcri.txt 1096 2363 56
Dal summary del corpus segmentato (summary(sep.corpus.s)
) possiamo costruire un grafico che mette a confronto ampiezza e vocabolario dei segmenti:

Figura 5: Numero di ‘parole’ nei segmenti
1.4.2 Parsing e token
L’operazione che scompone il testo in token si chiama parsing, o tokenizzazione.
I confini delle parole sono convenzionalmente rappresentati dagli spazi che li dividono e dai segni di punteggiatura. Poiché — anche prescindendo dalle lingue orientali — lingue diverse hanno convenzioni diverse per quanto riguarda l’uso dei segni di punteggiatura, le parole composte, gli apostrofi, i trattini ecc., il parsing dei testi realizzato con strumenti diversi può portare a risultati diversi. Con Quanteda:
Tokens consisting of 1 document.
text1 :
[1] "All'ombra" "de" "cipressi" "e" "dentro"
[6] "l'urne" "Confortate"
Come si vede, l’apostrofo non viene riconosciuto come confine di parola. Nel quarto capitolo, verranno illustrate diverse soluzioni per operare il corretto parsing del testo in italiano.
1.5 Trattamento del lessico e normalizzazione
Una prima normalizzazione dei testi avviene già in fase di tokenizzazione, e riguarda:
- numeri;
- segni di punteggiatura;
- simboli, quali “#” e “$”.
Per la gran parte delle analisi, però, questo trattamento non è sufficiente. Nella distribuzione di frequenze del testo della canzone in Tabella 1, si noterà ad esempio che due forme del verbo essere, siamo
e sono
, vengono conteggiate separatamente, mentre potremmo volerle considerarle insieme, come forme del verbo essere.
Le 15 forme più frequenti dei Sepolcri, rappresentate nel grafico in Figura 6, non sono significative dal punto di visto del contenuto, essendo tutte articoli, preposizioni e congiunzioni, ovvero di parole vuote (stopwords).

Figura 6: I Sepolcri. Le quindici forme più frequenti
Nelle analisi testuali e del contenuto, vengono solitamente applicate alcune procedure di normalizzazione dei testi (capitoli 5 e 6), che riguardano:
- l’eliminazione delle parole vuote, o stopwords: parole vuote prive di un significato autonomo, come gli articoli o le congiunzioni;
- il trattamento delle maiuscole, identificando (o meno) le entità significative, ovvero i nomi propri (di persona, di cosa o di luogo);
- il trattamento delle forme flesse;
- l’individuazione e il trattamento dei poliformi, o multiword (come “Presidente della Repubblica”, o “anche se”).
Il trattamento più opportuno dipende dal tipo e dall’ampiezza dei testi, nonché dal tipo e dagli obiettivi dell’analisi.
Il testo del primo segmento dei Sepolcri è:
“All’ ombra de’ cipressi e dentro l’ urne Confortate di pianto è forse il sonno Della morte men duro?”
I tokens, esclusa la punteggiatura, sono:
Tokens consisting of 1 document.
sepolcri.txt.1 :
[1] "All" "ombra" "de" "cipressi" "e"
[6] "dentro" "l" "urne" "Confortate" "di"
[11] "pianto" "è" "forse" "il" "sonno"
[16] "Della" "morte" "men" "duro"
Tolte le maiuscole e le parole vuote (stopwords), abbiamo:
Tokens consisting of 1 document.
sepolcri.txt.1 :
[1] "ombra" "cipressi" "dentro" "urne" "confortate"
[6] "pianto" "forse" "sonno" "morte" "men"
[11] "duro"
Infine, dopo aver ridotto le forme flesse (in questo caso con la lemmatizzazione; vedi capitolo 4), il testo risulta normalizzato in questo modo:
Tokens consisting of 1 document.
sepolcri.txt.1 :
[1] "ombra" "cipresso" "dentro" "urna" "confortare"
[6] "pianto" "forse" "sonno" "morte" "meno"
[11] "duro"
Le forme flesse (o lessemi) sono state sostituite dalle forme canoniche (o lemmi), ovvero dalla «forma di citazione convezionale di un lessema in un dizionario» (Bolasco 2013). Quindi, nel nostro esempio:
cipressi
è stato sostituito dacipresso
;urne
daurna
e così via.
Una volta normalizzato il testo dell’intera ode, i 15 lemmi più frequenti sono quelli rappresentati nel grafico in Figura 7, certamente più informativo di quello precedente.

Figura 7: I 15 lemmi più frequenti
In R, lemmatizzazione e tagging grammaticale possono essere effettuate grazie alle librerie di TreeTagger (Helmut Schmid 1994), disponibile nel pacchetto koRpus (Michalke 2021) e di spaCy (Benoit e Matsuo 2023), disponibile in Quanteda.
Nelle fasi che vanno dal parsing alla normalizzazione del testo diventano fondamentali non solo le decisioni dell’analista, ma anche le risorse linguistiche disponibili — dizionari e liste.
1.6 Dalla matrice testuale all’analisi
1.6.1 Frequenze
Completati questi passaggi, abbiamo una matrice delle forme normalizzate, a partire dalle quali è possibile costruire distribuzioni di frequenze e grafici.
Ad esempio, per avere una distribuzione di frequenze come quella in Tabella 1, scriveremo:
testo %>%
# tokens
tokens() %>%
# matrice documenti-forme
dfm() %>%
# distribuzione di frequenza
topfeatures(12)
Anche le wordcloud risulteranno più significative se costruite con un numero ridotto di lemmi, scelti in quanto più frequenti e rilevanti (Figura 8).
Figura 8: I Sepolcri: wordcloud dei 35 lemmi più frequenti
1.6.2 Co-occorrenze
Le co-occorrenze fra le forme possono essere studiate ricorrendo diverse misure di associazione, similarità o distanza. Vediamo ad esempio, nella Tabella 2, quali sono le parole più associate al termine amore
, in termini di coseno quadrato.
Lemmi | Indice |
---|---|
dolce | 0,6708204 |
osso | 0,4472136 |
grande | 0,4472136 |
patria | 0,4472136 |
udire | 0,4000000 |
caro | 0,4000000 |
terra | 0,3162278 |
dì | 0,2236068 |
mandare | 0,2236068 |
lungo | 0,2236068 |
Sempre con gli strumenti di R, sarà possibile rappresentare e studiare tali associazioni con gli strumenti dedicati ai grafi e alla network analysis (Figura 9).
A tale scopo, ho usato qui igraph (Csardi e Nepusz 2006; Csárdi et al. 2024).

Figura 9: Grafo delle associazioni
Nella Figura 10 il testo viene rappresentato come un network.

Figura 10: I Sepolcri: network dei lemmi con frequenza pari almeno a 4
1.6.3 Analisi multidimensionale
Naturalmente, a partire dalle matrici testuali è possibile realizzare diversi tipi di analisi multivariata.
In Figura 11, ad esempio, è rappresentato il risultato del multidimensional scaling.

Figura 11: Multidimensional Scaling
1.7 Sintesi: i passi operativi nei tre principali pacchetti
In generale, per passare dai documenti da analizzare alla matrice testuale, sono previsti i seguenti step:
- organizzazione della base dati (corpus o dataframe: §1.2; ed eventuale segmentazione: §1.3)
- parsing dei testi (tokenizzazione: §1.4)
- normalizzazione (§1.5)
- costruzione della matrice
1.7.1 Quanteda
L’esempio illustrato nel capitolo ha fatto uso di questo pacchetto. Rivediamo di seguito i passi salienti della procedura.
Figura 12: Quanteda: workflow
Pacchetti:
Date le caratteristiche del parser interno utilizzato da (quasi) tutti i pacchetti, suggerisco di correggere il testo, inserendo uno spazio dopo gli apostrofi non seguiti da uno spazio (cfr. capitolo 4, §4.2):
# correzione degli apostrofi
sepolcri <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' "))
e poi
- passiamo alla tokenizzazione (capitolo 4, §4.4.1), eliminando punteggiatura, numeri e simboli (le impostazioni generalmente adottate, anche se in questo caso non troviamo numeri e simboli):
# tokens
sep.tokens <- sep.corpus %>%
tokens(remove_punct = TRUE,
remove_numbers = TRUE,
remove_symbols = TRUE)
- e procediamo alla normalizzazione (capitolo ??). Consideriamo solo le due operazioni di base, ovvero l’eliminazione delle stopword:
# eliminazione delle stopwords per italiano
sep.tokens <- sep.tokens %>%
tokens_remove(stopwords("it"))
e lo stemming:
- Infine, dopo aver costruito la matrice:
- possiamo procedere con l’analisi dei dati, a partire dalla distribuzione delle frequenze (capitolo 5):
feature frequency rank docfreq group
1 d 26 1 1 all
2 de 16 2 1 all
3 ove 15 3 1 all
4 amor 9 4 1 all
5 mort 8 5 1 all
6 nè 8 5 1 all
7 sol 7 7 1 all
8 tomb 7 7 1 all
9 terr 6 9 1 all
10 te 6 9 1 all
Alcune parole vuote non sono incluse nella lista standard. Vedremo nel seguito come usare liste personali.
1.7.2 Tidytext
L’approccio di Tidytext (Robinson e Silge 2023) è alquanto diverso, in quanto i termini vengono organizzati in un dataframe, in cui le righe (i casi) corrispondono alle singole occorrenze, e in colonna sono riportate le informazioni relative (ad esempio i metadati). In questo modo, si ha una piena compatibilità con le funzioni degli altri pacchetti del Tidyverse (Wickham 2023).
Nello stesso tempo, è possibile passare da questo approccio a quello basato sulle matrici testuali trasformando i dataframe in matrici delle co-occorrenze o anche documenti-termini.
Figura 13: Tidytext: workflow
Pacchetti:
- L’importazione del testo viene effettuata come per Quanteda, ovvero:
sepolcri <- readtext("dati/sepolcri.txt")
# correzione degli apostrofi
sepolcri <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' "))
- Il passo successivo è la tokenizzazione (capitolo 4, § 4.5.4; punteggiatura, numeri e simboli vengono eliminati per default):
- Normalizzazione. Tidytext lavora sempre e solo con dataframe, e presuppone spesso l’utilizzo delle funzioni dei pacchetti del tidyverse.
Per l’eliminazione delle stopwords, ad esempio, ricorre alla funzione anti_join
di dplyr, che richiede una lista organizzata in dataframe.
Dal momento che la lista di stopword adatta è disponibile solo per l’inglese, dobbiamo crearne una. Usiamo la lista di Quanteda:
# dataframe delle stopwords
it_stopwords <- data.frame(word = stopwords::stopwords("italian"),
lexicon = "custom")
La funzione anti_join()
confronta le parole contenute nei due dataframe ed elimina quelle presenti nel secondo:
Per lo stemming, utilizziamo mutate()
di dplyr e il pacchetto degli stemmer SnowballC [Bouchet-Valat (2023)]3:
- Anche per l’analisi dei dati, essendo i token organizzati in dataframe, possiamo usare le funzioni del tidyverse. Useremo ad esempio
count()
per avere la distribuzione delle frequenze (cfr. capitolo 5, § 3.4):
# A tibble: 10 × 2
word n
<chr> <int>
1 d 26
2 de 16
3 ove 15
4 amor 9
5 mort 8
6 nè 8
7 sol 7
8 tomb 7
9 cant 6
10 patr 6
Per usare altre funzioni specifiche per l’analisi testuale, il dataframe potrà essere trasformato o esportato in matrici testuali compatibili con altri pacchetti. L’esportazione dei metadati dei documenti collegabili alle matrici testuali non è però automatica e può essere alquanto laboriosa.
1.7.3 tm
Figura 14: tm: workflow
Il primo passo, l’importazione del testo, può avvenire in vari modi (capitolo 3, § 3.5). In questo caso, partiremo dal dataframe importato sopra:
sepolcri.tm <- sepolcri %>%
# indichiamo il tipo di origine dei dati
DataframeSource() %>%
# corpus
VCorpus(readerControl = list(language = "it-IT"))
Per normalizzare il testo, eliminiamo la punteggiatura e i numeri, e trasformiamo il testo in minuscolo (capitolo ??):
sepolcri.tm <- sepolcri.tm %>%
# eliminiamo la punteggiatura
tm_map(removePunctuation) %>%
# eliminiamo i numeri
tm_map(removeNumbers) %>%
# trasformiamo il testo in minuscolo
tm_map(content_transformer(tolower))
Poi eliminiamo le stopwords (la lista inclusa in tm è la stessa inclusa in Quanteda):
E infine procediamo allo stemming, con una funzione personalizzata (o per meglio dire con un workaround):
# funzione personalizzata per lo stemming
stemming <- function(x) SnowballC::wordStem(words(x), language = "italian")
sepolcri.tm <- sepolcri.tm %>%
tm_map(content_transformer(stemming))
A questo punto, al netto di altri interventi di normalizzazione, possiamo passare a costruire la matrice (di default, non verranno incluse le parole composte da uno o due caratteri):
E questa è la lista dei dieci termini più frequenti:
$sepolcri.txt
ove amor mort sol tomb cant patr sott terr amic
15 9 8 7 7 6 6 6 6 5
Carichiamo qui i pacchetti del Tidyverse con il comando
library(tidyverse)
.
Per l’uso dell’operatore di forward piping (%>%
), cfr. pipe operator.↩︎Che è quello utilizzato anche dagli altri pacchetti in background, per così dire, ma che non viene installato insieme a Tidytext↩︎