재현가능한 과학적 분석을 위한 중급 R

For 루프

학습 목표

  • for 루프를 이해하고 작성한다.

반복되는 연산

문제를 해결하려 시도하거나, 분석을 실행할 때, 데이터 다른 그룹 집단 혹은 다른 파일 혹은 변수를 변형해서 동일한 작업을 반복하는 경우가 있다.

R에 관한 훌륭한 점, 일반적으로 프로그래밍의 대단한 점은 게으름(lazy)을 피울 수 있게 한다는 점이다. 컴퓨터가 대신 작업을 수행하게 되면, 왜 반복적인 작업을 우리가 직접 실행해야 되나?

예를 들어, 2007년 gapminder 데이터셋에서 각 대륙별로 전체 인구를 계산한다고 가정해보자. 몇가지 방식으로 연산작업을 실행할 수 있지만, 가장 기본적인 접근법은 수작업이다:

gap[year == 2007 & continent == "Asia", sum(pop)]
[1] 3811953827
gap[year == 2007 & continent == "Africa", sum(pop)]
[1] 929539692
gap[year == 2007 & continent == "Americas", sum(pop)]
[1] 898871184
gap[year == 2007 & continent == "Europe", sum(pop)]
[1] 586098529
gap[year == 2007 & continent == "Oceania", sum(pop)]
[1] 24549947

타이핑이 지겨울 수 있지만, 무난히 수행해 낼 수 있다. 하지만, 각 국가별로 어떤 연산을 수행하는 것을 상상해보라!

상기 작업을 수행할 수 있는 더 총명한 방법은 최근에 익힌 data.table 기술을 사용하는 것이다:

gap[year == 2007, sum(pop), by=continent]
   continent         V1
1:      Asia 3811953827
2:    Europe  586098529
3:    Africa  929539692
4:  Americas  898871184
5:   Oceania   24549947

하지만, 문제에 대한 해결책이 명확하지 않거나, 익숙히 사용했던 형태에 들어맞지는 않는다. 그래서, 문제해결 도구상자에 언제라도 믿고 쓸 수 있는 도구 다수를 구비하는 것이 도움이 된다.

대신에 for 루프를 가지고, 각 대륙별로 반복(iterate)해서, 동일한 명령을 실행하도록 R에게 지시한다:

for (cc in gap[,unique(continent)]) {
  popsum <- gap[continent == cc, sum(pop)]
  print(paste(cc, ":", popsum))
}
[1] "Asia : 30507333902"
[1] "Europe : 6181115304"
[1] "Africa : 6187585961"
[1] "Americas : 7351438499"
[1] "Oceania : 212992136"

상기 코드는 우선 in 연산자 우측에 각각을 샅샅이 찾아서 cc 변수에 저장한다. for 루프 몸통부문 내부, 즉 괄호 내부({})에 속한 코드에서는 cc 값을 접근해서 원하는 작업을 수행한다. 그래서 cc 변수는 “Asia” 값을 가장 먼저 갖고서 코드를 실행하고 나서, 루프 처음으로 되돌아간다. 다음으로 cc 변수는 “Europe” 값을 갖게 되고 동일한 작업을 반복한다. 계속해서…

만약 수년에 걸쳐 각 대륙별로 전체 인구 변화를 살펴보고자 하면 어떨가? 개별 조건 다수를 루프 돌리는데 “중첩”하면 된다:

for (cc in gap[,unique(continent)]) {
  for (yy in gap[,unique(year)]) {
    popsum <- gap[year == yy & continent == cc, sum(pop)]
    print(paste(cc, yy, ":", popsum))
  }
}
[1] "Asia 1952 : 1395357351.99999"
[1] "Asia 1957 : 1562780599"
[1] "Asia 1962 : 1696357182"
[1] "Asia 1967 : 1905662900"
[1] "Asia 1972 : 2150972248"
[1] "Asia 1977 : 2384513556"
[1] "Asia 1982 : 2610135582"
[1] "Asia 1987 : 2871220762"
[1] "Asia 1992 : 3133292191"
[1] "Asia 1997 : 3383285500"
[1] "Asia 2002 : 3601802203"
[1] "Asia 2007 : 3811953827"
[1] "Europe 1952 : 418120846"
[1] "Europe 1957 : 437890351"
[1] "Europe 1962 : 460355155"
[1] "Europe 1967 : 481178958"
[1] "Europe 1972 : 500635059"
[1] "Europe 1977 : 517164531"
[1] "Europe 1982 : 531266901"
[1] "Europe 1987 : 543094160"
[1] "Europe 1992 : 558142797"
[1] "Europe 1997 : 568944148"
[1] "Europe 2002 : 578223869"
[1] "Europe 2007 : 586098529"
[1] "Africa 1952 : 237640501"
[1] "Africa 1957 : 264837738"
[1] "Africa 1962 : 296516865"
[1] "Africa 1967 : 335289489"
[1] "Africa 1972 : 379879541"
[1] "Africa 1977 : 433061021"
[1] "Africa 1982 : 499348587"
[1] "Africa 1987 : 574834110"
[1] "Africa 1992 : 659081517"
[1] "Africa 1997 : 743832984"
[1] "Africa 2002 : 833723916"
[1] "Africa 2007 : 929539692"
[1] "Americas 1952 : 345152446"
[1] "Americas 1957 : 386953916"
[1] "Americas 1962 : 433270254"
[1] "Americas 1967 : 480746623"
[1] "Americas 1972 : 529384210"
[1] "Americas 1977 : 578067699"
[1] "Americas 1982 : 630290920"
[1] "Americas 1987 : 682753971"
[1] "Americas 1992 : 739274104"
[1] "Americas 1997 : 796900410"
[1] "Americas 2002 : 849772762"
[1] "Americas 2007 : 898871184"
[1] "Oceania 1952 : 10686006"
[1] "Oceania 1957 : 11941976"
[1] "Oceania 1962 : 13283518"
[1] "Oceania 1967 : 14600414"
[1] "Oceania 1972 : 16106100"
[1] "Oceania 1977 : 17239000"
[1] "Oceania 1982 : 18394850"
[1] "Oceania 1987 : 19574415"
[1] "Oceania 1992 : 20919651"
[1] "Oceania 1997 : 22241430"
[1] "Oceania 2002 : 23454829"
[1] "Oceania 2007 : 24549947"

For 혹은 Apply? 두번째 지옥의 순환.

두번째 악순환으로 들어가면, 대식가가 살고 있다.
We made our way into the second Circle, here live the gluttons.
The R inferno

R 초보자나 유경험자가 저지르는 가장 커다란 실수 중에 하나는 루프가 돌때마다 결과 객체(벡터, 리스트, 행렬, 데이터프레임)를 생성해 나가는 것이다. 예를 들어:

results <- data.frame(continent=character(), year=numeric(), popsum=numeric())
for (cc in gap[,unique(continent)]) {
  for (yy in gap[,unique(year)]) {
    popsum <- gap[year == yy & continent == cc, sum(pop)]
    this_result <- data.frame(continent=cc, year=yy, popsum=popsum)
    results <- rbind(results, this_result)
  }
}
results
   continent year     popsum
1       Asia 1952 1395357352
2       Asia 1957 1562780599
3       Asia 1962 1696357182
4       Asia 1967 1905662900
5       Asia 1972 2150972248
6       Asia 1977 2384513556
7       Asia 1982 2610135582
8       Asia 1987 2871220762
9       Asia 1992 3133292191
10      Asia 1997 3383285500
11      Asia 2002 3601802203
12      Asia 2007 3811953827
13    Europe 1952  418120846
14    Europe 1957  437890351
15    Europe 1962  460355155
16    Europe 1967  481178958
17    Europe 1972  500635059
18    Europe 1977  517164531
19    Europe 1982  531266901
20    Europe 1987  543094160
21    Europe 1992  558142797
22    Europe 1997  568944148
23    Europe 2002  578223869
24    Europe 2007  586098529
25    Africa 1952  237640501
26    Africa 1957  264837738
27    Africa 1962  296516865
28    Africa 1967  335289489
29    Africa 1972  379879541
30    Africa 1977  433061021
31    Africa 1982  499348587
32    Africa 1987  574834110
33    Africa 1992  659081517
34    Africa 1997  743832984
35    Africa 2002  833723916
36    Africa 2007  929539692
37  Americas 1952  345152446
38  Americas 1957  386953916
39  Americas 1962  433270254
40  Americas 1967  480746623
41  Americas 1972  529384210
42  Americas 1977  578067699
43  Americas 1982  630290920
44  Americas 1987  682753971
45  Americas 1992  739274104
46  Americas 1997  796900410
47  Americas 2002  849772762
48  Americas 2007  898871184
49   Oceania 1952   10686006
50   Oceania 1957   11941976
51   Oceania 1962   13283518
52   Oceania 1967   14600414
53   Oceania 1972   16106100
54   Oceania 1977   17239000
55   Oceania 1982   18394850
56   Oceania 1987   19574415
57   Oceania 1992   20919651
58   Oceania 1997   22241430
59   Oceania 2002   23454829
60   Oceania 2007   24549947

결과 객체를 “키워나가는” 것은 나쁜 습관이다. 매번 반복을 할 때마다, R이 컴퓨터 운영체제에 신규 결과 객체에 대해서 메모리 할당을 요청해야 된다. 모든 정치협상과 마찬가지로, 시간(적어도 컴퓨터 시간)이 걸린다! 결과적으로, 더 큰 데이터셋 혹은 더 복잡한 연산 작업을 수행할 때, for 루프를 돌리면 시간이 엄청 오래 걸리는 것을 볼 수 있다.

따라서, 결과 객체 크기를 사전에 R에게 정보를 제공하는 것이 훨씬 낫다. 이렇게 함으로써, 적절한 메모리 용량을 한번만 R이 컴퓨터에 요청하면 된다:

# First lets calculate the number of rows we need:
nresults <- gap[,length(unique(continent))] * gap[,length(unique(year))] 
results <- data.frame(
  continent=character(length=nresults), 
  year=numeric(length=nresults), 
  popsum=numeric(length=nresults), stringsAsFactors=FALSE
)
# Instead of iterating over values, we need to keep track of indices so we know
# which row to insert or new results into at each iteration. 
# `seq_along` will create a sequence of numbers based on the length of the 
# vector. So instead of c("Asia", "Americas", "Europe", "Africa", "Oceania"),
# ii will store c(1,2,3,4,5)
continents <- gap[,unique(continent)]
years <- gap[,unique(year)]
# We also need to keep track of which row to insert into. We could do fancy 
# math based on our indices, but this is hard to get right and can lead to hard
# to detect errors. Its much easier to just keep track of this manually. 
this_row <- 1
for (ii in seq_along(continents)) {
  for (jj in seq_along(years)) {
    # Now we need to look-up the appopriate values based on our indices
    cc <- continents[ii]
    yy <- years[jj]
    popsum <- gap[year == yy & continent == cc, sum(pop)]
    results[this_row,] <- list(cc, yy, popsum)
    # Increment the row counter
    this_row <- this_row + 1
  }
}
results
   continent year     popsum
1       Asia 1952 1395357352
2       Asia 1957 1562780599
3       Asia 1962 1696357182
4       Asia 1967 1905662900
5       Asia 1972 2150972248
6       Asia 1977 2384513556
7       Asia 1982 2610135582
8       Asia 1987 2871220762
9       Asia 1992 3133292191
10      Asia 1997 3383285500
11      Asia 2002 3601802203
12      Asia 2007 3811953827
13    Europe 1952  418120846
14    Europe 1957  437890351
15    Europe 1962  460355155
16    Europe 1967  481178958
17    Europe 1972  500635059
18    Europe 1977  517164531
19    Europe 1982  531266901
20    Europe 1987  543094160
21    Europe 1992  558142797
22    Europe 1997  568944148
23    Europe 2002  578223869
24    Europe 2007  586098529
25    Africa 1952  237640501
26    Africa 1957  264837738
27    Africa 1962  296516865
28    Africa 1967  335289489
29    Africa 1972  379879541
30    Africa 1977  433061021
31    Africa 1982  499348587
32    Africa 1987  574834110
33    Africa 1992  659081517
34    Africa 1997  743832984
35    Africa 2002  833723916
36    Africa 2007  929539692
37  Americas 1952  345152446
38  Americas 1957  386953916
39  Americas 1962  433270254
40  Americas 1967  480746623
41  Americas 1972  529384210
42  Americas 1977  578067699
43  Americas 1982  630290920
44  Americas 1987  682753971
45  Americas 1992  739274104
46  Americas 1997  796900410
47  Americas 2002  849772762
48  Americas 2007  898871184
49   Oceania 1952   10686006
50   Oceania 1957   11941976
51   Oceania 1962   13283518
52   Oceania 1967   14600414
53   Oceania 1972   16106100
54   Oceania 1977   17239000
55   Oceania 1982   18394850
56   Oceania 1987   19574415
57   Oceania 1992   20919651
58   Oceania 1997   22241430
59   Oceania 2002   23454829
60   Oceania 2007   24549947

지금까지 살펴봤듯이, 엄청난 작업이 관여되어 있다. R사용자 대부분은 for 루프가 나쁘다고까지 말하고, 대신에 apply를 사용해야 된다고 말하기까지 한다! apply는 다음 학습에서 다룰 예정이고, 추후 또다른 방법을 소개할 것이다: foreach는 객체 생성을 사용자를 대신하여 처리한다.

for 루프는 각 반복이 마지막 결과에 의존하는 연속 계산을 실행할 때 가장 유용하다. (예를 들어, 랜덤 워크)

도전과제 1

gapminder 데이터를 대륙별로 샅샅이 돌려 1952년 평균 기대수명을 출력하는 스크립트를 작성한다.

도전과제 2

연도 뿐만 아니라 대륙도 샅샅이 돌리도록 스크립트를 변경한다.

도전과제 3

100번 난보(랜덤워크, random walk)를 걷는 for 루프를 작성하고 결과를 도식화하시오.

힌트: sign(rnorm(1)) 함수를 for 루프 몸통부문에 사용해서 매번 반복루프를 돌 때마다 임의로 방향을 선택하게 한다.

힌트: 도식화를 위해서 매번 반복을 돈 다음에 결과값을 저장한다. (저장 시작값은 0)

힌트: x-축에 0:100 인덱스를 두고, y-축은 저장된 위치정보로 두고서 plot 함수로 도식화한다. type 인자를 l로 설정해서 경로를 선분으로 연결한다.