벡터 - R 의 기본 단위
이 장에서 배우는 것
R 에서 숫자 하나를 만들어도 그 값은 길이가 1인 벡터(vector)다. 벡터는 같은 종류의 값을 한 줄로 늘어놓은 것이다. 엑셀에서 한 열을 통째로 선택해 다루는 일과 비슷하다. 이 장에서는 편의점 하루 판매 기록을 벡터로 만들고, 계산하고, 필요한 칸만 꺼내 보는 방법을 익힌다.
이후 장에서 다룰 데이터 프레임, 함수, 그래프는 모두 벡터 위에 세워져 있다. 그래서 이 장의 규칙을 정확히 알아 두면 뒤에서 만나는 오류 메시지의 대부분을 스스로 읽을 수 있다.
- c(), seq(), rep(), 콜론(:) 으로 원하는 벡터를 만든다.
- 벡터끼리의 연산이 원소 단위로 일어난다는 것과, 길이가 다를 때 짧은 쪽이 다시 쓰이는 재사용 규칙을 설명한다.
- 양수·음수·논리 값 세 가지 방법으로 원소를 골라낸다.
- 원소에 이름을 붙이고 이름으로 꺼낸다.
문제 상황
편의점 점주가 오늘 팔린 상품 여섯 가지의 단가와 판매 수량을 적어 왔다. 점주가 알고 싶은 것은 세 가지다. 상품별 매출은 얼마인지, 매출이 2만 원을 넘긴 상품은 무엇인지, 그 상품들의 매출을 합치면 얼마인지.
엑셀이라면 단가 열과 수량 열을 곱하는 수식을 만들어 아래로 끌어 내리고, 필터를 걸어 합계를 볼 것이다. R 에서는 열 하나가 벡터 하나에 해당하고, 수식을 끌어 내리는 일 없이 price * qty 한 줄로 끝난다. 필터는 대괄호 안에 조건을 쓰는 것으로 대신한다. 이 장의 목표는 그 한 줄이 왜 그렇게 동작하는지 이해하는 것이다.
벡터 만들기와 연산
c() 로 값을 이어 붙이기
가장 기본적인 도구는 c() 다. 결합(combine)이라는 뜻으로, 괄호 안의 값을 순서대로 이어 붙여 벡터 하나를 만든다. 상품 이름은 문자열, 단가와 수량은 숫자로 만든다.
items <- c("kimbap", "water", "ramen", "milk", "coffee", "snack")
price <- c(1500, 800, 1300, 2200, 1800, 1700)
이 책의 예제에서는 출력이 어느 환경에서나 똑같이 정렬되도록 상품 이름을 영문 소문자로 적는다. 벡터에는 한 가지 종류의 값만 담을 수 있다. 숫자와 문자열을 섞어 c() 에 넣으면 R 이 모두 문자열로 바꿔 버린다.
print(c(1, "2", TRUE))
[1] "1" "2" "TRUE"
숫자 1 이 문자열 "1" 로 바뀐 것을 볼 수 있다. 이런 자료형 변환의 규칙은 다음 장에서 자세히 다룬다. 지금은 한 벡터 안의 값은 모두 같은 종류라는 점만 기억하면 된다. 또 벡터 안에 벡터를 넣어도 구조가 겹치지 않고 한 줄로 펼쳐진다. c(1, 2, c(3, 4)) 는 1 2 3 4 와 같다.
규칙적인 벡터: seq, rep, 콜론
값이 규칙을 따르면 하나씩 적을 필요가 없다. 아래 표에 자주 쓰는 방법을 모았다.
| 함수 | 하는 일 | 예 | 결과 |
|---|---|---|---|
| 콜론 | 1씩 늘거나 줄어드는 정수 나열 | 10:6 | 10 9 8 7 6 |
| seq(by) | 간격을 정해 나열 | seq(1, 15, by = 7) | 1 8 15 |
| seq(length.out) | 개수를 정해 고르게 나열 | seq(0, 1, length.out = 5) | 0 0.25 0.5 0.75 1 |
| rep(times) | 벡터 전체를 반복 | rep(c("mon", "tue"), times = 3) | mon tue mon tue mon tue |
| rep(each) | 원소 하나씩 반복 | rep(c("mon", "tue"), each = 2) | mon mon tue tue |
times 와 each 는 헷갈리기 쉽다. times 는 벡터 전체를 통째로 되풀이하고, each 는 원소를 하나씩 제자리에서 되풀이한다. 요일별 판매 기록에 날짜 열을 붙일 때 두 옵션을 번갈아 쓰게 된다.
벡터의 길이는 length() 로 확인한다. length(items) 는 6 이다.
원소 단위 연산과 재사용 규칙
R 의 산술 연산자는 벡터를 받으면 같은 위치의 원소끼리 계산한다. price * qty 는 단가의 첫 값에 수량의 첫 값을, 둘째 값에 둘째 값을 곱한다. 반복문을 쓰지 않아도 되는 이유가 이것이다. 이렇게 한 번에 벡터 전체를 계산하는 방식을 벡터화라고 하며, 앞으로 가장 많이 쓰게 될 성질이다.
길이가 다른 두 벡터를 계산하면 짧은 쪽이 처음부터 다시 쓰인다. 이것이 재사용 규칙(recycling)이다. 길이가 1인 값, 즉 price * 2 의 2 는 재사용 규칙의 가장 흔한 경우다. 2 가 여섯 번 되풀이된다고 생각하면 된다.
길이가 2인 벡터를 길이 6인 벡터에 더하면 다음과 같다.
긴 쪽의 길이가 짧은 쪽 길이의 정확한 배수이면 R 은 조용히 계산한다. 배수가 아니면 계산은 하되 경고를 낸다. 이 경고는 뒤의 실수 항목에서 다시 본다.
비교 연산자(>, >=, == 등)도 같은 규칙을 따른다. 결과는 TRUE 와 FALSE 로 이루어진 논리 벡터다. sales >= 20000 은 매출이 2만 원 이상인지 상품마다 판정한 여섯 개의 참·거짓이다. 이 논리 벡터가 다음 절에서 필터 역할을 한다.
인덱싱: 원하는 원소 꺼내기
벡터에서 일부를 꺼내는 일을 인덱싱(indexing)이라고 한다. 벡터 이름 뒤에 대괄호를 붙이고 무엇을 꺼낼지 적는다. 대괄호 안에 올 수 있는 것은 세 종류다.
| 종류 | 예 | 의미 | 주의 |
|---|---|---|---|
| 양수 | items[c(2, 4)] | 지정한 위치만 남긴다 | 위치는 1부터 센다 |
| 음수 | items[-1] | 지정한 위치만 뺀다 | 양수와 섞어 쓸 수 없다 |
| 논리 | items[big] | TRUE 인 위치만 남긴다 | 길이를 원래 벡터에 맞춘다 |
양수와 음수
R 의 위치 번호는 1부터 시작한다. 첫째 원소는 items[1] 이다. 다른 언어에서 0부터 세는 습관이 있더라도 여기서는 바꿔야 한다. items[0] 은 오류가 아니라 빈 벡터 character(0) 을 돌려주고, 범위를 벗어난 items[7] 은 NA 를 돌려준다. NA 는 값이 없음을 뜻하는 특별한 값으로, 다음 장에서 자세히 다룬다.
범위 2:4 를 인덱스로 쓰면 둘째부터 넷째까지가 나온다. 음수는 뺄 위치를 뜻해서 items[-c(1, 6)] 은 처음과 마지막을 제외한 네 개를 돌려준다.
논리 값으로 조건 걸기
실무에서 가장 많이 쓰는 방식이다. 조건식의 결과인 논리 벡터를 그대로 대괄호에 넣으면, 엑셀의 필터처럼 조건을 만족하는 원소만 남는다. 조건은 &(그리고), |(또는) 로 이을 수 있다. 조건을 만족하는 위치 번호가 필요하면 which() 를 쓴다. which() 는 TRUE 인 곳의 번호를 돌려준다.
대괄호는 값을 꺼낼 때만이 아니라 값을 바꿀 때도 쓴다. stock[stock < 10] <- 10 은 10 미만인 원소를 모두 10 으로 바꾼다. 왼쪽에 인덱싱이 있으면 꺼내는 대신 그 자리에 값을 넣는다.
참고: tidyverse 와의 대응
tidyverse 라는 패키지 모음에서는 같은 일을 데이터 프레임의 행 단위로 한다. 이 책은 base R 만 사용하므로 아래 표는 이름을 알아 두는 용도로만 본다.
| 하려는 일 | base R (이 장) | tidyverse |
|---|---|---|
| 조건에 맞는 것만 남기기 | sales[sales >= 20000] | dplyr::filter(df, sales >= 20000) |
| 앞의 세 개 고르기 | sales[1:3] | dplyr::slice(df, 1:3) |
| 첫 번째를 빼기 | sales[-1] | dplyr::slice(df, -1) |
이름 붙은 벡터
원소에 이름을 붙이면 위치 번호 대신 이름으로 꺼낼 수 있다. 이름은 names() 로 붙이거나 c() 안에서 c(milk = 2200) 처럼 붙인다. 이름이 붙은 벡터를 출력하면 이름이 값 위에 함께 나온다.
이름으로 꺼낼 때는 대괄호 안에 문자열을 넣는다. sales["milk"] 는 값 하나가 아니라 이름이 붙은 길이 1의 벡터를 돌려준다. 이름 없이 값만 필요하면 unname() 을 쓴다.
거꾸로 이름 자체를 벡터로 꺼낼 수도 있다. names(sales) 는 문자열 벡터이므로 논리 값으로 인덱싱할 수 있다. names(sales)[sales == max(sales)] 는 매출이 가장 큰 상품의 이름이다. 이름 붙은 벡터는 데이터 프레임을 배우기 전 단계에서 표의 한 열과 행 이름을 흉내 내는 데 유용하다.
완성 코드
아래는 위 내용을 한 파일에 모은 것이다. 파일 이름은 main.R 이다.
# main.R - 편의점 하루 판매 기록으로 벡터 익히기
items <- c("kimbap", "water", "ramen", "milk", "coffee", "snack")
price <- c(1500, 800, 1300, 2200, 1800, 1700)
qty <- c(12, 30, 9, 15, 21, 7)
cat("== 벡터 만들기 ==\n")
print(length(items))
print(seq(1, 15, by = 7))
print(seq(0, 1, length.out = 5))
print(rep(c("mon", "tue"), times = 3))
print(rep(c("mon", "tue"), each = 2))
print(10:6)
cat("== 벡터 연산과 재사용 ==\n")
sales <- price * qty
print(sales)
print(1:6 + c(10, 20))
big <- sales >= 20000
print(big)
cat("== 인덱싱 ==\n")
print(items[2])
print(items[c(1, 3)])
print(items[2:4])
print(items[-1])
print(items[-c(1, 6)])
print(items[big])
print(items[big & qty < 25])
print(which(big))
print(sum(sales[big]))
stock <- qty
stock[stock < 10] <- 10
print(stock)
cat("== 이름 붙은 벡터 ==\n")
names(sales) <- items
print(sales[c("water", "coffee")])
print(sales["milk"])
print(names(sales)[sales == max(sales)])
cat("합계: ", sum(sales), "\n", sep = "")
줄별 해설
- 2~4행: 상품 이름, 단가, 수량을 각각 길이 6의 벡터로 만든다. 같은 위치의 값이 같은 상품에 대응한다. 세 벡터의 순서를 맞춰 두었기 때문에 이후 계산이 성립한다.
- 6~12행: cat() 은 제목 줄을 찍는다. 이어서 길이, 간격 지정 나열, 개수 지정 나열, times 반복, each 반복, 콜론 나열을 차례로 출력한다. print() 는 벡터를
[1]로 시작하는 줄로 보여 준다. 대괄호 속 숫자는 그 줄 첫 원소의 위치 번호다. - 15~16행:
price * qty는 원소 단위 곱셈이다. 상품별 매출 여섯 개가 한 번에 나온다. - 17행: 길이 6과 길이 2의 덧셈이다. 6 이 2 의 배수이므로 경고 없이 10, 20, 10, 20, 10, 20 이 더해진다.
- 18~19행: 매출이 2만 원 이상인지를 논리 벡터 big 에 저장한다. 이 벡터가 인덱싱에 다시 쓰인다.
- 22~26행: 위치 하나, 위치 여러 개, 범위, 음수 하나, 음수 여러 개로 꺼낸다. 문자열 벡터의 출력은 가장 긴 원소에 폭을 맞추므로 짧은 원소 뒤에 공백이 붙는다.
- 27~28행: 논리 벡터로 꺼낸다. 27행은 매출 조건 하나, 28행은 매출 조건과 수량 25 미만 조건을 & 로 묶은 것이다. 물은 매출이 커도 수량이 30이라 빠진다.
- 29~30행: which() 로 TRUE 인 위치 번호를 얻고, 조건에 맞는 매출만 골라 합한다. 24000 + 33000 + 37800 = 94800 이다.
- 31~33행: 수량을 복사한 뒤 10 미만인 값을 10 으로 바꾼다. 원본 qty 는 그대로 둔다.
- 36행: names() 에 대입해 매출 벡터에 상품 이름을 붙인다. 이 시점부터 sales 는 이름 붙은 벡터다.
- 37~38행: 이름으로 두 개, 한 개를 꺼낸다. 결과에도 이름이 따라온다.
- 39행: 매출이 최댓값과 같은 위치의 이름을 꺼낸다.
- 40행: cat() 에
sep = ""를 주어 조각 사이에 공백이 끼지 않게 하고 합계를 출력한다.
실행 결과
터미널에서 main.R 이 있는 폴더로 옮겨 아래 명령을 실행한다.
Rscript main.R
== 벡터 만들기 ==
[1] 6
[1] 1 8 15
[1] 0.00 0.25 0.50 0.75 1.00
[1] "mon" "tue" "mon" "tue" "mon" "tue"
[1] "mon" "mon" "tue" "tue"
[1] 10 9 8 7 6
== 벡터 연산과 재사용 ==
[1] 18000 24000 11700 33000 37800 11900
[1] 11 22 13 24 15 26
[1] FALSE TRUE FALSE TRUE TRUE FALSE
== 인덱싱 ==
[1] "water"
[1] "kimbap" "ramen"
[1] "water" "ramen" "milk"
[1] "water" "ramen" "milk" "coffee" "snack"
[1] "water" "ramen" "milk" "coffee"
[1] "water" "milk" "coffee"
[1] "milk" "coffee"
[1] 2 4 5
[1] 94800
[1] 12 30 10 15 21 10
== 이름 붙은 벡터 ==
water coffee
24000 37800
milk
33000
[1] "coffee"
합계: 136400
문자열 벡터 출력에서 짧은 원소 뒤에 붙은 공백과 이름 붙은 벡터 각 줄 끝의 공백은 R 이 폭을 맞추느라 넣은 것이다. 값에는 영향이 없다. 결론은 다음과 같다. 매출이 2만 원 이상인 상품은 water, milk, coffee 이고 세 상품의 매출 합계는 94800 원이다. 하루 전체 매출은 136400 원이며 가장 많이 판 상품은 coffee 다.
실무에서 자주 틀리는 것
1. 길이가 배수가 아닌 벡터를 더한다
수량 기록이 하나 빠진 벡터를 다른 벡터와 계산하는 경우가 흔하다. R 은 오류를 내지 않고 경고와 함께 결과를 돌려주므로, 경고를 놓치면 잘못된 값이 그대로 다음 계산으로 흘러간다.
# 틀린 코드
c(1, 2, 3, 4) + c(10, 20, 30)
# [1] 11 22 33 14
# 경고: longer object length is not a multiple of shorter object length
# 고친 코드: 길이를 먼저 확인하고 맞춘다
a <- c(1, 2, 3, 4)
b <- c(10, 20, 30, 40)
length(a) == length(b)
# [1] TRUE
a + b
# [1] 11 22 33 44
2. 양수와 음수를 한 대괄호에 섞는다
첫 번째는 빼고 두 번째는 남기겠다는 뜻으로 쓰기 쉽지만 R 은 받아들이지 않는다.
# 틀린 코드
items[c(-1, 2)]
# Error: only 0's may be mixed with negative subscripts
# 고친 코드: 뺄 것만 음수로 적는다
items[-1]
3. 조건을 "또는" 으로 이을 때 앞 식을 생략한다
말로는 "milk 또는 water" 이지만 코드에서는 비교를 매번 완성해야 한다.
# 틀린 코드
items == "milk" | "water"
# Error: operations are possible only for numeric, logical or complex types
# 고친 코드 1: 비교를 두 번 쓴다
items == "milk" | items == "water"
# 고친 코드 2: %in% 으로 후보 목록에 있는지 묻는다
items %in% c("milk", "water")
# [1] FALSE FALSE FALSE TRUE FALSE FALSE
%in% 는 왼쪽 벡터의 원소가 오른쪽 벡터에 들어 있는지를 원소마다 참·거짓으로 알려 준다. 후보가 많을수록 두 번째 방식이 읽기 쉽다.
4. 콜론의 우선순위를 잊는다
콜론은 덧셈·뺄셈보다 먼저 계산된다. 1:n-1 은 1:n 을 만든 다음 각 원소에서 1 을 뺀다.
n <- 3
# 틀린 코드
1:n-1
# [1] 0 1 2
# 고친 코드: 괄호로 범위의 끝을 먼저 계산한다
1:(n-1)
# [1] 1 2
한눈에 보기
| 주제 | 도구 | 핵심 규칙 | 대표 실수 |
|---|---|---|---|
| 만들기 | c(), seq(), rep(), 콜론 | 한 벡터에는 한 종류의 값만 담긴다 | 숫자와 문자열을 섞어 문자열이 됨 |
| 연산 | + - * / 비교 연산자 | 같은 위치끼리 계산하고, 짧은 쪽을 재사용한다 | 배수가 아닌 길이의 경고를 넘김 |
| 인덱싱 | 양수, 음수, 논리 | 위치는 1부터, 음수는 빼기 | 양수와 음수를 섞음 |
| 조건 고르기 | &, |, which(), %in% | 조건식은 매번 완성해서 쓴다 | "a" | "b" 식으로 생략함 |
| 이름 | names(), 이름으로 꺼내기 | 결과에도 이름이 따라온다 | 값과 이름 붙은 벡터를 혼동함 |
인덱싱과 재사용 규칙의 정확한 정의는 R 공식 문서인 R Language Definition 에서 확인할 수 있다.
연습 문제
- seq() 로 2 부터 20 까지 2 씩 늘어나는 벡터를 만들어라. 이어서 rep() 로
1 1 1 2 2 2를 만들어라. - 본문의 items 와 qty 를 그대로 쓴다. 수량이 10 미만인 상품의 이름을 꺼내는 한 줄을 써라.
x <- c(5, 10, 15, 20, 25, 30)일 때x + c(1, 2, 3, 4)의 결과를 손으로 계산하고, 경고가 나오는지 그 이유와 함께 답하라.- 이름 붙은 벡터
price <- c(kimbap = 1500, water = 800, ramen = 1300, milk = 2200, coffee = 1800, snack = 1700)를 만든다. milk 와 snack 의 가격 합을 구하고, water 의 가격을 900 으로 바꾼 뒤 가격이 1000 미만인 상품의 이름을 구하라.
정답과 해설
-
seq(2, 20, by = 2) # [1] 2 4 6 8 10 12 14 16 18 20 rep(c(1, 2), each = 3) # [1] 1 1 1 2 2 2원소를 하나씩 되풀이하는 것이므로 each 를 쓴다.
times = 3을 쓰면1 2 1 2 1 2가 된다. -
items[qty < 10] # [1] "ramen" "snack"qty 의 셋째(9)와 여섯째(7)만 10 미만이므로 같은 위치의 이름이 나온다. 두 벡터의 순서가 같다는 전제가 필요하다.
-
결과는
6 12 18 24 26 32이다. 짧은 벡터가 1, 2, 3, 4, 1, 2 로 재사용되기 때문이다. 6 은 4 의 배수가 아니므로 "longer object length is not a multiple of shorter object length" 경고가 나온다. 결과는 계산되지만 마지막 두 원소는 짧은 벡터가 중간에서 끊겨 쓰인 것이라, 의도한 계산인지 확인해야 한다. -
price <- c(kimbap = 1500, water = 800, ramen = 1300, milk = 2200, coffee = 1800, snack = 1700) sum(price[c("milk", "snack")]) # [1] 3900 price["water"] <- 900 names(price)[price < 1000] # [1] "water"이름으로 꺼낸 두 값 2200 과 1700 을 합하면 3900 이다. 이름으로 인덱싱한 자리에 값을 대입하면 그 원소만 바뀐다. 900 도 1000 미만이므로 water 가 여전히 조건에 들어간다.