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.

#install.packages('dplyr')
library(dplyr)

Do ćwiczeń wykorzystamy dane pochodzące z badań nad uchowcami. Jak wyglądają te zwierzatka? Tak:

Uchowiec

Informacje na temat danych mogą Państwo znaleźć tutaj: http://archive.ics.uci.edu/ml/datasets/Abalone

Wczytajmy dane i zobaczmy jak one wyglądają za pomocą head.

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)
##   Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1   M  0.455    0.365  0.095       0.5140         0.2245         0.1010
## 2   M  0.350    0.265  0.090       0.2255         0.0995         0.0485
## 3   F  0.530    0.420  0.135       0.6770         0.2565         0.1415
## 4   M  0.440    0.365  0.125       0.5160         0.2155         0.1140
## 5   I  0.330    0.255  0.080       0.2050         0.0895         0.0395
## 6   I  0.425    0.300  0.095       0.3515         0.1410         0.0775
##   Shell.weight Rings
## 1        0.150    15
## 2        0.070     7
## 3        0.210     9
## 4        0.155    10
## 5        0.055     7
## 6        0.120     8

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”.

filter(df, Sex == 'M', Rings > 22)
##   Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1   M  0.600    0.495  0.195       1.0575         0.3840         0.1900
## 2   M  0.630    0.485  0.175       1.3000         0.4335         0.2945
## 3   M  0.665    0.535  0.225       2.1835         0.7535         0.3910
## 4   M  0.610    0.490  0.150       1.1030         0.4250         0.2025
## 5   M  0.515    0.400  0.160       0.8175         0.2515         0.1560
## 6   M  0.690    0.540  0.185       1.6195         0.5330         0.3530
##   Shell.weight Rings
## 1        0.375    26
## 2        0.460    23
## 3        0.885    27
## 4        0.360    23
## 5        0.300    23
## 6        0.555    24

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:

filter(df, Sex == 'M' & Rings > 22)
##   Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1   M  0.600    0.495  0.195       1.0575         0.3840         0.1900
## 2   M  0.630    0.485  0.175       1.3000         0.4335         0.2945
## 3   M  0.665    0.535  0.225       2.1835         0.7535         0.3910
## 4   M  0.610    0.490  0.150       1.1030         0.4250         0.2025
## 5   M  0.515    0.400  0.160       0.8175         0.2515         0.1560
## 6   M  0.690    0.540  0.185       1.6195         0.5330         0.3530
##   Shell.weight Rings
## 1        0.375    26
## 2        0.460    23
## 3        0.885    27
## 4        0.360    23
## 5        0.300    23
## 6        0.555    24

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ą.

filter(df, (Sex == 'M' | Sex == 'F') & Height > 0.24 & Rings >= 10)
##   Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1   M  0.705    0.565  0.515       2.2100         1.1075         0.4865
## 2   F  0.815    0.650  0.250       2.2550         0.8905         0.4200
## 3   M  0.775    0.630  0.250       2.7795         1.3485         0.7600
## 4   F  0.595    0.470  0.250       1.2830         0.4620         0.2475
##   Shell.weight Rings
## 1       0.5120    10
## 2       0.7975    14
## 3       0.5780    12
## 4       0.4450    14

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ę.

head(arrange(df, desc(Rings), desc(Length), Sex), 15)
##    Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1    F  0.700    0.585  0.185       1.8075         0.7055         0.3215
## 2    M  0.665    0.535  0.225       2.1835         0.7535         0.3910
## 3    F  0.550    0.465  0.180       1.2125         0.3245         0.2050
## 4    M  0.600    0.495  0.195       1.0575         0.3840         0.1900
## 5    F  0.645    0.490  0.215       1.4060         0.4265         0.2285
## 6    F  0.700    0.540  0.215       1.9780         0.6675         0.3125
## 7    M  0.690    0.540  0.185       1.6195         0.5330         0.3530
## 8    F  0.800    0.630  0.195       2.5260         0.9330         0.5900
## 9    M  0.630    0.485  0.175       1.3000         0.4335         0.2945
## 10   F  0.620    0.470  0.200       1.2255         0.3810         0.2700
## 11   F  0.620    0.520  0.225       1.1835         0.3780         0.2700
## 12   M  0.610    0.490  0.150       1.1030         0.4250         0.2025
## 13   F  0.550    0.415  0.135       0.7750         0.3020         0.1790
## 14   M  0.515    0.400  0.160       0.8175         0.2515         0.1560
## 15   F  0.490    0.385  0.150       0.7865         0.2410         0.1400
##    Shell.weight Rings
## 1         0.475    29
## 2         0.885    27
## 3         0.525    27
## 4         0.375    26
## 5         0.510    25
## 6         0.710    24
## 7         0.555    24
## 8         0.620    23
## 9         0.460    23
## 10        0.435    23
## 11        0.395    23
## 12        0.360    23
## 13        0.260    23
## 14        0.300    23
## 15        0.240    23

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.

head(select(df, Sex, Length, Diameter))
##   Sex Length Diameter
## 1   M  0.455    0.365
## 2   M  0.350    0.265
## 3   F  0.530    0.420
## 4   M  0.440    0.365
## 5   I  0.330    0.255
## 6   I  0.425    0.300

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.

head(select(df, Sex, contains('weight')))
##   Sex Whole.weight Shucked.weight Viscera.weight Shell.weight
## 1   M       0.5140         0.2245         0.1010        0.150
## 2   M       0.2255         0.0995         0.0485        0.070
## 3   F       0.6770         0.2565         0.1415        0.210
## 4   M       0.5160         0.2155         0.1140        0.155
## 5   I       0.2050         0.0895         0.0395        0.055
## 6   I       0.3515         0.1410         0.0775        0.120

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.

head(select(df, Sex, matches('.*ght')))
##   Sex Height Whole.weight Shucked.weight Viscera.weight Shell.weight
## 1   M  0.095       0.5140         0.2245         0.1010        0.150
## 2   M  0.090       0.2255         0.0995         0.0485        0.070
## 3   F  0.135       0.6770         0.2565         0.1415        0.210
## 4   M  0.125       0.5160         0.2155         0.1140        0.155
## 5   I  0.080       0.2050         0.0895         0.0395        0.055
## 6   I  0.095       0.3515         0.1410         0.0775        0.120

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.

head(select(df, -(Whole.weight:Shell.weight)))
##   Sex Length Diameter Height Rings
## 1   M  0.455    0.365  0.095    15
## 2   M  0.350    0.265  0.090     7
## 3   F  0.530    0.420  0.135     9
## 4   M  0.440    0.365  0.125    10
## 5   I  0.330    0.255  0.080     7
## 6   I  0.425    0.300  0.095     8

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)

nrow(distinct(df, Sex, Rings))
## [1] 68
head(distinct(df, Sex, Rings), 15)
##    Sex Rings
## 1    M    15
## 2    M     7
## 3    F     9
## 4    M    10
## 5    I     7
## 6    I     8
## 7    F    20
## 8    F    16
## 9    M     9
## 10   F    19
## 11   F    14
## 12   M    11
## 13   F    10
## 14   M    12
## 15   I    10

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.

head(mutate(df,
            Age = Rings + 1.5,
            Height.to.weight.ratio = Height/Whole.weight,
            Body.mass.ratio = sqrt(Height*Diameter)/Whole.weight))
##   Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1   M  0.455    0.365  0.095       0.5140         0.2245         0.1010
## 2   M  0.350    0.265  0.090       0.2255         0.0995         0.0485
## 3   F  0.530    0.420  0.135       0.6770         0.2565         0.1415
## 4   M  0.440    0.365  0.125       0.5160         0.2155         0.1140
## 5   I  0.330    0.255  0.080       0.2050         0.0895         0.0395
## 6   I  0.425    0.300  0.095       0.3515         0.1410         0.0775
##   Shell.weight Rings  Age Height.to.weight.ratio Body.mass.ratio
## 1        0.150    15 16.5              0.1848249       0.3622806
## 2        0.070     7  8.5              0.3991131       0.6848534
## 3        0.210     9 10.5              0.1994092       0.3517247
## 4        0.155    10 11.5              0.2422481       0.4139537
## 5        0.055     7  8.5              0.3902439       0.6967247
## 6        0.120     8  9.5              0.2702703       0.4802829

Funkcja transmute różni się od mutate tylko tym, że zwraca wyłacznie nowododane kolumny:

head(transmute(df,
            Age = Rings + 1.5,
            Height.to.weight.ratio = Height/Whole.weight,
            Body.mass.ratio = sqrt(Height*Diameter)/Whole.weight))
##    Age Height.to.weight.ratio Body.mass.ratio
## 1 16.5              0.1848249       0.3622806
## 2  8.5              0.3991131       0.6848534
## 3 10.5              0.1994092       0.3517247
## 4 11.5              0.2422481       0.4139537
## 5  8.5              0.3902439       0.6967247
## 6  9.5              0.2702703       0.4802829

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).

summarise(df,
          ring_mean = mean(Rings),
          wieight_sd = sd(Whole.weight))
##   ring_mean wieight_sd
## 1  9.933684   0.490389

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.

sample_n(df, 15)
##    Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1    F  0.460    0.355  0.130       0.5170         0.2205         0.1140
## 2    M  0.655    0.485  0.195       1.6200         0.6275         0.3580
## 3    F  0.630    0.510  0.185       1.2350         0.5115         0.3490
## 4    F  0.585    0.460  0.165       1.0580         0.4860         0.2500
## 5    M  0.640    0.510  0.170       1.3715         0.5670         0.3070
## 6    I  0.545    0.430  0.140       0.7720         0.2890         0.1900
## 7    M  0.420    0.340  0.115       0.4215         0.1750         0.0930
## 8    F  0.620    0.480  0.165       1.0125         0.5325         0.4365
## 9    F  0.595    0.465  0.150       1.0255         0.4120         0.2745
## 10   F  0.640    0.500  0.170       1.5175         0.6930         0.3260
## 11   M  0.530    0.420  0.165       0.8945         0.3190         0.2390
## 12   I  0.360    0.250  0.115       0.4650         0.2100         0.1055
## 13   I  0.415    0.325  0.115       0.3285         0.1405         0.0510
## 14   F  0.525    0.410  0.115       0.7745         0.4160         0.1630
## 15   M  0.505    0.400  0.125       0.7700         0.2735         0.1590
##    Shell.weight Rings
## 1        0.1650     9
## 2        0.4850    17
## 3        0.3065    11
## 4        0.2940     9
## 5        0.4090    10
## 6        0.2615     8
## 7        0.1350     8
## 8        0.3240    10
## 9        0.2890    11
## 10       0.4090    11
## 11       0.2450    11
## 12       0.1280     7
## 13       0.1060    12
## 14       0.1800     7
## 15       0.2550    13
sample_frac(df, 0.005)
##    Sex Length Diameter Height Whole.weight Shucked.weight Viscera.weight
## 1    F  0.560    0.445  0.180       0.9030         0.3575         0.2045
## 2    I  0.375    0.280  0.090       0.2150         0.0840         0.0600
## 3    M  0.505    0.390  0.115       0.5585         0.2575         0.1190
## 4    F  0.650    0.475  0.165       1.3875         0.5800         0.3485
## 5    F  0.615    0.475  0.155       1.0040         0.4475         0.1930
## 6    I  0.420    0.305  0.110       0.2800         0.0940         0.0785
## 7    F  0.570    0.450  0.180       0.9080         0.4015         0.2170
## 8    M  0.535    0.420  0.130       0.8055         0.3010         0.1810
## 9    F  0.510    0.395  0.120       0.6175         0.2620         0.1220
## 10   M  0.625    0.500  0.140       1.0960         0.5445         0.2165
## 11   I  0.545    0.430  0.140       0.6870         0.2615         0.1405
## 12   M  0.505    0.390  0.120       0.6530         0.3315         0.1385
## 13   I  0.455    0.325  0.135       0.8200         0.4005         0.1715
## 14   F  0.625    0.445  0.160       1.0900         0.4600         0.2965
## 15   F  0.700    0.550  0.170       1.6840         0.7535         0.3265
## 16   I  0.610    0.480  0.165       1.0970         0.4215         0.2640
## 17   M  0.630    0.485  0.160       1.2430         0.6230         0.2750
## 18   M  0.695    0.545  0.185       1.5715         0.6645         0.3835
## 19   M  0.625    0.500  0.195       1.3690         0.5875         0.2185
## 20   M  0.690    0.550  0.180       1.6915         0.6655         0.4020
## 21   M  0.570    0.480  0.175       1.1850         0.4740         0.2610
##    Shell.weight Rings
## 1        0.2950     9
## 2        0.0550     6
## 3        0.1535     8
## 4        0.3095     9
## 5        0.2895    10
## 6        0.0955     9
## 7        0.2550     9
## 8        0.2800    14
## 9        0.1930    12
## 10       0.2950    10
## 11       0.2500     9
## 12       0.1670     9
## 13       0.2110     8
## 14       0.3040    11
## 15       0.3200    11
## 16       0.3350    13
## 17       0.3000    10
## 18       0.4505    13
## 19       0.3700    17
## 20       0.5000    11
## 21       0.3800    11

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”.

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
## # A tibble: 3 × 5
##   Sex   count w.weight.mean s.weight.mean v.weight.mean
##   <chr> <int>         <dbl>         <dbl>         <dbl>
## 1 F      1307         1.05          0.446         0.302
## 2 I      1342         0.431         0.191         0.128
## 3 M      1528         0.991         0.433         0.282

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.
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))
## # A tibble: 28 × 3
##      Age height.iqr weight.iqr
##    <dbl>      <dbl>      <dbl>
##  1  24.5     0.045       0.409
##  2  23.5     0.0425      0.916
##  3  14.5     0.0425      0.601
##  4  18.5     0.04        0.554
##  5  17.5     0.04        0.532
##  6  13.5     0.0400      0.675
##  7  11.5     0.0400      0.537
##  8  15.5     0.0400      0.535
##  9  16.5     0.0400      0.495
## 10  22.5     0.0363      0.584
## # … with 18 more rows
## # ℹ Use `print(n = ...)` to see more rows

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:

x <- c(1, 2, 4, 3, 5, 3, 2, 1, 5, 6, 1, 0, 12, 3, 4, 6)
mean(x)
## [1] 3.625
x %>% mean()
## [1] 3.625

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.
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))
## # A tibble: 28 × 3
##      Age height.iqr weight.iqr
##    <dbl>      <dbl>      <dbl>
##  1  24.5     0.045       0.409
##  2  23.5     0.0425      0.916
##  3  14.5     0.0425      0.601
##  4  18.5     0.04        0.554
##  5  17.5     0.04        0.532
##  6  13.5     0.0400      0.675
##  7  11.5     0.0400      0.537
##  8  15.5     0.0400      0.535
##  9  16.5     0.0400      0.495
## 10  22.5     0.0363      0.584
## # … with 18 more rows
## # ℹ Use `print(n = ...)` to see more rows

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.

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
##   participant gender pre_coffee first_coffee second_coffee
## 1           1      M          3            5             7
## 2           2      M          4            4             5
## 3           3      K          3            4             7
## 4           4      K          4            3             5
## 5           5      M          5            6             8

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
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
## # A tibble: 15 × 4
##    participant gender measurement_point value
##          <dbl> <chr>  <chr>             <dbl>
##  1           1 M      pre_coffee            3
##  2           1 M      first_coffee          5
##  3           1 M      second_coffee         7
##  4           2 M      pre_coffee            4
##  5           2 M      first_coffee          4
##  6           2 M      second_coffee         5
##  7           3 K      pre_coffee            3
##  8           3 K      first_coffee          4
##  9           3 K      second_coffee         7
## 10           4 K      pre_coffee            4
## 11           4 K      first_coffee          3
## 12           4 K      second_coffee         5
## 13           5 M      pre_coffee            5
## 14           5 M      first_coffee          6
## 15           5 M      second_coffee         8

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
data_wide <- pivot_wider(data = data_long,
                         id_cols = c("participant", "gender"),
                         names_from = "measurement_point",
                         values_from = "value"
                         )
data_wide
## # A tibble: 5 × 5
##   participant gender pre_coffee first_coffee second_coffee
##         <dbl> <chr>       <dbl>        <dbl>         <dbl>
## 1           1 M               3            5             7
## 2           2 M               4            4             5
## 3           3 K               3            4             7
## 4           4 K               4            3             5
## 5           5 M               5            6             8