--- title: "Wprowadzenie do `dplyr`" author: "Bartosz Maćkiewicz" subtitle: Statystyka I z R output: html_document: default pdf_document: default --- # Wprowadzenie do `dplyr` Pakiet `dplyr` jest pakietem, który ma pozwolić nam ułatwić i przyspieszyć wykonywanie różnych często dokonywanych operacji na ramkach danych. Jego podstawowe zalety to przejrzysta składnia oraz szybkość działania (wiele z jego funkcji zostało napisanych w C++ dla lepszej optymalizacji). Przy naprawdę dużej ilości danych użycie tego pakietu może okazać się niezbędne. Wszystkie te operacje potrafią Państwo wykonać za pomocą "czystego" R, na czym bardzo mi zależało. Być może jednak czasami będzie wygodniej zrobić to używając funkcji dostarczanych przez pakiet `dplyr`. Na pewno traci się pewną elastyczność i kontrolę nad tym, co się robi, nie ulega jednak wątpliwości, że czasami warto zapłacić taką cenę za wygodę pracy. To wprowadzenie zostało przygotowane na podstawie oficjalnego krótkiego wprowadzenia do pakietu oraz dokumentacji. Omówimy sobie kilka możliwości jakie daje nam ten pakiet. Instalujemy pakiet za pomocą standardowego `install.packages` oraz ładujemy go za pomocą `library`. ```{r message=F} #install.packages('dplyr') library(dplyr) ``` Do ćwiczeń wykorzystamy dane pochodzące z badań nad uchowcami. Jak wyglądają te zwierzatka? Tak: ![Uchowiec](220px-LivingAbalone.JPG) Informacje na temat danych mogą Państwo znaleźć tutaj: [http://archive.ics.uci.edu/ml/datasets/Abalone](http://archive.ics.uci.edu/ml/datasets/Abalone) Wczytajmy dane i zobaczmy jak one wyglądają za pomocą `head`. ```{r} df <- read.csv('abalone.data', header = FALSE) col_names <- c('Sex', 'Length', 'Diameter', 'Height', 'Whole.weight', 'Shucked.weight', 'Viscera.weight', 'Shell.weight', 'Rings') colnames(df) <- col_names head(df) ``` W kolumnie `Sex` znajdujemy informacje o płci (I oznacza "infant"), dalej mamy dane dotyczące rozmiaru i wagi oraz liczby "kręgów" (rings?). Z dokumentacji danych możemy dowiedzieć się, co zmienna ta oznacza: "Rings / integer / -- / +1.5 gives the age in years" ## `filter` Funkcja `filter` pozwala nam wybierać wiersze z ramki danych. Warto zwrócić uwagę na jej składnie. Jako pierwszy argument przyjmuje ramkę danych, kolejne wyrażenia to warunki, jakie musi spełniać obserwacja. **Bardzo ważne** jest, żeby zauważyć, że do nazw kolumn odwołujemy się bez cudzysłowów. Spróbujmy wybrać wszystkie te ślimaczki, które są płci męskiej i mają więcej niż 22 "kręgi". ```{r} filter(df, Sex == 'M', Rings > 22) ``` Możemy oczywiście użyć bardziej oczywistej składni i posłużyć się operatorem logicznym koniunkcji. Efekt jest dokładnie taki sam, jak w poprzednim przykładzie: ```{r} filter(df, Sex == 'M' & Rings > 22) ``` Nic nie stoi na przeszkodzie, by używać wszystkich dostępnych w R operatorów logicznych i arytmetycznych. W poniższym przykładzie wybraliśmy te ślimaczki, które są płci żeńskiej lub męskiej, mają więcej niż 0.24mm wysokości oraz więcej niż 10 "kręgów". Wszystkie zasady związane z nawiasami jakie znamy ze standardowego R dalej obowiązują. ```{r} filter(df, (Sex == 'M' | Sex == 'F') & Height > 0.24 & Rings >= 10) ``` ## `arrange` Za pomocą funkcji `arrange` możemy porzadkować ramkę danych zgodnie z wartościami z poszczególnych kolumn. Jako pierwszy argument przekazujemy ramkę danych, kolejnymi są kolumny po których sortujemy. Kolejne kolumny używane są wówczas, gdy w poprzednich mamy do czynienia z "remisem". W 2 i 3 wierszu nasze ślimaczki mają tyle samo "kręgów", więc pod uwagę brana jest kolejna kolumna - długość. Za pomocą funkcji `desc` możemy zdecydować, czy porządkować chcemy rosnąco, czy malejąco. W zasadzie funkcja ta robi to samo co `order`, oferując (może) bardziej przejrzystą składnię. ```{r} head(arrange(df, desc(Rings), desc(Length), Sex), 15) ``` ## `select` `select` umożliwia nam wybieranie kolumn z ramki danych. Jako pierwszy argument przyjmuję ramkę danych jako kolejne kolumny, które chcemy wybrać. Z najprostszym przypadkiem mamy do czynienia wówczas, gdy po prostu przekazujemy nazwy kolumn. ```{r} head(select(df, Sex, Length, Diameter)) ``` Pakiet `dplyr` umożliwia nam radzenie sobie z sytuacjami, gdy w naszej ramce danych mamy bardzo dużo kolumn. Załóżmy, że chcemy wybrać tylko te z nich, których nazwa zawiera ciąg znaków `weight`. Możemy ten problem rozwiązać za pomoca funkcji `contains`. ```{r} head(select(df, Sex, contains('weight'))) ``` Dodatkową funkcjonalnościa jest fakt, że możemy posłużyć się wyrażeniami regularnymi. Jeżeli nie mieli Państwo z nimi styczności wcześniej, to najkrócej mówiąc są to ciągi znaków wyrażające pewne wzorce, do których dopasowywane są napisy. W poniższym przykładzie posłużyłem się patternem `^..ght`, który dopasowuje wszystkie napisy, które zaczynają się (`^`) od dwóch dowolnych znaków (`..`), po których następuje `ght`. W naszym przypadku tylko `Height` spełnia te warunki, dlatego tylko tę kolumnę wybraliśmy. ```{r} head(select(df, Sex, matches('.*ght'))) ``` Możemy również wybrać wszystkie kolumny z wyjątkiem jakichś. Posługując się składnią `nazwa_kolumny:_nazwa_kolumny` wybieramy wszystkie kolejne kolumny pomiędzy dwoma okreslonymi i używając znaku minusa (`-`) wybieramy wszystkie kolumny z ramki danych oprócz tych wyszczególnionych. W przykładzie poniżej posłużyliśmy się tą metodą, by wybrać wszystkie kolumny oprócz tych dotyczących wagi naszych zwierzatek. ```{r} head(select(df, -(Whole.weight:Shell.weight))) ``` ## `distinct` Wybiera unikatowe wartości z ramki danych, jako drugi i kolejne argumenty przekazujemy kolumny, które chcemy wziąć pod uwagę, decydując o "unikatowości" obserwacji. W poniższym przykładzie przekazaliśmy kolumny `Sex` i `Rings`, otrzymamy więc maksymalnie 3 (liczba płci) x 29 (liczba "kręgów") wartości (bo tyle jest unikatowych kombinacji w naszym przypadku). W rzeczywistości otrzymaliśmy 68 (ponieważ nie wszystkie kombinacje występują w naszym zbiorze danych) ```{r} nrow(distinct(df, Sex, Rings)) head(distinct(df, Sex, Rings), 15) ``` ## `mutate` Za pomocą funkcji `mutate` możemy łatwo dodawać kolumny. Składnia jest analogiczna jak przy poprzednich funkcjach - pierwszym argumentem jest ramka danych, kolejne to kolumny, które chcemy dodać. Określając wartości w kolumnach możemy posługiwać się operatorami arytmetycznymi ale też funkcjami R (np. `sqrt`). Korzystajac z wiedzy dotyczącej relacji między ilością "kręgów" oraz wiekiem dodajmy nową kolumnę `Age`. Dla demonstracji dodajmy również dwie dodatkowe kolumny ujmujące relację między wagą a wymiarami. ```{r} head(mutate(df, Age = Rings + 1.5, Height.to.weight.ratio = Height/Whole.weight, Body.mass.ratio = sqrt(Height*Diameter)/Whole.weight)) ``` Funkcja `transmute` różni się od `mutate` tylko tym, że zwraca wyłacznie nowododane kolumny: ```{r} head(transmute(df, Age = Rings + 1.5, Height.to.weight.ratio = Height/Whole.weight, Body.mass.ratio = sqrt(Height*Diameter)/Whole.weight)) ``` ## `summarise` `summarise` pozwala nam stworzyć ramkę danych, w której w jakiś sposób "podsumowujemy" wiele wartości do jednej. Możemy użyć tutaj standardowych funkcji R takich jak `mean` czy `sd`. W przypadku poniżej stworzyliśmy dwie nowe kolumny, które sprowadzają wartości w kolumnach `Rings` i `Whole.weight` do jednej wartości (do średniej oraz odchylenia standardowego). ```{r} summarise(df, ring_mean = mean(Rings), wieight_sd = sd(Whole.weight)) ``` ## `sample_n/frac` Funkcje `sample_n` i `sample_frac` pozwalają nam losować probę z ramki danych. Różnią się tym, że `sample_n` pozwala wylosować $n$ obserwacji, a `sample_frac` pozwala wylosować liczę obserwacji stanowiącą jakiś ułamek wszystkich (w przykładzie - $0.05%$ wszystkich obserwacji. Funkcje te mogą być bardzo przydatne przy pracy z dużymi zbiorami danych. ```{r} sample_n(df, 15) ``` ```{r} sample_frac(df, 0.005) ``` ## `group_by` Funkcja `group_by` odpowiada w moim przekonaniu za całą magię pakietu `dplyr`. Można o niej myśleć jako o funkcji `split` na dopingu. Pozwala ona podobnie jak `split` podzielić ramkę danych ze względu na wartości w jakiejś kolumnie. Jej przewagą jest to, że na obiekcie, który zwraca ta funkcja możemy dokonywać wszystkich operacji, jakich dokonywaliśmy za pomocą pakietu `dplyr` na ramkach danych. W poniższym przykładzie wykorzystaliśmy funkcję `group_by` aby podzielić naszą ramkę danych na na ramki ze wzgledu na płeć naszych zwierzatek. Następnie użyliśmy funkcji `summarise` by uzyskać informacje o średniej wadze ze skorupą, po zdjęciu skorupy i średniej wadze skorupy. Dzięki temu, że użyliśmy jej na obiekcie zwracanym przez funkcję `group_by`, dokonywaliśmy wszystkich operacji osobno na każdej "podramce". ```{r} by_sex <- group_by(df, Sex) weight_means <- summarise(by_sex, count = n(), # specjalna funkcja, która zwraca liczbę obserwacji w.weight.mean = mean(Whole.weight), s.weight.mean = mean(Shucked.weight), v.weight.mean = mean(Shell.weight)) weight_means ``` Tym, co stanowi o sile pakietu `dplyr` jest to, że poszczególne operacje możemy łączyć w łańcuszki. W tym krótkim fragmencie kodu wykonaliśmy następujące operacje (w kolejności): 1. Dodaliśmy nową kolumnę do ramki danych `Age`, która jest liczbą "kręgów" powiększoną o 1.5 2. Pogrupowaliśmy nasze obserwacje ze względu na wiek (kolumna `Age`) 3. Wyliczyliśmy rozstęp międzyćwiartkowy dla wysokości i wagi całkowitej z podziałem na wiek 4. Uporządkowaliśmy uzyskane wyniki malejąco ze względu na kolumny z rozstępami międzyćwiartkowymi wzrostu i wagi (proszę zobaczyć 2 i 3 wiersz - w kolumnie `height.iqr` jest remis, więc kolejność między nimi ustala wartość z kolumny `weight.iqr`. ```{r} arrange( summarise( group_by( mutate( df, Age = Rings+1.5) ,Age), height.iqr = IQR(Height), weight.iqr = IQR(Whole.weight)), desc(height.iqr), desc(weight.iqr)) ``` Tak skonstruowany kod czyta się i pisze jednak dość trudno. Aby zrekonstruować kolejność wykonywanych operacji, musimy zaczać "od środka", co jest niewygodne. Z tego względu preferowanym w `dplyr` sposobem łączenia operacji w łańcuchy jest specjalny operator `%>%`. Cała magia tego operatora polega na tym, że przekazuje argument po lewej stronie jako pierwszy argument funkcji po prawej stronie operatora. Zilustrujmy to na najprostszym przykładzie: ```{r collapse=TRUE} x <- c(1, 2, 4, 3, 5, 3, 2, 1, 5, 6, 1, 0, 12, 3, 4, 6) mean(x) x %>% mean() ``` Wywołaliśmy funkcję `mean` bez żadnego argumentu, ale operator `%>%` pozwolił nam przekazanie lewego operandu jako pierwszego argumentu funkcji stanowiącej prawy operand. Dzięki temu możemy zapisywać łańcuszki funkcji w bardziej czytelny sposób, który odpowiada rzeczywistej kolejności operacji. Poniższy kod robi dokładnie to samo, co kod wyżej, jest jednak bardziej czytelny, ponieważ odpowiada faktycznej kolejności operacji: 1. Dodaliśmy nową kolumnę do ramki danych `Age`, która jest liczbą "kręgów" powiększoną o 1.5 2. Pogrupowaliśmy nasze obserwacje ze względu na wiek (kolumna `Age`) 3. Wyliczyliśmy rozstęp międzyćwiartkowy dla wysokości i wagi całkowitej z podziałem na wiek 4. Uporządkowaliśmy uzyskane wyniki malejąco ze względu na kolumny z rozstępami międzyćwiartkowymi wzrostu i wagi. ```{r} df %>% mutate(Age = Rings+1.5) %>% group_by(Age) %>% summarise(height.iqr = IQR(Height), weight.iqr = IQR(Whole.weight) ) %>% arrange(desc(height.iqr), desc(weight.iqr)) ``` ## `pivot.longer` i `pivot.wider` Całkiem często podczas pracy z danymi będziemy potrzebować zmiany formatu danych. Wyobraźmy sobie sytuację, w której przeprowadziliśmy eksperyment, w którym chcieliśmy zbadać wpływ spożycia kawy na kompetencje matematyczne. W tym celu zarekrutowaliśmy 5 badanych i daliśmy im do rozwiązania test matematyczny trzy razy: przed wypiciem kawy, po wypiciu pierwszej filiżanki oraz po wypiciu dwóch filiżanek. ```{r} data <- data.frame(participant = c(1, 2, 3, 4, 5), gender = c("M", "M", "K", "K", "M"), pre_coffee = c(3, 4, 3, 4, 5), first_coffee = c(5, 4, 4, 3, 6), second_coffee = c(7, 5, 7, 5, 8) ) data ``` Taki format danych czesto nazywany jest w praktyce szerokim (*wide*) formatem danych. W tym formacie jeden wiersz tabeli odpowiada jednemu uczestnikowi eksperymentu (albo badanemu przedmiotowi, etc.). Zwróćmy uwagę, że każdy wiersz zawiera więcej niż jeden pomiar. Wiele funkcji w R wymaga jednak, aby w przekazywanej do niej ramce danych jeden wiersz odpowiadał jednemu pomiarowi. Taki format nazywa się formatem długim (*long*). Za pomocą funkcji `pivot.longer` z biblioteki `tidyr` możemy przekształcić nasza ramkę danych do formatu długiego. Funkcja ta przyjmuje wiele argumentów, wśród których najważniejsze to: - `data` - ramka danych, którą chcemy przetransformować do postaci długiej - `cols` - kolumny, w których znajdują się nasze obserwacje - `names_to` - nazwa nowej kolumny, w której znajdą się nazwy kolumn z obserwacjami - `values_to` - nazwa nowej kolumny, w której znajdą się wartości z kolumn z obserwacjami ```{r} library(tidyr) data_long <- pivot_longer(data = data, cols = c("pre_coffee", "first_coffee", "second_coffee"), names_to = "measurement_point", values_to = "value" ) data_long ``` Operacja ta jest odwracalana. Do przetransformowania formatu danych z długiego na szeroki służy funkcja `pivot_wider`. Aby jej użyć musimy przekazać tej funkcji kilka argumentów: - `data` - ramka danych, którą chcemy przetransformować do postaci szerokiej - `id_cols` - kolumny identyfikujące nową jednostkę obserwacji (tutaj: badanego) - `names_from` - nazwa kolumny, w której znajdują się nazwy poszczególnych kolumn w nowej ramce danych - `values_from` - nazwa kolumny, w których znajdują się wartości poszczególnych pomiarów ```{r} data_wide <- pivot_wider(data = data_long, id_cols = c("participant", "gender"), names_from = "measurement_point", values_from = "value" ) data_wide ```