因子到底是什么
因子是 R 处理分类数据的方式,这类数据的取值来自一份固定的选项清单:实验分组、问卷答案、T 恤尺码。打印出来时它看起来像字符向量,但它并不是。在内部,因子是一个整数编码向量加上一张标签查找表,这些标签叫作水平(levels):
打印结果先显示标签,再用一行 Levels: 列出选项清单。as.integer() 揭开了内部机制:编码是 3 1 3 2,因为每个值实际上都是水平表中的一个索引,而且水平默认按字母顺序排列(large、medium、small),而不是按数据出现的顺序。所以 small 的编码是 3,large 的编码是 1,这完全违反直觉。记住这个按字母排序的默认行为,下面有两个陷阱都源于它。
为什么要用这种两层设计,而不是直接用字符串?因为统计需要它。模型没法把 “small” 乘以一个系数,但可以把三个已知水平做哑变量编码,变成由 0 和 1 组成的列。lm()、glm()、table() 以及方差分析背后的机制,都靠因子来判断:这个变量是分类变量,完整的类别集合是什么(包括数据中没出现的类别),以及哪个类别是基线。普通的字符向量不带有这些信息。
创建因子:levels、labels 和 table()
默认情况下,factor() 会取它找到的不同值,按字母顺序排成水平。你常常需要同时控制水平的集合和顺序,这时就传入 levels =:
现在水平按尺码的自然顺序排列,table() 也按这个顺序报告计数。table() 是一行代码的频数统计,用因子时你会经常用到它。nlevels() 统计类别数。
levels = 还能带来两个好处。数据中不在水平列表里的值会变成 NA(这是好事:拼写错误会暴露出来,而不是自成一个类别)。而且出现次数为零的水平仍然存在,所以问卷结果的汇总会显示“非常不同意:0”,而不是假装这个选项从来不存在。
labels = 在创建时就给水平重新命名,原始数据用代码表示时很方便:
当默认设置就够用时,as.factor(x) 可以快速转换现有的向量。
用有序因子表示有序数据
普通因子把类别视为无序的:“red” 并不小于 “blue”。但有些分类尺度确实有排名:低/中/高,不同意/中立/同意。用 ordered = TRUE 声明这一点:
现在打印结果显示 Levels: low < medium < high,比较运算符也能用了:你可以问一个评级是否高于另一个,或者筛选出所有不低于 “medium” 的值。对无序因子做这两件事都会出问题(准确地说是警告和 NA)。有序因子还会改变模型对变量的编码方式(用多项式对比代替哑变量),对于有序预测变量,这通常正是你需要的。
只有当排名真实存在时才用 ordered = TRUE。把普通分组编码成有序因子,会以很容易被误读的方式改变模型输出。
参照水平和 relevel()
因子的第一个水平很特殊:模型函数把它当作参照类别,也就是基线,其他每个水平的系数都是相对它来衡量的。因为默认的水平顺序是按字母排的,所以除非你干预,否则基线是由字母表决定的,而 “control” 按字母排在 “aspirin” 后面,这可不是一个科学决定。
relevel() 把某个水平提到第一位:
之前,control 排在第一位只是因为字母顺序碰巧如此。经过 relevel(..., ref = "control") 之后,它是被有意放在第一位的。在用这个预测变量的线性回归中,treatment 的系数现在回答的是“treatment 和 control 有什么不同?”,这正是你真正想问的问题。每当模型中分类变量的系数看起来令人困惑时,先检查参照水平。
(如果要完整地重新排序,而不只是调整第一位,就再次把完整的 levels = 向量传给 factor()。)
经典陷阱:把因子转换成数值
有时数字会以因子的形式出现,通常是 CSV 中某一列混进了一个非数字的值。转换回来看似简单,结果却错得令人难忘:
as.numeric(f) 返回 2 1 3。不是 20、10、30,而是水平编码。水平按字母顺序排成 "10", "20", "30",所以 "20" 是第 2 个水平,转换出来就是……2。没有错误,没有警告,一串看起来合理的小整数悄悄替换了你的数据。曾有论文因为这个问题被撤稿。
正确的做法是先转成字符:as.numeric(as.character(f)) 先把标签作为文本取回来,再把文本解析成数字:20 10 30。把这个写法刻在脑子里:因子转数值,一定要经过 as.character()。
droplevels() 和 stringsAsFactors 的故事
对因子取子集时,完整的水平集合会保留下来,即使某些类别已经不再出现:
过滤掉 large 之后,table() 仍然会报告它,计数为 0。有时这正是你想要的(你希望空类别也能看到)。如果不想要,比如计数为零的组会让图表变得杂乱,还可能破坏分层分析,就用 droplevels() 丢弃没有观测值的水平。
阅读旧代码或 Stack Overflow 答案时,你需要了解一段历史:在 R 4.0(2020 年)之前,data.frame() 和 read.csv() 会自动把每个字符列转换成因子,因为 stringsAsFactors = TRUE 是默认值。十年间的教程里到处都是应付这些没人要的因子的变通办法。从 R 4.0 起,默认值变成了 FALSE:字符串保持为字符串,你只在数据框中某个变量确实是分类变量时,才有意地创建因子。这才是正确的习惯:显式地、有意地创建因子,水平由你自己选定。
要点总结
- 因子 = 整数编码 + 水平标签;正是它告诉统计函数一个变量是分类变量。
- 用
levels =控制类别集合和顺序,用labels =重命名,用table()计数。 ordered = TRUE让真正有序的尺度可以进行比较。- 第一个水平是模型的基线,用
relevel(f, ref = ...)有意识地设置它。 - 永远不要直接
as.numeric(f),一定要as.numeric(as.character(f))。 - 取子集后用
droplevels()清除未使用的水平;从 R 4.0 起,除非你自己创建因子,否则字符串保持为字符串。
下一篇:数据框,因子、数字和文本作为同一张表的列在这里共存。
常见问题
R 中的因子是什么?
因子是 R 用来表示分类数据的类型:取值来自一组固定的可能值,这组值叫作水平(levels)。在内部,它是一个整数编码向量加上一张水平标签表,正因如此,统计函数才能正确地处理类别(统计个数、在模型中做哑变量编码),而不是把它们当作随意的文本。
在 R 中怎么把因子转换成数值?
先转成字符:as.numeric(as.character(f))。直接调用 as.numeric(f) 返回的是内部的水平编码(1、2、3……),而不是标签上显示的值,所以一个显示为 "20" 的因子可能会变成 2。这是 R 中最常见的隐蔽 bug 之一。
R 中 relevel() 有什么作用?
它把指定的水平移到第一位:relevel(group, ref = "control")。第一个水平是参照(基线)类别,lm()、glm() 等模型函数会把其他每个水平都和它比较,所以有意识地选择它,回归系数才能表达你想要的含义。
为什么我的因子还显示已经删掉的水平?
对因子取子集时,即使某些水平已经不再出现,完整的水平集合也会保留下来,所以 table() 会显示计数为零的类别,模型也仍会为它们留出位置。对子集运行 droplevels() 就能丢弃未使用的水平。