03R 数据类型与对象结构

分清原子类型、向量、列表、矩阵、数据框与因子,理解数值取整、输出格式、类型转换,以及 R 存储类型和统计变量角色的区别。

2026-02-28
RData TypesObjectsFactors
本章目录 · 19

R 中的“数据类型”容易混淆,因为这个词经常同时指三件不同的事:

  1. 一个值在内存中怎样存储,例如 logical、integer、double;
  2. 多个值怎样组成对象,例如 vector、matrix、list、data frame;
  3. 一个变量在分析中扮演什么角色,例如连续、离散、名义、有序。

前两项属于 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() 默认计算样本标准差与样本方差,分母使用 n1n - 1quantile() 还存在不同的插值定义;正式分析若依赖精确分位数,应记录使用的 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 存储,却只是名义标识,不能拿去求平均值。

因此,建模前应分别确认:

  1. 当前列在 R 中实际是什么类型;
  2. 这个变量在研究问题中代表什么;
  3. 模型函数希望接收什么 class 或编码;
  4. 缺失值、参考水平和顺序是否已经明确。

类型转换的安全顺序

显式转换函数包括:

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 的整数编码当成了标签值?
  • NANaNInfNULL 是否被混用?
  • matrix 中是否因为一个字符值导致整张表变成 character?
  • 变量的统计角色是否与它在 R 中的存储类型混淆?

理解 R 对象的关键不是背一张类型表,而是沿着 底层类型 → 对象结构 → class/attributes → 分析语义 逐层判断。