在处理一组同质的原子数据(例如全是数值,或全是字符)时,R 中有以下结构可用:
vector:一维数据,用一个下标访问。matrix:二维的矩形(非不规则)数据。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
注意,如果取出的子集只有一行或只有一列,返回的是向量,而不是矩阵。
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 的一部分,仍然是对矩阵编程做更精细控制的最佳工具。
它的形参依次是:
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 时,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
解释向量和矩阵的各种其他乘法超出了本文的范围,不过可以极简地总结如下: