이번 문서의 목표: R의 5가지 데이터 구조(벡터·행렬·배열·리스트·데이터프레임)를 차원과 동질성 기준으로 구분하고, 각 구조에서 원하는 값을 인덱싱·슬라이싱으로 꺼내며, R 인덱스가 1부터 시작한다는 것과 벡터 재활용 규칙 같은 실행 결과 함정을 정확히 예측할 수 있다.
R 데이터 구조 5종 한눈에 보기
왜 필요한가
10편에서는 숫자 하나, 문자 하나처럼 값 하나짜리 변수를 다뤘다. 하지만 실제 분석 대상은 거의 항상 여러 값의 묶음이다. 학생 30명의 점수, 가로 3개·세로 3개 표 형태의 데이터, 이름과 나이와 성적처럼 서로 다른 성격의 값들이 뒤섞인 표까지, R은 이 다양한 묶음의 형태마다 어울리는 자료구조(data structure)를 따로 두고 있다. 어떤 구조를 쓰느냐에 따라 인덱싱 방법과 담을 수 있는 값의 종류가 달라지므로, 구조 5종의 차이를 먼저 지도처럼 그려 두면 이후 통계·데이터마이닝 단원에서 등장하는 모든 R 코드를 훨씬 쉽게 읽을 수 있다.
쉽게 말하면: 벡터는 한 줄 목록, 행렬은 같은 종류로 채운 표, 배열은 3차원 이상의 표, 리스트는 아무거나 담는 서랍장, 데이터프레임은 열마다 다른 종류를 담을 수 있는 표다.
정의: 5종 비교표
R의 대표적인 자료구조 5가지를 차원(dimension, 값이 몇 개의 축으로 배열되는가), 동질성(homogeneity, 하나의 구조 안에 같은 타입의 값만 담을 수 있는가), 대표 생성 함수 기준으로 비교하면 다음과 같다.
| 구조 | 영문 | 차원 | 동질성(같은 타입만 담는가) | 대표 생성 함수 |
|---|---|---|---|---|
| 벡터 | vector | 1차원 | 동질(같은 타입만) | c() |
| 행렬 | matrix | 2차원 | 동질(같은 타입만) | matrix() |
| 배열 | array | N차원(3차원 이상 가능) | 동질(같은 타입만) | array() |
| 리스트 | list | 1차원(원소 각각이 다른 구조일 수 있음) | 이질(서로 다른 타입 혼합 가능) | list() |
| 데이터프레임 | data.frame | 2차원(행·열) | 이질(열마다 다른 타입 가능, 단 한 열 안은 동질) | data.frame() |
이 표에서 가장 중요한 축은 동질성이다. 벡터와 행렬은 반드시 같은 타입의 값만 담을 수 있고, 리스트와 데이터프레임은 서로 다른 타입을 섞어 담을 수 있다. 배열은 행렬을 3차원 이상으로 확장한 구조라서 행렬과 마찬가지로 동질이다. 이 성질 하나만 정확히 기억해도 “다음 중 문자와 숫자를 함께 담을 수 있는 구조는?” 같은 문제를 거의 실수 없이 풀 수 있다.
벡터(vector): 가장 기본이 되는 1차원 구조
왜 필요한가
10편에서 이미 c(1, 2, 3) 같은 코드를 여러 번 썼다. 이것이 바로 벡터다. R은 사실 값 하나(10 같은 스칼라)조차 “길이가 1인 벡터”로 취급할 만큼, 벡터는 R의 가장 근본적인 자료구조다. 여러 값을 순서대로 묶어 한 번에 다루고 싶을 때(예: 학생 30명의 점수를 한꺼번에 평균 내기) 벡터를 쓴다.
쉽게 말하면: 벡터는 같은 종류의 값을 한 줄로 늘어놓은 목록이다.
정의와 생성
c()(combine, 값들을 하나로 묶는 함수)로 벡터를 만든다.
scores <- c(85, 90, 78, 92, 88)
scores
## [1] 85 90 78 92 88
length(scores)
## [1] 5R 인덱스는 1부터 시작한다
R에서 벡터의 첫 번째 원소는 인덱스 1이다. 파이썬(Python)이나 자바스크립트(JavaScript) 같은 다른 언어에 익숙한 사람은 인덱스가 0부터 시작한다고 생각하기 쉬운데, R은 그렇지 않다. 이것은 ADsP 시험에서 반복적으로 함정으로 쓰이는 가장 기본적이면서도 중요한 규칙이다.
scores <- c(85, 90, 78, 92, 88)
scores[1]
## [1] 85
scores[3]
## [1] 78scores[1]은 벡터의 0번째가 아니라 첫 번째 값인 85를 돌려준다. scores[3]은 세 번째 값인 78이다.
음수 인덱스는 “제외”를 뜻한다
R에서 인덱스에 음수를 쓰면 “그 위치의 값을 가져오라”가 아니라 “그 위치를 제외한 나머지를 가져오라”는 뜻이 된다. 이것 역시 다른 언어(예: 파이썬에서 -1은 마지막 원소)와 완전히 다른 R 고유의 규칙이라서 대표적인 함정으로 출제된다.
scores <- c(85, 90, 78, 92, 88)
scores[-1]
## [1] 90 78 92 88
scores[-3]
## [1] 85 90 92 88scores[-1]은 첫 번째 값(85)을 제외한 나머지 네 개(90, 78, 92, 88)를 돌려준다. scores[-3]은 세 번째 값(78)을 제외한 나머지다. “마지막 원소를 가져오는 것”과 “특정 위치를 제외하는 것”을 혼동하지 않도록 주의해야 한다.
슬라이싱: 범위로 여러 원소 꺼내기
콜론(:)으로 연속된 범위를 지정하거나, c()로 원하는 위치들을 골라 꺼낼 수 있다.
scores <- c(85, 90, 78, 92, 88)
scores[2:4]
## [1] 90 78 92
scores[c(1, 3, 5)]
## [1] 85 78 88scores[2:4]는 2번째부터 4번째까지(90, 78, 92)를, scores[c(1, 3, 5)]는 1·3·5번째 값만 골라 돌려준다.
조건으로 값 꺼내기
논리형 벡터를 인덱스 자리에 넣으면, TRUE에 해당하는 위치의 값만 골라진다.
scores <- c(85, 90, 78, 92, 88)
scores[scores >= 88]
## [1] 90 92 88scores >= 88은 각 원소가 88 이상인지 검사한 논리형 벡터(FALSE TRUE FALSE TRUE TRUE)를 만들고, 이를 다시 인덱스로 쓰면 TRUE인 위치의 값만 남는다.
벡터의 재활용 규칙(recycling rule)
길이가 다른 두 벡터를 함께 연산하면, R은 에러를 내는 대신 짧은 쪽 벡터를 길이가 맞을 때까지 처음부터 반복해서 연산한다. 이를 재활용 규칙(recycling rule)이라 부른다.
a <- c(1, 2, 3, 4)
b <- c(10, 20)
a + b
## [1] 11 22 13 24b는 길이 2인데 a는 길이 4다. R은 b를 c(10, 20, 10, 20)처럼 두 번 반복한 것으로 취급해 a와 원소별로 더한다. 그 결과 1+10, 2+20, 3+10, 4+20이 계산되어 11 22 13 24가 나온다.
c <- c(1, 2, 3)
d <- c(10, 20)
c + d
## [1] 11 22 13
## Warning message: 두 벡터의 길이가 서로 배수 관계가 아닙니다이번에는 길이 3과 길이 2로, 짧은 쪽 길이(2)가 긴 쪽 길이(3)의 배수가 아니다. 이 경우 R은 재활용을 하되 경고 메시지를 함께 띄운다. 계산 자체는 진행되어 결과가 나오지만(d를 10, 20, 10처럼 앞에서부터 재활용), 배수 관계가 아니라는 것을 알리는 경고가 뜬다는 점이 함정 포인트다. “길이가 다르면 무조건 에러가 난다”고 착각하면 이 문제를 틀리게 된다.
자주 틀리는 점
- 벡터의 첫 원소를
scores[0]으로 꺼내려 한다. R에서 인덱스는 1부터 시작하므로scores[0]은 존재하지 않는 원소를 요청한 것이 되어 길이 0인 빈 벡터를 돌려준다. - 음수 인덱스
scores[-1]이 “마지막 원소” 또는 “인덱스 -1에 대응하는 값”을 뜻한다고 착각한다. 실제로는 “1번째를 제외한 나머지”다. - 길이가 다른 벡터의 연산이 항상 에러가 난다고 착각한다. 실제로는 재활용 규칙에 따라 계산이 진행되며, 배수 관계가 아닐 때만 경고가 뜬다.
행렬(matrix): 같은 타입으로 채운 2차원 표
왜 필요한가
벡터가 한 줄짜리 목록이라면, 행렬은 그 목록을 여러 행과 열로 접어 만든 2차원 표다. 같은 성격의 수치 데이터를 표 형태로 다뤄야 할 때(예: 여러 변수 간의 상관계수를 행×열로 정리한 상관행렬) 행렬을 쓴다. 행렬은 벡터와 마찬가지로 동질(같은 타입만 허용)이라는 성질을 그대로 물려받는다.
쉽게 말하면: 행렬은 숫자를 격자무늬로 배열한 표다.
정의와 생성
matrix() 함수에 값과 행(row) 수 또는 열(column) 수를 지정해 만든다.
m <- matrix(1:6, nrow = 2, ncol = 3)
m
## [,1] [,2] [,3]
## [1,] 1 3 5
## [2,] 2 4 61:6은 1부터 6까지의 연속된 정수 벡터(c(1,2,3,4,5,6))를 만드는 축약 표기다. matrix()는 기본적으로 열 우선(column-major, 왼쪽 위부터 세로 방향으로 먼저 채움) 순서로 값을 채우므로, 1행 1열부터 세로로 1, 2가 채워지고 그다음 열로 넘어가 3, 4가 채워진다.
행렬 인덱싱
행렬[행, 열] 형태로 원하는 위치를 지정한다. 행이나 열 자리를 비워두면 그 축 전체를 뜻한다.
m <- matrix(1:6, nrow = 2, ncol = 3)
m[1, 2]
## [1] 3
m[, 2]
## [1] 3 4
m[1, ]
## [1] 1 3 5m[1, 2]는 1행 2열의 값 3을, m[, 2]는 2열 전체(3, 4)를, m[1, ]는 1행 전체(1, 3, 5)를 돌려준다.
자주 틀리는 점
matrix()가 값을 행 우선(가로 방향)으로 채운다고 착각한다. 기본값은 열 우선이며, 행 우선으로 채우려면byrow = TRUE옵션을 명시해야 한다.- 행렬 안에 문자와 숫자를 함께 넣을 수 있다고 착각한다. 행렬도 벡터와 마찬가지로 동질 구조이므로, 문자가 하나라도 섞이면 전체가 문자형 행렬로 변환된다.
배열(array): 행렬을 확장한 다차원 구조
왜 필요한가
행렬이 행과 열, 즉 2개의 축으로 이루어진 표라면, 배열은 여기에 축을 하나 이상 더 추가한 구조다. 예를 들어 “지역별 × 상품별 × 월별” 매출처럼 3개 이상의 기준으로 데이터를 정리해야 할 때 쓴다. 실무에서 자주 쓰이는 구조는 아니지만, “행렬은 2차원까지, 배열은 3차원 이상까지”라는 차원 확장 개념 자체가 시험 범위에 포함된다.
쉽게 말하면: 배열은 행렬을 여러 겹으로 쌓아 올린 입체 표다.
정의와 생성
arr <- array(1:8, dim = c(2, 2, 2))
arr
## , , 1
##
## [,1] [,2]
## [1,] 1 3
## [2,] 2 4
##
## , , 2
##
## [,1] [,2]
## [1,] 5 7
## [2,] 6 8dim = c(2, 2, 2)는 2행 2열짜리 표를 2장 쌓았다는 뜻이다. , , 1과 , , 2는 각각 1번째 장, 2번째 장을 가리키는 표시다. arr[1, 2, 2]처럼 인덱스 세 개를 지정하면 2번째 장의 1행 2열 값을 꺼낼 수 있다.
자주 틀리는 점
- 배열과 행렬을 같은 것으로 혼동한다. 행렬은 2차원으로 고정된 구조이고, 배열은 3차원 이상으로 확장할 수 있는 더 일반적인 구조다(2차원 배열은 행렬과 사실상 같다).
리스트(list): 무엇이든 담는 서랍장
왜 필요한가
벡터·행렬·배열은 모두 같은 타입만 담아야 한다는 제약이 있다. 하지만 실제로는 이름(문자), 나이(숫자), 성적표(벡터), 심지어 다른 리스트까지 한 번에 묶어 다루고 싶을 때가 많다. 리스트는 이런 요구에 맞춰 R이 제공하는, 서로 다른 타입과 서로 다른 길이의 값들을 하나로 묶을 수 있는 이질(heterogeneous) 구조다.
쉽게 말하면: 리스트는 칸마다 다른 종류의 물건을 넣어도 되는 서랍장이다.
정의와 생성
person <- list(name = "지훈", age = 28, scores = c(85, 90, 95))
person
## $name
## [1] "지훈"
##
## $age
## [1] 28
##
## $scores
## [1] 85 90 95리스트 하나에 문자형 값(name), 수치형 값(age), 길이 3짜리 벡터(scores)가 모두 함께 들어갔다. 이것은 벡터나 행렬이라면 애초에 불가능한 조합이다.
리스트 인덱싱: 대괄호 하나 vs 대괄호 둘
리스트 인덱싱에서 대괄호 하나([])와 대괄호 둘([[]])의 차이는 자주 나오는 함정이다.
person <- list(name = "지훈", age = 28, scores = c(85, 90, 95))
person[1]
## $name
## [1] "지훈"
person[[1]]
## [1] "지훈"person[1]은 리스트 형태를 유지한 채 첫 번째 원소를 감싸서 돌려준다(껍질째 꺼낸다). person[[1]]은 그 껍질을 벗기고 원소의 실제 값만 돌려준다. 리스트 이름으로 접근할 때는 $ 기호를 쓴다.
person$age
## [1] 28자주 틀리는 점
person[1]과person[[1]]이 항상 같은 결과를 낸다고 착각한다.[]는 리스트 안에 리스트가 든 형태(list 안의 하위 list)를,[[]]는 그 안의 실제 값을 돌려준다는 차이가 있다.- 리스트가 벡터처럼 동질이어야 한다고 착각한다. 리스트는 이질 구조가 핵심 특징이다.
데이터프레임(data.frame): 열마다 다른 타입을 허용하는 표
왜 필요한가
실무에서 다루는 대부분의 데이터는 엑셀 표처럼 행은 관측치(observation) 하나, 열은 변수(variable) 하나를 나타내는 표 형태다. 예를 들어 학생 명부는 이름(문자)·나이(숫자)·합격 여부(논리)처럼 열마다 성격이 다른 값을 가진다. 행렬은 동질 구조라서 이런 표를 담을 수 없고, 이때 쓰는 것이 데이터프레임이다. 데이터프레임은 “행렬처럼 행과 열을 가진 2차원 구조”이면서 동시에 “리스트처럼 열마다 다른 타입을 허용하는” 구조로, 두 구조의 장점을 결합한 형태라고 볼 수 있다. R을 이용한 데이터분석에서 가장 많이 다루는 구조가 바로 이 데이터프레임이며, 12편의 dplyr 전처리 전체가 데이터프레임을 다루는 방법이다.
쉽게 말하면: 데이터프레임은 엑셀 시트처럼 행은 사람 한 명, 열은 항목 하나를 나타내는 표다.
정의와 생성
df <- data.frame(
name = c("지훈", "수민", "예린"),
age = c(28, 25, 31),
pass = c(TRUE, FALSE, TRUE)
)
df
## name age pass
## 1 지훈 28 TRUE
## 2 수민 25 FALSE
## 3 예린 31 TRUEname 열은 문자형, age 열은 수치형, pass 열은 논리형이다. 열 하나 안에서는 반드시 같은 타입이어야 하지만(벡터의 동질성을 물려받음), 열과 열 사이에는 서로 다른 타입이 허용된다(리스트의 이질성을 물려받음). 이 이중적 성격이 데이터프레임을 이해하는 핵심이다.
데이터프레임 인덱싱
데이터프레임[행, 열] 형태는 행렬과 같고, 열 이름으로 바로 접근하는 $는 리스트와 같다.
df[1, ]
## name age pass
## 1 지훈 28 TRUE
df[, "age"]
## [1] 28 25 31
df$age
## [1] 28 25 31
df[df$age >= 28, ]
## name age pass
## 1 지훈 28 TRUE
## 3 예린 31 TRUEdf[1, ]는 1행 전체(첫 번째 사람의 모든 정보)를, df[, "age"]와 df$age는 둘 다 age 열 전체를 꺼내는 동일한 결과를 낸다. df[df$age >= 28, ]처럼 조건을 행 자리에 넣으면, 나이가 28세 이상인 사람의 행만 걸러낸다. 이 조건 필터링 방식은 12편에서 배울 filter() 함수가 하는 일과 본질적으로 같다.
구조 확인 함수
str(df)
## 'data.frame': 3 obs. of 3 variables:
## $ name: chr "지훈" "수민" "예린"
## $ age : num 28 25 31
## $ pass: logi TRUE FALSE TRUE
nrow(df)
## [1] 3
ncol(df)
## [1] 3str()(structure, 구조를 요약해 보여주는 함수)은 데이터프레임의 각 열이 몇 개의 관측치(obs.)로 이루어져 있고 어떤 타입인지 한눈에 보여준다. 새로운 데이터를 받으면 가장 먼저 실행해 보는 함수라고 할 만큼 실무에서 자주 쓰인다.
자주 틀리는 점
- 데이터프레임의 모든 열이 같은 타입이어야 한다고 착각한다. 열 사이에는 타입이 달라도 되지만, 한 열 안에서는 같은 타입이어야 한다.
- 데이터프레임과 행렬을 같은 구조로 착각해
matrix()에 문자·숫자·논리값을 섞어 넣으려 한다. 행렬은 동질 구조이므로 이렇게 하면 전체가 하나의 타입(대개 문자형)으로 강제 변환된다.
핵심 정리
- R의 5대 자료구조는 벡터·행렬·배열·리스트·데이터프레임이며, 벡터·행렬·배열은 동질, 리스트·데이터프레임은 이질이라는 것이 가장 중요한 구분 기준이다.
- R의 인덱스는 1부터 시작한다.
scores[1]은 첫 번째 값이며,scores[0]은 존재하지 않는다. - 음수 인덱스는 “제외”를 뜻한다.
scores[-1]은 첫 번째를 뺀 나머지 전부다. - 길이가 다른 벡터를 연산하면 에러가 아니라 재활용 규칙에 따라 짧은 벡터가 반복되며, 배수 관계가 아니면 경고가 뜬다.
- 리스트는
[](껍질째)와[[]](내용물만)의 반환 형태가 다르다. - 데이터프레임은 열마다 타입이 달라도 되지만, 한 열 안에서는 반드시 동질이어야 한다.
마무리 복습
a = c(1, 2, 3, 4, 5, 6)
b = c(1, 2)
a + b