Chapter 5 R 리스트

지금까지는 객체에 포함되는 요소가 모두 같은 타입인 벡터나 행렬을 살펴보았다. 이 장에서는 숫자와 문자 등 다른 타입의 데이터를 결합시킬 수 있는 리스트라는 데이터 구조를 살펴본다.

실제 데이터 분석을 수행할 때 사용자가 리스트를 직접적으로 생성하는 경우는 그리 많지 않다. 그러나 리스트를 이해하는 것은 매우 중요한데 그 이유는 다음과 같다.

리스트 이해의 중요성

첫째, 6 장에서 보겠지만 데이터 분석에서 가장 중요한 데이터 구조는 데이터 프레임이다. 그리고 데이터 프레임은 리스트를 기반으로 하고 있다. 따라서 데이터 프레임의 근간이 되는 리스트에 대해 명확하게 이해하는 것이 데이터를 효율적으로 조작하는 데 도움이 된다.

둘째, 통계 및 데이터 마이닝을 위해 사용하는 다양한 R의 함수는 복잡한 분석의 결과를 리스트 타입으로 제공하는 경우가 많다. 따라서 데이터 분석의 결과를 효과적으로 이용하기 위해서는 리스트 구조를 이해할 필요가 있다.

앞서 리스트란 타입이 다른 데이터를 결합시킬 수 있는 데이터 구조라고 했으므로, 본격적으로 리스트를 논하기 전에 R에서 사용하는 다양한 데이터 타입을 먼저 살펴보자.

5.1 객체, 객체의 타입, 객체의 속성 *

객체

R에서는 메모리에 저장하거나 메모리에서 읽어 올 수 있는 모든 데이터 단위를 객체(objects)라고 부른다. 앞에서 살펴본 숫자 벡터, 논리 벡터, 문자 벡터들은 모두 객체이다. 또한 행렬, 배열, 그리고 앞으로 살펴볼 리스트, 데이터 프레임뿐 아니라, 함수, 그래프 등도 모두 객체여서 변수에 할당하여 메모리에 저장하거나 필요시 메모리에서 읽어들일 수 있다.

객체의 타입

객체는 하나의 데이터 타입(type)을 갖는다. (모드(mode)라고도 한다.) 데이터 타입이 다른 객체는 다른 방식으로 메모리에 저장되고 읽어들여진다. 예를 들어 논리 벡터의 경우 논리값 타입을 가지며, 논리값 타입을 갖는 데이터는 논리값이 효율적으로 저장되고 연산될 수 있는 형식으로 메모리에 저장되어진다.

R에서는 함수를 나타내는 closure 타입, R의 표현식을 의미하는 expression 타입 등 다양한 타입이 존재한다. 그러나 데이터의 관점에서 보면 크게 두 가지 종류의 근본적인 데이터 타입이 존재한다. 하나는 원자적 벡터(atomic vectors)이고, 다른 하나는 일반적 벡터(generic vectors) 또는 리스트라고 불리는 데이터 타입이다.

원자적 벡터 atomic vector

원자적 벡터는 숫자 벡터, 논리 벡터, 문자 벡터처럼 하나의 데이터 형식으로 저장되는 데이터 타입을 의미한다. 반면 리스트는 숫자와 문자 등 서로 다른 데이터 형식으로 저장되는 요소를 가지는 데이터 타입이다. 원자적 벡터의 대표적인 타입은 논리값(logical), 정수(integer), 실수(double), 복소수(complex), 문자(chracter), 바이트(raw) 등이 있다.

typeof()

객체가 어떤 데이터 타입으로 저장되어 있는지를 확인하기 위해서 typeof() 함수를 이용할 수 있다.

a <- c(T, F, F, T); typeof(a)
[1] "logical"
b <- 1:4; typeof(b)
[1] "integer"
d <- c(1.5, 2.7, 3.3, 4.7); typeof(d)
[1] "double"
e <- c("car", "bus", "train", "plane"); typeof(e)
[1] "character"

데이터 자동 형변환

논리값, 정수, 실수, 문자 데이터 형식은 제시된 순서대로 데이터가 자동 형변환 된다. 논리값과 정수가 하나의 벡터에 같이 사용되면, 벡터는 하나의 데이터 형식으로 저장되어야 하므로 논리값보다 더 표현 범위가 넓은 정수 형식으로 데이터가 변환된다. 논리값의 FALSE는 0으로, TRUE는 1로 변환된다. 정수와 실수가 같이 사용되면 모두 실수 형식으로 변환된다. 실수와 문자 데이터가 같이 사용되면 모두 문자 형식으로 자동 변환된다.

f <- c(a, b); f; typeof(f)
[1] 1 0 0 1 1 2 3 4
[1] "integer"
g <- c(b, d); g; typeof(g)
[1] 1.0 2.0 3.0 4.0 1.5 2.7 3.3 4.7
[1] "double"
h <- c(d, e); h; typeof(h)
[1] "1.5"   "2.7"   "3.3"   "4.7"   "car"   "bus"   "train" "plane"
[1] "character"

물론 숫자 형식의 데이터가 문자로 변환되면 더이상 숫자로서의 연산을 지원되지 않는다.

g * 2
[1] 2.0 4.0 6.0 8.0 3.0 5.4 6.6 9.4
h * 2
Error in h * 2: 이항연산자에 수치가 아닌 인수입니다

내재적 속성

객체의 데이터 타입은 모든 객체의 내재적 속성 중 하나이다. 모든 객체가 가지는 또 다른 내재적 속성은 길이이다. 길이 속성은 해당 객체의 데이터 요소가 몇 개인지를 알려준다. typeof(objects)와 length(objects) 함수를 이용하면 객체의 데이터 타입과 길이를 알아낼 수 있다.

length(a)
[1] 4
length(b)
[1] 4

행렬의 타입

행렬도 결국은 모두 같은 데이터 형식을 가지는 데이터 요소로 구성되어 있으므로, 원자적 벡터 형식의 데이터라고 할 수 있다. 따라서 행렬의 데이터 타입도 결국 벡터가 가지는 데이터 타입과 동일함을 확인할 수 있다.

aa <- matrix(a, nrow=2); typeof(aa); length(aa)
[1] "logical"
[1] 4
bb <- matrix(b, nrow=2); typeof(bb); length(bb)
[1] "integer"
[1] 4
dd <- matrix(d, nrow=2); typeof(dd); length(dd)
[1] "double"
[1] 4
ee <- matrix(e, nrow=2); typeof(ee); length(ee)
[1] "character"
[1] 4

즉 벡터 b와 행렬 bb는 동일한 형식으로 정수가 4개 메모리에 저장되어 있는 데이터이다. 그러므로 데이터 타입도 integer로 동일하고 요소의 길이도 동일하다. 그러나 b와 bb를 출력해 보면 서로 다른 방식으로 처리되어 출력됨을 볼 수 있다. b는 일련의 숫자로 출력되고, bb는 행과 열이 2인 행렬의 형식으로 출력된다. R은 똑같이 저장되어 있는 데이터에서 이를 어떻게 구분하여 처리하는 것일까?

b
[1] 1 2 3 4
bb
     [,1] [,2]
[1,]    1    3
[2,]    2    4
class(b)
[1] "integer"
class(bb)
[1] "matrix" "array" 

클래스 class()

R은 객체에 데이터 타입 말고 클래스라는 속성을 부여할 수 있다. 데이터 타입은 내재적 속성으로 모든 객체에게 부여된다. 반면 클래스는 객체에 따라 부여되지 않을 수도 있고 여러 개가 부여될 수 있는 속성으로, 객체가 R 함수에 의해 처리될 때 어떤 방식으로 처리되어야 하는지를 알려준다. 위의 예에서 숫자 벡터 b는 아무 클래스도 부여되지 않았으므로 데이터 타입인 integer가 클래스로 지정되어 있고, 행렬 bb의 데이터 타입은 integer이지만 클래스로 matrix가 부여되어 있음을 확인할 수 있다. 따라서 R은 b와 bb의 클래스가 다르므로 print() 출력 함수로 데이터를 출력할 때 다른 방식으로 처리를 수행하였다.

attributes()

그러면 R은 4개의 정수가 차례로 저장되어 있는 bb에 대해 2개의 행과 2개의 열을 가진 행렬로 표현해야 한다는 것을 어떻게 알았을까? R은 객체의 데이터를 데이터 타입에 맞추어 저장하고 있을 뿐만 아니라, 그 객체의 부가 정보(메타 정보)를 속성이라는 형태로 저장하고 있다. 각 객체가 가진 내재적 속성인 데이터 타입과 길이를 제외한 모든 속성은 attributes(objects) 함수에 의해 확인할 수 있다.

attributes(b)
NULL
attributes(bb)
$dim
[1] 2 2

위의 예에서 벡터 b는 다른 속성 정보가 없지만, bb는 dim 속성에 행과 열의 길이 정보가 부여되어 있음을 알 수 있다. 사실 R은 벡터에 부여된 dim 속성을 보고 이 벡터가 행렬로 또는 배열로 처리되어야 하는지를 인식하고, 행과 열의 길에에 맞게 출력과 연산 등을 수행한다.

attr()

attributes(objects) 함수는 객체에 내재된 속성을 제외한 모든 속성을 보여주거나 모든 속성에 데이터를 할당할 때 이용되는 반면, attr(object, name) 함수는 객체에서 특정 이름을 가진 속성을 보여주거나 해당 속성에 데이터를 할당하기 위해 이용된다. 이러한 함수가 자주 사용되지는 않지만 속성에 대한 개념을 이해하는 것은 매우 중요하다. R의 객체 시스템과 각 속성들은 통합되어 있으므로 객체의 속성을 할당하거나 삭제할 때 주의할 필요가 있기 때문이다. 객체의 속성은 할당문의 좌변에 사용되어서 객체에 새로운 속성을 부여하거나 기존의 속성 값을 변경할 수 있다. 예를 들어 다음과 같이 z 객체에 dim 속성을 부여하여 R이 z를 3ⅹ3의 행렬인 것처럼 다루도록 할 수 있다.

z <- 1:9
z
[1] 1 2 3 4 5 6 7 8 9
class(z)
[1] "integer"
attr(z, "dim")
NULL
attr(z, "dim") <- c(3,3)
attributes(z)
$dim
[1] 3 3
z
     [,1] [,2] [,3]
[1,]    1    4    7
[2,]    2    5    8
[3,]    3    6    9

아래 예는 dimnames 속성에 행과 열의 이름을 할당하여 행렬이 출력될 때 행과 열의 이름이 같이 출력되도록 한 경우이다. 이 경우도 R은 저장되어 있는 9개의 정수뿐 아니라 객체의 부가적인 속성 정보를 이용하여 어떤 식으로 처리할 것인지를 결정하게 된다.

attr(z, "dimnames") <- list(c("A", "B", "C"), 1:3)
attributes(z)
$dim
[1] 3 3

$dimnames
$dimnames[[1]]
[1] "A" "B" "C"

$dimnames[[2]]
[1] "1" "2" "3"
z
  1 2 3
A 1 4 7
B 2 5 8
C 3 6 9

객체 속성 함수

attr() 함수를 이용하여 다양한 속성을 객체에 부가할 수 있다. 그러나 보통 속성 이름을 잘못 부여하여 오작동하는 것을 방지하고, 사용자의 편의를 도모하기 위해 자주 사용되는 속성을 확인하거나 할당하는 dim(), dimnames(), names(), row.names(), class() 등의 각 속성 전용의 함수들이 정의되어 있다. 각 함수의 사용법은 이 책의 관련 부분이나 R의 도움말을 참고하기 바란다. 객체 속성을 변경할 때는 이러한 attr()보다는 객체 속성 함수를 이용하는 것이 좋다. 왜냐하면 attr() 함수는 속성 이름이 잘 못되더라도 새로운 속성을 설정하는 것으로 생각하여 오류가 발생하지 않는다. 객체 속성 함수를 사용하면 해당 속성을 위한 함수를 잘 못 지정하면 오류가 발생하기 때문에 입력 오류를 바로 확인할 수 있다.

모드와 타입의 차이

마지막으로 모드와 타입의 차이를 설명하고자 한다. 많은 R 문서나 책을 보면 앞에서 이야기한 데이터 타입을 모드라는 이름으로 설명하는 경우가 많다. 사실 모드와 타입은 비슷한 개념으로 생각하면 된다. 타입이 R에서 이용되는 개념이라고 한다면, 모드는 R의 전신인 S 언어에서 데이터의 기본 형식을 지정하기 위해서 사용한 개념이다. R은 S 언어에 대한 호환성을 보장하기 위해 모드라는 개념을 같이 혼용하여 사용하고 있다. 객체의 모드를 확인하기 위해서는 mode() 함수를 이용한다.

typeof(a); mode(a)
[1] "logical"
[1] "logical"
typeof(b); mode(b)
[1] "integer"
[1] "numeric"
typeof(d); mode(d)
[1] "double"
[1] "numeric"
typeof(e); mode(e)
[1] "character"
[1] "character"

대부분의 경우 타입과 모드는 비슷한 결과를 주지만, 숫자 벡터의 경우 R의 타입은 integer와 double로 저장 형식이 정수인지 실수인지 구분하고, S 언어의 모드는 모두 numeric으로 표시한다. 그러나 S도 storage.mode()라는 함수를 이용하면 내부적으로 저장하는 형식을 확인할 수 있다.

storage.mode(b)
[1] "integer"
storage.mode(d)
[1] "double"

그러나 R에서는 mode()나 storage.mode() 함수 모두 typeof() 함수의 결과에 기반을 하고 있으므로, S 언어와의 하위 호환성 때문이 아니라면 타입이라는 개념과 typeof() 함수를 사용하는 것이 좋다.

5.2 리스트의 생성 및 필터링

R의 리스트는 요소(component)라고 불리는 객체들을 순서대로 모은 데이터 구조이다. 리스트의 요소들은 서로 다른 데이터 형식일 수 있다. 이론적으로 말하자면 리스트는 서로 다른 타입을 가지는 요소에 대한 주소 정보를 가지고 있는 데이터 형식이다.

list()

리스트 객체는 list() 함수를 이용하여 만드는데, 아래처럼 리스트 요소에 이름 없이 만드는 방법과 리스트 요소에 이름을 붙여서 만드는 방법이 있다.

list(요소1, 요소2, ...)
list(이름1=요소1, 이름2=요소2, ...)

다음은 list() 함수를 이용하여 리스트를 만드는 예를 보여주고 있다.

a <- list(name="Fred", age=43, wife="Mary", 
            no.children= 3, child.ages=c(4, 7, 9), 
          is.house.owner=T)
a
$name
[1] "Fred"

$age
[1] 43

$wife
[1] "Mary"

$no.children
[1] 3

$child.ages
[1] 4 7 9

$is.house.owner
[1] TRUE

a의 요소로 문자, 숫자, 논리값을 모두 포함할 수 있음을 볼 수 있다. 뒤에서 살펴보겠지만 리스트는 요소로 다른 리스트를 포함할 수도 있고, 함수 등의 다른 타입의 객체도 포함할 수 있다.

위의 예에서 list() 함수의 인수로 리스트에 포함될 요소를 ’이름=요소’의 형식으로 기술하였다. a 객체를 출력해 보면 각 요소의 이름이 $를 앞에 붙여 출력되고 그 다음에 요소의 내용이 출력된다.

리스트 객체의 타입, 길이, 속성 및 names()

typeof()와 length() 함수를 이용하면 객체의 내재적 속성인 타입과 요소의 길이를 확인할 수 있다. attributes() 함수를 이용하면 그 밖의 속성을 확인할 수 있는데, names 속성이 부여되어 있음을 볼 수 있다. 사실 attributes()에 의해 반환되는 결과는 속성을 요소로 갖는 리스트 객체이다.

typeof(a)
[1] "list"
length(a)
[1] 6
attributes(a)
$names
[1] "name"           "age"            "wife"           "no.children"   
[5] "child.ages"     "is.house.owner"
names(a)
[1] "name"           "age"            "wife"           "no.children"   
[5] "child.ages"     "is.house.owner"

list() 함수를 이용하여 객체를 정의할 때 요소에 이름을 부여하지 않으면, 특별한 이름 없이 각 요소가 순서대로 1부터 숫자가 매겨진다. 앞서와 달리 요소의 이름 대신 요소의 번호가 [[ ]] 안에 표시된 후 요소의 내용이 출력됨을 볼 수 있다.

a2 <- list(1:5, letters[1:8], LETTERS[1:3])
a2
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[3]]
[1] "A" "B" "C"

5.2.0.1 리스트 요소의 이름 자동으로 부여되지 않는다.

2차원 데이터 구조인 행렬을 만드는 cbind() 함수나, 또 다른 2차원 데이터 구조인 데이터프레임을 만드는 data.frame() 함수는 변수를 사용하여 행렬과 데이터프레임을 만들면 변수명을 열 이름으로 자동적으로 부여한다. (data.frame() 함수에 대해서는 6 장을 참조한다.)

var1 <- 1:3
var2 <- 4:6
cbind(var1, var2)
     var1 var2
[1,]    1    4
[2,]    2    5
[3,]    3    6
data.frame(var1, var2)
  var1 var2
1    1    4
2    2    5
3    3    6

반면 list() 함수는 변수를 요소로 사용해도 변수명을 요소의 이름으로 자동으로 부여하지 않는다.

list(var1, var2)
[[1]]
[1] 1 2 3

[[2]]
[1] 4 5 6

그러므로 리스트 생성 시 요소에 이름을 부여하려면 앞서 본 것과 같이 요소이름=요소 형식으로 리스트를 생성하여야 한다.

list(var1=var1, var2=var2)
$var1
[1] 1 2 3

$var2
[1] 4 5 6

리스트 요소 지정

리스트에서 요소 하나를 지정할 때는 주로 다음의 세가지 방법을 사용한다.

첫번째 방법은, [[ ]] 연산자와 요소의 번호로 지정하는 것이다.

두번째 방법은 list_name$component_name의 형태로 $ 뒤에 리스트 요소의 이름을 이용하여 지정하는 것이다.

세번째 방법은, [[ ]] 연산자 안에 숫자 대신 요소의 이름을 나타내는 문자열을 제시하여 지정하는 것이다. 이 때 두번째와 세번째 방법은 요소의 이름을 사용하는 것은 모두 동일하나, 두번째 방법에서는 $ 뒤에 사용되는 요소의 이름이 변수처럼 따옴표 없이 그대로 이용되는 반면, 세번째 방법에서는 요소의 이름이 문자열로 따옴표로 표현되는 차이가 있다. 숫자 대신 이름을 이용하는 것은 숫자를 기억하지 않아도 되므로 편리하다.

a[[3]]
[1] "Mary"
a$wife
[1] "Mary"
a[["wife"]]
[1] "Mary"

단계적으로 요소 지정

리스트 요소가 벡터나 행렬인 경우에는 지정된 요소에 다시 인덱스 벡터를 이용하여 부분을 선택할 수 있다.

a[[5]]
[1] 4 7 9
a[[5]][2:3]
[1] 7 9
a$child.ages[2:3]
[1] 7 9

요소 이름의 단축

요소의 이름이 다른 요소들의 이름과 충분히 구별 가능하면 축약된 형태로 적을 수도 있다. 그러나 되도록 이름의 단축 기능은 사용하지 말 것을 권장한다. 최근의 R 관련 프로그램은 대부분 코드 자동 완성 기능이 있으므로 긴 이름도 쉽게 작성할 수 있으므로, 이름의 단축으로 얻는 입력 타수 절약이라는 장점보다는 명령문의 가독성이 줄어드는 단점이 더 크기 때문이다.

a$no
[1] 3
a$child
[1] 4 7 9

부분 리스트로 필터링

지금까지는 리스트의 한 요소를 지정하는 방법에 대하여 설명하였다. 어떤 경우에는 리스트의 여러 요소를 지정할 필요가 있다. 이러한 경우에는 [[ ]] 연산자가 아니라 벡터의 필터링에 사용한 [ ]를 동일하게 이용한다.

a[1:3]
$name
[1] "Fred"

$age
[1] 43

$wife
[1] "Mary"
a[c(2, 5)]
$age
[1] 43

$child.ages
[1] 4 7 9
a[-(4:5)]
$name
[1] "Fred"

$age
[1] 43

$wife
[1] "Mary"

$is.house.owner
[1] TRUE
a[c("wife", "child.ages")]
$wife
[1] "Mary"

$child.ages
[1] 4 7 9
a[c(T, F, F)]
$name
[1] "Fred"

$no.children
[1] 3

벡터의 필터링과 마찬가지로 자연수를 이용하여 해당 위치의 요소를 뽑을 수도 있고, 음의 정수를 이용하여 해당 위치의 요소를 빼고 필터링을 수행할 수도 있다. 문자열 벡터를 이용하여 요소의 이름으로 필터링이 가능하다.

리스트 요소 지정 vs. 부분 리스트 필터링

여기서 주목해야 할 것은 벡터의 필터링의 결과는 원래 벡터의 부분으로 구성된 벡터인 것과 마찬가지로, 리스트의 [ ] 필터링의 결과는 리스트의 부분으로 구성된 또 다른 리스트가 된다. 반면 [[ ]]나 $ 연산자를 이용하여 요소를 지정하는 것은 리스트의 요소 그 자체가 된다. 즉, 또 다른 리스트가 되는 것이 아니라 리스트의 요소가 벡터이면 벡터 그 자체가 된다. 다음의 예는 [ ]와 [[ ]] 연산자를 이용하여 a의 5번째 요소를 뽑아낸 예이다.

a[[5]]
[1] 4 7 9
a[5]
$child.ages
[1] 4 7 9

전자는 벡터로 출력되고 후자는 다시 요소가 하나짜리 리스트로 표현됨을 볼 수 있다. 내용적으로 두 결과의 차이가 크지 않지만 형식적으로는 두 결과는 큰 차이를 가진다. 전자의 경우는 요소 그 자체인 벡터가 되므로 벡터 필터링이나 벡터와 관련된 연산이 가능하지만, 후자는 불가능하다. 대신 리스트와 관련된 연산이나 필터링을 수행해야 한다.

a[[5]][2:3]
[1] 7 9
a[[5]] * 7
[1] 28 49 63
a[5][2:3]
$<NA>
NULL

$<NA>
NULL
a[5] * 7
Error in a[5] * 7: 이항연산자에 수치가 아닌 인수입니다

위의 예에서 [[ ]]로 지정된 결과는 요소 그 자체가 되므로, 결과가 숫자 벡터가 된다. 따라서 벡터와 관련된 모든 연산이 가능하다. 반면 [ ]로 필터링된 결과는 리스트라는 형식이 유지되어 요소가 하나인 리스트가 된다. 따라서 2번째와 3번째 요소를 필터링하자, 해당 요소가 없으므로 모두 NULL로 표시된다. 리스트이므로 수식 벡터의 연산도 수행되지 않음을 알 수 있다.

5.3 리스트의 변경 및 연결

리스트 요소의 추가

이미 생성된 리스트는 새로운 요소를 추가함으로써 확장될 수 있다. 새로운 요소를 추가하거나 변경하는 방법은 여러 가지가 있을 수 있는데, 첫번째로 생각할 수 있는 방법은 [[ ]] 연산자나 $ 연산자를 이용하여 요소를 추가하거나 변경하는 방법이다. 다음은 새로운 요소를 리스트에 추가한 예이다.

length(a)
[1] 6
a[[7]] <- 1:5
a[["address"]] <- "Cheonan"
a$years.since.marrage <- 15
a
$name
[1] "Fred"

$age
[1] 43

$wife
[1] "Mary"

$no.children
[1] 3

$child.ages
[1] 4 7 9

$is.house.owner
[1] TRUE

[[7]]
[1] 1 2 3 4 5

$address
[1] "Cheonan"

$years.since.marrage
[1] 15

리스트 요소의 변경

벡터와 마찬가지로 기존 요소를 지정한 후 할당을 하면 기존 요소가 변경된다.

a[[7]] <- 10:18
a$address <- "Daejeon"
a[[9]] <- 16
a
$name
[1] "Fred"

$age
[1] 43

$wife
[1] "Mary"

$no.children
[1] 3

$child.ages
[1] 4 7 9

$is.house.owner
[1] TRUE

[[7]]
[1] 10 11 12 13 14 15 16 17 18

$address
[1] "Daejeon"

$years.since.marrage
[1] 16

자연수 인덱스를 이용한 리스트 요소의 추가와 빈 요소의 생성

위의 예에서 새로운 요소를 추가할 때 요소의 이름을 이용하면 현재까지 있는 요소의 다음 위치에 차례로 해당 이름을 갖는 요소가 추가된다. 그러나 숫자를 이용하여 위치를 지정하여 새로운 요소를 추가하면 해당 위치에 요소가 생성된다. 위의 예처럼 이미 해당 위치에 다른 요소가 있다면 새로운 값으로 변경이 될 것이며, 아무 것도 없었다면 해당 위치에 새로운 데이터가 추가된다. 그러면 기존 리스트의 마지막 요소의 다음 위치가 아니라 더 먼 위치에 새로운 요소를 추가하면 어떻게 될까?

a2
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[3]]
[1] "A" "B" "C"
a2[[5]] <- "New items"
a2
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[3]]
[1] "A" "B" "C"

[[4]]
NULL

[[5]]
[1] "New items"

위의 예에서 보듯이 비어 있는 요소는 아무 것도 없다는 뜻의 NULL로 표현되어 출력되고 마지막 위치에 있는 요소까지 모두 출력된다.

리스트 요소의 삭제

리스트의 요소를 삭제하기 위해서는 해당 요소를 [[ ]]나 $ 연산자를 이용하여 지정한 후 해당 요소에 NULL을 할당하면 된다.

a2[[5]] <- NULL
a2
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[3]]
[1] "A" "B" "C"

[[4]]
NULL
a2[[4]] <- NULL
a2
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[3]]
[1] "A" "B" "C"
names(a2) <- c("num", "lower.case", "upper.case")
a2
$num
[1] 1 2 3 4 5

$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"
a2$num <- NULL
a2
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

부분 리스트를 이용한 리스트 변경 *

지금까지는 [[ ]]나 $ 연산자를 이용하여 요소 하나를 추가, 변경, 삭제하는 것을 보여주었다. [ ] 연산자를 이용하면 리스트의 여러 요소를 추가 또는 변경할 수 있다. 이 때 할당문의 오른쪽에 있는 객체는 리스트 객체여야 한다. 왜냐하면 [ ] 연산자를 이용한 필터링의 결과는 원래 리스트의 부분 리스트이기 때문이다.

a2[3:4] <- list(1:5, month.name)
a2
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

[[3]]
[1] 1 2 3 4 5

[[4]]
 [1] "January"   "February"  "March"     "April"     "May"       "June"     
 [7] "July"      "August"    "September" "October"   "November"  "December" 

리스트의 recycling *

만약 오른쪽 항의 리스트가 할당될 부분 리스트보다 요소 수가 적으면 오른쪽 항의 리스트가 재사용된다.

a2[3:6] <- list(11:15, month.abb)
a2
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

[[3]]
[1] 11 12 13 14 15

[[4]]
 [1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"

[[5]]
[1] 11 12 13 14 15

[[6]]
 [1] "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"

벡터의 리스트로의 형변환 *

또한 부분 리스트를 지정한 후 할당하는 예에서, 오른쪽에 리스트가 아니라 벡터가 주어지면 벡터의 각 요소가 리스트의 요소로 변환된 후 할당이 이루어진다. 이는 R 등의 고급 언어들의 특징인데 할당해야 할 내용이 할당받을 객체의 타입과 다른 경우, 가능하면 할당문의 왼쪽에 있는 객체의 타입으로 변환하려고 노력한다. as.list() 함수를 이용하여 벡터를 리스트로 사용자가 명시적으로 변환할 수도 있다.

a2[3:6] <- c("X", "Y", "Z", "W")
a2
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

[[3]]
[1] "X"

[[4]]
[1] "Y"

[[5]]
[1] "Z"

[[6]]
[1] "W"
c("X", "Y", "Z", "W")
[1] "X" "Y" "Z" "W"
as.list(c("X", "Y", "Z", "W"))
[[1]]
[1] "X"

[[2]]
[1] "Y"

[[3]]
[1] "Z"

[[4]]
[1] "W"
a2[3:6] <- NULL
a2
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

위의 예에서 부분리스트에 NULL을 할당하면 부분 리스트에 해당되는 모든 요소가 삭제됨을 볼 수 있다. 이 때도 역시 NULL이 재사용되어 4개의 요소에 모두 NULL이 할당되었다.

c()를 이용한 리스트 연결하기

리스트는 벡터와 같이 c() 함수를 이용하여 리스트들을 연결하여 하나의 리스트로 만들 수 있다.

a3 <- list(first=1:3, second=4:6)
a3
$first
[1] 1 2 3

$second
[1] 4 5 6
a4 <- c(a2, a3)
a4
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

$first
[1] 1 2 3

$second
[1] 4 5 6
length(a4)
[1] 4

계층적인 리스트 만들기

위의 예에서 c()로 연결된 리스트들은 모든 요소가 평평하게 하나의 계층으로 연결되었음을 볼 수 있다. 그런데 리스트는 모든 타입의 요소를 포함할 수 있기 때문에 리스트의 요소에 리스트가 할당될 수 있다. 이 경우 리스트는 계층적인 구조를 갖게 된다.

a5 <- list(a2, a3)
a5
[[1]]
[[1]]$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[1]]$upper.case
[1] "A" "B" "C"


[[2]]
[[2]]$first
[1] 1 2 3

[[2]]$second
[1] 4 5 6
length(a5)
[1] 2

c()로 같은 계층으로 연결한 앞의 a4의 결과와 a5의 출력 결과를 비교해 보라. a4에서 upper.case 요소를 뽑기 위해서는 2번째 요소를 뽑으면 된다. 그러나 a5에서는 2번째 요소를 뽑으면 2번째 리스트 전체가 뽑혀져서 나오는 것을 볼 수 있다. a5에서 upper.case를 뽑으려면 첫번째 요소를 뽑은 후, 거기서 다시 2번째 요소를 뽑아야 한다.

a4[[2]]
[1] "A" "B" "C"
a5[[2]]
$first
[1] 1 2 3

$second
[1] 4 5 6
a5[[1]][[2]]
[1] "A" "B" "C"
a5[[2]]$second
[1] 4 5 6
a4$second
[1] 4 5 6

계층적인 구조의 리스트를 c()로 연결하면 첫 층위에서만 하나의 리스트로 합쳐진다. 모든 층위의 요소를 재귀적으로 하나로 합치려면 recursive=TRUE 인수를 설정한다. 그러면 모든 요소들이 하나로 합쳐져 벡터로 변환된다. 다음 예에서는 문자 요소가 있으므로 모두 문자로 자동 형변환되어 합쳐졌다.

c(a4, a5)
$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

$upper.case
[1] "A" "B" "C"

$first
[1] 1 2 3

$second
[1] 4 5 6

[[5]]
[[5]]$lower.case
[1] "a" "b" "c" "d" "e" "f" "g" "h"

[[5]]$upper.case
[1] "A" "B" "C"


[[6]]
[[6]]$first
[1] 1 2 3

[[6]]$second
[1] 4 5 6
c(a4, a5, recursive = TRUE)
lower.case1 lower.case2 lower.case3 lower.case4 lower.case5 lower.case6 
        "a"         "b"         "c"         "d"         "e"         "f" 
lower.case7 lower.case8 upper.case1 upper.case2 upper.case3      first1 
        "g"         "h"         "A"         "B"         "C"         "1" 
     first2      first3     second1     second2     second3 lower.case1 
        "2"         "3"         "4"         "5"         "6"         "a" 
lower.case2 lower.case3 lower.case4 lower.case5 lower.case6 lower.case7 
        "b"         "c"         "d"         "e"         "f"         "g" 
lower.case8 upper.case1 upper.case2 upper.case3      first1      first2 
        "h"         "A"         "B"         "C"         "1"         "2" 
     first3     second1     second2     second3 
        "3"         "4"         "5"         "6" 

unlist()로 리스트를 벡터로 형 변환하기

앞서 as.list()를 이용하여 벡터를 리스트로 변환한 예를 보았다. 어떤 경우에는 리스트의 모든 요소를 벡터로 변환할 필요가 있다. 앞의 예처럼 c( , recursive = TRUE)를 사용할 수도 있지만, 이 때 사용하는 전용 함수인 unlist()를 사용하는 것이 더 일반적이다.

unlist(a3)
 first1  first2  first3 second1 second2 second3 
      1       2       3       4       5       6 
unlist(a4)
lower.case1 lower.case2 lower.case3 lower.case4 lower.case5 lower.case6 
        "a"         "b"         "c"         "d"         "e"         "f" 
lower.case7 lower.case8 upper.case1 upper.case2 upper.case3      first1 
        "g"         "h"         "A"         "B"         "C"         "1" 
     first2      first3     second1     second2     second3 
        "2"         "3"         "4"         "5"         "6" 

예에서 보듯이 리스트의 요소가 모두 숫자로 표시가 가능하면 숫자 벡터로 변환하지만, 하나라도 문자 등이 있으면 모두 문자 벡터로 변환한다. 그리고 벡터의 각 요소에 리스트 요소의 이름과 벡터의 위치에 따라 자동으로 이름이 부여되었음을 확인할 수 있다. unlist() 함수는 strsplit() 결과를 하나의 문자 벡터로 만들 때 자주 사용된다.

a <- c("R은 통계분석을 위해 특화된 프로그램 언어입니다.", "다양한 데이터 분석 함수가 내장되어 있습니다.")
b <- strsplit(a, split = " ")
b
[[1]]
[1] "R은"         "통계분석을"  "위해"        "특화된"      "프로그램"   
[6] "언어입니다."

[[2]]
[1] "다양한"    "데이터"    "분석"      "함수가"    "내장되어"  "있습니다."
unlist(b)
 [1] "R은"         "통계분석을"  "위해"        "특화된"      "프로그램"   
 [6] "언어입니다." "다양한"      "데이터"      "분석"        "함수가"     
[11] "내장되어"    "있습니다."  

5.4 리스트에 함수 적용하기

행렬을 공부할 때 apply() 함수를 이용하여 행렬의 각 행 또는 각 열에 함수를 적용하는 방법을 공부하였다. 리스트에도 리스트의 각 요소에 함수를 적용하는 lapply()와 sapply() 함수가 있다.

5.4.1 lapply() 함수

lapply()

lapply() 함수는 다음의 문법 구조를 가진다.

lapply(리스트, 함수)

다음 예는 lapply()를 이용하여 숫자 벡터로 이루어진 리스트의 각 요소에 mean() 함수를 적용한 예이다. 어떤 객체에 함수를 적용할 때는 function(object) 형식으로 함수의 인수로 객체를 제공한다. 그러나 lapply()에서는 첫번째 인수로 리스트를 제공하고, 두번째 인수로 함수 객체를 제공한다.

b <- list(1:5, 21:29, seq(2, 20, by=2))
b
[[1]]
[1] 1 2 3 4 5

[[2]]
[1] 21 22 23 24 25 26 27 28 29

[[3]]
 [1]  2  4  6  8 10 12 14 16 18 20
mean(b[[1]])
[1] 3
mean(b[[2]])
[1] 25
mean(b[[3]])
[1] 11
lapply(b, mean)
[[1]]
[1] 3

[[2]]
[1] 25

[[3]]
[1] 11

위의 예에서 보듯이 lapply()는 리스트의 각 요소에 함수를 적용한 후 그 결과를 동일한 형식의 리스트 객체로 제공한다.

lapply() 결과 리스트의 요소 이름

아래 예는 리스트에 여러 함수들을 적용한 예이다. 리스트에 이름을 제공하면 결과에도 동일한 이름이 요소에 붙여짐을 확인할 수 있다.

lapply(b, max)
[[1]]
[1] 5

[[2]]
[1] 29

[[3]]
[1] 20
names(b) <- c("A", "B", "C")
lapply(b, range)
$A
[1] 1 5

$B
[1] 21 29

$C
[1]  2 20

5.4.2 sapply() 함수

sapply()

많은 경우에 최종 결과가 리스트보다는 벡터나 행렬 등의 간단한 형태로 제공되는 것이 편리하다. 이런 경우에는 sapply()를 사용한다. (simplified lapply로 기억하면 편리하다.) sapply()도 lapply()와 문법적 구조가 동일하다. 다른 점은 최종 결과를 리스트가 아니라 가능하면 벡터나 행렬처럼 간단한 형식으로 반환한다는 점이다. sapply()도 apply() 함수처럼 벡터로 결과를 반환하거나, 요소 하나에 함수를 적용한 결과가 길이가 2 이상인 벡터이면 열로 결과를 붙여서 행렬 형식으로 결과를 반환한다.

sapply(b, length)
 A  B  C 
 5  9 10 
sapply(b, range)
     A  B  C
[1,] 1 21  2
[2,] 5 29 20
lb <- lapply(b, length)
typeof(lb)
[1] "list"
sb <- sapply(b, length)
typeof(sb)
[1] "integer"

사용자 정의 함수의 적용

lapply()와 sapply(), 또는 apply() 등을 이용하여 객체의 각 요소나 부분에 함수를 적용할 때 기존의 함수뿐 아니라 사용자가 정의한 함수를 적용할 수도 있다. (함수를 정의하는 법은 10 장의 함수와 관련된 내용을 참조하기 바란다.) 다음은 리스트의 각 요소에서 10보다 큰 수의 개수를 세는 예이다.

sapply(b, function(x){
  sum(x > 10)
})
A B C 
0 9 5 

5.4.3 mapply() 함수

lapply()와 sapply()가 하나의 리스트에 대해 각 요소에 함수를 적용한다면, mapply()는 다수의 리스트에 대해 같은 위치의 요소들에 함수를 적용하기 위한 함수이다. mapply()는 sapply()처럼 결과를 벡터나 행렬 등의 단순한 형태로 제공할 수 있으면 리스트가 아니라 단순한 형태로 결과를 제공한다.

mapply()는 다음과 같은 문법 구조를 가진다. 먼저 리스트의 요소에 적용할 함수를 첫번째 FUN 인수로 제공한다. 그리고는 함수를 동시에 같이 적용할 리스트의 목록을 기술한다. 그러면 이 리스트의 같은 위치의 요소들이 함수의 인수로 제공된다. 리스트 외에도 별도의 인수를 함수에 제공할 필요가 있으면 MoreArgs 인수에 리스트 형태로 제공한다.

mapply(FUN, list_1(vector_1), ..., list_n(vector_n), 
       MoreArgs=NULL)

다음과 같은 2 개의 리스트가 존재한다고 하자.

a <- list(1:5, 10:5, letters[1:4])
b <- list(6:4, 4:7, LETTERS[5:1])

다음은 리스트의 같은 위치에 있는 요소를 c() 함수를 이용하여 각각 연결한 예이다.

mapply(c, a, b)
[[1]]
[1] 1 2 3 4 5 6 5 4

[[2]]
 [1] 10  9  8  7  6  5  4  5  6  7

[[3]]
[1] "a" "b" "c" "d" "E" "D" "C" "B" "A"

또한 mapply()에 리스트를 인수로 제공할 때 이름=리스트 형태로 인수를 제공하면 FUN 인수에 부여된 함수에 해당 이름으로 인수가 제공된다. 다음은 rep() 함수에 반복할 벡터 x와 반복 횟수를 times을 리스트로 제공한 예이다.

mapply(rep, times=2:4, x=list(1:2, 11:12, 21:22))
[[1]]
[1] 1 2 1 2

[[2]]
[1] 11 12 11 12 11 12

[[3]]
[1] 21 22 21 22 21 22 21 22

위의 결과에서 x 리스트의 각 요소와 times 벡터의 각 요소가 차례로 결합되어 rep() 함수가 적용되었음을 볼 수 있다.

5.5 리스트 활용 분야

리스트로 결과를 반환하는 함수의 예

이 장의 시작부분에서 리스트가 중요한 이유를 설명할 때, 많은 데이터 분석 함수들이 결과를 리스트 형태로 제공한다고 하였다. 실제 그러한지 예를 통해 살펴보도록 하자. 다음은 선형회귀분석을 수행하는 lm() 함수의 결과가 리스트 객체라는 것을 보여준다. 리스트 객체의 각 요소는 수행된 선형회귀분석에 대한 자세한 결과를 저장하고 있다.

x <- lm(dist~speed, data=cars)
x

Call:
lm(formula = dist ~ speed, data = cars)

Coefficients:
(Intercept)        speed  
    -17.579        3.932  
typeof(x)
[1] "list"
class(x)
[1] "lm"
names(x)
 [1] "coefficients"  "residuals"     "effects"       "rank"         
 [5] "fitted.values" "assign"        "qr"            "df.residual"  
 [9] "xlevels"       "call"          "terms"         "model"        
x$residuals
         1          2          3          4          5          6          7 
  3.849460  11.849460  -5.947766  12.052234   2.119825  -7.812584  -3.744993 
         8          9         10         11         12         13         14 
  4.255007  12.255007  -8.677401   2.322599 -15.609810  -9.609810  -5.609810 
        15         16         17         18         19         20         21 
 -1.609810  -7.542219   0.457781   0.457781  12.457781 -11.474628  -1.474628 
        22         23         24         25         26         27         28 
 22.525372  42.525372 -21.407036 -15.407036  12.592964 -13.339445  -5.339445 
        29         30         31         32         33         34         35 
-17.271854  -9.271854   0.728146 -11.204263   2.795737  22.795737  30.795737 
        36         37         38         39         40         41         42 
-21.136672 -11.136672  10.863328 -29.069080 -13.069080  -9.069080  -5.069080 
        43         44         45         46         47         48         49 
  2.930920  -2.933898 -18.866307  -6.798715  15.201285  16.201285  43.201285 
        50 
  4.268876 
x$terms
dist ~ speed
attr(,"variables")
list(dist, speed)
attr(,"factors")
      speed
dist      0
speed     1
attr(,"term.labels")
[1] "speed"
attr(,"order")
[1] 1
attr(,"intercept")
[1] 1
attr(,"response")
[1] 1
attr(,".Environment")
<environment: R_GlobalEnv>
attr(,"predvars")
list(dist, speed)
attr(,"dataClasses")
     dist     speed 
"numeric" "numeric" 

lm() 함수의 결과의 타입은 list이고, 클래스는 함수의 이름과 동일한 lm임을 볼 수 있다. (R에서는 사용자의 기억을 돕기 위해서 일반적으로 함수의 결과가 복잡한 경우 함수의 이름과 동일한 클래스 이름으로 결과를 반환한다. 클래스에 대해서는 11 장을 참조한다.) lm 클래스의 객체이긴 하지만 타입이 리스트이므로 names() 함수를 이용하여 요소의 이름을 확인하고, $ 연산자를 이용하여 요소에 접근하여 요소의 내용을 확인할 수 있다.

unclass()

주의할 점은 리스트이긴 하지만 lm 클래스이므로 출력을 해 보면 리스트의 모든 요소가 출력되는 것이 아니라 lm 클래스에 맞추어 회귀분석의 주요 결과만 간략하게 출력됨을 볼 수 있다. 만약 리스트로서 모든 요소를 확인하고 싶으면 unclass() 함수를 이용하여 객체에 부여된 클래스 속성을 제거한다. 그러면 단순한 리스트 객체가 되어서 모든 요소가 출력된다. 출력이 매우 길므로 출력 결과는 생략하였다.

unclass(x)

데이터 프레임

리스트가 중요한 또 다른 이유는 R에서 데이터를 저장할 때 빈번하게 이용하는 데이터 프레임이라는 형식이 리스트를 기반으로 하고 있기 때문이다. 정확히 이야기하면, 데이터 프레임은 data.frame이라는 클래스 속성을 갖는 리스트이다.

head(cars)
  speed dist
1     4    2
2     4   10
3     7    4
4     7   22
5     8   16
6     9   10
typeof(cars)
[1] "list"
class(cars)
[1] "data.frame"

이번에도 cars 객체는 리스트로 데이터를 저장하지만, 클래스가 data.frame이므로 출력해 보면 일반적인 리스트의 형식으로 출력되지 않고 행렬 형태로 출력된다. unclass()를 이용하면 클래스가 없어져서 이 데이터가 리스트 형식으로 저장되어 있음을 확인할 수 있다.

unclass(cars)
$speed
 [1]  4  4  7  7  8  9 10 10 10 11 11 12 12 12 12 13 13 13 13 14 14 14 14 15 15
[26] 15 16 16 17 17 17 18 18 18 18 19 19 19 20 20 20 20 20 22 23 24 24 24 24 25

$dist
 [1]   2  10   4  22  16  10  18  26  34  17  28  14  20  24  28  26  34  34  46
[20]  26  36  60  80  20  26  54  32  40  32  40  50  42  56  76  84  36  46  68
[39]  32  48  52  56  64  66  54  70  92  93 120  85

attr(,"row.names")
 [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
[26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50

결과에서 speed와 dist로 이름이 붙여진 숫자 벡터로 구성된 리스트임을 확인할 수 있다. 그리고 row.names라는 속성이 부여되어 있음을 확인할 수 있다.

데이터 프레임은 보통 다른 통계 소프트웨어에서 데이터 집합 또는 데이터 행렬이라고 불리는 것이다. R에서는 본질적으로 데이터 프레임은 data.frame 클래스인 리스트인데, 요소의 길이가 모두 같은 리스트이다. 그러므로 데이터를 행렬 형태로 표현할 수 있다. 리스트의 각 요소가 각 열로 표현된다.
리스트를 데이터 프레임으로 변환시키기 위해서는 as.data.frame()을 이용하면 된다.

y <- list(a=11:15, b=letters[11:15])
y
$a
[1] 11 12 13 14 15

$b
[1] "k" "l" "m" "n" "o"
typeof(y); class(y)
[1] "list"
[1] "list"
z <- as.data.frame(y)
z
   a b
1 11 k
2 12 l
3 13 m
4 14 n
5 15 o
typeof(z); class(z)
[1] "list"
[1] "data.frame"