트랙
/
R
R
/
학습 과정
/
행렬과 배열
행렬

행렬과 배열 에서 R

{other: "%{count}개의 연습 문제"}

행렬과 배열 소개

같은 종류의 원자적 데이터(예를 들어 모두 숫자이거나 모두 문자인 데이터)로 이루어진 컬렉션을 다룰 때, R에서는 다음과 같은 구조를 사용할 수 있어요:

  • vector는 1차원 데이터를 위한 구조로, 하나의 인덱스로 접근해요.
  • matrix는 2차원의 직사각형 데이터(들쭉날쭉하지 않은)를 위한 구조예요.
  • array는 일반적인 N차원 데이터를 위한 구조예요. matrix는 N=2인 array일 뿐이에요.
Note

Python 프로그래머라면: 이 부분이 익숙하게 느껴질 수도 있어요.

NumPy와 비슷해 보인다면 우연이 아니에요(R가 조금 더 오래되었고, 이 모든 기능을 기본 언어에 갖추고 있거든요).

행렬

행렬은 본질적으로 벡터에, 모양을 정의하는 행과 열의 개수를 더한 것일 뿐이에요.

행렬을 만들려면 matrix() 함수를 사용해요:

# data already available 
matrix(1:6, nrow=2, ncol=3)
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

# empty matrix to be filled in later
matrix(nrow=2, ncol=3)
#>      [,1] [,2] [,3]
#> [1,]   NA   NA   NA
#> [2,]   NA   NA   NA

데이터는 열 우선 순서로 저장돼요(즉, 아래로 먼저, 그다음 옆으로). 이는 Fortran, Matlab, Julia와 같지만 C/C++ 및 NumPy와는 달라요.

행렬의 차원은 dim()으로 행과 열을 모두 구할 수 있고, nrow()나 ncol()로 한 차원만 구할 수 있어요. length()를 사용하면 기반이 되는 벡터의 길이, 즉 전체 원소의 개수(위 예시에서는 6)를 반환할 뿐이에요.

다른 함수들도 행렬을 결과로 만들어요. 예를 들어 str_split_fixed()가 있어요.

library(stringr)

str_split_fixed(c("split some words", "and some more"), " ", 3)
#>      [,1]    [,2]   [,3]   
#> [1,] "split" "some" "words"
#> [2,] "and"   "some" "more" 

행렬 인덱싱

벡터 인덱싱과 모든 면에서 똑같이 동작하지만, 행과 열을 그 순서대로 지정해야 한다는 점만 달라요. 차원을 비워 두면 해당 행이나 열 전체를 가져와요.

위와 같은 행렬을 사용해요:

m <- matrix(1:6, nrow=2, ncol=3)
m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

m[2, 3]    # row 2, col 3
#> [1] 6

m[1:2, 3]  # rows 1:2, col 3
#> [1] 5 6

 m[2, ]    # all of row 2
#> [1] 2 4 6

행이나 열이 하나뿐인 부분집합은 행렬이 아니라 벡터로 반환된다는 점에 유의해요.

class(m)
#> [1] "matrix" "array" 
class(m[2,])
#> [1] "integer"

이런 dimension reduction은 항상 원하는 것은 아니에요. 특히 행렬 입력을 기대하는 함수 파이프라인에서는요.

drop=FALSE를 지정하면 행렬 입력에 대해 항상 행렬이 반환되도록 보장해요. 행이나 열이 하나뿐인 행렬은 벡터와 같은 타입이 아니라는 점에 유의해요.

m[2, , drop = FALSE]
#>      [,1] [,2] [,3]
#> [1,]    2    4    6

as.matrix() 함수는 벡터를 단일 열 행렬로 변환해요.

as.matrix(1:3)
#>      [,1]
#> [1,]    1
#> [2,]    2
#> [3,]    3

벡터와 마찬가지로, 행렬의 일부분도 값을 새로 할당하기만 하면 수정할 수 있어요.

m[, 2] <- c(10, 20)
m
#>      [,1] [,2] [,3]
#> [1,]    1   10    5
#> [2,]    2   20    6

행과 열 추가 및 삭제

벡터에서는 c() 함수만으로 데이터를 삽입하고 덧붙일 수 있었어요. 행렬은 구조가 더 복잡하기 때문에 무엇이 필요한지 더 신경 써서 지정해야 해요.

rbind()와 cbind() 함수는 행렬에 새로운 행이나 열을 결합해요.

m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
m2 <- matrix(7:9, nrow = 1, ncol = 3)

rbind(m, m2)
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
#> [3,]    7    8    9

행이나 열을 삭제할 때는 벡터에서처럼 음수 인덱싱을 사용해요.

m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

m[, -2]
     #> [,1] [,2]
#> [1,]    1    5
#> [2,]    2    6

필터링과 함수

모든 벡터 함수는 행렬에서도 동작하며, 모든 원소에 적용돼요. 하지만 행렬 구조를 잃고 벡터를 반환할 수도 있어요.

sqrt(m)
#>          [,1]     [,2]     [,3]
#> [1,] 1.000000 1.732051 2.236068
#> [2,] 1.414214 2.000000 2.449490

sum(m)
#> [1] 21

m[m > 3]
#> [1] 4 5 6

행렬에서는 colSums()와 rowSums() 함수도 유용하게 쓸 수 있어요.

이 경우에는 차원 축소가 의도된 것이므로, 반환값은 벡터예요.

# m as defined above
rowSums(m)
#> [1]  9 12
colSums(m)
#> [1]  3  7 11

마찬가지로 평균을 계산하는 rowMeans()와 colMeans() 함수도 있어요.

행렬에 작용하는 일반 함수

다른 개념에서 Tidyverse 라이브러리가 벡터와 배열(purrr) 또는 데이터프레임(dplyr)에 대한 연산을 단순화해 준다는 것을 살펴봤어요.

하지만 행렬과 배열은 Tidyverse 입력의 범위 밖에 있어요(적어도 지금은요).

apply() 함수는 Base R에 포함되어 있고, 행렬 프로그래밍을 더 세밀하게 제어하는 데 여전히 가장 좋은 도구예요.

매개변수는 순서대로 다음과 같아요:

  1. 입력 행렬이에요.
  2. 연산할 차원이에요(행은 1, 열은 2).
  3. 적용할 함수예요.
  4. 선택적으로, 함수에 전달할 추가 매개변수예요.

rowMeans() 함수는 Base R에 있지만, apply()와 mean()으로 똑같이 흉내 낼 수도 있어요.

randm <- sample(1:10, 16, replace = TRUE) |> matrix(4, 4)
randm
#>      [,1] [,2] [,3] [,4]
#> [1,]    4    9    8    3
#> [2,]    6    8    5    5
#> [3,]    4    2    1    2
#> [4,]    4    2    5    4

apply(randm, 1, mean)
#> [1] 6.00 6.00 2.25 3.75

rowMeans(randm)
#> [1] 6.00 6.00 2.25 3.75

선형대수

Note

이 부분은 Exercism의 범위를 벗어나는 수학적 지식을 전제로 해요.

전혀 이해가 되지 않는다면 건너뛰어도 괜찮아요.

하지만 R은 데이터 과학 언어이고, 많은 사용자가 응용 수학과 통계학을 배경으로 가지고 있어요. 이 짧은 요약이 일부 학습자에게는 흥미로울 수 있어요.

현대의 R은 "당연히도" 데이터 과학 언어예요. 주로 표 형식의 데이터를 다루도록 설계되었고, 통계학자, 역학자, 사회과학자 같은 사용자에게 인기가 많아요. 이 분야의 주요 경쟁자는 Pandas나 Polars 같은 패키지를 포함한 Python 과학 컴퓨팅 생태계예요.

하지만 초기 몇십 년 동안 R은 Matlab과 직접 경쟁하는 잠재적인 엔지니어링 언어로도 여겨졌어요.

놀랍게도, Base R은 여전히 벡터와 행렬에 대한 모든 범위의 선형대수 연산을 지원해요. 이런 기능이 지금 최종 사용자 코드에서 얼마나 쓰이는지는 분명하지 않지만, 이런 기법은 통계 함수를 구현하는 데 내부적으로 여전히 매우 중요해요.

R의 선형대수는 오늘날도 일부 대학 강의에서 가르쳐요. 예를 들어 이 계량경제학 강의가 있어요. 수학(프로그래밍이 아닌)에 대한 좋은 안내는 3Blue1Brown에 있어요.

Advanced

Exercism에서 선형대수를 가장 완전하게 다루는 곳은 Julia 교육 과정이에요(R 교육 과정과 같은 저자들이 만들었어요). 지금은 진지한 선형대수 작업에 R보다 Julia(또는 numpy.linalg 모듈을 사용할 수 있는 상황이라면 Python)가 더 적합한 언어라고 생각해요.

대각선

정사각 행렬(N x N)) A의 주대각선은 왼쪽 위(A[1,1])에서 오른쪽 아래(A[N,N])로 이어져요.

diag() 함수는 전달받는 인자에 따라 다르게 동작해요.

벡터 v를 주면 diag(v)는 대각 행렬을 만들어요.

diag(1:3)
#>      [,1] [,2] [,3]
#> [1,]    1    0    0
#> [2,]    0    2    0
#> [3,]    0    0    3

정사각 행렬 A를 주면 diag(A)는 주대각선 위의 원소들을 벡터로 반환해요.

A
#>      [,1] [,2] [,3]
#> [1,]   10    5    2
#> [2,]    9    3    7
#> [3,]    8   10    4
diag(A)
#> [1] 10  3  4

정수 하나 k를 주면 diag(k)는 k × k 단위 행렬을 만들어요. 아마 예상하지 못한 결과일 거예요.

진지한 R 교과서조차 이 지점에서 "참 묘하죠" 같은 주석을 달곤 해요.

diag(3)
#>      [,1] [,2] [,3]
#> [1,]    1    0    0
#> [2,]    0    1    0
#> [3,]    0    0    1

전치와 역행렬

행과 열을 맞바꾸려면 t() 함수가 어떤 행렬이든 전치해 줘요.

m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
t(m)
#>      [,1] [,2]
#> [1,]    1    2
#> [2,]    3    4
#> [3,]    5    6

복소수 값 행렬의 경우, 결과는 켤레복소수가 아니에요. 행렬 A의 켤레 전치(수반 행렬)를 얻으려면 Conj(t(A))를 사용해요.

더 높은 차원의 배열에서는 aperm()이 차원 순서를 바꿔요.

정사각 행렬 A의 역행렬은 solve(A)로 구할 수 있어요. 이 문법이 ... 뜻밖이라 느껴지는 사람도 있어요.

행렬에 그 역행렬을 곱하면 단위 행렬이 돼요(아주 작은 반올림 오차는 있을 수 있어요).

A <- matrix(sample(1:10, 9, replace = TRUE), 3, 3)
A
#>      [,1] [,2] [,3]
#> [1,]   10    5    2
#> [2,]    9    3    7
#> [3,]    8   10    4

solve(A)
#>             [,1]        [,2]        [,3]
#> [1,]  0.16666667  0.00000000 -0.08333333
#> [2,] -0.05747126 -0.06896552  0.14942529
#> [3,] -0.18965517  0.17241379  0.04310345

A %*% solve(A) |> round(digits = 10)
     [,1] [,2] [,3]
#> [1,]    1    0    0
#> [2,]    0    1    0
#> [3,]    0    0    1

곱셈

+, -, *, ^ 같은 일반적인 산술 연산자는 행렬에서 원소별로 동작해요. 차원이 맞지 않으면 재활용의 일반적인 규칙이 적용돼요.

m
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

m * m
#>      [,1] [,2] [,3]
#> [1,]    1    9   25
#> [2,]    4   16   36

# same calculation, different syntax
m ^ 2
#>      [,1] [,2] [,3]
#> [1,]    1    9   25
#> [2,]    4   16   36

벡터와 행렬의 여러 다른 곱셈 방식을 설명하는 것은 이 범위를 벗어나지만, 아주 간단히 요약하면 다음과 같아요:

GitHub에서 편집 링크가 새 창이나 탭에서 열려요