引言
数值向量是数据科学的基石,而你在本书前面的部分已经多次使用过它们。现在,是时候系统地了解在 R 中可以对它们做些什么了,以确保你具备良好的基础来应对未来任何涉及数值向量的问题。
我们将先向你介绍几个工具,帮助你在只有字符串时生成数字,然后再更详细地讲解 count()。接着,我们会深入介绍与 mutate() 很搭配的各种数值转换,包括一些更通用、可应用于其他类型向量的转换,不过它们通常还是与数值向量一起使用。最后,我们会介绍与 summarize() 很搭配的汇总函数,并展示它们也可以如何与 mutate() 一起使用。
生成数字
在大多数情况下,你拿到的数字已经以 R 的数值类型之一记录好了:整数或双精度数。不过,在某些情况下,你会把它们当作字符串遇到,可能是因为你通过将列标题转换生成了它们,或者是因为在数据导入过程中出了问题。
readr 提供了两个有用的函数,用于将字符串解析为数字:parse_double() 和 parse_number()。当你有以字符串形式写出的数字时,请使用 parse_double():
library(readr)x <- c("1.2", "5.6", "1e3")parse_double(x) # 需要readr包
当字符串包含你想忽略的非数字文本时,请使用 parse_number()。这对于货币数据和百分比尤其有用:
x <- c("$1,234", "USD 3,513", "59%")parse_number(x)
count
令人惊讶的是,仅靠计数和一点基础算术,你就能完成很多数据科学工作,因此 dplyr 努力让使用 count() 进行计数尽可能简单。这个函数非常适合在分析过程中进行快速探索和检查:
library(nycflights13)flights |> count(dest)
(我们通常把 count() 写在一行,因为它一般是在控制台里用来快速检查某个计算是否按预期工作的。)
如果你想查看最常见的值,请添加 sort = TRUE:
library(dplyr)flights |> count(dest, sort = TRUE)
并且请记住,如果你想查看所有值,可以使用 |> View() 或 |> print(n = Inf)。
你可以使用 group_by()、summarize() 和 n() 手动执行相同的计算。这很有用,因为它允许你同时计算其他摘要:
# 按目的地分组,统计每个目的地的航班数量,并计算平均到达延误时间flights |> group_by(dest) |> summarize( n = n(), # 计算每个目的地的航班总数 delay = mean(arr_delay, na.rm = TRUE) # 计算平均到达延误,忽略缺失值 )
n() 是一个特殊的摘要函数,它不接受任何参数,而是访问关于“当前”组的信息。这意味着它只能在 dplyr 动词内部使用:
n()
n() 和 count() 有几个变体,你可能会觉得很有用:
n_distinct(x) 计算一个或多个变量中不同(唯一)值的数量。例如,我们可以找出哪些目的地由最多的航空公司提供服务:
# 按目的地分组flights |> group_by(dest) |> # 统计每个目的地对应的不同航空公司数量 summarize(carriers = n_distinct(carrier)) |> # 按航空公司数量降序排列 arrange(desc(carriers))
加权计数就是求和。例如,你可以“统计”每架飞机飞行的英里数:
# 按飞机编号分组flights |> group_by(tailnum) |> # 统计每架飞机的总飞行里程 summarize(miles = sum(distance))
加权计数是一个常见问题,因此 count() 有一个 wt 参数,可以完成同样的事情:
# 按飞机编号统计,并对飞行里程 distance 求和flights |> count(tailnum, wt = distance)
你可以将 sum() 和 is.na() 结合起来统计缺失值。在 flights 数据集中,这表示被取消的航班:
# 按目的地分组flights |> group_by(dest) |> # 统计每个目的地中出发时间缺失的航班数,即取消的航班数 summarize(n_cancelled = sum(is.na(dep_time)))
数值变换
变换函数与 mutate() 很适配,因为它们的输出长度与输入相同。绝大多数变换函数已经内置在基础 R 中。把它们全部列出来是不现实的,因此这一节只展示最有用的那些。比如,虽然 R 提供了你能想到的所有三角函数,但我们这里不列出它们,因为数据科学很少需要用到。
算术与回收规则
我们在第 2 章介绍了算术的基础知识(+、-、*、/、^),并且之后也多次使用它们。这些函数不需要太多解释,因为它们的作用就是你在小学学过的那些。不过,我们需要简单谈一下回收规则,它决定了当左侧和右侧的长度不同时时会发生什么。这一点对像 flights |> mutate(air_time = air_time / 60) 这样的操作很重要,因为 / 左边有 336,776 个数,而右边只有一个。
R 通过回收,或者重复,较短的向量来处理长度不匹配的问题。我们可以通过在数据框外创建一些向量,更容易看到这一机制的运行:
x <- c(1, 2, 10, 20)x / 5# is shorthand forx / c(5, 5, 5, 5)通常,你只想回收单个数字(即长度为 1 的向量),但 R 会回收任何较短长度的向量。一般来说(但并非总是),如果较长的向量不是较短向量长度的倍数,R 会给你一个警告:
x * c(1, 2)x * c(1, 2, 3)
这些回收规则也适用于逻辑比较(==、<、<=、>、>=、!=),如果你不小心把 %in% 写成了 ==,而数据框的行数又碰巧不合适,就可能得到一个令人意外的结果。例如,看看这段试图找出 1 月和 2 月所有航班的代码:
# 过滤出 1 月和 2 月的航班flights |> filter(month == c(1, 2))
这段代码运行时不会报错,但它返回的结果并不是你想要的。由于回收规则的存在,它找到了奇数行中 1 月出发的航班,以及偶数行中 2 月出发的航班。遗憾的是,因为 flights 有偶数行,所以不会出现警告。
为防止这类静默失败,大多数 tidyverse 函数使用更严格的回收方式,只回收单个值。不幸的是,这在这里并没有帮助,在许多其他情况下也一样,因为关键的计算是由基础 R 函数 == 完成的,而不是 filter()。
最小值和最大值
df <- tribble( ~x, ~y, 1, 3, 5, 2, 7, NA,)df |> mutate( min = pmin(x, y, na.rm = TRUE), max = pmax(x, y, na.rm = TRUE) )
请注意,这些与摘要函数 min() 和 max() 不同,后者会接收多个观测值并返回单个值。若所有最小值和所有最大值都相同,你就能判断自己用了错误的形式:
df |> mutate( min = min(x, y, na.rm = TRUE), max = max(x, y, na.rm = TRUE) )
模运算
模算术是这种运算的技术名称:你在学习小数点之前就已经做过了,也就是除法会得到一个整数和一个余数。在 R 中,%/% 表示整数除法,%% 用来计算余数:
1:10 %/% 3 # 整除1:10 %% 3 # 取余模运算在 flights 数据集中很有用,因为我们可以用它把 sched_dep_time 变量拆解为小时和分钟:
flights |> mutate( hour = sched_dep_time %/% 100, # 提取计划起飞时间中的“小时”部分 minute = sched_dep_time %% 100, # 提取计划起飞时间中的“分钟”部分 .keep = "used" # 只保留本次计算中用到的原始列 )
我们可以将其与第 12.4 节中的 mean(is.na(x)) 技巧结合起来,看看取消航班的比例如何随一天中的时间变化。结果如图 13.1 所示。
library(ggplot2)flights |> group_by(hour = sched_dep_time %/% 100) |> # 按计划起飞时间的“小时”分组 summarize(prop_cancelled = mean(is.na(dep_time)), # 计算每小时取消航班的比例 n = n()) |> # 统计每小时的航班数量 filter(hour > 1) |> # 过滤掉 1 点及以前的数据 ggplot(aes(x = hour, y = prop_cancelled)) + # 以小时为 x 轴,取消比例为 y 轴作图 geom_line(color = "grey50") + # 绘制灰色折线 geom_point(aes(size = n)) # 绘制点,并用点大小表示航班数量
对数
对数是一种非常有用的变换,适用于处理跨越多个数量级的数据,并将指数增长转换为线性增长。在 R 中,你可以选择三种对数函数:log()(自然对数,以 e 为底)、log2()(以 2 为底)和 log10()(以 10 为底)。我们建议使用 log2() 或 log10()。log2() 很容易解释,因为在对数尺度上相差 1,对应于原始尺度上翻倍;相差 -1,则对应于减半;而 log10() 则便于反向转换,因为例如 3 表示 。log() 的逆函数是 exp();要计算 log2() 或 log10() 的逆函数,则需要使用 2^ 或 10^。
四舍五入
使用 round(x) 将数字四舍五入到最接近的整数:
round(123.456)
你可以使用第二个参数 digits 来控制四舍五入的精度。round(x, digits) 会四舍五入到最接近的 10^-n,因此 digits = 2 时会四舍五入到最接近的 0.01。这个定义很有用,因为它意味着 round(x, -3) 会四舍五入到最接近的千位,而它确实如此:
round(123.456, 2) # two digitsround(123.456, 1) # one digitround(123.456, -1) # round to nearest tenround(123.456, -2) # round to nearest hundred
round() 有一个看起来一开始有些出乎意料的怪现象:
round(c(1.5, 2.5))
round() 使用的是所谓的“向偶数舍入”或“银行家舍入”:如果一个数字正好处于两个整数的中间,它会被舍入到偶数的那个整数。这是一个很好的策略,因为它能保持舍入结果不偏不倚:所有 0.5 中有一半会向上舍入,另一半会向下舍入。
round() 与 floor() 和 ceiling() 成对使用:floor() 总是向下舍入,而 ceiling() 总是向上舍入:
x <- 123.456floor(x)ceiling(x)
将数字按范围分组
使用 cut() 将数值向量划分(即分箱)为离散的区间:
x <- c(1, 2, 5, 10, 15, 20)cut(x, breaks = c(0, 5, 10, 15, 20))
断点不需要均匀分布:
cut(x, breaks = c(0, 5, 10, 100))
# 将 x 按指定断点分组,并为各区间指定标签cut(x, breaks = c(0, 5, 10, 15, 20), labels = c("sm", "md", "lg", "xl"))
范围之外的任何值都会变成 NA:
y <- c(NA, -10, 5, 10, 30)cut(y, breaks = c(0, 5, 10, 15, 20))
查看文档了解其他有用的参数,例如 right 和 include.lowest,它们用于控制区间是 [a, b) 还是 (a, b],以及是否应将最低区间设为 [a, b]。
累积和滚动聚合
Base R 提供了 cumsum()、cumprod()、cummin()、cummax(),用于计算累计和、累计积、累计最小值和累计最大值。dplyr 提供了 cummean() 用于计算累计平均值。累计和在实际中最常见:
x <- 1:10cumsum(x)
发表评论