= 1:10
x = NULL # pusty obiekt
y for (i in x) {
= i * 2
wynik = c(y, wynik)
y
} y
[1] 2 4 6 8 10 12 14 16 18 20
Wektoryzacja operacji
Wektoryzacja operacji jest techniką programistyczną, która umożliwia jednoczesne wykonanie działań dla wszystkich elementów obiektu (np. wektora, macierzy czy listy), zamiast przetwarzania ich jeden po drugim za pomocą pętli. Technika ta stosowana jest w różnych językach i bibliotekach, np. NumPy
(Python), Eigen
(C++), Julia, i oczywiście R.
Język R jest zoptymalizowany pod kątem operacji wektorowych, co sprawia, że są one zazwyczaj znacznie wydajniejsze przede wszystkim z powodu zaimplementowania ich w językach niższego poziomu takich jak C czy Fortran. Kod jest również czytelniejszy i krótszy niż kod wykorzystujący pętle. Pomimo tego, nie wszystkie zadania mogą zostać zwektoryzowane, zwłaszcza jeśli wymagają złożonej logiki i iteracyjnego podejścia.
Rozpocznijmy od napisania prostej pętli, która wykona mnożenie elementów wektora od 1 do 10 przez 2. Iloczyn każdego elementu musimy przechowywać w nowej zmiennej y
, zatem należy ją wcześniej zdefiniować jako pusty obiekt (NULL
).
= 1:10
x = NULL # pusty obiekt
y for (i in x) {
= i * 2
wynik = c(y, wynik)
y
} y
[1] 2 4 6 8 10 12 14 16 18 20
Teraz wykonajmy tę samą operację używając wektoryzacji kodu.
= x * 2
y y
[1] 2 4 6 8 10 12 14 16 18 20
Iloczyn został obliczony dla każdego elementu wektora x
bez stosowania pętli i otrzymaliśmy dokładnie ten sam wynik.
Z założenia wszystkie operacje arytmetyczne, porównawcze oraz logiczne są zwektoryzowane i stosowane do wektorów element po elemencie.
# dodawanie
= c(1, 2, 3, 4)
x = c(4, 3, 2, 1)
y + y x
[1] 5 5 5 5
# porównanie
= c(1, 2, 3, 4)
x = c(1, 0, 0, 4)
y == y x
[1] TRUE FALSE FALSE TRUE
Podczas wykonywania operacji na wektorach o różnych długościach, krótszy wektor jest powtarzany, tak aby odpowiadał długości dłuższego wektora (jest to tak zwany recykling). W przypadku, gdy krótszy wektor nie jest wielokrotnością dłuższego wektora, to zostanie zwrócone ostrzeżenie, a wynikowa długość nie będzie pełną wielokrotnością. Należy o tym pamiętać, aby uniknąć niepożądanych skutków.
= 1:6
x = 1:2
y + y # 1+1, 2+2, 3+1, 4+2, 5+1, 6+2 x
[1] 2 4 4 6 6 8
= 1:6
x > 3 # 1>3, 2>3, 3>3, 4>3, 5>3, 6>3 x
[1] FALSE FALSE FALSE TRUE TRUE TRUE
Załóżmy, że mamy dany wektor z wartościami ujemnymi oraz dodatnimi i chcemy odwrócić znak dla wartości ujemnych. Możemy to zrobić używając poniższej pętli.
= -2:2
x = NULL
y
for (i in 1:length(x)) {
if (x[i] < 0) {
= c(y, -x[i])
y else {
} = c(y, x[i])
y
}
} y
[1] 2 1 0 1 2
Jednak, możemy ten kod znacząco uprościć używając zwektoryzowanej instrukcji warunkowej ifelse()
, która przyjmuje trzy argumenty:
x < 0
),= -2:2
x = ifelse(x < 0, -x, x)
y y
[1] 2 1 0 1 2
W funkcji ifelse()
można zagnieżdżać warunki, aby stworzyć bardziej złożoną logikę. Jednak, wraz ze wzrostem złożoności mogą stać się trudne do odczytania. Wtedy lepiej zastosować standardowe instrukcje warunkowe.
= -2:2
x ifelse(x < 0,
"Wartość ujemna",
ifelse(x == 0, "Zero", "Wartość dodatnia")
)
[1] "Wartość ujemna" "Wartość ujemna" "Zero" "Wartość dodatnia"
[5] "Wartość dodatnia"
W kontekście wektoryzacji sprawdzania warunków logicznych, możemy wykorzystać dwie bardzo przydatne funkcje, tj. any()
oraz all()
. Funkcja any()
sprawdza, czy przynajmniej jeden element w wektorze logicznym ma wartość TRUE
(jeśli tak, to zostanie zwrócona wartość TRUE
). Natomiast funkcja all()
zwróci wartość TRUE
, tylko wtedy, gdy wszystkie elementy w wektorze są TRUE
.
# czy przynajmniej jeden element jest TRUE?
any(c(FALSE, FALSE, TRUE))
[1] TRUE
# czy wszystkie elementy są TRUE?
all(c(TRUE, TRUE, TRUE))
[1] TRUE
Najczęściej te dwie funkcje stosowane są do walidacji danych.
= 1:5
x
if (any(x > 10)) {
print("Co najmniej jedna liczba jest większa od 10!")
}
if (all(x > 0)) {
print("Wszystkie liczby są dodatnie!")
}
[1] "Wszystkie liczby są dodatnie!"
Oprócz zwektoryzowanych operacji na wektorach czy macierzach, R zapewnia również bogaty zestaw zwektoryzowanych funkcji dla różnego rodzaju zadań, np. funkcje matematyczne, statystyczne, przetwarzania tekstu i inne.
= 1:10 x
# suma
sum(x)
[1] 55
# pierwiastek
sqrt(x)
[1] 1.000000 1.414214 1.732051 2.000000 2.236068 2.449490 2.645751 2.828427
[9] 3.000000 3.162278
# średnia
mean(x)
[1] 5.5
# odchylenie standardowe
sd(x)
[1] 3.02765
Przykładowo, funkcje takie jak paste()
, substr()
, toupper()
, mogą zostać wykorzystane do zwektoryzowanego przetwarzania wektorów tekstowych.
= c("rower", "budynek", "drzewo") x
# łączenie tekstu
paste(x, collapse = " ")
[1] "rower budynek drzewo"
# wyodrębnienie pierwszej litery
substr(x, start = 1, stop = 1)
[1] "r" "b" "d"
# zamiana na wielkie litery
toupper(x)
[1] "ROWER" "BUDYNEK" "DRZEWO"
W kontekście danych tekstowych przydatna jest także funkcja nchar()
, która zwraca liczbę liter w ciągu znaków.
# liczba wyrazów
length(x)
[1] 3
# liczba liter w każdym wyrazie
nchar(x)
[1] 5 7 6
Funkcja nchar()
działa także dla wektorów liczbowych poprzez ich automatyczną konwersję na typ tekstowych, co może powodować pewne nieoczekiwane wyniki, np. nchar(-1)
, nchar(1.23)
.
Do tej pory omówiliśmy podstawowe techniki wektoryzacji operacji. Niemniej, dostępne są również dodatkowe narzędzia, w szczególności służące do wektoryzacji operacji na bardziej złożonych strukturach danych niż wektory. W tym celu stosowana jest rodzina funkcji *apply()
.
apply
)Funkcja apply()
wykonuje wybraną funkcję do marginesów macierzy lub tablicy. Należy zdefiniować dwa argumenty – MARGIN
, który przyjmuje wartość 1 (obliczenia dla wierszy) lub 2 (obliczenia dla kolumn) oraz FUN
, w którym określamy funkcję, która zostanie zastosowana.
Sprawdźmy teraz dwa przykłady na macierzy składającej się z trzech wierszy i czterech kolumn. W pierwszym obliczymy sumę dla każdego wiersza macierzy, a w drugim sumę dla każdej kolumny. Wyniki zostaną zwrócone jako wektory o odpowiedniej długości.
= matrix(1:12, nrow = 3, ncol = 4)
mat mat
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
# suma dla każdego wiersza
apply(mat, MARGIN = 1, FUN = sum)
[1] 22 26 30
# suma dla każdej kolumny
apply(mat, MARGIN = 2, FUN = sum)
[1] 6 15 24 33
lapply
)Funkcja lapply()
wykonuje wybraną funkcję każdego elementu listy i zwraca listę o tej samej długości.
Dla przykładu zdefiniujmy listę, która będzie składać się z trzech wektorów liczbowych i obliczmy średnią dla każdego z nich.
= list(a = 1:3, b = 4:6, c = 7:9)
lst # średnia dla każdego wektora listy
lapply(lst, FUN = mean)
$a
[1] 2
$b
[1] 5
$c
[1] 8
Podobnie możemy zdefiniować listę, która będzie składać się z wektorów różnego typu i zwrócić ich długości.
= list(a = 1:5, b = letters[1:4], c = c(TRUE, FALSE))
lst # długość każdego wektora listy
lapply(lst, FUN = length)
$a
[1] 5
$b
[1] 4
$c
[1] 2
Jeśli chcielibyśmy uzyskać wynik w postaci wektora zamiast listy, to powinniśmy użyć funkcji sapply()
, która jest podobna do lapply()
, ale próbuje uprościć wynik do wektora (lub macierzy), pod warunkiem, że wyniki są tej samej długości i typu (w przeciwnym razie zwróci listę).
# uprość wynik do wektora (jeśli to możliwe)
sapply(lst, FUN = length)
a b c
5 4 2
Poznaliśmy tylko trzy podstawowe funkcje z rodziny *apply()
, ale jest ich więcej, np. tapply()
stosowana z podziałem na kategorie czy vapply()
z jawnym określeniem zwracanego typu. Dodatkowo, w przypadku bardzo dużych zbiorów danych, można połączyć wektoryzację operacji z przetwarzaniem równoległym wykorzystującym wiele rdzeni procesora w celu dalszego zwiększenia wydajności.
10, 20, 30, 40
i 40, 30, 20, 10
. Następnie wykonaj to samo działanie, ale używając podejścia zwektoryzowanego.okno
, kot
, stolik
, chmura
. Stwórz nowy wektor, który połączy te wyrazy z ich wersjami pisanymi wielkimi literami, np. okno_OKNO
.n
rzutów kostką bez użycia pętli, a następnie obliczy odsetek w procentach, ile razy wypadła wartość wskazana przez użytkownika.