-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy path21-stringhe.Rmd
347 lines (230 loc) · 14.2 KB
/
21-stringhe.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# Stringhe {#stringhe}
<!--
FONTI:
- https://statisticsglobe.com/r-substr-substring
- The art of R Programming
- R4DS
- https://bookdown.org/rdpeng/rprogdatascience/regular-expressions.html
-->
Abbiamo visto che R oltre ai numeri è in grado di gestire anche i **caratteri**. Nonostante le operazioni matematiche non siano rilevanti per questo tipo di dato, lavorare con le stringhe è altrettanto se non più complesso in termini di programmazione, le stringe infatti rispetto ai numeri:
- possono essere maiuscole/minuscole. Ad esempio la stringa `ciao` è *concettualmente* uguale a `Ciao` ma R le tratta in modo diverso.
```{r}
"ciao" == "Ciao"
```
- possono avere caratteri speciali come `?\$` oppure appartenere ad alfabeti diversi `ة α`
- l'indicizzazione per `numeri` e `stringhe` lavora in modo diverso. Se abbiamo un **vettore** di strighe, questo viene rappresentato allo stesso modo di un vettore numerico. Tuttavia la stringa stessa `ciao` può essere scomposta, manipolata e quindi indicizzata nei singoli caratteri che la compongono `c, i, a, o`:
```{r}
vec_string <- c("ciao", "come", "stai")
vec_num <- c(1,2,3)
length(vec_string)
length(vec_num)
vec_string[1]
vec_num[1]
# Usiamo la funzione nchar()
setNames(nchar(vec_string), vec_string)
```
Per creare una stringa in R dobbiamo usare le singole o doppie virgolette `"stringa"` o `'stringa'`. Queste due scritture in R sono interpretate in modo analogo. Possiamo usarle entrambe per scrivere una stringa che contenga le virgolette:
```{r error=TRUE}
x <- "stringa con all'interno un'altra 'stringa'"
x
x <- "stringa con all'interno un'altra "stringa""
# in questo caso abbaimo errore perchè non interpreta la doppia virgolettatura
```
All'interno delle stringhe possiamo utilizzare caratteri speciali come `/|\$%&`. Alcuni di questi vengono intepretati da R in modo particolare. Quando accade è necessario aggiungere il carattere `\` che funge da *escape*, ovvero dice ad R di trattare letterlamente quel carattere:
```{r}
x <- "ciao come stai? n io tutto bene"
cat(x)
```
Per questo in R ci sono una serie di funzioni e pacchetti che permettono di lavorare con le stringhe in modo molto efficiente. Vedremo qui una breve panoramica di queste funzioni con qualche suggerimento anche su come approfondire.
## Confrontare stringhe
Il primo aspetto è quello di confrontare stringhe. Il confronto logico tra `stringhe` è molto più stringente di quello numerico. Come abbiamo visto prima infatti, c'è molta più libertà rispetto alle stringhe, con il prezzo di avere più scenari da gestire.
```{r}
# Confronto due numeri rappresentati in modo diverso
intero <- as.integer(10)
double <- as.numeric(10)
intero == double
# Confronto stringhe
"ciao" == "Ciao"
"female" == "feMale"
```
Anche il concetto di spazio ` ` è rilevante perchè viene considerato come un carattere:
```{r}
"ciao " == "ciao"
```
Immaginate di avere un vettore dove una colonna rappresenta il genere dei partecipanti. Se questo vettore è il risultato di persone che liberamente scrivono nel campo di testo, potreste trovarvi una situazione così (è per questo che nei form online spesso ci sono opzioni predefinite piuttosto che testo libero):
```{r}
genere <- c("maLe", "masChio", "Male", "f", "female", "malew")
```
In questo vettore (volutamente esagerato) abbiamo chiaro il significato di `f` o di `malew` (probabilmente un errore di battitura) tuttavia se vogliamo lavorarci con R, diventa probelmatico:
```{r}
# Tabella di frequenza
table(genere)
# Non molto utile
```
## Comporre stringhe
Vediamo quindi alcune funzioni utili per lavorare con le stringhe.
### `tolower()` e `toupper()`
Queste funzioni sono estremamente utili perchè permettono di forzare il carattere maiscolo o minuscolo
```{r}
tolower(genere)
toupper(genere)
```
### `paste()` e `paste0()`
Queste funzioni servono a combinare diverse informazioni in una stringa. Possiamo combinare diverse stringhe ma anche numeri. Come tipico in R, `paste()` e `paste0()` sono vettorizzate e quindi possono essere utili per combinare due vettori di informazioni. La differenza è che `paste()` automaticamente aggiunge uno spazio tra le stringhe combinate mentre con `paste0()` deve essere messo esplicitamente.
```{r}
age <- c(10, 20, 35, 15, 18)
nomi <- c("Andrea", "Francesco", "Fabio", "Anna", "Alice")
paste(nomi, "ha", age, "anni")
paste0(nomi, "ha", age, "anni")
paste0(nomi, " ha ", age, " anni")
```
In questo caso nonostante `age` sia numerico, viene forzato a stringa per poter essere combinato con i nomi.
### `sprinf()`
`sprinf()` è simile a `paste*()` come funzionamento ma permette di comporre strighe usando dei *placeholder* e fornendo poi il contenuto.
```{r}
sprintf("%s ha %d anni", nomi, age)
```
In questo caso si compone una stringa usando `%` e una lettera che rappresenta il tipo di dato da inserire. Poi in ordine vengono fornite le informazioni. In questo caso prima `%s`(stringa) quindi `nomi` e poi `%d`(digits) quindi `age`. Con `?sprintf` avete una panoramica del tipo di *placeholder* che potete utilizzare.
## Indicizzare stringhe
### `nchar()`
Come abbiamo visto prima la stringa è formata da un insieme di caratteri. La funzione `nchar()` fornisce il numero di singoli caratteri che compongono una stringa.
```{r}
nchar("ciao")
nchar("Wow lavorare con le stringhe è molto divertente")
```
### `gregexpr()` e `regexpr()`
Per trovare la posizione di una o più caratteri all'interno di una stringa possiamo usare `gregexpr()`. La scrittura è `(g)gregexpr(pattern, stringa)`:
```{r}
gregexpr("t", "gatto")
regexpr("t", "gatto")
```
La differenza è che `regexpr()` restituisce solo la prima corrispondenza, nel nostro esempio la prima `t` si trova in 3 posizione mentre `gregexpr()` resituisce tutte le corrispondenze.
### `substr()` e `substring()`
Il processo inverso, quindi trovare la stringa che corrisponde ad un certo indice è il lavoro di `substr(stringa, start, stop)` dove `start` e `stop` sono gli indici della porzione di stringa che vogliamo trovare. `substring()` funziona allo stesso modo ma `start` e `stop` vengono chiamati `first` e `last`.
```{r}
substr("gatto", 1, 1) # solo la prima
substr("gatto", 2, 4) # dalla seconda alla quarta
```
Per questo tipo di compiti forniscono esattamente lo stesso risultato, vediamo quindi le differenze:
- `substring()` permette di fornire solo l'indice iniziale `first` e quello finale ha un valore di default di `1000000L`
- `substring()` permette anche di fornire un vettore di indici di inizio/fine per poter segmentare la stringa
```{r, error=TRUE}
substring("gatto", 1) # funziona
substr("gatto", 1) # errore
substring("gatto", 1, 1:5) # indice multiplo di fine
substring("gatto", 1:5, 1:5) # indice multiplo di inizio e fine
substr("gatto", 1, 1:5) # non funziona, viene usato solo 1 indice di fine
```
### `startWith()` e `endsWith()`
Alcune volte possiamo essere interessati all'inizio o alla fine di una stringa. Ad esempio `female` e `male` hanno una chiara differenza iniziale (`fe` e `ma`). E nonostante errori di battitura seguenti o altre differenze, selezionare solo l'inizio o la fine può essere efficiente. `startWith()` e `endsWith()` permettono rispettivamente di fornire `TRUE` o `FALSE` se una certa stringa o vettore di stringhe abbiamo un certo pattern iniziale o finale.
```{r}
startsWith("female", prefix = "fe")
endsWith("female", suffix = "ale")
```
Questa come le altre funzioni possono essere utilizzate in combinazione con `tolower()` o `toupper()` per ingnorare differenze non rilevanti.
### `grep()` e `grepl()`
Queste funzioni lavorano invece su **vettori** di stringhe trovando la posizione o la sola presenza di specifici *pattern*. `grep()` fornisce la posizione/i nel vettore dove è presente un match, mentre `grepl()` fornisce `TRUE` o `FALSE` in funzione della presenza del *pattern*. La scrittura è la stessa `grep*(pattern, vettore)`
```{r}
genere
grep("female", genere) # indice di posizione
grepl("female", genere) # true o false
```
Come abbiamo visto nell'indicizzazione logica dei vettori, possiamo usare sia `grep()` che `grepl()` per selezionare solo alcuni elementi:
```{r}
index_grep <- grep("female", genere) # indice di posizione
index_grepl <- grepl("female", genere) # indice di posizione
genere[index_grep]
genere[index_grepl]
```
Da notare ancora come tutte queste funzioni lavorino su una **corrispondenza molto stringente** (in termini di maiscolo, minuscolo, etc.) tra pattern e target.
## Manipolare stringhe
Molte delle funzioni che abbiamo visto permettono anche di sostituire un certo pattern all'interno di una stringa o di un vettore di stringhe.
Utilizzando infatti `substr()` o `substring()` con la funzione di assegnazione `<-` possiamo sostituire un certo carattere. Importante, la sostituzione deve avere lo stesso numero di caratteri della selezione `start:stop` oppure verrà usato solo il numero di caratteri corrispondente:
```{r}
x <- "gatto"
substr(x, 1, 1) <- "y"
x
x <- "gatto"
substr(x, 1, 1) <- "aeiou"
x # viene usata solo la a
# substring funziona esattamente allo stesso modo
x <- "gatto"
substring(x, 1, 1) <- "z"
x
```
Possono essere utilizzate anche in modo vettorizzato funzionando quindi su una serie di elementi:
```{r}
x <- c("cane", "gatto", "topo")
substring(x, 1, 1) <- "z"
x
```
### `gsub()` e `sub()`
Rispetto a `substring()`, `gsub()` e `sub()` permettono di sostituire un certo pattern e non usando indici di posizione. La scrittura è `*sub(pattern, replacement, target)`:
```{r}
x <- c("cane", "gatto", "topo")
sub("a", "z", x)
```
Come vedete per ogni elemento di `x` la funzione ha trovato il pattern `"a"` e lo ha sostituito con `"z"`.
La principale limitazione di `sub()` è quella di sostituire solo la prima corrispondenza trovata in ogni stringa.
```{r}
x <- c("cane", "gatto", "topo")
sub("o", "z", x)
```
Come vedete infatti, solo la prima "o" nella parola "topo" è stata sostituita. `gsub()` permette invece di sostituire tutti i caratteri che corrispondono al pattern richiesto:
```{r}
x <- c("cane", "gatto", "topo")
gsub("o", "z", x)
```
### `strsplit()`
Abbiamo già visto che con `substring()` ad esempio possiamo dividere una stringa in più parti. Secondo la documentazione di R la funzione `strsplit()` è più adatta ed efficiente per questo tipo di compito. La scrittura è `strsplit(target, split)` dove `split` è il carattere in base a cui dividere:
```{r}
frase <- "Quanto è bello usare le stringhe in R"
strsplit(frase, " ") # stiamo dividendo in base agli spazi
parola <- "parola1_parola2"
strsplit(parola, "_") # stiamo dividendo in base all'underscore
parola <- "ciao"
strsplit(parola, "") # dividiamo per ogni carattere
```
Quello che otteniamo è un vettore (all'interno di una lista, possiamo usare `unlist()`) che contiene il risultato dello splitting.
## Regular Expression (REGEX)
E' tutto così semplice con le stringhe? Assolutamente no! Fino ad ora abbiamo utilizzato dei semplici pattern come singoli caratteri o insieme di caratteri tuttavia possiamo avere problemi più complessi da affrontare come:
- trovare l'estensione di un insieme di file
- trovare il dominio di un sito web
Facciamo un esempio:
```{r}
files <- c(
"file1.txt",
"file2.docx",
"file3.doc",
"file4.sh"
)
```
In questo caso se noi vogliamo ad esempio estrarre tutte le estensioni `nomefile.estensione` gli strumenti che abbiamo visto finora non sono sufficienti:
- possiamo estrarre i caratteri dalla fine `substr()` contando con `nchar()` però le estensioni non hanno un numero fisso di caratteri
- possiamo cercare tutti i pattern con `grepl()` ma ci sono migliaia di estensioni diverse
Finora abbiamo visto 2 livelli di astrazione:
1. Corrispondenza letterale: `stringa1 == stringa2`
2. Indicizzazione: la posizione all'interno di una stringa
Il terzo livello di astrazione è quello di trovare dei pattern comuni nelle stringhe ed estrarli, indipendentemente dai singoli caratteri, dal numero o dalla posizione.
Le Regular Expressions (REGEX) sono un insieme di caratteri (chiamati **metacaratteri**) che vengono intepretati e permettono di trovare dei pattern nelle stringhe senza indicare un pattern specifico o un indice di posizione. L'argomento è molto complesso e non R-specifico. Ci sono parecchie guide online e tutorial che segnaliamo alla fine del capitolo. La cosa importante da sapere è che la maggior parte delle funzioni che abbiamo visto permettono di usare una `regex` oltre ad un pattern specifico in modo da risolvere problemi più complessi.
Per fare un esempio se vogliamo estrarre l'estensione da una lista di file il ragionamento è:
- dobbiamo trovare un `.` perchè (circa) tutti i file sono composti da `nomefile.estensione`
- dobbiamo selezionare tutti i caratteri dal punto alla fine della stringa
La "traduzione" in termini di REGEX è questa `"\\.([^.]+)$"` e quindi possiamo usare questo come *pattern* e quindi estrarre le informazioni che ci servono. Possiamo usare la funzione `regmatches(text, match)` che richiede la stringa da analizzare e un oggetto `match` che è il risultato della funzione `regexpr` che abbiamo già visto:
```{r}
match_regex <- regexpr("\\.([^.]+)$", files)
regmatches(files, match_regex)
```
## Per approfondire
In tutto questo libro abbiamo sempre cercato di affrontare R come linguaggio di programmazione concentrandosi sulle funzioni di base. Tuttavia in alcuni settori, come quello delle stringhe e delle REGEX ci sono dei pacchetti esterni altamente consigliati che non solo rendono più semplice ma anche più organizzato e consistente l'insieme di funzioni. Il pacchetto [`stringr`](https://stringr.tidyverse.org/index.html) è una fantastica risorsa per imparare ma anche lavorare in modo più efficiace con le stringhe. Contiene una serie di funzioni costruite al di sopra di quelle che abbiamo affrontato, semplificandole e uniformando il tutto.
L'ultimo esempio descritto non è molto leggibile contenendo il risultato di un'altra funzione e chiamando l'oggetto `target` due volte, in `stringr` abbiamo la funzione `str_extract()` che estrae un certo pattern o REGEX:
```{r}
stringr::str_extract(files, "\\.([^.]+)$")
```
### Risorse utili
- [`stringr`](https://stringr.tidyverse.org/index.html)
- [Capitolo 14](https://r4ds.had.co.nz/strings.html) di R for Data Science
- [Mastering Regular Expressions](http://regex.info/book.html)
- [Handling Strings With R](https://www.gastonsanchez.com/r4strings/)
### Altre funzioni utili
- `abbreviate()`