写在前面
本系列推文为《R for Data Science (2)》的中文翻译版本。所有内容都通过开源免费的方式上传至Github,欢迎大家参与贡献,详细信息见:
Books-zh-cn 项目介绍:
Books-zh-cn:开源免费的中文书籍社区
r4ds-zh-cn Github 地址:
https://github.com/Books-zh-cn/r4ds-zh-cn
r4ds-zh-cn 网站地址:
https://books-zh-cn.github.io/r4ds-zh-cn/
目录
-
12.4 汇总
-
12.5 条件转换
-
12.6 总结
12.4 汇总
以下部分将介绍几种用于汇总逻辑向量的实用技巧。 除了专门针对逻辑向量的函数外,您也可以使用适用于数值向量的函数。
12.4.1 逻辑汇总
逻辑向量主要有两种汇总方式:any() 和 all()。any(x) 相当于 |,只要 x 中存在任意 TRUE 就会返回 TRUE。all(x) 相当于 &,只有当 x 中所有值都为 TRUE 时才会返回 TRUE。 与所有汇总函数一样,若存在缺失值它们会返回 NA,而通过设置 na.rm = TRUE 可忽略缺失值。
例如,我们可以用 all() 和 any() 判断是否所有航班的起飞延误都不超过一小时,或者是否存在到达延误五小时以上的航班。 结合 group_by() 还能实现按日统计:
flights |>
group_by(year, month, day) |>
summarize(
all_delayed = all(dep_delay <= 60, na.rm = TRUE),
any_long_delay = any(arr_delay >= 300, na.rm = TRUE),
.groups = "drop"
)
#> # A tibble: 365 × 5
#> year month day all_delayed any_long_delay
#> <int> <int> <int> <lgl> <lgl>
#> 1 2013 1 1 FALSE TRUE
#> 2 2013 1 2 FALSE TRUE
#> 3 2013 1 3 FALSE FALSE
#> 4 2013 1 4 FALSE FALSE
#> 5 2013 1 5 FALSE TRUE
#> 6 2013 1 6 FALSE FALSE
#> # ℹ 359 more rows
不过在大多数情况下,any() 和 all() 显得过于笼统,我们往往需要更详细地了解 TRUE 和 FALSE 的具体数量。 这就引出了数值型汇总方法。
12.4.2 逻辑向量的数值总结
当在数值型语境中使用逻辑向量时,TRUE 会转换为 1,FALSE 会转换为 0。 这使得 sum() 和 mean() 在处理逻辑向量时特别有用:sum(x) 给出 TRUE 的数量,而 mean(x) 则给出 TRUE 的比例(因为 mean() 就是 sum() 除以 length())。
例如,通过这种方式,我们可以计算起飞延误不超过一小时的航班比例,以及到达延误五小时以上的航班数量:
flights |>
group_by(year, month, day) |>
summarize(
all_delayed = mean(dep_delay <= 60, na.rm = TRUE),
any_long_delay = sum(arr_delay >= 300, na.rm = TRUE),
.groups = "drop"
)
#> # A tibble: 365 × 5
#> year month day all_delayed any_long_delay
#> <int> <int> <int> <dbl> <int>
#> 1 2013 1 1 0.939 3
#> 2 2013 1 2 0.914 3
#> 3 2013 1 3 0.941 0
#> 4 2013 1 4 0.953 0
#> 5 2013 1 5 0.964 1
#> 6 2013 1 6 0.959 0
#> # ℹ 359 more rows
12.4.3 逻辑子集
逻辑向量在数据汇总中还有一个最终用途:您可以使用逻辑向量对单个变量进行子集筛选,从而聚焦感兴趣的数据子集。 这里需要用到基础的 [(称为"子集")操作符,Section 27.2 将对此进行更详细的讲解。
假设我们只想分析实际发生延误的航班的平均延误时间。 一种实现方法是先筛选出延误航班,再计算平均延误:
flights |>
filter(arr_delay > 0) |>
group_by(year, month, day) |>
summarize(
behind = mean(arr_delay),
n = n(),
.groups = "drop"
)
#> # A tibble: 365 × 5
#> year month day behind n
#> <int> <int> <int> <dbl> <int>
#> 1 2013 1 1 32.5 461
#> 2 2013 1 2 32.0 535
#> 3 2013 1 3 27.7 460
#> 4 2013 1 4 28.3 297
#> 5 2013 1 5 22.6 238
#> 6 2013 1 6 24.4 381
#> # ℹ 359 more rows
这种方法虽然可行,但如果我们还想计算提前抵达航班的平均延误时间呢? 那就需要额外执行一次筛选步骤,再想办法合并两个数据框。 其实您可以直接使用 [ 进行内联筛选:arr_delay[arr_delay > 0] 将仅返回正的到达延误值。
由此可得:
flights |>
group_by(year, month, day) |>
summarize(
behind = mean(arr_delay[arr_delay > 0], na.rm = TRUE),
ahead = mean(arr_delay[arr_delay < 0], na.rm = TRUE),
n = n(),
.groups = "drop"
)
#> # A tibble: 365 × 6
#> year month day behind ahead n
#> <int> <int> <int> <dbl> <dbl> <int>
#> 1 2013 1 1 32.5 -12.5 842
#> 2 2013 1 2 32.0 -14.3 943
#> 3 2013 1 3 27.7 -18.2 914
#> 4 2013 1 4 28.3 -17.0 915
#> 5 2013 1 5 22.6 -14.0 720
#> 6 2013 1 6 24.4 -13.6 832
#> # ℹ 359 more rows
还需注意分组规模的差异:第一段代码中 n() 统计的是每日延误航班数;第二段中 n() 统计的则是航班总数。
12.4.4 练习
-
sum(is.na(x))会返回什么结果?mean(is.na(x))呢? -
当对逻辑向量使用
prod()时返回什么?它等价于哪个逻辑汇总函数?对逻辑向量使用min()时返回什么?它等价于哪个逻辑汇总函数?阅读文档并进行一些实验验证。
12.5 条件转换
逻辑向量最强大的功能之一是用于条件转换,即对满足条件 x 的情况执行一种操作,对满足条件 y 的情况执行另一种操作。 这需要用到两个重要函数:if_else() 和 case_when()。
12.5.1 if_else()
当需要为 TRUE 条件返回一个值,为 FALSE 条件返回另一个值时,可以使用 dplyr::if_else()。if_else() 前三个参数是必须的。 第一个参数 condition 是逻辑向量;第二个参数 true 指定条件为真时的返回值;第三个参数 false 指定条件为假时的返回值。
从一个简单示例开始,我们将数字向量标记为"+ve"(正数)或"-ve"(负数):
x <- c(-3:3, NA)
if_else(x > 0, "+ve", "-ve")
#> [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" NA
还有一个可选的第四个参数,missing 用于处理输入为 NA 的情况:
if_else(x > 0, "+ve", "-ve", "???")
#> [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" "???"
你还可以为 true 和 false 参数使用向量。 例如,这样可以创建一个最小化的 abs() 实现:
if_else(x < 0, -x, x)
#> [1] 3 2 1 0 1 2 3 NA
目前所有参数都使用了相同的向量,但你当然可以混合搭配。 例如,可以实现一个简单版的 coalesce() 如下:
x1 <- c(NA, 1, 2, NA)
y1 <- c(3, NA, 4, 6)
if_else(is.na(x1), y1, x1)
#> [1] 3 1 2 6
你可能已经注意到我们上面的标签示例有一个小问题:零既不是正数也不是负数。 我们可以通过增加一个额外的 if_else() 来解决这个问题:
if_else(x == 0, "0", if_else(x < 0, "-ve", "+ve"), "???")
#> [1] "-ve" "-ve" "-ve" "0" "+ve" "+ve" "+ve" "???"
这样的代码已经开始变得难以阅读,若条件增多会更加混乱。 此时可以改用 dplyr::case_when()。
12.5.2 case_when()
dplyr 的 case_when() 借鉴了 SQL 的 CASE 语句,能够灵活地为不同条件执行不同运算。 其特殊语法与 tidyverse 其他函数风格迥异。 它采用 condition ~ output 的配对形式。condition 必须是逻辑向量;当为 TRUE 时,对应的 output 就会被采用。
通过这种方式,我们可以重构之前嵌套的 if_else() 代码如下:
x <- c(-3:3, NA)
case_when(
x == 0 ~ "0",
x < 0 ~ "-ve",
x > 0 ~ "+ve",
is.na(x) ~ "???"
)
#> [1] "-ve" "-ve" "-ve" "0" "+ve" "+ve" "+ve" "???"
虽然代码量增加了,但表达更加清晰明确。
为了说明 case_when() 的工作原理,我们先看几个简单例子。 当所有条件都不匹配时,输出结果会返回 NA:
case_when(
x < 0 ~ "-ve",
x > 0 ~ "+ve"
)
#> [1] "-ve" "-ve" "-ve" NA "+ve" "+ve" "+ve" NA
如果想设置默认值/兜底选项,可在左侧使用 TRUE:
case_when(
x < 0 ~ "-ve",
x > 0 ~ "+ve",
TRUE ~ "???"
)
#> [1] "-ve" "-ve" "-ve" "???" "+ve" "+ve" "+ve" "???"
需注意,若多个条件同时匹配,只会采用第一个匹配项:
case_when(
x > 0 ~ "+ve",
x > 2 ~ "big"
)
#> [1] NA NA NA NA "+ve" "+ve" "+ve" NA
与 if_else() 类似,你可以在 ~ 两侧使用变量,并根据问题需要自由组合变量。 例如,我们可以用 case_when() 为到达延误生成更易懂的标签:
flights |>
mutate(
status = case_when(
is.na(arr_delay) ~ "cancelled",
arr_delay < -30 ~ "very early",
arr_delay < -15 ~ "early",
abs(arr_delay) <= 15 ~ "on time",
arr_delay < 60 ~ "late",
arr_delay < Inf ~ "very late",
),
.keep = "used"
)
#> # A tibble: 336,776 × 2
#> arr_delay status
#> <dbl> <chr>
#> 1 11 on time
#> 2 20 late
#> 3 33 late
#> 4 -18 early
#> 5 -25 early
#> 6 12 on time
#> # ℹ 336,770 more rows
编写这类复杂的 case_when() 语句时需要格外谨慎;我最开始两次尝试混用了 < 和 > 运算符,结果不小心创建了相互重叠的条件判断。
12.5.3 兼容类型
请注意,if_else() 和 case_when() 都要求输出值的类型必须兼容。 如果类型不兼容,就会出现如下错误提示:
if_else(TRUE, "a", 1)
#> Error in `if_else()`:
#> ! Can't combine `true` <character> and `false` <double>.
case_when(
x < -1 ~ TRUE,
x > 0 ~ now()
)
#> Error in `case_when()`:
#> ! Can't combine `..1 (right)` <logical> and `..2 (right)` <datetime<local>>.
总体而言,兼容的类型相对较少,因为自动转换向量类型常会导致错误。 以下是几种最重要的兼容情况:
-
数值型与逻辑型向量兼容,如
Section 12.4.2所述。 -
字符串与因子兼容(
Chapter 16),因为因子本质是具有限定取值范围的字符串 -
Dates 与 date-times,我们将在
Chapter 17讨论,因为 date 是 date-time 的特殊形式 -
NA,技术上属于逻辑向量,与所有类型兼容,因为所有向量都有表示缺失值的方式。
这些规则无需死记硬背,随着对 tidyverse 的持续使用会自然掌握,因为它们在整套工具中保持高度一致性。
12.5.4 练习
-
判断一个数是否为偶数可通过
x %% 2 == 0实现。 请用此方法结合if_else()判断 0 到 20 之间每个数字的奇偶性。 -
给定日期向量如
x <- c("Monday", "Saturday", "Wednesday"),使用ifelse()标注周末或工作日。 -
用
ifelse()计算数值向量x的绝对值。 -
编写
case_when()语句,根据flights数据集的month和day列标注美国主要节日(如元旦、独立日、感恩节、圣诞节)。 先创建逻辑列,TRUE或FALSE,再创建字符列,节日名称或NA。
12.6 总结
逻辑向量的定义很简单,因为每个值只能是 TRUE、FALSE 或 NA。 但逻辑向量却蕴含着巨大能量。 本章我们学习了如何用 >, <, <=, =>, ==, !=, is.na() 创建逻辑向量,如何用 !, &, | 进行组合运算,以及如何用 any(), all(), sum(), mean() 进行汇总。 你还掌握了能根据逻辑向量返回不同值的强大函数 if_else() 和 case_when()。
后续章节我们会反复见到逻辑向量。 例如在 Chapter 14 你将学习 str_detect(x, pattern),它会对 x 中符合模式的元素返回 TRUE,在 Chapter 17 你将通过日期时间比较生成逻辑向量。 但现在,我们将转向另一种重要向量类型:数值向量。
-
R 通常会自动调用 print 函数(即,
x相当于print(x)的简写),但如果你想传入其他参数,显式调用会更方便。 -
也就是说,当 x 为 true 或 y 为 true,但不同时为 true 时,
xor(x, y)返回 true。 这正符合我们日常英语中使用 "or" 的方式。 "Both" 通常不是一个可接受的回答,对于"您想要冰淇淋还是蛋糕?"这个问题。 -
我们将在 @sec-joins 中详细讲解这一点。
-
dplyr 的
if_else()与 base R 的ifelse()非常相似。if_else()相比ifelse()有两大优势:可以自定义缺失值的处理方式,且在变量类型不兼容时更可能给出有意义的错误提示。
--------------- 本章结束 ---------------
本期翻译贡献:
@TigerZ生信宝库
注:本文已开启快捷转载,欢迎大家转载,只需标明文章出处即可。