본문 바로가기
  • plotly로 바로쓰는 동적시각화 in R & 파이썬
데이터 전처리

apply, lapply, sapply, tapply in R

by 아참형인간 2022. 3. 12.
apply.knit

apply 함수의 이해

앞선 포스트에서는 R에서 대규모 데이터에 대한 처리 과정에서 벡터 연산과 루프 연산간의 속도 차이를 살펴보았다. 사실 앞선 포스트에서 비교했던 세가지 방법(함수 자체의 벡터 연산을 통한 대규모 데이터 처리, for 루프에서 함수 호출을 사용한 대규모 데이터 처리, apply()를 사용한 대규모 데이터 처리)들에서 사실 첫번째 방법의 속도가 우수하게 나올수 밖에 없었던 하나의 이유는 함수 호출에 대한 오버헤드가 없었다는 점이다. 루프를 사용하는 방법이나 apply()를 사용하는 방법은 데이터를 계산할 때마다 함수를 호출해야하기 때문에 이에 대한 속도의 문제가 발생할 수 밖에 없다. 그렇기 때문에 함수 자체에서 벡터 연산을 지원하도록 설계하는 것이 R에서의 대규모 데이터 처리에 핵심일 것이다. 그렇다면 함수 호출이 빈번하게 발생하는 나머지 두 개의 방법에 대한 속도에서도 R에서 대규모의 데이터를 루프없이 실행시키는 apply() 계열 함수는 데이터의 갯수가 적으면 루프보다 비효율적이지만 대규모 데이터가 될수록 루프보다는 효율적임을 알 수 있었다. 그러면 apply()의 구체적인 사용방법에 대해 알아보도록 하겠다.

apply()는 R에서 기본적으로 제공하는 함수로써 대규모의 데이터를 처리할 때 루프를 대신하여 반복되는 작업을 함수화하여 처리할 수 있는 함수이다. apply()는 주로 벡터, 리스트, 데이터프레임에서 사용하는 sum(), mean(), median()과 같은 요약 함수들을 반복하여 적용할 때 사용된다. apply()apply()외에도 적용하는 데이터의 종류에 따라 lapply(), sapply(), tapply()들을 사용할 수 있다.

데이터 Import

이번 포스트에서 사용하는 데이터는 한국교육개발원 교육통계서비스 홈페이지고등교육기관 대학 시도별 학교수를 활용하겠다.

library(readxl)
library(tidyverse)

df <- read_excel('./대학 시도별 학생수.xlsx', col_names = T, col_types = c('text', rep('numeric', 18)))

df <- df |> filter(합계 != is.na(합계))

df |> head()
## # A tibble: 6 x 19
##   연도    합계   서울  부산  대구  인천  광주  대전  울산  세종  경기  강원
##   <chr>  <dbl>  <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1980  402979 172155 34771     0     0     0     0     0     0 23829 15040
## 2 1981  535876 211205 46899     0     0     0     0     0     0 36322 21152
## 3 1982  661125 250643 61415 51043 23366     0     0     0     0 29523 23797
## 4 1983  772907 282166 70577 59602 26556     0     0     0     0 39374 28332
## 5 1984  870170 309021 80564 67098 28977     0     0     0     0 48439 32486
## 6 1985  931884 308763 90123 72255 30531     0     0     0     0 65895 35760
## # ... with 7 more variables: 충북 <dbl>, 충남 <dbl>, 전북 <dbl>, 전남 <dbl>,
## #   경북 <dbl>, 경남 <dbl>, 제주 <dbl>

apply()

apply()apply()계열의 함수중에 가장 기본적으로 사용되는 함수이다. 사실 lapply(), sapply(), tapply()도 기본적으로 apply()에서 파생되어 나온 함수들이기 때문에 apply()의 사용법을 잘 알아두면 나머지 apply()계열의 함수를 사용하는데 큰 문제가 없다.

apply()의 기본적인 작동 원리는 다음의 그림과 같다.

apply()의 기본적인 문법은 다음과 같다.

apply(X, MARGIN, FUN, ..., simplify = TRUE)
  - X : apply를 적용할 array나 matrix
  - MARGIN : 함수를 행 단위(MARGIN = 1)로 적용할지 열 단위(MARGIN = 2)로 적용할지를 설정
  - FUN : 적용해야 할 함수 이름

앞서 설명한 바와 같이 apply()는 array나 matrix에 대한 행방향, 혹은 열방향으로 특정 요약 함수를 적용하는 것이기 때문에 MARGIN을 통해 FUN으로 지정된 함수를 행방향으로 실행할지 열방향으로 실행할지를 결정한다. array나 matrix는 데이터프레임과 같이 2차원 테이블 형태로 표현되는데 데이터프레임은 다양한 데이터타입을 포함할 수 있는 반면 array나 matrix는 동일한 하나의 데이터타입을 가져야하기 때문에 데이터프레임을 사용하기 위해서는 동일한 데이터타입을 가지는 데이터프레임으로 처리한 후 사용하여야 한다.

앞서 불러들인 데이터에 대해 apply()를 적용해보겠다.

앞서 불러들인 데이터에는 행방향으로 연도별 입학자 수가 들어있고 열방향으로 지역별 입학자수가 들어있다. 먼저 각 지역별 평균을 루프를 사용하여 평균을 산출하면 다음과 같다.

means_loop <- NULL
for(i in 2:(ncol(df))) {
  means_loop[i-1] <- mean(pull(df[, i]))
}
means_loop <- set_names(means_loop, colnames(df)[2:ncol(df)])

means_loop
##        합계        서울        부산        대구        인천        광주 
## 1511692.500  389251.905  146542.690   57595.810   35481.167   59574.619 
##        대전        울산        세종        경기        강원        충북 
##   67545.762   11784.738    3814.738  170260.976   71427.143   69262.833 
##        충남        전북        전남        경북        경남        제주 
##  113260.405   80075.548   41172.381  116164.238   65369.833   13107.714

위의 코드를 apply()를 사용하여 작성하면 다음과 같다.

means_apply <- apply(df[, 2:ncol(df)], 2, mean)

means_apply
##        합계        서울        부산        대구        인천        광주 
## 1511692.500  389251.905  146542.690   57595.810   35481.167   59574.619 
##        대전        울산        세종        경기        강원        충북 
##   67545.762   11784.738    3814.738  170260.976   71427.143   69262.833 
##        충남        전북        전남        경북        경남        제주 
##  113260.405   80075.548   41172.381  116164.238   65369.833   13107.714

위의 코드에서 보면 확실히 apply()를 사용하는 코드가 짧고 명확하게 느껴진다. 이번에는 연도별 합계를 구하는 코드를 loop와 apply()로 구분하여 살펴보면 다음과 같다.

means_loop <- NULL
for(i in 1:(nrow(df))) {
  means_loop[i] <- sum(df[i, 3:19])
}

means_loop
##  [1]  402979  535876  661125  772907  870170  931884  971127  989503 1003648
## [10] 1020771 1040166 1052140 1070169 1092464 1132437 1187735 1266876 1368461
## [19] 1477715 1587667 1665398 1729638 1771738 1808539 1836649 1859639 1888436
## [28] 1919504 1943437 1984043 2028841 2065451 2103958 2120296 2130046 2113293
## [37] 2084807 2050619 2030033 2001643 1981003 1938254
sum_apply <- apply(df[, 3:ncol(df)], 1, sum)

sum_apply
##  [1]  402979  535876  661125  772907  870170  931884  971127  989503 1003648
## [10] 1020771 1040166 1052140 1070169 1092464 1132437 1187735 1266876 1368461
## [19] 1477715 1587667 1665398 1729638 1771738 1808539 1836649 1859639 1888436
## [28] 1919504 1943437 1984043 2028841 2065451 2103958 2120296 2130046 2113293
## [37] 2084807 2050619 2030033 2001643 1981003 1938254

위에서 실행한 열방향의 apply()와 행방향의 apply() 코드 실행결과를 잘 살펴보면 하나 차이점이 눈에 보일 것이다. 열방향으로 apply()를 실행하면 결과값이 각각의 열 이름이 붙은 named vector가 리턴되지만 행방향의 apply()의 실행 결과는 단순 벡터가 리턴된다. 행 방향 apply()의 결과를 named vector로 바꾸기 위해서는 다음과 같이 추가적인 코드를 실행시킨다.

sum_apply <- set_names(sum_apply, pull(df[, 1])) 

sum_apply
##    1980    1981    1982    1983    1984    1985    1986    1987    1988    1989 
##  402979  535876  661125  772907  870170  931884  971127  989503 1003648 1020771 
##    1990    1991    1992    1993    1994    1995    1996    1997    1998    1999 
## 1040166 1052140 1070169 1092464 1132437 1187735 1266876 1368461 1477715 1587667 
##    2000    2001    2002    2003    2004    2005    2006    2007    2008    2009 
## 1665398 1729638 1771738 1808539 1836649 1859639 1888436 1919504 1943437 1984043 
##    2010    2011    2012    2013    2014    2015    2016    2017    2018    2019 
## 2028841 2065451 2103958 2120296 2130046 2113293 2084807 2050619 2030033 2001643 
##    2020    2021 
## 1981003 1938254

lapply()

lapply()는 기본적으로 apply()와 동일하게 작동하는 함수이다. 하지만 apply()는 array와 matrix을 대상으로 적용되는 함수이지만 lapply()의 ’l’은 list를 의미하는것으로 apply()와 달리 list를 대상으로 적용되며 그 결과를 list로 반환한다. lapply()의 코드를 살펴보기 위해서 먼저 앞의 데이터프레임을 리스트로 전환한다. 리스트는 행과 열이 없기때문에 apply()에 있던 MARGIN 매개변수는 사용하지 않는다.

df_list_row <- as.list(as.data.frame(t(df[, 3:19])))

names(df_list_row) <- pull(df[, 1])

df_list_row |> head() 
## $`1980`
##  [1] 172155  34771      0      0      0      0      0      0  23829  15040
## [11]  15202  22977  24565  26540  47175  17316   3409
## 
## $`1981`
##  [1] 211205  46899      0      0      0      0      0      0  36322  21152
## [11]  20867  34113  34707  34751  66799  24361   4700
## 
## $`1982`
##  [1] 250643  61415  51043  23366      0      0      0      0  29523  23797
## [11]  26044  44837  42279  43401  27871  30860   6046
## 
## $`1983`
##  [1] 282166  70577  59602  26556      0      0      0      0  39374  28332
## [11]  31373  55063  49351  51531  35066  36742   7174
## 
## $`1984`
##  [1] 309021  80564  67098  28977      0      0      0      0  48439  32486
## [11]  35657  63669  54504  58713  40797  42148   8097
## 
## $`1985`
##  [1] 308763  90123  72255  30531      0      0      0      0  65895  35760
## [11]  39134  68407  58380  63419  44420  46131   8666
df_list_col <- as.list(as.data.frame(df[, 3:19]))

df_list_col |> head() 
## $서울
##  [1] 172155 211205 250643 282166 309021 308763 312779 311852 292796 287859
## [11] 288642 284521 285530 289965 299102 310723 327977 345481 381331 408911
## [21] 422594 432787 438986 445169 453265 446599 451481 454639 440846 447982
## [31] 468509 471648 494016 495395 504569 500178 498944 499021 502807 505743
## [41] 507319 504661
## 
## $부산
##  [1]  34771  46899  61415  70577  80564  90123  94777  97420  96426 100737
## [11] 102449 103097  99020  99453 102253 106851 113339 135924 143897 152125
## [21] 158799 164039 169004 173664 176804 179114 184810 190344 196441 204627
## [31] 209389 211926 213882 213594 213240 208811 203419 198348 194927 190526
## [41] 186383 180585
## 
## $대구
##  [1]     0     0 51043 59602 67098 72255 74465 66758 66452 66211 65584 48566
## [13] 48060 47339 47548 48395 49786 51307 53165 56741 57442 59432 57990 58387
## [25] 59478 60591 62473 63117 64033 63507 63726 63203 63279 69015 68000 67079
## [37] 65812 64336 63316 62200 61667 60566
## 
## $인천
##  [1]     0     0 23366 26556 28977 30531 31514 31858 32007 31987 31728 30868
## [13] 30398 29676 29662 29854 31003 31346 32608 34562 34834 35736 36204 36617
## [25] 37415 38829 40364 41394 42910 45278 46728 50392 48520 49247 47608 46379
## [37] 45283 43768 44183 43779 43435 42805
## 
## $광주
##  [1]     0     0     0     0     0     0     0 55420 54810 54268 54430 54416
## [13] 53907 53794 54861 55530 57592 59741 62216 65874 68074 70078 70595 71177
## [25] 73330 75414 77219 79861 81218 82334 83602 84268 85459 86712 88274 87415
## [37] 86280 84814 84187 82653 82338 79973
## 
## $대전
##  [1]      0      0      0      0      0      0      0      0      0  49975
## [11]  51347  52687  53902  54949  56925  59753  63253  67027  71578  76006
## [21]  79833  82860  83567  86188  86764  86448  87968  88389  88924  91426
## [31]  95252 104426 109416 111612 113583 115724 114956 112638 113709 111699
## [41] 108791 105347

전환된 리스트를 사용하여 연도별 lapply()를 적용하여 합계를 내는 코드는 다음과 같다.

means_loop <- NULL
for(i in 1:length(df_list_row)) {
  means_loop[i] <- mean(df_list_row[[i]])
  
}

means_loop
##  [1]  23704.65  31522.12  38889.71  45465.12  51186.47  54816.71  57125.12
##  [8]  58206.06  59038.12  60045.35  61186.24  61890.59  62951.12  64262.59
## [15]  66613.94  69866.76  74522.12  80497.71  86924.41  93392.18  97964.59
## [22] 101743.41 104219.88 106384.65 108038.18 109390.53 111084.47 112912.00
## [29] 114319.82 116708.41 119343.59 121497.12 123762.24 124723.29 125296.82
## [36] 124311.35 122635.71 120624.65 119413.71 117743.71 116529.59 114014.94

위의 코드를 lapply()를 사용하면 다음과 같다.

means_lapply <- lapply(df_list_row, mean)

typeof(means_lapply)
## [1] "list"
means_lapply |> head()
## $`1980`
## [1] 23704.65
## 
## $`1981`
## [1] 31522.12
## 
## $`1982`
## [1] 38889.71
## 
## $`1983`
## [1] 45465.12
## 
## $`1984`
## [1] 51186.47
## 
## $`1985`
## [1] 54816.71

이번에는 데이터프레임의 열을 각각의 리스트로 뽑아낸 리스트를 lapply()를 사용하여 평균을 내는 코드는 다음과 같다.

sum_lapply <- lapply(df_list_col, sum)

typeof(sum_lapply)
## [1] "list"
sum_lapply |> head()
## $서울
## [1] 16348580
## 
## $부산
## [1] 6154793
## 
## $대구
## [1] 2419024
## 
## $인천
## [1] 1490209
## 
## $광주
## [1] 2502134
## 
## $대전
## [1] 2836922

sapply()

앞선 lapply()의 결과는 리스트로 반환된다. 하지만 각각의 리스트 엘리먼트는 평균값 하나만을 가지고 있는 리스트이다. 이는 리스트의 장점을 사용하기 어려운 형태의 결과이다. 이는 사실 벡터로 반환되는 것이 더 사용하기가 편리하다. 이렇게 리스트를 대상으로 apply()를 적용하여 결과를 벡터 형태로 돌려주는 함수가 sapply()이다. sapply()lapply()와 사용방법은 동일하지만 결과값의 형태가 벡터라는 점이 다르다.

means_sapply <- sapply(df_list_row, mean)

typeof(means_sapply)
## [1] "double"
means_sapply
##      1980      1981      1982      1983      1984      1985      1986      1987 
##  23704.65  31522.12  38889.71  45465.12  51186.47  54816.71  57125.12  58206.06 
##      1988      1989      1990      1991      1992      1993      1994      1995 
##  59038.12  60045.35  61186.24  61890.59  62951.12  64262.59  66613.94  69866.76 
##      1996      1997      1998      1999      2000      2001      2002      2003 
##  74522.12  80497.71  86924.41  93392.18  97964.59 101743.41 104219.88 106384.65 
##      2004      2005      2006      2007      2008      2009      2010      2011 
## 108038.18 109390.53 111084.47 112912.00 114319.82 116708.41 119343.59 121497.12 
##      2012      2013      2014      2015      2016      2017      2018      2019 
## 123762.24 124723.29 125296.82 124311.35 122635.71 120624.65 119413.71 117743.71 
##      2020      2021 
## 116529.59 114014.94
sum_sapply <- sapply(df_list_col, sum)

typeof(sum_sapply)
## [1] "double"
sum_sapply
##     서울     부산     대구     인천     광주     대전     울산     세종 
## 16348580  6154793  2419024  1490209  2502134  2836922   494959   160219 
##     경기     강원     충북     충남     전북     전남     경북     경남 
##  7150961  2999940  2909039  4756937  3363173  1729240  4878898  2745533 
##     제주 
##   550524

tapply

tapply()는 앞선 apply()와 같은 방식으로 작동하지만 그룹화된 각각의 그룹에 대해 적용된다는 점이 다르다. 이는 dplyr패키지의 group_by()summarize()를 사용하여 동작하는 것과 동일한 결과를 낸다. tapply()는 내부적으로 split()를 사용하여 벡터나 데이터프레임을 분할하기 때문에 split()가 작동되는 R 객체에 한해 작동한다. 따라서 내부적으로 그룹화할 컬럼을 INDEX 매개변수로 전달해야 한다. 앞선 df 데이터프레임을 tapply()에 적용하기 위해서 먼저 긴 형태의 데이터프레임으로 변환한다.

df_long <- pivot_longer(df, -1, names_to = '지역')

df_long <- df_long |> filter(지역 != '합계')

df_long$지역 <- as.factor(df_long$지역)

df_long |> head()
## # A tibble: 6 x 3
##   연도  지역   value
##   <chr> <fct>  <dbl>
## 1 1980  서울  172155
## 2 1980  부산   34771
## 3 1980  대구       0
## 4 1980  인천       0
## 5 1980  광주       0
## 6 1980  대전       0
tapply(df_long$value, INDEX = df_long$지역, sum)
##     강원     경기     경남     경북     광주     대구     대전     부산 
##  2999940  7150961  2745533  4878898  2502134  2419024  2836922  6154793 
##     서울     세종     울산     인천     전남     전북     제주     충남 
## 16348580   160219   494959  1490209  1729240  3363173   550524  4756937 
##     충북 
##  2909039
tapply(df_long$value, INDEX = df_long$연도, mean)
##      1980      1981      1982      1983      1984      1985      1986      1987 
##  23704.65  31522.12  38889.71  45465.12  51186.47  54816.71  57125.12  58206.06 
##      1988      1989      1990      1991      1992      1993      1994      1995 
##  59038.12  60045.35  61186.24  61890.59  62951.12  64262.59  66613.94  69866.76 
##      1996      1997      1998      1999      2000      2001      2002      2003 
##  74522.12  80497.71  86924.41  93392.18  97964.59 101743.41 104219.88 106384.65 
##      2004      2005      2006      2007      2008      2009      2010      2011 
## 108038.18 109390.53 111084.47 112912.00 114319.82 116708.41 119343.59 121497.12 
##      2012      2013      2014      2015      2016      2017      2018      2019 
## 123762.24 124723.29 125296.82 124311.35 122635.71 120624.65 119413.71 117743.71 
##      2020      2021 
## 116529.59 114014.94

댓글