Menu

apply, lapply, sapply w R: rodzina funkcji apply

Rodzina apply, czyli apply, lapply, sapply, vapply, mapply i tapply, uruchamia funkcję na każdym elemencie danych. Zobacz, co robi każda z nich i kiedy po nią sięgnąć.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Rodzina apply w jednym zdaniu

Każda funkcja z rodziny apply robi to samo: bierze funkcję, uruchamia ją na każdym elemencie jakiejś struktury i zbiera wyniki. Różnią się kształtem wejścia i kształtem wyjścia. Mapa:

  • apply(m, MARGIN, FUN): wiersze lub kolumny macierzy.
  • lapply(x, FUN): każdy element listy lub wektora; zawsze zwraca listę.
  • sapply(x, FUN): to samo co lapply, a potem upraszcza wynik do wektora lub macierzy, gdy się da.
  • vapply(x, FUN, FUN.VALUE): sapply z zadeklarowanym i wymuszanym typem wyniku.
  • mapply(FUN, x, y, ...): przechodzi równolegle po kilku wejściach, element po elemencie.
  • tapply(values, groups, FUN): stosuje FUN w każdej grupie osobno, jeden wynik na grupę.

Wszystkie przyjmują własne funkcje, także anonimowe, i właśnie stąd bierze się siła tej rodziny.

apply(): wiersze i kolumny macierzy

apply przyjmuje macierz, MARGIN (1 dla wierszy, 2 dla kolumn) i funkcję:

Macierz wypełnia się kolumna po kolumnie, więc wiersze to 1 3 5 oraz 2 4 6: sumy wierszy wypisują się jako 9 12, a sumy kolumn jako 3 7 11. Dla dwóch najczęstszych przypadków bazowy R ma osobne, szybsze funkcje (rowSums, colSums, rowMeans, colMeans), więc apply zostaw dla funkcji, które takich odpowiedników nie mają, np. apply(m, 2, max).

Jedna pułapka: apply na ramce danych po cichu zamienia ją najpierw w macierz, co sprowadza wszystkie kolumny do jednego wspólnego typu. Dla ramek danych traktuj kolumny jak listę i używaj lapply/sapply.

lapply() zawsze daje listę, sapply() upraszcza

lapply stosuje funkcję do każdego elementu i zwraca listę tej samej długości, niezależnie od wszystkiego:

Te same obliczenia, dwa kształty: lapply oddaje listę z wartościami 85 i 80; sapply zauważa, że każdy wynik ma długość 1, i upraszcza go do nazwanego wektora liczbowego, znacznie wygodniejszego do czytania i do dalszych obliczeń.

Ta wygoda ma jednak ostrą krawędź: typ wyniku sapply zależy od danych. Jeśli choć jeden element da inną długość, uproszczenie się nie uda i po cichu znów dostaniesz listę:

W konsoli wzruszysz ramionami; w skrypcie oznacza to kod, który działał cały rok i psuje się w dniu, w którym dane zmienią kształt. Właśnie dlatego istnieje vapply.

vapply(): sapply z kontrolą typu

vapply dodaje trzeci argument, FUN.VALUE: szablon, który określa, jak ma wyglądać jeden wynik. integer(1) oznacza "każde wywołanie zwraca dokładnie jedną liczbę całkowitą":

Dostajesz nazwany wektor liczb całkowitych (5 6 6) i, co ważniejsze, gwarancję: gdyby nchar kiedyś zwróciła dwie wartości albo tekst, vapply zatrzyma się z błędem w miejscu wywołania, zamiast przepuścić dalej wynik o złym kształcie. Deklaracja służy jednocześnie jako dokumentacja:

vapply(words, nchar, character(1))
# Error in vapply(words, nchar, character(1)) : values must be type 'character'

Zasada: sapply w konsoli, vapply w funkcjach i skryptach, które mają działać bez nadzoru.

mapply() i tapply(): równoległe wejścia i grupy

lapply przechodzi po jednej strukturze. Gdy każde wywołanie potrzebuje elementu z kilku struktur na tych samych pozycjach, użyj mapply. Zwróć uwagę, że funkcja jest pierwszym argumentem:

Każdy element wyniku łączy pierwsze wartości, potem drugie i tak dalej: 10 200 3000 40000.

tapply działa na grupach: dzieli values według groups i stosuje funkcję w każdej grupie, zwracając jeden wynik na grupę:

Średnia grupy a to 30, grupy b to 40. To odpowiedź bazowego R na "średnia dla każdej kategorii", czyli to samo pytanie, na które dla ramek danych odpowiada group_by + summarize. Jeśli już pracujesz z dplyr, użyj tego; tapply sprawdza się, gdy masz dwa zwykłe wektory i chcesz jednej linijki.

Dwie wygody działają w całej rodzinie. Dodatkowe argumenty po funkcji są przekazywane do niej przy każdym wywołaniu:

A funkcje anonimowe wstawisz wszędzie tam, gdzie nie pasuje żadna gotowa funkcja: sapply(x, \(v) max(v) - min(v)) oblicza rozstęp dla każdego elementu bez wcześniejszego nazywania funkcji pomocniczej.

Rodzina apply a pętle for

Usłyszysz, że pętle for są w R wolne, a rodzina apply szybka. To w dużej mierze mit. Naprawdę wolne jest powiększanie wyniku wewnątrz pętli: result <- c(result, new_value) kopiuje cały wektor w każdym przebiegu. Pętla, która z góry alokuje wynik, działa bez zarzutu:

Wybieraj więc według czytelności, nie wydajności. Wersja z apply mówi co w jednej linijce ("podnieś każdy element do kwadratu") i sama zajmuje się alokacją, dlatego to idiom do prostych operacji na elementach. Pętla for jest na miejscu, gdy iteracje zależą od poprzednich wyników, gdy potrzebujesz wcześniejszego wyjścia przez break albo gdy treść jest na tyle długa, że lambda bardziej by przeszkadzała, niż pomagała. Oba podejścia to pełnoprawny R.

Co warto zapamiętać

  • Cała rodzina to jeden pomysł: uruchom funkcję na każdym elemencie i zbierz wyniki.
  • apply służy do wierszy (MARGIN = 1) i kolumn (MARGIN = 2) macierzy; gdy istnieją rowSums/colMeans, wybierz je.
  • lapply zawsze zwraca listę; sapply upraszcza, gdy może, więc typ wyniku może się zmieniać razem z danymi.
  • vapply blokuje typ wyniku; używaj jej w kodzie, który nie może cię zaskoczyć.
  • mapply łączy kilka wejść naraz; tapply agreguje wartości w grupach.
  • Pętle for nie są z natury wolne, wolne jest powiększanie wektorów. Wybierz zapis, który czyta się najlepiej.

Dalej: potoki, czyli operator, który łączy te wywołania w czytelne kroki.

Najczęściej zadawane pytania

Czym różni się lapply od sapply w R?

lapply zawsze zwraca listę, bez wyjątków. sapply wykonuje te same obliczenia, a potem próbuje uprościć wynik: do wektora, jeśli każdy element ma długość 1, do macierzy, jeśli wszystkie mają tę samą długość, i z powrotem do listy, jeśli się nie da. sapply jest wygodniejsze w konsoli; lapply (lub vapply) jest bezpieczniejsze w skryptach, bo typ zwracanej wartości nigdy się nie zmienia.

Co robi apply() w R?

apply(m, MARGIN, FUN) uruchamia FUN na macierzy: MARGIN = 1 stosuje ją do każdego wiersza, MARGIN = 2 do każdej kolumny. apply(m, 1, sum) daje sumy wierszy, apply(m, 2, mean) średnie kolumn. To funkcja dla macierzy i tablic; dla list i wektorów używaj lapply/sapply.

Czy rodzina apply jest szybsza od pętli for w R?

Zwykle niewiele, to mit. Dobrze napisana pętla for z wcześniej zaalokowanym wektorem wyniku działa porównywalnie. Złą sławę pętlom przyniosły te, które powiększają wynik o jeden element naraz i kopiują wszystko w każdym przebiegu. Wybieraj funkcje apply ze względu na zwięzłość i czytelność intencji, nie szybkość.

Do czego służy vapply w R?

vapply to sapply z umową: deklarujesz typ i długość każdego wyniku, np. vapply(x, nchar, integer(1)). Jeśli funkcja zwróci coś innego, R od razu zgłosi błąd, zamiast po cichu oddać ci nieoczekiwaną strukturę. Używaj jej w kodzie, który ma działać bez nadzoru.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ