16  Transformace a sumarizace více proměnných

V předchozích dvou kapitolách jsme si představili, jak transformovat a sumarizovat proměnné. Vždy jsme však pracovali maximálně s jednou nebo dvěma proměnnými najednou. V praxi ovšem nejsou neobvyklé situace, ve kterých je nutné aplikovat určitou funkci na desítky, ne-li stovky proměnných najednou. Naštěstí pro nás, Tidyverse pro tyto příložitosti nabízí funkci across().

16.1 Transformace většího množství proměnných

Dataset countries obsahuje několik kategoriálních proměnných, mezi nimi postsoviet, eu_member, maj_belief a di_cat. Všechny tyto proměnné jsou typu character, pro analýzu by ovšem bylo lepší je převést na typ factor (pro typy objektů viz Kapitola 4).

Jednou možností je aplikovat funkci as.factor() na každou proměnnou zvlášť:

countries %>% 
  mutate(postsoviet = as.factor(postsoviet),
         eu_member  = as.factor(eu_member),
         maj_belief = as.factor(maj_belief),
         di_cat     = as.factor(di_cat)) %>% 
  select(postsoviet, eu_member, maj_belief, di_cat) %>% 
  head(5)
# A tibble: 5 × 4
  postsoviet eu_member maj_belief    di_cat          
  <fct>      <fct>     <fct>         <fct>           
1 no         yes       catholic      Flawed democracy
2 yes        yes       orthodox      Flawed democracy
3 yes        yes       nonbelief     Flawed democracy
4 no         yes       protestantism Full democracy  
5 yes        yes       catholic      Full democracy  

Tímto kódem dosáhneme našeho cíle, nejedná se však o nejelegantnější řešení, jelikož opakovaně aplikujeme stejnou funkci na každou z proměnných zvlášť. To nejen náš kód prodlužuje, ale zároveň zvyšuje šanci, že na některém řádku uděláme chybu. Alternativou je funkce across():

countries %>% 
  mutate(across(.cols = c(postsoviet, eu_member, maj_belief, di_cat),
                .fns  = as.factor)) %>% 
  select(postsoviet, eu_member, maj_belief, di_cat) %>% 
  head(5)
# A tibble: 5 × 4
  postsoviet eu_member maj_belief    di_cat          
  <fct>      <fct>     <fct>         <fct>           
1 no         yes       catholic      Flawed democracy
2 yes        yes       orthodox      Flawed democracy
3 yes        yes       nonbelief     Flawed democracy
4 no         yes       protestantism Full democracy  
5 yes        yes       catholic      Full democracy  

Funkce across() má dva nezbytné argumenty. Prvním z nich je .col, pomocí kterého specifikujeme proměnné, na které chceme naši funkci aplikovat. Argument .fns poté specifikuje funkci samotnou. Výsledek je stejný jako v předchozím případě, náš kód je ale kompaktnější.

Tímto ovšem výhody funkce across() nekončí. Proměnné je v ní možné specifikovat nejen tím, že je vyjmenuje jednu po druhé, ale i pomocí pomocných funkcí, se kterými jsme se již setkali v kapitole věnované výběrů sloupců (Sekce 10.2).

Pokud bychom například chtěli zaokrouhlit všechny numerické proměnné v datasetu na dvě desetinná místa, není nutné jejich názvy vypisovat ručně. Stačí využít kombinace funkcí where() a is.numeric():

countries %>% 
  mutate(across(.cols = where(is.numeric),
                .fns  = round, 2)) %>% 
  select(where(is.numeric)) %>% 
  head(5)
# A tibble: 5 × 9
       gdp population   area life_exp uni_prc poverty_risk mater…¹   hdi dem_i…²
     <dbl>      <dbl>  <dbl>    <dbl>   <dbl>        <dbl>   <dbl> <dbl>   <dbl>
1  450506.   11398589  30528     81.2    0.36         0.2     0.11  0.92    7.78
2   55182.    7050034 110879     74.8    0.25         0.39    0.44  0.81    7.03
3  207772.   10610055  78867     79.2    0.22         0.12    0.1   0.89    7.69
4  298276.    5781190  43094     81.2    0.33         0.17    0.07  0.93    9.22
5 3386000    82792351 357022     81      0.25         0.19    0.09  0.94    8.68
# … with abbreviated variable names ¹​material_dep, ²​dem_index

V rámci across() je také možné specifikovat argumenty pro aplikovanou funkci. Výše jsem určili, že numerické proměnné mají být zaoukrouhlené na dvě desetinná místa pomocí .fns = round, 2, kde 2 je argument funkce round(). Alternativně bychom mohli využít takzvanou tilda notaci (tilde notation):

countries %>% 
  mutate(across(.cols = where(is.numeric),
                .fns  = ~round(., 2))) %>% 
  select(where(is.numeric)) %>% 
  head(5)
# A tibble: 5 × 9
       gdp population   area life_exp uni_prc poverty_risk mater…¹   hdi dem_i…²
     <dbl>      <dbl>  <dbl>    <dbl>   <dbl>        <dbl>   <dbl> <dbl>   <dbl>
1  450506.   11398589  30528     81.2    0.36         0.2     0.11  0.92    7.78
2   55182.    7050034 110879     74.8    0.25         0.39    0.44  0.81    7.03
3  207772.   10610055  78867     79.2    0.22         0.12    0.1   0.89    7.69
4  298276.    5781190  43094     81.2    0.33         0.17    0.07  0.93    9.22
5 3386000    82792351 357022     81      0.25         0.19    0.09  0.94    8.68
# … with abbreviated variable names ¹​material_dep, ²​dem_index

Na rozdíl od předchozího příkladu, funkci round() zde předchazí tilda (~) a prvním argumentem je .. Tato tečka (.) slouží jako placeholder pro proměnné dosazované do funkce round(). Jinak řečeno, funkce across() postupně dosadí každou proměnnou specifikovanou pomocí argumentu .cols na místo placeholderu .. Tilda notace je o něco komplexnější, než předchozí způsob, je ale o mnoho flexibilnější, protože nám umožňuje kontrolovat, do kterého argumentu budou námi proměnné dosazeny.

16.2 Sumarizace většího množsví proměnných

Funkci across() je možné aplikovat v rámci summarise() identicky jako v případě mutate(). Toho využijeme primárně pro výpočet deskriptivních statistik. Stejně jako v předchozích kapitál, i zde můžeme můžeme funkce navazovat na sebe:

countries %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = mean, na.rm = TRUE)) %>% 
  mutate(across(.cols = everything(),
                .fns = round, 2))
# A tibble: 1 × 9
      gdp population    area life_exp uni_prc poverty_risk mater…¹   hdi dem_i…²
    <dbl>      <dbl>   <dbl>    <dbl>   <dbl>        <dbl>   <dbl> <dbl>   <dbl>
1 484601.   16754743 156019.     79.6    0.29         0.24    0.18  0.87    7.64
# … with abbreviated variable names ¹​material_dep, ²​dem_index

Všimněme si, že při výpočtu průměru numerických proměnných bylo nutné odstranit chybějící proměnné pomocí na.rm = TRUE (s tímto argumentem jsme se již setkali, viz Sekce 6.2). Všechny získané průměry jsme poté zaokrouhlili pomocí mutate().

Výsledkem jsou data v širokém formátu (Kapitola 12). Pro čitelnost bude lepší je převést do formátu dlouhého:

countries %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = mean, na.rm = TRUE)) %>% 
  mutate(across(.cols = everything(),
                .fns = round, 2)) %>% 
  pivot_longer(cols = everything(),
               names_to = "variable",
               values_to = "mean")  
# A tibble: 9 × 2
  variable            mean
  <chr>              <dbl>
1 gdp            484601.  
2 population   16754743   
3 area           156019.  
4 life_exp           79.6 
5 uni_prc             0.29
6 poverty_risk        0.24
7 material_dep        0.18
8 hdi                 0.87
9 dem_index           7.64

16.3 Analýza po skupinách

Stejně v předchozích kapitolách, i zde můžeme aplikovat funkci group_by() pro skupinovou sumarizaci (a transformaci) dat:

countries %>% 
  group_by(eu_member) %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = mean, na.rm = TRUE)) %>% 
  mutate(across(.cols = -eu_member,
                .fns = round, 2))
# A tibble: 2 × 10
  eu_member     gdp popul…¹   area life_…² uni_prc pover…³ mater…⁴   hdi dem_i…⁵
  <chr>       <dbl>   <dbl>  <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <dbl>   <dbl>
1 no        152949.  1.19e7 1.45e5    78.6    0.27    0.31    0.26  0.84    6.87
2 yes       567514.  1.83e7 1.60e5    79.9    0.3     0.23    0.17  0.88    7.89
# … with abbreviated variable names ¹​population, ²​life_exp, ³​poverty_risk,
#   ⁴​material_dep, ⁵​dem_index

Výsledkem je dataframe, který sumarizuje numerické proměnné pro západní a postsovětské země zvlášť. Všimněme si, že při zaokrouhlování je nutné funkci round() aplikovat na všechny proměnné s vyjímkou proměnné eu_member.

Stejně jako v předchozí sekci, i zde je pro čitelnost lepší převést data do dlouhého formátu. Výsledkem bude dataset vhodný pro vizualizaci nebo statistické modelování:

countries %>% 
  group_by(eu_member) %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = mean, na.rm = TRUE)) %>% 
  mutate(across(.cols = -eu_member,
                .fns = round, 2)) %>% 
  pivot_longer(cols = -eu_member,
               names_to = "variable",
               values_to = "mean")
# A tibble: 18 × 3
   eu_member variable            mean
   <chr>     <chr>              <dbl>
 1 no        gdp            152949.  
 2 no        population   11949585.  
 3 no        area           144874.  
 4 no        life_exp           78.6 
 5 no        uni_prc             0.27
 6 no        poverty_risk        0.31
 7 no        material_dep        0.26
 8 no        hdi                 0.84
 9 no        dem_index           6.87
10 yes       gdp            567514.  
11 yes       population   18299258.  
12 yes       area           159999.  
13 yes       life_exp           79.9 
14 yes       uni_prc             0.3 
15 yes       poverty_risk        0.23
16 yes       material_dep        0.17
17 yes       hdi                 0.88
18 yes       dem_index           7.89

Na rozdíl od počítače, lidským čtenářům tento formát zpravdila nepřijde příliš přirozený. Ideálně proto data převedeme zpět do širšího formátu, abychom mohli jednoduše porovnat, která skupina zemí si vede lépe:

countries %>% 
  group_by(eu_member) %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = mean, na.rm = TRUE)) %>% 
  mutate(across(.cols = -eu_member,
                .fns = round, 2)) %>% 
  pivot_longer(cols = -eu_member,
               names_to = "variable",
               values_to = "mean") %>% 
  pivot_wider(names_from = eu_member,
              values_from = mean) %>% 
  mutate(difference = no - yes)
# A tibble: 9 × 4
  variable              no         yes    difference
  <chr>              <dbl>       <dbl>         <dbl>
1 gdp            152949.     567514.    -414565.    
2 population   11949585.   18299258.   -6349673.    
3 area           144874.     159999.     -15124.    
4 life_exp           78.6        79.9        -1.31  
5 uni_prc             0.27        0.3        -0.0300
6 poverty_risk        0.31        0.23        0.08  
7 material_dep        0.26        0.17        0.09  
8 hdi                 0.84        0.88       -0.0400
9 dem_index           6.87        7.89       -1.02  

16.4 Sumarizace více proměnných bez použití across()

Přestože je kombinace funkcí summarise() a across() velmi užitečná, výsledný dataset není vždy ve formátu, se kterým je jednoduché dále pracovat, zvláště pokud aplikujeme více než jednu funkci najednou. Existuje ovšem trik, využívající převedu mezi širokým a dlouhým formátem, kterým si můžeme práci ulehčit.

Naším cílem může být spočítat průměr, směrodatnou odchylku, maximum a minimum všech numerickách proměnných. Jednou variantou je aplikovat funkci across() a specifikovat více funkcí pomocí lst(). Tato funkce umožňuje aplikovat několik funkcí v rámci jednoho across() Výsledek ovšem není příliš vzhledný:

countries %>% 
  summarise(across(.cols = where(is.numeric),
                   .fns  = lst(mean, sd, min, max), na.rm = TRUE))
# A tibble: 1 × 36
  gdp_m…¹ gdp_sd gdp_min gdp_max popul…² popul…³ popul…⁴ popul…⁵ area_…⁶ area_sd
    <dbl>  <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
1 484601. 7.94e5  10735. 3386000  1.68e7  2.41e7   38114  8.28e7 156019. 189008.
# … with 26 more variables: area_min <dbl>, area_max <dbl>,
#   life_exp_mean <dbl>, life_exp_sd <dbl>, life_exp_min <dbl>,
#   life_exp_max <dbl>, uni_prc_mean <dbl>, uni_prc_sd <dbl>,
#   uni_prc_min <dbl>, uni_prc_max <dbl>, poverty_risk_mean <dbl>,
#   poverty_risk_sd <dbl>, poverty_risk_min <dbl>, poverty_risk_max <dbl>,
#   material_dep_mean <dbl>, material_dep_sd <dbl>, material_dep_min <dbl>,
#   material_dep_max <dbl>, hdi_mean <dbl>, hdi_sd <dbl>, hdi_min <dbl>, …

Výsledek není nepoužitelný, abychom se ovšem dostali k čitelné tabulce, museli bychom několikrát využít převodu mezi širokým a dlouhým formátem.

Alternativním způsobem je vybrat proměnné, se kterými chceme pracovat, převést data do dlouhého formátu a poté již aplikovat klasickou funkci summarise(). Nakonec jen výsledky zaokrouhlíme:

countries %>% 
  select(where(is.numeric)) %>% 
  pivot_longer(everything()) %>% 
  group_by(name) %>% 
  summarise(mean = mean(value, na.rm = TRUE),
            sd   = sd(value, na.rm = TRUE),
            min  = min(value, na.rm = TRUE),
            max  = max(value, na.rm = TRUE)) %>% 
  mutate(across(.cols = where(is.numeric),
                .fns = round, 2))
# A tibble: 9 × 5
  name                mean          sd      min         max
  <chr>              <dbl>       <dbl>    <dbl>       <dbl>
1 area           156019.     189008.     160      783562   
2 dem_index           7.64        1.3      4.37        9.87
3 gdp            484601.     793693.   10735.    3386000   
4 hdi                 0.87        0.05     0.76        0.95
5 life_exp           79.6         2.82    74.8        83.3 
6 material_dep        0.18        0.13     0.04        0.48
7 population   16754743    24110721.   38114    82792351   
8 poverty_risk        0.24        0.08     0.12        0.42
9 uni_prc             0.29        0.08     0.16        0.41

Hlavní výhodou této metody je, kromě podle našeho názoru větší přehlednosti, že umožňuje specifikovat argumenty pro každou statistickou funkci zvlášť.