데이터분석 전문가(ADP) 시험 노트개념 정리15 MIN
R 자료 구조와 제어문, 파일 입출력
ADP 4과목의 첫 자리입니다. R의 자료형과 형 변환 규칙, 벡터·행렬·배열·데이터프레임·리스트, 조건문과 반복문, 사용자 정의 함수와 apply 계열, 파일 읽기와 쓰기를 코드 읽기 문제와 함께 정리합니다.
4과목 「데이터분석」은 R 코드를 읽고 결과를 맞히는 문항으로 시작합니다. 통계와 머신러닝 문항도 결국 R의 출력을 보여 주고 해석하게 하므로, 이 노트의 자료 구조가 뒤의 모든 노트의 바닥입니다. 외울 함수가 많아 보이지만 실제로 틀리는 자리는 몇 군데로 모입니다 — 형이 섞일 때 무엇으로 바뀌는가, 행렬이 어느 방향으로 채워지는가, apply 계열이 무엇을 돌려주는가입니다.
자료형
기본 자료형
R의 값 하나하나는 자료형을 가집니다. 수치형(numeric), 정수형(integer), 문자형(character), 논리형(logical), 복소수형(complex)이 기본이고, class()나 typeof()로 확인합니다. 3은 수치형이고 정수형으로 쓰려면 3L처럼 L을 붙입니다.
값이 없음을 나타내는 표시가 셋이라 자주 묻습니다. NA는 값이 있어야 할 자리가 비어 있다는 결측이고, NULL은 자리 자체가 없다는 뜻이며, NaN은 0/0처럼 계산 결과가 수가 아닌 경우입니다. 그래서 length(NA)는 1이고 length(NULL)은 0입니다.
형 변환
as.numeric(), as.integer(), as.character(), as.logical()처럼 as.로 시작하는 함수가 명시적 형 변환입니다. 바꿀 수 없는 값은 오류가 아니라 경고와 함께 NA가 됩니다. as.numeric("abc")가 그 예이고, as.integer(3.9)는 반올림이 아니라 소수점 아래를 버려 3이 됩니다.
더 자주 나오는 것은 암묵적 변환입니다. 벡터는 한 가지 형만 담을 수 있어서 형이 섞이면 더 넓은 쪽으로 맞춥니다. 순서는 논리형 < 정수형 < 수치형 < 복소수형 < 문자형입니다.
c(1, TRUE, FALSE) # 1 1 0 — 논리형이 수치형으로
c(1, "2", TRUE) # "1" "2" "TRUE" — 전부 문자형으로
sum(c(TRUE, TRUE, FALSE)) # 2 — TRUE는 1로 센다
자료 구조
벡터
벡터는 같은 형의 값을 한 줄로 늘어놓은 것이고 R의 가장 작은 단위입니다. 값 하나도 길이 1인 벡터입니다. c(), seq(), rep(), 1:10으로 만들고, 대괄호 안의 조건으로 거릅니다. 인덱스가 1부터 시작하고 음수 인덱스는 「그 자리를 뺀다」는 뜻입니다.
길이가 다른 두 벡터를 계산하면 짧은 쪽을 되풀이해 맞추는데, 이것을 재사용 규칙이라 합니다. c(1, 2, 3, 4) + c(10, 20)은 11 22 13 24입니다. 긴 쪽 길이가 짧은 쪽의 배수가 아니면 계산은 되지만 경고가 나옵니다.
행렬과 배열
행렬은 벡터에 행과 열 두 차원을 준 것이고, 배열은 그것을 셋 이상의 차원으로 늘린 것입니다. 둘 다 벡터처럼 한 가지 형만 담습니다. matrix()는 값을 열 방향으로 먼저 채우고, 행 방향으로 채우려면 byrow = TRUE를 줍니다.
m <- matrix(1:6, nrow = 2)
#> [,1] [,2] [,3]
#> [1,] 1 3 5
#> [2,] 2 4 6
dim(m) # 2 3
m[2, 3] # 6
t(m) # 전치: 3행 2열
a <- array(1:24, dim = c(2, 3, 4)) # 2행 3열짜리가 4장
행렬끼리 원소별로 곱하는 것은 *이고 행렬곱은 %*%입니다. cbind()와 rbind()로 열이나 행을 붙여 행렬을 만들 수도 있습니다.
데이터프레임과 리스트
리스트는 형이 다른 것들을 한 묶음에 담는 구조입니다. 원소가 벡터든 행렬이든 다른 리스트든 상관없습니다. [로 꺼내면 리스트가 그대로 돌아오고 [[나 $로 꺼내야 원소 자체가 나옵니다.
데이터프레임은 길이가 같은 벡터들을 열로 세운 리스트입니다. 열마다 형이 달라도 되고 한 열 안에서는 같아야 합니다 — 행렬과 가르는 지점이 이것입니다. 분석 데이터는 거의 전부 데이터프레임으로 다룹니다.
| 구조 | 차원 | 담는 형 |
|---|---|---|
| 벡터 | 1 | 한 가지 |
| 행렬 | 2 | 한 가지 |
| 배열 | 2 이상 | 한 가지 |
| 데이터프레임 | 2 | 열마다 다를 수 있다 |
| 리스트 | 1 | 원소마다 다를 수 있다 |
str()이 구조를, head()가 앞 몇 줄을, summary()가 열마다의 요약을 보여 줍니다. 문자열 범주를 수준(level)이 있는 값으로 바꾼 것이 요인(factor)이고, factor()로 만들며 levels()로 수준을 봅니다.
제어문
조건문
if (조건) { } else { }는 조건 하나를 보고 갈래를 고릅니다. 조건 자리에 길이가 2 이상인 벡터를 넣으면 R 4.2부터는 오류입니다. 벡터 전체에 조건을 걸 때는 ifelse(조건, 참일 때, 거짓일 때)를 씁니다.
ifelse(c(5, -2, 0) > 0, "양", "비양") # "양" "비양" "비양"
반복문
for (i in 벡터)는 벡터의 원소를 차례로 돌고, while (조건)은 조건이 참인 동안 돌며, repeat는 break를 만날 때까지 돕니다. 반복 안에서 next는 이번 회차를 건너뛰고 break는 반복을 끝냅니다.
i <- 0; s <- 0
while (i < 5) {
i <- i + 1
if (i == 3) next
s <- s + i
}
s # 12 — 1 + 2 + 4 + 5
R에서는 반복문보다 벡터 연산이 훨씬 빠릅니다. for로 원소를 하나씩 더하는 코드를 sum(x) 한 줄로 바꾸는 것이 R다운 쓰기이고, 반복이 꼭 필요할 때도 다음 절의 apply 계열이 먼저 떠올라야 합니다.
함수
사용자 정의 함수
function(인자) { 본문 }으로 사용자 정의 함수를 만들고 이름에 대입합니다. 마지막으로 계산된 값이 돌려주는 값이 되고, 중간에 끝내려면 return()을 씁니다. 인자에 기본값을 줄 수 있고, 부를 때 이름으로 넘기면 순서를 바꿔도 됩니다.
cv <- function(x, na.rm = TRUE) {
sd(x, na.rm = na.rm) / mean(x, na.rm = na.rm)
}
cv(c(2, 4, 6)) # 0.5 — 표준편차 2, 평균 4
함수 안에서 만든 변수는 함수 밖에서 안 보입니다. 밖의 변수를 함수 안에서 바꾸려면 <<-를 써야 하는데, 결과가 추적하기 어려워지므로 실무에서는 값을 돌려받아 다시 대입합니다.
apply 계열 함수
반복문 대신 함수를 한 묶음에 고루 적용하는 것이 apply 계열입니다. 무엇을 받아 무엇을 돌려주는가가 문항의 전부입니다.
| 함수 | 받는 것 | 돌려주는 것 |
|---|---|---|
apply(X, MARGIN, FUN) |
행렬·배열. 1은 행, 2는 열 | 벡터나 행렬 |
lapply(X, FUN) |
벡터·리스트 | 리스트 |
sapply(X, FUN) |
벡터·리스트 | 줄일 수 있으면 벡터나 행렬 |
tapply(X, INDEX, FUN) |
벡터와 그룹 | 그룹별 결과 |
mapply(FUN, ...) |
여러 벡터 | 원소끼리 짝지어 적용한 결과 |
apply(m, 1, sum) # 9 12 — 행 합
apply(m, 2, sum) # 3 7 11 — 열 합
sapply(1:4, function(i) i^2) # 1 4 9 16
tapply(c(10, 20, 30, 40), c("a", "b", "a", "b"), mean) # a 20, b 30
파일 입출력
파일 읽기
표 모양 텍스트는 read.csv()와 read.table()로 읽어 데이터프레임을 받습니다. read.csv()는 쉼표 구분에 첫 줄을 열 이름으로 보는 것(header = TRUE)이 기본이고, read.table()은 공백 구분에 header = FALSE가 기본입니다. 한글 파일이 깨지면 fileEncoding = "CP949"나 "UTF-8"을 줍니다.
그 밖에 줄 단위 문자열은 readLines(), 값을 벡터로 바로 읽는 것은 scan(), R 객체 하나를 저장한 파일은 readRDS(), 여러 객체를 저장한 .RData는 load()로 읽습니다. 엑셀 파일은 기본 함수로 못 읽어 readxl 같은 패키지를 씁니다.
파일 쓰기
읽기와 짝이 되는 것이 write.csv(), write.table(), saveRDS(), save()입니다. write.csv()는 행 이름을 첫 열로 함께 쓰는 것이 기본이라 다시 읽으면 뜻 없는 X 열이 하나 생깁니다. 막으려면 row.names = FALSE를 줍니다.
write.csv(df, "out.csv", row.names = FALSE)
saveRDS(model, "model.rds") # 객체 하나 — readRDS로 이름을 새로 붙여 읽는다
save(df, model, file = "work.RData") # 여럿 — load는 원래 이름으로 되살린다
saveRDS()와 save()의 차이가 지문으로 나옵니다. 앞쪽은 객체 하나를 저장해 읽을 때 원하는 이름에 대입하고, 뒤쪽은 여러 객체를 이름째 저장해 읽으면 작업 공간에 원래 이름으로 되살아납니다.
연습 문제
다음 코드의 결과는?
x <- c(1, "2", TRUE); class(x)
① "numeric"
② "logical"
③ "character"
④ "list"③. 벡터는 한 가지 형만 담으므로 가장 넓은 문자형으로 맞춰져"1" "2" "TRUE"가 됩니다. 형이 다른 것을 그대로 담으려면 리스트를 써야 합니다.다음 코드의 결과는?
m <- matrix(1:12, nrow = 3); m[2, 3]
① 6
② 8
③ 9
④ 11②. 열 방향으로 채우므로 첫 열이 1·2·3, 둘째 열이 4·5·6, 셋째 열이 7·8·9입니다. 2행 3열은 셋째 열의 두 번째 값인 8입니다. 행 방향으로 채웠다면 7이 됩니다.다음 코드의 결과는?
x <- 1:10; sum(x[x %% 3 == 0])
① 12
② 18
③ 27
④ 55②.%%는 나머지이므로 3으로 나누어떨어지는 3·6·9만 남고 합은 18입니다.리스트를 받아 원소마다 함수를 적용하고 결과를 반드시 리스트로 돌려주는 함수는?
① apply
② lapply
③ sapply
④ tapply②.sapply는 결과를 줄일 수 있으면 벡터나 행렬로 바꾸고,apply는 행렬의 행이나 열에,tapply는 그룹별로 적용합니다.다음 코드의 결과는?
i <- 0; s <- 0; while (i < 6) { i <- i + 1; if (i %% 2 == 0) next; s <- s + i }; s
① 6
② 9
③ 12
④ 21②. i가 1부터 6까지 오르는 동안 짝수 회차는next로 건너뛰므로 1·3·5만 더해져 9입니다.next대신break였다면 i가 2일 때 반복이 끝나 1이 됩니다.write.csv(df, "a.csv")로 저장한 3열짜리 데이터프레임을read.csv("a.csv")로 다시 읽었을 때의 열 수와 그 이유로 옳은 것은?
① 3열. 저장한 그대로 읽힌다
② 4열. 행 이름이 첫 열로 함께 저장됐다
③ 2열. 첫 줄이 열 이름으로 쓰였다
④ 3열. 행 이름은 저장되지 않는다②.write.csv()는row.names = TRUE가 기본이라 행 이름이 한 열로 들어가고, 읽으면 그 열이X라는 이름으로 생깁니다.서술형 연습입니다. 고객 번호(문자), 나이(정수), 가입 여부(논리)로 이뤄진 표를 행렬로 담았더니 나이의 평균을 구할 수 없었습니다. 이유를 짚고, 알맞은 자료 구조와 평균을 구하는 코드를 5줄 이내로 쓰시오.
채점 기준은 셋입니다. (1) 원인을 짚었는가 — 행렬은 한 가지 형만 담으므로 가장 넓은 문자형으로 전부 바뀌었고, 문자형에는mean()이NA와 경고를 돌려줍니다. (2) 구조를 바르게 골랐는가 — 열마다 형이 달라도 되는 데이터프레임이 답입니다. 리스트도 형을 지키지만 열 단위 표 연산에 맞지 않아 부분 점수입니다. (3) 코드가 돌아가는가 —df <- data.frame(id = c("C01", "C02"), age = c(31L, 45L), member = c(TRUE, FALSE))로 만들고mean(df$age)로 구하면 됩니다. 이미 문자형이 된 값을as.numeric()으로 되돌리는 답도 인정하되, 구조를 바꾸지 않으면 배점의 3분의 1만 붙습니다.

