跳到正文

R语言数据类型:向量、因子、矩阵与列表详解

R 里没有「标量」这个类型。你敲进去的 1 是长度为 1 的向量,"a" 也是。这一点想通了,后面很多看起来莫名其妙的行为都能解释。数据框、矩阵、因子,本质上都是向量加上若干属性(attribute)。

原子向量:typeof 和 class 说的不是一回事

Section titled “原子向量:typeof 和 class 说的不是一回事”

原子向量(atomic vector)只能装同一种类型的数据,一共五种:logical(逻辑)、integer(整数)、double(双精度浮点)、character(字符)、complex(复数)。raw 类型在数据分析里基本用不到,先不管。

x <- c(1, 2, 3)
typeof(x)
class(x)
y <- 1:3
typeof(y)
[1] "double"
[1] "numeric"
[1] "integer"

typeof()class() 在这里给出了不同的答案,这是新手最容易混的一对函数。区别在于:typeof() 问「底层怎么存的」,class() 问「R 认为它是什么」。双精度向量的存储类型是 double,但 class() 返回 numeric;整数向量的 class() 就是 integer

写判断时别用 class(x) == "double",这个条件永远为假。要用 is.numeric()is.character()is.integer() 这类函数。另外 1:3 是整数,c(1, 2, 3) 是双精度,尽管打印出来一模一样。需要明确的整数时写 1L,那个 L 后缀表示 integer literal。

强制转换:c(1, “a”) 会变成什么

Section titled “强制转换:c(1, “a”) 会变成什么”

c() 遇到不同类型的参数,会把它们统一到「能装下所有元素」的那个类型上。优先级是 logical < integer < double < complex < character:

c(TRUE, 1)
c(1, TRUE, "a")
[1] 1 1
[1] "1" "TRUE" "a"

第一行把 TRUE 变成 1(逻辑值参与算术时就是 0/1),第二行把整个向量变成字符。这个过程叫强制转换(coercion),不报错也不警告,静默发生。它是 R 里最难查的一类 bug 来源:一列本该是数字的数据里混进一个 "N/A",整列变成字符,后面所有算术全部失效。

比较运算遵循同样的规则。1 == "1" 返回 TRUE,因为 "1" 被转成了数字;但 "10" < "9" 也返回 TRUE,因为两边都是字符,按字典序比较,"1" 排在 "9" 前面。字符和数字混着比大小,结果几乎一定是错的。

显式转换用 as.numeric()as.character()。转不动的时候不报错,给你 NA 加一条警告:

as.numeric("3.14") + 1
as.numeric("3.14abc")
[1] 4.14
[1] NA
Warning message:
NAs introduced by coercion

那句警告别忽略。数据清洗阶段它经常是「有一列混进了脏字符」的唯一线索。

跟 Python、C 都不一样,R 没有 0 号位置:

v <- c(10, 20, 30, 40, 50)
v[1]
v[-1]
v[c(TRUE, FALSE, TRUE, FALSE, TRUE)]
v[v > 25]
[1] 10
[1] 20 30 40 50
[1] 10 30 50
[1] 30 40 50

四种写法各有用途。负索引是另一个容易出错的地方:v[-1] 的含义是「排除第 1 个」,而不是 Python 里那个「取最后一个」。v[-1] 在两边都是合法语法,语义却完全相反,从 Python 转过来的人十有八九要在这里栽一次。

条件索引 v[v > 25] 是 R 里最常用的操作,多个条件用 &| 组合(注意不是 &&||,后者只比较第一个元素,专用于 if 语句)。

向量可以带名字,取的时候按名字取:

h <- c(alice = 165, bob = 178, carol = 170)
h["bob"]
bob
178

因子:本体是 levels,不是那些标签

Section titled “因子:本体是 levels,不是那些标签”

因子(factor)用来表示分类变量。它内部存的是整数编码,另挂一个 levels 属性记录每个编码对应的标签:

f <- factor(c("10", "9", "100"))
f
levels(f)
as.numeric(f)
[1] 10 9 100
Levels: 10 100 9
[1] "10" "100" "9"
[1] 1 3 2

注意 levels() 的顺序:因子默认按字符串排序,所以 "100" 排在 "9" 前面。编码结果是 10→1、9→3、100→2。这意味着 as.numeric() 作用在因子上返回的是编码,不是原来的数值。想把 "10" 还原成 10,必须经字符中转:as.numeric(as.character(f))

这个坑在真实数据里到处都是。Excel 导出的 CSV 里一列编号被读成了因子,as.numeric() 之后得到一串 1、2、3,数值全错但程序一声不吭。R 4.0 之后 read.csv() 不再默认把字符串转成因子,但你自己用 factor() 处理过的列、或者接手的老代码,问题照旧。

另一个要留意的地方:levels 和实际出现过的取值是两回事。

f2 <- factor(c("a", "b"), levels = c("a", "b", "c"))
table(f2)
a b c
1 1 0

c 一次都没出现过,但它仍是合法取值。分组统计、画图、建模型时因子会带上全部 levels,所以表格里会出现计数为 0 的空组。反过来,删掉部分行之后 levels 不会自动收缩,要用 droplevels() 手动清理,否则画出来的图会多出一根空柱子。

矩阵(matrix)就是带 dim 属性的向量:

m <- matrix(1:6, nrow = 2, byrow = TRUE)
m
dim(m)
[,1] [,2] [,3]
[1,] 1 2 3
[2,] 4 5 6
[1] 2 3

byrow = TRUE 表示按行填充,默认是 FALSE(按列填充)。这两个经常记反,写完 matrix() 一定看一眼结果再往下走。转置用 t(),矩阵乘法用 %*%——普通的 * 是对应元素相乘,不是线性代数乘法,两者混用是数值计算里最隐蔽的错误之一。

取子集时维度会自动丢失:

m[, 2]
class(m[, 2])
class(m[, 2, drop = FALSE])
[1] 2 5
[1] "integer"
[1] "matrix" "array"

取一列,R 默认把它降维成向量。如果后面还要做矩阵运算,得显式写 drop = FALSE。数据框有同样的默认行为,下一篇会再遇到它。

数组(array)是矩阵的推广,dim 可以超过两维:

a <- array(1:8, dim = c(2, 2, 2))
a[, , 1]
[,1] [,2]
[1,] 1 3
[2,] 2 4

三维数组处理「年 × 月 × 站点」这类数据很方便,日常分析碰到的频率不高,知道它存在就够了。

列表(list)是最宽松的容器,元素可以是不同类型、不同长度,甚至可以嵌套另一个列表:

fit <- list(model = "lm", r2 = 0.75, coefs = c(1.2, -0.3))
length(fit)
fit$coefs
[1] 3
[1] 1.2 -0.3

[[[ 的区别在这里必须分清:[ 取子集,返回的还是列表;[[ 取出元素本身,返回元素的原类型。fit["r2"] 是长度为 1 的列表,fit[["r2"]] 是数字 0.75。把前者拿去算术,会得到 non-numeric argument to binary operator 这种看不出所以然的报错。

数据框本质上就是每个元素长度相等的列表。所以 iris$Species 取出向量,iris[1] 取出来还是数据框——规则跟列表完全一致,不是额外的设计。

完整示例:分组均值其实是个矩阵

Section titled “完整示例:分组均值其实是个矩阵”

tapply() 按因子分组做计算,它的返回值值得单独看一眼。用 R 内置的 ToothGrowth 数据(60 只豚鼠的牙齿长度,supp 是投喂方式,dose 是剂量):

str(ToothGrowth)
'data.frame': 60 obs. of 3 variables:
$ len : num 4.2 11.5 7.3 5.8 6.4 10 11.2 11.2 5.2 7 ...
$ supp: Factor w/ 2 levels "OJ","VC": 2 2 2 2 2 2 2 2 2 2 ...
$ dose: num 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 ...

传两个分组变量进去:

res <- tapply(ToothGrowth$len, list(ToothGrowth$supp, ToothGrowth$dose), mean)
res
class(res)
dim(res)
res["OJ", "2"]
0.5 1 2
OJ 13.23 22.70 26.06
VC 7.98 16.77 26.14
[1] "matrix" "array"
[1] 2 3
[1] 26.06

结果是一个二维数组:行是 supp 的两个水平,列是 dose 的三个取值,单元格是均值。这正好说明数组是从哪来的——分组变量有几个,结果就有几维。按名字取值 res["OJ", "2"] 比数位置 res[1, 3] 可读得多,也不会因为 levels 顺序变化而取错。

顺带能读出结论:剂量从 0.5 提到 2,两种投喂方式的牙齿长度都涨了一倍多;但在 0.5 和 1 两个剂量上,橙汁(OJ)组明显好于抗坏血酸(VC)组,到 2.0 时两者几乎持平。

结构 能装的类型 典型用途
原子向量 单一类型 一列数据、一组编号
因子 整数编码 + levels 分类变量、分组
矩阵 / 数组 单一类型 + 维度 数值计算、按组汇总
列表 任意 函数的多个返回值、嵌套结构
data.frame 每列单一类型、列之间可不同 数据集本身

判断标准不复杂:一串同质的值用向量,分类标签用因子,纯数值的二维表用矩阵,装不同种类东西用列表,有行有列的观测数据用 data.frame。

如果你也使用 Python,这些结构在 NumPy 里都有对应物(ndarray 的 dtypeshape 大致对应 R 的 typeofdim),可以对照 /python/basics/numpy 看同一套概念的另一套写法。数据结构熟悉之后,下一步是日常使用频率最高的数据框操作:/r/basics/data-frames