dplyr 심화

across()로 column 동시 처리
dplyr
R
Published

February 2, 2023

인트로

데이터 분석을 수행할 때, 여러 열에 대해 동일한 작업을 해주는 경우가 종종 있습니다. 그러나 동일한 코드를 복사해서 붙여넣기 하는 것은 굉장히 번거로운 일이고, 때로는 실수의 원인이 되기도 합니다.

이번 글에서는 dplyr에서 여러 개의 열에 대해 동일한 작업을 수행할 수 있는 across() 에 대해 살펴보도록 하겠습니다.

dplyr에 있는 storms 데이터를 통해 예시를 들어보겠습니다. 예를 들어 wind, pressure, tropicalstorm_force_diameter, hurricane_force_diameter 변수의 평균을 구한다고 해보겠습니다. 이 경우, 4가지 열에 대해 각각 mean(na.rm=T)를 입력해야 합니다.

4개 정도면 괜찮지 않은가 생각할 수도 있지만, 만약 변수가 더 많아진다면? 훨씬 번거로울 수 밖에 없겠죠.

library(dplyr)
storms |> 
  group_by(name, month) |> 
  summarise(mean_wind = mean(wind, na.rm=T),
            mean_pressure = mean(pressure,na.rm=T),
            mean_tropical = mean(tropicalstorm_force_diameter, na.rm=T),
            mean_hurricane_force = mean(hurricane_force_diameter, na.rm=T))

하지만 across() 를 사용할 경우, 위의 코드는 다음과 같이 입력할 수 있습니다.

storms |> 
  group_by(name,month) |> 
  summarise(across(wind:hurricane_force_diameter, mean, na.rm=T))

기본 사용법

본격적으로 across() 사용법에 대해 알아봅시다.

across() 에는 핵심적인 두 가지의 인자를 받을 수 있습니다.

  • .cols: 함수를 적용시킬 열들을 입력합니다. 열의 위치, 이름, 유형을 통해 열들을 선택할 수 있습니다.

  • .fns: 열들에 적용시킬 함수를 입력합니다. purrr 패키지 스타일의 식 ~.x/2와 같은 형태로도 입력 가능합니다.

across()는 주로 summarise() 와 함께 사용해 여러 열에 동일한 함수를 적용시켜줍니다.

# 열의 유형으로 선택하기
starwars |> 
  summarise(across(where(is.character), n_distinct))
# 열의 이름으로 선택
starwars |> 
  group_by(species) |> 
  filter(n()>1) |> 
  summarise(across(c(sex,gender,homeworld), n_distinct))
# purrr 방식 함수 적용
starwars |> 
  group_by(species) |> 
  filter(n()>1) |> 
  summarise(across(is.numeric, ~mean(.x, na.rm=T)))

여러 함수 적용하기

across()를 이용해 여러 열에 두 가지 이상의 함수를 적용할 수 있습니다.

min_max <- list(
  min = ~min(.x, na.rm=T),
  max = ~max(.x ,na.rm=T)
)

starwars |> 
  group_by(species) |> 
  summarise(across(is.numeric,min_max))

여러 개의 함수를 적용한 경우, .names 를 통해 결과 데이터에서 출력되는 열의 이름을 변경합니다.

starwars |> 
  summarise(across(is.numeric, min_max, .names = "{.fn}_{.col}")) |> 
  relocate(starts_with('min'))

다른 함수와의 활용

1. mutate()

min_max_scale <- function(x){
  m <- min(x, na.rm=T)
  M <- max(x, na.rm=T)
  return((x-m)/(M-m))
}

df <- tibble(x=1:4, y=rnorm(4))
df |> mutate(
  across(is.numeric, min_max_scale)
)

2. distinct() , count()

count(), distinct() 와 같은 함수는 summarise() 를 생략할 수 있습니다.

distinct(): unique한 값 찾기

starwars |> distinct(across(contains('color')))

count(): 수를 셀 때 사용하는 함수입니다. across()와 함께 사용할 경우, 조건에 해당하는 열들의 조합별로 수를 셉니다.

starwars |> count(across(contains('color')), sort = T)

3. filter()

filter()across()는 바로 사용할 수 없습니다. 사실 filter() 에서는 across()가 아닌 다른 함수를 통해 조건을 만족하는 값들을 출력해야 합니다.

  • if_any() : 열들 중 하나의 열만 조건을 충족하면 선택합니다.

    starwars |> 
      filter(if_any(everything(),~!is.na(.x)))
  • if_all() : 열들 중 모든 열들이 조건을 충족해야 선택합니다.

starwars |> 
  filter(if_all(everything(),~!is.na(.x)))

across() vs _if(), _at(), _all()

_if(), _at(), _all()dplyr 이전 버전에서 쓰이던 함수들로, across() 처럼 여러 열에 대해 동시에 작업을 하기 위해 사용하는 함수들입니다.

across() 가 더 좋은 이유는 다음과 같습니다.

  1. 여러 열들에 대해 특정 함수를 사용하여 요약할 수 있습니다.
  2. 각 함수별로 _if(), _at(), _all() 이 존재했습니다. across()는 이런 함수들의 기능을 아우르기 때문에, 사용해야 할 함수의 숫자를 줄여줍니다.

across()_if(), _at(), _all() 을 대응시켜보면 다음과 같습니다. 예를 들어 mutate()를 통해 열들을 변화시키고자 할 경우,

  • _if() numeric인 열들에 대해 평균을 계산하는 경우

    starwars |> mutate_if(is.numeric, mean, na.rm=T) 
    starwars |> mutate(across(is.numeric,mean, na.rm=T))
  • _at() 특정한 조건 만족하는 열들

    # 최빈값 출력 사용자 지정 함수
    Mode <- function(x){
      y <- names(which.max(table(x)))
      return(y)
    }
    starwars |> 
      mutate_at(vars(ends_with('color')),Mode) |> 
      select(ends_with('color'))
    starwars |> 
      mutate(across(ends_with('color'),Mode)) |> 
      select(ends_with('color'))
  • _all() 모든 열에 대해 함수 적용

    df <- tibble(x=2, y=4, z=8)
    df |> mutate_all(~.x/y)
    df |> mutate(across(everything(),~.x/y))

    _all() 함수는 across() 안에 everything()을 사용하여 구현이 가능합니다.

_all(), _if(), _at() 가 적용되던 함수들은 mutate() 뿐만 아니라 select(), summarise() 등에도 동일하게 적용할 수 있었습니다.

이처럼 across() 하나만으로 _all(), _if(), _at() 함수들을 모두 구현 가능하기 때문에, 굳이 이전 버전의 함수들을 사용하지 않아도 될 것 같습니다.

Back to top