学习路径
/
R
R
/
教学大纲
/
矩阵与数组
矩阵

矩阵与数组 属于 R

{other: "%{count} 个练习"}

关于 矩阵与数组

在处理一组同质的原子数据(例如全是数值,或全是字符)时,R 中有以下结构可用:

  • vector:一维数据,用一个下标访问。
  • matrix:二维的矩形(非不规则)数据。
  • 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“显然”是一门数据科学语言:它主要为处理表格数据而设计,在统计学家、流行病学家和社会科学家等用户中很受欢迎。这一领域的主要竞争者是 Python 科学计算栈,包括 Pandas 或 Polars 等包。

不过,在更早的几十年里,R 也被视为一门有潜力的工程语言,与 Matlab 直接竞争。

也许出乎意料的是,Base R 仍然支持对向量和矩阵进行全套线性代数运算。如今这些运算在最终用户代码中出现的频率有多高,已经不太清楚,但在实现统计函数的_内部_,这类技术依然非常重要。

R 中的线性代数至今仍在一些大学课程里讲授,比如这门计量经济学课程。关于其中的数学(而不是编程),3Blue1Brown 上有一份很好的讲解。

Advanced

Exercism 上关于线性代数最完整的讨论在 Julia 的教学大纲里(与 R 的教学大纲出自同一批作者)。我们认为,如今要做严肃的线性代数工作,Julia(或在 numpy.linalg 模块可用的情况下使用 Python)会比 R 更合适。

对角线

方阵(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 编辑 该链接会在新窗口或标签页中打开