5. 배열과 행렬

5. 배열과 행렬 (Arrays and matrices)

R에서 데이터를 다룰 때 벡터만으로는 표현하기 힘든 경우가 많아요. 예를 들어 표 형태의 데이터나 여러 차원으로 묶인 데이터를 다룰 땐 "배열(array)"과 그 특수한 형태인 "행렬(matrix)"이 훨씬 편리합니다. 이번 장에서는 배열과 행렬을 만들고, 원하는 부분만 뽑아내고, 수학 연산을 수행하는 방법까지 차근차근 살펴볼게요.

출처: R 공식 매뉴얼

본문

5.1 배열 (Arrays)

배열은 "여러 개의 첨자(subscript)"로 접근하는 데이터 묶음이라고 생각하면 돼요. 예를 들어 숫자 데이터를 여러 차원으로 늘어놓은 형태죠. R은 배열을 만들고 다루는 간단한 기능을 제공하고, 특히 배열의 특수한 경우인 행렬을 다루는 기능도 기본으로 갖추고 있어요.

여기서 "차원 벡터(dimension vector)"란 음수가 아닌 정수들의 벡터를 말해요. 차원 벡터의 길이가 k라면 그 배열은 k차원 배열이 되죠. 예를 들어 행렬은 2차원 배열입니다. 각 차원의 첨자는 1부터 시작해서 차원 벡터가 가리키는 값까지 커져요.

어떤 벡터가 R에서 배열로 취급되려면, 그 벡터에 dim 속성(attribute)으로 차원 벡터가 붙어 있어야 해요. 예를 들어 z가 1500개 원소를 가진 벡터라고 해볼게요.

> dim(z) <- c(3,5,100)

이렇게 대입하면 zdim 속성이 생기면서 3 × 5 × 100 배열로 취급할 수 있게 돼요.

물론 matrix()array() 같은 함수를 쓰면 훨씬 간단하고 자연스럽게 배열을 만들 수 있어요. 이 내용은 나중에 The array() function에서 자세히 다룰게요.

한 가지 짚고 넘어갈 점은, 데이터 벡터의 값이 배열에 채워지는 순서예요. FORTRAN에서 쓰던 방식과 같은 "열 우선 순서(column major order)"를 따르는데, 쉽게 말하면 첫 번째 첨자가 가장 빨리 변하고 마지막 첨자가 가장 느리게 변하는 순서로 값이 채워져요.

예를 들어 배열 a의 차원 벡터가 c(3,4,2)라면 a에는 3 × 4 × 2 = 24개의 항목이 있고, 데이터 벡터는 다음과 같은 순서로 값들을 담고 있어요.

a[1,1,1], a[2,1,1], …, a[2,4,2], a[3,4,2]

배열은 1차원일 수도 있어요. 이런 배열은 보통 (출력할 때를 포함해) 벡터처럼 취급되는데, 예외적인 경우가 있어서 헷갈릴 수 있으니 참고해 두세요.


5.2 배열 인덱싱. 배열의 부분(subsection) (Array indexing. Subsections of an array)

배열의 개별 원소는 배열 이름 뒤에 대괄호 안에 첨자를 쉼표로 구분해서 적어주면 참조할 수 있어요.

좀 더 일반적으로는, 첨자 자리에 *인덱스 벡터(index vectors)*의 나열을 주면 배열의 부분을 지정할 수 있어요. 단, 어떤 인덱스 자리에 빈 인덱스 벡터를 주면 그 첨자의 전체 범위가 선택된다는 점을 기억하세요.

앞의 예 a를 이어서 봅시다. a[2,,]는 차원 벡터가 c(4,2)인 4 × 2 배열이고, 데이터 벡터는 다음 값을 담고 있어요.

c(a[2,1,1], a[2,2,1], a[2,3,1], a[2,4,1],
  a[2,1,2], a[2,2,2], a[2,3,2], a[2,4,2])

그 순서대로요. 그리고 a[,,]는 배열 전체를 가리키는데, 사실상 첨자를 아예 생략하고 a만 쓰는 것과 똑같아요.

어떤 배열 Z에 대해서든 차원 벡터는 dim(Z)로 명시적으로 참조할 수 있어요 (대입식의 왼쪽에 쓰면 차원을 설정하는 것이고, 오른쪽에 쓰면 차원을 읽는 것이죠).

또 하나 기억할 점은, 배열 이름 뒤에 첨자나 인덱스 벡터를 하나만 주면 데이터 벡터의 해당 값들만 사용되고 이때는 차원 벡터가 무시된다는 거예요. 다만 그 단일 인덱스가 벡터가 아니라 그 자체로 배열이라면 이야기가 달라지는데, 바로 다음 절에서 다룰게요.


5.3 인덱스 행렬 (Index matrices)

어느 첨자 자리든 인덱스 벡터를 쓸 수 있지만, **인덱스 행렬(index matrix)**을 하나만 사용해서 특정 위치에 흩어져 있는 원소들에 한꺼번에 값을 할당하거나, 그런 원소들을 벡터로 추출할 수도 있어요.

예제로 살펴보면 훨씬 명확해져요. 2중 첨자 배열(즉 행렬)의 경우, 인덱스 행렬은 두 개의 열(column)과 원하는 만큼의 행(row)으로 이루어져요. 인덱스 행렬의 원소들이 바로 2중 첨자 배열의 행 인덱스와 열 인덱스를 가리키는 값들이죠. 예를 들어 4 × 5 배열 X가 있고, 다음 두 가지를 하려고 한다고 해볼게요.

  • 원소 X[1,3], X[2,2], X[3,1]을 벡터 구조로 추출하고,
  • 이 원소들을 배열 X에서 0으로 바꾼다.

이 경우 3 × 2 첨자 배열이 필요해요. 다음 예시를 보세요.

> x <- array(1:20, dim=c(4,5))   # Generate a 4 by 5 array.
> x
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    5    9   13   17
[2,]    2    6   10   14   18
[3,]    3    7   11   15   19
[4,]    4    8   12   16   20
> i <- array(c(1:3,3:1), dim=c(3,2))
> i                             # i is a 3 by 2 index array.
     [,1] [,2]
[1,]    1    3
[2,]    2    2
[3,]    3    1
> x[i]                          # Extract those elements
[1] 9 6 3
> x[i] <- 0                     # Replace those elements by zeros.
> x
     [,1] [,2] [,3] [,4] [,5]
[1,]    1    5    0   13   17
[2,]    2    0   10   14   18
[3,]    0    7   11   15   19
[4,]    4    8   12   16   20
>

인덱스 행렬에서는 음수 인덱스를 쓸 수 없어요. NA와 0 값은 허용되는데, 인덱스 행렬에서 0이 들어있는 행은 무시되고, NA가 들어있는 행은 결과에 NA를 만들어 냅니다.

조금 더 실용적인 예를 들어볼게요. 요인 blocks(b개 수준)와 varieties(v개 수준)로 정의되는 블록 설계(block design)에 대한 (축약하지 않은) 설계 행렬을 만들고 싶다고 해봐요. 실험에 n개의 구획(plot)이 있다고 가정하면 다음과 같이 진행할 수 있어요.

> Xb <- matrix(0, n, b)
> Xv <- matrix(0, n, v)
> ib <- cbind(1:n, blocks)
> iv <- cbind(1:n, varieties)
> Xb[ib] <- 1
> Xv[iv] <- 1
> X <- cbind(Xb, Xv)

이때 접적 행렬(incidence matrix) N을 만들려면 다음과 같이 쓸 수 있어요.

> N <- crossprod(Xb, Xv)

하지만 더 간단하게는 table() 함수를 쓰는 방법이 있어요.

> N <- table(blocks, varieties)

인덱스 행렬은 반드시 숫자형이어야 해요. 논리 행렬이나 문자 행렬처럼 다른 형태의 행렬을 인덱스 자리에 주면, 그것은 인덱스 벡터로 취급되요.


5.4 array() 함수 (The array() function)

벡터에 dim 속성을 붙이는 방법 말고도, array 함수를 이용해서 벡터로부터 배열을 만들 수 있어요. 그 형태는 다음과 같아요.

> Z <- array(data_vector, dim_vector)

예를 들어 벡터 h가 24개 이하의 숫자를 담고 있다면, 다음 명령은

> Z <- array(h, dim=c(3,4,2))

h를 이용해 3 × 4 × 2 배열을 Z에 만들어 넣어요. h의 크기가 정확히 24라면 그 결과는 아래와 똑같아요.

> Z <- h ; dim(Z) <- c(3,4,2)

그런데 h가 24보다 짧다면, 값을 24 크기로 맞추기 위해 앞에서부터 다시 재활용(recycle)해요 (이 규칙은 Mixed vector and array arithmetic. The recycling rule에서 다룹니다). 반면 dim(h) <- c(3,4,2)처럼 쓰면 길이가 안 맞는다는 오류가 나요.

아주 흔한 예로, 모든 값이 0인 배열을 만들 때는 다음과 같이 써요.

> Z <- array(0, c(3,4,2))

이렇게 하면 Z가 전부 0으로 채워진 배열이 돼요.

이 시점에서 dim(Z)는 차원 벡터 c(3,4,2)를 뜻하고, Z[1:24]h에 있던 그대로의 데이터 벡터를 뜻해요. 그리고 Z[]처럼 빈 첨자를 주거나 Z만 쓰는 경우는 배열 자체(전체 배열)를 뜻합니다.

배열은 산술 표현식에도 쓸 수 있는데, 이때 결과는 데이터 벡터에 대해 원소별(element-by-element) 연산을 수행해 만들어진 배열이에요. 피연산자들의 dim 속성은 보통 같아야 하고, 그 속성이 결과의 차원 벡터가 돼요. 그래서 A, B, C가 모두 비슷한 배열이라면

> D <- 2*A*B + C + 1

D를 비슷한 형태의 배열로 만들고, 그 데이터 벡터는 위의 원소별 연산 결과가 돼요. 다만 배열과 벡터가 섞인 계산에 대한 정확한 규칙은 조금 더 주의 깊게 봐야 해요.

5.4.1 벡터와 배열이 섞인 연산. 재활용 규칙 (Mixed vector and array arithmetic. The recycling rule)

벡터와 배열이 섞인 원소별 계산에 적용되는 정확한 규칙은 좀 특이해서 참고문헌에서 찾기도 어려워요. 경험상 다음이 믿을 만한 지침이에요.

  • 표현식은 왼쪽에서 오른쪽으로 훑어요.
  • 짧은 벡터 피연산자는 다른 피연산자의 크기에 맞을 때까지 값을 재활용해서 늘려요.
  • 짧은 벡터와 배열만 나오는 한, 배열들은 전부 같은 dim 속성을 가져야 하고 그렇지 않으면 오류가 나요.
  • 행렬이나 배열 피연산자보다 긴 벡터 피연산자가 있으면 오류가 나요.
  • 배열 구조가 있고 오류나 벡터로의 강제 변환이 일어나지 않았다면, 결과는 배열 피연산자들이 공통으로 가진 dim 속성을 지닌 배열 구조가 돼요.

5.5 두 배열의 외적 (The outer product of two arrays)

배열에서 중요한 연산 중 하나가 *외적(outer product)*이에요. ab가 두 숫자 배열이라면, 두 배열의 외적은 차원 벡터가 두 배열의 차원 벡터를 이어붙인 것(순서가 중요!)이고, 데이터 벡터는 a의 데이터 벡터 원소들과 b의 데이터 벡터 원소들의 모든 가능한 곱으로 만들어진 배열이에요. 외적은 특별 연산자 %o%로 만들어요.

> ab <- a %o% b

다른 방법으로는 아래처럼 쓸 수도 있어요.

> ab <- outer(a, b, "*")

여기서 곱셈 함수 대신 두 변수를 받는 임의의 함수를 넣을 수도 있어요. 예를 들어 f(x; y) = cos(y)/(1 + x^2) 함수를 x, y 좌표로 정의된 규칙적인 격자 위에서 계산하고 싶다면 다음과 같이 할 수 있어요.

> f <- function(x, y) cos(y)/(1 + x^2)
> z <- outer(x, y, f)

특히 두 일반 벡터의 외적은 2중 첨자 배열, 즉 (랭크가 기껏해야 1인) 행렬이 돼요. 참고로 외적 연산자는 당연히 교환 법칙이 성립하지 않아요. 자신만의 R 함수를 정의하는 방법은 Writing your own functions에서 자세히 다룰게요.

예시: 2 × 2 한 자리 숫자 행렬의 행렬식

인위적이지만 재미있는 예로, 각 항목이 0에서 9 사이의 음이 아닌 정수(즉 한 자리 숫자)인 2 × 2 행렬 *[a, b; c, d]*의 행렬식을 생각해 봐요.

문제는 이런 형태의 모든 행렬에 대해 행렬식 ad − bc를 구하고, 각 값이 나타나는 빈도를 high density 플롯으로 그리는 거예요. 즉 각 숫자가 독립적이고 균일하게 무작위로 선택될 때 행렬식의 확률 분포를 찾는 셈이에요.

이걸 푸는 깔끔한 방법은 outer() 함수를 두 번 사용하는 거예요.

> d <- outer(0:9, 0:9)
> fr <- table(outer(d, d, "-"))
> plot(fr, xlab="Determinant", ylab="Frequency")

여기서 plot()fr"table" 클래스라는 것을 "알아보고" 히스토그램 같은 플롯 메서드를 사용해요. Grouping, loops and conditional execution에서 다룰 for 루프로 이 문제를 푸는 "당연한" 방법은 너무 비효율적이라 실용적이지 않아요.

또 한 가지 재미있는 사실은, 이런 행렬의 약 1/20이 특이 행렬(singular)이라는 점이에요. 의외이지요?


5.6 배열의 일반화된 전치 (Generalized transpose of an array)

aperm(a, perm) 함수는 배열 a의 차원을 재배열(permute)하는 데 쓸 수 있어요. 인자 perm은 정수 *{1, ..., k}*의 순열이어야 하는데, 여기서 ka의 첨자 수예요. 이 함수의 결과는 a와 크기가 같되, perm[j]가 가리키는 옛 차원이 새 j번째 차원이 되는 배열이에요. 이 연산을 이해하는 가장 쉬운 방법은 행렬의 전치(transpose)를 일반화한 것이라고 생각하는 거예요. 실제로 A가 행렬(즉 2중 첨자 배열)이라면,

> B <- aperm(A, c(2,1))

로 만들어지는 BA의 전치 행렬이 돼요. 이 특수한 경우에는 더 간단한 함수 t()가 있으므로 B <- t(A)라고 쓸 수도 있었어요.


5.7 행렬 기능 (Matrix facilities)

앞에서 말했듯이 행렬은 그냥 두 개의 첨자를 가진 배열이에요. 하지만 그렇게 중요한 특수한 경우라서 별도로 다뤄야 해요. R에는 행렬에만 쓸 수 있는 연산자와 함수가 아주 많아요. 예를 들어 t(X)는 행렬 전치 함수이고, nrow(A)ncol(A)는 각각 행렬 A의 행 수와 열 수를 알려줘요.

5.7.1 행렬 곱셈 (Matrix multiplication)

행렬 곱셈에는 연산자 %*%를 사용해요.

물론 n × 1이나 1 × n 행렬을 문맥에 맞게 n-벡터로 쓸 수 있어요. 반대로 행렬 곱셈 표현식에 등장하는 벡터는, 가능하면 곱셈이 성립하는 쪽으로 자동으로 행 벡터나 열 벡터로 승격돼요 (다만 나중에 보겠지만 항상 명확하게 정해지지는 않아요).

예를 들어 AB가 크기가 같은 정사각 행렬이라면,

> A * B

는 원소별 곱(element by element product)의 행렬이고,

> A %*% B

는 행렬 곱(matrix product)이에요. x가 벡터라면,

> x %*% A %*% x

은 이차 형식(quadratic form)이 돼요.^16^

crossprod() 함수는 "교차곱(cross products)"을 만들어요. 즉 crossprod(X, y)t(X) %*% y와 같은 결과지만 연산이 더 효율적이에요. crossprod()의 두 번째 인자를 생략하면 첫 번째 인자와 같은 것으로 간주해요.

diag()의 의미는 인자에 따라 달라져요. v가 벡터일 때 diag(v)는 벡터의 원소들을 대각선 항목으로 하는 대각 행렬을 만들어요. 반면 M이 행렬일 때 diag(M)M의 주대각선 항목들을 벡터로 돌려줘요. 이것은 MATLAB의 diag()와 같은 관례이고요. 그리고 다소 혼란스럽게도, k가 단일 숫자 값이면 diag(k)k × k **단위 행렬(identity matrix)**이 돼요!

5.7.2 선형 방정식과 역행렬 (Linear equations and inversion)

선형 방정식을 푸는 것은 행렬 곱셈의 역연산이에요.

> b <- A %*% x

다음에 Ab만 주어졌다면, 벡터 x가 바로 그 선형 방정식계의 해예요. R에서는

> solve(A,b)

가 그 연립방정식을 풀어서 x를 (어느 정도의 정밀도 손실을 감수하고) 돌려줘요. 선형대수에서 형식적으로는 x = A^{-1} %*% b인데, 여기서 A^{-1}A의 *역행렬(inverse)*을 뜻하며 다음과 같이 계산할 수 있어요.

solve(A)

하지만 이건 실제로 거의 필요하지 않아요. 수치적으로 solve(A,b) 대신 x <- solve(A) %*% b처럼 역행렬을 먼저 계산하는 것은 비효율적이고 수치적으로 불안정할 수도 있어요.

다변량 계산에서 쓰이는 이차 형식 x %*% A^{-1} %*% x도, A의 역행렬을 계산하는 대신 ^17^ x %*% solve(A,x)처럼 계산해야 해요.

5.7.3 고유값과 고유벡터 (Eigenvalues and eigenvectors)

eigen(Sm) 함수는 대칭 행렬 Sm의 고유값(eigenvalue)과 고유벡터(eigenvector)를 계산해요. 이 함수의 결과는 valuesvectors라는 두 구성 요소로 이루어진 리스트예요. 다음 대입은

> ev <- eigen(Sm)

이 리스트를 ev에 할당해요. 그러면 ev$valSm의 고유값 벡터이고, ev$vec은 대응하는 고유벡터들의 행렬이에요. 고유값만 필요하다면 다음처럼 쓸 수도 있어요.

> evals <- eigen(Sm)$values

이제 evals는 고유값 벡터를 담고 두 번째 구성 요소는 버려져요. 그런데

> eigen(Sm)

이라는 표현을 그 자체로 명령으로 쓰면 두 구성 요소가 이름과 함께 출력돼요. 큰 행렬의 경우 고유벡터가 필요 없다면 다음처럼 계산을 피하는 게 좋아요.

> evals <- eigen(Sm, only.values = TRUE)$values

5.7.4 특이값 분해와 행렬식 (Singular value decomposition and determinants)

svd(M) 함수는 임의의 행렬 인자 M을 받아 M의 특이값 분해(singular value decomposition)를 계산해요. 이 분해는 M과 같은 열 공간(column space)을 갖는 정규직교 열들로 이루어진 행렬 U, 열 공간이 M의 행 공간(row space)인 또 하나의 정규직교 열 행렬 V, 그리고 양수 항목들로 이루어진 대각 행렬 D로 구성되며 M = U %*% D %*% t(V)를 만족해요. 실제로 D는 대각 원소들의 벡터로 반환돼요. svd(M)의 결과는 d, u, v라는 세 구성 요소로 이루어진 리스트인데, 각 의미는 당연히 짐작할 수 있어요.

M이 정사각 행렬이라면 다음 코드가

> absdetM <- prod(svd(M)$d)

M의 행렬식의 절댓값을 계산한다는 걸 쉽게 알 수 있어요. 이 계산을 다양한 행렬에 대해 자주 해야 한다면 R 함수로 정의해 둘 수 있어요.

> absdet <- function(M) prod(svd(M)$d)

이렇게 해두면 absdet()을 그냥 하나의 R 함수처럼 쓸 수 있어요. 또 하나 사소하지만 유용할 수 있는 예로, 정사각 행렬의 대각합(trace)을 계산하는 함수 tr()을 작성해 보는 것도 좋겠어요. [힌트: 명시적인 루프를 쓸 필요 없어요. diag() 함수를 다시 살펴보세요.]

R에는 부호를 포함해 행렬식을 계산하는 내장 함수 det와, 부호와 절댓값(선택적으로 로그 스케일)을 주는 determinant 함수도 있어요.

5.7.5 최소제곱 적합과 QR 분해 (Least squares fitting and the QR decomposition)

lsfit() 함수는 최소제곱 적합(least squares fitting) 절차의 결과를 담은 리스트를 돌려줘요. 다음과 같은 대입은

> ans <- lsfit(X, y)

y를 관측 벡터, X를 설계 행렬로 하는 최소제곱 적합의 결과를 제공해요. 자세한 내용은 도움말을 참고하시고, 회귀 진단(regression diagnostics) 등에 쓰이는 후속 함수 ls.diag()도 함께 살펴보세요. 큰 평균(grand mean) 항은 자동으로 포함되므로 X의 열로 명시적으로 넣을 필요 없어요. 또 회귀 모델링에서는 lsfit()보다 거의 항상 lm(.)(참고: Linear models)을 쓰는 게 좋아요.

이와 밀접하게 관련된 함수로 qr()과 그 동료들이 있어요. 다음 대입들을 생각해 보세요.

> Xplus <- qr(X)
> b <- qr.coef(Xplus, y)
> fit <- qr.fitted(Xplus, y)
> res <- qr.resid(Xplus, y)

이들은 yX의 열 공간(range)에 정사영한 것을 fit에, 그 직교 보수(orthogonal complement)에 정사영한 것을 res에, 그리고 그 정사영의 계수 벡터를 b에 계산해 넣어요. 즉 b는 본질적으로 MATLAB의 'backslash' 연산자의 결과와 같아요.

X가 완전한 열 랭크(full column rank)를 가질 필요는 없어요. 중복된 열은 발견되는 대로 제거돼요.

이 방법은 최소제곱 계산을 수행하는 더 오래된, 저수준 방식이에요. 일부 맥락에서는 여전히 유용하지만, 요즘은 보통 Statistical models in R에서 다룰 통계 모델 기능으로 대체돼요.


5.8 분할 행렬 만들기, cbind()rbind() (Forming partitioned matrices, cbind() and rbind())

앞에서 비공식적으로 봤듯이, 행렬은 다른 벡터와 행렬들로부터 cbind()rbind() 함수로 만들 수 있어요. 대략 cbind()는 행렬들을 가로로, 즉 열 방향(column-wise)으로 묶고, rbind()는 세로로, 즉 행 방향(row-wise)으로 묶어서 행렬을 만들어요.

다음 대입에서

> X <- cbind(arg_1, arg_2, arg_3, ...)

cbind()의 인자는 어떤 길이의 벡터들이거나, 같은 열 크기(즉 같은 행 수)를 가진 행렬들이어야 해요. 그 결과는 인자 arg_1, arg_2, …들이 각각 열을 이루는 행렬이 돼요.

cbind()의 인자 중 일부가 벡터라면 그 벡터들은 어떤 행렬의 열 크기보다 짧을 수 있어요. 이 경우 행렬의 열 크기(행렬이 없으면 가장 긴 벡터의 길이)에 맞춰 순환적으로 확장돼요.

rbind() 함수는 행에 대해 대응하는 연산을 해요. 이 경우 벡터 인자(순환적으로 확장될 수도 있는)는 당연히 행 벡터로 취급돼요.

X1X2가 같은 행 수를 가진다고 해볼게요. 이들을 열로 묶어 행렬 X를 만들고, 맨 앞에 1로 채워진 열을 추가하고 싶다면 다음과 같이 쓸 수 있어요.

> X <- cbind(1, X1, X2)

rbind()cbind()의 결과는 항상 행렬 상태(matrix status)를 가져요. 따라서 cbind(x)rbind(x)는 각각 벡터 x를 열 행렬이나 행 행렬로 명시적으로 다루게 하는 가장 간단한 방법일 수 있어요.


5.9 배열과 함께 쓰는 결합 함수 c() (The concatenation function, c(), with arrays)

cbind()rbind()dim 속성을 존중하는 결합 함수인 반면, 기본 c() 함수는 dim 속성을 존중하지 않는다는 점에 주의해야 해요. 오히려 숫자 객체에서 모든 dimdimnames 속성을 지워버려요. 이 자체로 가끔 유용할 때가 있어요.

배열을 다시 단순한 벡터 객체로 강제 변환하는 공식적인 방법은 as.vector()를 쓰는 거예요.

> vec <- as.vector(X)

비슷한 결과는 c()를 인자 하나만 써서, 바로 이 부수 효과를 위해 얻을 수도 있어요.

> vec <- c(X)

둘 사이에는 약간의 차이가 있지만, 궁극적으로는 (전자가 더 권장되기는 하지만) 대체로 스타일의 문제예요.


5.10 요인으로부터의 빈도표 (Frequency tables from factors)

앞에서 요인(factor)이 그룹으로의 분할을 정의한다는 걸 기억하세요. 마찬가지로 한 쌍의 요인은 이원 교차 분류(two way cross classification)를 정의하고, 그 이상도 계속돼요. table() 함수는 길이가 같은 요인들로부터 빈도표(frequency table)를 계산해요. k개의 요인 인자가 있으면 결과는 k-원 배열의 빈도가 돼요.

예를 들어 statef가 데이터 벡터의 각 항목에 대한 주(state) 코드를 주는 요인이라고 해볼게요. 다음 대입은

> statefr <- table(statef)

statefr에 표본에서 각 주의 빈도표를 만들어 줘요. 빈도는 요인의 levels 속성에 따라 정렬되고 라벨이 붙어요. 이 단순한 경우는 아래와 동등하지만 더 편리해요.

> statefr <- tapply(statef, statef, length)

이제 incomef가 데이터 벡터의 각 항목에 대해 적절히 정의된 "소득 구간(income class)"을 주는 요인이라고 해볼게요. 예를 들어 cut() 함수를 쓰면 돼요.

> factor(cut(incomes, breaks = 35+10*(0:7))) -> incomef

그러면 이원 빈도표는 이렇게 계산해요.

> table(incomef,statef)
         statef
incomef   act nsw nt qld sa tas vic wa
  (35,45]   1   1  0   1  0   0   1  0
  (45,55]   1   1  1   1  2   0   1  3
  (55,65]   0   3  1   3  2   2   2  1
  (65,75]   0   1  0   0  0   0   1  0

더 높은 차원의 빈도표로 확장하는 것도 바로 가능해요.


더 알아보기

  • R 공식 매뉴얼 원문: An Introduction to R — 5 Arrays and matrices
  • array(), matrix(), dim(), aperm(), outer(), t(), %*%, solve(), eigen(), svd(), cbind(), rbind(), table() 등 이번 장에서 다룬 함수들의 R 도움말 (help() 또는 ?함수명)
  • 6장 "Lists and data frames"에서 이어지는 리스트와 데이터 프레임 다루기