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:3typeof(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") + 1as.numeric("3.14abc")[1] 4.14[1] NAWarning message:NAs introduced by coercion那句警告别忽略。数据清洗阶段它经常是「有一列混进了脏字符」的唯一线索。
索引从 1 开始
Section titled “索引从 1 开始”跟 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"]bob178因子:本体是 levels,不是那些标签
Section titled “因子:本体是 levels,不是那些标签”因子(factor)用来表示分类变量。它内部存的是整数编码,另挂一个 levels 属性记录每个编码对应的标签:
f <- factor(c("10", "9", "100"))flevels(f)as.numeric(f)[1] 10 9 100Levels: 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 c1 1 0c 一次都没出现过,但它仍是合法取值。分组统计、画图、建模型时因子会带上全部 levels,所以表格里会出现计数为 0 的空组。反过来,删掉部分行之后 levels 不会自动收缩,要用 droplevels() 手动清理,否则画出来的图会多出一根空柱子。
矩阵与数组:给向量加一个 dim
Section titled “矩阵与数组:给向量加一个 dim”矩阵(matrix)就是带 dim 属性的向量:
m <- matrix(1:6, nrow = 2, byrow = TRUE)mdim(m) [,1] [,2] [,3][1,] 1 2 3[2,] 4 5 6[1] 2 3byrow = 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三维数组处理「年 × 月 × 站点」这类数据很方便,日常分析碰到的频率不高,知道它存在就够了。
列表:什么都能装
Section titled “列表:什么都能装”列表(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)resclass(res)dim(res)res["OJ", "2"] 0.5 1 2OJ 13.23 22.70 26.06VC 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 的 dtype 和 shape 大致对应 R 的 typeof 和 dim),可以对照 /python/basics/numpy 看同一套概念的另一套写法。数据结构熟悉之后,下一步是日常使用频率最高的数据框操作:/r/basics/data-frames。