Capitolo 4 Il parsing dei testi
Testi e dati degli esempi (cartella Proton Drive, in aggiornamento). Gli script che seguono presuppongono che testi e dati siano in una sottocartella “dati”.
4.1 Introduzione
4.1.1 Il parsing e la tokenizzazione
Parsing è una espressione usata in informatica per indicare il riconoscimento della struttura sintattica di un flusso di dati, e dell’analisi lessicale delle sue parti (token).
Le parti di un testo oggetto dell’analisi lessicale sono solitamente le parole, ma per tokenizzazione può intendersi anche la suddivisione di uno o più testi in lettere o caratteri, linee, sequenze (n-grammi), frasi, paragrafi e frammenti di testo di lunghezza e definizione arbitraria (segmentazione; cfr. §4.1.2).
Nell’approccio bag of words, si lavora sulla matrice documenti-termini, o documenti-forme, dove i documenti o i frammenti sono le unità di contesto, e le forme le unità di testo scelte, normalizzate e selezionate.
Per questa ragione, prenderemo in considerazione separatamente la segmentazione del testo in frammenti (§4.1.2), che ridefinisce le unità di contesto, e la tokenizzazione, espressione con la quale ci riferiremo alla definizione delle unità di testo, e dunque delle parole.
4.1.2 La segmentazione dei testi
I testi che compongono un corpus possono essere (o meno) segmentati in frammenti, che saranno le unità di contesto rispetto alle quali saranno valutate - ad esempio - la vicinanza o lontananza delle parole, e quindi le diverse misure statistiche utilizzate (Vedi paragrafo 1.3).
I testi vengono solitamente segmentati:
- in frasi, frammenti di testo divisi da segni di interpunzione forti (punto, punto esclamativo, punto interrogativo; eventualmente anche punto e virgola, due punti);
- in paragrafi, frammenti di testo divisi dall’interruzione di riga;
- in frammenti di struttura e/o lunghezza stabilita dal ricercatore.
Non esiste una regola valida in assoluto per stabilire la dimensione dei frammenti. Ad esempio, in molti degli esempi disponibili per i pacchetti di R viene utilizzato il corpus dei romanzi di Jane Austin, e le unità di contesto possono essere tanto i libri quanto i capitoli. Di contro, se si lavora su testi molto brevi (risposte a domande aperte, messaggi di Twitter, ecc), questo passaggio non è necessario.
Ai fini dell’analisi, è importante però che i frammenti abbiano un identificativo proprio, e che, nello stesso tempo, essi siano riconducibili al testo di partenza e ai relativi metadati.
Per arrivare alla soluzione più consona agli obiettivi della nostra analisi, esistono diverse possibilità (Tabella 8). Per la segmentazione del testo, consiglio di scegliere, nell’ordine:
- le funzioni presenti nel pacchetto o nella famiglia di pacchetti che stiamo utilizzando, per semplicità;
- quelle di Tidytext, se abbiamo un dataframe di testi;
- quelle di altri pacchetti, in particolare tokenizers.
| Frammenti | Quanteda | tidytext | tokenizers |
|---|---|---|---|
| corpus | dataframe | vettori carattere (testi) | |
| Frasi | corpus_reshape |
unnest_sentences |
tokenize_sentences |
| Paragrafi | ” | unnest_paragraphs |
tokenize_paragraphs |
| Linee | corpus_segment |
unnest_lines |
tokenize_lines |
| Pattern (regex) | ” | unnest_regex |
tokenize_regex |
| Dimensioni in numero di caratteri | tokens_chunk(si applica ai token) |
chunk_text |
Quanteda prevede due funzioni specifiche utilizzabili per la segmentazione dei documenti organizzati in un corpus: corpus_reshape() e corpus_segment(), che si applicano esclusivamente ai corpora di Quanteda e restituiscono come risultato un corpus segmentato.
Le funzioni di Tidytext si applicano invece a dataframe: iniziano tutte con unnest_ in quanto una colonna di testo viene divisa in parti, per ciascuna delle quali viene creata una nuova riga (le parti vengono in questo senso unnested). Queste funzioni possono essere usate per ridefinire le unità di contesto a prescindere dal pacchetto che si vorrà usare per le fasi successive dell’analisi.
Le funzioni di Tidytext sono costruite intorno a quelle di tokenizers, che però si applicano a vettori carattere, e restituiscono vettori carattere o liste.
4.1.3 Strumenti per il parsing dei testi in R
I principali strumenti che prenderemo in considerazione sono:
- Le funzioni presenti nel pacchetto tokenizer (Mullen 2022), utilizzato internamente da Quanteda e Tidytext;
- Le funzioni del pacchetto hunspell (Ooms 2023), che abbiamo visto nel paragrafo dedicato alla correzione ortografica (§ 2.4.1);
- La libreria spaCy (https://spacy.io/), di Python, accessibile in R attraverso il pacchetto spacyr (Benoit e Matsuo 2023), integrato in Quanteda (§ 4.6.1);
- TreeTagger (Helmut Schmid 1994; Schmid 1999), integrato in koRpus (Michalke 2021) (§ 4.7.2);
- udpipe (Wijffels 2023)
Queste tre ultime librerie consentono di effettuare anche il tagging grammaticale e la lemmatizzazione, di cui ci occuperemo nel capitolo ??. Esse si rivelano strumenti fondamentali se l’analisi prevede questo tipo di trattamento delle unità di testo. Vedremo quindi come usarle e come trasformare i risultati nei formati di Quanteda e Tidytext, compresi gli eventuali metadati dei documenti.
| Forme | Quanteda | tidytext | tokenizers |
|---|---|---|---|
| corpus, vettori carattere | dataframe | vettori carattere (testi) | |
| word | tokens |
unnest_tokens |
tokenize_words |
| tweets | tokens |
unnest_tweets |
tokenize_tweets |
| characters | tokens(what = "character") |
unnest_characters |
tokenize_characters |
| token | tokens |
unnest_tokens |
tokenize_words |
| ngrams | tokens_ngrams |
unnest_ngrams |
tokenize_ngrams |
| skip_ngrams | tokens_skipgrams |
unnest_skip_ngrams |
tokenize_skip_ngrams |
4.2 I confini delle parole e i parser disponibili
Il riconoscimento delle parti del testo (token) procede dalla loro definizione, ed in particolare da quella dei loro confini. Di conseguenza, i risultati delle funzioni che vedremo nel seguito potranno essere diversi, in misura maggiore o minore, in funzione dei modelli linguistici disponibili nei pacchetti e degli algoritmi utilizzati.
Anche questa fase è influenzata dalla preparazione, correzione e organizzazione dei testi.
Il tokenizzatore utilizzato da Quanteda e Tidytext è quello del pacchetto tokenizers, che non riconosce gli apostrofi come confini di parola per l’italiano.
La funzione per il parsing delle parole è tokenize_words():
frase <- "Se fosse un’orchestra a parlare per noi,
Sarebbe più facile cantarsi un addio"
tokenizers::tokenize_words(frase)[[1]]
[1] "se" "fosse" "un’orchestra" "a"
[5] "parlare" "per" "noi" "sarebbe"
[9] "più" "facile" "cantarsi" "un"
[13] "addio"
l’espressione “un’orchestra” è trattata come unico token.
Questo comporta evidentemente problemi non solo per ciò che riguarda il conteggio dei token, ma anche per il riconoscimento delle parole, gli indici di leggibilità e la normalizzazione del testo.
Al momento, per la lingua italiana, le soluzioni praticabili sono due: modificare i confini delle parole intervenendo sui testi dopo la loro importazione, oppure usare un altro parser (quello di hunspell, o quello di spacyr).
Se si intende procedere al parsing morfologico (lemmatizzazione e tagging grammaticale), il problema, come si vedrà, non si pone, e in ogni caso non è possibile adottare la prima soluzione.
4.2.1 Modificare a mano i confini delle parole
La prima soluzione consiste dunque nell’inserire uno spazio dopo gli apostrofi, ad esempio con str_replace_all():
frase %>%
# sostituzione degli apostrofi
str_replace_all("'", "' ") %>%
tokenizers::tokenize_words()Oppure, per essere certi di sostituire sia gli apostrofi che gli apici inglesi:
[[1]]
[1] "se" "fosse" "un" "orchestra" "a"
[6] "parlare" "per" "noi" "sarebbe" "più"
[11] "facile" "cantarsi" "un" "addio"
O ancora, per non aggiungere spazi anche dopo parole come “po’”:
[[1]]
[1] "se" "fosse" "un" "orchestra" "a"
[6] "parlare" "per" "noi" "sarebbe" "più"
[11] "facile" "cantarsi" "un" "addio"
Si tratta della soluzione più semplice, in quanto può essere implementata — con una sola riga di codice — subito dopo l’importazione, modificando il testo importato,
# non eseguire: il testo viene modificato stabilmente
sepolcri <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) oppure prima di qualunque altra procedura, che si tratti della creazione del corpus o della costruzione degli oggetti contenenti i token:
# non eseguire: il testo contenuto nel dataset non viene modificato
sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus()In particolare, si tratta dell’unica opzione quando:
- si usi tm, che lavora esclusivamente con i corpora;
- o si usino i corpora in Quanteda, che effettua un primo parsing del testo in fase di costruzione del corpus (nel capitolo precedente, siamo infatti stati in grado di costruire tabelle e grafici con i token e i type).
Senza correzione dell’apostrofo, avremo, ad esempio
Corpus consisting of 1 document, showing 1 document:
Text Types Tokens Sentences
sepolcri.txt 1103 2173 56
mentre il numero corretto di token e type, punteggiatura inclusa, è:
Corpus consisting of 1 document, showing 1 document:
Text Types Tokens Sentences
sepolcri.txt 1096 2363 56
Anche con tidytext può essere preferibile adottare questa soluzione per usare direttamente le funzioni interne per il parsing a partire dal dataframe corretto.
In Appendice (§ 4.9), è disponibile una piccola funzione che può semplificare la scrittura di questa riga di comando.
4.2.2 Il parser di Hunspell e di udpipe
Un’altra possibilità consiste nell’utilizzare altri parser.
Uno è quello di hunspell (funzione hunspell_parse()):
[[1]]
[1] "Se" "fosse" "un" "orchestra" "a"
[6] "parlare" "per" "noi" "Sarebbe" "più"
[11] "facile" "cantarsi" "un" "addio"
Questo parser esclude sempre la punteggiatura (isola cioè le “parole”), e, come si vede da questo risultato, restituisce una lista di vettori carattere, uno per ciascun testo (si veda oltre, § ??).
Altri parser utilizzabili sono quelli di spaCy (§ 4.6.1), TreeTagger (§ 4.7.2) e udpipe, utili in particolare quando si desideri il riconoscimento delle parti del discorso (POS), oppure normalizzare il testo mediante lemmatizzazione.
In particolare, spaCy e udpipe individuano correttamente i confini delle parole e lasciano la punteggiatura – incluso l’apostrofo alla fine della parola elisa. Presento rapidamente un esempio di risultati
library(udpipe)
# scaricare il modello linguistico
itl <- udpipe_download_model(language = "italian")
# caricare il modello da applicare
model <- udpipe_load_model(itl)udpipe_annotate(x=frase, model) %>%
as.data.frame() %>%
# seleziono alcune colonne del risultato
select(c(1:3,5:7)) doc_id paragraph_id sentence_id token_id token lemma
1 doc1 1 1 1 Se se
2 doc1 1 1 2 fosse essere
3 doc1 1 1 3 un’ uno
4 doc1 1 1 4 orchestra orchestra
5 doc1 1 1 5 a a
6 doc1 1 1 6 parlare parlare
7 doc1 1 1 7 per per
8 doc1 1 1 8 noi noi
9 doc1 1 1 9 , ,
10 doc1 1 1 10 Sarebbe essere
11 doc1 1 1 11 più più
12 doc1 1 1 12 facile facile
13 doc1 1 1 13-14 cantarsi <NA>
14 doc1 1 1 13 cantar cantare
15 doc1 1 1 14 si si
16 doc1 1 1 15 un uno
17 doc1 1 1 16 addio addio
udpipe, come anche spaCy, divide il testo anche in paragrafi e frasi. Notare come venga conservata la forma “un’” con l’apostrofo, e come viene articolata la tokenizzazione di “cantarsi” (“cantar” e “si”).
La cosa importante è ottenere, alla fine, una matrice testuale corretta, che includa eventuali metadati (le variabili relative ai documenti).
4.3 Quanteda: la segmentazione del corpus
4.3.1 Segmentazione in frasi e paragrafi: corpus_reshape()
corpus_reshape() non modifica in maniera irreversibile la struttura delle unità di analisi, ma consente di ristrutturarle in frasi (sentences), paragrafi (paragraphs) o documenti (documents).
corpus_reshape(
x,
to = c("sentences", "paragraphs", "documents"),
use_docvars = TRUE,
...
)
I frammenti avranno un identificativo univoco, e le variabili dei documenti verranno ripetute per ciascuno di essi (use_docvars = TRUE).
Costruiamo il corpus dei Sepolcri13
library(quanteda)
sep.corpus <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus()Segmentiamo il testo in frasi:
Confrontiamo i due corpora:
[1] 1
[1] 56
Il corpus risulta ora segmentato in 56 frammenti, corrispondenti alle frasi, definite dal punto, dal punto esclamativo e dal punto interrogativo (e non anche dai due punti e dal punto e virgola)14.
[1] "sepolcri.txt.1" "sepolcri.txt.2" "sepolcri.txt.3"
[4] "sepolcri.txt.4" "sepolcri.txt.5" "sepolcri.txt.6"
Per cambiare i confini delle frasi, tenendo conto anche del punto e virgola e dei due punti, useremo corpus_segment() con una espressione regolare.
4.3.2 Segmentazione in base ad un pattern: corpus_segment()
La funzione corpus_segment() consente di segmentare “a mano” il corpus, anche in fasi successive.
corpus_segment(
x,
pattern = "##*",
valuetype = c("glob", "regex", "fixed"),
case_insensitive = TRUE,
extract_pattern = TRUE,
pattern_position = c("before", "after"),
use_docvars = TRUE
)
A tale scopo, bisogna indicare un pattern mediante una stringa di testo, che verrà interpretata come “glob pattern” (con sintassi analoga a quella che utilizziamo solitamente in Word): ad esempio, il pattern predefinito è “##*”, che significa “##” seguito da qualunque carattere (“*”), e precede il segmento (pattern_position).
Per usare una espressione regolare scriveremo valuetype = "regex", mentre per indicare una stringa così com’è, scriveremo valuetype = "fixed".
In questo caso, le unità di contesto vengono ridefinite permanentemente, e il pattern viene usato per costruire una nuova variabile (extract_pattern = TRUE).
Elemento di testo come pattern
Ad esempio, per segmentare il testo dell’articolo in “Titolo”, “Sottotitolo” e “Articolo”, inseriamo — prima dell’importazione — una sequenza definita di caratteri (pattern) per riconoscere queste diverse parti. Ad esempio “##” seguita dal valore della nuova variabile che verrà creata:
##TITOLO Sanremo: Fiorello al festival mai più ospite, magari in gara
##SOTTOTITOLO "Mi ha colpito Achille Lauro. Con Amadeus sketch improvvisati"
##ARTICOLO "Ve lo dico, da ospite al festival di Sanremo non tornerò più. L'ho
fatto già troppe volte. Da conduttore non è proprio il caso. Ma potrei tornare
da cantante". Tra il serio e il faceto Fiorello apre alla possibilità in un
futuro di tornare a cantare e di farlo al Festival. "Mi piacerebbe tornare a
fare il cantante serio. Ho anche un sacco di amici che potrebbero scrivere per
me canzoni davvero belle. Da Lorenzo Jovanotti a Giuliano Sangiorgi".
...
Una volta costruito il corpus, possiamo segmentarlo indicando il pattern “##”:
art.corpus <- articolo %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus() %>%
# segmentazione
corpus_segment(pattern = "##*")Dal momento che "##*" è impostato nella funzione come pattern di default, possiamo anche ometterlo, e scrivere:
articolo %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus() %>%
# segmentazione
corpus_segment()Corpus consisting of 3 documents and 3 docvars.
ansa_2020-02-05.txt.1 :
"Sanremo: Fiorello al festival mai più ospite, magari in gara"
ansa_2020-02-05.txt.2 :
""Mi ha colpito Achille Lauro. Con Amadeus sketch improvvisat..."
ansa_2020-02-05.txt.3 :
""Ve lo dico, da ospite al festival di Sanremo non tornerò pi..."
I documenti del corpus sono 3. Gli identificativi dei frammenti sono strutturati nel modo visto sopra:
[1] "ansa_2020-02-05.txt.1" "ansa_2020-02-05.txt.2"
[3] "ansa_2020-02-05.txt.3"
Mentre il pattern è diventato una nuova variabile:
fonte data pattern
1 ansa 2020-02-05 ##TITOLO
2 ansa 2020-02-05 ##SOTTOTITOLO
3 ansa 2020-02-05 ##ARTICOLO
Questa variabile potrà naturalmente essere modificata, ad esempio per eliminare i caratteri “##” (str_remove_all("##")), e riportare il testo ai caratteri minuscoli e l’iniziale maiuscola (str_to_title()).
art.corpus$pattern <- art.corpus$pattern %>%
str_remove_all("##") %>%
str_to_title()
docvars(art.corpus) fonte data pattern
1 ansa 2020-02-05 Titolo
2 ansa 2020-02-05 Sottotitolo
3 ansa 2020-02-05 Articolo
Corpus consisting of 3 documents, showing 3 documents:
Text Types Tokens Sentences fonte data
ansa_2020-02-05.txt.1 12 12 1 ansa 2020-02-05
ansa_2020-02-05.txt.2 11 12 2 ansa 2020-02-05
ansa_2020-02-05.txt.3 196 345 28 ansa 2020-02-05
pattern
Titolo
Sottotitolo
Articolo
Possiamo usare la variabile per avere una tabella sintetica di summary, con le funzioni group_by e summarise():
# A tibble: 3 × 3
pattern Tokens Types
<chr> <int> <int>
1 Articolo 345 196
2 Sottotitolo 12 11
3 Titolo 12 12
Con la nuova variabile sarà possibile creare un subset del corpus, ad esempio per analizzare solo il corpo dell’articolo:
Corpus consisting of 1 document and 3 docvars.
ansa_2020-02-05.txt.3 :
""Ve lo dico, da ospite al festival di Sanremo non tornerò pi..."
oppure confrontare il linguaggio di titoli, sottotitoli e articoli, utilizzandola come variabile di raggruppamento (funzione corpus_group()).
Espressioni regolari: frasi, paragrafi, linee
Volendo segmentare il testo in frasi delimitate da tutti i segni di punteggiatura forte, compresi i due punti e il punto e virgola:
- useremo come pattern l’espressione “
[\\.\\!\\?;:]”, esplicitando che si tratta di una espressione regolare; - non estrarremo il pattern come nuova variabile (
extract_pattern = FALSE), a meno che non ci serva per qualche motivo (ad esempio per riconoscere le domande); - useremo l’argomento
pattern_positionper indicare che i segni di punteggiatura chiudono, e dunque seguono, il segmento:
sep.corpus %>%
corpus_segment(pattern ="[\\.\\!\\?;:]",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")Corpus consisting of 98 documents.
sepolcri.txt.1 :
"All' ombra de' cipressi e dentro l' urne Confortate di piant..."
sepolcri.txt.2 :
"Ove più il Sole Per me alla terra non fecondi questa Bella d..."
sepolcri.txt.3 :
"Vero è ben, Pindemonte!"
sepolcri.txt.4 :
"Anche la Speme, Ultima Dea, fugge i sepolcri;"
sepolcri.txt.5 :
"e involve Tutte cose l' obblio nella sua notte;"
sepolcri.txt.6 :
"E una forza operosa le affatica Di moto in moto;"
[ reached max_ndoc ... 92 more documents ]
Per dividere il testo in paragrafi (qui si tratta delle strofe), useremo l’espressione "\\n\\n" (ovvero il segno di nuova riga ripetuto due volte):
sep.corpus %>%
corpus_segment(pattern ="\\n\\n",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")Corpus consisting of 12 documents.
sepolcri.txt.1 :
"All' ombra de' cipressi e dentro l' urne Confortate di piant..."
sepolcri.txt.2 :
"Ossa che in terra e in mar semina morte? Vero è ben, Pindemo..."
sepolcri.txt.3 :
"Poca gioia ha dell' urna; e se pur mira Dopo l' esequie, err..."
sepolcri.txt.4 :
"Cui già di calma era cortese e d' ombre. Forse tu fra plebei..."
sepolcri.txt.5 :
"Con veci eterne a' sensi altri destina. Testimonianza a' fas..."
sepolcri.txt.6 :
"Mandano i petti alla fuggente luce. Le fontane versando acqu..."
[ reached max_ndoc ... 6 more documents ]
Per dividere il testo in linee useremo "\\n":
sanremo_21 %>%
corpus_segment(pattern = "\\n",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")Corpus consisting of 1,834 documents and 2 docvars.
Ora.1 :
"Ora ora ora ora"
Ora.2 :
"Mi parli come allora"
Ora.3 :
"Quando ancora non mi conoscevi"
Ora.4 :
"Pensavi le cose peggiori"
Ora.5 :
"Quella notte io e te"
Ora.6 :
"Sesso ibuprofene"
[ reached max_ndoc ... 1,828 more documents ]
4.3.3 Esempio: i turni di parola nelle interviste
Vogliamo dividere i turni di parola segmentando i testi importati e organizzati in un corpus (vedi §3.3.5).
Agli intervistatori sono state indicazioni precise su come segnalare i turni di parola (#RIC per l’intervistatore e #INT per l’intervistato) e mettere eventuali commenti e note sull’interazione fra parentesi quadrate.
Si effettuerà quindi un controllo preliminare per una prima correzione ortografica con gli strumenti di Word, e per controllare che i marcatori all’interno del testo siano corretti.
Useremo corpus_segment() per separare i turni di parola e isolare solo le parole pronunciate dalla persona intervistata. Se infatti l’interazione è importante per comprendere il contesto dei contenuti espressi, è preferibile non analizzare contemporaneamente il linguaggio dei due interlocutori.
- Segmentiamo il testo in base al pattern (§ 4.3), e poi creiamo un subset in base alla nuova variabile “pattern” creata in conseguenza della segmentazione:
# segmentazione
demo_int_corpus <- corpus_segment(demo_corpus, pattern = "#*") %>%
# subset
corpus_subset(pattern == "#INT")Gli identificativi sono stati modificati:
demo_int_corpus[1]
Corpus consisting of 1 document and 1 docvar.
DCMSP0.2 :
"Salve, buon pomeriggio"
Fra i metadati di documento abbiamo però la variabile “ID”, che ci permetterà, volendolo, di raggruppare di nuovo tutti i segmenti riferibili ad una intervista in un solo documento, da segmentare successivamente, ad esempio, in frasi:
Corpus consisting of 10 documents and 24 docvars.
DCMSP0 :
"Salve, buon pomeriggio. Ok spero si senta tutto. Vabbè Nient..."
DSA0P0 :
"Sì, la sento, ma non so come si attiva la videocamera. Solam..."
DSMSP1 :
"No. Ok quindi non storico… pensavo. Ah no ok ok. Allora epis..."
...
4.4 Quanteda: la tokenizzazione
In Quanteda, la tokenizzazione può avvenire dopo la creazione del corpus e della sua eventuale segmentazione, o prescindere da questo passaggio.
4.4.1 La funzione tokens()
La funzione per il parsing dei testi (in parole, lettere o frasi) è tokens(), che si applica a un corpus o a un vettore carattere. Consideriamo qui il corpus costruito nel capitolo precedente, con i confini delle parole corretti:
Tokens consisting of 1 document.
sepolcri.txt :
[1] "All" "'" "ombra" "de" "'"
[6] "cipressi" "e" "dentro" "l" "'"
[11] "urne" "Confortate"
[ ... and 2,351 more ]
Applicandola ad un corpus segmentato, otteniamo un oggetto costituito da tanti elementi quanti sono i segmenti15:
Tokens consisting of 56 documents.
sepolcri.txt.1 :
[1] "All" "'" "ombra" "de" "'"
[6] "cipressi" "e" "dentro" "l" "'"
[11] "urne" "Confortate"
[ ... and 11 more ]
sepolcri.txt.2 :
[1] "Ove" "più" "il" "Sole" "Per" "me"
[7] "alla" "terra" "non" "fecondi" "questa" "Bella"
[ ... and 91 more ]
sepolcri.txt.3 :
[1] "Vero" "è" "ben" "," "Pindemonte"
[6] "!"
sepolcri.txt.4 :
[1] "Anche" "la" "Speme" "," "Ultima" "Dea"
[7] "," "fugge" "i" "sepolcri" ";" "e"
[ ... and 46 more ]
sepolcri.txt.5 :
[1] "Ma" "perché" "pria" "del" "tempo"
[6] "a" "sé" "il" "mortale" "Invidierà"
[11] "l" "'"
[ ... and 11 more ]
sepolcri.txt.6 :
[1] "Non" "vive" "ei" "forse" "anche" "sotterra"
[7] "," "quando" "Gli" "sarà" "muta" "l"
[ ... and 17 more ]
[ reached max_ndoc ... 50 more documents ]
I principali argomenti della funzione sono:
x |
corpus o vettore carattere | |
what |
il testo viene scomposto in parole (word) di default (alternative: character, sentence) |
|
split_hyphens = FALSE |
le parole unite da trattini vengono mantenute unite di default | |
include_docvars = TRUE |
le variabili dei documenti vengono incluse nell’oggetto tokens | |
remove_punct remove_symbols remove_numbers remove_url = FALSE |
Punteggiatura, simboli, numeri e url vengono conservati di default |
4.4.2 Punteggiatura, numeri e simboli
Una volta terminata la segmentazione del testo, punteggiatura e simboli non dovrebbero essere più necessari, e andrebbero eliminati. Si tratta della primo fondamentale step di normalizzazione dei testi.
Nota: In generale, in questa prima fase del trattamento dei testi non conviene trasformare il testo in lettere minuscole, in quanto le maiuscole possono servire ad individuare i nomi propri. In Quanteda, ad ogni modo, molte funzioni sono case-insensitive: di conseguenza, questa trasformazione non è necessaria.
Codice: esempio Sepolcri
Costruiamo l’oggetto tokens del testo segmentato, partendo dal corpus con gli apostrofi corretti:
# corpus con correzione dei confini (apostrofi)
sep.corpus <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus()
# segmentazione
sep.corpus.s <- corpus_reshape(sep.corpus, to = "sentences")
# tokenizzazione
sep.toks <- sep.corpus.s %>%
tokens(remove_punct = T,
remove_symbols = T,
remove_numbers = T)4.4.3 Individuazione degli n-gram
Un altro tipo di tokenizzazione del testo produce n-grams, ovvero le sequenze di token presenti nel testo. Tokenizziamo il corpus o il testo eliminando la punteggiatura, e poi usiamo la funzione tokens_ngrams():
Tokens consisting of 3 documents and 3 docvars.
ansa_2020-02-05.txt.1 :
[1] "Sanremo_Fiorello" "Fiorello_al" "al_festival"
[4] "festival_mai" "mai_più" "più_ospite"
[7] "ospite_magari" "magari_in" "in_gara"
ansa_2020-02-05.txt.2 :
[1] "Mi_ha" "ha_colpito" "colpito_Achille"
[4] "Achille_Lauro" "Lauro_Con" "Con_Amadeus"
[7] "Amadeus_sketch" "sketch_improvvisati"
ansa_2020-02-05.txt.3 :
[1] "Ve_lo" "lo_dico" "dico_da" "da_ospite"
[5] "ospite_al" "al_festival" "festival_di" "di_Sanremo"
[9] "Sanremo_non" "non_tornerò" "tornerò_più" "più_L"
[ ... and 266 more ]
Questi dati possono essere usati per l’analisi delle sequenze (modelli markoviani), ma anche per individuare le espressioni multilessicali presenti nel testo (cfr. anche §5.6.
Per estrarre sequenze di ordine superiore, indicheremo il numero di token da collegare:
Tokens consisting of 1 document.
sepolcri.txt :
[1] "All_ombra_de" "ombra_de_cipressi"
[3] "de_cipressi_e" "cipressi_e_dentro"
[5] "e_dentro_l" "dentro_l_urne"
[7] "l_urne_Confortate" "urne_Confortate_di"
[9] "Confortate_di_pianto" "di_pianto_è"
[11] "pianto_è_forse" "è_forse_il"
[ ... and 1,984 more ]
Ricordiamo infine la possibilità di estrarre skipgrams, ovvero sequenze composte tanto da token adiacenti, quanto da token separati da 1 o più altri token, allo scopo di evidenziare sequenze significative composte da parole non immediatamente adiacenti: nel testo “tazzina da caffè e piattino” sono linguisticamente significative tanto la sequenza “tazzina_da_ caffè”, quanto “tazzina_e_piattino”.
Per estrarre sequenze di 2 token, sia adiacenti che separati da un token, scriveremo:
Tokens consisting of 1 document.
sepolcri.txt :
[1] "All_ombra" "All_de" "ombra_de"
[4] "ombra_cipressi" "de_cipressi" "de_e"
[7] "cipressi_e" "cipressi_dentro" "e_dentro"
[10] "e_l" "dentro_l" "dentro_urne"
[ ... and 3,981 more ]
4.4.4 L’oggetto tokens
Il risultato è un oggetto di classe tokens, al quale potranno essere applicate alcune funzioni (§ ??), e a partire dal quale sarà possibile costruire una matrice testuale.
I metadati
L’oggetto porta con sé i metadati del corpus (include_docvars = TRUE), inclusi i nomi dei documenti e le eventuali altre variabili, che possono essere richiamati con meta(), docnames() e docvars() (cfr. § 3.3.3):
$fonte
[1] "angolotesti.it"
[1] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
ID cantante
1 1 Achille Lauro
2 2 Alberto Urso
Se applichiamo la funzione direttamente ad un testo o a un vettore carattere, i metadati potranno essere aggiunti successivamente, così come abbiamo visto per il corpus (§ 3.3.3).
toks <- tokens(canzoni$text,
remove_punct = T,
remove_symbols = T,
remove_numbers = T)
docnames(toks)[1] "text1" "text2"
[1] "01_Achille Lauro.txt" "02_Alberto Urso.txt"
ID cantante
1 1 Achille Lauro
2 2 Alberto Urso
Informazioni sui token
Con types() estraiamo un vettore con le occorrenze uniche dei token:
[1] "All" "ombra" "de" "cipressi" "e"
[6] "dentro" "l" "urne" "Confortate" "di"
[11] "pianto" "è" "forse" "il" "sonno"
[16] "Della" "morte" "men" "duro" "Ove"
Per conoscere il numero di token e type:
sepolcri.txt.1 sepolcri.txt.2 sepolcri.txt.3 sepolcri.txt.4
19 90 4 49
sepolcri.txt.5 sepolcri.txt.6 sepolcri.txt.7 sepolcri.txt.8
21 24 77 68
sepolcri.txt.9 sepolcri.txt.10 sepolcri.txt.11 sepolcri.txt.12
17 64 6 21
sepolcri.txt.13 sepolcri.txt.14 sepolcri.txt.15 sepolcri.txt.16
35 15 37 57
sepolcri.txt.17 sepolcri.txt.18 sepolcri.txt.19 sepolcri.txt.20
11 1 57 46
[1] 1998
[1] 1807
Le funzioni ntoken() e ntype()si applicano anche ai corpora.
4.5 Tidytext
4.5.1 Le funzioni per il parsing
In Tidytext, tutte le funzioni per il parsing iniziano con unnest_ ed hanno in comune i seguenti argomenti:
unnest_ ... (
tbl,
output,
input,
format = c("text", "man", "latex", "html", "xml"),
to_lower = TRUE,
drop = TRUE,
collapse = NULL,
...
)
tbl |
dataframe o tibble |
output |
nome della colonna che conterrà i tokens |
input |
nome della colonna che contiene i testi |
format |
formato del testo: “text” (parsing con tokenizers), “man”, “latex”, “html”, or “xml” (parsing con hunspell, estraendo come token esclusivamente le parole) |
to_lower = TRUE |
le maiuscole vengono trasformate in minuscolo di default |
drop = TRUE |
la colonna con il testo verrà eliminata di default |
Il testo originario, contenuto in una colonna del dataset (input) viene diviso in parti (frammenti, parole o altre unità di testo), in una nuova colonna (output), per ciascuna delle quali viene creata una nuova riga. Se il dataframe contiene altre variabili, le nuove righe replicheranno i loro valori.
Di default, la colonna di input viene eliminata (drop = TRUE), e i caratteri in maiuscolo vengono portati a minuscolo (to_lower = TRUE).
Possono inoltre essere usati argomenti non esplicitati nell’aiuto, ma ereditati dalle corrispettive funzioni del pacchetto tokenizers, ad esempio per controllare il trattamento della punteggiatura, dei numeri e dei simboli.
Mentre in Quanteda gli identificativi dei frammenti (“sepolcri.txt.1”, “sepolcri.txt.2”, …) sono univoci e conservano l’identificativo del documento originario (“sepolcri.txt”), in questo caso il riferimento al documento originario viene conservato (doc_id), ma non viene creato un identificativo di segmento.
4.5.2 La segmentazione
Segmentazione in frasi
La funzione per segmentare i testi in frasi è unnest_sentences():
tidy.sep <- sepolcri %>%
unnest_sentences(output = text,
input = text,
# lasciamo le maiuscole
to_lower = FALSE) Dal momento che vorremo usare questi frammenti come nuovi documenti, abbiamo usato per la colonna di output lo stesso nome “text”, e abbiamo lasciato il testo così com’è, conservando le maiuscole.
readtext object consisting of 6 documents and 0 docvars.
# A data frame: 6 × 2
doc_id text
* <chr> <chr>
1 sepolcri.txt "\"All'ombra \"..."
2 sepolcri.txt "\"Ove più il\"..."
3 sepolcri.txt "\"Vero è ben\"..."
4 sepolcri.txt "\"Anche la S\"..."
5 sepolcri.txt "\"Ma perché \"..."
6 sepolcri.txt "\"Non vive e\"..."
Classes ‘readtext’ and 'data.frame': 56 obs. of 2 variables:
$ doc_id: chr "sepolcri.txt" "sepolcri.txt" "sepolcri.txt" "sepolcri.txt" ...
$ text : chr "All'ombra de' cipressi e dentro l'urne Confortate
...
Segmentazione in paragrafi
Le funzione per segmentare i testi, rispettivamente, in paragrafi e in linee, sono: unnest_paragraphs() e unnest_lines().
Dal momento che i primi argomenti di queste funzioni sono “tbl”, “output”, “input”, possiamo scrivere direttamente:
oppure:
doc_id text
Length:12 Length:12
Class :character Class :character
Mode :character Mode :character
doc_id text cantante
Length:1833 Length:1833 Length:1833
Class :character Class :character Class :character
Mode :character Mode :character Mode :character
titolo
Length:1833
Class :character
Mode :character
Segmentazione in base ad espressioni regolari
La funzione unnest_regex() consente di segmentare il testo in base ad una stringa di testo, interpretata come espressione regolare.
doc_id text
Length:98 Length:98
Class :character Class :character
Mode :character Mode :character
Per suddividere l’articolo su Sanremo in titolo, sottotitolo e articolo, dovremo però procedere diversamente. Prima di tutto dividiamo il testo nelle tre parti:
readtext object consisting of 3 documents and 2 docvars.
# A data frame: 3 × 4
doc_id text fonte data
<chr> <chr> <chr> <date>
1 ansa_2020-02-05.txt "\"TITOLO San\"..." ansa 2020-02-05
2 ansa_2020-02-05.txt "\"SOTTOTITOL\"..." ansa 2020-02-05
3 ansa_2020-02-05.txt "\"ARTICOLO \"\"..." ansa 2020-02-05
4.5.3 Ricostruire l’identificativo del segmento
Mentre Quanteda crea o modifica le variabili automaticamente, Tidytext fa conto sulle altre funzioni del tidyverse.
Ad esempio, per creare la nuova variabile di raggruppamento (‘titolo’, ‘sottotitolo’ e ‘articolo’), usiamo separate() di tidyr, che divide le colonne in base ad un carattere di separazione (in questo caso lo spazio)16:
tidy.art <- tidy.art %>%
# colonna da dividere
separate(col = text,
# nomi delle colonne di risultato
c('seg_id', 'text'),
sep = " ",
# come trattare il testo che resta dopo il primo spazio
extra = 'merge')
tidy.art doc_id seg_id
1 ansa_2020-02-05.txt TITOLO
2 ansa_2020-02-05.txt SOTTOTITOLO
3 ansa_2020-02-05.txt ARTICOLO
text
1 Sanremo: Fiorello al festival mai più ospite, magari in gara\n\n
2 "Mi ha colpito Achille Lauro. Con Amadeus sketch improvvisati"\n\n
3 "Ve lo dico, da ospite al festival di Sanremo non tornerò più. L'ho fatto già troppe volte. Da conduttore non è proprio il caso. Ma potrei tornare da cantante". Tra il serio e il faceto Fiorello apre alla possibilità in un futuro di tornare a cantare e di farlo al Festival. "Mi piacerebbe tornare a fare il cantante serio. Ho anche un sacco di amici che potrebbero scrivere per me canzoni davvero belle. Da Lorenzo Jovanotti a Giuliano Sangiorgi".\n\n"Il momento che mi ha colpito di più? Achille Lauro mi ha fatto morire", dice Fiorello nella sua incursione a sorpresa in sala stampa. "L'esibizione del cantante è stata teatro, spettacolo. L'ho visto entrare in scena e ho pensato, guarda c'è Belfagor. Poi mi sono girato un attimo e l'ho visto nudo. Mi ha divertito molto". \n\nCon Amadeus, racconta, "eravamo d'accordo che dopo il numero iniziale sarei andato via e mi sarei fatto sentire magari al telefono. Invece ho pensato: ma dove vado? Solo in albergo? E sono rimasto lì. Tutto quello che è successo dopo è nato così, anche gli sketch con Amadeus. So quello che sa fare. Poco, ma lo fa bene. Fa ridere perché lo fa a modo suo". Quanto agli ascolti della prima serata, oltre 10 milioni di spettatori, "non pensavo che andasse così bene. Mi vesto da De Filippi come ho promesso? Non lo so, ci sto pensando anche se sui social insistono". Nella seconda serata del festival, stasera 5 febbraio, annuncia lo showman, "canterò una canzone inedita talmente bella che se fosse stata in gara avrebbe avuto l'opportunità di vincere qualcosa. La classica canzone di Sanremo". (ANSA).
fonte data
1 ansa 2020-02-05
2 ansa 2020-02-05
3 ansa 2020-02-05
Infine, volendo, modifichiamo i nomi dei segmenti:
tidy.art <- tidy.art %>%
# inseriamo la maiuscola ai nomi dei segmenti
mutate(seg_id = str_to_title(seg_id))
tidy.art doc_id seg_id
1 ansa_2020-02-05.txt Titolo
2 ansa_2020-02-05.txt Sottotitolo
3 ansa_2020-02-05.txt Articolo
text
1 Sanremo: Fiorello al festival mai più ospite, magari in gara\n\n
2 "Mi ha colpito Achille Lauro. Con Amadeus sketch improvvisati"\n\n
3 "Ve lo dico, da ospite al festival di Sanremo non tornerò più. L'ho fatto già troppe volte. Da conduttore non è proprio il caso. Ma potrei tornare da cantante". Tra il serio e il faceto Fiorello apre alla possibilità in un futuro di tornare a cantare e di farlo al Festival. "Mi piacerebbe tornare a fare il cantante serio. Ho anche un sacco di amici che potrebbero scrivere per me canzoni davvero belle. Da Lorenzo Jovanotti a Giuliano Sangiorgi".\n\n"Il momento che mi ha colpito di più? Achille Lauro mi ha fatto morire", dice Fiorello nella sua incursione a sorpresa in sala stampa. "L'esibizione del cantante è stata teatro, spettacolo. L'ho visto entrare in scena e ho pensato, guarda c'è Belfagor. Poi mi sono girato un attimo e l'ho visto nudo. Mi ha divertito molto". \n\nCon Amadeus, racconta, "eravamo d'accordo che dopo il numero iniziale sarei andato via e mi sarei fatto sentire magari al telefono. Invece ho pensato: ma dove vado? Solo in albergo? E sono rimasto lì. Tutto quello che è successo dopo è nato così, anche gli sketch con Amadeus. So quello che sa fare. Poco, ma lo fa bene. Fa ridere perché lo fa a modo suo". Quanto agli ascolti della prima serata, oltre 10 milioni di spettatori, "non pensavo che andasse così bene. Mi vesto da De Filippi come ho promesso? Non lo so, ci sto pensando anche se sui social insistono". Nella seconda serata del festival, stasera 5 febbraio, annuncia lo showman, "canterò una canzone inedita talmente bella che se fosse stata in gara avrebbe avuto l'opportunità di vincere qualcosa. La classica canzone di Sanremo". (ANSA).
fonte data
1 ansa 2020-02-05
2 ansa 2020-02-05
3 ansa 2020-02-05
Possiamo creare l’identificativo del segmento usando i numeri di riga:
tidy.sep <- tidy.sep %>%
mutate('seg_id' = as.integer(row.names(.))) %>%
select(doc_id, seg_id, everything())
head(tidy.sep)readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 3
doc_id seg_id text
* <chr> <int> <chr>
1 sepolcri.txt 1 "\"All'ombra \"..."
2 sepolcri.txt 2 "\"Ove più il\"..."
3 sepolcri.txt 3 "\"Vero è ben\"..."
4 sepolcri.txt 4 "\"Anche la S\"..."
5 sepolcri.txt 5 "\"Ma perché \"..."
6 sepolcri.txt 6 "\"Non vive e\"..."
Nota. In questo caso, con select() è stato utilizzato il selettore everything(): le variabili doc_id e seg_id sono state messe ai primi due posti, lasciando invariato l’ordine di tutte le altre (everything).
Vedi https://www.agnesevardanega.eu/wiki/r/tidyverse/selezionare_variabili.
Quando i documenti sono più di uno, potremmo preferire una numerazione progressiva all’interno dei singoli documenti. In questo caso, raggrupperemo il dataset in base alla variabile “doc_id” con group_by(), e poi useremo la funzione seq():
sanremo21 %>%
# segmenti (versi)
unnest_lines(text, text, to_lower = F) %>%
# group_by
group_by(doc_id) %>%
mutate(seg_id = seq(doc_id)) %>%
# cambiamo l'ordine delle colonne
select(doc_id, seg_id, everything()) %>%
# visualizziamo alcune righe
.[42:47,1:2]# A tibble: 6 × 2
# Groups: doc_id [2]
doc_id seg_id
<chr> <int>
1 Ora 42
2 Ora 43
3 Ora 44
4 Dieci 1
5 Dieci 2
6 Dieci 3
Volendo modificare l’identificativo di documento in maniera da renderlo univoco, unendolo a quello di segmento, useremo la funzione unite() di tidyr:
tidy.sep.id <- tidy.sep %>%
unite(doc_id,
# colonne da unire
doc_id, seg_id,
# separatore
sep=".")
head(tidy.sep.id$doc_id)[1] "sepolcri.txt.1" "sepolcri.txt.2" "sepolcri.txt.3"
[4] "sepolcri.txt.4" "sepolcri.txt.5" "sepolcri.txt.6"
4.5.4 La tokenizzazione
La funzione di base per il parsing dei testi in parole in è unnest_tokens():
Per ottenere come risultato un dataframe, o un dataframe tibble, possiamo modificare il dataframe di readtext:
o
È importante prestare attenzione al nome del campo con i token estratt. Per usare molte funzioni di Tidytext è importante che si chiami word (output = word), in quanto questo nome è quello presente anche in vari dizionari e lessici.
Per usare Tidytext insieme ad altri strumenti, sarà a volte meglio usare token, che è lo standard.
readtext object consisting of 6 documents and 0 docvars.
# A data frame: 6 × 3
doc_id word text
* <chr> <chr> <chr>
1 sepolcri.txt all'ombra "\"\"..."
2 sepolcri.txt de "\"\"..."
3 sepolcri.txt cipressi "\"\"..."
4 sepolcri.txt e "\"\"..."
5 sepolcri.txt dentro "\"\"..."
6 sepolcri.txt l'urne "\"\"..."
Si tratta della funzione più generale, in quanto, rispetto alle altre funzioni per il parsing, questa permette di definire il tipo di token, mediante l’argomento token: “words” (impostato come predefinito), ma anche “sentences”, “lines”, “paragraphs”, “regex” (viste nel § 4.5.2), nonché “characters”, “character_shingles”, “ngrams”, “skip_ngrams”, “tweets” – che pure hanno le loro funzioni.
unnest_tokens(
tbl,
output,
input,
token = "words",
format = c("text", "man", "latex", "html", "xml"),
to_lower = TRUE,
drop = TRUE,
collapse = NULL,
...
)
Dal momento che anche Tidytext, per il formato txt (vettori carattere), usa tokenizer e non consente di scegliere la funzione per il parsing, possiamo correggere i confini delle parole inserendo gli spazi fra gli apostrofi e la parola successiva:
sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_tokens(word, text) %>%
head()readtext object consisting of 6 documents and 0 docvars.
# A data frame: 6 × 3
doc_id word text
* <chr> <chr> <chr>
1 sepolcri.txt all "\"\"..."
2 sepolcri.txt ombra "\"\"..."
3 sepolcri.txt de "\"\"..."
4 sepolcri.txt cipressi "\"\"..."
5 sepolcri.txt e "\"\"..."
6 sepolcri.txt dentro "\"\"..."
Conviene effettuare questa correzione in questa fase, in quanto potremmo voler modificare il dataframe dei testi, e poi usarlo in altri contesti.
Otteniamo questo stesso risultato con la funzione unnest_regex(), indicando come pattern gli spazi (\s+) e la punteggiatura (^[:punct:]), dal momento che gli apostrofi sono considerati tali:
readtext object consisting of 6 documents and 0 docvars.
# A data frame: 6 × 3
doc_id word text
* <chr> <chr> <chr>
1 sepolcri.txt all "\"\"..."
2 sepolcri.txt ombra "\"\"..."
3 sepolcri.txt de "\"\"..."
4 sepolcri.txt cipressi "\"\"..."
5 sepolcri.txt e "\"\"..."
6 sepolcri.txt dentro "\"\"..."
Per eliminare i numeri e modificare il trattamento della punteggiatura, si possono utilizzare gli argomenti della funzione di tokenizers (quindi non esplicitati nell’aiuto della funzione, ma indicati in quanto ereditati con ...), di default: strip_punct = TRUE e strip_numeric = FALSE. Per eliminare anche i numeri, ad esempio:
sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_tokens(word, text,
strip_numeric = TRUE)
4.5.5 Codice: esempio Sepolcri
Possiamo anche effettuare segmentazione e tokenizzazione in un solo passaggio:
tidy.sep.toks <- sepolcri %>%
# segmenti
unnest_sentences(text, text) %>%
# identificativi di segmento
mutate('seg_id' = as.integer(row.names(.))) %>%
select(doc_id, seg_id, everything()) %>%
# correzione
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
# tokenizzazione
unnest_tokens(word, text,
strip_numeric = TRUE)
head(tidy.sep.toks)readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 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.5.6 Gli n-grammi in Tidytext
Per il parsing del testo in n-grammi, la funzione da utilizzare è unnest_ngrams():
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_ngrams(word, text) %>%
head()readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 4
doc_id seg_id word text
* <chr> <int> <chr> <chr>
1 sepolcri.txt 1 all ombra de "\"\"..."
2 sepolcri.txt 1 ombra de cipressi "\"\"..."
3 sepolcri.txt 1 de cipressi e "\"\"..."
4 sepolcri.txt 1 cipressi e dentro "\"\"..."
5 sepolcri.txt 1 e dentro l "\"\"..."
6 sepolcri.txt 1 dentro l urne "\"\"..."
La funzione equivale a unnest_tokens(token = "ngrams", ...) (cfr. Tabella 10):
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_tokens(token = "ngrams", word, text) Diversamente da Quanteda, il default è qui la sequenza di 3 token, e gli n-grammi non vengono uniti da “_” (vedi § 4.4.3). Per ottenere lo stesso risultato:
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_ngrams(word, text, n = 2, ngram_delim = "_") %>%
head()readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 4
doc_id seg_id word text
* <chr> <int> <chr> <chr>
1 sepolcri.txt 1 all_ombra "\"\"..."
2 sepolcri.txt 1 ombra_de "\"\"..."
3 sepolcri.txt 1 de_cipressi "\"\"..."
4 sepolcri.txt 1 cipressi_e "\"\"..."
5 sepolcri.txt 1 e_dentro "\"\"..."
6 sepolcri.txt 1 dentro_l "\"\"..."
Possiamo infine estrarre skipgrams, ovvero sequenze composte tanto dai token adiacenti, quanto da quelli separati da 1 o più altri token, con unnest_skip_ngrams (o con unnest_tokens(token = "skip_ngrams")). Il numero di token da “saltare” va indicato con l’argomento k, e verrà interpretato come 0:k: quindi verranno prodotte le sequenze adiacenti e quelle con un numero di token “saltati” da 1 a k:
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_skip_ngrams(word, text, k = 2) %>%
head()readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 4
doc_id seg_id word text
* <chr> <int> <chr> <chr>
1 sepolcri.txt 1 all "\"\"..."
2 sepolcri.txt 1 all ombra "\"\"..."
3 sepolcri.txt 1 all de "\"\"..."
4 sepolcri.txt 1 all cipressi "\"\"..."
5 sepolcri.txt 1 all ombra de "\"\"..."
6 sepolcri.txt 1 all ombra cipressi "\"\"..."
Inoltre, vengono estratti per default anche i singoli token (n_min = 1, anziché n_min = n come nella funzione precedente)
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_skip_ngrams(word, text, k = 2,
n = 3, n_min = 3) %>%
head()readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 4
doc_id seg_id word text
* <chr> <int> <chr> <chr>
1 sepolcri.txt 1 all ombra de "\"\"..."
2 sepolcri.txt 1 all ombra cipressi "\"\"..."
3 sepolcri.txt 1 all ombra e "\"\"..."
4 sepolcri.txt 1 all de cipressi "\"\"..."
5 sepolcri.txt 1 all de e "\"\"..."
6 sepolcri.txt 1 all de dentro "\"\"..."
4.6 Altri parser
Come si è detto sopra, possiamo costruire un oggetto tokens anche con altri parser.
4.6.1 spaCy e spacyr
Quanteda implementa le funzioni di parsing, tagging grammaticale e lemmatizzazione attraverso SpaCy, una libreria di Python (https://spacy.io/) (vedi § (ref?)(lem-spacy)). Il pacchetto dedicato, spacyr, prevede anche una funzione per la semplice tokenizzazione.
Installiamo il pacchetto.
La funzione spacy_install() installerà tutto quello che serve (incluso Python) in un ambiente dedicato.
Dobbiamo inoltre scaricare ed installare i modelli linguistici che intendiamo usare, in questo caso, quello per l’italiano17.
Per usare le configurazioni del modello, scriveremo:
Con Quanteda
Per effettuare la semplice tokenizzazione del testo in parole, useremo la funzione spacy_tokenize(), che ha gli stessi argomenti e le stesse impostazioni di default della funzione interna di Quanteda:
what = c("word", "sentence"),
remove_punct = FALSE,
remove_url = FALSE,
remove_numbers = FALSE,
remove_separators = TRUE,
remove_symbols = FALSE,
....
output = c("list", "data.frame")
spacy_tokenize("All'ombra de' cipressi e dentro l'urne\nConfortate di pianto è forse il sonno\nDella morte men duro?")$text1
[1] "All'" "ombra" "de" "'" "cipressi"
[6] "e" "dentro" "l'" "urne" "Confortate"
[11] "di" "pianto" "è" "forse" "il"
[16] "sonno" "Della" "morte" "men" "duro"
[21] "?"
Il risultato è una lista che può essere trasformata in un oggetto tokens di Quanteda, con as.tokens().
Usando questa funzione per il corpus segmentato:
spacy.sep.toks <- sepolcri %>%
# segmentazione
corpus() %>%
corpus_reshape("sentences") %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T) %>%
as.tokens() Length Class Mode
sepolcri.txt.1 19 -none- character
sepolcri.txt.2 90 -none- character
sepolcri.txt.3 4 -none- character
sepolcri.txt.4 49 -none- character
sepolcri.txt.5 21 -none- character
sepolcri.txt.6 24 -none- character
Tokens consisting of 1 document.
sepolcri.txt.1 :
[1] "All'" "ombra" "de" "cipressi" "e"
[6] "dentro" "l'" "urne" "Confortate" "di"
[11] "pianto" "è"
[ ... and 7 more ]
Nota: Gli apostrofi fra due parole senza spazio (“All’ombra”) sono considerati parte della parola elisa. Quelli seguiti da spazio (“de’ cipressi”) vengono invece trattati come punteggiatura. Questo può avere conseguenze per quel che riguarda l’uso di liste e dizionari.
Con Tidytext
Per usare questo tokenizzatore in Tidytext, è sufficiente indicare output = "data.frame":
sepolcri %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T,
# data frame
output = "data.frame") %>%
head() doc_id token
1 sepolcri.txt All'
2 sepolcri.txt ombra
3 sepolcri.txt de
4 sepolcri.txt cipressi
5 sepolcri.txt e
6 sepolcri.txt dentro
Per un testo segmentato, si vorranno creare prima identificativi univoci:
# dataframe dei segmenti
tidy.sep.id <- tidy.sep %>%
unite(doc_id,
# colonne da unire
doc_id, seg_id,
# separatore
sep=".") tidy.sep.id %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T,
# data frame
output = "data.frame") %>%
head() doc_id token
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
Per eliminare gli apostrofi e portare tutti i termini in minuscolo, ed avere così un dataframe delle parole con la stessa struttura di quelli di Tidytext, useremo anche unnest_tokens():
tidy.sep.id %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T,
# data frame
output = "data.frame") %>%
unnest_tokens(token, token) %>%
head() doc_id token
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.6.2 Hunspell
Altra opzione per la semplice tokenizzazione del testo in parole è il parser di Hunspell (si veda anche §@ref(#hunspell-corr)).
Con Quanteda
La funzione per la tokenizzazione è (https://www.rdocumentation.org/packages/hunspell/topics/hunspell)[hunspell_parse()], che si applica ad un vettore carattere, e richiede di indicare il dizionario:
List of 2
$ : chr [1:367] "Achille" "Lauro" "Me" "Ne" ...
$ : chr [1:208] "Alberto" "Urso" "Il" "Sole" ...
Il risultato è una lista che può essere trasformata in un oggetto di classe tokens, con as.tokens(), oppure con tokens(), che ci consente di rimuovere numeri e simboli (in questo caso, non è necessario rimuovere la punteggiatura perché è stata già eliminata dal parser di Hunspell):
toks <- canzoni$text %>%
hunspell::hunspell_parse(dict = "it_IT") %>%
tokens(remove_numbers = T,
remove_symbols = T)
is.tokens(toks)[1] TRUE
All’oggetto tokens così costruito aggiungeremo i nomi dei documenti, e — se necessario — i metadati (che possono essere aggiunti anche successivamente alla matrice testuale):
Con Tidytext
La funzione hunspell_parse() può essere applicata alla colonna contentente i testi di un dataframe, con mutate():
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 "\"c(\"Achille\"..." 1 Achille Lauro
2 02_Alberto Urso.txt "\"c(\"Alberto\"..." 2 Alberto Urso
Come si vede, nella colonna “text” troviamo la lista dei token. Si tratta di una lista annidata, che può essere unnested con la funzione unnest() di tidyr (la “progenitrice” delle funzioni per il parsing di tidytext):
tidy.sep %>%
mutate(text = hunspell::hunspell_parse(text, dict = "it_IT")) %>%
unnest(text) %>%
head()# A tibble: 6 × 3
doc_id seg_id text
<chr> <int> <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.7 Lemmatizzazione e POS
Qualora si decida di usare i lemmi nella costruzione della matrice testuale, conviene farlo sin da subito, costruendo cioè un oggetto token di lemmi anziché di forme flesse.
In questo caso, non solo non ha senso, ma è preferibile non correggere gli apostrofi, in quanto fanno parte della punteggiatura e servono dunque a identificare le funzioni grammaticali e le parti del discorso. Inoltre, i parser disponibili, trattano gli apostrofi nel modo corretto.
4.7.1 spacyr
Il principale vantaggio dell’uso di spacyr consiste nella sua piena integrazione nel sistema adottato da Quanteda.
spacy_parse() è la funzione principale del pacchetto (cfr. anche § 4.6.1), e può essere applicata tanto al dataframe dei testi quanto al corpus.
# dal dataframe dei testi
spacy.sep.pos <- spacy_parse(sepolcri)
# righe dalla 20 alla 29
spacy.sep.pos[20:29,] doc_id sentence_id token_id token lemma pos entity
20 sepolcri.txt 1 20 morte morte NOUN MISC_I
21 sepolcri.txt 1 21 men Men ADV MISC_I
22 sepolcri.txt 1 22 duro duro ADJ MISC_I
23 sepolcri.txt 1 23 ? ? PUNCT MISC_I
24 sepolcri.txt 2 1 Ove Ove ADV
25 sepolcri.txt 2 2 più più ADV
26 sepolcri.txt 2 3 il il DET
27 sepolcri.txt 2 4 Sole Sole PROPN PER_B
28 sepolcri.txt 2 5 \n \n SPACE PER_I
29 sepolcri.txt 2 6 Per per ADP PER_I
Il risultato è un dataframe che di default contiene, oltre ai token, anche i lemmi, il tagging grammaticale (pos, part of speech), il riconoscimento delle entità (entity).
È possibile personalizzare l’output utilizzando gli argomenti della funzione18:
spacy_parse(
x,
pos = TRUE,
tag = FALSE,
lemma = TRUE,
entity = TRUE,
dependency = FALSE,
nounphrase = FALSE,
multithread = TRUE,
additional_attributes = NULL,
...
)
Nei risultati, la punteggiatura viene conservata, così come gli spazi doppi (\n alla riga 28 è un confine di paragrafo \n\n): la colonna “pos” permette di selezionarli e/o filtrarli.
Il dataframe risultante contiene anche le colonne con l’identificativo del documento (doc_id) e della frase (sentence_id). La funzione può essere dunque utilizzata anche per la segmentazione del testo in frasi.
La numerazione dei token (token_id) è relativa a quella delle frasi (sentence_id), ovvero ricomincia da 1 a ogni nuova frase.
Segmentazione in frasi
In questo caso, le frasi (sentences) individuate sono 89, anziché 56:
[1] 89
Sono infatti considerati confini di frase:
- i segni di punteggiatura forti, inclusi “;” e “:”,
- i doppi spazi di nuova riga (
\n\n), ovvero quelli che indicano un nuovo paragrafo.
Alternativamente, è anche possibile utilizzare il corpus di Quanteda segmentato, sempre senza correzione degli apostrofi.
doc_id sentence_id token_id token lemma pos entity
1 sepolcri.txt.1 1 1 All' a il ADP
2 sepolcri.txt.1 1 2 ombra ombra NOUN
3 sepolcri.txt.1 1 3 de de ADP
4 sepolcri.txt.1 1 4 ' ' PUNCT
5 sepolcri.txt.1 1 5 cipressi cipresso NOUN
6 sepolcri.txt.1 1 6 e e CCONJ
doc_id sentence_id token_id token lemma pos entity
20 sepolcri.txt.1 1 20 duro duro ADJ MISC_I
21 sepolcri.txt.1 1 21 ? ? PUNCT MISC_I
22 sepolcri.txt.2 1 1 Ove Ove SCONJ
23 sepolcri.txt.2 1 2 più più ADV
24 sepolcri.txt.2 1 3 il il DET
25 sepolcri.txt.2 1 4 Sole Sole PROPN MISC_B
26 sepolcri.txt.2 1 5 Per per ADP MISC_I
27 sepolcri.txt.2 1 6 me me PRON MISC_I
28 sepolcri.txt.2 1 7 alla a il ADP MISC_I
29 sepolcri.txt.2 1 8 terra terra NOUN MISC_I
Uso in Quanteda e Tidytext
Il dataframe creato da spacyr può essere usato direttamente in Tidytext, e trasformato come oggetto tokens di Quanteda:
Tokens consisting of 56 documents.
sepolcri.txt.1 :
[1] "All'" "ombra" "de" "'"
[5] "cipressi" "e" "dentro" "l'"
[9] "urne" "Confortate" "di" "pianto"
[ ... and 9 more ]
sepolcri.txt.2 :
[1] "Ove" "più" "il" "Sole" "Per" "me"
[7] "alla" "terra" "non" "fecondi" "questa" "Bella"
[ ... and 88 more ]
sepolcri.txt.3 :
[1] "Vero" "è" "ben" ","
[5] "Pindemonte" "!"
...
In ogni caso, l’oggetto non conterrà le altre variabili di documento, oltre al nome.
Volendo eliminare la punteggiatura nell’oggetto tokens, sfrutteremo il tagging grammaticale:
oppure
# prima trasformare il dataframe in oggetto tokens
as.tokens(spacy.sep.toks) %>%
# poi applicare la funzione
tokens(remove_punct = T) Per costruire l’oggetto tokens con i lemmi:
Chiudiamo la sessione di spaCy, in questo modo:
4.7.2 koRpus e TreeTagger
Molti utenti italiani usano TreeTagger per la lemmatizzazione e il riconoscimento delle parti del discorso.
Il pacchetto koRpus (Michalke 2021) trova il suo contesto d’uso di elezione nell’ambito dell’analisi lessicale (tagging grammaticale, misure di diversità lessicale, indici di leggibilità) e della statistica dei corpora.
Nonostante il nome, consente di trattare un solo testo alla volta (anche se è disponibile un plugin di tm, per trattare un corpus composto di più testi o segmenti; vedi § 4.7.3).
La funzione (treetag())19 effettua la lemmatizzazione e il tagging grammaticale usando TreeTagger (Helmut Schmid 1994; Schmid 1999), che deve essere installato nel sistema20.
È possibile leggere direttamente un testo o una serie di testi da una cartella, ma in questo caso la codifica deve essere UTF-821.
L’output — un dataframe — può essere facilmente utilizzato con Tydytext e Quanteda, e/o trasformato in una matrice testuale.
4.7.2.1 Il parsing con TreeTagger
I pacchetti da installare sono quello della libreria, e quello dei parametri per la lingua:
Al momento, il modello per la lingua italiana può essere installato con il comando:
Poi, richiamiamo direttamente la libreria per la lingua italiana:
La funzione treetag() ha diversi argomenti per personalizzare il risultato, utilizzando sia le funzionalità di TreeTag che quelle di koRpus.
Fondamentale è indicare il percorso di sistema in cui è installato TreeTagger (su Windows, quello consigliato è ‘C:/TreeTagger’).
tagged.sep <- treetag(
sepolcri$text,
format = "obj", # oggetto interno, e non file esterno
lang="it",
doc_id = "sepolcri", # identificativo
sentc.end = c(".", "!", "?"), # confini delle frasi
treetagger="manual", # indicare le successive opzioni
TT.options=list(
path="C:/TreeTagger", # percorso di sistema
preset="it"
)
)In particolare:
è necessario specificare che si sta utilizzando un oggetto dello spazio di lavoro (
format = "obj"); se si usa un file esterno, invece, non è necessario;possiamo scegliere i confini delle frasi (
sentc.end = c(".", "!", "?")): in questo caso, abbiamo adottato quelli usati da Quanteda;possiamo modificare l’identificativo del documento (il consueto campo
doc_id)
Il risultato del comando consiste in una lista di oggetti ed informazioni, fra cui il dataframe dei token:
doc_id token tag lemma lttr wclass desc stop stem
1 sepolcri All' PRE:det al 4 preposition NA NA NA
2 sepolcri ombra NOM ombra 5 noun NA NA NA
3 sepolcri de NPR de 2 name NA NA NA
4 sepolcri ' PON ' 1 punctuation NA NA NA
5 sepolcri cipressi NOM cipresso 8 noun NA NA NA
6 sepolcri e CON e 1 conjunction NA NA NA
idx sntc
1 1 1
2 2 1
3 3 1
4 4 1
5 5 1
6 6 1
Il dataframe dei token può essere richiamato anche con la funzione taggedText():
Le funzioni tokens() e types() estraggono i vettori carattere — appunto — dei token e dei type:
[1] "all'" "ombra" "de" "cipressi" "e" "dentro"
[1] "e" "il" "l'" "di" "le" "a"
Nella colonna ‘sntc’ troviamo l’identificativo di frase o segmento, i cui confini sono stati indicati fra gli argomenti della funzione. Avendo usato gli stessi criteri di Quanteda, le frasi individuate sono 56:
[1] 56
Uso in Quanteda e Tidytext
La funzione as.tokens() può essere applicata solo a liste, oggetti di spacyr e altri oggetti di classe tokens. Dobbiamo pertanto trasformare il vettore carattere dei token o dei lemmi in una lista, e poi usare as.tokens():
Tokens consisting of 1 document.
text1 :
[1] "All'" "ombra" "de" "cipressi" "e"
[6] "dentro" "l'" "urne" "Confortate" "di"
[11] "pianto" "è"
[ ... and 1,979 more ]
Per esportare i lemmi:
Tokens consisting of 1 document.
text1 :
[1] "al" "ombra" "de" "cipresso" "e"
[6] "dentro" "il" "urna" "confortare" "di"
[11] "pianto" "essere"
[ ... and 1,979 more ]
In questo caso, però, perdiamo la segmentazione in frasi.
Conviene pertanto passare da Tidytext, e poi costruire una matrice testuale per le analisi successive (§ 5.8.2).
tagged.sep@tokens %>%
# elimina punteggiatura, apostrofi ecc.
unnest_tokens(token, token) %>%
head() doc_id token tag lemma lttr wclass desc stop stem
1 sepolcri all PRE:det al 4 preposition NA NA NA
2 sepolcri ombra NOM ombra 5 noun NA NA NA
3 sepolcri de NPR de 2 name NA NA NA
4 sepolcri cipressi NOM cipresso 8 noun NA NA NA
5 sepolcri e CON e 1 conjunction NA NA NA
6 sepolcri dentro PRE dentro 6 preposition NA NA NA
idx sntc
1 1 1
2 2 1
3 3 1
4 5 1
5 6 1
6 7 1
4.7.3 Il parsing di più testi con tm
Per trattare un corpus composto da più testi, è disponibile un plugin di tm, tm.plugin.koRpus:
In questo caso, impostiamo le opzioni per il parsing con la funzione di koRpus set.kRp.env():
e poi usiamo la funzione readCorpus(), sempre indicando che i testi si trovano in un oggetto dello spazio di lavoro (format = "obj"):
Notare che la struttura del dataframe di partenza deve contenere, come in tm almeno un campo doc_id e un campo text.
Il risultato che si ottiene è un corpus di tm (vedi § (ref?)(corpus-tm)), insieme a un corpus di koRpus:
<<VCorpus>>
Metadata: corpus specific: 0, document level (indexed): 3
Content: documents: 31
I metadati sono mantenuti, come parte del corpus di tm:
# A tibble: 31 × 3
cantante titolo doc_id
<chr> <chr> <chr>
1 Aiello Ora Ora
2 Annalisa Scarrone Dieci Dieci
3 Arisa Potevi Fare Di Più Potev…
4 Bugo E Invece Sì E_Inv…
5 COLAPESCEDIMARTINO Musica Leggerissima Music…
6 Coma Cose Fiamme Negli Occhi Fiamm…
7 Ermal Meta Un Milione Di Cose Da Dirti Un_Mi…
8 Extraliscio Bianca Luce Nera (Feat. Davide … Bianc…
9 Fasma Parlami Parla…
10 Francesca Michielin & Fedez Chiamami Per Nome Chiam…
# ℹ 21 more rows
Il dataframe dei token contiene anche gli identificativi dei testi.
doc_id token tag lemma lttr wclass desc stop stem idx sntc
1 Ora Ora ADV ora 3 adverb NA NA NA 1 NA
2 Ora ora ADV ora 3 adverb NA NA NA 2 NA
3 Ora ora ADV ora 3 adverb NA NA NA 3 NA
4 Ora ora ADV ora 3 adverb NA NA NA 4 NA
5 Ora Mi PRO:pers mi 2 pronoun NA NA NA 5 NA
6 Ora parli VER:pres parlare 5 verb NA NA NA 6 NA
4.8 Funzioni usate
Prendere decisioni e gestire il parsing del testo richiede una certa conoscenza delle funzioni che consentono il trattamento dei vettori carattere e dei testi, compreso l’uso delle espressioni regolari22.
In particolare, per gestire al meglio il parsing con Tidytext, e soprattutto con il tagger di koRpus, TreeTagger, abbiamo fatto ampio uso delle funzioni per la ricodifica delle variabili e la trasformazione dei dataframe.
| Funzioni | pacchetto | |
|---|---|---|
as.integer(),as.numeric() |
trasformare il tipo di vettore | base |
format(), formatC() |
formattare i valori di una variabile | base |
separate(), unite() |
separare / unire due colonne | tidyr | ÿ tr
mutate() |
modificare i valori di una variabile | dplyr |
everything() |
helper nella selezione delle variabili | dplyr |
group_by() |
definire una variabile di raggruppamento | dplyr |
summarise() |
sintetizzare un dataset in base a statistiche | dplyr |
str_to_title() |
cambiare la prima lettera delle parole in maiuscolo | stringr |
4.9 Appendice
Una funzione per correggere gli apostrofi
Anziché scrivere ogni volta:
come in
possiamo usare una funzione personale, che renderà più rapida la scrittura. Qui l’abbiamo chiamata “apostrofo”:
Una volta eseguito il comando precedente, possiamo scrivere:
Le funzioni personali possono essere salvate in un file di script con estensione .R, da caricare con il comando source() all’inizio della sessione23.
I testi segmentati in tm
Uno dei vantaggi dell’uso di Tidytext è la facilità con la quale permette di passare da uno strumento all’altro, fra quelli disponibili per l’analisi semi-automatica dei testi in R.
Il dataframe dei frammenti costruito in Tidytext, con gli identificativi univoci
readtext object consisting of 6 documents and 1 docvar.
# A data frame: 6 × 3
doc_id seg_id text
* <chr> <int> <chr>
1 sepolcri.txt 1 "\"All'ombra \"..."
2 sepolcri.txt 2 "\"Ove più il\"..."
3 sepolcri.txt 3 "\"Vero è ben\"..."
4 sepolcri.txt 4 "\"Anche la S\"..."
5 sepolcri.txt 5 "\"Ma perché \"..."
6 sepolcri.txt 6 "\"Non vive e\"..."
può essere usato per costruire un corpus in tm, che non prevede funzioni specifiche per la segmentazione.
Prima di richiamare la libreria, è bene chiudere Quanteda, se in uso:
library(tm)
# identificativo univoco
tidy.sep.id <- tidy.sep %>%
unite(doc_id,
# colonne da unire
doc_id, seg_id,
# separatore
sep=".")
sep.tm.corpus <- tidy.sep.id %>%
DataframeSource() %>%
VCorpus(readerControl = list(language = "it-IT"))[1] "sepolcri.txt.1" "sepolcri.txt.2" "sepolcri.txt.3"
[4] "sepolcri.txt.4" "sepolcri.txt.5" "sepolcri.txt.6"
Senza identificativi univoci, non sarebbe possibile costruire una matrice testuale valida.
<<PlainTextDocument>>
Metadata: 7
Content: chars: 23
Vero è ben, Pindemonte!
4.10 Codice del capitolo
## Strumenti per l'analisi testuale e il text mining con R
## Agnese Vardanega avardanega@unite.it
## Capitolo 4 - Il parsing
## Pacchetti --------------------------------------------------------------
# install.packages("spacyr")
# spacy_download_langmodel("it_core_news_sm")
# install.packages("koRpus")
# install.packages("koRpus.lang.it",
# repo="https://undocumeantit.github.io/repos/l10n")
# install.packages("tm.plugin.koRpus")
library(tidyverse)
# QUANTEDA ----------------------------------------------------------------
# SEGMENTAZIONE
library(quanteda)
quanteda_options(language_stemmer ="italian")
sep.corpus <- sepolcri %>%
#mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus()
# corpus_reshape
sep.corpus.s <- corpus_reshape(sep.corpus,
to = "sentences")
# corpus_segment, pattern
art.corpus <- articolo %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus() %>%
# segmentazione
corpus_segment(pattern = "##*")
# correggere la nuova variabile
art.corpus$pattern <- art.corpus$pattern %>%
str_remove_all("##") %>%
str_to_title()
# tabella del summary con group_by e summarise
summary(art.corpus) %>%
group_by(pattern) %>%
summarise(Tokens = sum(Tokens),
Types = sum(Types))
# segmentazione con pattern: frasi
sep.corpus %>%
corpus_segment(pattern ="[\\.\\!\\?;:]",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")
# paragrafi
sep.corpus %>%
corpus_segment(pattern ="\\n\\n",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")
# linee
sanremo_21 %>%
corpus_segment(pattern = "\\n",
valuetype = "regex",
extract_pattern = FALSE,
pattern_position = "after")
# TOKENIZZAZIONE
# tokens (funzione base)
sep.corpus %>%
tokens()
sep.corpus.s %>%
tokens()
## Esempio Sepolcri
# corpus con correzione dei confini (apostrofi)
sep.corpus <- sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
corpus()
# segmentazione
sep.corpus.s <- corpus_reshape(sep.corpus, to = "sentences")
# tokenizzazione
sep.toks <- sep.corpus.s %>%
tokens(remove_punct = T,
remove_symbols = T,
remove_numbers = T)
## ngrams
tokens(art.corpus, remove_punct = T) %>%
tokens_ngrams()
# sequenze di 3 tokens
tokens(sep.corpus, remove_punct = T) %>%
tokens_ngrams(3)
#skipgrams
tokens(sep.corpus, remove_punct = T) %>%
tokens_skipgrams(n = 2, skip = 0:1)
# metadati
tokens(sanremo) %>%
meta()
tokens(sanremo) %>%
docnames()
tokens(sanremo) %>%
docvars()
# modificare i metadati
toks <- tokens(canzoni$text,
remove_punct = T,
remove_symbols = T,
remove_numbers = T)
docnames(toks) <- canzoni$doc_id
docvars(toks) <- canzoni[,-c(1,2)]
docnames(toks)
docvars(toks)
# informazioni sui tokens
# types
types(sep.toks)
# tokens per ciascun documento
ntoken(sep.toks)
# somma
ntoken(sep.toks) %>% sum()
# types
ntype(sep.toks) %>% sum()
# TIDYTEXT ----------------------------------------------------------------
library(tidytext)
# unnest_sentences
tidy.sep <- sepolcri %>%
unnest_sentences(output = text,
input = text,
# lasciamo le maiuscole
to_lower = FALSE)
## SEGMENTAZIONE (frammenti)
# unnest_paragraphs
unnest_paragraphs(sepolcri, text, text)
# o
sepolcri %>%
unnest_paragraphs(text, text)
# unnest_lines
sanremo21 %>%
unnest_lines(text, text)
# unnest_regex
sepolcri %>%
unnest_regex(text, text,
pattern ="[\\.\\!\\?;:]")
# unnest_regex: esempio articolo
tidy.art <- articolo %>%
unnest_regex(text, text, pattern = "##",
to_lower = FALSE)
## Variabili di documento
# creare la nuova variabile
tidy.art <- tidy.art %>%
# colonna da dividere
separate(col = text,
# nomi delle colonne di risultato
c('seg_id', 'text'),
sep = " ",
# come trattare il testo che resta dopo il primo spazio
extra = 'merge') %>%
# nomi dei segmenti
mutate(seg_id = str_to_title(seg_id))
# identificativo di segmento
# progressivo (numero di riga)
tidy.sep <- tidy.sep %>%
mutate('seg_id' = as.integer(row.names(.))) %>%
# cambiamo l'ordine delle colonne
select(doc_id, seg_id, everything())
# progressivo all'interno dei singoli documenti
sanremo21 %>%
# segmenti (versi)
unnest_lines(text, text, to_lower = F) %>%
# con group_by
group_by(doc_id) %>%
# e poi seq
mutate(seg_id = seq(doc_id)) %>%
select(doc_id, seg_id, everything())
# uniamo identificativi di documento e segmento
tidy.sep.id <- tidy.sep %>%
unite(doc_id,
# colonne da unire
doc_id, seg_id,
# separatore
sep=".")
## TOKENIZZAZIONE
# unnest_token (funzione base)
sepolcri %>%
unnest_tokens(output = word, input = text)
# con correzione dell'apostrofo
sepolcri %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_tokens(output = word, input = text)
# equivale a
# confini con espressione regolare (regex)
sepolcri %>%
unnest_regex(word, text, pattern = "[\\s+^[:punct:]]")
## Esempio Sepolcri (con segmentazione)
tidy.sep.toks <- sepolcri %>%
# segmenti
unnest_sentences(text, text) %>%
# identificativi di segmento
mutate('seg_id' = as.integer(row.names(.))) %>%
select(doc_id, seg_id, everything()) %>%
# correzione
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
# tokenizzazione
unnest_tokens(word, text,
strip_numeric = TRUE)
## ngrams
# unnest_ngrams
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_ngrams(word, text)
tidy.sep %>%
mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>%
unnest_skip_ngrams(word, text, k = 2,
n = 3, n_min = 3)
# ALTRI PARSER ------------------------------------------------------------
## spacyr in Quanteda (oggetto token)
library(spacyr)
#spacy_download_langmodel("it_core_news_sm")
spacy_initialize(model = "it_core_news_sm")
toks <- canzoni %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T) %>%
as.tokens()
docnames(toks)
docvars(toks) <- canzoni[,-c(1,2)]
spacy.sep.toks <- sepolcri %>%
# segmentazione
corpus() %>%
corpus_reshape("sentences") %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T) %>%
as.tokens()
docnames(spacy.sep.toks)
# uso con Tidytext
sepolcri %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T,
# data frame
output = "data.frame")
# identificativo univoco
tidy.sep.id %>%
spacy_tokenize(remove_punct = T,
remove_numbers = T,
remove_symbols = T,
# data frame
output = "data.frame")
spacy_finalize()
detach("package:spacyr")
## hunspell in Quanteda
toks <- canzoni$text %>%
hunspell::hunspell_parse(dict = "it_IT") %>%
tokens(remove_numbers = T,
remove_symbols = T)
# variabili dei documenti
docnames(toks) <- canzoni$doc_id
docvars(toks) <- canzoni[,-c(1,2)]
## hunspell in Tidytext
tidy.sep %>%
mutate(text = hunspell::hunspell_parse(text, dict = "it_IT")) %>%
unnest(text)
# LEMMATIZZAZIONE E POS ---------------------------------------------------
## spacyr
library(spacyr)
spacy_initialize(model = "it_core_news_sm")
# dal dataframe dei testi
spacy_parse(sepolcri)
# dal corpus segmentato
spacy_parse(sep.corpus.s)
# attenzione, gli apostrofi qui servono:
spacy.sep.pos <- sepolcri %>%
corpus() %>%
corpus_reshape(to = "sentences") %>%
spacy_parse()
spacy_finalize()
detach("package:spacyr")
# costruire un oggetto token
spacy.sep.pos %>%
as.tokens()
# senza punteggiatura
spacy.sep.pos %>%
filter(pos != "PUNCT" ) %>%
as.tokens()
# esportare i lemmi
spacy.sep.pos %>%
filter(pos != "PUNCT" ) %>%
as.tokens(use_lemma=T)
## koRpus e TreeTagger
# incompatibile con Quanteda
detach("package:quanteda")
library(koRpus.lang.it)
# applicato al testo
tagged.sep <- treetag(
sepolcri$text,
format = "obj", # oggetto interno, e non file esterno
lang="it",
doc_id = "sepolcri", # identificativo
sentc.end = c(".", "!", "?"), # confini delle frasi
treetagger="manual",
TT.options=list(
path="C:/TreeTagger", # percorso di sistema
preset="it"
)
)
# il dataframe dei tokens
head(tagged.sep@tokens)
taggedText(tagged.sep) %>% head()
# vettore dei tokens
tokens(tagged.sep)
# vettore dei types
types(tagged.sep)
# uso in Quanteda
library(quanteda)
# quanteda_options(language_stemmer ="italian")
list(tagged.sep@tokens$token) %>%
quanteda::tokens(remove_punct = T)
detach("package:quanteda")
# koRpus e tm --------------------------
library(tm)
library(tm.plugin.koRpus)
# opzioni di sistema
set.kRp.env(lang="it",
TT.cmd = "manual",
TT.options=list(
path="C:/TreeTagger",
preset="it"))
tagged.sanremo21 <- sanremo21 %>%
readCorpus(format = "obj")
corpusTm(tagged.sanremo21)
meta(corpusTm(tagged.sanremo21))
head(taggedText(tagged.sanremo21))
# disattivare i pacchetti
detach("package:tm.plugin.koRpus")
detach("package:tm")
detach("package:koRpus.lang.it")
detach("package:koRpus")
# APPENDICE ---------------------------------------------------------------
## fuzione "apostrofo"
apostrofo <- function (x) {
stringr::str_replace_all(x, "[\'’](?!\\s)", "' ")
}
# uso
sepolcri %>% mutate(text = apostrofo(text))
## corpus segmentato in tm con Tidytest
# incompatibile con Quanteda
# detach("package:quanteda")
library(tm)
# dataframe segmentato con identificativi univoci
sep.tm.corpus <- tidy.sep.id %>%
DataframeSource() %>%
VCorpus(readerControl = list(language = "it-IT"))
detach("package:tm")
Usiamo qui una regex, espressione regolare. Per sintassi e uso, cfr. Beri (2007). Vedi anche le voci: “Ricerca e sostituzione testo” e “Espressioni regolari in R”.↩︎
Il risultato coincide con la segmentazione in frasi della funzione di tokenizer (
tokenize_sentences).↩︎Si tratta di un oggetto di classe tokens, che vedremo in dettaglio nel § 4.4.4.↩︎
Vedi https://www.agnesevardanega.eu/wiki/r/tidyverse/tidyr_unite_separate.↩︎
La lista dei modelli disponibili e la valutazione della loro qualità è disponibile all’indirizzo https://spacy.io/usage/models. Vale la pena controllare gli aggiornamenti di questi modelli, che sono in rapida evoluzione.↩︎
L’argomento
additional_attributes()serve ad estrarre altri attributi dei token da spaCy (https://spacy.io/api/token#attributes) — incluse, come vedremo le stopword.↩︎Esiste anche a una funzione interna (
tokenize()).↩︎Il file da scaricare e le istruzioni sono disponibili all’indirizzo https://www.cis.uni-muenchen.de/~schmid/tools/TreeTagger. L’installazione su Windows richiede Perl (ad esempio http://strawberryperl.com/) e un software per l’estrazione degli archivi .tar.gz (ad esempio https://7-zip.org/).↩︎
Vedi: paragrafo sulla codifica nel secondo capitolo.↩︎
Si veda Beri (2007), e anche https://www.agnesevardanega.eu/wiki/r/concetti_di_base/regex.↩︎
Vedi: https://www.agnesevardanega.eu/wiki/r/comandi/scrivere_le_funzioni.↩︎