Chapter 4 R 행렬
벡터보다 조금 더 복잡한 데이터 구조가 행렬과 배열이다. 행렬과 배열은 벡터처럼 모든 요소가 동일한 데이터의 타입을 가져야 한다.
행렬과 배열은 다차원적 데이터 구조이다.
지금까지 배운 벡터는 일차원적인 데이터 구조였다. 벡터의 길이가 50이라면 벡터의 각 요소의 위치는 \(1, 2, \ldots, 50\)까지 하나의 숫자로 특정할 수 있다. 반면 행렬과 배열은 다차원적인 데이터 구조이다. 행렬은 2차원적 데이터 구조로 행과 열로 구성된다. 행렬의 각 요소의 위치는 어떤 행과 어떤 열에 포함되는지를 나타내는 두 개의 숫자로 특정할 수 있다. 배열은 행렬을 일반화한 것으로 다차원적인 데이터 구조이다. 예로 3차원 배열은 세 개의 숫자에 의해 데이터의 위치를 특정할 수 있다.
4.1 행렬 만들기
행렬의 필요성
행렬의 예로 다음을 고려해 보자. 어떤 강의의 수강생을 성별, 학년의 두 가지 기준으로 분류한다고 해 보자. 그러면 표 4.1 같은 형식으로 데이터를 정리할 수 있을 것이다. 이와 같이 두 범주형 변수에 대해 관측도수를 요약한 표를 교차표(cross table) 또는 분할표라고 한다.
1 학년 | 2 학년 | 3 학년 | 4 학년 | |
---|---|---|---|---|
남 | 0 | 5 | 7 | 5 |
여 | 2 | 4 | 8 | 2 |
표 4.1 같은 데이터는 일차원적인 벡터 형태로 데이터를 저장하면 각 데이터 요소가 어떤 의미를 갖는지 파악하기가 쉽지 않다. 이러한 경우에는 2차원으로 구성된 행렬을 이용하는 것이 좋다.
행렬을 만드는 방법은 (1) 벡터를 행렬로 변환시키는 방법과 (2) 벡터를 결합하여 행렬을 만드는 방법이 있다. 이 절에서는 벡터를 행렬로 변환시키는 방법부터 살펴보자.
matrix()
matrix() 함수를 이용하면 데이터 벡터를 데이터 행렬로 변환시킬 수 있다. 다음의 예를 보면서 설명해 보자.
[1] 1 2 3 4 5 6 7 8 9 10
[,1] [,2] [,3] [,4] [,5]
[1,] 1 3 5 7 9
[2,] 2 4 6 8 10
위 예에서 n은 1부터 10까지 숫자를 가진 벡터이다. matrix() 함수의 첫 인수로 행렬로 바꿀 벡터를 주고, nrow 인수에 새로 만들 행렬의 행 수를 주면 행렬이 만들어짐을 볼 수 있다. 이렇게 만들어진 행렬 m은 크기가 \(2 \times 5\) 인 행렬이 된다. 왜냐하면 전체 데이터의 수는 10개인데 2 개의 행이 지정되었으므로 모두 5개의 열이 있어야 모든 데이터를 수용할 수 있기 때문이다.
matrix()는 열을 순서대로 채워나간다.
벡터의 요소는 차례대로 1열의 1행을 채우고 2행을 채운다. 그리고 난 후 2열의 모든 행을 순서대로 채운다. 이러한 방식으로 차례차례 모든 열의 데이터를 채워 나감을 볼 수 있다. 만약 열이 아니라 행을 차례대로 채워나가면서 행렬을 만드고 싶으면 byrow=TRUE 인수를 사용하면 된다.
[,1] [,2] [,3] [,4] [,5]
[1,] 1 2 3 4 5
[2,] 6 7 8 9 10
행렬과 dim 속성
원래의 벡터 n과 벡터로 만들어진 행렬 m의 속성을 보기 위해 attributes() 함수를 이용하자. 벡터 n에는 아무 속성도 부여되지 않았지만 행렬 m은 차원을 의미하는 dim 속성이 부여되었음을 볼 수 있다. dim 속성에는 길이가 2인 숫자 벡터가 부여되어 있는데, 첫번째 요소는 행의 개수, 두번째 요소는 열의 개수를 나타낸다.
NULL
$dim
[1] 2 5
matrix()로 벡터를 행렬로 변환시킬 때 행의 개수 대신 열의 개수를 지정할 수도 있다. 이 경우 nrow 대신 ncol 인수를 사용하면 된다. 아래 예에서는 열의 개수를 2로 지정하였으므로 행렬 m은 크기가 \(5 \times 2\)인 행렬이 되었음을 볼 수 있다. 이는 dim 속성에서도 확인할 수 있다.
[,1] [,2]
[1,] 1 6
[2,] 2 7
[3,] 3 8
[4,] 4 9
[5,] 5 10
$dim
[1] 5 2
벡터 출력 vs. 행렬 출력
행렬 m을 출력한 내용을 보면 벡터와는 다름을 볼 수 있다. 먼저 벡터는 데이터를 R 콘솔의 모든 줄을 차례대로 채우면서 출력하고, 줄이 바뀔 때마다 각 줄의 첫 요소가 벡터의 몇 번째 데이터인지를 [i] 형태로 보여준다. 반면 행렬은 행과 열의 개수에 맞추어 출력시킬 뿐 아니라 [i,j] 형태로 데이터의 위치를 보여준다.
matrix()에서 벡터 재사용
matrix() 함수를 이용하여 행렬을 만들 때 행과 열의 크기를 모두 지정할 수 있다. 만약 벡터의 크기가 행렬의 요소 개수보다 적으면 벡터가 재사용된다. 반대로 벡터의 크기가 행렬의 요소 개수보다 크거나 배수가 되지 않으면 경고 메시지가 출력되고 행렬을 만드는 데는 앞에 있는 벡터 요소들만 사용된다.
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
[,1] [,2] [,3]
[1,] 1 1 1
[2,] 1 1 1
[3,] 1 1 1
[,1] [,2] [,3]
[1,] 1 1 1
[2,] 2 2 2
[3,] 3 3 3
Warning in matrix(11:30, nrow = 3, ncol = 3): data length [20] is not a
sub-multiple or multiple of the number of rows [3]
[,1] [,2] [,3]
[1,] 11 14 17
[2,] 12 15 18
[3,] 13 16 19
4.2 벡터를 결합하여 행렬 만들기
cbind()와 rbind()
기존의 벡터나 행렬을 열이나 행으로 묶어서 새로운 행렬을 만들 수 있다. 아래 예는 벡터를 cbind()와 rbind() 함수를 이용하여 열 또는 행으로 묶은 경우이다.
[,1] [,2]
[1,] 1 10
[2,] 2 9
[3,] 3 8
[4,] 4 7
[5,] 5 6
[,1] [,2] [,3] [,4] [,5]
[1,] 1 2 3 4 5
[2,] 10 9 8 7 6
[3,] 11 12 13 14 15
벡터+벡터, 벡터+행렬, 행렬+행렬 결합
벡터들만 연결하여 행렬을 만드는 것이 아니라, 행렬과 벡터 또는 행렬과 행렬을 행 또는 열로 묶어 새로운 행렬을 만들 수 있다.
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
[,1] [,2] [,3]
[1,] 10 11 12
[2,] 1 4 7
[3,] 2 5 8
[4,] 3 6 9
[,1] [,2] [,3] [,4] [,5] [,6]
[1,] 1 4 7 1 4 7
[2,] 2 5 8 2 5 8
[3,] 3 6 9 3 6 9
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
[4,] 10 11 12
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
[4,] 1 4 7
[5,] 2 5 8
[6,] 3 6 9
[7,] 10 11 12
Error in cbind(A, t): 각각의 행렬이 가진 행의 개수는 반드시 서로 일치해야 합니다 (인자 2를 참고하세요)
행렬+행렬 연결은 행 또는 열의 수가 같아야 한다.
마지막 예에서 볼 수 있듯이 행렬을 cbind 할 때는 행의 수가 같아야 하며, rbind를 할 때에는 열의 수가 일치하여야 한다. 하지만 벡터의 경우에는 가장 긴 길이에 맞추어 요소가 재사용된다.
[,1] [,2]
[1,] 1 7
[2,] 2 8
[3,] 3 9
[4,] 4 7
[5,] 5 8
[6,] 6 9
[,1] [,2] [,3]
[1,] 1 7 10
[2,] 2 8 11
[3,] 3 9 10
[4,] 4 7 11
[5,] 5 8 10
[6,] 6 9 11
[,1] [,2] [,3] [,4] [,5]
[1,] 1 1 1 1 1
[2,] 1 2 3 4 5
4.3 행렬의 필터링
행과 열 인덱스 벡터
벡터와 마찬가지로 행렬도 인덱스 벡터에 의해 필터링을 할 수 있다. 벡터는 오직 한 개의 인덱스 벡터를 가지는데, 행렬은 2차원인 만큼 각 차원을 지정할 수 있도록 2개의 인덱스 벡터를 가진다. 첫번째 인덱스 벡터는 추출할 행의 위치를, 두번째 인덱스 벡터는 추출할 열의 위치를 지정하게 된다. 벡터와 마찬가지로 행렬도 자연수, 음의 정수, 논리, 이름 인덱스 벡터를 가질 수 있다.
자연수 인덱스 벡터
아래는 1부터 25까지의 숫자를 크기 \(5 \times 5\)인 행렬로 만든 후 인덱스 벡터로 각 데이터 요소를 특정하는 예를 보여주고 있다.
[,1] [,2] [,3] [,4] [,5]
[1,] 1 6 11 16 21
[2,] 2 7 12 17 22
[3,] 3 8 13 18 23
[4,] 4 9 14 19 24
[5,] 5 10 15 20 25
[1] 1
[1] 25
[1] 1 2 3
[1] 2 7 12
[,1] [,2]
[1,] 8 18
[2,] 10 20
다음 예처럼 인덱스 벡터 위치가 비어 있으면 그 인덱스는 전체 범위를 취한다. 인덱스 벡터가 모두 비어 있으면 행렬 그 자체가 된다. 즉 z[ , ]은 z와 같다.
[1] 6 7 8 9 10
[1] 3 8 13 18 23
[,1] [,2]
[1,] 11 21
[2,] 12 22
[3,] 13 23
[4,] 14 24
[5,] 15 25
[,1] [,2] [,3] [,4] [,5]
[1,] 1 6 11 16 21
[2,] 2 7 12 17 22
[3,] 3 8 13 18 23
[4,] 4 9 14 19 24
[5,] 5 10 15 20 25
음의 정수 인덱스 벡터
벡터와 마찬가지로 음의 정수를 이용하면 해당 행과 열을 제외한 행렬을 구할 수 있다.
[,1] [,2] [,3] [,4]
[1,] 1 11 16 21
[2,] 2 12 17 22
[3,] 3 13 18 23
[4,] 4 14 19 24
[5,] 5 15 20 25
[,1] [,2] [,3] [,4] [,5]
[1,] 1 6 11 16 21
[2,] 2 7 12 17 22
[3,] 4 9 14 19 24
[4,] 5 10 15 20 25
[,1] [,2] [,3]
[1,] 1 11 21
[2,] 2 12 22
[3,] 3 13 23
[4,] 4 14 24
[5,] 5 15 25
[,1] [,2] [,3] [,4]
[1,] 1 11 16 21
[2,] 2 12 17 22
[3,] 4 14 19 24
[4,] 5 15 20 25
인덱스 벡터로 행과 열 재배치 하기
행렬의 인덱스 벡터를 이용하여 행 단위 재배치나 열 단위 재배치가 가능하다.
[,1] [,2] [,3] [,4] [,5]
[1,] 5 10 15 20 25
[2,] 4 9 14 19 24
[3,] 3 8 13 18 23
[4,] 2 7 12 17 22
[5,] 1 6 11 16 21
[,1] [,2] [,3] [,4] [,5]
[1,] 11 16 21 1 6
[2,] 12 17 22 2 7
[3,] 13 18 23 3 8
[4,] 14 19 24 4 9
[5,] 15 20 25 5 10
아울러 인덱스 벡터를 사용하여 행과 열에 대한 정렬도 가능하다. 다음은 행렬의 첫번째 열을 기준으로 행을 큰 순에서 작은 순으로 정렬한 예이다.
[,1] [,2] [,3] [,4]
[1,] 5 8 2 12
[2,] 15 1 3 4
[3,] 6 11 10 13
[4,] 14 9 7 16
[1] 5 15 6 14
[1] 1 3 4 2
[,1] [,2] [,3] [,4]
[1,] 5 8 2 12
[2,] 6 11 10 13
[3,] 14 9 7 16
[4,] 15 1 3 4
논리 인덱스 벡터
벡터와 마찬가지로 논리 인덱스 벡터를 사용할 수 있다. 다음의 예에서 보듯이 행과 열의 인덱스 벡터에서 TRUE가 되는 행과 열만 선택된다.
height <- c(180, 172, 167, 175, 182)
weight <- c(72, 78, 58, 64, 68)
year <- c(1, 3, 2, 4, 2)
A <- cbind(height, weight, year)
A[ , c(F, T, T)]
weight year
[1,] 72 1
[2,] 78 3
[3,] 58 2
[4,] 64 4
[5,] 68 2
height year
[1,] 180 1
[2,] 172 3
[3,] 167 2
[4,] 175 4
[5,] 182 2
height weight year
182 68 2
height weight year
[1,] 167 58 2
[2,] 175 64 4
[3,] 182 68 2
행과 열 이름 부여하기: rownames()와 colnames()
벡터와 마찬가지로 행과 열의 이름을 인덱스로 하여 행렬의 부분을 참조할 수 있다. 행렬의 행과 열 이름의 확인과 할당은 rownames()와 colnames() 함수를 이용하여 이루어진다. rbind()나 cbind()에 의해 벡터 변수가 결합되면 변수의 이름이 자동적으로 행이나 열의 이름으로 부여된다.
[1] "height" "weight" "year"
NULL
height weight year
student1 180 72 1
student2 172 78 3
student3 167 58 2
student4 175 64 4
student5 182 68 2
student1 student2 student3 student4 student5
1 3 2 4 2
height weight year
180 72 1
[1] 78
[1] 78
4.4 행렬의 연산
행렬의 요소 단위 연산
행과 열의 수가 같은 행렬은 산술 연산을 할 수 있다. 이 경우 벡터와 마찬가지로 산술 연산은 같은 위치의 요소 단위(element-by-element)로 이루어진다.
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
[,1] [,2] [,3]
[1,] 10 40 70
[2,] 20 50 80
[3,] 30 60 90
[,1] [,2] [,3]
[1,] 11 44 77
[2,] 22 55 88
[3,] 33 66 99
[,1] [,2] [,3]
[1,] 9 36 63
[2,] 18 45 72
[3,] 27 54 81
[,1] [,2] [,3]
[1,] 10 160 490
[2,] 40 250 640
[3,] 90 360 810
[,1] [,2] [,3]
[1,] 10 10 10
[2,] 10 10 10
[3,] 10 10 10
행렬은 재사용되지 않는다
행과 열의 개수가 다른 행렬의 산술연산은 오류를 발생시킨다. 그 이유는 행렬은 벡터와 달리 요소의 재사용(recycling)이 일어나지 않기 때문이다. 그렇기 때문에 행렬과 행렬의 연산에서는 행과 열이 같은 크기이어야 한다.
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
Error in B + C: 배열의 크기가 올바르지 않습니다
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
[4,] 1 2 3
Error in B * C: 배열의 크기가 올바르지 않습니다
벡터와 행렬의 연산: 벡터는 재사용된다
벡터와 행렬의 산술연산도 같은 위치의 요소 단위로 이루어진다. 벡터 a와 행렬 A의 연산시 벡터 a는 먼저 행렬 A와 같은 차원의 행렬로 변환되어 연산이 수행된다. 만약 벡터의 길이가 행렬의 길이보다 작으면 벡터의 데이터는 행렬의 길이만큼 순환 재사용된다. 연산을 위해 벡터가 행렬로 변환될 때도 matrix()로 행렬이 만들어질 때와 마찬가지로 열 별로 데이터를 채워나간다.
[,1] [,2] [,3]
[1,] 10 10 10
[2,] 10 10 10
[3,] 10 10 10
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 12 15 18
[3,] 103 106 109
[,1] [,2] [,3]
[1,] 0 0 0
[2,] 20 50 80
[3,] 300 600 900
숫자와 행렬이 연산은 길이가 1인 벡터와 행렬이 연산되는 것이므로 모든 행렬의 요소와 숫자간에 연산이 이루어진다. 다음 예에서 숫자 10이 A의 모든 요소에 대응하여 계산되었음을 볼 수 있다.
[,1] [,2] [,3]
[1,] 10 40 70
[2,] 20 50 80
[3,] 30 60 90
그러나 벡터의 길이가 배열의 길이보다 길면 오류가 발생한다.
Warning in b * A: 두 객체의 길이가 서로 배수관계에 있지 않습니다
Error in eval(expr, envir, enclos): dims [product 9] do not match the length of object [10]
현재까지는 행렬의 연산 중 요소 단위의 연산만을 설명하였다. R은 이 외에도 행렬의 외적, 행렬의 곱, 역행렬 구하기 등 다양한 행렬 연산을 지원한다. 관심 있는 독자는 4.7 절을 참조한다.
4.5 행렬과 함수
4.5.1 행렬을 인수로 하는 함수들
이 절에서는 행렬을 인수로 하는 R 함수를 살펴본다.
t()
어떤 행렬에서 행과 열이 바뀐 행렬을 전치행렬(transposed matrix)이라 한다. t() 함수를 이용하면 전치행렬을 구할 수 있다.
[,1] [,2]
[1,] 1 3
[2,] 2 4
[,1] [,2]
[1,] 1 2
[2,] 3 4
행렬의 차원 정보를 주는 함수: nrow(), ncol(), dim()
행렬의 행 수와 열 수를 구할 때는 nrow(), ncol() 함수를 이용한다. 행과 열의 차원 전체를 얻고자 하면 dim() 함수를 이용한다.
[1] 2
[1] 2
[1] 2 2
$dim
[1] 2 2
행과 열에 이름 함수: rownames(), colnames()
행과 열 이름을 확인하거나, 새로 할당할 때는 rownames()와 colnames() 함수를 이용한다.
NULL
NULL
left right
upper 1 3
lower 2 4
$dim
[1] 2 2
$dimnames
$dimnames[[1]]
[1] "upper" "lower"
$dimnames[[2]]
[1] "left" "right"
벡터로 변환: as.vector(), c()
as.vector()나 c() 함수를 이용하면 행렬을 벡터로 변경할 수 있다. 배열을 벡터로 바꾸는 공식적인 방법은 as.vector() 함수를 이용하는 것이다. 다음 예를 살펴보자. (R이 대소문자를 구분하는 것에 주의.)
[,1] [,2] [,3] [,4] [,5]
[1,] 1 2 3 4 5
[2,] 5 4 3 2 1
[1] 1 5 2 4 3 3 4 2 5 1
NULL
하지만 c() 함수도 이와 유사한 효과를 나타낸다. cbind()와 rbind()는 행렬과 벡터의 차원 속성을 유지하며 데이터를 연결한다. 그러나 c() 함수는 dim과 dimnames 속성을 지운다.
[1] 1 5 2 4 3 3 4 2 5 1
NULL
4.5.2 행렬의 행별 또는 열별로 함수를 적용하는 방법
apply() 함수를 이용하면 행렬의 행별 또는 열별로 함수를 적용할 수 있다.
apply()
apply() 함수의 X 인수에는 행별 또는 열별로 함수를 적용할 행렬을, MARGIN 인수에는 함수를 적용할 방향을 지정한다. MARGIN 인수가 1이면 행별로, 2이면 열별로 함수가 적용된다. apply() 함수의 FUN 인수에는 행이나 열별로 적용할 함수를 전달한다.
다음은 행렬 A에 대해 행별 합과 열별 합을 구한 예이다. 행렬 전체를 sum()한 결과와의 차이를 비교해 보라.
[1] 15 19 14 3 10 18 11 5 23 6 9 21 24 20 22 25 17 1 12 13 2 16 7 8 4
[,1] [,2] [,3] [,4] [,5]
[1,] 15 18 9 25 2
[2,] 19 11 21 17 16
[3,] 14 5 24 1 7
[4,] 3 23 20 12 8
[5,] 10 6 22 13 4
[1] 69 84 51 66 55
[1] 61 63 96 68 37
[1] 325
apply() 함수가 적용되는 행렬의 행과 열에 이름이 있으면 apply()의 결과 벡터에도 해당 이름이 적용되어 출력된다.
A B C D E
X1 15 18 9 25 2
X2 19 11 21 17 16
X3 14 5 24 1 7
X4 3 23 20 12 8
X5 10 6 22 13 4
X1 X2 X3 X4 X5
2 11 1 3 4
A B C D E
19 23 24 25 16
함수의 결과가 벡터이면 열로서 결합하여 출력한다.
지금까지 apply()의 FUN 인수에 부여된 함수는 sum()처럼 오직 하나의 값만을 반환하는 함수였다. 그렇기 때문에 apply() 함수로 행 또는 열별 합을 구하면 행 또는 열의 수만큼의 길이를 가지는 벡터로서 결과값이 반환되었다. 그러면 range()나 summary()처럼 길이가 2 이상인 벡터로서 결과를 주는 함수를 FUN 인수에 적용하면 어떻게 될까? 이 경우에는 함수가 적용되어 나온 결과 벡터를 cbind()처럼 열로서 결합하여 최종 결과를 출력한다.
A B C D E
[1,] 3 5 9 1 2
[2,] 19 23 24 25 16
X1 X2 X3 X4 X5
[1,] 2 11 1 3 4
[2,] 25 21 24 23 22
X1 X2 X3 X4 X5
Min. 2.0 11.0 1.0 3.0 4
1st Qu. 9.0 16.0 5.0 8.0 6
Median 15.0 17.0 7.0 12.0 10
Mean 13.8 16.8 10.2 13.2 11
3rd Qu. 18.0 19.0 14.0 20.0 13
Max. 25.0 21.0 24.0 23.0 22
range() 함수는 최소값과 최대값이라는 두 요소로 이루어진 벡터를 결과로서 반환하는 함수이다. 따라서 첫번째 예는 A의 열별 최소값과 최대값을 열로 하는 행렬을, 두번째 예는 A의 행별 최대값과 최소값을 열로 하는 행렬을 반환하였다. 마지막 예에서는, summary() 함수가 최소값, 사분위수, 최대값, 평균 등의 통계치를 벡터로 반환하기 때문에, apply() 결과는 A의 행별 통계치를 열로 하는 행렬로서 반환되었다.
4.6 배열 *
4.6.1 배열의 생성
벡터는 일차원 데이터 구조로 하나의 인덱스 벡터로 요소의 위치를 지정한다.
[1] 11 12 13 14 15 16 17 18 19 20
[1] 11
[1] 16 17
행렬은 2차원의 데이터 구조로 두 개의 인덱스 벡터로 요소의 위치를 지정한다. 첫번째 인덱스 벡터가 행의 위치를 지정하고, 두번째 인덱스 벡터가 열 위치를 지정한다.
[,1] [,2] [,3]
[1,] 11 14 17
[2,] 12 15 18
[3,] 13 16 19
[1] 14
[1] 12 13
배열은 행렬을 일반화한 것으로 2차원 이상의 차원을 가지는 데이터 구조이다. 사실 행렬은 2차인 구조를 가지는 배열의 특수한 예이다. 배열은 벡터, 행렬과 마찬가지로 동일한 형식의 데이터만 그 요소로 가질 수 있으므로, 따라서 숫자 배열, 문자 배열, 논리값 배열 등이 만들어질 수 있다.
dim 인수
matrix() 함수와 마찬가지로 데이터 요소를 가지고 있는 벡터가 array()의 첫번째 인수로 제공된다. matrix() 함수와 다른 점은 차원의 크기가 dim 인수에 벡터 형식으로 제공된다는 것이다. 행렬은 2차원으로 차원이 정해져 있으므로 첫번째 차원인 행의 크기는 nrow 인수에 두번째 차원인 열의 크기는 ncol 인수에 숫자 하나로 제공된다. 그러나 배열은 2차원 이상의 다양한 차원을 가질 수 있으므로, 원하는 차원의 수만큼 각 차원의 크기를 결정해야 한다. 따라서 차원의 개수만큼의 요소를 가지는 벡터가 dim 인수에 제공된다. 따라서 어떤 배열의 차원 벡터의 길이가 k이면 그 배열은 k차원 배열이라고 한다. 다음은 3차원 배열을 생성한 예이다.
, , 1
[,1] [,2] [,3]
[1,] 11 14 17
[2,] 12 15 18
[3,] 13 16 19
, , 2
[,1] [,2] [,3]
[1,] 20 23 26
[2,] 21 24 27
[3,] 22 25 28
3차원 배열의 출력 방식
행렬과 마찬가지로 3차원 배열의 첫번째 차원은 행(rows)을, 두번째 차원은 열(columns)을, 세번째 차원은 층(layers)을 나타낸다. 3차원 큐브를 생각하면 이해하기 쉽다. 앞의 예에서는 행은 3개, 열도 3개, 층은 2개로 데이터 요소가 배열되었다. 3차원 배열의 경우 컴퓨터 화면은 2차원이기 때문이 각 층을 차례로 보여주는 방식으로 배열의 요소를 출력한다. 따라서 각 층의 데이터는 2차원의 행렬의 형태로 출력된다. 출력된 행렬이 어떤 층의 데이터인지 보여주기 위해서 ’ , , (층 번호)’가 각 행렬의 위에 같이 출력되었다.
3차원 배열에서 데이터가 채워지는 순서
예에서 보듯이 행렬과 마찬가지로 앞 차원의 인덱스들이 먼저 변하며 데이터가 채워진다. 1층의 데이터가 다 채워진 후 2 층의 데이터가 채워지고, 동일 층에서는 앞의 열이 먼저 채워지고 뒤의 열이 다음에 채워진다. 동일 층의 동일 열에서는 앞의 행이 먼저 채워지고 뒤의 행이 다음에 채워진다. 즉, [행, 열, 층]의 인덱스가 [1, 1, 1]에서 시작하여 [2, 1, 1]로 진행하며 1층-1열의 모든 행을 다 채우고 나서, 다시 [1, 2, 1]부터 1층-2열의 데이터를 채운다. 그리고 1층의 데이터가 모두 채워지면 다시 2층의 1열의 1행, 즉 [1, 1, 2]부터 차례대로 1층과 같은 방식으로 데이터를 채워나간다. 비유적으로 설명하자면 숫자의 자리수가 일의 자리가 채워지면 십의 자리, 그리고 그 다음 백의 자리가 채워지듯이, 첫번째 차원의 인덱스가 모두 채워지면 두번째 차원의 인덱스가 하나 증가하고, 두번째 차원의 인덱스가 모두 채워지면 세번째 차원이 인덱스가 하나 증가하며 데이터가 채워진다고 할 수 있다. 다만 십진법에서는 숫자의 자리수는 모두 10으로 크기가 같고, 0부터 시작하며, 숫자 표기상 뒷쪽 자릿수가 먼저 채워지는데, R의 배열에서는 차원의 크기가 서로 다를 수 있고, 1부터 시작하고, 앞 쪽의 차원이 먼저 채워지는 것에 차이가 있다.
k차원 배열은 k개의 인덱스 벡터로 요소의 위치를 지정한다.
3차원 배열에서 요소의 위치를 지정하기 위해서는 3개의 인덱스 벡터가 필요하다. 마찬가지로 k-차원의 배열에서 요소의 위치를 지정하기 위해서는 k개의 인덱스 벡터가 필요하다.
[1] 12
[1] 21
[1] 23 26
행렬은 2차원 배열이므로 array() 함수를 이용해서도 행렬을 만들 수 있다.
[,1] [,2] [,3]
[1,] 11 14 17
[2,] 12 15 18
[3,] 13 16 19
벡터를 재사용하여 배열 만들기
행렬을 만들 때와 마찬가지로 데이터로 제공된 벡터가 배열의 모든 공간을 채울 수 없으면 벡터가 재사용된다.
, , 1
[,1] [,2] [,3]
[1,] 1 1 1
[2,] 1 1 1
, , 2
[,1] [,2] [,3]
[1,] 1 1 1
[2,] 1 1 1
, , 1
[,1] [,2] [,3]
[1,] 1 4 1
[2,] 2 5 2
[3,] 3 6 3
, , 2
[,1] [,2] [,3]
[1,] 4 1 4
[2,] 5 2 5
[3,] 6 3 6
참고로 벡터를 재사용하여 배열을 채울 때, 벡터의 길이가 배열의 길이의 배수가 아니어도 경고 메시지가 나오지 않는다. 이는 행렬의 경우와 다른데 차원이 커지면 벡터의 길이와 배열의 길이를 배수로 맞추는 것이 쉽지 않기 때문에 생략한 것으로 보인다.
, , 1
[,1] [,2] [,3]
[1,] 1 4 2
[2,] 2 5 3
[3,] 3 1 4
, , 2
[,1] [,2] [,3]
[1,] 5 3 1
[2,] 1 4 2
[3,] 2 5 3
마찬가지로 벡터의 길이가 배열의 길이보다 길어도 아무 경고 메시지 없이 배열을 잘 생성한다.
, , 1
[,1] [,2] [,3]
[1,] 1 4 7
[2,] 2 5 8
[3,] 3 6 9
, , 2
[,1] [,2] [,3]
[1,] 10 13 16
[2,] 11 14 17
[3,] 12 15 18
4차원 배열 예
마지막으로 4차원 배열을 만들어 보자. 컴퓨터 화면은 2차원이므로 4차원 배열을 표현하기 위해, 세번째와 네번째 차원의 인덱스가 정해져 있을 때의 데이터를 2차원 행렬로 출력해 주는 것을 볼 수 있다. 3차원 배열 때와 마찬가지로 앞의 차원의 인덱스가 먼저 변하면서 데이터가 차례로 배정됨을 볼 수 있다.
, , 1, 1
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
, , 2, 1
[,1] [,2] [,3]
[1,] 7 9 11
[2,] 8 10 12
, , 1, 2
[,1] [,2] [,3]
[1,] 13 15 17
[2,] 14 16 18
, , 2, 2
[,1] [,2] [,3]
[1,] 19 21 23
[2,] 20 22 24
4.6.2 배열도 결국 벡터
배열은 dim 속성을 가진 벡터이다.
행렬과 마찬가지로 배열도 사실 벡터이다. 그러므로 typeof() 함수를 이용하여 생성된 배열의 타입을 확인해 보면 숫자 벡터임을 보여준다. 그러나 class() 함수로 배열의 클래스를 확인하면 array 클래스로 처리됨을 볼 수 있다. 앞의 예에서도 벡터가 array 클래스로 처리되기 때문에 행, 열, 층 등으로 잘 구분되어 출력될 수 있었던 것이다. Array 클래스의 데이터는 본질적으로 벡터가 dim 속성을 가지고 있는 것 뿐이다. Dim 속성은 해당 배열의 각 차원의 크기를 나타내 준다. R은 dim 속성에 저장된 차원 정보를 이용하여 데이터 요소를 각 차원에 맞게 출력해 준다.
[1] "integer"
[1] "array"
$dim
[1] 3 3 2
벡터에 dim 속성을 부여하여 배열 만들기
배열은 결국 벡터가 dim 속성을 가지고 있는 것이므로 벡터에 dim 속성을 부여하여 배열을 만들 수도 있다. 다음 예에서도 보듯이 데이터는 변함이 없고 dim 속성에 따라 데이터가 보여지는 형식이 달라짐을 볼 수 있다.
NULL
[1] 1 2 3 4 5 6 7 8 9 10 11 12
[,1] [,2] [,3] [,4]
[1,] 1 4 7 10
[2,] 2 5 8 11
[3,] 3 6 9 12
, , 1
[,1] [,2] [,3]
[1,] 1 3 5
[2,] 2 4 6
, , 2
[,1] [,2] [,3]
[1,] 7 9 11
[2,] 8 10 12
[1] 1 2 3 4 5 6 7 8 9 10 11 12
dimnames 속성과 dimnames() 함수
배열도 행렬과 마찬가지로 각 차원에 이름을 부여할 수 있다. 배열의 dimnames 속성에 차원의 이름 벡터를 리스트 객체로 만들어 부여하면 된다. (리스트 객체에 대한 설명은 5장을 살펴보기 바란다.) 배열에 dimnames 속성을 부여하려면 array() 함수의 dimnames 인수를 이용하거나, 다음처럼 dimnames() 함수를 사용한다.
dim(x) <- c(2, 3, 2)
dimnames(x) <- list(c("A", "B"),
paste("group",1:3,sep="-"),
c("Male", "Female"))
x
, , Male
group-1 group-2 group-3
A 1 3 5
B 2 4 6
, , Female
group-1 group-2 group-3
A 7 9 11
B 8 10 12
$dim
[1] 2 3 2
$dimnames
$dimnames[[1]]
[1] "A" "B"
$dimnames[[2]]
[1] "group-1" "group-2" "group-3"
$dimnames[[3]]
[1] "Male" "Female"
4.6.3 배열의 연산과 필터링
배열의 요소 단위 연산
배열 연산과 필터링은 행렬 연산과 필터링의 경우와 동일한 원리로 작동한다. 다른 점은 행렬보다 더 많은 차원의 데이터를 다룬다는 것이다. 행렬 연산과 마찬가지로 배열 연산도 동일한 차원 벡터를 갖는 배열들이 요소 단위로 연산이 이루어진다.
, , 1
[,1] [,2]
[1,] 1 3
[2,] 2 4
, , 2
[,1] [,2]
[1,] 5 7
[2,] 6 8
, , 1
[,1] [,2]
[1,] 1 100
[2,] 10 1000
, , 2
[,1] [,2]
[1,] 1e+04 1e+06
[2,] 1e+05 1e+07
, , 1
[,1] [,2]
[1,] 2 103
[2,] 12 1004
, , 2
[,1] [,2]
[1,] 10005 1000007
[2,] 100006 10000008
, , 1
[,1] [,2]
[1,] 1 300
[2,] 20 4000
, , 2
[,1] [,2]
[1,] 5e+04 7e+06
[2,] 6e+05 8e+07
행렬 연산과 마찬가지로 차원 벡터가 다른 배열과 연산을 시도하면 오류가 발생한다.
, , 1
[,1] [,2] [,3]
[1,] 11 13 15
[2,] 12 14 16
, , 2
[,1] [,2] [,3]
[1,] 17 19 21
[2,] 18 20 22
Error in a + d: 배열의 크기가 올바르지 않습니다
벡터와 배열이 연산되면 벡터는 배열의 길이만큼 재사용되어 연산이 수행된다.
, , 1
[,1] [,2]
[1,] 2 6
[2,] 4 8
, , 2
[,1] [,2]
[1,] 10 14
[2,] 12 16
, , 1
[,1] [,2]
[1,] 1 3
[2,] 4 8
, , 2
[,1] [,2]
[1,] 5 7
[2,] 12 16
차원 축소와 drop 인수
배열도 행렬과 마찬가지로 차원의 수만큼의 자연수, 음의 정수, 논리, 이름 인덱스 벡터로 필터링을 수행할 수 있다. 필터링의 결과로 어떤 차원의 크기가 1로 줄어들게 되면, 더 낮은 차원의 데이터 구조로 차원이 축소된다. 예를 들어 3차원 배열을 필터링한 결과가 1차원 벡터나 2차원 행렬이 될 수 있다. 이러한 차원 축소를 원하지 않으면 drop=FALSE 인수를 사용하여 필터링을 하면 된다.3
, , Male
group-1 group-2 group-3
A 1 3 5
B 2 4 6
, , Female
group-1 group-2 group-3
A 7 9 11
B 8 10 12
group-1 group-2 group-3
A 7 9 11
B 8 10 12
A B
9 10
group-2 group-3
9 11
Male Female
group-2 3 9
group-3 5 11
, , Male
group-2 group-3
A 3 5
, , Female
group-2 group-3
A 9 11
group-1 group-3
A 1 5
B 2 6
[1] 9
4.7 행렬과 배열의 고급 연산 *
행렬은 배열의 매우 중요하고 특수한 경우이다. 따라서 R에서는 행렬에만 가능한 여러 연산과 기능들이 있다.
4.7.1 행렬의 곱
지금까지 행렬의 연산은 요소 단위로 이루어졌다. 따라서 행렬에 곱하기 연산자 *
를 사용하면 같은 위치의 요소끼리 곱해졌다. 그러나 수학에서 일반적으로 정의하는 행렬의 곱은 다음과 같은 형식으로 수행된다.
\[\begin{equation*} \begin{pmatrix} a & b \\ c & d \end{pmatrix} \times \begin{pmatrix} e & f \\ g & h \end{pmatrix} = \begin{pmatrix}b\,g+a\,e & b\,h+a\,f \\ d\,g+c\,e & d\,h+c\,f \end{pmatrix} \end{equation*}\]
연산자 %*%
는 행렬의 곱 연산을 수행한다. 다음의 예는 요소별로 곱이 이루어지는 *
연산자와 행렬의 곱 연산자 %*%
이 어떻게 다른지 보여준다. 결과에서 알 수 있듯이 행렬 곱은 교환법칙이 성립하지 않는다. 반면 요소 단위의 곱은 교환법칙이 성립한다.
[,1] [,2]
[1,] 1 3
[2,] 2 4
[,1] [,2]
[1,] 1 100
[2,] 10 1000
[,1] [,2]
[1,] 1 300
[2,] 20 4000
[,1] [,2]
[1,] 1 300
[2,] 20 4000
[,1] [,2]
[1,] 31 3100
[2,] 42 4200
[,1] [,2]
[1,] 201 403
[2,] 2010 4030
벡터가 행렬의 곱 수식에 나타나면 자동적으로 열 벡터나 행 벡터로 변환이 이루어진다. 그리고 행렬의 곱의 결과과 행이나 열 벡터로 표현되더라도 차원 축소는 일어나지 않는다.
[,1] [,2]
[1,] 3 7
[,1]
[1,] 4
[2,] 6
[,1]
[1,] 10
4.7.2 행렬의 주대각선 요소와 대각행렬
행렬을 이용한 수학적 연산에서 행렬의 주대각선 요소나 대각행렬은 매우 중요한 역할을 수행한다. R은 diag() 함수를 이용하여 행렬에서 주대각선 요소를 추출하거나, 대각행렬을 만든다. diag() 함수는 첫번째 인수 x에 어떤 형식의 데이타가 오는지에 따라 다른 연산을 수행한다.
- 인수 x가 행렬이면, 그 행렬의 주대각선 요소를 벡터로 반환한다.
- 인수 x가 벡터이고 벡터의 길이가 1보다 크면, 그 벡터를 주대각선 요소로 하는 대각행렬(diagonal matrix)를 반환한다. 이 경우 nrow나 ncol이 주어지면 벡터가 재사용되어 대각행렬을 만들 수 있다.
- 인수 x가 숫자 하나이고 그 값이 k이면, \(k \times k\) 단위 행렬(identity matrix)을 반환한다. 만약 nrow나 ncol 등이 주어지면 k가 주대각선 요소인 대각행렬이 생성된다.
[1] 1 4
[,1] [,2] [,3] [,4]
[1,] 1 0 0 0
[2,] 0 2 0 0
[3,] 0 0 3 0
[4,] 0 0 0 4
[,1] [,2] [,3]
[1,] 1 0 0
[2,] 0 1 0
[3,] 0 0 1
[,1] [,2]
[1,] 3 0
[2,] 0 3
4.7.3 연립 일차 방정식과 역행렬
다음과 같은 연립 일차 방정식이 있다고 하자.
\[\begin{align*} 2 x - 3 y &= 5 \\ -2 x + 4 y &= -4 \end{align*}\]
위의 연립 일차 방정식은 행렬과 벡터를 이용하여 나타내면 다음처럼 표현된다.
\[\begin{equation*} \mathbf{A} \mathbf{x} = \mathbf{b} \end{equation*}\]
단,
\[\begin{equation*} \mathbf{A} = \begin{pmatrix} 2 & -3 \\ -2 & 4 \end{pmatrix}, \, \mathbf{x} = \begin{pmatrix} x \\ y \end{pmatrix}, \, \mathbf{b} = \begin{pmatrix} 5 \\ -4 \end{pmatrix}. \end{equation*}\]
위의 식의 해를 구하기 위해서는 앙변에 \(\mathbf{A}\)의 역행렬 \(\mathbf{A}^{-1}\)을 곱하면 된다.
\[\begin{equation*} \mathbf{x} = \mathbf{A}^{-1} \mathbf{b} \end{equation*}\]
R에서는 solve() 함수를 이용하여 해를 구한다. solve(a, b) 함수의 첫번째 인수 a에는 연립 일차 방정식의 좌변의 계수를 나타내는 정방행렬이, 두번째 인수 b에는 우변의 상수항을 나타내는 벡터가 제공된다.
[,1] [,2]
[1,] 2 -3
[2,] -2 4
[1] 5 -4
[1] 4 1
[,1]
[1,] 5
[2,] -4
위의 문제의 답은 \(x = 4\)이고 \(y = 1\)임을 알 수 있다. 행렬 A와 해 x를 곱하면 우변항의 b와 같아짐을 확인할 수 있다.
solve() 함수의 두번째 인수 b가 주어지지 않으면 b를 단위 행렬로 하여 해를 구한다. 그러면 해는 좌변 행렬의 역행렬이 된다.
\[\begin{align*} & \mathbf{A} \mathbf{x} = \mathbf{I} \\ & \mathbf{x} = \mathbf{A}^{-1} \end{align*}\]
단, \(\mathbf{I}\)는 \(\mathbf{A}\)와 차원이 같은 단위 행렬이다. 따라서 다음은 행렬 \(\mathbf{A}\)의 역행렬 \(\mathbf{A}^{-1}\)을 구해준다. 결과에서 solve()의 해를 원래의 행렬로 곱하면 단위 행렬이 됨을 확인할 수 있다.
[,1] [,2]
[1,] 2 1.5
[2,] 1 1.0
[,1] [,2]
[1,] 1 0
[2,] 0 1
\(\mathbf{A}^{-1}\)을 구하였으므로 \(\mathbf{x} = \mathbf{A}^{-1} \mathbf{b}\) 관계를 이용하여 앞의 방정식의 해를 구할 수도 있다.
[,1]
[1,] 4
[2,] 1
하지만 이러한 방법은 수치 계산적인 측면에서 비효율적이고 불안정한 결과를 만들 수 있다. 따라서 \(\mathbf{A}\)의 역행렬을 직접 구하기보다는 solve(A, b)를 이용하여 해를 구하는 것이 좋다. \(\mathbf{x}^{t} \mathbf{A}^{-1} \mathbf{x}\) 처럼 이차형식(quadratic form)을 구할 때도 역행렬을 직접 구하기보다는 x %*% solve(A, x)
로 계산하는 것이 더 좋다.
4.7.4 고유치(eigenvalues)와 고유벡터(eigenvectors)
행렬의 고유치와 고유벡터를 구하기 위해서는 eigen() 함수를 이용한다. eigen() 함수의 결과는 values와 vectors라는 이름을 가진 두 요소를 가지는 리스트이다. (리스트에 대해서는 5 장을 참조하라.) 고유치 또는 고유벡터를 확인하려면 values 또는 vectors로 리스트 요소를 지정하면 된다.
[,1] [,2]
[1,] 2 -3
[2,] -2 4
[1] 5.6457513 0.3542487
[,1] [,2]
[1,] 0.6354064 -0.8767397
[2,] -0.7721779 -0.4809652
매우 큰 행렬의 경우 고유벡터가 필요하지 않다면 계산하지 않는 것이 효율적이다. eigen() 함수의 only.values 옵션을 TRUE로 설정하면 고유벡터는 계산하지 않고 고유치만 계산한다.
$values
[1] 5.6457513 0.3542487
$vectors
NULL
C나 Java 등의 언어에 익숙한 독자들은 배열의 인덱스에 drop이라는 인수를 사용할 수 있다는 것이 의아할 수 있을 것이다. R은 함수적 프로그래밍 언어(functional programming language)로 배열의 필터링마저도 사실 함수의 형태로 구현된다. 필터링을 지원하는 함수는
[
인데 이 함수는 drop 인수를 지원한다.↩︎