R의 다양한 자료구조들

이제 거의 8월 달도 끝나가는군요. 간만에 비오는 주말입니다. 태풍 솔록이 지나가면서부터 드디어 대한민국에 2018년 첫 여름비가 내린 것 같은 느낌이네요. (혹시 정말 첫 여름비일지도 모르겠군요…)

Data Structure in R

프로그래밍을 하게 되면 자연스럽게 만날 수 있는 것은 바로 자료구조입니다. 처음 학부시간에 자료구조를 배웠을 때는 ‘왜 이것을 배워야하지?’라는 의문을 잔득 가졌었습니다. 그 때는 프로그래밍에 대해서 관심이 많지도 않았고 대학에서 가르쳐주는 그저 영어 같았습니다.

본론으로 넘어가서 자료구조는 컴퓨터에서 자료를 보관하기 위한 한 방법입니다. 코딩을 하게 되면 우리는 필요한 데이터를 저장해야 합니다. 하지만 그것은 우리가 생각하는 것처럼 “이거 필요하니까 그냥 메모리에 저장해 줘”라는 단순한 느낌이 아닙니다. 내가 원하는 데이터를 뽑으려면 어떤 규칙이 정해져 있고 그 규칙에 따라 저장되어 있는 데이터를 가져오는 것이 바로 컴퓨터의 자료구조입니다.

실제로 이러한 자료구조들에는 배열(Array), 리스트(List), 스택(Stack), 큐(Queue), 그래프(Graph) 등이 있습니다. 실제로 학부 수업 때 까지는 저 위에 있는 자료구조들을 다룰 것이며 실제 구현도 하게 될 것입니다.

그러나 저 자료구조들을 모두 사용하지는 않을 것입니다. 저의 경우, 보통 C++ 언어에서 자주 사용하는 자료구조라고 하면, Array와 Vector, Pair, Map을 자주 사용합니다. 물론 Pair는 그렇게 크게 쓸 일 없이 구조체로도 충분히 구현 가능한 부분이어서 잘 쓰지도 않구요. Java의 경우 ArrayList, HashMap, Set 을 사용하는 것처럼 수많은 자료구조 중에서 여러분들이 알고리즘을 구현하거나 프로그래밍 하는 데 있어 여러 가지 자료구조를 사용할 것입니다.

R언어에도 이러한 자료구조들이 존재합니다. 어떠한 자료구조들이 있는지 알아보고 각각의 접근 방법과 장단점에 대해 간단히 적어보도록 하겠습니다.

Array

1번재는 가장 기본적인 배열입니다. 배열은 프로그래밍 언어를 처음 배울 때 다루는 자료구조 중 하나입니다. 아주 간단하죠. C, C++, Java 어디서든지 사용할 수 있는 기본적인 자료구조입니다.

1
arr <- array()

기본적인 선언 방법은 간단하게 array라고만 적어주시면 됩니다.

1
arr <- array(0:25)

기본적으로 1차원 배열의 생성은 위와 같이 하게 된다. 그러면 0 ~ 25까지의 데이터가 한 행에 쭈욱 나타나게 된다.
그러면 2차원 배열 이상은 어떻게 생성할까?

1
2
3
4
5
# 1 ~ 6까지의 숫자를 2 x 3 형태로..
arr <- array(1:6, c(2,3))

# 1 ~ 24까지의 숫자를 2 x 3 형태의 4층짜리로..
arr <- array(1:24, c(2,3,4))

감이 오나요? 배열 다음에 나오는 첫번째 인자는 숫자의 범위이고, 두번째는 벡터형을 사용하여 행, 열, 층으로 선언하면 그에 해당하는 배열을 만들어줍니다. 어떤가요? 저는 처음에 안익숙했지만 차라리 저는 C언어의 배열이 더 쉽다고 느껴지는건 모순인걸까요 ㅜㅠ

Vector

2번째는 벡터라는 자료구조입니다. 아마 벡터라는 이름을 보면 C++ 언어에 존재하는 Vector라고 생각하시는 분들이 계실 것입니다.

비슷하지만 약간 다른 점이 있습니다. C++ 언어에서의 벡터는 같은 자료형의 데이터만을 적재할 수 있지만 R 언어에서의 벡터는 한 자료구조에 여러개의 자료형을 넣는 것이 가능합니다.

1
vec <- c()

기본적인 선언 방법은 위와 같습니다. 그런데, “아니, 벡터인데 왜 c 라는 알파벳을 쓰는거죠?” 여기서의 c는 Column이라는 뜻을 가지고 있습니다. 차후 매트릭스에 대해 다루게 되면 이해하기 쉬울 것입니다. 밑에 부분에서 자세히 설명하도록 하겠습니다.

1
vec <- c("NEONKID", 1412, 0.1011, TRUE)

위와 같은 형태로 여러가지 자료형을 넣을 수 있지만 주의해야할 사항이 있습니다.

1
[1] "NEONKID"	"1412"	"0.1011"	"TRUE"

바로 적재될 때는 모두 character 형태로 적재가 된다는 것입니다. C++의 vector에서는 애시당초에 한 데이터형만 집어넣게 되지만 R에서의 Vector는 다른 자료형을 넣으면모두 character 형태로 들어가게 됨을 적재하실 때 유의하여야 합니다.

1
length(vec)

길이를 구할 때는 length 함수를 사용하시면 됩니다.

Matrix

매트릭스는 n * n 형태의 자료구조입니다. 아마 C++, Java 프로그래밍 언어에서 이를 구현하려면 보통 2차원 배열을 만들어야 했습니다. 하지만 R 언어에서는 이러한 자료구조를 기본적으로 제공하며 실제로 이의 조합은 Vector를 여러개 조합한 형태라고 할 수 있습니다.

1
2
3
4
5
6
7
8
# 0부터 100까지 10씩 증가하는 정수형 벡터 생성
vec <- seq(0, 110, by = 10)

# 열의 수가 6인 행렬
matC <- matrix(vec, ncol = 4)

# 행의 수가 6인 행렬
matR <- matrix(vec, nrow = 4)

하지만 설령 칼럼 벡터를 하나만 주었다 하더라도 Matrix 생성자의 인자값을 이용해서 행이나 열을 특정 형태로 짤라낼 수 있습니다.

List

리스트는 벡터와 같은 형태를 지니고 있지만 약간 다릅니다. 벡터는 한 자료형의 한 단일 데이터를 넣을 수 있다고 한다면 리스트는 한 자료구조 전체를 담을 수가 있습니다. 즉, 벡터처럼 한 칼럼의 형태를 가지고는 있지만 넣을 수 있는 데이터의 형태가 단일 데이터를 넘어, 매트릭스, 벡터의 한 자료구조도 담을 수 있다는 것입니다.

1
listA <- list()

기본적인 리스트의 선언은 위와 같이 가능합니다.

1
2
3
4
5
6
7
# Vector
vec <- seq(1, 10)

# Matrix
mat <- matrix(1:12, ncol = 4)

listA <- list(dataA = vec, dataB = mat)

위와 같이 다른 자료구조의 형태도 가능하여 별도로 접근할 수 있습니다. 참 편한 자료구조지요. 언제 주로 쓰냐면 여러개의 csv 데이터릃 하나의 자료구조로 불러올 때 매우 유용할 것입니다.

Data frame

데이터 프레임은 매트릭스와 마찬가지로 n * n 형태의 자료구조입니다. 하지만 매트릭스와 역시 다른 점이 존재합니다. 바로 벡터와 리스트처럼 들어갈 수 있는 데이터 형태가 다르다는 것입니다. 데이터 프레임은 리스트와 마찬가지로 다른 자료형 형태의 여러가지 벡터를 담을 수 있습니다.

하지만 그에는 조건이 있습니다. 반드시 넣으려고 하는 벡터가 같은 행과 열의 갯수를 가지고 있어야 하며, 벡터가 각각 다른 행과 열의 갯수를 가질 경우 오류가 발생합니다.

1
df <- data.frame()

기본적인 빈 데이터 프레임의 생성은 다른 자료구조와 동일하게 진행됩니다.

1
2
3
4
5
6
7
8
9
10
11
# 두 개의 벡터를 생성합니다.
# 벡터의 특성대로 한 벡터당 한 칼럼이 적용됩니다.
vecA <- 1:100
vecB <- 101:200

# 이를 리스트에 저장해놓습니다.
# 이렇게 되면 위의 리스트의 정의대로 개별 접근이 가능합니다.
myList <- list(vecA, vecB)

# 이제 리스트 한 개로 데이터 프레임 안에 적재합니다.
df <- data.frame(myList)

데이터 프레임은 매트릭스의 집합으로도 사용할 수 있습니다. 왜냐하면 아까 매트릭스도 봤다시피 행렬을 특정 형태로 변환할 수가 있는데, 이를 같게 만들어놓고 data frame에 적재하게 되면 그것도 가능하겠죠? 하지만 대부분은 벡터의 집합으로 많이 사용합니다. 왜 그러냐구요? 매트릭스에 굳이 넣지 않고 벡터에 그 데이터를 고르게 적재한 다음에 데이터 프레임에 넣기만 하면 바로 들어가지기 때문이죠.

만약 행과 열의 갯수가 다르게 나올 케이스가 있다면 매트릭스로 정확하게 처리하는 것도 답이겠지만 보통은 로직에서 NULL이 나오지 않도록 설계를 한 후, 바로 데이터 프레임에 넣는 것이 아마 효율적일 것입니다. 만약 그렇게 안된다면? 그 때는 어쩔 수 없이 매트릭스를…

Factor

Factor는 제가 제일 싫어하는 R의 자료구조 중 하나입니다. 왜 싫어하냐면, 이놈은 접근이 너무 힘들어요. 물론 다루는 것도 만만치 않죠. 처음에 데이터 프레임에 적재할 때 이 녀석 덕분에 많은 고생을 했답니다.

Factor는 벡터를 조금 변형하여 개발한 자료구조라 하며 약간 특수한 형태를 지닙니다. 이를 테면 인적성검사에서 볼 수 있는 선택문항의 카테고리를 저장할 수 있는 자료구조라고 보시면 됩니다.

1
fac <- factor()

역시 다른 자료구조와 마찬가지로 선언은 위와 같이 진행합니다.

1
2
vec <- c("A", "C", "B", "A", "A", "C", "B")
fac <- factor(xv, levels = c("A", "B", "C"))

Factor는 벡터와 그 형태가 비슷하지만 여기에 레벨이라는 추가 형태를 담고 있습니다. 레벨이라는 것은 이 카테고리의 순서를 의미하는 것이죠. 심지어 이 녀석은 R의 내부에서 아주 다른 형태로 취급하며 만약 as.numeric(fac)을 하게 되면 출력값을 레벨 순서대로 1 3 2 1 1 3 2 형태로 출력하게 된다.

만약 벡터였다면? 무차별하게 “모르는 값입니다. “라고 하면서 NA로 도배를 하게될 것이니…

그래서 일반적으로 데이터를 저장할 때 Factor를 사용하는 것은 정말 적절치 않다. 이 자료구조의 본래 목적은 안에 있는 데이터들의 레벨을 판단하고 나열하기 위한 자료구조로 실제로 안에 있는 데이터를 무작위로 나열해서 나중에 쉽게 접근하거나 이전하기 위한 용도로 사용한다면 Factor의 사용은 금물. 물론 그렇게 굳이 선언만 한다면 안쓰는 것인데 이 놈은 조금 예외가 있다.

바로 데이터 프레임에 적재하는 경우이다.

1
2
3
4
5
6
7
8
9
10
# 데이터 프레임 선언
df <- data.frame()

# 벡터에 데이터 적재.. (중간 코드 생략)
vecA <- ...
vecB <- ...
vecC <- ...

# 데이터 프레임에 칼럼 데이터 적재
df(ListA, ListB, ListC)

데이터를 넣다보면 이런 일이 있을 수가 있다. 그런데 나는 분명 데이터 프레임에 벡터 데이터형을 넣었음에도 불구하고 나중에 접근해보면 엥? 왠걸, Factor로 삐져나오게 된다.

그 이유는 바로 데이터 프레임을 생성할 때 등장하는 stringAsFactors 라는 옵션 때문이다. 이 옵션은 외부 데이터(파일, 자료구조 포함)를 읽을 때 문자열(Character)를 Factor Object로 읽는다는 옵션이다. 기본값으로 TRUE라 되어 있으며 내가 적재한 벡터 데이터들이 Factor로 되버리는 어마무시한 일이 발생한다.

1
2
3
4
5
6
7
8
9
10
# 데이터 프레임 선언
df <- data.frame()

# 벡터에 데이터 적재.. (중간 코드 생략)
vecA <- ...
vecB <- ...
vecC <- ...

# 데이터 프레임에 칼럼 데이터 적재
df(ListA, ListB, ListC, stringAsFactors = FALSE)

그래서 반드시 이러한 현상을 방지하기 위해 반드시 stringsAsFactors의 옵션을 FALSE로 바꿔주고 진행한다.

마치며…

여기까지 R에서 사용하는 6개의 자료구조에 대해 알아봤습니다. 원래는 Scala를 포함하면 7가지의 자료구조이지만 스칼라는 벡터를 하나의 자료로 표현한 형태이기 때문에 제외하였습니다. 사실 잘 사용하지도 않구요.

R 언어를 사용한 프로젝트를 진행하면서 가장 많이 사용했던 자료구조는 벡터, 리스트, 데이터 프레임을 주로 사용하였습니다. 특히 외부 데이터들은 대부분 데이터 프레임으로 처리되며 나중에 ETL 과정에서 생겨난 데이터들도 SQL에 적재할 때 모두 데이터 프레임을 사용했습니다.

확실히 R은 데이터 처리 언어라고 할 정도로 필요 이상으로 쉽게 다룰 수 있는 자료 구조들이 존재합니다. 하지만 저는 그래도 Python이 더 익숙한 것 같습니다. 물론 Python에 있는 자료구조와 조금 상이한 부분들이 있습니다. (같은 이름의 자료구조인데도 왜 이런 차이가 나는 것인지 ㅠㅠ)

0%