= aggregate(diamonds[,c("price", "depth","carat")],
baseR by = list(diamonds$color, diamonds$cut),
FUN = mean)
데이터프레임 성능 비교하기
base R 함수들을 공부하다 aggregate()
를 알게 되었습니다. 그러다 문득, 이 기본 함수가 다른 패키지들의 그룹 함수와 얼마나 성능 차이가 나는지 궁금해졌습니다.
그래서 단순하면서도 가장 많이 활용되는 그룹 별 데이터 요약하기 성능 테스트를 진행해보았습니다.
실험에 사용된 데이터는 ggplot2
의 diamonds
데이터입니다.
성능 비교를 위해 사용된 방법들은 다음과 같습니다.
base R
dplyr 1.0
dplyr 1.1
dtplyr
data.table
data.table
1.14.9 (dev)
1. base R
외부 패키지 없이 그룹 별로 요약값을 계산하려면 aggregate()
을 사용해야 합니다.
2. dplyr
가장 많은 R 사용자들이 활용할 것으로 예상되는 dplyr
의 방법입니다. dplyr
의 경우, 최근에 업데이트된 1.1 버전과 1.0 버전을 가지고 비교를 해보았습니다.
1) dplyr 1.0
dplyr에서 group_by()
와 summarise()
를 통해 분석을 그룹 별 요약 값을 계산하는 방법입니다.
|>
diamonds group_by(color, cut) |>
summarise(across(c(price, depth, carat), mean, na.rm=T)) |>
ungroup()
2) dplyr 1.1
dplyr
버전 1.1에서부터는 group_by()
를 따로 사용하지 않더라도, 각각의 함수에서 .by
인자를 통해 그룹 변수를 지정해줄 수 있게 되었습니다.
|>
diamonds summarise(across(c(price, depth, carat), mean),
.by=c(color, cut))
3. dtplyr
dtplyr
는 dplyr의 코드와 data.table
의 backend를 합쳐놓은 패키지입니다. dplyr
와 같은 코드를 활용해 data.table
과 같이 빠른 성능을 낼 수 있는 패키지입니다.
비록 dtplyr
가 data.table
처럼 빠른 성능을 보여줄 수 있다고는 하나, 순수한 data.table
보다는 성능이 떨어지는 것으로 알려져 있습니다. 그 이유는 dplyr
의 코드를 data.table
문법으로 변경하는 과정에서 걸리는 시간 때문입니다.1
dtplyr
에서는 lazy_dt()
를 이용해 데이터를 dtplyr
클래스로 만들어줍니다.
lazy_dt(diamonds) |>
summarise(across(c(price, depth, carat), mean))
4. data.table
속도로 잘 알려진 data.table
입니다.
data.table
같은 경우, 총 4개의 버전으로 비교해보았습니다.
1) j, by만 이용
as.data.table(diamonds)[,.(mean_price = mean(price),
mean_depth = mean(depth),
mean_carat = mean(carat)),
=.(color, cut)] by
2) lapply
+ .SD
이용
lapply
+ .SD
문법을 이용하여 계산하고자 하는 변수들을 동시에 입력해줍니다.
as.data.table(diamonds)[,lapply(.SD, mean),.SDcols=c("price","depth","carat"), by=.(color, cut)]
3) lapply
+ .SD
+ keyby
data.table
의 장점 중 하나는 key를 설정해 데이터를 정렬할 수 있다는 것이죠. 마찬가지로 keyby
를 이용해 그룹 변수를 key로 만들어줌으로써, 그룹 변수를 기준으로 데이터를 정렬합니다.
as.data.table(diamonds)[,lapply(.SD, mean),.SDcols=c("price","depth","carat"), keyby=.(color, cut)]
4) data.table 개발 버전 (1.14.9)
현재 github에 공개되어있는 1.14.9 버전에서는 data.table
클래스가 아닌 데이터셋도 DT()
를 이용해 data.table
문법을 적용할 수 있습니다.
|> DT(,.(mean_price = mean(price),
diamonds mean_depth = mean(depth),
mean_carat = mean(carat)),
by=.(color, cut))
이제, microbenchmark
패키지를 이용해 성능 비교를 진행한 뒤, 이를 boxplot으로 그려 시각화를 해보겠습니다.
set.seed(123)
<- microbenchmark(
result baseR = aggregate(diamonds[,c("price", "depth","carat")],
by = list(diamonds$color, diamonds$cut),
FUN = mean),
dplyr_1_0= diamonds |>
group_by(color, cut) |>
summarise(across(c(price, depth, carat), mean, na.rm=T)) |>
ungroup(),
dplyr_1_1 = diamonds |>
summarise(across(c(price, depth, carat), mean),
.by=c(color, cut)),
dtplyr = lazy_dt(diamonds) |>
summarise(across(c(price, depth, carat), mean),
.by=c(color, cut)),
dt = as.data.table(diamonds)[,.(mean_price = mean(price),
mean_depth = mean(depth),
mean_carat = mean(carat)),
by=.(color, cut)],
dt_lapply_sd = as.data.table(diamonds)[,lapply(.SD, mean),.SDcols=c("price","depth","carat"), by=.(color, cut)],
dt_DT = diamonds |> DT(,.(mean_price = mean(price),
mean_depth = mean(depth),
mean_carat = mean(carat)),
by=.(color, cut)),
dt_key = as.data.table(diamonds)[,.(mean_price = mean(price),
mean_depth = mean(depth),
mean_carat = mean(carat)),
keyby=.(color, cut)],
times=50
)
Unit: milliseconds
expr min lq mean median uq max
baseR 10.552662 11.173197 12.164903 11.546440 12.962560 20.493071
dplyr_1_0 116.217042 118.796475 132.369553 123.273039 133.731381 246.319923
dplyr_1_1 2.897798 3.051425 3.747189 3.183015 3.350643 16.925046
dtplyr 3.506443 3.692419 4.080181 3.911830 4.210495 6.900628
dt 1.388014 1.813020 2.963913 2.015950 2.462214 19.836948
dt_lapply_sd 1.448530 1.809535 2.476250 2.076978 2.352867 7.611691
dt_DT 1.215199 1.509661 2.024631 1.710049 1.893708 9.833071
dt_key 1.148820 1.475221 2.090865 1.636802 2.269391 5.287114
neval
50
50
50
50
50
50
50
50
예상된 결과였지만, 역시 data.table
을 사용한 방법이 가장 빠른 것으로 나타났습니다.
재미있는 점은 dplyr 1.0의 group_by()
가 생각보다 느리다는 것이었습니다. 심지어 base R의 aggregate()
보다 느린 것으로 나타났습니다.
확실히 데이터가 커지면 커질수록 data.table
이 속도 측면에서 큰 우위를 가져가는 것 같습니다.
Footnotes
https://dtplyr.tidyverse.org↩︎