R dplyr:如何在分组汇总中使用自定义函数计算升学率?
可以把 progressionby19() 改为接收列向量并返回单个数值的函数,再在 summarise() 中传入各列。dplyr 会将当前组的数据传给函数,算出每个 Type 对应的升学率,无需在函数内部使用 do()。
原代码的问题在哪里?
在下面的写法中:
example %>%
group_by(Type) %>%
summarise(
learner_count = n_distinct(Student.ID),
progression_rate = progressionby19(.)
)
Student.ID 是 summarise() 当前组中的列,而 %>% 的占位符 . 指向传入管道的整个分组数据对象,不会自动变成当前组的数据。两个表达式使用的数据范围因此不同。
原函数中的 total 和 progressed 都是 summarise() 返回的表格,(progressed / total) * 100 的结果也是表格,而非单个数值。如果传入的数据仍带有分组属性,内部汇总也会受这些分组影响。
新增的 do() 分支会再次对整个传入对象执行分组计算,返回一张结果表,无法让结果自然对应外层 summarise() 当前正在计算的组。由于没有具体报错信息,无法确定原先触发的是哪一种错误,但数据范围和返回类型都需要调整。
改成接收列向量的函数
以下代码使用问题中已定义的 example:
library(dplyr)
progressionby19 <- function(student_id, attended, entry) {
participated <- attended %in% "Yes"
total <- n_distinct(
student_id[participated],
na.rm = TRUE
)
progressed <- n_distinct(
student_id[participated & (entry %in% "Yes")],
na.rm = TRUE
)
if (total == 0L) {
return(NA_real_)
}
100 * progressed / total
}
result <- example %>%
group_by(Type) %>%
summarise(
learner_count = n_distinct(
Student.ID[Student.Attended %in% "Yes"],
na.rm = TRUE
),
progression_rate = progressionby19(
Student.ID,
Student.Attended,
entryby19
)
) %>%
ungroup()
result
函数的计算口径是:
- 分母:当前组中,存在
Student.Attended == "Yes"记录的不同学生人数。 - 分子:当前组中,存在同一条记录同时满足
Student.Attended == "Yes"和entryby19 == "Yes"的不同学生人数。 - 升学率:分子除以分母,再乘以
100。
n_distinct() 避免同一名学生的重复记录增加人数。na.rm = TRUE 明确排除缺失的学生 ID;%in% "Yes" 则只把明确标记为 "Yes" 的记录纳入相应条件。
示例应该得到什么结果?
根据给定数据,结果应为:
| Type | learner_count | progression_rate |
|---|---|---|
| Exhibition | 2 | 100 |
| Mentoring | 2 | 50 |
Exhibition 中有两名参加者,两人均进入 HE,升学率为 100%。
Mentoring 中,#003 没有参加活动,需要排除。参加者为 #004 和 #005,其中只有 #004 进入 HE,升学率为 50%。
如果需要计算整体升学率,可以直接传入完整列:
progressionby19(
example$Student.ID,
example$Student.Attended,
example$entryby19
)
# 按给定数据计算,结果应为 75
人数口径和边界情况
上面的 learner_count 统计参加活动的学生人数,与升学率的分母一致。原代码中的 n_distinct(Student.ID) 会统计组内所有学生,按原始写法,Mentoring 的人数是 3。如果需要保留这个口径,可以将该字段改回原表达式,并用名称区分总人数与参加人数。
当某组没有有效参加者时,函数返回 NA_real_,表示该组没有可用的升学率,避免把没有分母的情况解释成 0%。由于没有提前删除未参加记录,这类组仍会出现在结果中。
如果同一名学生属于多个 Type,他会在每个符合条件的组中各计一次。如果同一学生的多条记录存在状态冲突,当前函数沿用原代码的筛选逻辑。若业务要求按最终状态或最新记录计算,需要先明确记录选择规则,再进行汇总。
备注:内容仅供参考。
