1 Wstęp

Pakiet dplyr służy do prostego i szybkiego przetwarzania danych. Zalety:

Nazwa:

1.1 Instalowanie i ładowanie pakietu

#install.packages("dplyr") #instalujemy tylko raz
library(dplyr)

1.2 Dane do przykładów i zadań

Do przykładów użyjemy wbudowanych zestawu danych mtcars oraz iris, a także countries z pakietu Przewodnik - zobaczmy co jest w tych danych

head(mtcars)
head(iris)
library(Przewodnik)
head(countries)

1.3 Operatory: -> oraz %>% - pipe

Standardowo, wartości w R przypisujemy za pomocą <- lub =, tzn.:

a1<-4
a2=3.5
a1
## [1] 4
a2
## [1] 3.5

Możliwe jest też użycie operatora “w drugą stronę” ->:

a1->a3
a3
## [1] 4

W pakiecie dplyr mamy dodatkowy operator %>% (pipe operator) do przetwarzania potokowego, to znaczy przekazywania danych z funkcji do funkcji bez pośredniego przypisywania wartości do zmiennych.

Będziemy chcieli wybrać te pojazdy ze zbioru mtcars, które mają moc powyżej 100 hp (KM) a następnie posortować malejąco względem mpg (miles per gallon) tj. “wydajności paliwowej”.

Możemy zrobić to bez operatora pipe ze zmienną pomocniczą:

# beż użycia pipe'a
hp100 = filter(mtcars,  hp>100) #wybranie rekordów z odpowiednim warunkiem
posortowane = arrange(hp100, desc(mpg)) #sortowanie - więcej tych poleceniach później

head(posortowane)

oraz z operatorem pipe:

mtcars %>%
  filter(hp>100) %>% # poszczególne funkcje omówimy w dalszej części laboratorium
  arrange(desc(mpg)) %>%
  head()

gdzie nie tworzymy pośredniej zmiennej. W tym przypadku wynik działania poprzedzającego polecenia staje się pierwszym argumentem kolejnego polecenia.
Stąd też head jest niejako bez jawnego argumentu.

Dobra praktyka zwiększająca czytelność jest taka, by w jednej linijce znajdowało się jedno polecenie.

UWAGA: Jeżeli chcielibyśmy, aby wynik został przekazany dalej jako inny niż pierwszy argument używamy kropki .:

# przygotowanie
a1=rnorm(10)+10 # wylosowanie próbki i dodanie 10
mean(a1) #sprawdźmy średnią
## [1] 10.06038
# drugie losowanie, średnia z a1 zostanie parametrem średniej w drugiej próbce i policzymy średnią (dla sprawdzenia)
# średnia z a1 zostaje przekazana jako drugi argument poprzez kropkę
mean(a1) %>%
  rnorm(1000, mean=., sd=1) %>%
  mean()
## [1] 10.04776

2 Wybrane funkcje z pakietu dplyr

Funkcje pakietu dplyr można podzielić na 5 grup związanych z

2.1 Funkcje grupowania

2.1.1 Funkcja group_by()

Funkcja group_by zazwyczaj nie jest używana samodzielnie, sama w sobie nie zmienia danych. Dodaje pewien znacznik na podstawie wybranego czynnika (lub czynników) i następne operacje będą wykonywane na powstałych w ten sposób grupach

countries %>%
  group_by(continent)%>%  #podzielenie na grupy względem kontynentów
  summarize(mean(birth.rate, na.rm=TRUE)) # policzenie średniej birth.rate na każdym z kontynentów osobno

2.1.2 Funkcja ungroup()

Odwrotne działanie do group_by() ma funkcja ungroup(), np. jeżeli chcemy z powrotem “odzyskać” pierwotną ramkę, po działaniu na grupach:

countries %>%
  group_by(continent)%>%  #podzielenie na grupy względem kontynentów
  mutate(mean.b.r=mean(birth.rate, na.rm=TRUE))%>% #dodanie nowej kolumny, więcej o tej funkcji później
  ungroup() # "odgrupowanie"

2.1.3 Zadanie - iris - max sepal.length

Dla wbudowanej ramki danych iris:

  • policzyć maksimum zmiennej Sepal.Length dla każdego gatunku osobno

  • dodać kolumnę z powyższymi danymi do oryginalnej ramki danych

2.2 Wybór kolumn i filtrowanie

2.2.1 Funkcja select()

Do wyboru lub odrzucania wybranych kolumn z ramki danych służy funkcja select(). Przez nazwy kolumn podajemy te kolumny, które wybieramy, a przez nazwy kolumn poprzedzone minusem - te, które chcemy odrzucić:

# wybrane kolumny
countries%>%
  select(country, population, continent)%>%
  head()
#  odrzucenie kolumny
countries%>%
  select(-death.rate)%>%
  head()

2.2.2 Funkcja filter()

Funkcja filter() służy do wyboru danych (wierszy) spełniających wybrane kryteria. Dla danych liczbowych mamy standardowe operatory <, =<, >, >=, ==, !=. Dla danych tekstowych można zastosować zmienna %in% c(mozliwosc1, mozliwosc2, ...), gdy zmienna ma należeć do danego zbioru “możliwości” lub !zmienna %in% c(mozliwosc1, mozliwosc2, ...) dla zbioru wykluczeń. Warunki można elastycznie łączyć za pomocą logicznych | - “lub”, alternatywa, & - “i”, koniunkcja.

Oczywiście, znaczenie mają nawiasy. Wykonać poniższe 2 kody i wyjaśnić różnice:

# wykonać poniższy kod
countries%>%
  filter(continent %in% c("Asia", "Europe") & (birth.rate>32 | birth.rate<9))
# wykonać poniższy kod. Dlaczego wynik jest różny od powyższego?
countries%>%
  filter((continent %in% c("Asia", "Europe") & birth.rate<10) | birth.rate>45)

UWAGA: można też wymienić warunki “po przecinku” wewnątrz funkcji filter() jako koniunkcję warunków.

2.2.3 Funkcja distinct()

Funkcja distinct() pozwala w prostu sposób na pozbycie się zdublowanych danych. Stwórzmy ramkę:

ramka=data.frame(płeć=sample(c('K', 'M'),20, replace = TRUE),
  ocena=sample(c(2.0, 3.0, 3.5, 4.0, 4.5, 5.0), 20, replace = TRUE))
dim(ramka) # w ramce mamy 20 obserwacji
## [1] 20  2

a teraz usuniemy duplikaty:

ramka%>%
  distinct()%>%
  dim() #teraz obserwacji jest mniej, być może mniej niż 2x6=12 kombinacji
## [1] 11  2

2.2.4 Funkcja arrange()

Funkcja arrange() służy sortowaniu względem wybranego czynnika, przy czym można określić więcej niż jeden czynnik brany pod uwagę w przypadku remisu. Dla porządku malejącego dany czynnik należy umieścić w desc(tutaj)

#przetestować poniższy kod

#utworzenie ramki
ramka=data.frame(płeć=sample(c('K', 'M'),100, replace = TRUE),
ocena=sample(c(2.0, 3.0, 3.5, 4.0, 4.5, 5.0), 100, replace = TRUE),
wzrost=rnorm(100,170,25))


#przykład sortowania ze względu na 2 zmienne
ramka%>%
  arrange(płeć, desc(wzrost))

2.2.5 Zadanie - iris - ciąg poleceń

Ze zbioru iris za pomocą ciągu poleceń:

  • usunąć kolumny Petal.Length i Petal.Width

  • wybrać tylko gatunki setosa i virginica

  • wybrać tylko te obserwacje, gdzie Sepal.Length jest między wartościami 5.1 a 6.4

  • wybrać tylko te obserwacje, gdzie Sepal.Width jest mniejsze niż 3.0 lub większe niż 3.3

  • posortować dane malejąco ze względu na Sepal.Width

2.2.6 Zadanie - iris - unikalne dane

Czy w zbiorze iris wszystkie dane są unikalne (sprawdzić)?

2.3 Tworzenie podsumowań

2.3.1 Funkcja summarize()

Funkcja summarize() lub alternatywnie summarise() pozwala na dostarczanie pewnych metryk, statystyk dla badanego zbioru danych. Często jest używane wraz z group_by() by umożliwić podsumowanie dla każdej podgrupy.

Użycie tej funkcji wymaga określenia nazw nowych kolumn, które zostaną utworzone oraz przekazania do funkcji summarize() danych i funkcji (np min, sd, max) do wykorzystania. Prosty przykład:

countries%>%
  summarize(
    pop_avar=mean(population),#"pop_avar" to nazwa tworzonej zmiennej, "mean" to użyta funkcja
    # a "population" to dane
    pop_max=max(population),
    pop_min=min(population)
  )

oraz z użyciem grupowania:

countries%>%
  group_by(continent)%>%
  summarize(
    pop_avar=mean(population), 
    pop_max=max(population),
    pop_min=min(population)
  )

2.3.2 Funkcja across()

W przypadku, gdy chcemy zastosować serię funkcji (min, max, średnia, odchylenie standardowe itd.) do więcej niż jednej zmiennej możemy użyć funkcji summarize() wielokrotnie. Efektywniejsze jest użycie funkcji across() wewnątrz summarize(). Jej argumentami jest lista funkcji oraz zbiór zmiennych, do których te funkcje mają zostać zastosowane:

countries%>%
  group_by(continent)%>%
  summarize(
    across(
      c(birth.rate, death.rate, population), #zbiór zmiennych
      list(min, max, mean) # lista funkcji (dla jednej funkcji pomijamy `list`)
    )
  )

2.3.3 Funkcja count()

Funkcja count() służy do zliczania liczby obserwacji w grupie

iris%>%
  group_by(Species)%>%
  count()

UWAGA: alternatywnie można użyć funkcji n() wewnątrz summarize() z podobnym efektem (wynik ten sam, trochę dłuższy kod).

2.3.4 Zadanie - iris - podstawowe statystyki

Wyznaczyć podstawowe statystyki (średnia, odchylenie standardowe) dla każdego gatunku w iris dla zmiennej Petal.Width.

2.3.5 Zadanie - iris - podstawowe statystyki wg gatunków

Wyznaczyć podstawowe statystyki (średnia, odchylenie standardowe) dla każdego gatunku w iris dla wszystkich zmiennych ilościowych jednocześnie.

2.3.6 Zadanie - countries - zliczanie

Ile państw ze zbioru danych countries pakietu Przewodnik znajduje się na poszczególnych kontynentach?

2.4 Funkcje związane z przekształcaniem danych

Jest to grupa funkcji służących zmianie istniejących kolumn, dodawaniu nowych oraz zmianie ich nazw.

2.4.1 Funkcja mutate()

# na podstawie birth.rate oraz death.rate możemy wyliczyć przyrost naturalny
countries%>%
  mutate(
    przyr.natur = birth.rate-death.rate
  )%>%
  head() # wypróbować kod i wyświetlić całą ramkę, tj. bez `head`

2.4.2 Funkcja case_when()

Funkcja case_when() pozwala na rozważenie wielu przypadków (coś jak wielokrotny if). Przykładowo, w przypadku gdy przyrost naturalny jest większy od zera, populacja rozwija się, w przypadku ujemnego przyrostu naturalnego mówimy o populacji wymierającej, a dla zera mamy stagnację. Dodamy taką informację do powyższego przykładu:

# na podstawie birth.rate oraz death.rate możemy wyliczyć przyrost naturalny
countries%>%
  mutate(
    przyr.natur = birth.rate-death.rate
  )%>%
  mutate(
    status=case_when(
     przyr.natur> 0.0 ~ "rozwój",
     przyr.natur==0.0 ~ "stagnacja",
     przyr.natur< 0.0 ~ "wymieranie"
    )
  )%>%
  head(n=18) # wypróbować kod i wyświetlić całą ramkę, tj. bez `head`

2.4.3 Funkcja transmute()

Funkcja transmute() jest niejako połączeniem funkcji mutate() tworzącej nowe zmienne oraz select() wybierającej odpowiednie kolumny. Powiedzmy, że chcemy stworzyć zbiór danych zawierający starą zmienną country oraz nowo wyliczony przyrost naturalny przyr.natur:

# na podstawie birth.rate oraz death.rate możemy wyliczyć przyrost naturalny
countries%>%
  transmute(
    country, # ta stara zmienna zostaje
    przyr.natur = birth.rate-death.rate # ta nowa zmienna jest właśnie utworzona
  )%>%
  head() # wypróbować kod i wyświetlić całą ramkę, tj. bez `head`

2.4.4 Funkcja rename()

Funkcja rename() służy do zmiany nazw kolumn. W przykładzie jak wyżej spolszczymy wyraz “country”:

# na podstawie birth.rate oraz death.rate możemy wyliczyć przyrost naturalny
countries%>%
  transmute(
    country, # ta stara zmienna zostaje
    przyr.natur = birth.rate-death.rate # ta nowa zmienna jest właśnie utworzona
  )%>%
  rename(kraj=country)%>%
  head() # wypróbować kod i wyświetlić całą ramkę, tj. bez `head`

UWAGA: podobny efekt można uzyskać za pomocą mutate().

2.4.5 Zadanie - iris - pole powierzchni płatka

W ramce iris zmienne Petal.Length i Petal.Width to odpowiednio długość i szerokość płatka. Przy założeniu, że płatek jest w przybliżeniu elipsą obliczyć jego pole powierzchni (długość i szerokość to osie elipsy). Dodać wynik jako nową zmienną do ramki, a następnie posortować ją względem nowej zmiennej.

2.4.6 Zadanie - iris - klasyfikacja słowna płatków

  1. Do powyższej ramki dodać kolejną kolumnę, która sklasyfikuje “słownie” płatki jako “małe”, “średnie” lub “duże” na podstawie pola powierzchni płatka. Do klasyfikacji użyć można np. kwartyli (małe płatki poniżej pierwszego kwartyla, średnie - pomiędzy pierwszym a trzecim, duże - powyżej trzeciego, kwartyle trzeba wyliczyć np. summary).

  2. Ile jest obserwacji w każdej grupie powstałej w ten sposób klasyfikacji?

2.4.7 Zadanie - iris - redukcja liczby kolumn

Utworzyć ramkę, w której znajdować się będą jedynie informacje dotyczące płatka (length, width, pole powierzchni).

2.4.8 Zadanie - countries - rename

W sekcji dotyczącej funkcji across() automatycznie nadane zostały nazwy kolumn typu birth.rate_1 itd. Zmienić nazwy kolumn na bardziej intuicyjne, np. birth.rate.min itp.

2.5 Łączenie danych - opercje na wierszach danych

Podzielimy ramkę iris na 2 nierozłączne części:

iris_p1=iris[1:100,]
iris_p2=iris[51:150,]

2.5.1 Zadanie - łączenie danych

Jakie jest efekt działania poniższych instrukcji?

# przetestować działanie kolejnych instrukcji
bind_rows(iris_p1,iris_p2)
union(iris_p1,iris_p2)
intersect(iris_p1,iris_p2)
setdiff(iris_p1,iris_p2)
setequal(iris_p1,iris_p2)

2.6 Łączenie danych - dodawanie nowych kolumn z innej ramki danych

2.6.1 Przygotowanie ramek do przykładów

Wczytamy na początku 2 pliki danych w formacie csv. Ponieważ są to dość duże zbiory, zredukujemy liczby kolumn do paru dobrze ilustrujących działanie prezentowanych funkcji. Dane te można pobrać ze strony przedmiotu lub zaimportować z bezpośrednio z www.

Pierwszy zbiór danych dotyczy klientów, drugi zamówień.

clients = read.csv("https://home.agh.edu.pl/~bras/R/clients.csv")
clients
clients%>%
  select(num_client, first, last, job, age, priority)->clients_small
orders = read.csv("https://home.agh.edu.pl/~bras/R/orders.csv")
orders
orders%>%
  select(ref, num_client, first, last, timestamp, state)->orders_small

2.6.2 Funkcja join_left()

Funkcja join_left(x,y) zwraca wszystkie komórki z x nawet jeżeli nie występują w y. Do danych z ‘x’ dopasowywane są dane z ‘y’ za pomocą klucza: w przykładzie by= c("num_client" = "num_client").

Możemy zauważyć, że jeden klient złożył 2 zamówienia, nie wszyscy klienci składali zamówienia (NA w kolumnie ref), jednak na liście znajdują się wszyscy klienci z clients_small:

clients_small %>%
  left_join(orders_small, by = c("num_client" = "num_client")) #lewa strona, tj. "x" zostaje przekazana przez operator %>%

2.6.3 Funkcja join_right()

Działa analogicznie do join_left(), za tym że dane z prawej ramki pozostają, a dopasowywane są te z lewej.

2.6.4 Funkcja inner_join()

Funkcja inner_join() zwraca te dane, które mają swoje odpowiedniki (poprzez klucz by) w obu zbiorach. Liczba wierszy jest teraz istotnie mniejsza, nie ma “pustych” pół w ref:

clients_small %>%
  inner_join(orders_small, by = c("num_client" = "num_client")) #lewa strona, tj. "x" zostaje przekazana przez operator %>%

2.6.5 Funkcja semi_join()

Funkcja semi_join() zwraca elementy lewej ramki (i tylko lewej), które mają odpowiedniki w prawej ramce (poprzez klucz by).

clients_small %>%
  semi_join(orders_small, by = c("num_client" = "num_client")) #lewa strona, tj. "x" zostaje przekazana przez operator %>%

2.6.6 Funkcja anti_join()

Funkcja anti_join() zwraca elementy lewej ramki (i tylko lewej), które nie mają odpowiedników w prawej ramce (poprzez klucz by), jest to niejako dopełnienie semi_join():

clients_small %>%
  anti_join(orders_small, by = c("num_client" = "num_client")) #lewa strona, tj. "x" zostaje przekazana przez operator %>%

2.6.7 Funkcja full_join()

Funkcja full_join() łączy 2 ramki nie robiąc dopasowania. Brakujące wartości oznaczane są przez NA

  full_join(clients_small,orders_small) #tutaj dla odmiany oba argumenty są jawnie użyte, bez operatora %>%