library(tidyverse)
library(dplyr)
library(nycflights13)
#探索性数据分析(exploratory data analysis,EDA)
#• 变量:一种可测量的数量、质量或属性。
#• 值:变量在测量时的状态。变量值在每次测量之间可以发生改变。
#• 观测:或称个案,指在相同条件下进行的一组测量(通常,一个观测中的所有测量是在同一时间对同一对象进行的)。
#一个观测会包含多个值,每个值关联到不同的变量。有时我们会将观测称为数据点。
#• 表格数据:一组值的集合,其中每个值都关联一个变量和一个观测。
#如果每个值都有自己所属的“单元”,每个变量都有自己所属的列,每个观测都有自己所属的行,那么表格数据就是整洁的。
#变动
#变动是每次测量时数据值的变化趋势。
#所有变量都有自己的变动模式,可以揭示出一些有趣的信息。
#理解这种模式 的最好方法就是对变量值的分布进行可视化表示。
#对分布进行可视化表示
#对变量分布进行可视化的方法取决于变量是分类变量还是连续变量。
#如果仅在较小的集合内取值,那么这个变量就是分类变量。
#分类变量在 R 中通常保存为因子或字符向量。
#要想检查分类变量的分布,可以使用条形图:
ggplot(data = diamonds) +
geom_bar(mapping = aes(x = cut))
#条形的高度表示每个 x 值中观测的数量,你可以使用 dplyr::count() 手动计算出这些值:
diamonds %>%
count(cut)
#如果可以在无限大的有序集合中任意取值,那么这个变量就是连续变量。
#数值型和日期时 间型变量就是连续变量的两个例子。
#要想检查连续变量的分布,可以使用直方图:
ggplot(data = diamonds) +
geom_histogram(mapping = aes(x = carat), binwidth = 0.5)
#你可以通过 dplyr::count() 和 ggplot2::cut_width() 函数的组合来手动计算结果:
diamonds %>%
count(cut_width(carat, 0.5))
#直方图对 x 轴进行等宽分箱,然后使用条形的高度来表示落入每个分箱的观测的数量。
#在上图中,最高的条形表示几乎有 30 000 个观测的 carat 值在 0.25 和 0.75 之间,这两个值分别是条形的左侧值和右侧值。
#使用 binwidth 参数来设定直方图中的间隔的宽度,该参数是用 x 轴变量的单位来度量的
#在使用直方图时,你应该试验一下不同的分箱宽度,因为不同的分箱宽度可以揭示不同的模式。
#如果只考虑重量小于 3 克拉的钻石,并选择一个更小的分箱宽度,那么直方图如下所示:
smaller <- diamonds %>%
filter(carat < 3)
ggplot(data = smaller, mapping = aes(x = carat)) +
geom_histogram(binwidth = 0.1)
diamonds %>%
filter(carat < 3) %>%
ggplot(mapping = aes(x = carat)) +
geom_histogram(binwidth = 0.1)
#如果想要在同一张图上叠加多个直方图,那么我们建议你使用 geom_freqploy() 函数来代替 geom_histogram() 函数。
#geom_freqploy() 可以执行和 geom_histogram() 同样的计算过程,
#但前者不使用条形来显示计数,而是使用折线。
#叠加的折线远比叠加的条形更容易理解
ggplot(data = smaller, mapping = aes(x = carat, color = cut)) +
geom_freqpoly(binwidth = 0.1)
diamonds %>%
filter(carat < 3) %>%
ggplot(mapping = aes(x = carat, color = cut)) +
geom_freqpoly(binwidth = 0.1)
#典型值
#条形图和直方图都用比较高的条形表示变量中的常见值,
#而用比较矮的条形表示变量中不常见的值。
#没有条形的位置表示数据中没有这样的值。
# • 哪些值是最常见的?为什么?
# • 哪些值是非常罕见的?为什么?这和你的预期相符吗?
# • 你能发现任何异乎寻常的模式吗?如何解释?
# • 为什么重量为整数克拉和常见分数克拉的钻石更多?
# • 为什么位于每个峰值稍偏右的钻石比稍偏左的钻石更多?
# • 为什么没有重量超过 3 克拉的钻石?
ggplot(data = smaller, mapping = aes(x = carat)) +
geom_histogram(binwidth = 0.01)
#一般来说,相似值聚集形成的簇表示数据中存在子组。为了理解子组,我们提出以下问题。
# • 每个簇中的观测是如何相似的?
# • 不同簇之间的观测是如何不相似的?
# • 如何解释或描述各个簇?
# • 为什么有些簇的外观可能具有误导作用?
#异常值
#异常值是与众不同的观测或者是模式之外的数据点。
#有时异常值是由于数据录入错误而产生的;
#有时异常值则能开辟出一块重要的新科学领域。
#如果数据量比较大,有时很难在直 方图上发现异常值。
#例如,查看钻石数据集中 y 轴变量的分布,唯一能表示存在异常值的证据是,y 轴的取值范围出奇得宽:
ggplot(diamonds) +
geom_histogram(mapping = aes(x = y), binwidth = 0.5)
#为了更容易发现异常值,我们可以使用 coord_cartesian() 函数将 y 轴靠近 0 的部分放大
ggplot(diamonds) +
geom_histogram(mapping = aes(x = y), binwidth = 0.5) +
coord_cartesian(ylim = c(0, 50))
#coord_cartesian() 函数中有一个用于放大 x 轴的 xlim() 参数
#我们就可以看出有 3 个异常值,分别位于 0、30 左右和 60 左右。
#我们使用 dplyr 将它们找出来:
unusual <- diamonds %>%
filter(y < 3 | y > 20) %>%
arrange(y)
unusual
#钻石的宽度不可能是 0 毫米,因此这些值肯定是错误的。
#32 毫米和 59 毫米同样是令人难以置信的, 这样的钻石长度超过 1 英寸(1 英寸 =2.54 厘米),简直就是无价之宝!
#使用带有异常值和不带异常值的数据分别进行分析,是一种良好的做法。
#如果两次分析的结果差别不大,而你又无法说明为什么会有异常值,那么完全可以用缺失值替代异常值,然后继续进行分析。
#但如果两次分析的结果有显著差别,那么你就不能在没有正当理由的情况下丢弃它们。
#你需要弄清出现异常值的原因(如数据输入错误),并在文章中说明丢弃它们的理由。
#缺失值
#如果在数据集中发现异常值,但只想继续进行其余的分析工作,那么有 2 种选择。
#将带有可疑值的行全部丢弃:
diamonds2 <- diamonds %>%
filter(between(y, 3, 20))
#我们不建议使用这种方式,因为一个无效测量不代表所有测量都是无效的。
#此外,如果数据质量不高,若对每个变量都采取这种做法,那么你最后可能会发现数据已经所剩无几!
#相反,我们建议使用缺失值来代替异常值。
#最简单的做法就是使用 mutate() 函数创建 一个新变量来代替原来的变量。
#你可以使用 ifelse() 函数将异常值替换为 NA:
diamonds2 <- diamonds %>%
mutate(y = ifelse(y < 3 | y > 20, NA, y))
#ifelse() 函数有 3 个参数。第一个参数 test 应该是一个逻辑向量,
#如果 test 为 TRUE,函数结果就是第二个参数 yes 的值;
#如果 test 为 FALSE,函数结果就是第三个参数 no 的值。
#和 R 一样,ggplot2 也遵循不能无视缺失值的原则。
#因为无法明确地绘制出缺失值,
#所以 ggplot2 在绘图时会忽略缺失值,但会提出警告以通知缺失值被丢弃了:
ggplot(data = diamonds2, mapping = aes(x = x, y = y)) +
geom_point()
#> Warning: Removed 9 rows containing missing values
#> (geom_point).
#要想不显示这条警告,可以设置 na.rm = TRUE:
ggplot(data = diamonds2, mapping = aes(x = x, y = y)) +
geom_point(na.rm = TRUE)
#有时你会想弄清楚造成有缺失值的观测和没有缺失值的观测间的区别的原因。
#例如,在 nycflights13::flights 中,dep_time 变量中的缺失值表示航班取消了。
#因此,你应该比较 一下已取消航班和未取消航班的计划出发时间。
#可以使用 is.na() 函数创建一个新变量来 完成这个操作:
nycflights13::flights %>%
mutate(
cancelled = is.na(dep_time),
sched_hour = sched_dep_time %/% 100,
sched_min = sched_dep_time %% 100,
sched_dep_time = sched_hour + sched_min / 60
) %>%
ggplot(mapping = aes(sched_dep_time)) +
geom_freqpoly(
mapping = aes(color = cancelled),
binwidth = 1/4
)
#相关变动
#如果变动描述的是一个变量内部的行为,那么相关变动描述的就是多个变量之间的行为。
#相关变动是两个或多个变量以相关的方式共同变化所表现出的趋势。
#查看相关变动的最好方式是将两个或多个变量间的关系以可视化的方式表现出来。
#如何进行这种可视化表示同样取决于相关变量的类型。
#分类变量与连续变量
#我们经常需要探索连续变量的分布,这种分布按照一个分类变量的值可以分为几个组,
#就像前面的频率多边形图一样。
#geom_freqpoly() 的默认外观不太适合这种比较,因为高度是由计数给出的。
#这就意味着,如果一组观测的数量明显少于其他组的话,就很难看出形状上的差别。
#举个例子,我们探索一下钻石价格是如何随着质量而变化的:
ggplot(data = diamonds, mapping = aes(x = price)) +
geom_freqpoly(mapping = aes(color = cut), binwidth = 500)
#很难看出分布上的差别,因为总体看来各组数量的差别太大了
ggplot(diamonds) +
geom_bar(mapping = aes(x = cut))
#为了让比较变得更容易,需要改变 y 轴的显示内容,不再显示计数,而是显示密度。
#密度是对计数的标准化,这样每个频率多边形下边的面积都是 1:
ggplot(
data = diamonds,
mapping = aes(x = price, y = ..density..) )+
geom_freqpoly(mapping = aes(color = cut), binwidth = 500)
#这张图的部分内容非常令人惊讶,其显示出一般钻石(质量最差)的平均价格是最高的!
#但这可能是因为频率多边形图很难解释,所以这张图还有很多可以改进的地方。
#按分类变量的分组显示连续变量分布的另一种方式是使用箱线图。
#箱线图是对变量值分布 的一种简单可视化表示,这种图在统计学家中非常流行。
#每张箱线图都包括以下内容。
#• 一个长方形箱子,下面的边表示分布的第 25 个百分位数,上面的边表示分布的第 75 个 百分位数,上下两边的距离称为四分位距。
# 箱子的中部有一条横线,表示分布的中位数, 也就是分布的第 50 个百分位数。
# 这三条线可以表示分布的分散情况,还可以帮助我们明确数据是关于中位数对称的,还是偏向某一侧。
#• 圆点表示落在箱子上下两边 1.5 倍四分位距外的观测,这些离群点就是异常值,因此需 要单独绘出。
#• 从箱子上下两边延伸出的直线(或称为须)可以到达分布中最远的非离群点处。
#使用 geom_boxplot() 函数查看按切割质量分类的价格分布:
ggplot(data = diamonds, mapping = aes(x = cut, y = price)) +
geom_boxplot()
#虽然看不出太多关于分布的信息,但箱线图更加紧凑,因此可以更容易地比较多个类别
#与前面的图形一样,我们可以从箱线图中发现违反直觉的 现象:质量更好的钻石的平均价格更低!
#cut 是一个有序因子:“一般”不如“较好”、“较好”不如“很好”,以此类推。
#因为很多分类变量并没有这种内在的顺序,所以有时需要对其重新排序来绘制信息更丰富的图形。
#重新排序的其中一种方法是使用 reorder() 函数。
#我们看一下 mpg 数据集中的 class 变量。
#你可能很想知道公路里程因汽车类别的不 同会有怎样的变化:
ggplot(data = mpg, mapping = aes(x = class, y = hwy)) +
geom_boxplot()
#为了更容易发现趋势,可以基于 hwy 值的中位数对 class 进行重新排序:
ggplot(data = mpg) +
geom_boxplot(
mapping = aes(
x = reorder(class, hwy, FUN = median),
y = hwy
) )
#如果变量名很长,那么将图形旋转 90 度效果会更好一些。
#你可以通过 coord_flip() 函数完成这一操作:
ggplot(data = mpg) +
geom_boxplot(
mapping = aes(
x = reorder(class, hwy, FUN = median),
y = hwy
) )+
coord_flip()
#两个分类变量
#要想对两个分类变量间的相关变动进行可视化表示,
#需要计算出每个变量组合中的观测数量。
#完成这个任务的其中一种方法是使用内置的 geom_count() 函数
ggplot(data = diamonds) +
geom_count(mapping = aes(x = cut, y = color))
#图中每个圆点的大小表示每个变量组合中的观测数量。
#相关变动就表示为特定 x 轴变量值 与特定 y 轴变量值之间的强相关关系。
#计算变量组合中的观测数量的另一种方法是使用 dplyr:
diamonds %>%
count(color, cut)
#接着使用 geom_tile() 函数和填充图形属性进行可视化表示:
diamonds %>%
count(color, cut) %>%
ggplot(mapping = aes(x = color, y = cut)) +
geom_tile(mapping = aes(fill = n))
#如果分类变量是无序的,那么可以使用 seriation 包对行和列同时进行重新排序,以便更清楚地表示出有趣的模式。
#对于更大的图形,你可以使用 d3heatmap 或 heatmaply 包,这两个包都可以生成有交互功能的图形。
# 一般会将大数量的分类变量放在y轴或者名字比较长的放y轴
#两个连续变量
#对于两个连续变量间的相关变动的可视化表示,我们已经介绍了一种非常好的方法:
#使用 geom_point() 画出散点图。你可以将相关变动看作点的模式。
#例如,你可以看到钻石的克 拉数和价值之间存在一种指数关系:
ggplot(data = diamonds) +
geom_point(mapping = aes(x = carat, y = price))
#随着数据集规模的不断增加,散点图的用处越来越小,
#因为数据点开始出现过绘制,并堆积在一片黑色区域中(如上面的散点图所示)。
#使用 alpha 图形属性添加透明度
ggplot(data = diamonds) +
geom_point(
mapping = aes(x = carat, y = price),
alpha = 1 / 100
)
#但是很难对特别大的数据集使用透明度。
#另一种解决方法是使用分箱。
#之前使用了 geom_histogram() 和 geom_freqpoly() 函数在一个维度上进行分箱
#geom_bin2d() 和 geom_hex() 函数在两个维度上进行分箱
#geom_bin2d() 和 geom_hex() 函数将坐标平面分为二维分箱,
#并使用一种填充颜色表示落入每个分箱的数据点。
#geom_bin2d() 创建长方形分箱。geom_hex() 创建六边形分箱。
#要想使用 geom_hex(),需要安装 hexbin 包:
ggplot(data = smaller) +
geom_bin2d(mapping = aes(x = carat, y = price))
# install.packages("hexbin")
ggplot(data = smaller) +
geom_hex(mapping = aes(x = carat, y = price))
#另一种方式是对一个连续变量进行分箱,因此这个连续变量的作用就相当于分类变量。
#例如,你 可以对 carat 进行分箱,然后为每个组生成一个箱线图:
ggplot(data = smaller, mapping = aes(x = carat, y = price)) +
geom_boxplot(mapping = aes(group = cut_width(carat, 0.1)))
#以上示例使用了 cut_width(x, width) 函数将 x 变量分成宽度为 width 的分箱。
#默认情况下,不管其中有多少个观测,箱线图看上去都差不多(除了离群点的数量不同)
#因此很 难分辨出每个箱线图是对不同数量的观测进行摘要统计的。
#如果想要体现这种信息,可以使用参数 varwidth = TRUE 让箱线图的宽度与观测数量成正比。
ggplot(data = smaller, mapping = aes(x = carat, y = price)) +
geom_boxplot(mapping = aes(group = cut_width(carat, 0.1)),varwidth = TRUE)
#另一种方法是近似地显示每个分箱中的数据点的数量,此时可以使用 cut_number() 函数
ggplot(data = smaller, mapping = aes(x = carat, y = price)) +
geom_boxplot(mapping = aes(group = cut_number(carat, 20)))
#二维图形可以显示一维图形中看不到的离群点。
#例如,以下图形中的有些点具有异常的 x 值和 y 值组合,这使得这些点成为了离群点,
#即使这些点的 x 值和 y 值在单独检验时似乎是正常的。
ggplot(data = diamonds) +
geom_point(mapping = aes(x = x, y = y)) +
coord_cartesian(xlim = c(4, 11), ylim = c(4, 11))
#模式和模型
#数据中的模式提供了关系线索。
#如果两个变量之间存在系统性的关系,那么这种关系就会在数据中表现为一种模式。
#模式是数据科学中最有效的工具之一,因为其可以揭示相关变动。
#如果说变动会生成不确定性,那么相关变动就是减少不确定性。
#如果两个变量是共同变化的,就可以使用一个变 量的值来更好地预测另一个变量的值。
#如果相关变动可以归因于一种因果关系(一种特殊 情况),那么就可以使用一个变量的值来控制另一个变量的值。
#模型是用于从数据中抽取模式的一种工具。
#例如,我们思考一下钻石数据。
#切割质量与价格之间的关系是很难理解的,因为切割质量和克拉数以及克拉数和价格之间是紧密相关的。
#我们可以使用模型去除价格和克拉数之间的强关系,这样就可以继续研究剩余的微妙关系。
#以下代码拟合了一个模型,可以根据 carat 预测 price,并计算出残差(预测值和 实际值之间的差别)。
#一旦去除克拉数对价格的影响,残差就能反映出钻石的价格:
library(modelr)
#拟合了一个模型,可以根据 carat 预测 price
mod <- lm(log(price) ~ log(carat), data = diamonds)
#计算出残差(预测值和实际值之间的差别)
diamonds2 <- diamonds %>%
add_residuals(mod) %>%
mutate(resid = exp(resid))
ggplot(data = diamonds2) +
geom_point(mapping = aes(x = carat, y = resid))
#去除克拉数和价格之间的强关系后,就可以看到预料中的切割质量与价格的关系,
ggplot(data = diamonds2) +
geom_boxplot(mapping = aes(x = cut, y = resid))
#对于同样大小的钻石,切割质量更好的钻石更昂贵:
#ggplot2调用
#介绍 ggplot2 代码的一种更精简表示
ggplot(data = faithful, mapping = aes(x = eruptions)) +
geom_freqpoly(binwidth = 0.25)
#一个函数的前一个或前两个参数是非常重要的,你应该将它们牢记于心。
#ggplot() 函数的前两个参数是 data 和 mapping,aes() 函数的前两个参数是 x 和 y。
#在本书剩余的部分中,我们不再写出这些参数名,
#这样既可以节省输入时间,也可以让代码样板更精简,以便更容易找出两张图之间的不同之处。
#以更精简的方式重写前面的绘图语句,结果如下所示:
ggplot(faithful, aes(eruptions)) +
geom_freqpoly(binwidth = 0.25)
#有时我们会将数据转换管道操作的最终结果转换为一张图。
#注意,此时转换是用 + 号实现的,不是 %>%。
#我也不希望如此,但遗憾的是,ggplot2 是在管道操作方式发明前开发出来的。
diamonds %>%
count(cut, clarity) %>%
ggplot(aes(clarity, cut, fill = n)) +
geom_tile()