Devin.KR

R · 기본

데이터로 시작하는 R

벡터 - R 의 기본 단위

c()·seq·rep, 벡터 연산과 재사용 규칙, 인덱싱(양수·음수·논리), 이름 붙은 벡터

개발자KR · 원고 갱신

이 장에서 배우는 것

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:610 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인 벡터에 더하면 다음과 같다.

길이 2인 벡터 c(10, 20) 이 길이 6인 벡터에 더해질 때 처음부터 세 번 반복해서 쓰인다.

긴 쪽의 길이가 짧은 쪽 길이의 정확한 배수이면 R 은 조용히 계산한다. 배수가 아니면 계산은 하되 경고를 낸다. 이 경고는 뒤의 실수 항목에서 다시 본다.

비교 연산자(>, >=, == 등)도 같은 규칙을 따른다. 결과는 TRUE 와 FALSE 로 이루어진 논리 벡터다. sales >= 20000 은 매출이 2만 원 이상인지 상품마다 판정한 여섯 개의 참·거짓이다. 이 논리 벡터가 다음 절에서 필터 역할을 한다.

인덱싱: 원하는 원소 꺼내기

벡터에서 일부를 꺼내는 일을 인덱싱(indexing)이라고 한다. 벡터 이름 뒤에 대괄호를 붙이고 무엇을 꺼낼지 적는다. 대괄호 안에 올 수 있는 것은 세 종류다.

양수는 고른 위치를 남기고, 음수는 고른 위치를 빼고, 논리 값은 TRUE 인 위치를 남긴다.
대괄호 안에 쓸 수 있는 세 종류의 인덱스
종류예의미주의
양수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 만 사용하므로 아래 표는 이름을 알아 두는 용도로만 본다.

벡터 인덱싱과 tidyverse 데이터 프레임 함수의 대응
하려는 일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 에서 확인할 수 있다.

연습 문제

  1. seq() 로 2 부터 20 까지 2 씩 늘어나는 벡터를 만들어라. 이어서 rep() 로 1 1 1 2 2 2 를 만들어라.
  2. 본문의 items 와 qty 를 그대로 쓴다. 수량이 10 미만인 상품의 이름을 꺼내는 한 줄을 써라.
  3. x <- c(5, 10, 15, 20, 25, 30) 일 때 x + c(1, 2, 3, 4) 의 결과를 손으로 계산하고, 경고가 나오는지 그 이유와 함께 답하라.
  4. 이름 붙은 벡터 price <- c(kimbap = 1500, water = 800, ramen = 1300, milk = 2200, coffee = 1800, snack = 1700) 를 만든다. milk 와 snack 의 가격 합을 구하고, water 의 가격을 900 으로 바꾼 뒤 가격이 1000 미만인 상품의 이름을 구하라.

정답과 해설

  1. 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 가 된다.

  2. items[qty < 10]
    # [1] "ramen" "snack"

    qty 의 셋째(9)와 여섯째(7)만 10 미만이므로 같은 위치의 이름이 나온다. 두 벡터의 순서가 같다는 전제가 필요하다.

  3. 결과는 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" 경고가 나온다. 결과는 계산되지만 마지막 두 원소는 짧은 벡터가 중간에서 끊겨 쓰인 것이라, 의도한 계산인지 확인해야 한다.

  4. 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 가 여전히 조건에 들어간다.

오탈자·오류 제보 비공개로 접수되어 원고 수정에 반영됩니다

이메일 등 개인정보는 받지 않습니다. 답변이 필요한 질문은 아래 댓글을 이용해 주세요.

READER FEEDBACK

질문·의견

내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.