리스트(list)와 데이터 프레임(data frame)

리스트(list)와 데이터 프레임(data frame)

벡터가 같은 모드의 값들만 담는 '한 가지 타입'의 상자라면, 리스트는 서로 다른 모드와 타입을 자유롭게 섞어 담는 '서랍장' 같은 존재예요. 이번 절에서는 R에서 데이터를 묶는 가장 유연한 구조인 리스트를 만드는 법과, 통계 분석에서 사실상 표준처럼 쓰이는 데이터 프레임을 다루는 법을 차근차근 살펴볼게요.

출처: R 공식 매뉴얼

본문

6.1 리스트(Lists)

R의 리스트(list) 는 순서가 있는 객체들의 모음으로, 그 안의 각 객체를 구성요소(components) 라고 불러요.

구성요소들이 반드시 같은 모드나 타입일 필요는 없어요. 예를 들어 어떤 리스트는 숫자 벡터, 논리값, 행렬, 복소수 벡터, 문자 배열, 함수 등을 한꺼번에 담을 수 있죠. 리스트를 만드는 간단한 예를 먼저 볼게요.

> Lst <- list(name="Fred", wife="Mary", no.children=3,
              child.ages=c(4,7,9))

구성요소는 항상 번호가 매겨져 있고, 그 번호로 언제든 참조할 수 있어요. 그래서 리스트 이름이 Lst이고 구성요소가 네 개라면, 각각 Lst[[1]], Lst[[2]], Lst[[3]], Lst[[4]]처럼 개별적으로 접근할 수 있어요. 만약 Lst[[4]]가 벡터로 첨자화된 배열이라면, Lst[[4]][1]은 그 첫 번째 항목을 가리키죠.

Lst가 리스트라면 length(Lst)로 (최상위) 구성요소가 몇 개인지 알 수 있어요.

리스트의 구성요소에는 이름(name) 을 붙일 수도 있어요. 이 경우 구성요소를 지칭할 때 이중 대괄호 안에 숫자 대신 문자형 이름을 넣거나, 더 편리하게는 다음과 같은 형태의 표현식을 쓸 수 있어요.

> name$component_name

이름을 붙여두는 관례는 숫자를 잊어버렸을 때도 올바른 구성요소를 쉽게 찾게 해 주는 아주 유용한 방법이에요.

위의 간단한 예에서 구체적으로 확인해 볼게요.

  • Lst$nameLst[[1]]과 같고, 값은 문자열 "Fred"예요.
  • Lst$wifeLst[[2]]와 같고, 값은 문자열 "Mary"예요.
  • Lst$child.ages[1]Lst[[4]][1]과 같고, 값은 숫자 4예요.

추가로, 이중 대괄호 안에 구성요소 이름을 넣는 방법도 쓸 수 있어요. 즉 Lst[["name"]]Lst$name과 같죠. 이 방법은 추출하려는 구성요소의 이름이 다른 변수에 저장되어 있을 때 특히 유용해요.

> x <- "name"; Lst[[x]]

여기서 Lst[[1]]Lst[1]을 구분하는 건 아주 중요해요. [[…]]단일 요소 하나를 선택하는 연산자이고, […]는 일반적인 첨자(subscripting) 연산자예요. 즉 전자는 리스트 Lst 안의 첫 번째 객체 그 자체를 가리키는데, 이름이 붙은 리스트라면 그 이름은 포함되지 않아요. 반면 후자는 리스트 Lst에서 첫 번째 항목만으로 이루어진 부분 리스트(sublist) 죠. 이름이 붙은 리스트라면 그 이름들이 부분 리스트로 그대로 전달돼요.

구성요소의 이름은 서로 구별되는 데 필요한 최소 글자 수까지 줄여 쓸 수 있어요. 예를 들어 Lst$coefficientsLst$coe로, Lst$covarianceLst$cov로 줄여 쓸 수 있죠.

이름의 벡터는 사실 리스트의 그냥 하나의 속성(attribute)일 뿐이라, 다른 속성과 마찬가지로 다룰 수 있어요. 물론 리스트가 아닌 다른 구조에서도 비슷하게 names 속성을 부여할 수 있어요.

6.2 리스트 만들기와 수정하기(Constructing and modifying lists)

새로운 리스트는 list() 함수로 기존 객체들로부터 만들 수 있어요. 다음 형태의 할당은

> Lst <- list(name_1=object_1, ..., name_m=object_m)

object_1, …, object_m을 구성요소로 하고 인자 이름에 지정된 대로 이름을 붙인, 구성요소 m 개짜리 리스트 Lst를 만들어요. 이름은 자유롭게 정할 수 있는데, 만약 이름을 생략하면 구성요소는 번호만 매겨져요. 리스트를 만들 때 사용된 구성요소들은 새 리스트로 복사(copy) 되며, 원본 객체는 영향을 받지 않아요.

리스트도 첨자로 참조되는 여타 객체처럼, 추가 구성요소를 지정해 확장할 수 있어요. 예를 들어

> Lst[5] <- list(matrix=Mat)

6.2.1 리스트 이어 붙이기(Concatenating lists)

연결 함수 c()에 리스트 인자들을 주면 결과도 mode가 list인 객체가 되는데, 그 구성요소들은 인자 리스트들의 구성요소가 순서대로 이어진 것이 돼요.

> list.ABC <- c(list.A, list.B, list.C)

벡터 객체들을 인자로 줄 때 연결 함수가 모든 인자를 하나의 벡터 구조로 이어 붙였던 것을 기억하세요. 이 경우에는 dim 같은 다른 속성들은 모두 버려진다는 점도 참고할게요.

6.3 데이터 프레임(Data frames)

데이터 프레임(data frame) 은 클래스가 "data.frame"인 리스트예요. 다만 데이터 프레임이 될 수 있는 리스트에는 몇 가지 제약이 있어요.

  • 구성요소는 벡터(숫자, 문자, 논리), 팩터(factor), 숫자 행렬, 리스트, 또는 다른 데이터 프레임이어야 해요.
  • 행렬, 리스트, 데이터 프레임은 각각 자신의 열(column), 요소(element), 변수(variable) 개수만큼의 변수를 새 데이터 프레임에 제공해요.
  • 데이터 프레임의 변수로 등장하는 벡터 구조는 모두 같은 길이여야 하고, 행렬 구조는 모두 같은 행 수를 가져야 해요.

데이터 프레임은 여러 목적에 있어 열의 모드와 속성이 서로 다를 수 있는 행렬로 간주할 수 있어요. 행렬 형태로 표시할 수 있고, 행렬 인덱싱 규칙을 이용해 행과 열을 추출할 수 있죠.

6.3.1 데이터 프레임 만들기(Making data frames)

데이터 프레임의 열(구성요소)에 대한 제약을 충족하는 객체들은 data.frame 함수로 한데 묶어 데이터 프레임을 만들 수 있어요.

> accountants <- data.frame(home=statef, loot=incomes, shot=incomef)

구성요소가 데이터 프레임의 제약을 모두 만족하는 리스트는 as.data.frame() 함수를 이용해 데이터 프레임으로 강제 변환(coerce) 할 수 있어요.

데이터 프레임을 처음부터 만드는 가장 간단한 방법은 read.table() 함수로 외부 파일에서 통째로 읽어 오는 거예요. 이 내용은 'Reading data from files' 절에서 더 자세히 다룰게요.

6.3.2 attach()detach()

리스트 구성요소를 위한 $ 표기(예: accountants$home)가 항상 편리한 건 아니에요. 리스트나 데이터 프레임의 구성요소를, 매번 리스트 이름을 적지 않고 그 구성요소 이름 그대로 변수처럼 잠시 사용할 수 있게 해 주는 기능이 있으면 참 좋겠죠.

attach() 함수는 리스트나 데이터 프레임 같은 '데이터베이스(database)'를 인자로 받아요. 예를 들어 lentils가 세 변수 lentils$u, lentils$v, lentils$w를 가진 데이터 프레임이라고 해 볼게요. 다음처럼 attach 하면

> attach(lentils)

데이터 프레임이 검색 경로(search path)의 위치 2에 놓이고, 위치 1에 u, v, w라는 변수가 없다면 u, v, w를 데이터 프레임의 변수로 그 자체를 쓸 수 있게 돼요. 이 시점에서

> u <- v+w

같은 할당을 해도 데이터 프레임의 u 구성요소를 바꾸지 않고, 검색 경로의 위치 1에 있는 워크스페이스 안에 u라는 다른 변수로 가려버려요(mask). 데이터 프레임 자체를 영구적으로 바꾸려면 가장 간단하게 다시 $ 표기를 쓰는 방법이 있어요.

> lentils$u <- v+w

다만 구성요소 u의 새 값은 데이터 프레임을 detach하고 다시 attach하기 전까지는 보이지 않아요.

데이터 프레임을 detach하려면 다음 함수를 써요.

> detach()

좀 더 정확히 말하면, 이 문장은 검색 경로에서 현재 위치 2에 있는 객체를 detach해요. 따라서 지금 상황에서는 u, v, w라는 변수는 더 이상 보이지 않고, lentils$u처럼 리스트 표기로만 접근할 수 있게 돼요. 검색 경로의 위치 2보다 더 위에 있는 객체는 그 번호를 detach에 넘겨 detach할 수 있지만, 항상 이름을 쓰는 편이 훨씬 안전해요. 예를 들어 detach(lentils)detach("lentils")처럼요.

참고: R에서 리스트와 데이터 프레임은 위치 2 이상에서만 attach할 수 있고, attach되는 것은 원본 객체의 복사본이에요. assign을 통해 attach된 값을 바꿀 수는 있지만, 원래 리스트나 데이터 프레임은 변하지 않아요.

6.3.3 데이터 프레임 다루기(Working with data frames)

한 워크스페이스에서 여러 가지 다른 문제를 편하게 함께 다룰 수 있게 해 주는 유용한 관례가 있어요.

  • 잘 정의된 각각의 분리된 문제에 대한 모든 변수를, 적절하고 정보를 담은 이름의 데이터 프레임 하나에 모아두세요.
  • 문제를 다룰 때는 해당 데이터 프레임을 위치 2에 attach하고, 위치 1의 워크스페이스는 중간 산출물과 임시 변수용으로 쓰세요.
  • 문제를 마치기 전에, 앞으로 참조하고 싶은 변수들은 $ 형태의 할당으로 데이터 프레임에 추가하고 나서 detach() 하세요.
  • 마지막으로 워크스페이스에서 필요 없는 변수를 모두 제거해서, 남은 임시 변수가 최대한 없도록 깨끗하게 유지하세요.

이렇게 하면 예를 들어 모두 x, y, z라는 변수를 가진 여러 문제를 같은 디렉터리에서 아주 쉽게 다룰 수 있어요.

6.3.4 임의의 리스트 attach하기(Attaching arbitrary lists)

attach()는 제네릭(generic) 함수라서 디렉터리와 데이터 프레임뿐 아니라 다른 클래스의 객체도 검색 경로에 attach할 수 있어요. 특히 mode가 "list"인 어떤 객체든 같은 방식으로 attach할 수 있죠.

> attach(any.old.list)

attach된 것은 무엇이든 detach로, 위치 번호로도 (되도록이면) 이름으로도 detach할 수 있어요.

6.3.5 검색 경로 관리하기(Managing the search path)

search 함수는 현재 검색 경로를 보여 줘서, 어떤 데이터 프레임과 리스트(그리고 패키지)가 attach되고 detach되었는지 추적하는 데 아주 유용해요. 처음에는 다음과 같이 표시돼요.

> search()
[1] ".GlobalEnv"   "Autoloads"    "package:base"

여기서 .GlobalEnv가 바로 워크스페이스예요.

lentils를 attach한 뒤에는

> search()
[1] ".GlobalEnv"   "lentils"      "Autoloads"    "package:base"
> ls(2)
[1] "u" "v" "w"

처럼 보이고, ls(또는 objects)로 검색 경로의 어느 위치에든 있는 내용을 살펴볼 수 있음을 알 수 있어요.

마지막으로 데이터 프레임을 detach하고 검색 경로에서 제거되었는지 확인해 볼게요.

> detach("lentils")
> search()
[1] ".GlobalEnv"   "Autoloads"    "package:base"

더 알아보기

  • [[ ]][ ]의 차이는 R에서 아주 헷갈리기 쉬운데, 리스트에서는 단일 요소 추출 vs 부분 리스트라는 큰 차이가 있어요. 실제로 둘의 결과 구조가 어떻게 다른지 콘솔에서 str()로 직접 확인해 보면 훨씬 와닿아요.
  • attach()는 편리하지만 나중에 의도치 않은 변수 가림(masking)을 일으키기 쉬워요. 요즘 R에서는 attach() 대신 with()$ 표기, 그리고 dplyr 같은 패키지의 파이프 연산을 더 권장하는 추세예요.
  • 데이터 프레임은 통계 분석에서 가장 기본적인 데이터 구조라, read.table(), data.frame(), as.data.frame()의 차이를 직접 데이터로 실습해 보는 걸 추천해요.