흐름제어 01: 조건과 반복
흐름 제어
재연 가능과 스크립트(Scripts)
- 스크립트란 순차적으로 실행하게 될 함수들을 모아 놓은 텍스트이다. 이는 두 가지 이유에서 큰 의미가 있다.
- 동일한 분석 과정을 재연할 수 있다. 따라서 다른 사람이 쉽게 분석 과정을 검증하고 수정할 수 있다.
- 동일한 분석을 다른 자료에 적용할 수 있다.
- 특히 최근에 재연가능한 연구가 큰 이슈가 되었다. 그 하위 개념으로 재연 가능한 분석은 누구라도 동일한 데이터에 동일한 분석을 적용해서 동일한 분석 결과를 얻을 수 있음을 의미한다.
- R에서 재연 가능한 분석을 하기 위해서는 다음과 같이 설정하는 것이 좋다.
- 스크립트를 실행하기 전에
rm(list=ls())
을 실행하여 관련없는 변수를 모두 삭제한다. - 분석을 하기 전에 혹시 남아 있을 수 있는 변수를 모두 삭제하는 게 좋다. R Studio – Options – General – Restore .Rdata into workspace at startup은 R Studio 시작 시
.RData
(저장해둔 환경)을 불러들여오는지를 설정한다. 특별한 이유가 없다면 체크하지 않는 것이 좋다.
- 스크립트를 실행하기 전에
제어문: 조건 또는 반복
- 일련의 함수들 중 일부를 조건에 따라, 혹은 반복적으로 실행해야 할 경우가
있다.- 조건문 :
if
/else
,ifelse
,switch
- 반복문 :
for
,while
,repeat
(next
,break
)
- 조건문 :
조건문
- 조건에 따라 다른 결과를 얻게 되는
if
/else
,ifelse
,switch
중ifelse
와switch
는 함수이고,if
/else
는 흐름 제어문이다.if
/else
의 의미는 바뀔 수 없고,switch
와ifelse
는 다른 함수로 정의할 수도 있다.
s = 'here'
if (s == "here") {
print("Seoul")
} else {
print("Somewhere else than Seoul")
}
## [1] "Seoul"
s2 <- ifelse(s=="here",
"Moon", # if s=="here"
"Somewhere else than Moon") # if s!="here"
x <- "two"
switch(x,
one = 1,
two = 2,
3)
## [1] 2
x <- "two"; z <- 3
switch(x,
one=1,
two={
y <- paste(x,"is entered.")
z + 1},
three=z^2)
## [1] 4
- 문제: 변수
x
에1
,2
,3
,4
중의 한 수가 저장되어 있다.1
일 때는"one"
,2
일 때는"two"
, 그 외에는else!
라고 출력하는 R 스크립트를 작성하세요.
반복문
- 다음은 1부터 10까지 더하는 프로그램을
for
,while
,repeat
를 사용하여 구현하였다. for, while, repeat는 거의 모든 컴퓨터 언어에서 사용하는 반복문이기 때문에 쉽게 이해할 수 있을 것이다. 단,repeat
의 경우 조건 반복이 아니라 무조건 무한 반복하는 제어문이기break
로 밖으로 빠져 나올 수 있어야 한다.
# for
s=0
for (i in 1:10)
s = s + i
print(s)
## [1] 55
# while
s=0; i=1
while (i <= 10) {
s=s+i;
i=i+1
}
print(s)
## [1] 55
# repeat
s=0; i=1
repeat {
s=s+i; i=i+1
if (i>10) break
}
print(s)
## [1] 55
break
는for
,while
,repeat
를 끝내라는 의미이고,next
는 반복되는 부분 중 나머지를 건너뛰고 반복되는 부분의 처음으로 되돌아 가라는 의미이다.
다중 반복문
for
와 같은 반복 안에 다시for
를 써서 다중 반복문을 만들 수 있다. 반복 안의 반복이다. 러시아의 마트료시카[1] 나 영화 인셉션(Inception)[2] 의 꿈 속의 꿈과 비슷하다.- 다중 반복문에서 까다로운 문제 하나는 어떻게 깊은 꿈 속에서 깨어나는가이다. 여기에는 두 가지 선택지가 있을 수 있다.
- 여러 번
break
를 반복한다.[3] - 함수 안에서
return
을 쓴다.
- 여러 번
[1] : 까고 까도 새로운 인형이 나오는 러시아의 목각 인형
[2] : 꿈 속에서 다시 꿈을 꾸고, 그 꿈 속에서 다시 꿈을 꾸는 내용의 영화
[3] : 몇몇 프로그래머들은 이런 이유로 구조적 프로그래머들의 주장과 달리 goto
문을 적절하게 사용할 수 있어야 한다고 주장한다.(구조적 프로그래밍: goto
와 같이 멋대로 분기하는 프로그램은 스파게티 프로그램(스파게티처럼 너무 얽키고 설켜서 사람이 이해하기 힘든 프로그램)이 되기 때문에 goto
문을 금지하고 for
또는 while
과 같은 반복문, 조건문으로 대체하는 프로그래밍 방법)
for (i in 1:10) {
for (j in 1:10) {
for (z in 1:10) {
if (i+j+z == 15) break
}
if (i+j+z == 15) break
}
if (i+j+z == 15) break
}
print(c(i,j,z))
## [1] 1 4 10
f = function() {
for (i in 1:10) {
for (j in 1:10) {
for (z in 1:10) {
if (i+j+z == 15) return(c(i,j,z))
}
}
}
}
f()
## [1] 1 4 10
반복문 다시 보기
반복문의 기본적인 구조
- 결과를 저장할 장소(변수)를 마련한다.
- 특정한 값을 따라, 혹은 주어진 조건이 만족할 때까지 반복하도록 한다.
- 반복되는 내용
- 예: 1부터 10까지 더하기
s=0 # 결과를 저장할 변수 s
for (i in 1:10) # 변수 i가 1부터10까지 반복된다.
s = s + i # 반복되어 수행되는 작업
print(s)
## [1] 55
- 예: 벡터
x
의 모든 원소에 대해 제곱한 값 구하기
x = c(1, 3, 5, 9, 15)
s = rep(NA, 5) # 결과를 저장할 변수 s
#for (i in 1:5) # 변수 가i 벡터 의x 길이에 맞춰 변한다.
#for (i in 1:length(x))
for (i in seq_along(x))
s[i] = x[i]^2 # 반복되어 수행되는 작업
print(s)
## [1] 1 9 25 81 225
- 위의 코드에는
for
문을 돌리는 3 가지 방법을 주석문에 제시하였다.for (i in 1:5)
: 만약 벡터의 길이를 알고, 그 길이가 변하지 않는다면.for (i in 1:length(x))
: 만약 벡터의 길이를 사전에 알 수 없다면. 이때 잠재적 문제의 하나는 벡터x
의 길이가 0일 수도 있다는 점이다. 만약 벡터x
의 길이가 0이라면for (i in 1:0)
이 실행되고x[1]
과x[0]
은 모두NULL
이다.for (i in seq_along(x))
: 위의 단점 때문에 Hadley(2017)은 이 방법을 권장했다.seq_along(x)
는 벡터x
의 길이가 0보다 클 때에는1:length(x)
와 동일하다. 만약 벡터x
의 길이가 0이라면for
문은 실행되지 않는다. 하지만 필자가 이 방법을 사용해 본 결과 단점이 전혀 없는 것은 아니었다. 만약 계속 스크립트를 수정, 개선하고 디버깅을 하고 있는 경우에는for
문을i
가1
이 아니라 중간 숫자부터 돌려봐야 하는 경우가 생기게 마련이기 때문이다. 이 경우에는 다음의 형태를 사용하면 될 것이다.
x = c(1, 3, 5, 9, 15)
s = rep(NA, 5)
if (length(x)>0) {
for (i in 1:length(x)) {
s[i] = x[i]^2
}
}
반복문의 대체
- 다음의 코드는
for
문을 대체하는 두 가지 방법을 보여준다.- 벡터화된 함수 : 벡터화된 함수는 벡터의 모든 원소에 함수를 적용한다.
sapply
함수 :sapply(x, func)
는x[[1]]
에func
을 적용한 결과를 첫 번째 원소에,x[[2]]
에func
을 적용한 결과를 두 번째 원소에 저장한다.
#01. Vectorized function
x <- c(1, 3, 5, 9, 15)
s <- sqrt(x); s
## [1] 1.000000 1.732051 2.236068 3.000000 3.872983
#02. sapply
x <- c(1, 3, 5, 9, 15)
s <- sapply(x, sqrt); s
## [1] 1.000000 1.732051 2.236068 3.000000 3.872983
- 다음의 예는 벡터화되지 않은 함수를
Vectorize
란 함수를 활용하여 벡터화하는 과정을 보여준다.
#01. if the function is not vectorized
tonum = function(x) {
switch(x,
one = 1,
two = 2,
3)
}
x <- c("one","three","two","four","two")
s <- tonum(x)
## Error in switch(x, one = 1, two = 2, 3): EXPR must be a length 1 vector
tonumV = Vectorize(tonum)
s <- tonumV(x)
print(s)
## one three two four two ## 1 3 2 3 2
#02. sapply with Vectorized function
x <- c("one","three","two","four","two")
s <- sapply(x, tonumV)
print(s)
## one.one three.three two.two four.four two.two ## 1 3 2 3 2
sapply
을for
문을 대신해서 쓸 수 있다. 예전에 작성된 R 스크립트에 종종 등장한다. 하지만for
문과 달리next
/break
를 사용할 수 없고, 최근에는 속도도 더 느리기 때문에 다른 언어 사용자들을 골탕먹일 의미가 아니라면 굳이 사용할 이유가 없어 보인다.
# 01a. for
x <- c(1,3,7,2,5)
s <- rep(NA, length(x))
for (i in 1:10) {
s[i] <- x[i]^2
}
# 01b. sapply
x <- c(1,3,7,2,5)
s <- rep(NA, length(x))
s <- sapply(1:5, function(i) x[i]^2)
# 02a. for
x <- c(1,3,7,2,5)
s <- 0
for (i in 1:10) {
s <- s + x[i]^2
}
# 02b. sapply
x <- c(1,3,7,2,5)
s <- 0
invisible(sapply(1:5, function(i) s <<- x[i]^2))
for
의 몇 가지 변형
for (x in xs), for (nm in names(xs))
for
문은for (i in 1:100)
처럼 인덱스를 하나씩 증가시키지 않고, 필요한 내용을 벡터 또는 리스트에 저장하여서 반복하거나, 필요한 내용을 담고 있는 벡터 또는 리스트의 원소 이름을 사용하여 반복할 수도 있다.
- 결과의 길이가 가변적일 때 : 결과를 먼저 리스트에 담는다.
x <- c(1,3,2,4)
result = vector("list", length(x))
#result = c()
for (i in seq_along(x)) {
result[[i]] = rep(x,x)
#result = c(result, rep(x,x))
}
result <- unlist(result)
- 위의 코드에서 주석은 초보자가 하기 쉬운 실수를 보여주고 있다. 처음에 빈 벡터를 하나 만든 후에 계속 기존의 벡터와 새로운 벡터를 concatenate(연결) 해 나간다(
result=c(result, rep(x,x))
). 하지만 이런 방법은 벡터가 길어질 수록 엄청나게 느려지는 단점이 있으므로 지양해야 한다. - 반복의 횟수가 가변적일 때 :
while
,repeat/break
- 만약 반복의 횟수가
for
을 시작하기 전에 알 수 없을 때에는while
또는repeat
문을 써야 한다.
- 만약 반복의 횟수가
반복문의 속도 비교
- 동일한 반복 작업을 수행하는 여러 가지 다른 방법의 속도를 비교해 보면 속도의 순서는 대부분 다음과 같다.
R의 내장 함수 >
apply
>for
mat <- matrix(1:1000, 1000, 1000, byrow=T)
# R 내장 함수
result <- colSums(mat)
# apply
result <- apply(mat, 2, sum)
# for
result <- rep(NA, 1000) # result <- NA, length(result) <- 1000
for (i in 1:ncol(mat)) {
result[i] <- sum(mat[,i])
}
- R의 과거 버전에서는
for
문을 쓰는 것보다apply
을 쓰는 것이 압도적으로 빨랐다. 하지만 Hadley가 지적했듯이 최근 버전의 R에서는for
문을 사용해도 성능 저하가 그리 크지 않다. - 여기서 잠깐 실험을 해보자. 아래의 코드로 동일한 기능(행렬의 각 열을 합한다)을 하는 서로 다른 방법에 대한 소요 시간을 측정해 보면 다음과 같다.
#gc()
mat <- matrix(1:1000, 1000, 1000, byrow=T)
result <- vector(mode="list", 1000)
# R 내장 함수
tColsum <- system.time({for (i in 1:1000)
result[[i]] <- colSums(mat)})
#gc()
mat <- matrix(1:1000, 1000, 1000, byrow=T)
result <- vector(mode="list", 1000)
# for
tFor <- system.time(for(iter in 1:1000) {
result[[iter]] <- rep(NA, 1000) # result <- NA, length(result) <- 1000
for (i in 1:ncol(mat)) {
result[[iter]][i] <- sum(mat[,i])
}})
#gc()
mat <- matrix(1:1000, 1000, 1000, byrow=T)
result <- vector(mode="list", 1000)
# apply
tApply <- system.time({for (i in 1:1000)
result[[i]] <- apply(mat, 2, sum)
})
colSums |
apply( , 2, sum) |
for |
|
---|---|---|---|
소요 시간(초) | 0.86 | 10.23 | 5.45 |
- 위의 결과를 보면 속도 순서는 첫 번째가 R에서 그 기능만을 위해 특별히 제작된 함수, 두 번째가
for
, 세 번째가apply
이다. 이 경우에는 위에서 얘기한 일반적인 경우와 다소 큰 차이를 보인다. 따라서 속도가 중요한 경우에는 상황과 조건에 맞춰 모의 실험을 해보는 것이 좋을 것이다.
Leave a comment