R语言的逻辑向量

https://wp.me/p80aHo-2Ef

引言

在这一章中,你将学习处理逻辑向量的工具。逻辑向量是最简单的向量类型,因为每个元素只能取三种可能的值之一:TRUE、FALSE 和 NA。在你的原始数据中,逻辑向量相对少见,但在几乎每一次分析过程中,你都会创建和操作它们。

我们将从讨论创建逻辑向量最常见的方法开始:使用数值比较。然后你将学习如何使用布尔代数来组合不同的逻辑向量,以及一些有用的汇总方法。最后,我们会介绍 if_else() 和 case_when(),这两个有用的函数可以借助逻辑向量进行条件变更。

先修知识

随着我们开始介绍更多工具,并不总能找到一个完美的真实示例。因此,我们将开始使用 c() 来虚构一些示例数据:

x <- c(1, 2, 3, 5, 7, 11, 13)
x * 2
#> [1] 2 4 6 10 14 22 26

这会更容易解释各个函数,但代价是更难看出它如何应用到你的数据问题中。只需记住,我们对一个独立向量所做的任何操作,你都可以借助 mutate() 及其相关函数对数据框中的变量进行相同的处理。

library(dplyr)
df <- tibble(x)
df |>
mutate(y = x * 2)

比较

创建逻辑向量的一种非常常见的方法,是使用 <、<=、>、>=、!= 和 == 进行数值比较。到目前为止,我们主要是在 filter() 中临时创建逻辑变量——它们被计算、使用,然后就被丢弃了。例如,下面的 filter() 会找出所有大致准时到达的白天出发航班:

flights |>
filter(dep_time > 600 & dep_time < 2000 & abs(arr_delay) < 20)

了解这一点很有用:这是一种快捷方式,而你也可以使用 mutate() 显式创建底层的逻辑变量:

flights |>
mutate(
daytime = dep_time > 600 & dep_time < 2000, # 判断是否为白天出发:起飞时间在 06:00 到 20:00 之间
approx_ontime = abs(arr_delay) < 20, # 判断是否大致准点:到达延误绝对值小于 20 分钟
.keep = "used" # 只保留本次计算中用到的列
)

这对于更复杂的逻辑尤其有用,因为为中间步骤命名,不仅能让代码更易读,也更容易检查每一步是否都已正确计算。

总的来说,最初的 filter() 等同于:

flights |>
mutate(
daytime = dep_time > 600 & dep_time < 2000, # 判断是否为白天出发:起飞时间在 06:00 到 20:00 之间
approx_ontime = abs(arr_delay) < 20, # 判断是否大致准点:到达延误绝对值小于 20 分钟
) |>
filter(daytime & approx_ontime) # 筛选出同时满足“白天出发”和“大致准点”的航班

浮点数比较

注意不要对数字使用 ==。例如,看起来这个向量包含数字 1 和 2:

x <- c(1 / 49 * 49, sqrt(2) ^ 2) # 计算两个看似应为 1 和 2 的浮点数表达式,可能因精度问题得到近似值
x

但如果你测试它们是否相等,结果会是 FALSE:

x == c(1, 2)

怎么回事?计算机用固定数量的小数位来存储数字,因此无法精确表示 1/49 或 sqrt(2),后续计算会有非常轻微的偏差。我们可以通过将 digits 参数传给 print() 来查看精确值:

print(x, digits = 16)

既然你已经看到 == 为什么会失效,那该怎么办呢?一个选择是使用 dplyr::near(),它会忽略微小差异:

library(dplyr)
near(x, c(1, 2))

缺失值

缺失值表示未知,因此它们具有“传染性”:几乎任何涉及未知值的操作结果也会是未知:

NA > 5
10 == NA

最令人困惑的结果是这一条:

NA == NA

如果我们人为提供一点更多上下文,就最容易理解为什么这是真的:

# We don't know how old Mary is
age_mary <- NA
# We don't know how old John is
age_john <- NA
# Are Mary and John the same age?
age_mary == age_john

所以,如果你想找出所有 dep_time 缺失的航班,下面的代码不会起作用,因为 dep_time == NA 会对每一行都返回 NA,而 filter() 会自动丢弃缺失值:

library(nycflights13)
flights |>
filter(dep_time == NA)

我们需要一个新工具:is.na()

is.na()

is.na(x) 适用于任何类型的向量,并且会对缺失值返回 TRUE,对其他所有值返回 FALSE

is.na(c(TRUE, NA, FALSE))
is.na(c(1, NA, 3))
is.na(c("a", NA, "b"))

我们可以使用 is.na() 找出所有 dep_time 缺失的行:

flights |>
filter(is.na(dep_time))

is.na() 也可以在 arrange() 中派上用场。arrange() 通常会把所有缺失值放到最后,但你可以先按 is.na() 排序,从而覆盖这一默认行为:

flights |>
filter(month == 1, day == 1) |>
arrange(dep_time)
flights |>
filter(month == 1, day == 1) |>
arrange(desc(is.na(dep_time)), dep_time)

布尔代数

一旦你有了多个逻辑向量,就可以使用布尔代数将它们组合起来。在 R 中,& 表示“与”,| 表示“或”,! 表示“非”,而 xor() 表示异或。例如,df |> filter(!is.na(x)) 会找到所有 x 不缺失的行,而 df |> filter(x < -10 | x > 0) 会找到所有 x 小于 -10 或大于 0 的行。图 12.1 展示了常用布尔运算的示例及其工作方式。

除了 & 和 | 之外,R 还有 && 和 ||。不要在 dplyr 函数中使用它们!它们被称为短路运算符,并且只会返回单个 TRUE 或 FALSE。它们对编程很重要,但不适用于数据科学。

缺失值

布尔代数中缺失值的规则有点难以解释,因为乍一看它们似乎是不一致的:

df <- tibble(x = c(TRUE, FALSE, NA))
df |>
mutate(
and = x & NA,
or = x | NA
)

要理解这是怎么回事,可以想一想 NA | TRUE(NA 或 TRUE)。逻辑向量中的缺失值表示这个值可能是 TRUE 也可能是 FALSE。TRUE | TRUE 和 FALSE | TRUE 都是 TRUE,因为它们中至少有一个为 TRUE。NA | TRUE 也必须是 TRUE,因为 NA 既可能是 TRUE,也可能是 FALSE。然而,NA | FALSE 是 NA,因为我们不知道 NA 是 TRUE 还是 FALSE。对于 &,类似的推理也适用,只不过这时两个条件都必须满足。因此,NA & TRUE 是 NA,因为 NA 既可能是 TRUE 也可能是 FALSE,而 NA & FALSE 是 FALSE,因为至少有一个条件为 FALSE。

运算顺序

请注意,运算顺序并不像英语那样起作用。看下面这段代码,它用于找出所有在 11 月或 12 月起飞的航班:

flights |>
filter(month == 11 | month == 12)

你可能会想像用英语那样来写:“找出所有在 11 月或 12 月起飞的航班。”:

flights |>
filter(month == 11 | 12)

这段代码不会报错,但看起来也没有达到预期效果。这是怎么回事?

在这里,R 会先计算 month == 11,生成一个逻辑向量,我们把它叫做 nov。然后它再计算 nov | 12

当你把数字和逻辑运算符一起使用时,R 会把除了 0 以外的所有数都转换成 TRUE。所以 12 会被当作 TRUE,因此这就等价于 nov | TRUE

而 nov | TRUE 的结果永远是 TRUE,所以最终会选中每一行

flights |>
mutate(
nov = month == 11,
final = nov | 12,
.keep = "used"
)

%in%

避免把 == 和 | 的顺序弄错的一个简单方法是使用 %in%x %in% y 会返回一个与 x 长度相同的逻辑向量,只要 x 中的某个值出现在 y 中,就会是 TRUE

1:12 %in% c(1, 5, 11)
letters[1:10] %in% c("a", "e", "i", "o", "u")

所以,要找出所有在 11 月和 12 月起飞的航班,我们可以这样写:

flights |>
filter(month %in% c(11, 12))

请注意,%in% 对 NA 的处理规则与 == 不同,因为 NA %in% NA 的结果是 TRUE

c(1, 2, NA) == NA
c(1, 2, NA) %in% NA

这可以成为一个有用的快捷写法:

flights |>
filter(dep_time %in% c(NA, 0800))

汇总

以下各节将介绍一些用于汇总逻辑向量的有用技巧。除了只适用于逻辑向量的函数外,你还可以使用适用于数值向量的函数。

逻辑汇总

有两个主要的逻辑汇总函数:any() 和 all()any(x) 相当于 |;如果 x 中存在任何 TRUE,它就会返回 TRUEall(x) 相当于 &;只有当 x 的所有值都是 TRUE 时,它才会返回 TRUE。和大多数汇总函数一样,你可以通过 na.rm = TRUE 来忽略缺失值。

例如,我们可以使用 all() 和 any() 来判断是否所有航班的起飞延误都不超过一小时,或者是否有航班的到达延误达到了五小时或以上。而使用 group_by() 则可以让我们按天进行这样的分析:

# 按年、月、日分组,汇总每天的航班延误情况
flights |>
group_by(year, month, day) |>
summarize(
# 如果当天所有航班的起飞延误都不超过 60 分钟,则为 TRUE
all_delayed = all(dep_delay <= 60, na.rm = TRUE),
# 如果当天有任一航班的到达延误达到或超过 300 分钟,则为 TRUE
any_long_delay = any(arr_delay >= 300, na.rm = TRUE),
# 汇总后取消分组
.groups = "drop"
)

不过在大多数情况下,any() 和 all() 都有些过于粗略;如果能更详细地了解有多少值为 TRUE 或 FALSE 就好了。这就引出了数值汇总。

逻辑向量的数值汇总

当你在数值上下文中使用逻辑向量时,TRUE 会变成 1FALSE 会变成 0。这使得 sum() 和 mean() 在处理逻辑向量时非常有用,因为 sum(x) 会给出 TRUE 的个数,而 mean(x) 会给出 TRUE 的比例(因为 mean() 其实就是 sum() 除以 length())。

这就让我们能够,例如,看到出发延误最多不超过一小时的航班所占比例,以及到达延误达到五小时或以上的航班数量:

flights |>
group_by(year, month, day) |>
summarize(
proportion_delayed = mean(dep_delay <= 60, na.rm = TRUE),
count_long_delay = sum(arr_delay >= 300, na.rm = TRUE),
.groups = "drop"
)

逻辑子集选择

逻辑向量在汇总中的最后一种用途是:你可以用逻辑向量将单个变量筛选为感兴趣的子集。

假设我们想只查看那些确实发生了延误的航班的平均延误时间。一种做法是先筛选出这些航班,然后再计算平均延误时间:

# 按日期分组,筛选出到达延误大于 0 的航班
flights |>
filter(arr_delay > 0) |>
# 按年、月、日分组
group_by(year, month, day) |>
# 计算每一天的平均到达延误和航班数量
summarize(
behind = mean(arr_delay),
n = n(),
.groups = "drop"
)

这就导致:

# 按年、月、日分组,分别计算:
# 1. 仅对到达延误大于 0 的航班求平均延误时间
# 2. 仅对到达延误小于 0 的航班求平均提前时间
# 3. 每组航班总数
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"
)

还要注意组大小的差异:在第一个分组中,n() 表示每天延误的航班数量;而在第二个分组中,n() 表示航班总数。

条件变换

逻辑向量最强大的功能之一是它们可用于条件变换,也就是在条件 x 下做一件事,而在条件 y 下做另一件事。实现这一点有两个重要工具:if_else() 和 case_when()

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")

还有一个可选的第四个参数 missing,当输入为 NA 时会使用它:

if_else(x > 0, "+ve", "-ve", "???")

你也可以将向量用于 true 和 false 参数。例如,这使我们能够创建一个 abs() 的最简实现:

if_else(x < 0, -x, x)

到目前为止,所有参数都使用了相同的向量,但当然你也可以混合搭配。例如,你可以像这样实现一个简单版本的 coalesce()

你可能已经注意到上面的标记示例中有一个小问题:零既不是正数,也不是负数。我们可以通过再添加一个 if_else() 来解决这个问题:

if_else(x == 0, "0", if_else(x < 0, "-ve", "+ve"), "???")

这已经有点难读了,而且你可以想象,如果条件更多,情况只会更难读。相反,你可以改用 dplyr::case_when()

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) ~ "???"
)

这段代码更多,但也更明确。

为了解释 case_when() 是如何工作的,我们先来看看一些更简单的情况。如果没有任何条件匹配,输出就会得到一个 NA

case_when(
x < 0 ~ "-ve",
x > 0 ~ "+ve"
)

如果你想创建一个“默认”/兜底值,可以使用 .default

case_when(
x < 0 ~ "-ve",
x > 0 ~ "+ve",
.default = "???"
)

并且请注意,如果多个条件都匹配,只会使用第一个:

case_when(
x > 0 ~ "+ve",
x > 2 ~ "big"
)

就像 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"
)

在编写这种复杂的 case_when() 语句时要小心;我最初的两次尝试混用了 < 和 >,结果总是不小心创建出重叠的条件。

兼容类型

请注意,if_else() 和 case_when() 都要求输出中的类型彼此兼容。如果它们不兼容,你会看到类似这样的错误:

if_else(TRUE, "a", 1)
case_when(
x < -1 ~ TRUE,
x > 0 ~ now()
)

总体来说,兼容的类型相对较少,因为自动将一种向量类型转换为另一种向量类型是错误的常见来源。以下是最重要的兼容情况:

  • 数值向量和逻辑向量是兼容的。
  • 字符串和因子是兼容的,因为你可以把因子看作是取值范围受限的字符串。
  • 日期和日期时间是兼容的,因为你可以把日期看作日期时间的一种特殊情况。
  • NA 在技术上属于逻辑向量,它与任何类型都兼容,因为每种向量都有某种表示缺失值的方式。

我们并不指望你把这些规则都背下来,但随着时间推移,它们会变得很自然。

评论

发表评论

了解 数据控|突破是我们的每一步 的更多信息

立即订阅以继续阅读并访问完整档案。

继续阅读