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.
Tabella 8: Funzioni per il parsing dei documenti in frammenti (segmentazione)
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:

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.

Tabella 9: Funzioni per il parsing dei testi in parole, caratteri, n-grammi
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:

frase %>% 
  str_replace_all("[\'’]", "' ") %>% 
  tokenizers::tokenize_words()
[[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’”:

frase %>% 
  str_replace_all("[\'’](?!\\s)", "' ") %>% 
  tokenizers::tokenize_words()
[[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

library(quanteda)
quanteda_options(language_stemmer ="italian")
sepolcri %>% 
  corpus() %>% summary()
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, è:

sepolcri %>% 
  mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 
  corpus() %>% summary()
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()):

library(hunspell)
hunspell_parse(frase, dict = "it_IT")
[[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:

# segmentazione
sep.corpus.s <- corpus_reshape(sep.corpus, 
                               to = "sentences")

Confrontiamo i due corpora:

ndoc(sep.corpus)
[1] 1
ndoc(sep.corpus.s)
[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.

docnames(sep.corpus.s) %>% head()
[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:

docnames(art.corpus)
[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:

docvars(art.corpus)
  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
summary(art.corpus)
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():

summary(art.corpus) %>% 
  group_by(pattern) %>% 
  summarise(Tokens = sum(Tokens),
            Types = sum(Types)) 
# 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_subset(art.corpus, 
              pattern == "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_position per 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.

  1. 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_group(demo_int_corpus, groups = ID, 
             # conserva la punteggiatura, in questo caso
             fill = TRUE)
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

quanteda_options(language_stemmer ="italian") 

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:

# corpus
sep.corpus %>% 
  tokens()
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:

# corpus segmentato
sep.corpus.s %>% 
  tokens()
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(art.corpus, remove_punct = T) %>% 
  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(sep.corpus, remove_punct = T) %>% 
  tokens_ngrams(3)
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(sep.corpus, remove_punct = T) %>% 
  tokens_skipgrams(n = 2, skip = 0:1)
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):

tokens(sanremo) %>% 
  meta()
$fonte
[1] "angolotesti.it"
tokens(sanremo) %>%
  docnames()
[1] "01_Achille Lauro.txt" "02_Alberto Urso.txt" 
tokens(sanremo) %>%
  docvars()
  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"
docnames(toks) <- canzoni$doc_id
docvars(toks) <- canzoni[,-c(1,2)]
docnames(toks)
[1] "01_Achille Lauro.txt" "02_Alberto Urso.txt" 
docvars(toks)
  ID      cantante
1  1 Achille Lauro
2  2  Alberto Urso

Informazioni sui token

Con types() estraiamo un vettore con le occorrenze uniche dei token:

types(sep.toks) %>% 
  # visualizziamo solo i primi 20 elementi
  head(20)
 [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:

# tokens per ciascun documento
ntoken(sep.toks) %>% head(20)
 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 
# somma
ntoken(sep.toks) %>% sum()
[1] 1998
# types
ntype(sep.toks) %>% sum()
[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,
  ...
)
Tabella 10: Argomenti delle funzioni unnest_
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():

library(tidytext)
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.

head(tidy.sep)
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\"..."
str(tidy.sep)
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:

unnest_paragraphs(sepolcri, text, text)

oppure:

sepolcri %>% 
  unnest_paragraphs(text, text) %>% 
  summary()
    doc_id              text          
 Length:12          Length:12         
 Class :character   Class :character  
 Mode  :character   Mode  :character  
sanremo21 %>% 
  unnest_lines(text, text) %>% 
  summary()
    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.

sepolcri %>% 
  unnest_regex(text, text, 
               pattern ="[\\.\\!\\?;:]") %>% 
  summary()
    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:

tidy.art <- articolo %>% 
    unnest_regex(text, text, pattern = "##",
                 to_lower = FALSE)
tidy.art
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():

sepolcri %>%
  unnest_tokens(output = word, input = text)

Per ottenere come risultato un dataframe, o un dataframe tibble, possiamo modificare il dataframe di readtext:

as.data.frame(sepolcri) %>%
  unnest_tokens(word, text)

o

as_tibble(sepolcri) %>%
  unnest_tokens(word, text)

È 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.

tidy.sep.toks <- unnest_tokens(sepolcri, 
                               word, text)
head(tidy.sep.toks)
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:

sepolcri %>% 
  unnest_regex(word, text, pattern = "[\\s+^[:punct:]]")  %>% 
  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   "\"\"..."

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.

install.packages("spacyr")
library(spacyr)

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.

spacy_download_langmodel("it_core_news_sm")

Per usare le configurazioni del modello, scriveremo:

spacy_initialize(model = "it_core_news_sm")

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() 
spacy.sep.toks %>% 
  summary() %>% head()
               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
spacy.sep.toks[1]
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:

canzoni$text %>% 
  hunspell::hunspell_parse(dict = "it_IT") %>% str()
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):

docnames(toks) <- canzoni$doc_id
docvars(toks) <- canzoni[,-c(1,2)]

Con Tidytext

La funzione hunspell_parse() può essere applicata alla colonna contentente i testi di un dataframe, con mutate():

canzoni %>% 
  mutate(text = hunspell::hunspell_parse(text, dict = "it_IT")) %>% 
  head()
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:

length(unique(spacy.sep.pos$sentence_id))
[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.

spacy.sep.pos <- sepolcri %>% 
  corpus() %>% 
  corpus_reshape(to = "sentences") %>% 
  spacy_parse()
head(spacy.sep.pos)
          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       
spacy.sep.pos[20:29,]
           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:

as.tokens(spacy.sep.pos)
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:

spacy.sep.pos %>% 
  # senza punteggiatura
  filter(pos != "PUNCT" ) %>% 
  as.tokens() 

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:

# esportare i lemmi
spacy.sep.pos %>% 
  filter(pos != "PUNCT" ) %>% 
  as.tokens(use_lemma=T)  

Chiudiamo la sessione di spaCy, in questo modo:

spacy_finalize()
detach("package:spacyr")

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:

install.packages("koRpus")
install.packages("koRpus.lang.en")

Al momento, il modello per la lingua italiana può essere installato con il comando:

install.packages("koRpus.lang.it",
                 repo="https://undocumeantit.github.io/repos/l10n")
detach("package:quanteda")

Poi, richiamiamo direttamente la libreria per la lingua italiana:

library(koRpus.lang.it)

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:

head(tagged.sep@tokens)
    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():

taggedText(tagged.sep)

Le funzioni tokens() e types() estraggono i vettori carattere — appunto — dei token e dei type:

tokens(tagged.sep) %>% head()
[1] "all'"     "ombra"    "de"       "cipressi" "e"        "dentro"  
types(tagged.sep) %>% head()
[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:

length(unique(tagged.sep@tokens$sntc))
[1] 56
# disattiviamo i pacchetti
detach("package:koRpus.lang.it")
detach("package:koRpus")

Uso in Quanteda e Tidytext

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

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():

list(tagged.sep@tokens$token) %>% 
  tokens(remove_punct = T) %>% head()
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:

list(tagged.sep@tokens$lemma) %>% 
  tokens(remove_punct = T) %>% head()
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
detach("package:quanteda")

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:

install.packages("tm.plugin.koRpus")
library(koRpus.lang.it)
library(tm.plugin.koRpus)

In questo caso, impostiamo le opzioni per il parsing con la funzione di koRpus set.kRp.env():

set.kRp.env(lang="it",
            TT.cmd = "manual",
            TT.options=list(
              path="C:/TreeTagger", 
              preset="it"))

e poi usiamo la funzione readCorpus(), sempre indicando che i testi si trovano in un oggetto dello spazio di lavoro (format = "obj"):

tagged.sanremo21 <- sanremo21 %>% 
  readCorpus(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:

corpusTm(tagged.sanremo21)
<<VCorpus>>
Metadata:  corpus specific: 0, document level (indexed): 3
Content:  documents: 31

I metadati sono mantenuti, come parte del corpus di tm:

meta(corpusTm(tagged.sanremo21))
# 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.

head(taggedText(tagged.sanremo21))
  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
# disattiviamo i pacchetti
detach("package:koRpus.lang.it")
detach("package:tm.plugin.koRpus")
detach("package:koRpus")

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.

ÿtr
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
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:

mutate(text = str_replace_all(text, "[\'’](?!\\s)", "' ")) %>% 

come in

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

possiamo usare una funzione personale, che renderà più rapida la scrittura. Qui l’abbiamo chiamata “apostrofo”:

apostrofo <- function (x) {
  stringr::str_replace_all(x, "[\'’](?!\\s)", "' ")
}

Una volta eseguito il comando precedente, possiamo scrivere:

sepolcri %>% 
  mutate(text = apostrofo(text)) %>% 
  corpus()

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

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\"..."

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:

detach("package:quanteda")
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"))
names(sep.tm.corpus) %>% head()
[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.

inspect(sep.tm.corpus[[3]])
<<PlainTextDocument>>
Metadata:  7
Content:  chars: 23

Vero è ben, Pindemonte!
detach("package:tm")

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")

  1. 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”.↩︎

  2. Il risultato coincide con la segmentazione in frasi della funzione di tokenizer (tokenize_sentences).↩︎

  3. Si tratta di un oggetto di classe tokens, che vedremo in dettaglio nel § 4.4.4.↩︎

  4. Vedi https://www.agnesevardanega.eu/wiki/r/tidyverse/tidyr_unite_separate.↩︎

  5. 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.↩︎

  6. L’argomento additional_attributes() serve ad estrarre altri attributi dei token da spaCy (https://spacy.io/api/token#attributes) — incluse, come vedremo le stopword.↩︎

  7. Esiste anche a una funzione interna (tokenize()).↩︎

  8. 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/).↩︎

  9. Vedi: paragrafo sulla codifica nel secondo capitolo.↩︎

  10. Si veda Beri (2007), e anche https://www.agnesevardanega.eu/wiki/r/concetti_di_base/regex.↩︎

  11. Vedi: https://www.agnesevardanega.eu/wiki/r/comandi/scrivere_le_funzioni.↩︎