未记录事实核验 · 状态:现行内容
记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。
warning 适用边界与维护说明
本文适合学习常用 R 清洗函数与自建模拟数据流程,不是可直接套用的生产清洗规范;本次仅作全文结构审阅,未运行代码或进行外部事实核验。异常值检测的小节提供salary、age、reg_date三列自建教学输入,不沿用前文只有date_raw的df;业务阈值不代表真实业务标准。填充方法、删列比例及年龄、薪资阈值应结合业务与数据分布确定;按日期保留最新记录的示例未规定并列日期的唯一取舍。完整管道先填充后按 user_id 保留首条记录,最终断言未检查注册日期完整性,因此输出不代表所有字段均满足生产分析要求。assertr 中明确标注的失败示例会终止所在管道;重塑目录项为普通文本,可按同名章节查找。 已取消无法确定目标的可点击引用「#数据重塑:pivot_longer 与 pivot_wider」,保留原提及;Missing anchor: #数据重塑:pivot_longer 与 pivot_wider
R 数据清洗技巧
数据清洗是数据分析中的常见工作,其耗时占比取决于数据质量、任务和工作流程;本文不提供统一的时间比例。真实数据中几乎必然存在缺失值、重复记录、格式不统一和逻辑异常。本文系统介绍 R 语言中处理这些问题的工具和策略,并给出从原始脏数据到可分析数据框的完整管道示例。
目录
- NA 处理
- 重复值处理
- 字符串清洗:stringr
- 日期时间处理:lubridate
- 类型转换与转换失败
- 异常值检测
- #数据重塑:pivot_longer 与 pivot_wider
- 列重命名
- 数据验证:assertr
- 完整清洗流程示例
NA 处理
识别 NA
x <- c(1, NA, 3, NA, 5)
is.na(x) # FALSE TRUE FALSE TRUE FALSE(逐元素)
sum(is.na(x)) # 2(NA 的个数)
which(is.na(x)) # 2 4(NA 的位置索引)
# 数据框中各列的 NA 统计
df <- data.frame(a = c(1, NA, 3), b = c("x", "y", NA))
colSums(is.na(df)) # 各列 NA 个数
sapply(df, function(x) mean(is.na(x))) # 各列 NA 比例
warning `NA == NA` 返回 `NA` 而不是 `TRUE`。判断某值是否为 NA,必须用 `is.na()`,不能用 `==`。
删除 NA
x <- c(1, NA, 3, NA, 5)
# 向量级删除
na.omit(x) # 1 3 5(返回去掉NA后的向量)
x[!is.na(x)] # 同上
# 数据框级删除(行)
df <- data.frame(a = c(1, NA, 3), b = c(4, 5, NA))
na.omit(df) # 删除含任意NA的行
df[complete.cases(df), ] # 同上,等价写法
library(tidyr)
drop_na(df) # tidyr 版,删除含任意NA的行
drop_na(df, a) # 只删除列 a 中有 NA 的行
drop_na(df, c(a, b)) # 删除 a 或 b 有 NA 的行
NA 填充策略
library(tidyr)
library(dplyr)
# 数值型:均值填充
df <- df %>%
mutate(a = ifelse(is.na(a), mean(a, na.rm = TRUE), a))
# 数值型:中位数填充(对异常值更鲁棒)
df <- df %>%
mutate(a = ifelse(is.na(a), median(a, na.rm = TRUE), a))
# 前向填充(用前一个非NA值填充)
df_ts <- data.frame(
date = as.Date("2024-01-01") + 0:5,
value = c(10, NA, NA, 40, NA, 60)
)
df_ts %>% fill(value, .direction = "down") # 前向
df_ts %>% fill(value, .direction = "up") # 后向
df_ts %>% fill(value, .direction = "downup")# 先前向再后向
# replace_na():为不同列指定不同填充值
df %>% replace_na(list(a = 0, b = 0))
填充策略选择指南:
| 场景 | 推荐策略 | 备注 |
|---|---|---|
| 时间序列、有序数据 | 前向/后向填充 | 保持时序连贯性 |
| 正态分布数值 | 均值填充 | 不改变分布均值 |
| 偏斜分布数值 | 中位数填充 | 对离群值不敏感 |
| 分类变量 | 众数填充 | 或单独标记"未知"类别 |
| 缺失比例 > 30% | 考虑删除该列 | 填充引入过多噪声 |
重复值处理
df <- data.frame(
id = c(1, 2, 2, 3, 4, 4),
name = c("张三","李四","李四","王五","赵六","赵六"),
age = c(25, 30, 30, 28, 35, 35)
)
# 识别重复行
duplicated(df) # FALSE FALSE TRUE FALSE FALSE TRUE(第一次出现标FALSE)
duplicated(df, fromLast = TRUE) # 反向检测,最后一次出现标FALSE
# 查看所有重复的行(包括第一次出现)
df[duplicated(df) | duplicated(df, fromLast = TRUE), ]
# 计算重复行数
sum(duplicated(df)) # 2
# 删除重复行,保留第一条
df_unique <- df[!duplicated(df), ]
# dplyr 版(更简洁,推荐)
library(dplyr)
distinct(df) # 完全去重
distinct(df, id, .keep_all = TRUE) # 基于 id 去重,保留其他列
distinct(df, id, name) # 只保留 id 和 name 列并去重
部分字段重复的处理策略
# 业务场景:同一 id 有多条记录,需要保留最新的一条
df_orders <- data.frame(
user_id = c(1, 1, 2, 3, 3),
amount = c(100, 200, 150, 80, 120),
date = as.Date(c("2024-01-01","2024-03-01","2024-02-01","2024-01-15","2024-04-01"))
)
# 每个 user_id 保留最新的订单
df_orders %>%
group_by(user_id) %>%
slice_max(date, n = 1) %>%
ungroup()
字符串清洗:stringr
stringr 是 tidyverse 的字符串处理包,常用字符串操作函数以 str_ 开头,接口较为一致。
常用函数速查
| 函数 | 用途 | 示例 |
|---|---|---|
str_trim(x) |
去除两端空白 | " 张三 " → "张三" |
str_squish(x) |
去除两端空白+压缩内部多余空白 | "张 三" → "张 三" |
str_length(x) |
字符数 | str_length("张三") → 2 |
str_to_upper(x) |
转大写 | "hello" → "HELLO" |
str_to_lower(x) |
转小写 | "HELLO" → "hello" |
str_replace(x, pattern, replacement) |
替换首次匹配 | |
str_replace_all(x, pattern, replacement) |
替换所有匹配 | |
str_detect(x, pattern) |
是否包含模式(返回逻辑向量) | |
str_extract(x, pattern) |
提取首次匹配的子串 | |
str_extract_all(x, pattern) |
提取所有匹配 | |
str_split(x, pattern) |
按模式拆分 | |
str_sub(x, start, end) |
截取子串 | str_sub("abcde", 2, 4) → "bcd" |
str_pad(x, width, side) |
补齐宽度 | str_pad("5", 3, "left", "0") → "005" |
str_c(...) |
拼接字符串 | 等同 paste0 |
实际清洗示例
library(stringr)
raw <- c(" 张三 ", "李四ABC", " ", "WANG_WU", "138-0000-1234")
# 去空格
str_trim(raw) # "张三" "李四ABC" "" "WANG_WU" "138-0000-1234"
str_squish(raw) # 同上,还会压缩内部多余空格
# 正则提取:从混合字符串中提取手机号
phones <- c("联系电话:138-0000-1234,备用:139-8888-9999", "无手机")
str_extract(phones, "\\d{3}-\\d{4}-\\d{4}") # 首个手机号
str_extract_all(phones, "\\d{3}-\\d{4}-\\d{4}") # 所有手机号(返回列表)
# 替换:清洗价格字段中的货币符号
prices <- c("¥1,234.56", "$500.00", "EUR 300")
str_replace_all(prices, "[¥$,EUR\\s]", "") # "1234.56" "500.00" "300"
# 检测:筛选包含特定模式的行
emails <- c("user@qq.com", "invalid", "admin@163.com")
valid <- str_detect(emails, "^[\\w.]+@[\\w.]+\\.[a-z]{2,}$")
emails[valid] # "user@qq.com" "admin@163.com"
# 拆分
str_split("北京,上海,广州", ",")[[1]] # "北京" "上海" "广州"
常用正则表达式速查
| 模式 | 含义 |
|---|---|
\\d |
数字(等同 [0-9]) |
\\w |
字母数字下划线 |
\\s |
空白字符(空格、制表符等) |
^ / $ |
字符串开头/结尾 |
+ |
1次或多次 |
* |
0次或多次 |
? |
0次或1次 |
{n,m} |
n 到 m 次 |
[abc] |
字符集合 |
[^abc] |
非字符集合 |
日期时间处理:lubridate
lubridate 让 R 中的日期运算变得直观。函数名由格式字母拼成:y(年)、m(月)、d(日)、h(时)、m(分)、s(秒)。
解析日期字符串
library(lubridate)
# 函数名 = 格式顺序
ymd("2024-03-15") # "2024-03-15"(ISO 8601)
mdy("03/15/2024") # 美国格式
dmy("15-03-2024") # 欧洲格式
ymd_hms("2024-03-15 14:30:00") # 带时间
# 批量解析混合格式(parse_date_time 更灵活)
dates <- c("2024-03-15", "03/15/2024", "15.03.2024")
parse_date_time(dates, orders = c("ymd", "mdy", "dmy"))
日期运算
d <- ymd("2024-03-15")
# 提取组件
year(d) # 2024
month(d) # 3
day(d) # 15
wday(d, label = TRUE) # "Fri"(星期几)
quarter(d) # 1(第几季度)
yday(d) # 75(第几天)
# 加减运算(直觉化函数)
d + days(7) # 一周后
d %m+% months(3) # 三个月后;目标月份无对应日期时回退到月末
d + years(1) # 一年后
d - weeks(2) # 两周前
# 时间间隔
d2 <- ymd("2024-12-31")
as.numeric(d2 - d, "days") # 291 天
interval(d, d2) / months(1) # 9.5 个月
difftime(d2, d, units = "weeks") # 41.57 周
# 时区处理
now() # 当前时刻(本地时区)
with_tz(now(), "UTC") # 转换为 UTC
force_tz(now(), "Asia/Shanghai") # 强制设定时区(不改变时钟)
实际清洗场景
# 场景:将多种日期格式统一转换
df <- data.frame(
date_raw = c("2024-01-15", "20240215", "03/20/2024", "2024年4月5日", NA)
)
df %>% mutate(
date_clean = case_when(
str_detect(date_raw, "^\\d{4}-\\d{2}-\\d{2}$") ~ ymd(date_raw),
str_detect(date_raw, "^\\d{8}$") ~ ymd(date_raw),
str_detect(date_raw, "\\d{2}/\\d{2}/\\d{4}") ~ mdy(date_raw),
str_detect(date_raw, "年") ~ ymd(str_replace_all(date_raw, "[年月日]", "-")),
TRUE ~ as.Date(NA)
)
)
类型转换与转换失败
# 基础类型转换
as.numeric("3.14") # 3.14
as.integer("42") # 42
as.character(100) # "100"
as.logical(0) # FALSE(0→FALSE,非0→TRUE)
as.Date("2024-03-15") # Date 对象
# 以下数值转换失败会产生 NA,并发出警告
as.numeric("abc") # NA + 警告 "NAs introduced by coercion"
as.integer("3.5x") # NA
# 安全转换模式:检测失败数量
safe_to_numeric <- function(x) {
result <- suppressWarnings(as.numeric(x))
failures <- sum(is.na(result)) - sum(is.na(x)) # 新增 NA(转换失败的)
if (failures > 0) {
warning(sprintf("有 %d 个值无法转换为数值,已转为 NA", failures))
}
result
}
x_raw <- c("1", "2", "abc", "4", "5.5")
x_num <- safe_to_numeric(x_raw) # 警告: 有1个值无法转换
warning 上述数值转换失败会产生 NA 并发出警告,但其他类型转换可能报错。在 ETL 管道中,应比较转换前后的缺失情况并定位新增缺失;单独使用 `sum(is.na())` 只能统计当前缺失总数,不能区分原有缺失与转换失败。
异常值检测
IQR 方法(箱线图标准)
超出 [Q1 - 1.5×IQR, Q3 + 1.5×IQR] 范围的值被标记为异常:
x <- c(23, 25, 27, 24, 26, 100, 22, 28, -5, 25)
Q1 <- quantile(x, 0.25)
Q3 <- quantile(x, 0.75)
IQR_val <- IQR(x)
lower <- Q1 - 1.5 * IQR_val
upper <- Q3 + 1.5 * IQR_val
outliers <- x[x < lower | x > upper] # 100 和 -5
# 直接用 boxplot.stats() 获取异常值
boxplot.stats(x)$out # 返回异常点的值:100 -5
Z-score 方法
适用于近似正态分布的数据,Z 值绝对值超过 3 视为异常:
z_scores <- scale(x) # 标准化
outliers_z <- x[abs(z_scores) > 3]
# 下列两段共用自建教学输入,不代表真实业务阈值或资料
df <- tibble::tibble(
salary = c(5000, 6000, 7000, 150000),
age = c(25, 30, 40, 70),
reg_date = as.Date(c("2024-01-01", "2024-01-02", "2024-01-03", "2099-01-01"))
)
# 结合 dplyr 在数据框中标记异常
df %>%
mutate(
z_score = as.numeric(scale(salary)),
is_outlier = abs(z_score) > 3
)
业务规则检测
统计方法找到的"异常"未必是业务异常,最终应结合领域知识:
df %>%
mutate(
age_invalid = age < 18 | age > 65, # 工作年龄范围
salary_invalid = salary < 3000 | salary > 100000, # 合理薪资区间
date_future = reg_date > Sys.Date() # 未来注册日期
) %>%
filter(age_invalid | salary_invalid | date_future) # 查看所有异常记录
三种方法对比:
| 方法 | 原理 | 适用场景 | 局限 |
|---|---|---|---|
| IQR | 四分位距 | 偏斜分布、有离群点 | 不依赖正态假设 |
| Z-score | 标准差倍数 | 近似正态分布 | 对极端值敏感 |
| 业务规则 | 领域知识 | 有明确业务约束 | 需要先验知识 |
数据重塑:pivot_longer 与 pivot_wider
宽格式转长格式:pivot_longer
library(tidyr)
# 宽格式:每个科目是一列
df_wide <- data.frame(
name = c("张三", "李四"),
math = c(85, 90),
english = c(88, 92),
science = c(78, 85)
)
# 转为长格式:每行一个观测值
df_long <- df_wide %>%
pivot_longer(
cols = c(math, english, science), # 要转换的列
names_to = "subject", # 原列名变成新列
values_to = "score" # 原值变成新列
)
# name subject score
# 1 张三 math 85
# 2 张三 english 88
# ...
长格式转宽格式:pivot_wider
# 逆操作:长格式变回宽格式
df_back_wide <- df_long %>%
pivot_wider(
names_from = "subject", # 哪列的值变成新列名
values_from = "score" # 哪列的值填入格子
)
note 旧版 tidyr 使用 `gather()`(宽转长)和 `spread()`(长转宽),这两个函数已被 `pivot_longer` 和 `pivot_wider` 取代,但仍可用。新代码应优先使用 pivot 系列。
常见重塑场景
# 月度销售数据(宽格式)→ 适合可视化的长格式
sales_wide <- data.frame(
product = c("A", "B"),
jan = c(100, 200), feb = c(120, 180), mar = c(140, 220)
)
sales_long <- sales_wide %>%
pivot_longer(-product, names_to = "month", values_to = "sales")
# 调查问卷(多个选项在多列)→ 分析用长格式
survey <- data.frame(
id = 1:3,
q1_ans = c("满意","不满意","满意"),
q2_ans = c("一般","满意","非常满意")
)
survey %>% pivot_longer(-id, names_to = "question", values_to = "answer")
列重命名
library(dplyr)
df <- data.frame(Name = 1, AGE = 2, Salary.2024 = 3)
# rename():指定改哪几列(新名 = 旧名)
df %>% rename(name = Name, age = AGE)
# rename_with():用函数批量重命名
df %>% rename_with(tolower) # 全部转小写
df %>% rename_with(~ gsub("\\.", "_", .x)) # 点替换为下划线
# 直接赋值 names()
names(df) <- c("name", "age", "salary_2024")
# janitor::clean_names() 一键规范化(推荐)
library(janitor)
df %>% clean_names() # 转小写、空格替换为_、去特殊字符
数据验证:assertr
assertr 提供管道友好的数据断言,适合在数据管道的关键节点进行质量门控。
# install.packages("assertr")
library(assertr)
library(dplyr)
df <- data.frame(
id = c(1, 2, 3, NA),
age = c(25, 30, -5, 28),
salary = c(5000, 6000, 7000, 8000)
)
# verify():整体条件断言
df %>%
verify(nrow(.) > 0) %>% # 不能是空数据框
verify(ncol(.) == 3) # 本示例必须有3列
# assert():逐元素条件断言;本例 id 含 NA,断言会失败并终止管道,后续 age 断言不会执行
df %>%
assert(not_na, id) %>% # id 列不允许 NA
assert(within_bounds(18, 65), age) # age 在 18-65 之间
# insist():基于统计属性的断言
df %>%
insist(within_n_sds(3), salary) # salary 不超出均值±3SD
# 自定义断言规则;本例 age 含负数,以下断言会失败
no_negative <- function(x) x >= 0
df %>% assert(no_negative, age, salary)
tip assertr 适合放在数据管道的进口和出口:进口验证输入数据符合预期,出口验证清洗结果满足质量标准。一旦断言失败,立即报错终止,防止带病数据流入分析。
完整清洗流程示例
以下示例模拟从一份原始"脏数据"到可分析数据框的完整管道:
library(dplyr)
library(tidyr)
library(stringr)
library(lubridate)
library(janitor)
library(assertr)
# ─── 1. 原始脏数据 ───────────────────────────────────
raw <- data.frame(
`User ID` = c("U001", "U002", "U003", "U002", "U004"), # 有重复
`User Name` = c(" 张三 ", "李四", NA, "李四", "王五"), # 有空格、有缺失
Age = c("28", "thirty", "25", "30", "22"), # 有非数值
Salary = c("¥5,000", "6000元", "7500", NA, "250000"), # 格式混乱、有异常值
Reg_Date = c("2024-01-15", "2024/02/20", "15-03-2024", "2024-02-20", NA),
stringsAsFactors = FALSE,
check.names = FALSE
)
# ─── 2. 列名标准化 ────────────────────────────────────
df <- raw %>%
clean_names()
# user_id, user_name, age, salary, reg_date
# ─── 3. 字符串清洗 ────────────────────────────────────
df <- df %>%
mutate(
user_name = str_trim(user_name), # 去除前后空格
user_name = na_if(user_name, "") # 空字符串转 NA
)
# ─── 4. 类型转换 ──────────────────────────────────────
df <- df %>%
mutate(
# 年龄:去非数字字符后转整数
age = suppressWarnings(as.integer(str_extract(age, "\\d+"))),
# 薪资:去货币符号和千分位后转数值
salary = suppressWarnings(
as.numeric(str_replace_all(salary, "[¥元,\\s]", ""))
),
# 日期:支持多种格式
reg_date = parse_date_time(
reg_date,
orders = c("Ymd", "Y/m/d", "d-m-Y"),
quiet = TRUE
) %>% as.Date()
)
# ─── 5. 缺失值处理 ────────────────────────────────────
df <- df %>%
mutate(
age = if_else(is.na(age), as.integer(median(age, na.rm=TRUE)), age),
salary = if_else(is.na(salary), median(salary, na.rm=TRUE), salary),
user_name = if_else(is.na(user_name), paste0("用户_", str_sub(user_id, -3)), user_name)
)
# ─── 6. 去重 ──────────────────────────────────────────
df <- df %>%
distinct(user_id, .keep_all = TRUE) # 保留每个 user_id 的第一条记录
# ─── 7. 异常值处理 ────────────────────────────────────
df <- df %>%
mutate(
age_flag = age < 18 | age > 65,
salary_flag = salary > 100000,
age = if_else(age_flag, as.integer(median(age, na.rm=TRUE)), age),
salary = if_else(salary_flag, median(salary, na.rm=TRUE), salary)
) %>%
select(-age_flag, -salary_flag) # 删除临时标记列
# ─── 8. 数据验证 ──────────────────────────────────────
df_validated <- df %>%
verify(nrow(.) > 0) %>%
assert(not_na, user_id, user_name) %>%
assert(within_bounds(18, 65), age) %>%
assert(within_bounds(1000, 100000), salary)
# ─── 9. 结果输出 ──────────────────────────────────────
cat("清洗完成:\n")
cat(sprintf(" 原始行数:%d 清洗后:%d\n", nrow(raw), nrow(df_validated)))
cat(sprintf(" NA 数量:%d\n", sum(is.na(df_validated))))
print(df_validated)
note 生产管道中应在每一步之后记录日志(行数变化、NA 变化),便于追溯问题。可以将上面每个步骤封装为独立函数,用 `%>%` 串联,使代码可测试、可复用。
掌握检查
完成本文后,请验证自己能否回答以下问题:
- [ ]
na.omit(df)和drop_na(df, col_a)的行为有何区别? - [ ]
str_trim()和str_squish()分别适合处理哪类问题? - [ ]
ymd("2024-03-15")返回的是什么类型?与as.Date("2024-03-15")有何差异? - [ ] IQR 方法和 Z-score 方法在什么情况下应该优先选哪个?
- [ ]
pivot_longer和pivot_wider各自把数据变成了什么形状?
练习题
题一(基础):创建一个包含 20 行的"脏"数据框,字段包括:name(含空格和 NA)、phone(含格式如 138-0000-1234 和 13800001234 两种格式,均为练习示例)、salary(含 ¥ 符号和千分位逗号)。完成:
- 清洗 name 列(去空格、NA 标记为"未知");
- 用 stringr 正则把 phone 统一为 11 位纯数字;
- 把 salary 转为 numeric 类型。
题二(进阶):有一份宽格式的月度销售数据(产品 × 月份),要求:
- 用
pivot_longer转为长格式; - 用 lubridate 解析月份字符串为 Date 类型;
- 用
assertr::assert验证所有销售额 > 0; - 用 IQR 方法检测并标记异常值;
- 输出清洗后的宽格式数据。
关联文档:R数据结构 | 数据导入导出 | dplyr数据操作
创建时间:2024-12-11 | 更新时间:2026-10-09 预计学习时长:90-120分钟
#R语言 #数据清洗 #数据质量 #缺失值处理 #异常值检测 #stringr #lubridate #tidyr #assertr #数据预处理