Track
/
R
R
/
Esercizi
/
Club del formaggio
Club del formaggio

Club del formaggio

Esercizio di apprendimento

Introduzione

Che cos'è la programmazione funzionale?

Per i nostri scopi, ecco alcune caratteristiche fondamentali:

  • Funzioni pure, che mappano gli input agli output in modo coerente e senza effetti collaterali.
  • Dati immutabili.
  • Funzioni di prima classe, che possono essere trattate come valori al pari di qualsiasi altro tipo.
  • Funzioni di ordine superiore, che prendono altre funzioni come argomenti o come valori restituiti.

Tuttavia, R non è un linguaggio funzionale puro (a differenza, per esempio, di Haskell).

Considera R un linguaggio pragmatico, che (sempre più spesso) offre strumenti funzionali là dove questi rappresentano un modo pulito e semplice per portare a termine un lavoro. In genere l'obiettivo è l'analisi dei dati, e la programmazione è solo un mezzo per raggiungerlo.

Le pipe

L'uso delle pipe |> per collegare le funzioni è una tecnica sempre più comune in diversi linguaggi.

In breve:

  • Un risultato a sinistra della pipe diventa il primo argomento della funzione a destra (dove «destra» ignora le interruzioni di riga facoltative).
  • Gli eventuali argomenti rimanenti possono essere inclusi nella chiamata alla funzione.
  • In R, dobbiamo usare la pipe verso una chiamata a funzione, non solo verso un nome, quindi le parentesi sono sempre necessarie (a differenza di alcuni altri linguaggi).

Alcuni programmatori trovano utile leggere la pipe come «poi» quando rileggono il codice: fai il passo 1 POI fai il passo 2...

L'esempio qui sotto illustra la sintassi.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"

Questo tipo di pipe è stato aggiunto al Base R nella versione 4.1.0, e potresti vederla chiamata «native pipe».

L'editor RStudio (tra gli altri) usa Ctrl-Shift-M come scorciatoia da tastiera per inserire una pipe con gli spazi adatti.

Le funzioni apply

Abbiamo visto in precedenza che molte funzioni in R sono «vettorizzate»: operano su interi vettori, senza bisogno di scrivere cicli espliciti.

Le funzioni che usano solo funzioni vettorizzate sono probabilmente anch'esse vettorizzate automaticamente. In un esempio banale:

triple <- function(x) return(3 * x)
triple(1:10)
 [1]  3  6  9 12 15 18 21 24 27 30

Tuttavia, funzioni più complesse potrebbero non accettare input vettoriali, per esempio se contengono cicli o blocchi if-else.

Ci serve un modo più generale per applicare le funzioni ai vettori (simile a map() in alcuni altri linguaggi).

Poiché R ha una grande varietà di strutture dati nel linguaggio di base, ha anche un'intera famiglia di funzioni *apply() per operare su di esse. Più tecnicamente, i programmatori le chiamano spesso «funzioni di ordine superiore» e i matematici «funzionali», perché prendono funzioni come argomenti.

Per ora, consideriamo lapply() e sapply().

lapply()

«List-apply» è progettata per lavorare su input di tipo lista, ma i vettori verranno silenziosamente convertiti in liste quando necessario.

L'output è una lista, quindi potrebbe servire aprirla per ottenere un vettore:

h <- function(size) {
    switch(size,
           "small" = 6,
           "medium" = 8,
           "large" = 10
    )
}

v <- c("small", "large", "medium")

h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) : 
#>   EXPR must be a length 1 vector

lapply(v, h)  # output is a list
#> [[1]]
#> [1] 6

#> [[2]]
#> [1] 10

#> [[3]]
#> [1] 8

lapply(v, h) |> unlist() # output is a vector
#> [1]  6 10  8

Chi ha già familiarità con il concetto Switch noterà che potremmo in alternativa sostituire lo switch scalare con la versione vettorizzata di recode_values() della libreria dplyr. La vettorizzazione è una buona cosa, e il R moderno offre diversi modi per ottenerla.

library(dplyr)

h <- function(size) {
  recode_values(size,
         "small" ~ 6,
         "medium" ~ 8,
         "large" ~ 10
  )
}

v <- c("small", "large", "medium")
h(v)
#> [1]  6 10  8

sapply()

«Simplified-apply» fa qualcosa di simile a lapply, ma cerca anche di semplificare il valore restituito: per esempio un vettore con nomi invece di una lista. A seconda del contesto, potrebbe essere necessario usare unname():

sapply(v, h)
 small  large medium 
     6     10      8 

sapply(v, h) |> unname()
#> [1]  6 10  8

La libreria purrr

lapply e sapply sono semplici, familiari alla maggior parte degli utenti di R, e ancora piuttosto popolari per vettori e liste semplici.

Il Base R ha anche molte altre funzioni apply. Offrire un'alternativa più semplice e più coerente a questo tipo di confusione è uno degli obiettivi principali del Tidyverse.

Ora c'è il pacchetto purrr, che offre diverse opzioni aggiuntive come map(), migliorando al tempo stesso la coerenza del design e la chiarezza dei messaggi di errore.

Meno vincolato del Base R dalla retrocompatibilità, questo pacchetto avvicina R in modo significativo alla programmazione funzionale moderna che si trova in altri linguaggi.

In purrr ci sono funzioni molteplici e diverse, difficili da riassumere in poche parole. Tra le risorse utili per orientarsi nella libreria ci sono:

Le numerose funzioni map

La funzione map() applica una funzione a ogni elemento dell'input e restituisce sempre una lista.

Fin qui sembra lapply(), solo ribattezzata, e magari con messaggi di errore migliori.

Tuttavia, map() è solo un membro di una grande famiglia.

Alcune domande da porsi quando si sceglie una funzione:

  • Cosa vuoi restituire? map() restituisce sempre una lista, ma se vuoi un tipo specifico di vettore atomico usa map_chr() per le stringhe, map_int() per gli interi, ecc.
  • Ti serve l'indice di posizione di ogni elemento dell'input? Anteponi i ai nomi delle funzioni, come in imap().
  • Vuoi elaborare più input in parallelo?
    • Sostituisci map con map2 nel nome, come in map2_dbl() per due input (ad esempio valori e pesi, per calcolare una media ponderata)
    • Sostituisci con pmap per una lista di input paralleli, come in pmap_chr(). E sì, il parallelismo a livello hardware è possibile.
  • Vuoi elaborare tutti gli elementi dell'input? In caso contrario, pensa a map_if() per applicare un filtro tramite predicato (booleano), a map_at() per specificare criteri sugli indici di posizione, a head_while() per elaborare l'input finché un elemento non fallisce un predicato.
  • I dati di input sono profondamente annidati? Esistono delle funzioni per questa situazione.
  • Vuoi che il valore restituito sia sempre dello stesso tipo dell'input? Usa le funzioni modify() invece di map, oppure modify_tree() per l'applicazione ricorsiva.

Queste funzioni hanno i dati di input come primo argomento quando possibile, così da funzionare bene con le pipe.

Inoltre, purrr è (volutamente) più pignolo del Base R. Se chiedi qualcosa di specifico, le funzioni restituiranno esattamente quello, oppure si fermeranno con un messaggio di errore. Severo, ma molto efficace nel ridurre bug difficili da individuare.

Le funzioni di riduzione

Le funzioni map in genere restituiscono un valore della stessa lunghezza dell'input.

Altre funzioni invece riducono la dimensionalità dei dati, per esempio sum().

sum(1:100)
#> [1] 5050

Nel caso precedente, convertiamo un vettore di lunghezza 100 in un intero di lunghezza 1. Vedremo in un prossimo concetto su matrici e array che il concetto di riduzione di dimensionalità è più generale.

La funzione sum() è integrata (come molte altre funzioni statistiche). Tuttavia, ci serve un modo per applicare funzioni arbitrarie di riduzione della dimensionalità su una struttura dati, usando una funzione di ordine superiore equivalente a map().

In diversi altri linguaggi si usano nomi di funzione come fold, foldl e foldr. In R la funzione in questione è reduce(), che corrisponde al framework MapReduce, ben noto (agli appassionati di algoritmi).

Il primo argomento è, come al solito, i dati di input. Il secondo argomento è una funzione a 2 argomenti: spesso una funzione anonima, ma talvolta semplice come un operatore aritmetico, per esempio +.

# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050  # same as sum(1:100)

# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7

Fa differenza se applichiamo la funzione dall'inizio o dalla fine dell'input?

Non con operatori associativi come + o *. Chiaramente, (1 + 2) + 3 == 1 + (2 + 3).

Al contrario, - e / non sono associativi:

(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2

Per le funzioni in cui la direzione conta, esiste un argomento facoltativo .dir.

# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5

Come per map() e map2(), esiste anche una funzione reduce2() per operare in parallelo su due vettori di input.

In R ci piace lavorare con i vettori. Supponi di voler vedere tutti i passaggi intermedi invece di un unico valore finale.

Per questo, le funzioni corrispondenti sono accumulate() e accumulate2(). La sintassi è simile a reduce(), con alcuni argomenti facoltativi aggiuntivi per specificare il tipo di output.

accumulate(1:10, `+`)
#> [1]  1  3  6 10 15 21 28 36 45 55

Alcuni tipi comuni di accumulo hanno funzioni dedicate. Il concetto Vector Functions ha trattato cumsum() e cumprod() nel Base R, e la libreria dplyr ha cummean() per le medie numeriche, cumall() e cumany() per i vettori logici.

Le funzioni di filtro

Spesso vogliamo includere (o escludere) gli elementi dell'input che soddisfano un certo predicato.

Nel concetto Vector Filtering abbiamo visto un modo idiomatico per farlo.

v <- 1:3
v[v >= 2]
#> [1] 2 3

È molto utilizzato nel R tradizionale, ma si integra male nelle moderne pipeline funzionali.

Di conseguenza, purrr fornisce un insieme di funzioni di predicato, che seguono la solita convenzione sugli argomenti f(input, func, ...) per funzionare con le pipe.

I nomi sono un po' diversi da quelli della maggior parte degli altri linguaggi (nomi come filter e drop hanno già un significato del tutto diverso in R).

Le funzioni purrr corrispondenti includono keep() e discard(), più diverse altre.

Abbiamo anche every(), some() e none(), che sostituiscono all() e any() nel restituire un singolo valore logico.

Istruzioni

Stiamo avviando un club del formaggio, che userà il machine learning per selezionare nuovi formaggi da offrire ai nostri clienti amanti del formaggio, in base al loro storico e ai loro gusti.

I nuovi membri devono compilare un sondaggio iniziale, così possiamo raccogliere alcuni dati di base per iniziare. Da questo è emerso che esiste un sottoinsieme di clienti enfatici che mancano di sfumature nelle loro critiche. Poiché questo può finire per distorcere irreversibilmente un algoritmo più raffinato, è stato creato un algoritmo separato per gestire le loro esigenze. Ti viene chiesto di fornire alcune funzioni ausiliarie per gestire i loro dati.

Note

Sebbene ci siano diversi modi per risolvere i compiti seguenti, ognuno può essere risolto con non più di due funzioni di ordine superiore.

1. Classifica i clienti

Il sistema di valutazione si basa su cinque stelle e consiste semplicemente in numeri interi 1:5. I clienti enfatici danno solo valutazioni di 1 o 5, e vogliamo sapere se un cliente mostra questo comportamento.

Implementa all_15() che prende un vettore di valutazioni e restituisce true se tutte le valutazioni sono 1 o 5, e false altrimenti.

all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE

all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE

2. Assegna un nome ai clienti

Dobbiamo associare il nome del cliente (o l'ID) alle sue valutazioni.

Implementa name_customers(), che prende un vettore di nomi e un array di valutazioni, e li «zippa» in un array di array.

Gli elementi interni dell'array devono avere i nomi name e rating.

names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"

#> [[1]]$rating
#> [1] 2 3 5

#> [[2]]
#> [[2]]$name
#> [1] "c2"

#> [[2]]$rating
#> [1] 1 1 5

3. Separa i clienti enfatici

Dobbiamo separare i clienti più enfatici dagli altri.

Implementa emphatics() che prende clienti e valutazioni. Restituisce un array limitato a coloro che usano solo valutazioni di 1 o 5 stelle.`

# names and ratings as in the task 2 example

emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"

#> [[1]]$rating
#> [1] 1 1 5

4. Converti le valutazioni in binarie

Poiché i clienti enfatici usano solo valutazioni 1 e 5, sarà più conveniente dal punto di vista computazionale cambiarle in 0 e 1.

Implementa to_binary() che prende un vettore di valutazioni enfatiche. Restituisce valutazioni binarie, in cui 1 è stato cambiato in 0 e 5 è stato cambiato in 1.

to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0

5. Calcola le valutazioni medie nel tempo

Vuoi continuare a migliorare le raccomandazioni per tutti i singoli clienti (non solo gli enfatici) nel tempo, man mano che comprendi meglio i loro gusti.

Implementa satisfactions() che prende un vettore di valutazioni. Restituisce un vettore di valutazioni medie cumulative fino a questo punto. Arrotonda i risultati a 2 cifre decimali.

Speriamo che i numeri continuino a crescere, per clienti felici!

satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
R Exercism

Vuoi iniziare Club del formaggio?

Iscriviti a Exercism per imparare e padroneggiare R con 21 concetti111 esercizi e il mentoring di persone reali, tutto gratis.