La famiglia apply in una frase
Ogni membro della famiglia apply fa lo stesso lavoro: prende una funzione, la esegue su ogni elemento di una struttura e raccoglie i risultati. Ciò che cambia è la forma dell'input e la forma dell'output. La mappa:
apply(m, MARGIN, FUN): righe o colonne di una matrice.lapply(x, FUN): ogni elemento di una lista o di un vettore; restituisce sempre una lista.sapply(x, FUN): comelapply, ma poi semplifica in un vettore o in una matrice quando può.vapply(x, FUN, FUN.VALUE):sapplycon un tipo di ritorno dichiarato e imposto.mapply(FUN, x, y, ...): percorre più input in parallelo, elemento per elemento.tapply(values, groups, FUN): applicaFUNdentro ogni gruppo, con un risultato per gruppo.
Accettano tutte le funzioni che scrivi tu, comprese quelle anonime, ed è da qui che la famiglia trae la sua forza.
apply(): righe e colonne di una matrice
apply prende una matrice, un MARGIN (1 per le righe, 2 per le colonne) e una funzione:
La matrice si riempie colonna per colonna, quindi le righe sono 1 3 5 e 2 4 6: le somme per riga vengono stampate come 9 12 e quelle per colonna come 3 7 11. Per i due casi più comuni R base offre helper dedicati e più veloci, rowSums, colSums, rowMeans, colMeans, quindi riserva apply alle funzioni che non ne hanno uno, come apply(m, 2, max).
Una trappola: apply su un data frame lo converte prima in silenzio in una matrice, forzando tutte le colonne a un unico tipo comune. Per i data frame, tratta le colonne come una lista e usa invece lapply/sapply.
lapply() restituisce sempre una lista, sapply() semplifica
lapply applica una funzione a ogni elemento e restituisce una lista della stessa lunghezza, qualunque cosa succeda:
Stesso calcolo, due forme: lapply ti restituisce una lista che contiene 85 e 80; sapply si accorge che ogni risultato ha lunghezza 1 e semplifica in un vettore numerico con nomi, molto più comodo da leggere e da usare in altri calcoli.
La comodità però ha un lato tagliente: il tipo di ritorno di sapply dipende dai dati. Se anche un solo elemento produce una lunghezza diversa, la semplificazione fallisce e ottieni di nuovo, in silenzio, una lista:
In console è una scrollata di spalle; dentro uno script significa codice che ha funzionato tutto l'anno e si rompe il giorno in cui i dati cambiano forma. È proprio per questo che esiste vapply.
vapply(): la sapply con controllo dei tipi
vapply aggiunge un terzo argomento, FUN.VALUE: un modello che dichiara come deve essere fatto un singolo risultato. integer(1) significa "ogni chiamata restituisce esattamente un intero":
Ottieni un vettore di interi con nomi, 5 6 6, e soprattutto una garanzia: se nchar restituisse due valori o un carattere, vapply si fermerebbe con un errore nel punto della chiamata invece di lasciare che un risultato malformato scivoli più avanti. La dichiarazione fa anche da documentazione:
vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'
Regola pratica: sapply in console, vapply nelle funzioni e negli script che devono girare senza supervisione.
mapply() e tapply(): input paralleli e gruppi
lapply percorre una sola struttura. Quando ogni chiamata ha bisogno di un elemento da più strutture nelle stesse posizioni, usa mapply; nota che la funzione viene per prima:
Ogni elemento dell'output accoppia i primi valori, poi i secondi, e così via: 10 200 3000 40000.
tapply è il membro che lavora per gruppi: divide values secondo groups e applica la funzione dentro ogni gruppo, restituendo un risultato per gruppo:
Il gruppo a ha media 30, il gruppo b ha media 40. È la risposta di R base a "media per categoria", la stessa domanda a cui group_by + summarize risponde per i data frame: se sei già nel mondo dplyr usa quello; tapply dà il meglio quando hai due vettori semplici e vuoi una riga sola.
Due comodità valgono per tutta la famiglia. Gli argomenti aggiuntivi dopo la funzione le vengono passati a ogni chiamata:
E le funzioni anonime entrano in gioco ogni volta che nessuna funzione già pronta va bene: sapply(x, \(v) max(v) - min(v)) calcola l'intervallo di ogni elemento senza dover prima dare un nome a un helper.
Famiglia apply contro cicli for
Sentirai dire che i cicli for in R sono lenti e che la famiglia apply è veloce. È in gran parte un mito. Ciò che è davvero lento è far crescere un risultato dentro un ciclo: result <- c(result, new_value) copia l'intero vettore a ogni passaggio. Un ciclo che prealloca il suo output va benissimo:
Quindi scegli in base alla leggibilità, non alle prestazioni. La versione apply dice cosa fare in una riga, "eleva al quadrato ogni elemento", e gestisce l'allocazione al posto tuo: per questo è l'idioma per il lavoro semplice elemento per elemento. Un ciclo for si guadagna il posto quando le iterazioni dipendono dai risultati precedenti, quando ti servono uscite anticipate con break, o quando il corpo è così lungo che una lambda complicherebbe le cose invece di aiutare. Entrambi sono R legittimo.
Cosa ti porti a casa
- Tutta la famiglia è un'unica idea: esegui una funzione su ogni elemento e raccogli i risultati.
applyserve per le righe (MARGIN = 1) e le colonne (MARGIN = 2) di una matrice; preferiscirowSums/colMeansquando esistono.lapplyrestituisce sempre una lista;sapplysemplifica quando può, il che significa che il suo tipo di ritorno può cambiare con i dati.vapplyblocca il tipo di ritorno; usala nel codice che non deve riservarti sorprese.mapplycombina più input insieme;tapplyaggrega i valori dentro i gruppi.- I cicli for non sono lenti di per sé: lo è far crescere i vettori. Scegli la forma che si legge meglio.
Prossimo passo: le pipe, l'operatore che concatena queste chiamate in pipeline leggibili, passo dopo passo.
Domande frequenti
Qual è la differenza tra lapply e sapply in R?
lapply restituisce sempre una lista, senza eccezioni. sapply esegue lo stesso calcolo e poi prova a semplificare il risultato: in un vettore se ogni elemento ha lunghezza 1, in una matrice se hanno tutti la stessa lunghezza, e di nuovo in una lista se non ci riesce. sapply è più comoda in console; lapply (o vapply) è più sicura negli script perché il suo tipo di ritorno non cambia mai.
Cosa fa apply() in R?
apply(m, MARGIN, FUN) esegue FUN su una matrice: MARGIN = 1 la applica a ogni riga, MARGIN = 2 a ogni colonna. apply(m, 1, sum) dà le somme per riga, apply(m, 2, mean) le medie per colonna. È pensata per matrici e array: per liste e vettori usa invece lapply/sapply.
La famiglia apply è più veloce di un ciclo for in R?
Di solito non di molto: è un mito. Un ciclo for scritto bene, con il vettore dei risultati preallocato, ha prestazioni paragonabili. I cicli che hanno fatto la cattiva fama del for fanno crescere il risultato un elemento alla volta, copiando tutto a ogni passaggio. Scegli le funzioni della famiglia apply per concisione e chiarezza, non per velocità.
A cosa serve vapply in R?
vapply è sapply con un contratto: dichiari il tipo e la lunghezza di ogni risultato, per esempio vapply(x, nchar, integer(1)). Se la funzione restituisce qualcos'altro, R solleva subito un errore invece di consegnarti in silenzio una struttura inattesa. Preferiscila nel codice che deve continuare a funzionare senza supervisione.