R 中的“数据类型”容易混淆,因为这个词经常同时指三件不同的事:
- 一个值在内存中怎样存储,例如 logical、integer、double;
- 多个值怎样组成对象,例如 vector、matrix、list、data frame;
- 一个变量在分析中扮演什么角色,例如连续、离散、名义、有序。
前两项属于 R 的对象系统,第三项属于 统计学语义。它们有关联,但不能放在同一张类型表里当成同一级概念。
用三个问题认识一个对象
拿到一个陌生对象时,先问:
x <- c(1, 2, 3)
typeof(x) # 底层存储类型
class(x) # 面向方法分派的类别
attributes(x) # 额外属性
typeof()回答对象在 R 内部以什么形式存储;class()回答 R 应把它当作哪一类对象使用;attributes()解释同一份底层数据怎样获得维度、名称、类别等结构。
日常检查还常用:
length(x)
str(x)
is.numeric(x)
is.character(x)
is.factor(x)
str() 往往是最快的入口,因为它同时展示结构、长度、类型和部分内容。
六种原子类型
原子向量中的所有元素必须具有同一种底层类型。R 的六种常见原子类型是:
typeof() |
用途 | 示例 |
|---|---|---|
logical |
逻辑判断 | TRUE, FALSE |
integer |
整数 | 10L |
double |
双精度数值 | 3.14, 10 |
complex |
复数 | 2 + 3i |
character |
字符串 | "R" |
raw |
原始字节 | charToRaw("R") |
typeof(TRUE) # "logical"
typeof(10L) # "integer"
typeof(10) # "double"
typeof(2 + 3i) # "complex"
typeof("R") # "character"
typeof(charToRaw("R")) # "raw"
R 中直接写 10 得到的是 double;只有写成 10L 或显式转换,才得到 integer。
“numeric”通常是一个更宽的判断概念,而不是 typeof() 返回的独立底层类型:
is.numeric(10) # TRUE
is.numeric(10L) # TRUE
typeof(10) # "double"
typeof(10L) # "integer"
向量与类型提升
原子向量不能混合存储不同类型。用 c() 组合不同类型时,R 会把元素提升为能够容纳所有值的共同类型。
c(TRUE, 2L) # integer
c(TRUE, 2L, 3.5) # double
c(1, "two") # character
常见的提升方向可以简化为:
logical → integer → double → complex → character
这也是很多“值看起来突然变了”的来源:
x <- c(1, 2, "3")
typeof(x) # "character"
如果确实需要在同一个对象中保存不同类型,应使用 list,而不是依赖自动转换。
使用 pmin() 和 pmax() 逐元素比较
min() 和 max() 从所有输入值中各返回一个极值;pmin() 和 pmax() 则沿多个向量的对应位置逐一比较,返回与输入长度相同的向量。
x <- c(1, 5, 3)
y <- c(2, 4, 6)
min(x, y)
# [1] 1
pmin(x, y)
# [1] 1 4 3
pmax(x, y)
# [1] 2 5 6
它们可以同时比较两个以上的向量:
a <- c(1, 3, 5)
b <- c(2, 1, 4)
c <- c(3, 2, 0)
pmin(a, b, c)
# [1] 1 1 0
pmax(a, b, c)
# [1] 3 3 5
这类操作常用于在数据框中按行比较若干列,而不需要显式循环:
scores <- data.frame(
exam_1 = c(82, 91, 76),
exam_2 = c(88, 86, 79)
)
scores$lower <- pmin(scores$exam_1, scores$exam_2)
scores$higher <- pmax(scores$exam_1, scores$exam_2)
也可以用它们限制数值范围。例如将 x 截断到 0 与 100 之间:
x <- c(-10, 35, 120)
pmax(0, pmin(x, 100))
# [1] 0 35 100
默认情况下,只要某个位置包含 NA,该位置的结果就是 NA:
x <- c(1, NA, 3)
y <- c(2, 4, NA)
pmin(x, y)
# [1] 1 NA NA
pmin(x, y, na.rm = TRUE)
# [1] 1 4 3
输入长度不同时,R 会采用向量回收规则。标量与向量组合通常正是想要的行为,例如 pmax(x, 0);多个非标量向量则应先确认长度和行的对应关系,不能把回收误当作按键值匹配。
数值取整与格式化
数值计算和显示格式是两件事。round()、floor()、ceiling()、trunc() 与 signif() 返回数值,适合继续计算;format()、sprintf() 与 formatC() 返回字符,适合报表、标签和文件输出。
| 函数 | 用途 | 示例 | 返回类型 |
|---|---|---|---|
round() |
保留指定小数位 | round(2.678, 2) |
numeric |
floor() |
向下取整 | floor(2.678) |
numeric |
ceiling() |
向上取整 | ceiling(2.678) |
numeric |
trunc() |
朝零截断 | trunc(-2.678) |
numeric |
signif() |
保留有效数字 | signif(123.456, 2) |
numeric |
format() |
控制通用显示格式 | format(2.6, nsmall = 2) |
character |
sprintf() |
按格式模板输出 | sprintf("%.1f", 2.678) |
character |
formatC() |
按宽度、位数等格式化 | formatC(2.678, format = "f", digits = 2) |
character |
round(3.456, 2) # 3.46
floor(3.456) # 3
ceiling(3.456) # 4
trunc(-3.456) # -3
signif(0.012345, 3) # 0.0123
round(x, 2) 产生的仍是数值,因此打印时不保证总是显示两个小数位。需要固定显示位数时再进行字符格式化:
format(3.1, nsmall = 2) # "3.10"
sprintf("%.2f", 3.14159) # "3.14"
sprintf("%05d", 23) # "00023"
sprintf("%.1f%%", 99.158) # "99.2%"
sprintf("%.2e", 0.000123) # "1.23e-04"
formatC(12, width = 4, flag = "0") # "0012"
向量化函数可以直接处理整列;在数据框中,也可以配合 dplyr::across() 批量应用:
nums <- c(1.12, 2.34, 3.56)
round(nums, 1)
df <- data.frame(a = c(2.314, 3.187), b = c(7.654, 1.459))
df |>
dplyr::mutate(dplyr::across(a:b, ~ sprintf("%.2f", .x)))
最后一个操作会把列转成 character。若后面还要计算,保留原始数值列,只在最终展示层格式化;否则排序、汇总和绘图都可能按字符串而不是数值处理。
基础数学与描述统计
Base R 的常用数学函数都可以直接处理数值向量:
| 任务 | 函数 | 示例 |
|---|---|---|
| 绝对值 | abs() |
abs(-3.5) |
| 平方根 | sqrt() |
sqrt(16) |
| 求和 | sum() |
sum(x) |
| 均值 | mean() |
mean(x) |
| 中位数 | median() |
median(x) |
| 标准差 | sd() |
sd(x) |
| 方差 | var() |
var(x) |
| 分位数 | quantile() |
quantile(x, probs = 0.25) |
| 最小值与最大值 | range() |
range(x) |
| 极差 | diff(range()) |
diff(range(x)) |
x <- c(1, 2, 3, 4, 5)
sum(x) # 15
mean(x) # 3
median(x) # 3
sd(x)
var(x)
quantile(x, probs = 0.25) # 25% 分位数
range(x) # 1 5
diff(range(x)) # 4
这些函数大多会传播缺失值。只有确认缺失观测可以从当前汇总中排除时,才设置 na.rm = TRUE:
x <- c(1, 2, NA, 4)
mean(x) # NA
mean(x, na.rm = TRUE) # 2.333333
sd() 和 var() 默认计算样本标准差与样本方差,分母使用 。quantile() 还存在不同的插值定义;正式分析若依赖精确分位数,应记录使用的 type,而不只记录函数名。
quantile(x, probs = c(0.25, 0.5, 0.75), na.rm = TRUE, type = 7)
取整函数已经改变数值,描述统计函数则汇总数值;二者都不同于仅改变打印外观的 format() 与 sprintf()。
特殊值不是同一种东西
R 中常见的特殊值表示不同状态:
| 值 | 含义 | 典型来源 |
|---|---|---|
NA |
值缺失或未知 | 数据缺失、匹配失败 |
NaN |
计算结果不是一个数 | 0 / 0 |
Inf, -Inf |
正负无穷 | 1 / 0, -1 / 0 |
NULL |
对象或组件不存在 | 空返回值、删除 list 元素 |
is.na(NA) # TRUE
is.na(NaN) # TRUE
is.nan(NA) # FALSE
is.nan(NaN) # TRUE
is.finite(Inf) # FALSE
is.null(NULL) # TRUE
NA 也带类型。R 会根据向量类型使用逻辑、整数、双精度、复数或字符缺失值:
typeof(NA) # "logical"
typeof(NA_integer_) # "integer"
typeof(NA_real_) # "double"
typeof(NA_character_) # "character"
不要用 x == NA 判断缺失值,因为和未知值比较仍然得到未知;应使用 is.na(x)。
缺失值、空字符串和 NULL 分开处理
这三类“空”具有不同结构:
length(NA) # 1:一个未知值
length("") # 1:一个已知的空字符串
length(NULL) # 0:没有对象元素
删除数据框中任意列含 NA 的行:
df <- data.frame(
a = c(1, NA, 3),
b = c("x", "y", NA)
)
na.omit(df)
需要先查看哪些行完整时,使用 complete.cases():
complete <- complete.cases(df)
df[complete, ]
这类整行删除会改变分析样本,并可能引入 complete-case selection bias;不能只为消除报错就默认使用。只关心部分必要列时,应明确限定检查范围:
complete.cases(df[c("a", "b")])
空字符串不会被 is.na() 或 na.omit() 当作缺失值:
x <- c(NA, "", "text")
is.na(x)
x == ""
确实需要统一识别 NA 与空字符串时,可以组合条件:
is_blank <- function(x) {
is.na(x) | (is.character(x) & trimws(x) == "")
}
NULL 需要先单独判断,因为它表示长度为 0,而不是向量中的一个缺失位置:
is_void <- function(x) {
if (is.null(x)) {
return(TRUE)
}
is.na(x) | (is.character(x) & trimws(x) == "")
}
对于 list,NULL 可以作为元素保存,但在原子向量中不能占据一个位置。清洗前应先决定业务上哪些状态真的等价,不能因为它们打印起来“像空的”就全部替换成同一个值。
复合对象怎样构成
R 中很多看似不同的结构,本质上是向量或列表加上 attributes。
Matrix 与 array
矩阵是带二维 dim 属性的原子向量;array 则可以有更多维度。因此,一个矩阵里的所有元素仍必须具有相同类型。
m <- matrix(1:6, nrow = 2, ncol = 3)
typeof(m) # "integer"
class(m) # "matrix" "array"
attributes(m) # 包含 dim
a <- array(1:8, dim = c(2, 2, 2))
dim(a)
List
list 是递归向量,每个元素可以是不同类型、长度和结构。
person <- list(
id = 1L,
name = "Alice",
scores = c(90, 95, 88),
active = TRUE
)
str(person)
函数返回多个不同对象、嵌套 JSON、模型结果和复杂配置时,通常都会使用 list。
Data frame
data frame 本质上是一个各列等长的命名 list,并带有 data.frame class 和行名。
df <- data.frame(
name = c("Alice", "Bob"),
age = c(23L, 25L),
score = c(91.5, 88.0)
)
typeof(df) # "list"
class(df) # "data.frame"
str(df)
每一列内部必须保持单一类型,但不同列之间可以有不同类型。tibble 延续了这个基本模型,只是打印、取子集和名称处理等行为更严格。
快速检查数据框
拿到陌生数据后,先检查结构、类型、缺失和分布,而不是立即建模:
str(iris)
summary(iris)
table(iris$Species, useNA = "ifany")
使用 dplyr 时,glimpse() 以列为中心展示结构:
dplyr::glimpse(iris)
需要每列更系统的摘要,可以使用 skimr:
skimr::skim(iris)
三个工具回答的问题不同:
| 工具 | 适合查看 |
|---|---|
str() / glimpse() |
对象结构、列名、类型与部分值 |
summary() / skim() |
数值分布、缺失和分类变量摘要 |
table() |
一个或多个离散变量的频数 |
table() 产生的是计数。需要总体、行或列比例以及多维列联表时,参见 Statistics 系列的频数、比例与列联表。
自动摘要适合发现问题,不能替代对变量含义、单位、编码和合理范围的核查。
Factor:整数编码加类别标签
factor 用于表示分类变量。它的底层是整数向量,levels 属性保存类别标签,class 告诉 R 按 factor 行为处理它。
group <- factor(c("control", "treated", "control"))
typeof(group) # "integer"
class(group) # "factor"
levels(group) # "control" "treated"
unclass(group) # 1 2 1,并附带 levels 属性
名义变量没有自然顺序:
blood_type <- factor(c("A", "B", "O", "AB"))
有序变量需要显式声明 levels 的次序:
pain <- factor(
c("mild", "severe", "moderate"),
levels = c("mild", "moderate", "severe"),
ordered = TRUE
)
pain[1] < pain[2] # TRUE
factor 最常见的转换陷阱是:
x <- factor(c("10", "20", "30"))
as.numeric(x) # 1 2 3:底层 level 编码
as.numeric(as.character(x)) # 10 20 30:标签表示的数值
使用 forcats 整理因子
Base R 的 factor() 负责创建因子;需要重新排序、改名、合并或清理 levels 时,可以使用 forcats。它的函数大多以 fct_ 开头,适合放在 dplyr::mutate() 中,也会直接影响 ggplot2 中类别的显示顺序。
library(forcats)
常用函数可以按任务分成几组:
| 任务 | 函数 | 作用 |
|---|---|---|
| 按数据排序 | fct_inorder() |
按首次出现顺序排列 levels |
| 按频数排序 | fct_infreq() |
按出现频率从高到低排列 levels |
| 手动排序 | fct_relevel() |
将指定 level 移到需要的位置 |
| 反转顺序 | fct_rev() |
反转全部 levels |
| 删除空 level | fct_drop() |
移除数据中未使用的 levels |
| 合并低频类别 | fct_lump() |
将较少出现的类别归入 Other |
| 修改标签 | fct_recode() |
为已有 levels 重新命名 |
| 合并类别 | fct_collapse() |
将多个 levels 归为一个新类别 |
| 检查分布 | fct_count() |
统计各 level 的频数 |
| 检查成员 | fct_match() |
判断值是否属于指定 levels |
控制类别顺序
字符向量转为因子时,默认 levels 往往按字母顺序排列。若要保留数据首次出现的顺序:
status <- c("pending", "running", "done", "pending")
fct_inorder(status)
手动指定优先显示的类别:
priority <- factor(c("low", "medium", "high", "medium"))
priority <- fct_relevel(priority, "high", "medium", "low")
levels(priority)
# [1] "high" "medium" "low"
按频数排序特别适合条形图:
fruit <- factor(c("apple", "banana", "apple", "pear", "banana", "apple"))
fruit <- fct_infreq(fruit)
levels(fruit)
# [1] "apple" "banana" "pear"
如果横向条形图需要反向显示,可以继续使用 fct_rev():
fruit <- fct_rev(fruit)
这里改变的是 levels 顺序,不是观测行的顺序。
重命名与合并类别
fct_recode() 的左侧是新标签,右侧是原标签:
rating <- factor(c("L", "M", "H", "M"))
rating <- fct_recode(
rating,
Low = "L",
Medium = "M",
High = "H"
)
多个原类别需要归为一组时,使用 fct_collapse():
fruit <- factor(c("apple", "banana", "kiwi", "orange", "pear"))
fruit_group <- fct_collapse(
fruit,
Common = c("apple", "banana"),
Other = c("kiwi", "orange", "pear")
)
对于类别很多的变量,fct_lump() 可以保留频数最高的若干类,将其余类别合并为 Other:
fruit <- c("apple", "apple", "banana", "banana", "kiwi", "orange", "pear")
fct_lump(fruit, n = 2)
这种合并会改变数据表达的类别,应根据分析目的明确决定,不能只为让图看起来更整齐。
清理与检查 levels
筛选数据后,原因子可能仍保留已经没有观测的 level:
group <- factor(c("A", "B", "C"), levels = c("A", "B", "C", "D"))
fct_drop(group)
查看各 level 的频数:
fct_count(group)
检查值是否属于指定类别:
fct_match(group, c("A", "C"))
在数据清洗中,推荐先检查 levels 和频数,再决定排序、重命名或合并。这样可以避免拼写差异被误当作两个类别,也能保留每一步类别变化的含义。
导入数据时更稳妥的做法,是先明确列应当是字符、数值还是 factor,再在数据验证阶段转换,而不是等建模时报错后猜测类型。
Class 可以改变同一底层数据的意义
Date 和 POSIXct 展示了 typeof() 与 class() 的区别。
day <- as.Date("2026-02-27")
time <- as.POSIXct("2026-02-27 12:30:00", tz = "Asia/Shanghai")
typeof(day) # "double"
class(day) # "Date"
typeof(time) # "double"
class(time) # "POSIXct" "POSIXt"
底层数字分别记录相对某个时间原点的天数或秒数;class 决定打印、计算和方法分派时把这些数字解释为日期或时间。
这也是为什么只看 typeof() 不足以理解一个 R 对象。
R 类型与统计变量类型分开判断
“double”“factor”和“continuous”“nominal”不在同一个分类层级。
| 问题 | 分类示例 |
|---|---|
| R 怎样存储? | integer、double、character |
| R 怎样组织? | vector、matrix、list、data frame |
| R 应怎样处理? | factor、Date、POSIXct、模型对象 |
| 分析中代表什么? | 连续、离散、名义、有序 |
例如年龄可能在 R 中存为 double,但分析时既可以作为连续变量,也可能被人为分组后作为有序 factor;患者编号可能由 integer 存储,却只是名义标识,不能拿去求平均值。
因此,建模前应分别确认:
- 当前列在 R 中实际是什么类型;
- 这个变量在研究问题中代表什么;
- 模型函数希望接收什么 class 或编码;
- 缺失值、参考水平和顺序是否已经明确。
类型转换的安全顺序
显式转换函数包括:
as.logical()
as.integer()
as.double()
as.character()
as.factor()
转换前先检查原值,转换后再验证结果:
x <- c("1.2", "3.4", "missing")
result <- as.double(x)
# Warning: NAs introduced by coercion
data.frame(original = x, converted = result)
一个稳妥的工作流是:
str(df)
summary(df)
# 明确转换目标
df$group <- factor(df$group)
df$age <- as.integer(df$age)
# 转换后再次确认
str(df)
不要为了消除错误消息而盲目 as.numeric();类型转换应该表达数据含义,而不是只让代码继续运行。
最小检查清单
遇到类型相关问题时,依次检查:
typeof(x)
class(x)
attributes(x)
length(x)
str(x)
然后再问:
- 是否发生了自动类型提升?
- 是否把 factor 的整数编码当成了标签值?
NA、NaN、Inf和NULL是否被混用?- matrix 中是否因为一个字符值导致整张表变成 character?
- 变量的统计角色是否与它在 R 中的存储类型混淆?
理解 R 对象的关键不是背一张类型表,而是沿着 底层类型 → 对象结构 → class/attributes → 分析语义 逐层判断。