同種のデータの集まり(すべて数値、あるいはすべて文字など)を扱うときは、Rで次の構造を使えます。
vector:1次元のデータ用で、1つのインデックスでアクセスします。matrix:2次元の長方形(不揃いでない)データ用です。array:一般的なN次元の場合に使います。matrixはN=2のarrayにすぎません。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
行や列が1つだけの部分集合は、行列ではなくベクトルとして返ることに注意してください。
class(m)
#> [1] "matrix" "array"
class(m[2,])
#> [1] "integer"
この種のdimension reductionは、特に入力を行列と想定した関数のパイプラインでは、常に望ましいとは限りません。
drop=FALSEを指定すると、行列を入力したときに必ず行列が戻り値になります。行や列が1つしかない行列は、ベクトルとは種類が_違う_ことに注意してください。
m[2, , drop = FALSE]
#> [,1] [,2] [,3]
#> [1,] 2 4 6
as.matrix()関数は、ベクトルを1列の行列に変換します。
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に含まれていて、行列プログラミングをより細かく制御するための、今でも最良の道具です。
引数は、順に次のとおりです。
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
このセクションは、Exercismの範囲を超える数学の知識を前提としています。
意味がよくわからない場合は、読み飛ばしてください。
ただし、Rはデータサイエンスの言語であり、多くの利用者が応用数学や統計学の背景を持っています。この簡潔なまとめは、一部の学習者にとって興味深いかもしれません。
現代のRは「明らかに」データサイエンスの言語です。主に表形式のデータを扱うように設計され、統計学者や疫学者、社会科学者といった利用者に人気があります。この分野の主な競合はPythonの科学技術スタックで、PandasやPolarsといったパッケージが含まれます。
しかし、数十年前には、Rは工学向けの言語としても見られ、Matlabと直接競合していました。
意外に思われるかもしれませんが、Base Rは今でもベクトルと行列に対する線形代数演算を一通りサポートしています。これらが現在どれほど利用者のコードに登場するかは定かではありませんが、統計関数を実装するうえで、そうした技法は_内部的に_とても重要であり続けています。
Rの線形代数は、たとえばこの計量経済学の授業のように、今でも大学の講義で教えられています。(プログラミングではなく)数学のよい解説は3Blue1Brownにあります。
_正方形_の(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を1つ渡すと、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
ベクトルや行列のその他の乗算の種類を説明するのは本題の範囲を超えますが、ごく簡単にまとめると次のとおりです。