20장 purr와 broom 를 이용한 많은 모델

20.1 들어가기

  • 이 장에서는 많은 양의 모델을 쉽게 작업할 수 있도록 하는 세 가지 아이디어에 대해 학습할 것이다.

  • 간단한 모델을 여러 개 사용하여 복잡한 데이터셋을 잘 이해 한다.

-리스트-열(리스트 형식의 열)을 사용하여 임의의 데이터 구조를 데이터프레임에 저장한다. 예를 들어 리스트-열 선형 모델을 포함하는 열을 가질 수 있다.

20.1 준비하기

  • 많은 모델을 작업하려먼 (데이터 탐색, 정제 및 프로그래밍을 위한)tidyverse의 많은 패키지와 모델링을 위한 modelr 패키지가 필요하다.
library(modelr)
library(tidyverse)

20.2 gapminder 데이터

  • 많은 수의 간단한 모델의 위력을 보여주기 위해 ‘gapminder’ 데이터를 살펴볼 것 이다.
library(gapminder)
gapminder
## # A tibble: 1,704 x 6
##    country     continent  year lifeExp      pop gdpPercap
##    <fct>       <fct>     <int>   <dbl>    <int>     <dbl>
##  1 Afghanistan Asia       1952    28.8  8425333      779.
##  2 Afghanistan Asia       1957    30.3  9240934      821.
##  3 Afghanistan Asia       1962    32.0 10267083      853.
##  4 Afghanistan Asia       1967    34.0 11537966      836.
##  5 Afghanistan Asia       1972    36.1 13079460      740.
##  6 Afghanistan Asia       1977    38.4 14880372      786.
##  7 Afghanistan Asia       1982    39.9 12881816      978.
##  8 Afghanistan Asia       1987    40.8 13867957      852.
##  9 Afghanistan Asia       1992    41.7 16317921      649.
## 10 Afghanistan Asia       1997    41.8 22227415      635.
## # ... with 1,694 more rows

이 사례 연구에서는 ’각 국가(country)별 기대 수명(lifeEXP)은 시간(year)에 따라 어떻게 변하는가’라는 질문에 대답하기 위해 세 가지 변수에 초점을 맞출 것이다.

gapminder %>%
  ggplot(aes(year, lifeExp, group = country)) +
  geom_line(alpha = 1/3)

  • 이 데이터는 약 1,700개의 관측값과 3 개의 변수로 이루어진 작은 데이터 셋이다. 그렇지만 무슨일이 일어나고 있는지 파악하는 것은 여전히 어렵다.
nz <- filter(gapminder, country == "New Zealand")
nz %>%
  ggplot(aes(year,lifeExp)) +
  geom_line() +
  ggtitle("Full data = ")

nz_mod <- lm(lifeExp ~ year, data = nz)
nz %>%
  add_predictions(nz_mod) %>%
  ggplot(aes(year, pred)) +
  geom_line() +
  ggtitle("Linear trend + ")

nz %>%
  add_residuals(nz_mod) %>%
  ggplot(aes(year, resid)) +
  geom_hline(yintercept = 0, color = "white",
  size = 3) +
  geom_line() +
  ggtitle("Remaining pattern")

  • 어떻게 하면 모든 국가에 대해 손쉽게 모델을 적합할 수 있겠는가?

20.2.1 중첩된 데이터 (Nested Data)

  • 위 코드를 여러 번 복사하여 붙여넣는 것을 생각해 볼 수 있다. -함수로 공통 코드를 추출하고 purr의 map 함수를 사용하여 반복해보자.
by_country <- gapminder %>%
  
  group_by(country, continent) %>%
 
   nest()
by_country %>% head 
## # A tibble: 6 x 3
## # Groups:   country, continent [6]
##   country     continent data             
##   <fct>       <fct>     <list>           
## 1 Afghanistan Asia      <tibble [12 x 4]>
## 2 Albania     Europe    <tibble [12 x 4]>
## 3 Algeria     Africa    <tibble [12 x 4]>
## 4 Angola      Africa    <tibble [12 x 4]>
## 5 Argentina   Americas  <tibble [12 x 4]>
## 6 Australia   Oceania   <tibble [12 x 4]>

-(여기서는 continent와 country를 그룹화는 약간의 편법을 사용한다. country에 대해 continent는 고정되므로 더 이상의 그붑은 추가하지 않으면거 추가 변수를 가져갈 수 있는 쉬운방법이다.)

by_country$data[[1]]
## # A tibble: 12 x 4
##     year lifeExp      pop gdpPercap
##    <int>   <dbl>    <int>     <dbl>
##  1  1952    28.8  8425333      779.
##  2  1957    30.3  9240934      821.
##  3  1962    32.0 10267083      853.
##  4  1967    34.0 11537966      836.
##  5  1972    36.1 13079460      740.
##  6  1977    38.4 14880372      786.
##  7  1982    39.9 12881816      978.
##  8  1987    40.8 13867957      852.
##  9  1992    41.7 16317921      649.
## 10  1997    41.8 22227415      635.
## 11  2002    42.1 25268405      727.
## 12  2007    43.8 31889923      975.
  • 일반적인 그룹화된 데이터프레임과 중첩된 데이터프레임 사이에는 차이점이 존제한다. -그룹화된 데이터프레임의 각 행은 관측값이고, 중첩된 데이터프레임의 각 행은 그룹을 나타낸다.

20.2.2 리스트 열

  • 이제 중접된 데이터프레임을 생성했으므로 모델을 적합해 볼 수 있다.
  • 다음과 같이 모델을 적합하는 함수가 있다.
country_model <- function(df) {
  lm(lifeExp ~ year, data = df)
}

-또한 함수를 모든 데이터프레임에 적용하려고 한다. - 데이터프레임이 리스트 안에 있으므로 Purr::map()을 사용하여 각 요소에 대해 country_model 함수를 적용할 수 있다.

models <- map(by_country$data, country_model)
  • 그러나 모델의 리스트를 자유로운 객체로 두기보다는 by_country 데이터프레임의 열로 저장하는것이 좋다.
by_country <- by_country %>%
  mutate(model = map(data, country_model))
by_country 
## # A tibble: 142 x 4
## # Groups:   country, continent [142]
##    country     continent data              model 
##    <fct>       <fct>     <list>            <list>
##  1 Afghanistan Asia      <tibble [12 x 4]> <lm>  
##  2 Albania     Europe    <tibble [12 x 4]> <lm>  
##  3 Algeria     Africa    <tibble [12 x 4]> <lm>  
##  4 Angola      Africa    <tibble [12 x 4]> <lm>  
##  5 Argentina   Americas  <tibble [12 x 4]> <lm>  
##  6 Australia   Oceania   <tibble [12 x 4]> <lm>  
##  7 Austria     Europe    <tibble [12 x 4]> <lm>  
##  8 Bahrain     Asia      <tibble [12 x 4]> <lm>  
##  9 Bangladesh  Asia      <tibble [12 x 4]> <lm>  
## 10 Belgium     Europe    <tibble [12 x 4]> <lm>  
## # ... with 132 more rows
  • 이 방법에는 큰 장점이 있다. 모든 관련에 객체가 함꼐 저장되므로 필터링하거나 정렬할 때 수동으로 동기화 할 필요가 없다. 데이터프레임의 의미는 다음과 같이 처리된다,
by_country %>%
  filter(continent == "Europe")
## # A tibble: 30 x 4
## # Groups:   country, continent [30]
##    country                continent data              model 
##    <fct>                  <fct>     <list>            <list>
##  1 Albania                Europe    <tibble [12 x 4]> <lm>  
##  2 Austria                Europe    <tibble [12 x 4]> <lm>  
##  3 Belgium                Europe    <tibble [12 x 4]> <lm>  
##  4 Bosnia and Herzegovina Europe    <tibble [12 x 4]> <lm>  
##  5 Bulgaria               Europe    <tibble [12 x 4]> <lm>  
##  6 Croatia                Europe    <tibble [12 x 4]> <lm>  
##  7 Czech Republic         Europe    <tibble [12 x 4]> <lm>  
##  8 Denmark                Europe    <tibble [12 x 4]> <lm>  
##  9 Finland                Europe    <tibble [12 x 4]> <lm>  
## 10 France                 Europe    <tibble [12 x 4]> <lm>  
## # ... with 20 more rows
by_country %>%
  arrange(continent, country)
## # A tibble: 142 x 4
## # Groups:   country, continent [142]
##    country                  continent data              model 
##    <fct>                    <fct>     <list>            <list>
##  1 Algeria                  Africa    <tibble [12 x 4]> <lm>  
##  2 Angola                   Africa    <tibble [12 x 4]> <lm>  
##  3 Benin                    Africa    <tibble [12 x 4]> <lm>  
##  4 Botswana                 Africa    <tibble [12 x 4]> <lm>  
##  5 Burkina Faso             Africa    <tibble [12 x 4]> <lm>  
##  6 Burundi                  Africa    <tibble [12 x 4]> <lm>  
##  7 Cameroon                 Africa    <tibble [12 x 4]> <lm>  
##  8 Central African Republic Africa    <tibble [12 x 4]> <lm>  
##  9 Chad                     Africa    <tibble [12 x 4]> <lm>  
## 10 Comoros                  Africa    <tibble [12 x 4]> <lm>  
## # ... with 132 more rows

-데이터프레임의 리스트와 모델의 리스트가 분리된 객체인 경우, 하나의 백터를 재 정렬하거나 하위 집합으로 만들 때마다 동기화하기 위해 다른 모든것을 재정렬 하거나 하위 집합으로 만들어야 한다.

20.2.3 중첩 해체하기 (unnesting)

  • 이 전에는 하나의 데이터셋의 단일 모델에 대한 잔차를 계산하였지만,
  • 이제는 142개의 데이터프레임과 142개의 모델을 보유하고 있다.
by_country <- by_country %>%
  mutate(
    resids = map2(data, model, add_residuals))
by_country %>% head()
## # A tibble: 6 x 5
## # Groups:   country, continent [6]
##   country     continent data              model  resids           
##   <fct>       <fct>     <list>            <list> <list>           
## 1 Afghanistan Asia      <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
## 2 Albania     Europe    <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
## 3 Algeria     Africa    <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
## 4 Angola      Africa    <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
## 5 Argentina   Americas  <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
## 6 Australia   Oceania   <tibble [12 x 4]> <lm>   <tibble [12 x 5]>
  • 그런데 데이터프레임 리스트를 어떻게 하면 플롯으로 나타낼 수 있을까?
  • 이 질문에 답하기 위해 고민하는대신 데이터프레임 리스트를 일반 데이터프레임 으로 되돌려보자.
resids <- unnest(by_country , resids)
resids
## # A tibble: 1,704 x 9
## # Groups:   country, continent [142]
##    country   continent data        model   year lifeExp    pop gdpPercap   resid
##    <fct>     <fct>     <list>      <list> <int>   <dbl>  <int>     <dbl>   <dbl>
##  1 Afghanis~ Asia      <tibble [1~ <lm>    1952    28.8 8.43e6      779. -1.11  
##  2 Afghanis~ Asia      <tibble [1~ <lm>    1957    30.3 9.24e6      821. -0.952 
##  3 Afghanis~ Asia      <tibble [1~ <lm>    1962    32.0 1.03e7      853. -0.664 
##  4 Afghanis~ Asia      <tibble [1~ <lm>    1967    34.0 1.15e7      836. -0.0172
##  5 Afghanis~ Asia      <tibble [1~ <lm>    1972    36.1 1.31e7      740.  0.674 
##  6 Afghanis~ Asia      <tibble [1~ <lm>    1977    38.4 1.49e7      786.  1.65  
##  7 Afghanis~ Asia      <tibble [1~ <lm>    1982    39.9 1.29e7      978.  1.69  
##  8 Afghanis~ Asia      <tibble [1~ <lm>    1987    40.8 1.39e7      852.  1.28  
##  9 Afghanis~ Asia      <tibble [1~ <lm>    1992    41.7 1.63e7      649.  0.754 
## 10 Afghanis~ Asia      <tibble [1~ <lm>    1997    41.8 2.22e7      635. -0.534 
## # ... with 1,694 more rows
  • 보통의 열은 중첩된 열의 행으로 한 번씩 반복된다.
  • 이제 일반적인 데이터프레임을 가지고 있으므로 잔차를 플롯으로 나타낼 수 있다.
resids %>%
  ggplot(aes(year, resid)) +
  geom_line(aes(group = country ), alpha = 1/3) +
  geom_smooth(se = FALSE)

-플롯을 대륙으로 면분할하면 더 잘 나타난다.

resids %>%
  ggplot(aes(year, resid, group = country   )) +
  geom_line(alpha = 1/3) + 
  facet_wrap(~ continent)

-플롯을 보면 가벼운 패턴은 포착하지 못한 것 처럼 보인다.

20.2.4 모델의 성능

  • 모델의 잔차를 탐색하는 대신, 모델의 성능에 대한 일반적인 측정값을 살펴볼수 있다.
broom::glance(nz_mod)
## # A tibble: 1 x 12
##   r.squared adj.r.squared sigma statistic p.value    df logLik   AIC   BIC
##       <dbl>         <dbl> <dbl>     <dbl>   <dbl> <dbl>  <dbl> <dbl> <dbl>
## 1     0.954         0.949 0.804      205. 5.41e-8     1  -13.3  32.6  34.1
## # ... with 3 more variables: deviance <dbl>, df.residual <int>, nobs <int>

-mutate()와 unnest()를 사용하여 국가별로 하나의 행이 존재하는 데이터프레임을 만들수있다.

library(dplyr)

by_country %>%
  mutate(glance = map(model, broom::glance)) %>%
  unnest(glance)
## # A tibble: 142 x 17
## # Groups:   country, continent [142]
##    country continent data  model resids r.squared adj.r.squared sigma statistic
##    <fct>   <fct>     <lis> <lis> <list>     <dbl>         <dbl> <dbl>     <dbl>
##  1 Afghan~ Asia      <tib~ <lm>  <tibb~     0.948         0.942 1.22      181. 
##  2 Albania Europe    <tib~ <lm>  <tibb~     0.911         0.902 1.98      102. 
##  3 Algeria Africa    <tib~ <lm>  <tibb~     0.985         0.984 1.32      662. 
##  4 Angola  Africa    <tib~ <lm>  <tibb~     0.888         0.877 1.41       79.1
##  5 Argent~ Americas  <tib~ <lm>  <tibb~     0.996         0.995 0.292    2246. 
##  6 Austra~ Oceania   <tib~ <lm>  <tibb~     0.980         0.978 0.621     481. 
##  7 Austria Europe    <tib~ <lm>  <tibb~     0.992         0.991 0.407    1261. 
##  8 Bahrain Asia      <tib~ <lm>  <tibb~     0.967         0.963 1.64      291. 
##  9 Bangla~ Asia      <tib~ <lm>  <tibb~     0.989         0.988 0.977     930. 
## 10 Belgium Europe    <tib~ <lm>  <tibb~     0.995         0.994 0.293    1822. 
## # ... with 132 more rows, and 8 more variables: p.value <dbl>, df <dbl>,
## #   logLik <dbl>, AIC <dbl>, BIC <dbl>, deviance <dbl>, df.residual <int>,
## #   nobs <int>

-그렇지만 여전히 모든 리스트-열을 포함하고 있으므로 이는 우리가 원하는 결과물이 아니다.

glance <- by_country %>%
  mutate(glance = map(model, broom :: glance)) %>%
  unnest(glance, drop = TRUE)
glance
## # A tibble: 142 x 18
## # Groups:   country, continent [142]
##    country continent data  model resids r.squared adj.r.squared sigma statistic
##    <fct>   <fct>     <lis> <lis> <list>     <dbl>         <dbl> <dbl>     <dbl>
##  1 Afghan~ Asia      <tib~ <lm>  <tibb~     0.948         0.942 1.22      181. 
##  2 Albania Europe    <tib~ <lm>  <tibb~     0.911         0.902 1.98      102. 
##  3 Algeria Africa    <tib~ <lm>  <tibb~     0.985         0.984 1.32      662. 
##  4 Angola  Africa    <tib~ <lm>  <tibb~     0.888         0.877 1.41       79.1
##  5 Argent~ Americas  <tib~ <lm>  <tibb~     0.996         0.995 0.292    2246. 
##  6 Austra~ Oceania   <tib~ <lm>  <tibb~     0.980         0.978 0.621     481. 
##  7 Austria Europe    <tib~ <lm>  <tibb~     0.992         0.991 0.407    1261. 
##  8 Bahrain Asia      <tib~ <lm>  <tibb~     0.967         0.963 1.64      291. 
##  9 Bangla~ Asia      <tib~ <lm>  <tibb~     0.989         0.988 0.977     930. 
## 10 Belgium Europe    <tib~ <lm>  <tibb~     0.995         0.994 0.293    1822. 
## # ... with 132 more rows, and 9 more variables: p.value <dbl>, df <dbl>,
## #   logLik <dbl>, AIC <dbl>, BIC <dbl>, deviance <dbl>, df.residual <int>,
## #   nobs <int>, drop <lgl>
  • 유용한 정보가 많이 포함되었지만 인쇄되지 않은 변수에 주목해보자.
  • 이 데이터프레임을 사용하면 잘 맞지 않는 모델을 찾을 수 있다.
glance %>%
  arrange(r.squared)
## # A tibble: 142 x 18
## # Groups:   country, continent [142]
##    country continent data  model resids r.squared adj.r.squared sigma statistic
##    <fct>   <fct>     <lis> <lis> <list>     <dbl>         <dbl> <dbl>     <dbl>
##  1 Rwanda  Africa    <tib~ <lm>  <tibb~    0.0172      -0.0811   6.56     0.175
##  2 Botswa~ Africa    <tib~ <lm>  <tibb~    0.0340      -0.0626   6.11     0.352
##  3 Zimbab~ Africa    <tib~ <lm>  <tibb~    0.0562      -0.0381   7.21     0.596
##  4 Zambia  Africa    <tib~ <lm>  <tibb~    0.0598      -0.0342   4.53     0.636
##  5 Swazil~ Africa    <tib~ <lm>  <tibb~    0.0682      -0.0250   6.64     0.732
##  6 Lesotho Africa    <tib~ <lm>  <tibb~    0.0849      -0.00666  5.93     0.927
##  7 Cote d~ Africa    <tib~ <lm>  <tibb~    0.283        0.212    3.93     3.95 
##  8 South ~ Africa    <tib~ <lm>  <tibb~    0.312        0.244    4.74     4.54 
##  9 Uganda  Africa    <tib~ <lm>  <tibb~    0.342        0.276    3.19     5.20 
## 10 Congo,~ Africa    <tib~ <lm>  <tibb~    0.348        0.283    2.43     5.34 
## # ... with 132 more rows, and 9 more variables: p.value <dbl>, df <dbl>,
## #   logLik <dbl>, AIC <dbl>, BIC <dbl>, deviance <dbl>, df.residual <int>,
## #   nobs <int>, drop <lgl>
  • 가장 좋지 않은 모델은 아프리카 대륙에서 나타난다.
  • 이를 플롯으로 다시 확인 해보자.
  • 상대적으로 적은 수의 관측값과 이산형 변수가 존재하기 때문에 geom_jitter() 함수가 효과적일 것이다.
glance %>%
  ggplot(aes(continent, r.squared)) +
  geom_jitter(width = 0.5)

  • R 값이 작은 국가를 제거한 데이터를 플롯으로 나타낼 수 있다.
bad_fit <- filter(glance, r.squared < 0.25)
gapminder %>%
  semi_join(bad_fit, by = "country") %>%
  ggplot(aes(year, lifeExp, color = country)) +
  geom_line()

-여기서는 HIV/AIDS 전염병과 르완다 집단 학살의 비극이라는 두 가지 주요 효과를 확인할 수 있다.

20.2.5 연습문제

  1. 선형 주세는 전체적인 추세에 비해 너무 단수해 보인다. 이를 2차 다향식으로 개선할 수 있는가? 2차 식의 계수는 어떻게 해석할 수 있는가? (힌트: year를 평균이 0이 되도록 변환할 수 있다.)

  2. 대륙별 R의 분포를 시각화하는 다른 방법을 탐색해보자. jitter와 같이 점들 이 겹처지는 것을 피하면서 결정론적 방법을 사용하는 ggbeeWarm 패키지를 사용해 볼 수 있다.

  3. (가장 좋지 않는 모델이 생성된 국가의 데이터를 나타내는) 마지막 플롯을 생성하기 위해 두 가지 단계가 필요하다. 먼저 국가마다 한 행의 데이터프레임을 생성한 후, 원 데이터셋에 내부 조인하다. unnest(.drop = true) 대신에 unnest() 를 사용하면 이 조인을 피할수 있다. 이를 어떻게 할 수 있는가?

20.3 리스트-열(list-column)

  • 많은 모델을 관리하기 위한 기본 워크폴로를 살펴보았으므로 몇 가지 세부사항 으로 들어가보자.
  • 이 절에서는 리스트-열 데이터 구조에 대해 좀 더 자세히 살펴볼 것이다.
data.frame(x = list(1:3,3:5))
##   x.1.3 x.3.5
## 1     1     3
## 2     2     4
## 3     3     5
  • I()를 사용하면 data.frame()에서 이를 막을 수 있지만, 다음과 같이 제대로 출력되지 않는다.
data.frame (
  x = I(list(1:3, 3:5)), 
  y = c("1, 2", "3, 4, 5")
)
##         x       y
## 1 1, 2, 3    1, 2
## 2 3, 4, 5 3, 4, 5

-tibble()은 입력값을 수정하지 않고도 더 나온 출력 방법을 제공하여 이 문제를 해결할 수 있다.

library(tibble)
tibble(
  x = I(list(1:3, 3:5)),
  y = c("1, 2", "3, 4, 5")
)
## # A tibble: 2 x 2
##   x         y      
##   <I<list>> <chr>  
## 1 <int [3]> 1, 2   
## 2 <int [3]> 3, 4, 5

-tibble()은 필요한 리스트를 자동으로 생성할 수 있는 더 간단한 방법이다.

tribble(
  ~x, ~y,
  1:3, "1, 2", 3:5, "3, 4, 5"
)
## # A tibble: 2 x 2
##   x         y      
##   <list>    <chr>  
## 1 <int [3]> 1, 2   
## 2 <int [3]> 3, 4, 5

-리스트-열은 중급 데이터 구조로 가장 유용하다. 대부분의 R 함수가 원자 백터 또는 데이터프레임에서 동작하기 때문에 리스트-열로 직접 작업하기는 어렵다.

  • 일반적으로 리스트-열 파이프 라인에는 효과적인 측면이 세 가지이다.
  1. 394쪽의 ’리스트-열 생성하기에서 설명하는 내용과 같이 nest(),summarize()
  • list()또는 mutate() + map 함수 중 하나를 사용하여 리스트-열을 생성한다.
  1. 기존의 리스트-열을 map(), map()2 또는 pmap()으로 변형하여 다른 중간 리스트 -열을 만든다. 예를 들어 이전 사례에서 데이터프레임의 리스트-열을 변형하여 모델의 리스트-열을 생성하였다.

  2. 399쪽의 ’리스트-열 단순화하기’의 내용과 같이 리스트-열을 데이터프레임 또는 원자 백터로 다시 단순화한다.

20.4 리스트-열 생성하기

  • 늘 하는 식으로 tibble()을 사용하여 리스트-열을 생성하지 않을 것이다.
  • 다음의 세가지 방법 중 한가지를 사용하여 일반 열에서 리스트-열을 생성할것 이다.
  1. tidyr::nest()를 사용하여 그룹화된 데이터프레임을 데이터프레임의 리스트-열을 포함하는 중첩된 데이터로 변환한다.

  2. 리스트를 반환하는 mutate()와 백터화 함수를 사용한다.

  3. 여러 결과를 반환하는 summarize()와 요약 함수를 사용한다.

-또는 tibble::enframe()를 사용하여 명명된 리스트에서 생성할 수도 있다.

20.4.1 중첩을 사용하여 생성하기

-nest()함수는 중첩된 데이터프레임(즉, 데이터프레임의 리스트-열로 이루어진 데이터프레임)을 생성한다.

  • nest()를 사용하는 두 가지 방법이 있다. 지금까지는 그룹화된 데이터프레임에 사용하는 방법을 살펴보았다.
gapminder %>%
  group_by(country, continent) %>%
  nest()
## # A tibble: 142 x 3
## # Groups:   country, continent [142]
##    country     continent data             
##    <fct>       <fct>     <list>           
##  1 Afghanistan Asia      <tibble [12 x 4]>
##  2 Albania     Europe    <tibble [12 x 4]>
##  3 Algeria     Africa    <tibble [12 x 4]>
##  4 Angola      Africa    <tibble [12 x 4]>
##  5 Argentina   Americas  <tibble [12 x 4]>
##  6 Australia   Oceania   <tibble [12 x 4]>
##  7 Austria     Europe    <tibble [12 x 4]>
##  8 Bahrain     Asia      <tibble [12 x 4]>
##  9 Bangladesh  Asia      <tibble [12 x 4]>
## 10 Belgium     Europe    <tibble [12 x 4]>
## # ... with 132 more rows
  • 그룹화되지 않은 데이터프레임에서도 중점하고자 하는 열을 지정하면 사용할 수 있다.
gapminder %>%
  nest(year: gdpPercap)
## # A tibble: 142 x 3
##    country     continent data             
##    <fct>       <fct>     <list>           
##  1 Afghanistan Asia      <tibble [12 x 4]>
##  2 Albania     Europe    <tibble [12 x 4]>
##  3 Algeria     Africa    <tibble [12 x 4]>
##  4 Angola      Africa    <tibble [12 x 4]>
##  5 Argentina   Americas  <tibble [12 x 4]>
##  6 Australia   Oceania   <tibble [12 x 4]>
##  7 Austria     Europe    <tibble [12 x 4]>
##  8 Bahrain     Asia      <tibble [12 x 4]>
##  9 Bangladesh  Asia      <tibble [12 x 4]>
## 10 Belgium     Europe    <tibble [12 x 4]>
## # ... with 132 more rows

20.4.2 백터화 함수에 생성하기

  • 몇 가지 유용한 함수는 원자 백터를 입력하여 리스트를 반환한다.

  • 예를 들어 11 장에서는 문자형 백터를 사용하여 백터의 리스트를 반환하는 stringr:: str_split()에 대해 배웠다.

  • mutate 함수 안에서 이를 사용하여 리스트-열을 얻을 수 있다.

df <- tribble(
  ~x1, "a,b,c", 
  "d,e,f,g"
)
df %>%
  mutate(x2 = stringr::str_split(x1, "."))
## # A tibble: 2 x 2
##   x1      x2       
##   <chr>   <list>   
## 1 a,b,c   <chr [6]>
## 2 d,e,f,g <chr [8]>
  • unnest() 함수는 백터 리스트 다루는 방법을 알고 있다.
library(unnest)
df %>%
  mutate(x2 = stringr::str_split(x1,".")) %>%
 unnest()
##      x1    x1.2 x2.1 x2.1.2 x2.1.3 x2.1.4 x2.1.5 x2.1.6 x2.2 x2.2.2 x2.2.3
## 1 a,b,c d,e,f,g                                                           
##   x2.2.4 x2.2.5 x2.2.6 x2.2.7 x2.2.8
## 1
  • (이 패턴을 많이 사용하는 경우에 공통 패턴을 포함하는 tidyr:separate_rows() 를 반드시 확인한다.)

  • 이 패턴의 또 다른 예제는 purr의 map(), map2(), pmap() 함수를 사용하는 것이다.

  • 예를 들어 319쪽의 ’다른 함수 불러오기’의 마지막 예제를 활용하여 mutate() 함수를 사용하도록 다시 작성해 볼 수 있다.

sim <- tribble(
  ~f,  ~params,
  "runif", list(min = -1, max = -1),
  "rnorm", list(sd = 5),
  "rpois", list(lambda = 10)
)
sim %>%
  mutate(sims = invoke_map(f, params, n = 10))
## # A tibble: 3 x 3
##   f     params           sims      
##   <chr> <list>           <list>    
## 1 runif <named list [2]> <dbl [10]>
## 2 rnorm <named list [1]> <dbl [10]>
## 3 rpois <named list [1]> <int [10]>
  • sim은 더블형과 정수형 백터 둘 다 포함하므로 기술적으로 똑같지 않다. 그러나 정수형과 더블형 백터 모두 수치형 백터이므로 많은 문제가 발생하지 않는다.

20.4.3 다중값 요약에서 생성하기

  • summarize()의 한 가지 제약은 단일 값을 반환하는 요약 함수로만 동작한다는 것 이다. 즉, 임의 길이의 백터를 반환하는 quantile()과 같을 함수와 함께 사용할 수 없다는 것을 의미한다.
mtcars %>%
  group_by(cyl) %>%
  summarize(q = quantile(mpg))
## # A tibble: 15 x 2
## # Groups:   cyl [3]
##      cyl     q
##    <dbl> <dbl>
##  1     4  21.4
##  2     4  22.8
##  3     4  26  
##  4     4  30.4
##  5     4  33.9
##  6     6  17.8
##  7     6  18.6
##  8     6  19.7
##  9     6  21  
## 10     6  21.4
## 11     8  10.4
## 12     8  14.4
## 13     8  15.2
## 14     8  16.2
## 15     8  19.2
  • 그렇지만 결과를 리스트에 넣을 수 있다. 각각의 요약은 길이가 1인 리스트(백터) 이므로 summarize() 함수의 규칙을 따른다.
mtcars %>%
  group_by(cyl) %>%
  summarize(q = list(quantile(mpg)))
## # A tibble: 3 x 2
##     cyl q        
##   <dbl> <list>   
## 1     4 <dbl [5]>
## 2     6 <dbl [5]>
## 3     8 <dbl [5]>
  • unnest()를 사용하여 유용한 결과를 얻기 위해서는 확률값을 포착해야 한다.
probs <- c(0.01, 0.25, 0.5, 0.75, 0.99)
mtcars %>%
  group_by(cyl) %>%
  summarize(p = list(probs), q = list(quantile(mpg, probs))) %>%
  unnest()
##   cyl cyl.2 cyl.3  p.1 p.1.2 p.1.3 p.1.4 p.1.5  p.2 p.2.2 p.2.3 p.2.4 p.2.5
## 1   4     6     8 0.01  0.25   0.5  0.75  0.99 0.01  0.25   0.5  0.75  0.99
##    p.3 p.3.2 p.3.3 p.3.4 p.3.5   q.1 q.1.2 q.1.3 q.1.4 q.1.5    q.2 q.2.2 q.2.3
## 1 0.01  0.25   0.5  0.75  0.99 21.41  22.8    26  30.4 33.75 17.818 18.65  19.7
##   q.2.4  q.2.5  q.3 q.3.2 q.3.3 q.3.4  q.3.5
## 1    21 21.376 10.4  14.4  15.2 16.25 19.135

20.4.4 명명된 리스트에서 생성하기

  • 리스트- 열이 있는 데이터프레임은 다음의 일반적인 문제에 대한 해결책을 제공 한다. 리스트의 내용과 요소, 둘다 반복하고자 할때는 어떻게 할 것인가? 모든 것을 하나의 객체로 묶으려고 하는 대신 데이터프레임을 만드는 것이 더 쉽다.
x <- list(
  a=1:5,
  b=3:4, 
  c=5:6)
df <- enframe(x)
df
## # A tibble: 3 x 2
##   name  value    
##   <chr> <list>   
## 1 a     <int [5]>
## 2 b     <int [2]>
## 3 c     <int [2]>
  • 이 구조의 장점은 간단한 방법으로 일반화한다는 것이다. 메타 데이터에 문자형 백터가 있는 경우 이름(name)에는 유용하지만 다른 유형의 데이터 또는 여러 백터가 있는 경우 유용하지 않는다.

  • 이제 이름(name)과 값(value)을 동시에 반복하고자 한다면 map2()를 사용할 수 있다.

df %>%
  mutate(
    smry = map2_chr(
      name, 
      value, 
      ~ stringr::str_c(.x, ": ", .y[1])
    )
  )
## # A tibble: 3 x 3
##   name  value     smry 
##   <chr> <list>    <chr>
## 1 a     <int [5]> a: 1 
## 2 b     <int [2]> b: 3 
## 3 c     <int [2]> c: 5

20.4.5 연습문제

  1. 원자 백터를 입력하여 리스트르 반환하는 모든 함수를 나열해보자.

  2. quantitle()처럼 여러 개의 값을 반환하는 유용한 요약 함수를 생각해보자.

  3. 다음의 데이터프레임에는 무엇이 누락되었는가? quantitle()은 누락된 부분을 어떻게 반환하는가? 왜 그것은 도움이 되지 않는가?

mtcars %>%
  group_by(cyl) %>%
  summarize(q = list(quantile(mpg))) %>%
  unnest()
##   cyl cyl.2 cyl.3  q.1 q.1.2 q.1.3 q.1.4 q.1.5  q.2 q.2.2 q.2.3 q.2.4 q.2.5
## 1   4     6     8 21.4  22.8    26  30.4  33.9 17.8 18.65  19.7    21  21.4
##    q.3 q.3.2 q.3.3 q.3.4 q.3.5
## 1 10.4  14.4  15.2 16.25  19.2
  1. 다음의 코드는 무엇을 하는가? 이것은 왜 유용한가?
mtcars %>%
  group_by(cyl) %>%
  summarize_each(funs(list))
## # A tibble: 3 x 11
##     cyl mpg     disp    hp      drat   wt     qsec   vs     am     gear   carb  
##   <dbl> <list>  <list>  <list>  <list> <list> <list> <list> <list> <list> <list>
## 1     4 <dbl [~ <dbl [~ <dbl [~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~
## 2     6 <dbl [~ <dbl [~ <dbl [~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~
## 3     8 <dbl [~ <dbl [~ <dbl [~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~ <dbl ~

20.5 리스트-열 단순화 하기

  • 이 책에서 배운 데이터 처리 및 시각화 기술을 적용하기 위해서는 리스트-열을 일반 열(원자 백터) 또는 열의 집합으로 다시 단순화해야 한다.

  • 단일 값을 원하는 경우 map_lgl(), map_int(), map_dbl(), map_chr()에 mutate() 를 사용하여 원자 백터를 생성한다.

  • 많은 값을 원하는 경우 unnest()를 사용하여 리스트-열을 일반 열로 다시 변환 하고 필요한 만큼 행을 반복한다. 자세한 내용은 다음 절에서 설명할 것이다.

20.5.1 리스트를 벡터로 만들기

-리스트 열을 원자 벡터로 줄일 수 있다면 리스트 열은 일반 열이 될 것이다. 예를 들어 타입과 길이를 가진 객체는 항상 요약할 수 있으므로 다음 코드는 리스트 열의 종류에 관계없이 작동할 것이다.

df <- tribble(
  ~x, 
  letters[1:5],
  1:3,
  runif(5)
)
df %>% mutate(
  type = map_chr(x, typeof),
  length = map_int(x, length)
)
## # A tibble: 3 x 3
##   x         type      length
##   <list>    <chr>      <int>
## 1 <chr [5]> character      5
## 2 <int [3]> integer        3
## 3 <dbl [5]> double         5
  • 이는 기본 tbl print 방법에서 얻은 것과 같은 기본 정보지만, 여기에서는 필터링 용도로 사용할 수 있다. 다차원적인 리스트에 대해서 작동하지 않은 부분을 필터링 하고자 할 때 유용한 기법이다.

  • map_*() 단축어를 기억하자. 예를 들어 map_chr(x, “apple”)를 사용하여 x의 각 요소에 대해 apple 저장된 문자열을 추출할 수 있다.

df <- tribble(
  ~x,
  list(a=1,b=2),
  list(a=2,c=4)
)
df %>% mutate(
  a = map_dbl(x,"a"),
  b = map_dbl(x, "b", .null = NA_real_)
)
## # A tibble: 2 x 3
##   x                    a     b
##   <list>           <dbl> <dbl>
## 1 <named list [2]>     1     2
## 2 <named list [2]>     2    NA

20.5.2 중첩 해체하기

  • unnest()는 리스트-열의 각 요소를 한 줄씩 일반 열로 반복하며 동작한다. 예를 들어 다음의 아주 간단한 예제에서는 (y의 첫번째 요소 길이가 4 이므로) 첫 번 째 행은 4 반복하고 두 번째 행은 한 번만 반복한다.
library(tibble)
library(dplyr)
library(unnest)

tibble(x = 1:2, y = list(1:4, 1)) %>% unnest(y)
## Error in unnest(., y): 객체 'y'를 찾을 수 없습니다
  • 즉, 이는 다른 수의 요소가 포함되 두 개의 열을 동시에 중점 해제할 수없다는 것을 의미한다.

모든 행에 대해서 y와 z는 같은 수의 요소를 포함하므로 동작한다.

df1 <- tribble(
  ~x, ~y,         ~z,
  1, c("a","b"), 1:2,
  2, "c",          3    
)
df1
## # A tibble: 2 x 3
##       x y         z        
##   <dbl> <list>    <list>   
## 1     1 <chr [2]> <int [2]>
## 2     2 <chr [1]> <dbl [1]>
df1 %>% unnest(y, z)
## Error in unnest(., y, z): 객체 'y'를 찾을 수 없습니다

y와 Z가 가진 요소의 개수가 다르므로 동작하지 않는다.

df2 <- tribble(
  ~x, ~y,             ~z,
  1, "a",             1:2,
  2, c("a","b","c"),    3
)
df2
## # A tibble: 2 x 3
##       x y         z        
##   <dbl> <list>    <list>   
## 1     1 <chr [1]> <int [2]>
## 2     2 <chr [3]> <dbl [1]>
df2 %>% unnest(y,z)
## Error in unnest(., y, z): 객체 'y'를 찾을 수 없습니다
  • 데이터프레임의 리스트-열을 중첩 해체할 때도 같은 원칙이 적용된다. 각 행의 모든 데이터프레임이 같은 개수의 행을 가지고 있다면 여러 개의 리스트-열을 중첩해체할 수 있다.

20.5.3 연습문제

  1. 리스트-열에서 원자 벡터 열을 만들때 lengths() 함수가 유용한 이유는 무엇인가?

  2. 데이터프레임에서 발견되는 가장 일반적인 유형의 벡터를 나열해보자. 어떤 점이 이 리스트를 다르게 만드는가?

20.6 broom으로 타이디 데이터 만들기

  • broom 패키지는 모텔을 타이디 데이터프레임으로 변환할 수 있는 세 가지의 일 반적인 도구를 제공한다.

  • broom::glance(model)은 각 모델에 대한 행을 반환한다. 각 열에는 모델 요약 (모델 성능 척도 또는 복잡성 또는 둘의 조합)이 표시된다.

  • broom::glance(model)은 모델의 각 계수에 대한 행을 반환한다. 각 열은 추청값 또는 변동성에 대한 정보를 제공한다.

  • broom:augment(model,data)는 data의 각 행에 잔차와 같은 영향 통계량을 추가 하여 반환한다.

-Broom은 가장 인기 있는 모델링 패키지로 생성한 다양한 종류의 모델을 다룬다.