R数据分析实战:用dplyr的case_when处理缺失值(NA)和复杂业务规则
在真实业务数据分析中,我们经常遇到数据不完整或需要根据复杂规则创建新变量的场景。想象一下这样的情境:你正在处理一份学生成绩数据,其中部分科目成绩缺失,而学校要求根据各科成绩的标准化值计算综合评分,最后按照特定比例划分等级。这种需求几乎每天都会出现在数据分析师的工作中。
dplyr
包的
case_when()
函数就像一把瑞士军刀,能够优雅地处理这类多条件判断任务。与传统的嵌套
ifelse
相比,它的语法更清晰、可读性更强,特别适合处理包含缺失值和复杂业务逻辑的场景。本文将带你从实战角度,探索如何用
case_when()
解决真实数据分析中的棘手问题。
1. 理解case_when的核心机制
case_when()
的工作原理类似于编程语言中的
switch-case
语句,但专为数据操作优化。它按顺序评估每个条件,一旦某个条件为真,就返回对应的结果,并且不再检查后续条件。这种特性使得条件顺序的安排变得至关重要。
让我们先看一个基础示例,创建一个简单的成绩等级划分:
library(dplyr)
students <- data.frame(
name = c("Alice", "Bob", "Charlie", "David"),
score = c(92, 76, 58, NA)
)
students %>%
mutate(grade = case_when(
score >= 90 ~ "A",
score >= 80 ~ "B",
score >= 70 ~ "C",
score >= 60 ~ "D",
is.na(score) ~ "Missing",
TRUE ~ "F" # 相当于else
))
这里有几个关键点需要注意:
-
条件顺序
:我们把最高分条件放在前面,因为
case_when()会按顺序评估 -
NA处理
:专门用
is.na()处理缺失值,避免意外结果 -
默认情况
:
TRUE ~ "F"捕获所有未匹配的情况
实际业务中,我们经常会遇到更复杂的条件组合。比如电商平台需要根据用户购买金额、频率和最近购买时间计算客户价值等级。
2. 处理缺失值的专业策略
缺失值处理是数据分析中最常见的挑战之一。
case_when()
配合
is.na()
可以构建精细的缺失值处理逻辑。以下是几种典型场景的处理方法:
2.1 优先识别缺失值
# 不安全的写法 - NA可能意外落入其他条件
df %>%
mutate(status = case_when(
value > 100 ~ "High",
value > 50 ~ "Medium",
TRUE ~ "Low"
))
# 安全的写法 - 先处理NA
df %>%
mutate(status = case_when(
is.na(value) ~ "Missing",
value > 100 ~ "High",
value > 50 ~ "Medium",
TRUE ~ "Low"
))
2.2 多列缺失值处理
当需要同时考虑多列的缺失情况时:
customer_data %>%
mutate(risk_category = case_when(
is.na(income) & is.na(credit_score) ~ "High Risk",
is.na(income) | is.na(credit_score) ~ "Medium Risk",
income < 30000 & credit_score < 600 ~ "Caution",
TRUE ~ "Low Risk"
))
2.3 缺失值替代策略
有时我们不想简单标记缺失,而是用合理值替代:
sales_data %>%
mutate(
adjusted_sales = case_when(
is.na(q1) ~ (q2 + q3 + q4) / 3,
is.na(q2) ~ (q1 + q3 + q4) / 3,
TRUE ~ (q1 + q2 + q3 + q4) / 4
)
)
提示:在金融或医疗领域,缺失值处理通常有严格的业务规范,务必了解清楚再实施。
3. 构建复杂业务规则的实战技巧
真实业务规则往往涉及多列判断和特殊条件。让我们通过一个电商用户分层的完整案例来演示。
3.1 数据准备
假设我们有用户行为数据:
users <- tibble(
user_id = 1:6,
last_purchase = as.Date(c("2023-01-15", "2022-11-20", NA, "2023-02-28", "2021-12-10", "2023-03-05")),
total_orders = c(15, 8, 0, 22, 3, NA),
avg_spend = c(120, 85, NA, 150, 45, 200)
)
3.2 多条件用户分层
user_segments <- users %>%
mutate(
segment = case_when(
# 先处理数据质量问题
is.na(last_purchase) ~ "Inactive",
is.na(total_orders) | is.na(avg_spend) ~ "Data Issue",
# 活跃高价值用户
last_purchase > Sys.Date() - 90 &
total_orders > 10 &
avg_spend > 100 ~ "Premium",
# 活跃但价值中等
last_purchase > Sys.Date() - 90 &
(total_orders > 5 | avg_spend > 80) ~ "Regular",
# 休眠用户
last_purchase < Sys.Date() - 365 ~ "Dormant",
# 其他情况
TRUE ~ "Casual"
)
)
3.3 条件顺序的重要性
调整条件顺序可能完全改变结果。例如:
# 版本A
case_when(
x > 5 ~ "High",
x > 3 ~ "Medium",
TRUE ~ "Low"
)
# 版本B
case_when(
x > 3 ~ "Medium",
x > 5 ~ "High",
TRUE ~ "Low"
)
在版本B中,"High"类别永远不会被匹配,因为所有x>5的情况都已经被x>3捕获。
4. 综合案例:学生成绩标准化与评级系统
让我们回到最初提到的学生成绩案例,实现一个更完整的解决方案:
# 准备数据
students <- tibble(
name = c("Alice", "Bob", "Charlie", "David", "Eve"),
math = c(88, 76, NA, 92, 65),
science = c(92, 85, 78, NA, 72),
english = c(NA, 82, 75, 88, 68)
)
# 标准化函数
scale_na <- function(x) {
(x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}
# 完整处理流程
student_results <- students %>%
mutate(across(c(math, science, english), scale_na, .names = "{.col}_z")) %>%
rowwise() %>%
mutate(
avg_z = mean(c(math_z, science_z, english_z), na.rm = TRUE),
missing_subjects = sum(is.na(c(math, science, english)))
) %>%
ungroup() %>%
mutate(
grade = case_when(
missing_subjects >= 2 ~ "Incomplete",
avg_z > 1 ~ "A",
avg_z > 0 ~ "B",
avg_z > -1 ~ "C",
avg_z > -2 ~ "D",
TRUE ~ "F"
),
grade = if_else(missing_subjects == 1, paste0(grade, "*"), grade)
)
这个解决方案有几个亮点:
- 自定义标准化函数 :处理包含NA的标准化
- 缺失科目计数 :作为评级考虑因素
- 双重判断 :先检查缺失情况,再评分
- 特殊标记 :用星号标注有科目缺失的学生
注意:rowwise()操作在大数据集中可能较慢,实际项目中可以考虑使用purrr的map函数或data.table实现。
5. 性能优化与替代方案
虽然
case_when
非常强大,但在某些场景下可能需要考虑替代方案:
5.1 大数据集处理
对于千万级以上的数据,可以考虑:
# data.table方案
library(data.table)
setDT(df)[, grade := fcase(
score >= 90, "A",
score >= 80, "B",
score >= 70, "C",
is.na(score), "Missing",
default = "F"
)]
5.2 复杂逻辑拆分
当条件特别复杂时,可以分步处理:
df <- df %>%
mutate(
temp_group = case_when(
condition1 ~ 1,
condition2 ~ 2,
TRUE ~ 3
)
) %>%
mutate(
final_result = case_when(
temp_group == 1 & condition3 ~ "OptionA",
temp_group == 2 ~ "OptionB",
TRUE ~ "OptionC"
)
) %>%
select(-temp_group)
5.3 与其它dplyr动词结合
case_when
可以与
across()
等结合,实现批量条件处理:
df %>%
mutate(across(
starts_with("test"),
~ case_when(
is.na(.) ~ "Missing",
. > 90 ~ "Excellent",
. > 75 ~ "Good",
TRUE ~ "Needs Improvement"
),
.names = "{.col}_eval"
))
6. 调试与错误排查
即使对经验丰富的R用户,复杂的
case_when
语句也可能出现意外行为。以下是几个实用技巧:
常见错误1:类型不一致
# 错误:结果类型不一致
df %>%
mutate(status = case_when(
x > 10 ~ "High",
x > 5 ~ 2, # 数字与字符混合
TRUE ~ "Low"
))
# 修正:统一返回类型
df %>%
mutate(status = case_when(
x > 10 ~ "High",
x > 5 ~ "2", # 全部转为字符
TRUE ~ "Low"
))
常见错误2:NA处理遗漏
# 意外行为
df %>%
mutate(category = case_when(
x > 5 ~ "A",
y < 3 ~ "B",
TRUE ~ "C" # NA会落入这里
))
# 更好的做法
df %>%
mutate(category = case_when(
is.na(x) | is.na(y) ~ "Unknown",
x > 5 ~ "A",
y < 3 ~ "B",
TRUE ~ "C"
))
调试技巧:逐步验证
# 添加临时列检查条件
df %>%
mutate(
cond1 = x > 5,
cond2 = y < 3,
final = case_when(
cond1 ~ "A",
cond2 ~ "B",
TRUE ~ "C"
)
) %>%
View() # 交互式检查
在实际项目中,我经常遇到条件顺序导致的bug。有次在客户分群时,把"高净值客户"条件放在了"普通客户"条件之后,导致所有客户都被分类为普通客户。这个教训让我养成了总是先写最严格条件的习惯。

1132

被折叠的 条评论
为什么被折叠?



