从这篇文章开始起,我觉得还是给我的文章系列编个号吧。因为如果没有编号,大家阅读可能会遗漏、跳过,我提及前面文章中的部分内容时,也没有太好的标签。比如这一篇文章和上一篇《dplyr语法基础(为管道操作做准备)》是联系的比较紧密的。
为了探索dplyr的基本数据操作,我们还是使用上篇文章用到的nycflights13::flights。这个数据框包含了2013年从纽约出发的所有336,776个航班。数据来自美国运输统计局,可以用?flights查看帮助文件。
library(nycflights13)
library(tidyverse)
flights
将多重操作与管道相结合想象一下,我们想要探索每个位置的距离和平均延迟之间的关系。根据您对dplyr的了解,您可以编写如下代码:
by_dest <- group_by(flights, dest)
delay <- summarize(by_dest,
count = n(),
dist = mean(distance, na.rm = TRUE),
delay = mean(arr_delay, na.rm = TRUE)
)
delay <- filter(delay, count > 20, dest != "HNL")
# It looks like delays increase with distance up to ~750 miles
# and then decrease. Maybe as flights get longer there's more
# ability to make up delays in the air?
ggplot(data = delay, mapping = aes(x = dist, y = delay)) +
geom_point(aes(size = count), alpha = 1/3) +
geom_smooth(se = FALSE)
准备此数据有三个步骤:
1.按目的地分组航班。
2.汇总计算距离、平均延误和航班数。
3.过滤掉噪声点和火奴鲁鲁机场,火奴鲁鲁机场的距离几乎是第二个最近机场的两倍。
这段代码编写起来有点令人沮丧,因为我们必须为每个中间数据帧指定一个名称,尽管我们并不关心它。给事物命名很难,所以这会减慢我们的分析速度。
还有另一种方法来解决相同问题,使用管道%>%:
delays <- flights %>%
group_by(dest) %>%
summarize(
count = n(),
dist = mean(distance, na.rm = TRUE),
delay = mean(arr_delay, na.rm = TRUE)
) %>%
filter(count > 20, dest != "HNL")
这侧重于转换,而不是正在转换的内容,这使得代码更易于阅读。您可以将其理解为一系列命令性语句:分组,然后汇总,然后过滤。阅读代码时%>%发音的一个好方法是“then”。在幕后,x%>%f(Y)变成f(x,y),x%>%f(y)%>%g(z)变成g(f(x,y),z),依此类推。您可以使用管道以一种可以从左到右、从上到下阅读的方式重写多个操作。从现在开始我们将经常使用管道,因为它极大地提高了代码的可读性。使用管道是属于tidyverse的关键标准之一。唯一的例外是ggplot2:它是在发明管道之前写好的。不幸的是,ggplot2的下一代ggvis(它确实使用管道)还没有准备好进入黄金时间。
缺少值
您可能对我们之前使用的na.rm参数感到疑惑。如果我们不设置会发生什么?
flights %>%
group_by(year, month, day) %>%
summarize(mean = mean(dep_delay))
我们得到了很多遗漏的值!这是因为聚合函数遵循通常的缺失值规则:如果输入中有任何缺失值,则输出将是缺失值。幸运的是,所有聚合函数都有一个na.rm参数,它可以在计算之前删除未命中的值:
flights %>%
group_by(year, month, day) %>%
summarize(mean = mean(dep_delay, na.rm = TRUE))
在这种情况下,如果缺少的值表示取消的航班,我们也可以通过首先删除取消的航班来解决这个问题。我们将保存此数据集,以便在接下来的几个示例中重用它:
not_cancelled <- flights %>%
filter(!is.na(dep_delay), !is.na(arr_delay))
not_cancelled %>%
group_by(year, month, day) %>%
summarize(mean = mean(dep_delay))
计数
每当您进行任何聚合时,最好包括一个计数(n())或一个非缺失值的计数(sum(!is.na(x)))。这样你就可以检查你不是基于非常少量的数据得出结论。例如,让我们看一下平均延迟最高的飞机(由其尾部编号标识):
delays <- not_cancelled %>%
group_by(tailnum) %>%
summarize(
delay = mean(arr_delay)
)
ggplot(data = delays, mapping = aes(x = delay)) +
geom_freqpoly(binwidth = 10)
哇,有些飞机平均延误5小时(300分钟)!这个故事实际上更加细致入微。如果我们画一张航班数量与平均延误的散点图,我们可以得到更多的洞察力:
delays <- not_cancelled %>%
group_by(tailnum) %>%
summarize(
delay = mean(arr_delay, na.rm = TRUE),
n = n()
)
ggplot(data = delays, mapping = aes(x = n, y = delay)) +
geom_point(alpha = 1/10)
不足为奇的是,当航班很少时,平均延误的变化要大得多。这张图的形状很有特色:每当你绘制平均值(或其他汇总)与组大小的关系图时,你会看到变化随着样本量的增加而减少。在查看这类曲线图时,筛选出观察次数最少的组通常很有用,这样您就可以在最小的组中看到更多的模式和更少的极端变化。下面的代码就是这样做的,并向您展示了一个将ggplot2集成到dplyr流中的便捷模式。必须从%>%切换到+有点痛苦,但是一旦掌握了诀窍,就相当方便了:
delays %>%
filter(n > 25) %>%
ggplot(mapping = aes(x = n, y = delay)) +
geom_point(alpha = 1/10)
这种类型的图案还有另一种常见的变体。让我们来看看棒球击球手的平均表现与他们击球次数之间的关系。在这里,我使用来自Lahman包的数据来计算每个大联盟棒球运动员的击球平均数(击打次数/尝试次数)。当我绘制击球手的技术(用击球平均数ba衡量)和击球机会(用at bat衡量,ab)时,你会看到两种模式:
- 如上所述,随着数据点的增加,我们的总变化量会减少。
- 技术(ba)与击球机会(Ab)呈正相关。这是因为球队控制着谁可以上场,显然他们会挑选出他们最好的球员:
# Convert to a tibble so it prints nicely
batting <- as_tibble(Lahman::Batting)
batters <- batting %>%
group_by(playerID) %>%
summarize(
ba = sum(H, na.rm = TRUE) / sum(AB, na.rm = TRUE),
ab = sum(AB, na.rm = TRUE)
)
batters %>%
filter(ab > 100) %>%
ggplot(mapping = aes(x = ab, y = ba)) +
geom_point() +
geom_smooth(se = FALSE)
#> `geom_smooth()ùsing method = 'gam'
这对排名也有重要影响。如果你天真地按照desc(ba)排序,那么击球率最好的人显然是幸运的,而不是熟练的:
batters %>%
arrange(desc(ba))
有用的汇总函数
仅使用均值、计数和求和可以让您走很长一段路,但是R提供了许多其他有用的汇总函数:
位置度量
我们已经使用了mean(x),但是medium(x)也很有用。平均值是总和除以长度;中位数是x的50%在其上方,50%在其下方的值。
有时将聚合与逻辑子集结合起来很有用。
not_cancelled %>%
group_by(year, month, day) %>%
summarize(
# average delay:
avg_delay1 = mean(arr_delay),
# average positive delay:
avg_delay2 = mean(arr_delay[arr_delay > 0])
)
分布的测量sd(x), IQR(x), mad(x)
均方差,或标准差,简称sd,是分布的标准度量。四分位数范围IQR()和中位数绝对偏差mad(x)是稳健的等价物,如果您有异常值,它们可能会更有用:
# Why is distance to some destinations more variable
# than to others?
not_cancelled %>%
group_by(dest) %>%
summarize(distance_sd = sd(distance)) %>%
arrange(desc(distance_sd))
秩的测量 min(x), quantile(x, 0.25), max(x)
分位数是中位数的推广。例如,quantile(x, 0.25)将找到大于25%的值、小于其余75%的值x:
# When do the first and last flights leave each day?
not_cancelled %>%
group_by(year, month, day) %>%
summarize(
first = min(dep_time),
last = max(dep_time)
)
位置的测量 first(x), nth(x, 2), last(x)
它们的工作方式类似于x[1]、x[2]和x[length(x)],但是如果该位置不存在(即,您试图从只有两个元素的组中获取第三个元素),则允许您设置默认值。例如,我们可以找到每天的第一个和最后一个出发的航班:
not_cancelled %>%
group_by(year, month, day) %>%
summarize(
first_dep = first(dep_time),
last_dep = last(dep_time)
)
这些函数是对秩过滤的补充。filter为您提供所有变量,每个观察值都在单独的行中:
not_cancelled %>%
group_by(year, month, day) %>%
mutate(r = min_rank(desc(dep_time))) %>%
filter(r %in% range(r))
计数
您已经看到n(),它不接受任何参数,并返回当前组的大小。要计算未丢失的值的数量,请使用sum(!is.na(x))。要计算不同(唯一)值的数量,请使用n_distinct(x):
# Which destinations have the most carriers?
not_cancelled %>%
group_by(dest) %>%
summarize(carriers = n_distinct(carrier)) %>%
arrange(desc(carriers))
计数非常有用,如果您只想要一个计数,dplyr会提供一个简单的帮助器:
not_cancelled %>%
count(dest)
您可以选择提供权重变量。例如,您可以使用此命令“计算”(求和)飞机飞行的总里程数:
not_cancelled %>%
count(tailnum, wt = distance)
逻辑值的计数和比例sum(x > 10), mean(y == 0)
与数值函数一起使用时,TRUE转换为1,FALSE转换为0。这使得sum()和means()非常有用:sum(x)给出x中的TRUE数,而mean(x)给出比例:
# How many flights left before 5am? (these usually
# indicate delayed flights from the previous day)
not_cancelled %>%
group_by(year, month, day) %>%
summarize(n_early = sum(dep_time < 500))
# What proportion of flights are delayed by more
# than an hour?
not_cancelled %>%
group_by(year, month, day) %>%
summarize(hour_perc = mean(arr_delay > 60))
按多个变量分组
当您按多个变量分组时,每个摘要都会剥离分组的一个级别。这使得逐步汇总数据集变得很容易:
daily <- group_by(flights, year, month, day)
(per_day <- summarize(daily, flights = n()))
(per_month <- summarize(per_day, flights = sum(flights)))
(per_year <- summarize(per_month, flights = sum(flights)))
在逐级汇总汇总时要小心:总和和计数都可以,但需要考虑加权方法和方差,而不可能完全针对基于排名的统计量(如中位数)这样做。换句话说,groupwise 总和是总和,但groupwise 中位数的中位数不是总体中位数。
取消分组
如果需要删除分组,并返回到对取消分组的数据的操作,请使用ungroup():
daily %>%
ungroup() %>% # no longer grouped by date
summarize(flights = n()) # all flights
分组的mutate(和filter)分组与summarize()配合使用最有用,但您也可以使用mutate()和filter()执行方便的操作:
- 找出每个组中最差的成员:
not_cancelled %>%
group_by(year, month, day) %>%
filter(rank(desc(arr_delay)) < 10)
- 查找所有大于阈值的组:
popular_dests <- flights %>%
group_by(dest) %>%
filter(n() > 365)
popular_dests
- 标准化以按组计算指标:
popular_dests %>%
filter(arr_delay > 0) %>%
mutate(prop_delay = arr_delay / sum(arr_delay)) %>%
select(year:day, dest, arr_delay, prop_delay)