Pętle while

Na koniec przyjrzymy się pętlom while, które wykorzystują inny sposób zapętlania niż pętle for. W przypadku pętli for znamy liczbę elementów w obiekcie, których używamy do iteracji, więc z góry znamy liczbę iteracji, które zostaną wykonane. Jednak są chwile, w których nie znamy tej liczby przed rozpoczęciem iteracji, a zamiast tego będziemy iterować w oparciu o spełnienie pewnego warunku po każdej iteracji. Wtedy przydatne są pętle while Sposób działania pętli while polega na tym, że określamy warunek, tak jak w przypadku warunków if …else i jeśli warunek jest spełniony, przechodzimy do iteracji. Po zakończeniu iteracji ponownie sprawdzamy warunek i jeśli nadal jest prawdziwy, powtarzamy iterację i tak dalej. Zauważ, że w tym przypadku, jeśli chcemy zatrzymać się w pewnym momencie, musimy zmodyfikować elementy użyte w warunku tak, aby w pewnym momencie wyniósł FALSE. Możesz także użyć break i next wewnątrz pętli while. Poniższy przykład pokazuje, jak wydrukować wszystkie liczby całkowite od 1 do 10. Zauważ, że jeśli zaczynamy od 1 tak jak robimy, ale zamiast dodawać 1 po każdej iteracji, odejmujemy 1 lub nie zmieniamy didn’t nigdy nie przestanie iterować. Dlatego musisz być bardzo ostrożny podczas używania pętli while, ponieważ liczba iteracji może być nieskończona:

x <- 1

while (x <= 10) {

prit(x)

x<- x + 1

}

#> [1] 1

#> [1] 2

#> [1] 3

#> [1] 4

#> [1] 5

#> [1] 6

#> [1] 7

#> [1] 8

#> [1] 9

#> [1] 10

Jeśli chcesz wykonać nieskończoną pętlę, możesz użyć pętli while z wartością TRUE zamiast warunku. Jeśli nie dołączysz polecenia break, kod skutecznie zapewni nieskończoną pętlę i będzie się powtarzał, dopóki nie zostanie zatrzymany za pomocą polecenia klawiaturowego CTRL + C lub innego mechanizmu zatrzymywania w używanym IDE. Jednak w takich przypadkach czystsze jest użycie konstrukcji powtarzania, jak pokazano poniżej. Może się to wydawać sprzeczne z intuicją, ale są chwile, kiedy użycie nieskończonych pętli jest przydatne. W takich przypadkach masz zewnętrzny mechanizm używany do zatrzymania programu na podstawie warunku zewnętrznego względem R. następujący przykład spowoduje awarię sesji R:

# DO NOT ExECUTE THIS IT’s AN INFIITE LOOP

x <- 1

repeat {

print(x)

x <- x +1

}

#> [1] 1

#> [1] 2

#> [1] 3

#> [1] 4

#> [1] 5

#> [1] 5

Pętla for

Istnieją dwie ważne właściwości pętli for. Po pierwsze, wyniki nie są wypisywane wewnątrz pętli, chyba że jawnie wywołasz funkcję print(). Po drugie, zmienna indeksująca używana w pętli for będzie zmieniana po kolei po każdej iteracji. Ponadto, aby zatrzymać iterację, możesz użyć słowa kluczowego break, a aby przejść do następnej iteracji, możesz użyć następnego polecenia. W pierwszym przykładzie tworzymy wektor znaków o nazwie words i iterujemy przez każdy z jego elementów w kolejności, używając składni for (słowo w słowach). Spowoduje to pobranie pierwszego elementu z words, przypisanie go do word i przekazanie go przez wyrażenie zdefiniowane w bloku zdefiniowanym przez nawiasy klamrowe, które w tym przypadku wypisują słowo do konsoli, jako a także liczbę znaków w słowie. Po zakończeniu iteracji słowo zostanie zaktualizowane następnym słowem, a pętla będzie powtarzana w ten sposób, aż wszystkie słowa zostaną użyte:

words <- c(„Hello”, „there”, „dear”, „reader”)

for (word in words){

print(word)

print(nchar(word))

}

#> [1] “Hello”

#> [1] 5

#> [1] “there”

#> [1] 5

#> [1] “deare”

#> [1] 4

#> [1] “reader”

#> [1] 6

Ciekawe zachowanie można osiągnąć, używając zagnieżdżonych pętli for, które są  pętlami for wewnątrz innych pętli for. W tym przypadku obowiązuje ta sama logika, kiedy napotkamy pętlę for, wykonujemy ją aż do zakończenia. Rezultat takiego zachowania łatwiej jest zobaczyć, niż go wyjaśnić, więc spójrz na zachowanie następującego kodu:

for (i in 1:5) {

print(i)

for (j in 1:3) {

print(paste)(„    „ , j))

}

}

#> [1] 1

#> [1] “1”

#> [1] “2”

#> [1] “3”

#> [1] 2

#> [1] “1”

#> [1] “2”

#> [1] “3”

#> [1] 3

#> [1] “1”

#> [1] “2”

#> [1] “3”

#> [1] 4

#> [1] “1”

#> [1] “2”

#> [1] “3”

#> [1] 5

#> [1] “1”

#> [1] “2”

#> [1] “3”

Używanie takich zagnieżdżonych pętli for jest sposobem, w jaki ludzie wykonują operacje podobne do macierzy, gdy używają języków, które nie oferują operacji wektoryzowanych. Na szczęście możemy użyć składni przedstawionej w poprzednich sekcjach, aby wykonać te operacje bez konieczności samodzielnego używania zagnieżdżonych pętli for, co czasami może być trudne. Teraz zobaczymy, jak używać funkcji sapply() i lapply(), aby zastosować funkcję dla każdego elementu wektora. W tym przypadku wywołamy funkcję nchr() na każdym z elementów w wektorze słów, który utworzyliśmy wcześniej. Różnica między funkcjami sapply() i lapply() polega na tym, że pierwsza zwraca wektor, a druga zwraca listę. Na koniec zwróć uwagę, że jawne używanie którejkolwiek z tych funkcji nie jest konieczne, ponieważ, jak widzieliśmy wcześniej, funkcja nchar() jest już dla nas zwektoryzowana:

sapply(words, nchar)

#> Witam drogi czytelniku

#> 5 5 4 6

slapply(words, nchar)

#> [[1]]

#> [1] 5

#>

#> [[2]]

#> [1] 5

#>

#> [[3]]

#> [1] 4

#>

#> [[4]]

#> [1] 6

nchar(words)

#> [1] 5 5 4 6

Kiedy masz funkcję, która nie została wektoryzowana, jak nasza funkcja distance(). Nadal można go używać w sposób zwektoryzowany, korzystając z funkcji, o których właśnie wspomnieliśmy. W tym przypadku zastosujemy go do listy x, która zawiera trzy różne wektory numeryczne. Użyjemy funkcji lapply(), przekazując jej listę, po której nastąpi funkcja, którą chcemy zastosować do każdego z jej elementów (w tym przypadku distance(). Zwróć uwagę, że w przypadku, gdy funkcja, której używasz, otrzyma inne argumenty oprócz tego, który zostanie pobrany z x i który zostanie przekazany jako pierwszy argument do takiej funkcji, możesz przekazać je po nazwie funkcji, tak jak robimy to tutaj argumenty c(1,1,1) i l1_norm, które zostaną odebrane przez funkcję distance() jako argumenty y i norm, i pozostają ustalone dla wszystkich elementów listy x:

x <- list (c(1,2,3), c(4,5,6), c(7,8,9)

lapply(x ,distance ,c(1,1,1), l1_norm)

#> [[1]]

#> [1] 3

#>

#> [[2]]

#> [1] 12

#>

#> [[3]]

#> [1] 21

If … else warunkowe

Jak wskazuje ich nazwa, if … else warunkowe sprawdzą warunek i jeśli zostanie oszacowany jako wartość TRUE, zostanie wybrana jedna ścieżka wykonania, ale jeśli warunek zostanie oceniony jako FALSE value, wybrana zostanie inna ścieżka wykonania i wykluczają się one wzajemnie. Aby pokazać, jak działają if … else warunki, zaprogramujemy tę samą funkcję distance(), której używaliśmy wcześniej, ale zamiast przekazywać jej trzeci argument w postaci funkcji , przekażemy jej ciąg znaków, który zostanie sprawdzony, aby zdecydować, której funkcji należy użyć. W ten sposób możesz porównać różne sposoby implementacji tej samej funkcjonalności. Jeśli przekażemy łańcuch 12 do argumentu norm, zostanie użyta funkcja 12_norm(), ale jeśli przepuszczony zostanie jakikolwiek inny ciąg, zostanie użyta 11_norm(). Zauważ, że używamy podwójnego operatora równości (==), aby sprawdzić równość. Nie myl tego z pojedynczym równa się, co oznacza przypisanie:

distance <- function(x,y = 0, norm „12”){

if (norm == „12”) {

return(12_norm(x,y))

} else [

return(11_norm(x,y))

}

}

a <- c(1,2,3)

b <- c(4,5,6)

distance(a,b)

#> 27

distance(a,b,”12”)

#> 27

distance(a,b,”12”)

#> 9

distance(a,b,”l1 will be also used in this case”)

#> 9

Jak widać w ostatnim wierszu poprzedniego przykładu, użycie warunków warunkowych w sposób nieregularny może wprowadzić potencjalne błędy, ponieważ w tym przypadku użyliśmy funkcji l1_norm(), nawet jeśli argument norm w ostatnie wywołanie funkcji nie miało żadnego sensu. Aby uniknąć takich sytuacji, możemy wprowadzić więcej warunków warunkowych, aby wyczerpać wszystkie prawidłowe możliwości i zgłosić błąd funkcją stop(), jeśli wykonywana jest gałąź else, co oznaczałoby, że nie podano żadnej ważnej opcji :

distance <- function(x,y = 0, norm = „12”){

if (norm == „12”) {

return(12_norm(x,y))

}else if (norm == „l1”){

return(l1_norm(x,y)

}else {

stop(„Inwvalid norm option”)

}

}

distance(a,b,”l1”_

#> [1] 9

distance(a,b, „this will produce an error”)

#> Błąd w distance (a, b, “spowoduje to błąd”):

#> Nieprawidłowa opcja norm

Czasami else część  if … else nie jest potrzebna. W takim przypadku możesz po prostu uniknąć umieszczania go, a R wykona gałąź if, jeśli warunek zostanie spełniony, i zignoruje go, jeśli nie jest. Istnieje wiele różnych sposobów generowania wartości logicznych, których można użyć w ramach kontroli if(). Na przykład, możesz określić opcjonalny argument z wartością domyślną NULL i sprawdzić, czy nie został on wysłany w wywołaniu funkcji, sprawdzając, czy odpowiednia zmienna nadal zawiera obiekt NULL w momencie sprawdzania, używając funkcji is.null(). Rzeczywisty warunek wyglądałby mniej więcej tak if(is.null(optional_argument)). Innym razem możesz otrzymać wektor logiczny i jeśli jedną z jego wartości jest TRUE, to chcesz wykonać fragment kodu, w takim przypadku możesz użyć czegoś takiego jak if(any(logical_vector)) jako warunek lub w przypadku, gdy wymagasz, aby wszystkie wartości w wektorze logicznym były TRUE do wykonania fragmentu kodu, możesz użyć czegoś takiego jak if(all(logical_vector)). Tę samą logikę można zastosować do funkcji samoopisowych nazwanych is.na() iis.nan().

Innym sposobem generowania tych wartości logicznych jest użycie operatorów porównania. Należą do nich mniej niż (<), mniejsze lub równe (<=), większe niż (>), większe lub równe (> =), dokładnie równe (co widzieliśmy, ==) i nie równe (! =). Wszystkie z nich mogą być używane do testowania liczb, a także znaków, w takim przypadku używany jest porządek alfanumeryczny. Ponadto wartości logiczne można łączyć między sobą, aby zapewnić bardziej złożone warunki. Na przykład! operator zaneguje logikę, co oznacza, że ​​jeśli !TRUE jest równe FALSE, a !FALSE jest równe TRUE. Innymi przykładami operatorów tego typu są operator OR, gdzie w przypadku, gdy którakolwiek z wartości logicznych to TRUE, wówczas całe wyrażenie daje w wyniku TRUE oraz operator AND, w którym wszystkie logiczne muszą mieć wartość TRUE ocenić na TRUE. Chociaż nie pokazujemy konkretnych przykładów informacji wymienionych w ostatnich dwóch akapitach, zobaczysz, że zostały one użyte w przykładach, które rozwiniemy w dalszej. Na koniec zwróć uwagę, że wektoryzowana postać warunku if…else jest dostępna w funkcji ifelse(). W poniższym kodzie używamy operatora modulo w warunku, który jest pierwszym argumentem funkcji, aby określić, które wartości są parzyste, w takim przypadku używamy gałęzi TRUE, która jest drugim argumentem, aby wskazać, że liczba całkowita jest parzysta, a które nie, w takim przypadku używamy gałęzi FALSE, która jest trzecim argumentem, aby wskazać, że liczba całkowita jest nieparzysta:

ifelse(c(1,2,3,4,5,6) %% 2 == 0, „parzyste”, „nieparzyste”

#> [1] “nieparzyste” “parzyste” “nieparzyste” “parzyste” “nieparzyste” “parzyste”

Złożona logika ze strukturami sterującymi

Ostatnim tematem, który powinniśmy omówić, jest wprowadzenie złożonej logiki za pomocą struktur sterujących. Kiedy piszę wprowadzenie złożonej logiki, nie mam na myśli sugestii, że jest to skomplikowane. Złożona logika odnosi się do kodu, który ma wiele możliwych ścieżek wykonania, ale w rzeczywistości jest dość prosty do zaimplementowania. Prawie każda operacja w R może być zapisana jako funkcja, a te funkcje można przekazać do innych funkcji, aby stworzyć bardzo złożone zachowanie. Jednak implementacja logiki w ten sposób nie zawsze jest wygodna, a użycie prostych struktur sterowania może być czasami lepszą opcją. Struktury kontrolne, którym przyjrzymy się, to if … else warunkowe, pętle for i pętle while. Istnieją również warunkowe przełączniki, które są bardzo podobne do warunkowych if … else warunkowych, ale nie będziemy ich przeglądać, ponieważ nie będziemy ich używać w naszych przykładach.

 

Przymus

Na koniec krótko wspomnimy, czym jest przymus w R, ponieważ jest to temat zamieszania dla nowicjuszy. Kiedy wywołujesz funkcję z argumentem innego typu niż oczekiwano, R spróbuje wymusić wartości, aby funkcja działała, a to może wprowadzić błędy, jeśli nie zostanie poprawnie obsłużona. R będzie postępować zgodnie z mechanizmem podobnym do tego, który był używany podczas tworzenia wektorów. Języki z silnymi typami (takie jak Java) będą zgłaszać wyjątki, gdy obiekt przekazany do funkcji jest niewłaściwego typu i będą próbować nie konwertować obiektu na typ zgodny. Jednak, jak wspomnieliśmy wcześniej, R został zaprojektowany do działania po wyjęciu z pudełka w wielu nieprzewidzianych kontekstach, więc wprowadzono przymus. W poniższym przykładzie pokazujemy, że jeśli wywołasz naszą funkcję distance() i przekażemy wektory logiczne zamiast liczbowych, R przekształci wektory logiczne w wektory numeryczne, używając TRUE jako 1 i FALSE jako 0 i kontynuuj obliczenia. Aby uniknąć tego problemu we własnych programach, należy jawnie wymuszać typy danych za pomocą funkcji as.*(), o których wspominaliśmy wcześniej:

x <- c(1,2,3)

y <- c(TRUE, FALSE, TRUE)

distance(x,y)

#> [1] 8

Operatory to funkcje

Teraz, gdy już wiesz, jak działają funkcje ,powinieneś wiedzieć, że nie wszystkie wywołania funkcji wyglądają jak te, które pokazaliśmy do tej pory, gdzie używasz nazwy funkcji, po której następuje nawias zawierający argumenty funkcji. Właściwie wszystkie instrukcje w R, w tym ustawianie zmiennych i operacje arytmetyczne, są funkcjami w tle, nawet jeśli najczęściej nazywamy je inną składnią. Pamiętaj, że wspomnieliśmy, że do obiektów R można się odwoływać za pomocą prawie każdego łańcucha, ale powinieneś tego unikać. Tutaj pokazujemy, jak używanie tajemniczych nazw może być przydatne w pewnych kontekstach. Poniższy przykład pokazuje, jak operatory przypisania, wyboru i dodawania są zwykle używane ze składnią cukru (termin używany do opisania składni, która istnieje w celu ułatwienia użycia), ale w tle używają funkcji o nazwie [ <- , [, i +, odpowiednio. Funkcja [<- () otrzymuje trzy argumenty: wektor, który chcemy zmodyfikować, pozycję jaką chcemy zmodyfikować w wektorze i wartość, jaką chcemy, aby miał on w tej pozycji. Funkcja [()otrzymuje dwa argumenty, wektor, z którego chcemy pobrać wartość oraz pozycję wartości, którą chcemy pobrać. Ostatnia funkcja +() otrzymuje dwie wartości jakie chcemy dodać. Poniższy przykład pokazuje lukier składni, po którym następuje wywołanie funkcji w tle, które wykonuje dla nas R:

x <- c(1,2,3,4,5)

x

#> [1] 1 2 3 4 5

x[1] <- 10

x

#> [1] 10 2 3 4 5

[<- (x,1,20)

#> [1] 20 2 3 4 5

x

#> [1] 10 2 3 4 5

x[1]

#> [1] 10

‘[‘(x,1)

#> [1] 10

x[1]+x[2]

#> [1] 12

‘+’ (c[1],x[2])

#> [1] 12

‘+’ (‘[‘(x,1),’[‘(x,1))

#> [1] 20

W praktyce prawdopodobnie nigdy nie napisałbyś tych instrukcji jako jawnych wywołań funkcji. Cukier składniowy jest znacznie bardziej intuicyjny i łatwiejszy do odczytania. Jednak aby skorzystać z niektórych zaawansowanych technik przedstawionych w tej książce, warto wiedzieć, że każda operacja w języku R jest funkcją.

Funkcje jako argumenty

Czasami, gdy chcesz uogólnić funkcje, możesz chcieć podłączyć określoną funkcjonalność do funkcji. Możesz to zrobić na różne sposoby. Na przykład, możesz użyć warunków warunkowych, co zobaczymy w następnej sekcji tego rozdziału, aby zapewnić im różne funkcje w zależności od kontekstu. Jednak w miarę możliwości należy unikać warunkowych, ponieważ mogą one niepotrzebnie komplikować nasz kod. Lepszym rozwiązaniem byłoby przekazanie funkcji jako parametru, który będzie wywoływany w razie potrzeby, a jeśli chcemy zmienić sposób zachowania funkcji, możemy zmienić funkcję, przez którą przechodzimy dla określonego zadania. Może się to wydawać skomplikowane, ale w rzeczywistości jest to bardzo proste. Zacznijmy od utworzenia funkcji l1_norm, która oblicza odległość między dwoma wektorami, ale używa sumy bezwzględnych różnic między odpowiadającymi im współrzędnymi zamiast sumy kwadratów różnic, tak jak robi to nasza funkcja l2_norm. Zauważ, że używamy tego samego podpisu dla naszych dwóch funkcji, co oznacza, że ​​obie otrzymują te same wymagane i opcjonalne argumenty, którymi w tym przypadku są x i y. Jest to ważne, ponieważ jeśli chcemy zmienić zachowanie poprzez przełączanie funkcji, musimy upewnić się, że są one w stanie pracować z tymi samymi danymi wejściowymi, w przeciwnym razie możemy uzyskać nieoczekiwane wyniki a nawet błędy:

l1_norm <- function(x,y = 0) sum (abs(x-y)

l1_norm(a)

#> [1] 6

l1_norm(a,b)

#> [1] 9

Teraz, gdy nasze l2_norm() i l1_norm() są zbudowane tak, że można je przełączać aby zapewnić inne zachowanie, stworzymy trzecią funkcję distance(), która przyjmie dwa wektory jako argumenty, ale otrzyma również argument norm, który będzie zawierał funkcję, której chcemy użyć do obliczyć odległość. Zauważ, że określamy, że chcemy domyślnie używać l2_norm() w przypadku, gdy nie ma jawnego wyboru podczas wywoływania funkcji, i aby to zrobić, po prostu określamy symbol, który zawiera obiekt funkcji, bez nawiasów . Na koniec zwróć uwagę, że jeśli chcemy uniknąć wysyłania wektora 􀁚, ale chcemy określić, jaka norma ma być użyta, to musimy przekazać ją jako nazwany argument, w przeciwnym razie R zinterpretowałby drugi argument jako wektor 􀁚, a nie funkcja norm:

distance <- function(x,y = 0, norm = l2_norm) norm(x,y)

distance(a)

#> [1] 14

distance(a,b)

#> [1] 27

distance(a,b,l2_norm)

#> [1] 27

distance(a,b,l1_norm)

#> [1] 9

distance(a,norm = l1_norm)

#> [1] 6

Opcjonalne argumenty

Podczas tworzenia funkcji możesz określić domyślną wartość argumentu, a jeśli to zrobisz, argument ten jest uważany za opcjonalny. Jeśli nie określisz wartości domyślnej dla argumentu i nie określisz wartości podczas wywoływania funkcji, otrzymasz błąd, jeśli funkcja spróbuje użyć argumentu. W poniższym przykładzie pokazujemy, że jeśli pojedynczy wektor numeryczny zostanie przekazany do naszej funkcji l2_norm() w obecnym kształcie, spowoduje to błąd, ale jeśli przedefiniujemy go tak, aby drugi wektor był opcjonalny, to po prostu zwrócimy normę pierwszego wektora, a nie odległość między dwoma różnymi wektorami Aby to osiągnąć, zapewnimy zerowy wektor o długości jeden, ale ponieważ R powtarza elementy wektorowe aż wszystkie wektory zaangażowane w operację będą miały tę samą długość , jak widzieliśmy wcześniej w tym rozdzial, automatycznie rozszerzy nasz zerowektor do odpowiedniego wymiaru:

l2_norm(a)

#> Błąd w l2_norm (a): brak argumentu “y”, brak wartości domyślnej

l2_norm <-  function(x,y,= 0) sum ((x-y)^2)

l2_norm(a)

#> [1] 14

l2_norm(a,b)

#> [1] 27

Jak widać, teraz nasza funkcja może opcjonalnie otrzymać wektor 􀁚, ale bez niego będzie działać zgodnie z oczekiwaniami. Zwróć również uwagę, że wprowadziliśmy kilka komentarzy do naszego kodu. Cokolwiek pojawi się po symbolu 􀀅 w wierszu, R zignoruje, co pozwala nam wyjaśnić nasz kod tam, gdzie jest to konieczne. Wolę unikać komentarzy, ponieważ uważam, że kod powinien być wyrazisty i przekazywać swój zamiar bez potrzeby komentowania, ale są one przydatne od czasu do czasu.

Dziel i zwyciężaj funkcjami

Funkcje są podstawowym budulcem języka R. Aby opanować wiele z bardziej zaawansowanych technik opisanych w tej książce, potrzebujesz solidnych podstaw ich działania. Skorzystaliśmy już z kilku funkcji powyżej, ponieważ bez nich nie można zrobić nic ciekawego w R. Są po prostu tym, co pamiętasz z zajęć z matematyki, sposobem przekształcania danych wejściowych w wyniki. W szczególności w języku R funkcja jest obiektem, który przyjmuje inne obiekty jako dane wejściowe, zwane argumentami, i zwraca obiekt wyjściowy. Większość funkcji ma następującą postać f(argumet_1,argument_2, …) Gdzie f to nazwa funkcji, a argumet_1,argument_2 i tak dalej to argumenty funkcji. Zanim przejdziemy dalej, powinniśmy pokrótce wspomnieć o roli nawiasów klamrowych ({}) w R. Często są one używane do grupowania zestawu operacji w treści funkcji, ale mogą być również używane w innych później, Dodawanie interaktywności z pulpitami nawigacyjnymi). Nawiasy klamrowe służą do oceny serii wyrażeń, które są oddzielone znakami nowej linii lub średnikami i zwracają jako wynik tylko ostatnie wyrażenie. Na przykład poniższy wiersz wyświetla tylko operację x + y na ekranie, ukrywając wynik operacji x * y, która zostałaby wydrukowana, gdybyśmy wpisywali wyrażenia krok po kroku. W tym sensie nawiasy klamrowe służą do hermetyzacji zestawu zachowań i zapewniają wynik tylko z ostatniego wyrażenia:

{ x <- 1; y <- 2; x * y ; x = y }

#> [1] 3

Możemy stworzyć własną funkcję używając konstruktora function90 i przypisać ją do symbolu. Konstruktor fuctio() przyjmuje dowolną liczbę nazwanych argumentów, których można użyć w treści funkcji. Nienazwane argumenty można również przekazywać przy użyciu notacji argumentów „…”, ale jest to zaawansowana technika, której nie będziemy omawiać . Zachęcamy do przeczytania dokumentacji funkcji, aby dowiedzieć się więcej o nich. Podczas wywoływania funkcji argumenty mogą być przekazywane według pozycji lub nazwy. Porządek pozycyjny musi odpowiadać kolejności podanej w sygnaturze funkcji (czyli specyfikacji function() z odpowiednimi argumentami), ale używając nazwanych argumentów, możemy je przesłać w dowolnej kolejności . Jak pokazuje poniższy przykład. W poniższym przykładzie tworzymy funkcję, która oblicza odległość euklidesową między dwoma wektorami numerycznymi i pokazujemy, jak można zmienić kolejność argumentów, jeśli użyjemy nazwanych argumentów. Aby zrealizować ten efekt, używamy funkcji print(), aby upewnić się, że w konsoli możemy zobaczyć, co R odbiera jako wektory x i y. Podczas tworzenia własnych programów używanie funkcji w podobny sposób jest bardzo przydatne, aby zrozumieć, co się dzieje. Zamiast używać nazwy funkcji takiej jak euclidian_distance, użyjemy 12_norm, ponieważ jest to uogólniona nazwa takiej operacji podczas pracy ze spacjami dowolne wymiary liczbowe oraz ponieważ ułatwi to zrozumienie kolejnego przykładu. Zwróć uwagę, że chociaż poza wywołaniem funkcji nasze wektory nazywane są a i b, ponieważ są one przekazywane do argumentów x i y, to są to nazwy, których musimy używać w naszej funkcji. Początkującym łatwo jest pomylić te obiekty jako takie same, gdybyśmy użyli nazw x i y w obu miejscach:

12_norm <- function(x,y) {

print(„x”)

print(x)

print(„y”)

print(y)

element_to_element_difference <- x – y

result ,- sum(element_to_element_difference^2)

return(result)

}

a <- c(1,2,3)

b <- c(4,5,6)

12_norm(a,b)

#> [1] “x”

#> [1] 1 2 3

#> [1] “y”

#> [1] 4 5 6

#> [1] 27

12_norm(b,a)

#> [1] “x”

#> [1] 4 5 6

#> [1] “y”

#> [1] 1 2 3

#> [1] 27

12_norm(x =a , y = b)

#> [1] “x”

#> [1] 1 2 3

#> [1] “y”

#> [1] 4 5 6

#> [1] 27

12_notm(y = b, x = a)

#> [1] “x”

#> [1] 1 2 3

#> [1] “y”

#> [1] 4 5 6

#> [1] 27

Funkcje mogą używać funkcji return() do określenia wartości zwracanej przez funkcję. Jednak R po prostu zwróci ostatnie oszacowane wyrażenie jako wynik funkcji, więc możesz zobaczyć kod, który nie używa jawnie funkcji return(). Nasza poprzednia implementacja funkcji 12_norm() wydaje się być nieco zagracona. Jeśli funkcja ma jedno wyrażenie, możemy uniknąć używania nawiasów klamrowych, co możemy osiągnąć, usuwając wywołania funkcji print() i unikając tworzenia obiektów pośrednich, a ponieważ wiemy, że działa dobrze, możemy zrób to bez wahania. Ponadto unikamy jawnego wywoływania funkcji return(), aby jeszcze bardziej uprościć nasz kod. Jeśli to zrobimy, nasza funkcja wygląda znacznie bliżej swojej matematycznej definicji i jest łatwiejsza do zrozumienia, prawda? 12_norm <- function(x,y)  sum ((x-y)^2)

Ponadto, jeśli nie zauważyłeś, ponieważ używamy operacji wektoryzowanych, możemy wysłać wektory o różnych długościach (wymiarach), pod warunkiem, że oba wektory mają tę samą długość, a funkcja będzie działać tak, jak się tego spodziewamy, bez względu na wymiarowość przestrzeni, z którą pracujemy. Jak wspomniałem wcześniej, wektoryzacja może być dość potężna. W poniższym przykładzie pokazujemy takie zachowanie z wektorami o wymiarze 1 (matematycznie nazywanymi skalarami), a także wektorami o wymiarze 5 utworzonymi za pomocą składni skrótu „:”:

12_norm(1,2)

#> [1] 1

12_norm(1:5, 6:10)

#> [1] 125

Zanim przejdziemy dalej, chcę tylko wspomnieć, że należy zawsze starać się przestrzegać zasady pojedynczej odpowiedzialności, która mówi, że każdy przedmiot (w tym przypadku funkcjonuje) powinien skupiać się na robieniu jednej rzeczy i robić to bardzo dobrze. Ilekroć opisujesz funkcję utworzyłeś jako robiąc „coś” i „coś innego”, prawdopodobnie robisz to źle, ponieważ „i” powinny informować Cię, że funkcja wykonuje więcej niż jedną rzecz, i powinieneś podzielić ją na dwie lub więcej funkcji, które ewentualnie zadzwonić do siebie.

Ramki danych

Teraz przejdźmy do ramek danych, które bardzo przypominają arkusze kalkulacyjne lub tabele bazy danych. W kontekście naukowym eksperymenty składają się z pojedynczych obserwacji (wierszy), z których każda obejmuje kilka różnych zmiennych (kolumn). Często zmienne te zawierają różne typy danych, których nie można przechowywać w macierzach, ponieważ muszą zawierać jeden typ danych. Ramka danych jest naturalnym sposobem reprezentowania takich heterogenicznych danych tabelarycznych. Każdy element w kolumnie musi być tego samego typu, ale różne elementy w wierszu mogą być różnych typów, dlatego mówimy, że ramka danych jest heterogeniczną strukturą danych. Z technicznego punktu widzenia ramka danych to lista, której elementami są wektory o równej długości i dlatego pozwala na niejednorodność. Ramki danych są zwykle tworzone przez wczytywanie danych za pomocą read.table(), read.csv() lub innych podobnych funkcji ładowania danych. Jednak można je również utworzyć jawnie za pomocą funkcji data.frame() lub wymusić na podstawie innych typów obiektów, takich jak listy. Aby utworzyć ramkę danych za pomocą funkcji data.frame(), zwróć uwagę, że wysyłamy wektor (który, jak wiemy, musi zawierać elementy jednego typu) do każdej z nazw kolumn, które chcemy nasze ramki danych, którymi w tym przypadku są A, B i C. Ramka danych, którą utworzymy poniżej, ma cztery wiersze (obserwacje) i trzy zmienne, odpowiednio z typami liczbowymi, znakowymi i logicznymi. Na koniec wyodrębnij podzbiory danych za pomocą technik macierzowych, które widzieliśmy wcześniej, ale możesz również odwoływać się do kolumn za pomocą operatora $, a następnie wyodrębnić z nich elementy:

x <- data.frame(

A = c(1,2,3,4),

B= c(„D”, „E”, „F”, „G”),

C = c(TRUE ,FALSE, NA, FALSE)

)

x[1, ]

#> A B C

#> 1 1 D TRUE

x[, 1]

#> [1] 1 2 3 4

x[1:2, 1:2]

#> A B

#> 1 1 D

#> 2 2 E.

x$B

#> [1] D E F G

#> Levels: D E F G

x$B[2]

#> [1] E.

#> Levels: D E F G

W zależności od sposobu organizacji danych, ramka danych może mieć szeroki lub wąski format. Wreszcie, jeśli chcesz zachować tylko obserwacje, dla których masz pełne obserwacje, czyli tylko wiersze, które nie zawierają żadnych wartości NA dla żadnej ze zmiennych, wtedy powinieneś użyć complete.cases() funkcja, która zwraca logiczny wektor o długości równej liczbie wierszy i która zawiera TRUE wartość dla tych wierszy, które nie mają żadnych NA wartości i FALSE dla tych, które mają co najmniej jedną taką wartość. Zauważ, że kiedy stworzyliśmy ramkę danych x, kolumna C zawiera NA w swojej trzeciej wartości. Jeśli użyjemy funkcjicomplete.cases() na x, otrzymamy wartość FALSE dla tego wiersza i wartość TRUE dla wszystkich pozostałych. Możemy następnie użyć tego wektora logicznego do podzbioru ramki danych, tak jak to zrobiliśmy wcześniej z macierzami. Może to być bardzo przydatne podczas analizowania danych, które mogą nie być czyste i dla których chcesz zachować tylko te obserwacje, dla których masz pełne informacje:

x

#> A B C

#> 1 1 D TRUE

#> 2 2 E FAŁSZ

#> 3 3 F NA

#> 4 4 G FAŁSZ

cmplete.cases(x)

#> [1] TRUE TRUE FALSE TRUE

x[complete.cases(x), ]

#> A B C

#> 1 1 D TRUE

#> 2 2 E FALSE

#> 4 4 G FALSE