2장. 간단한 조작 — 숫자와 벡터

2장. 간단한 조작 — 숫자와 벡터

R에서 가장 자주 만나게 되는 데이터 구조는 벡터예요. 이 장에서는 벡터를 만들고, 이름을 붙이고, 연산하는 기본기를 하나씩 익혀볼게요. 숫자·논리·문자 벡터는 물론, 벡터의 일부만 골라내거나 바꾸는 방법까지 — R의 거의 모든 데이터 작업의 토대가 되는 내용이라 꼭 차근차근 짚고 넘어가야 해요.

출처: R 공식 매뉴얼

본문

R은 이름 붙은 데이터 구조(named data structures) 를 다뤄요. 그중 가장 단순한 구조가 숫자 벡터(vector) 인데, 쉽게 말해 숫자들이 정해진 순서대로 모여 있는 하나의 덩어리예요. 예를 들어 x라는 이름의 벡터 하나에 10.4, 5.6, 3.1, 6.4, 21.7이라는 다섯 개의 숫자를 담고 싶다면, 이렇게 R 명령을 쓰면 돼요.

> x <- c(10.4, 5.6, 3.1, 6.4, 21.7)

이건 할당(assignment) 문장이에요. 여기서 c()는 이 문맥에서 벡터 인자(argument) 를 원하는 만큼 받을 수 있는 함수인데, 인자들을 끝에서 끝으로 이어 붙인 벡터를 결과로 돌려줘요.7

혼자서 식(expression)에 등장하는 숫자는 길이가 1인 벡터로 취급돼요.

할당 연산자 <-의 두 글자를 눈여겨볼까요. <-는 "보다 작다"는 뜻의 <와 "빼기"를 뜻하는 -가 나란히 붙은 모양인데, 말 그대로 값을 받는 객체를 향해 "가리키고" 있어요. 대부분의 상황에서는 = 연산자를 대신 써도 돼요.

할당은 assign() 함수로도 할 수 있어요. 위와 똑같은 할당을 다른 방식으로 하면 이렇게 돼요.

> assign("x", c(10.4, 5.6, 3.1, 6.4, 21.7))

보통 쓰는 <-는 이 함수의 문법적 축약형이라고 생각하면 돼요.

할당은 반대 방향으로도 가능해요. 연산자의 방향만 바꾸면 되죠. 같은 할당을 이렇게 만들 수도 있어요.

> c(10.4, 5.6, 3.1, 6.4, 21.7) -> x

만약 어떤 식을 완전한 명령으로 실행하면, 그 값은 화면에 출력되긴 하지만 그리고 나서 사라져요.8 그래서 지금 아래 명령을 치면,

> 1/x

다섯 값 각각의 역수가 터미널에 출력돼요 (물론 x의 값 자체는 변하지 않아요).

이어서 이렇게 할당하면,

> y <- c(x, 0, x)

x가 두 번 들어가고 가운데에 0이 끼어 있는, 원소 11개의 벡터 y가 만들어져요.

2.2 벡터 연산

벡터는 산술식에서 바로 쓸 수 있어요. 이때 연산은 원소끼리(element by element) 하나씩 수행돼요. 한 식 안에 등장하는 벡터들의 길이가 꼭 같을 필요는 없어요. 길이가 다르면, 그 식의 결과는 식 안에서 가장 긴 벡터와 같은 길이를 갖는 벡터가 돼요. 이때 재활용(recycling) 이 일어나는데, 각 이항 연산에서 더 짧은 벡터가 더 긴 벡터의 길이에 맞을 때까지 (어쩌면 분수만큼이라도) 필요할 때마다 반복돼요. 특히 상수는 그냥 반복돼요. 그래서 위의 할당들을 마친 뒤에 이런 명령을 쓰면,

> v <- 2*x + y + 1

새로운 벡터 v가 만들어져요. 길이는 11이고, 원소별로 2*x가 2.2번 반복된 것, y가 딱 한 번 반복된 것, 1이 11번 반복된 것을 서로 더해 만들어진 거예요.

기본 산술 연산자는 흔히 쓰는 +, -, *, / 그리고 거듭제곱을 뜻하는 ^예요.

게다가 흔한 산술 함수도 모두 쓸 수 있어요. log, exp, sin, cos, tan, sqrt 등이 모두 평소에 알던 의미 그대로 동작해요.

maxmin은 각각 벡터에서 가장 큰 원소와 가장 작은 원소를 골라줘요.

range는 길이가 2인 벡터, 곧 c(min(x), max(x))를 결과로 주는 함수예요.

length(x)x 안의 원소 개수고요, sum(x)x의 원소들의 합을, prod(x)는 원소들의 곱을 돌려줘요.

통계 함수도 둘 있어요. mean(x)는 표본 평균을 계산하는데, sum(x)/length(x)와 같아요. 그리고 var(x)는 표본 분산을 주는데,

sum((x-mean(x))^2)/(length(x)-1)

이렇게 계산한 값이에요. 만약 var()의 인자가 n×p 행렬이면 결과는 p×p 표본 공분산 행렬이 되는데, 행들을 서로 독립인 p변량 표본 벡터로 보고 계산해요.

sort(x)x와 크기가 같은 벡터를 돌려주되, 원소들을 증가하는 순서로 정렬해 줘요. 다만 더 유연한 정렬 기능도 따로 있으니 참고할게요 (order()sort.list()가 정렬을 수행할 순열을 만들어 주는 걸 보면 돼요).

참고로 maxmin은 인자로 벡터를 여러 개 받더라도 그 인자들 전체에서 가장 큰 값·가장 작은 값을 골라요. 이와 달리 병렬 최대·최소 함수인 pmaxpmin은 (가장 긴 인자와 같은 길이의) 벡터를 돌려주는데, 각 위치에서 입력 벡터들 중 그 위치의 가장 큰(작은) 원소를 담아요.

대부분의 경우, 숫자 벡터 안의 "숫자"가 정수인지 실수인지, 심지어 복소수인지는 사용자가 신경 쓸 일이 거의 없어요. 내부적으로 계산은 배정밀도 실수로, 입력 데이터가 복소수라면 배정밀도 복소수로 처리돼요.

복소수로 작업하려면 복소수 부분을 명시적으로 넣어주면 돼요. 그래서

sqrt(-17)

이건 NaN과 경고를 주지만,

sqrt(-17+0i)

이건 복소수로 계산을 수행해요.

2.3 규칙적인 수열 만들기

R에는 자주 쓰는 숫자 수열을 만들어 주는 기능이 여러 개 있어요. 예를 들어 1:30은 벡터 c(1, 2, …, 29, 30)이에요.

콜론 연산자는 식 안에서 우선순위가 높아요. 그래서 예컨대 2*1:15는 벡터 c(2, 4, …, 28, 30)이 되어요. n <- 10이라고 둔 뒤에 1:n-11:(n-1)이라는 두 수열의 차이를 비교해 보면 우선순위가 왜 중요한지 바로 알 수 있어요.

30:1처럼 쓰면 거꾸로 가는 수열도 만들 수 있어요.

seq() 함수는 수열을 더 일반적으로 만들 수 있는 기능이에요. 인자가 다섯 개인데, 한 번 호출할 때 그중 일부만 지정해도 돼요. 처음 두 인자에 수열의 시작과 끝을 주면 되고, 이 둘만 주면 콜론 연산자와 같은 결과가 나와요. 곧 seq(2,10)2:10과 같은 벡터예요.

seq()의 인자는 물론 다른 많은 R 함수의 인자도, 이름을 붙인 형태로 줄 수 있어요. 이때는 인자가 나오는 순서는 상관없어요. 처음 두 인자에는 from=값, to=값이라는 이름을 붙일 수 있어요. 그래서 seq(1,30), seq(from=1, to=30), seq(to=30, from=1)은 모두 1:30과 같아요. seq()의 다음 두 인자에는 각각 by=값length=값이라는 이름을 붙일 수 있는데, 수열의 간격(step)과 길이를 지정해 줘요. 둘 다 지정하지 않으면 기본값 by=1이 적용돼요.

예를 들어

> seq(-5, 5, by=.2) -> s3

이렇게 하면 s3에 벡터 c(-5.0, -4.8, -4.6, …, 4.6, 4.8, 5.0)이 만들어져요. 비슷하게,

> s4 <- seq(length=51, from=-5, by=.2)

이 코드도 s4에 같은 벡터를 만들어 줘요.

다섯 번째 인자는 along=벡터라는 이름으로 줄 수 있는데, 보통 그것만을 인자로 써서 1, 2, …, length(벡터)라는 수열을 만들 때 써요. 벡터가 비어 있으면 (그럴 수도 있어요) 빈 수열을 만들어요.

여기에 관련된 함수로 rep()이 있어요.

이건 객체를 여러 가지 복잡한 방식으로 복제(replicate)할 때 쓰는 함수예요. 가장 단순한 형태는 이렇죠.

> s5 <- rep(x, times=5)

이건 x 다섯 개를 끝에서 끝으로 이어 붙여 s5에 넣어요. 또 유용한 버전으로는,

> s6 <- rep(x, each=5)

이건 x의 각 원소를 다음 원소로 넘어가기 전에 다섯 번씩 반복해요.

2.4 논리 벡터

R은 숫자 벡터뿐 아니라 논리량(logical quantities)도 다룰 수 있어요. 논리 벡터의 원소는 TRUE, FALSE, 그리고 NA(not available, 아래에서 다룰게요)라는 값을 가질 수 있어요. 처음 두 값은 각각 TF로 줄여 쓰기도 해요. 다만 TF는 기본값으로 TRUEFALSE에 맞춰진 그냥 변수일 뿐, 예약어가 아니라서 사용자가 덮어쓸 수 있어요. 그래서 항상 TRUEFALSE를 쓰는 편이 좋아요.

논리 벡터는 조건(condition) 으로 만들어져요. 예를 들어

> temp <- x > 13

이렇게 하면 tempx와 같은 길이의 벡터가 만들어지는데, 조건이 충족되지 않는 x의 원소 자리에는 FALSE가, 충족되는 자리에는 TRUE가 들어가요.

논리 연산자는 <, <=, >, >=, 정확히 같음을 묻는 ==, 같지 않음을 묻는 !=예요.

추가로 c1c2가 논리식이라면, c1 & c2는 두 식의 교집합("그리고", and), c1 | c2는 합집합("또는", or), !c1c1의 부정이에요.

논리 벡터는 보통 산술에서도 쓸 수 있어요. 이때는 숫자 벡터로 강제 변환(coerced) 되는데, FALSE0이 되고 TRUE1이 돼요. 다만 논리 벡터와 그렇게 변환된 숫자 벡터가 항상 동등한 건 아니라는 점, 바로 다음 절에서 만나볼게요.

2.5 결측값

때로는 벡터의 어떤 구성 요소가 완전히 알려지지 않았을 수 있어요. 어떤 원소나 값이 통계적으로 "이용할 수 없거나" "결측값"일 때, 벡터 안의 그 자리에 특별한 값 NA를 넣어 예약해 둘 수 있어요.

일반적으로 NA에 대한 어떤 연산도 결과가 NA가 돼요. 이 규칙의 이유는 단순해요. 연산의 명세가 불완전하면 그 결과를 알 수 없으니, 결과도 "이용할 수 없는" 게 맞기 때문이에요.

is.na(x) 함수는 x와 크기가 같은 논리 벡터를 주는데, x의 해당 원소가 NA일 때만 그 값이 TRUE가 돼요.

> z <- c(1:3,NA);  ind <- is.na(z)

여기서 주의할 점은, 논리식 x == NAis.na(x)와 전혀 다르다는 거예요. NA는 진짜 값이 아니라 "이용할 수 없는 양"을 표시하는 표식이거든요. 그래서 x == NAx와 같은 길이의 벡터인데 그 값이 전부 NA가 돼요. 논리식 자체가 불완전해서 판정할 수 없기 때문이죠.

한편, 숫자 계산에서 생겨나는 두 번째 종류의 "결측" 값도 있어요. 이른바 Not a Number, 곧 NaN 값이에요. 예를 들면,

> 0/0

이나

> Inf - Inf

같은데, 결과를 제대로 정의할 수 없으니 둘 다 NaN을 줘요.

정리하면, is.na(xx)NANaN둘 다에서 TRUE예요. 이 둘을 구분하려면 is.nan(xx)를 쓰는데, 이건 NaN일 때만 TRUE가 돼요.

결측값은 문자 벡터를 따옴표 없이 출력할 때 <NA>로 표시되기도 해요.

2.6 문자 벡터

문자량(character quantities)과 문자 벡터는 R에서 자주 쓰여요. 예를 들어 그래프의 라벨처럼 말이죠. 필요할 때는 큰따옴표로 둘러싼 일련의 문자로 표시해요. 예컨대 "x-values", "New iteration results"처럼요.

문자열은 짝이 맞는 큰따옴표(")나 작은따옴표(')로 입력하지만, 출력은 큰따옴표로 (때로는 따옴표 없이) 돼요. 문자열은 C 스타일의 이스케이프 시퀀스를 쓰는데, 이스케이프 문자로 \를 사용해요. 그래서 \\\로 입력하고 출력하며, 큰따옴표 안에서 "\"로 입력해요. 그 밖에 유용한 이스케이프로는 줄바꿈 \n, 탭 \t, 백스페이스 \b가 있어요. 전체 목록은 ?Quotes에서 볼 수 있어요.

문자 벡터는 c() 함수로 이어 붙여 하나의 벡터로 만들 수 있어요. 그 사용 예시는 앞으로 자주 만나게 될 거예요.

paste() 함수는 인자를 원하는 만큼 받아서 하나씩 이어 붙여 문자 문자열을 만들어요. 인자 중 숫자는 평소 출력될 때와 똑같은 방식으로 문자 문자열로 강제 변환돼요. 인자들은 기본적으로 결과에서 공백 문자 하나로 구분되는데, 이름 붙은 인자 sep=문자열로 바꿀 수 있어요. string으로 바꾸거나, 빈 문자열로 둘 수도 있죠.

예를 들어,

> labs <- paste(c("X","Y"), 1:10, sep="")

이렇게 하면 labs가 아래와 같은 문자 벡터가 돼요.

c("X1", "Y2", "X3", "Y4", "X5", "Y6", "X7", "Y8", "X9", "Y10")

여기서도 짧은 목록의 재활용이 일어난다는 점에 주목할게요. 곧 c("X", "Y")가 수열 1:10에 맞추려고 5번 반복되는 거예요.9

2.7 인덱스 벡터 — 데이터 집합의 부분집합 고르기와 바꾸기

벡터 원소의 부분집합을 고를 때는 벡터 이름 뒤에 대괄호 안의 인덱스 벡터(index vector) 를 붙여 주면 돼요. 더 일반적으로, 벡터로 평가되는 어떤 식이든 그 뒤에 대괄호 안의 인덱스 벡터를 바로 붙여 원소의 부분집합을 고를 수 있어요.

이런 인덱스 벡터는 서로 다른 네 가지 유형이 있어요.

  1. 논리 벡터. 이 경우 인덱스 벡터는 원소를 고를 대상 벡터와 같은 길이가 되도록 재활용돼요. 인덱스 벡터에서 TRUE에 해당하는 값은 선택되고, FALSE에 해당하는 값은 빠져요. 예를 들어
> y <- x[!is.na(x)]

이렇게 하면 x의 결측값이 아닌 값들을 같은 순서로 담은 객체 y가 만들어지거나 다시 만들어져요. 주의할 점은, x에 결측값이 있으면 yx보다 짧아진다는 거예요. 또,

> (x+1)[(!is.na(x)) & x>0] -> z

이렇게 하면 객체 z가 만들어지고, 그 안에 x의 해당 값이 결측값도 아니고 양수이기도 한 위치의 x+1 값들이 담겨요.

  1. 양의 정수로 된 벡터. 이 경우 인덱스 벡터의 값들은 {1, 2, …, length(x)} 집합 안에 있어야 해요. 벡터의 해당 원소들이 선택돼서 그 순서대로 이어 붙은 결과가 돼요. 인덱스 벡터의 길이는 아무 길이나 될 수 있고, 결과는 인덱스 벡터와 같은 길이가 돼요. 예컨대 x[6]x의 여섯 번째 성분이고,
> x[1:10]

이건 x의 처음 10개 원소(length(x)가 10 이상일 때)를 선택해요. 또,

> c("x","y")[rep(c(1,2,2,1), times=4)]

(솔직히 잘 안 하는 짓이긴 한데) 길이 16의 문자 벡터를 만들어요. "x", "y", "y", "x" 패턴이 네 번 반복되는 거예요.

  1. 음의 정수로 된 벡터. 이런 인덱스 벡터는 포함할 값을 지정하는 게 아니라 제외할 값을 지정해요. 그래서
> y <- x[-(1:5)]

이건 x의 첫 다섯 원소를 뺀 나머지 전부를 y에 넣어 줘요.

  1. 문자 문자열로 된 벡터. 이 방식은 객체가 자기 성분을 구분하는 names 속성을 가질 때만 적용돼요. 이 경우 이름 벡터의 부분 벡터를, 위 2번 항목의 양의 정수 라벨과 똑같은 방식으로 쓸 수 있어요.
> fruit <- c(5, 10, 1, 20)
> names(fruit) <- c("orange", "banana", "apple", "peach")
> lunch <- fruit[c("apple","orange")]

여기서의 이점은 숫자 인덱스보다 알파벳 이름이 기억하기 훨씬 쉽다는 거예요. 이 방식은 데이터 프레임과 함께 쓸 때 특히 유용한데, 이건 나중에 만나볼게요.

인덱스된 식은 할당의 받는 쪽에도 나타날 수 있어요. 이 경우 할당 연산은 그 벡터의 해당 원소에만 수행돼요. 식은 벡터[인덱스_벡터] 형태여야 해요. 벡터 이름 자리에 임의의 식을 쓰는 건 이 맥락에서는 별 의미가 없거든요.

예를 들어

> x[is.na(x)] <- 0

이건 x의 결측값을 전부 0으로 바꿔 주고,

> y[y < 0] <- -y[y < 0]

이건 아래와 같은 효과를 내요.

> y <- abs(y)

2.8 그 밖의 객체 유형

벡터는 R에서 가장 중요한 객체 유형이지만, 나중에 더 정식으로 만나게 될 다른 유형들도 여럿 있어요.

  • 행렬(matrices), 더 일반적으로는 배열(arrays) 은 벡터를 다차원으로 일반화한 거예요. 사실 이들은 두 개 이상의 인덱스로 색인할 수 있는 벡터들이고, 특별한 방식으로 출력돼요. Arrays and matrices를 보세요.
  • 요인(factors) 은 범주형 데이터를 다루는 간결한 방법을 제공해요. Ordered and unordered factors를 보세요.
  • 리스트(lists) 는 벡터의 일반화된 형태로, 여러 원소가 꼭 같은 유형일 필요는 없고, 종종 그 원소들이 다시 벡터나 리스트이기도 해요. 리스트는 통계 계산의 결과를 돌려주기에 편리한 방법이에요. Lists를 보세요.
  • 데이터 프레임(data frames) 은 행렬과 비슷한 구조인데, 열마다 유형이 달라도 돼요. 데이터 프레임을 관측 단위마다 한 행씩 있되 (어쩌면) 숫자 변수와 범주형 변수를 함께 담는 "데이터 행렬"이라고 생각해 보세요. 많은 실험은 데이터 프레임으로 가장 잘 표현돼요. 처리는 범주형인데 반응은 숫자형이니까요. Data frames를 보세요.
  • 함수(functions) 도 그 자체로 R에서 객체라서 프로젝트 작업 공간에 저장할 수 있어요. 이건 R을 확장하는 간단하고 편리한 방법이 돼요. Writing your own functions를 보세요.

더 알아보기

  • 벡터에 이름을 붙이는 names()는 이 장에서 잠깐 보았는데, 데이터 프레임에서 열 이름을 다룰 때 핵심이 돼요. 뒤의 데이터 프레임 장에서 다시 보게 될 거예요.
  • 조건에 따라 값이 달라지는 벡터를 만들고 싶다면 ifelse()TRUE/FALSE 인덱스와 자연스럽게 이어져요.
  • 벡터 원소를 고르는 대괄호 인덱싱은 R 데이터 작업의 만능 열쇠라서, 데이터 프레임의 행·열을 자르는 데까지 계속 쓰여요.
  • 매뉴얼 원문에서는 이 장 다음에 객체의 모드(mode)와 속성(attribute)을 다루는데, "모든 게 벡터다"라는 R의 사고방식과 연결되니 이어서 읽어 보면 좋아요.