龙空技术网

第二章 使用tidyverse整理、操作和绘制数据

涛哥的医学与生信之旅 165

前言:

此刻我们对“csstidy”都比较注意,朋友们都想要分析一些“csstidy”的相关内容。那么小编同时在网络上汇集了一些关于“csstidy””的相关资讯,希望兄弟们能喜欢,姐妹们一起来学习一下吧!

本章内容包括:

什么是tidyverse什么是整洁数据如何安装和加载tidyverse如何使用tidyverse的tibble、dplyr、ggplot2、tidyr和purrr包

我真的很兴奋能开始教你们机器学习。但在我们深入讨论之前,我想教你们一些技巧,这些技巧会让你们的学习体验更简单、更有效。这些技能也将提高你的一般数据科学和R编程技能。

想象一下,我请你给我造一辆车(朋友之间的典型请求)。你可以用老式的:你可以购买金属,玻璃和所有的部件,手工切割所有的部件,锤打成型,然后铆接在一起。这辆车可能看起来很漂亮,工作起来也很完美,但这需要很长时间,如果你不得不再做一辆,你很难准确地记住你做了什么。

相反,你可以在工厂里使用现代化的机械臂。你可以给他们编程,让他们把这些碎片切割和弯曲成预定义的形状,然后让他们为你组装这些碎片。在这种情况下,制造汽车对您来说会更快、更简单,而且将来很容易重复相同的过程。

现在想象一下,我向你提出一个更合理的请求,要求你重新组织和绘制一个数据集,准备将其传递给机器学习管道。你可以用R基函数来做这个,它们会很好地工作。但是代码会很长,不太容易被人读懂(所以在一个月内你很难记住你做了什么),而且情节会很麻烦。

相反,您可以使用更现代的方法,使用tidyverse包家族中的函数。这些函数将有助于使数据操作过程更简单、可读性更强,并允许您用最少的输入生成非常吸引人的图形。

2.1 什么是tidyverse,什么是整洁的数据?

本书的目的是让你掌握将机器学习方法应用于你的数据的技能。虽然我不打算涵盖数据科学的所有其他方面(我也不可能在一本书中),但我确实想向您介绍tidyverse。在您将数据输入机器学习算法之前,数据需要采用算法乐于使用的格式。

tidyverse是一个“为数据科学设计的R包的集合”,创建的目的是使R中的数据科学任务更简单,更可读,更可复制。这些包是“固执己见”的,因为它们被设计成使包作者认为是好的实践的任务变得容易,而使他们认为是坏的实践的任务变得困难。这个名字来自于整洁数据的概念,一种数据结构,其中:

每一行代表一个观测,每一列代表一个变量。

请看表2 - 1中的数据。想象一下,我们让四个跑步者接受一个新的训练方案。我们想知道这个训练方案是否改善了他们的跑步时间,所以我们记录了他们在新训练开始前(第0个月)和之后三个月的最佳时间。

表2 - 1 混乱数据的一个例子该表包含四名跑步者在开始新的训练方案之前以及之后三个月的跑步时间。

这是一个数据的例子。你知道为什么吗?好吧,让我们回到我们的规则。每一行是否代表一个观察结果?没有。实际上,我们每行有四个观测(每个月一个)。每列是否代表一个变量?没有。此数据中只有三个变量:运动员,月份,和最佳时间,但我们有5列!

同样的数据以整齐的格式看起来会怎样?表2 - 2展示了这些。

表2 - 2 本表包含与表2 - 1相同的数据,但格式简洁

这一次,我们将包含月份标识符的列(以前是Month)用作单独的列,将保存每个运动员的最佳时间的列用作每个月的最佳时间。每行是否代表一个观测?是的! 每列是否代表一个变量?是的! 所以这些数据是整齐的格式。

确保数据格式整齐是任何机器学习管道的重要早期步骤,因此tidyverse包含tidyr包,可以帮助您实现这一点。tidyverse中的其他软件包与tidyr以及其他软件包一起工作,以帮助您:

以合理的方式组织和显示数据(tibble)操作和子集化数据(dplyr)绘制数据(ggplot2)用函数式编程方法替换for循环(purrr)

tidyverse中所有可用的操作都可以使用base R代码实现,但我强烈建议您将tidyverse合并到您的工作中,因为它可以帮助您保持代码更简单、更具可读性和可复制性。

tidyverse的核心和可选包我将教你使用tidyverse的tibble、dplyr、ggplot2、tidyr和purrr包。这些构成了“核心”tidyverse软件包,以及:

readr,用于将数据从外部文件读入Rforcats,用于使用因子stringr,用于使用字符串

除了这些可以一起加载的核心包之外,tidyverse还包括许多需要单独加载的可选包。

要了解更多关于tidyverse的其他工具,请参阅加勒特和威克姆(O'Reilly Media,Inc.,科学2016)。

2.2 加载tidyverse

tidyverse的软件包可以一起安装和加载(推荐):

install.packages(“tidyverse”)library(tidyverse)

或根据需要单独安装和加载:

install.packages(c(“tibble”, “dplyr”, “ggplot2”, “tidyr”, "purrr"))library(tibble)library(dplyr)library(ggplot2)library(tidyr)library(purrr)
2.3 什么是tibble软件包及其功能

如果您曾经在R中做过任何形式的数据科学或分析,您肯定会遇到数据帧(data frame)作为存储矩形数据的结构。

数据框工作正常,并且在很长一段时间内,是存储具有不同类型列的矩形数据的唯一方法(与只能处理相同类型数据的矩阵相反),但是对于数据科学家不喜欢的数据框方面,几乎没有做什么改进。

注:如果每一行的元素数等于列数,每一列的元素数等于行数,则数据为矩形。数据并不总是这样的!

tibble软件包引入了一个新的数据结构tibble,以“保留那些经受住时间考验的特性,放弃那些曾经很方便但现在令人沮丧的特性”(cran.r-project.org/web/packages/tibble/vignettes/tibble.html)。让我们看看这意味着什么。

2.3.1 创建tibbles

使用tibble()函数创建tibble与创建数据框的工作原理相同:

清单2.1 使用tibble() 创建tibbles

myTib <- tibble(x = 1:4,y = c("london", "beijing", "las vegas", "berlin"))myTib# A tibble: 4 × 2       x y          <int> <chr>    1     1 london   2     2 beijing  3     3 las vegas4     4 berlin
line4: 告诉我们这是一个4行2列的tibbleline5: 变量名line6: 变量类<int>=整数,<chr>=字符

如果您习惯于使用数据框,则会立即注意到数据框打印方式的两个不同之处:

Tibbles告诉你它们是一个Tibble以及它们的维度,当你打印它们的时候,Tibbles告诉你每个变量的类型

第二个特性在避免由于不正确的变量类型而导致的错误方面特别有用。

提示:打印tibble时,<int>表示整数变量,<chr>表示字符变量,<dbl>表示浮点数(十进制),<lgl>表示逻辑变量。

2.3.2 将现有数据帧转换为tibble

正如可以使用as.data.frame()函数将对象强制转换为数据框一样,也可以使用as_tibble()函数将对象强制转换为tibble:

清单2.2 将数据框转换为tibble

myDf <- data.frame(x = 1:4,                   y = c("london", "beijing", "las vegas", "berlin"))dfToTib <- as_tibble(myDf)dfToTib# A tibble: 4 × 2      x y          <int> <chr>    1     1 london   2     2 beijing  3     3 las vegas4     4 berlin

注:在本书中,我们将使用已经内置到R中的数据。通常,我们需要将数据从.csv文件读入R会话。要将数据作为tibble加载,请使用函数read_csv()。read_csv()来自readr包,该包在调用时加载,是read.csv()的tidyverse版本。

2.3.3 数据帧和tibble之间的差异

如果您习惯于使用数据框,您会注意到tibble的一些不同之处。在本节中,我总结了数据帧和tibble之间最显著的区别。

TIBLES不转换您的数据类型

人们在创建数据框时遇到的一个常见问题是,默认情况下,他们会将字符串变量转换为因子。这可能很烦人,因为这可能不是处理变量的最佳方式。要防止这种转换,必须提供stringsAsFactors = FALSE参数。

相反,tibbles默认情况下不会将字符串变量转换为因子。这种行为是可取的,因为将数据自动转换为某些类型可能会导致令人沮丧的错误:

清单2.3 Tibbles不能将字符串转换为因子

myDf <- data.frame(x = 1:4,                   y = c("london", "beijing", "las vegas", "berlin"))myDfNotFactor <- data.frame(x = 1:4,                            y = c("london", "beijing", "las vegas", "berlin"),                            stringsAsFactors = FALSE)myTib <- tibble(x = 1:4,                y = c("london", "beijing", "las vegas", "berlin"))class(myDf$y)#[1] "factor"class(myDfNotFactor$y)#[1] "character"class(myTib$y)#[1] "character"

如果你想让一个变量成为tibble中的一个因子,你只需要把这个函数包装在factor()的c()函数中:

myTib <- tibble(x = 1:4,y = factor(c("london", "beijing", "las vegas", "berlin")))myTib

输出简洁,与数据大小无关

打印数据框时,所有列都打印到控制台(默认情况下),因此很难查看早期变量和案例。当你打印一个tibble时,它只打印前10行和适合你屏幕的列数(默认情况下),这样更容易快速理解数据。请注意,未打印的变量名列在输出的底部。运行清单2.4中的代码,并将starwars tibble(包含在dplyr中,在调用library(tidyverse)时可用)的输出与转换为数据帧时的输出进行对比:

清单2.4 starwars数据作为一个tibble和数据帧

data(starwars)starwarsas.data.frame(starwars)

提示:data() 函数将包含在base R或R包中的数据集加载到全局环境中。使用不带参数的data() 列出当前加载的包可用的所有数据集。

使用“[”进行子集化将始终返回另一个tibble对象

当子集化数据框时,如果保留多个列,则操作符将返回另一个数据框,如果只保留一个列,则操作符将返回矢量。当设置tibble的子集时,操作符[将返回另一个tibble。如果您希望显式地将tibble列作为向量返回,请始终使用或操作符。这种行为是可取的,因为我们应该在[[ 或$中明确表示我们是否想要向量或矩形数据结构,以避免bug:

清单2.5 使用[、[[和$对tibbles进行子集化

myDf[, 1]myTib[, 1]myTib[[1]]myTib$x

注: 一个例外是,如果你的数据帧子集使用没有逗号的单一索引(如myDF[1])。在本例中,[操作符将返回单个列数据帧,但该方法不允许我们合并行和列的子集。

变量按顺序创建

在构建tibble时,变量是按顺序创建的,以便后面的变量可以引用前面定义的变量。这意味着我们可以在同一个函数调用中动态地创建引用其他变量的变量:

清单2.6 按顺序创建变量

sequentialTib <- tibble(nItems = c(12, 45, 107),cost = c(0.5, 1.2, 1.8),totalWorth = nItems * cost)sequentialTib# A tibble: 3 × 3  nItems  cost totalWorth   <dbl> <dbl>      <dbl>1     12   0.5         6 2     45   1.2        54 3    107   1.8       193.
2.4 dplyr包是什么和它做什么

在处理数据时,我们经常需要对数据执行一些操作,例如:

只选择感兴趣的行和/或列,创建新的变量,获取汇总统计信息按照某些变量的升序或降序排列数据。

在执行这些操作时,我们需要维护的数据中可能还有一个自然的分组结构。dplyr包允许我们以一种非常直观的方式执行这些操作。让我们来看一个例子。

2.4.1 使用dplyr操作CO2数据集

让我们在r中加载内置的二氧化碳数据集。我们有84个案例和5个变量,记录了不同植物在不同条件下对二氧化碳的吸收。我将用这个数据集来教你一些基本的dplyr技能。

清单2.7 探索CO2数据集

library(tibble)data(CO2)CO2tib <- as_tibble(CO2)CO2tib# A tibble: 84 × 5   Plant Type   Treatment   conc uptake   <ord> <fct>  <fct>      <dbl>  <dbl> 1 Qn1   Quebec nonchilled    95   16   2 Qn1   Quebec nonchilled   175   30.4 3 Qn1   Quebec nonchilled   250   34.8 4 Qn1   Quebec nonchilled   350   37.2 5 Qn1   Quebec nonchilled   500   35.3 6 Qn1   Quebec nonchilled   675   39.2 7 Qn1   Quebec nonchilled  1000   39.7 8 Qn2   Quebec nonchilled    95   13.6 9 Qn2   Quebec nonchilled   175   27.310 Qn2   Quebec nonchilled   250   37.1# … with 74 more rows# ℹ Use `print(n = ...)` to see more rows

假设我们只想要第1、2、3和5列,我们可以使用select()函数来做到这一点。在代码清单2.8中的select()函数调用中,第一个参数是数据,然后提供希望选择的列的编号或名称,用逗号分隔。

清单2.8 使用select()函数选择列

library(dplyr)selectedData <- select(CO2tib, 1, 2, 3, 5)selectedData# A tibble: 84 × 4   Plant Type   Treatment  uptake   <ord> <fct>  <fct>       <dbl> 1 Qn1   Quebec nonchilled   16   2 Qn1   Quebec nonchilled   30.4 3 Qn1   Quebec nonchilled   34.8 4 Qn1   Quebec nonchilled   37.2 5 Qn1   Quebec nonchilled   35.3 6 Qn1   Quebec nonchilled   39.2 7 Qn1   Quebec nonchilled   39.7 8 Qn2   Quebec nonchilled   13.6 9 Qn2   Quebec nonchilled   27.310 Qn2   Quebec nonchilled   37.1# … with 74 more rows# ℹ Use `print(n = ...)` to see more rows

现在让我们假设我们希望过滤我们的数据,只包括那些uptake大于16的病例。我们可以使用filter()函数来实现这一点。filter()的第一个参数同样是数据,第二个参数是一个逻辑表达式,将对每一行进行计算。我们可以用逗号分隔多个条件。

清单2.9 使用filter()函数过滤行

filteredData <- filter(selectedData, uptake > 16)filteredData# A tibble: 66 × 4   Plant Type   Treatment  uptake   <ord> <fct>  <fct>       <dbl> 1 Qn1   Quebec nonchilled   30.4 2 Qn1   Quebec nonchilled   34.8 3 Qn1   Quebec nonchilled   37.2 4 Qn1   Quebec nonchilled   35.3 5 Qn1   Quebec nonchilled   39.2 6 Qn1   Quebec nonchilled   39.7 7 Qn2   Quebec nonchilled   27.3 8 Qn2   Quebec nonchilled   37.1 9 Qn2   Quebec nonchilled   41.810 Qn2   Quebec nonchilled   40.6# … with 56 more rows# ℹ Use `print(n = ...)` to see more rows

接下来,我们将按单个植物分组,并对数据进行汇总,以得到每组内uptake的平均值和标准差。我们可以分别使用group_by()和summarize()函数来实现这一点。

在group_by()函数中,第一个参数是数据,后面是分组变量。我们可以通过用逗号分隔多个变量来分组。当我们打印groupedData时,除了在数据上面得到一个指示,即它们被分组、根据哪个变量以及有多少组外,没有多大变化。这告诉我们,我们应用的任何进一步操作都将在分组的基础上执行。

清单2.10 使用group_by()函数分组数据

groupedData <- group_by(filteredData, Plant)groupedData# A tibble: 66 × 4# Groups:   Plant [11]   Plant Type   Treatment  uptake   <ord> <fct>  <fct>       <dbl> 1 Qn1   Quebec nonchilled   30.4 2 Qn1   Quebec nonchilled   34.8 3 Qn1   Quebec nonchilled   37.2 4 Qn1   Quebec nonchilled   35.3 5 Qn1   Quebec nonchilled   39.2 6 Qn1   Quebec nonchilled   39.7 7 Qn2   Quebec nonchilled   27.3 8 Qn2   Quebec nonchilled   37.1 9 Qn2   Quebec nonchilled   41.810 Qn2   Quebec nonchilled   40.6# … with 56 more rows# ℹ Use `print(n = ...)` to see more rows

提示: 你可以通过在ungroup()函数中包装来移除tibble中的分组结构。

在summary() 函数中,第一个参数是数据,在第二个参数中,我们命名要创建的新变量,后跟一个=号,然后是该变量的定义。我们可以创建任意多的新变量,用逗号分隔,所以在清单2 - 11中,我创建了两个汇总变量,每组的uptake均值(meanUp)和uptake的标准差(sdUp)。现在,当我们打印summarizedData时,我们可以看到,除了分组变量之外,原始变量已被我们刚刚创建的汇总变量所替换。

清单2.11 使用summary() 函数创建变量汇总

summarizedData <- summarize(groupedData, meanUp = mean(uptake),sdUp = sd(uptake))summarizedData# A tibble: 11 × 3   Plant meanUp   sdUp   <ord>  <dbl>  <dbl> 1 Qn1     36.1  3.42  2 Qn2     38.8  6.07  3 Qn3     37.6 10.3   4 Qc1     32.6  5.03  5 Qc3     35.5  7.52  6 Qc2     36.6  5.14  7 Mn3     26.2  3.49  8 Mn2     29.9  3.92  9 Mn1     29.0  5.70 10 Mc3     18.4  0.82611 Mc1     20.1  1.83 

最后,我们将从现有变量中变异出一个新变量,以计算每组的变异系数,然后按照我们刚刚创建的新变量的顺序排列数据。我们可以用mutate() 和arrange() 函数来实现这一点。

对于mutate() 函数,第一个参数是数据,第二个参数是要创建的新变量的名称,后面跟一个=号,最后是它的定义。我们可以创建任意多的新变量,用逗号分隔它们。

清单2.12 使用mutate() 函数创建新变量

mutatedData <- mutate(summarizedData, CV = (sdUp / meanUp) * 100)mutatedData# A tibble: 11 × 4   Plant meanUp   sdUp    CV   <ord>  <dbl>  <dbl> <dbl> 1 Qn1     36.1  3.42   9.48 2 Qn2     38.8  6.07  15.7  3 Qn3     37.6 10.3   27.5  4 Qc1     32.6  5.03  15.4  5 Qc3     35.5  7.52  21.2  6 Qc2     36.6  5.14  14.1  7 Mn3     26.2  3.49  13.3  8 Mn2     29.9  3.92  13.1  9 Mn1     29.0  5.70  19.6 10 Mc3     18.4  0.826  4.4811 Mc1     20.1  1.83   9.11

TIP dplyr函数中的参数求值是连续的,这意味着我们可以通过引用meanUp和sdUp变量来定义summary()函数中的CV变量,即使它们还没有创建!

arrange() 函数将数据作为第一个参数,后面跟着我们希望用来排列用例的变量。我们可以用逗号分隔多个列来进行排列,其中它将按照第一个的顺序排列案例,并使用后续变量来打破束缚。

清单2.13 使用arrange() 函数按变量排列tibble

arrangedData <- arrange(mutatedData, CV)arrangedData# A tibble: 11 × 4   Plant meanUp   sdUp    CV   <ord>  <dbl>  <dbl> <dbl> 1 Mc3     18.4  0.826  4.48 2 Mc1     20.1  1.83   9.11 3 Qn1     36.1  3.42   9.48 4 Mn2     29.9  3.92  13.1  5 Mn3     26.2  3.49  13.3  6 Qc2     36.6  5.14  14.1  7 Qc1     32.6  5.03  15.4  8 Qn2     38.8  6.07  15.7  9 Mn1     29.0  5.70  19.6 10 Qc3     35.5  7.52  21.2 11 Qn3     37.6 10.3   27.5

提示:如果你想按照变量值的顺序排列tibble,降序只需要将变量包装在desc():arrange(mutatedData, desc(CV))。

2.4.2 将dplyr功能链接在一起

我们在2.4.1节中所做的一切都可以使用base R实现,但我希望你能看到dplyr函数,或者它们经常被称为动词(因为它们是人类可读的,并且清楚地暗示了它们的作用),有助于使代码更简单,更容易被人类阅读。但dplyr的强大之处真正来自于将这些功能链接在一起成为直观的、顺序的过程的能力。

在CO2数据操作的每个阶段,我们都保存中间数据并对其应用下一个函数。这非常繁琐,在R环境中创建了许多不必要的数据对象,并且不便于人类阅读。相反,我们可以使用管道操作符%>%,它在我们加载dplyr时可用。管道传递左侧函数的输出,作为右侧函数的第一个参数。让我们看一个基本的例子:

library(dplyr)c(1, 4, 7, 3, 5) %>% mean()#[1] 4

%>%操作符获取左侧c() 函数的输出(一个长度为5的向量),并且将其“输送”到mean() 函数的第一个参数中。所以我们可以使用%>%操作符来链接多个函数结合在一起,使代码更简洁,更易于阅读。

还记得我说过每个dplyr函数的第一个参数是数据吗?这非常重要和有用的原因是,它允许我们将数据从上一个操作传输到下一个操作,因此,我们在2.4.1节中经历的整个数据操作过程变为:

清单2.14 将dplyr操作与运算符链接在一起

arrangedData <- CO2tib %>%select(c(1:3, 5)) %>%filter(uptake > 16) %>%group_by(Plant) %>%summarize(meanUp = mean(uptake), sdUp = sd(uptake)) %>%mutate(CV = (sdUp / meanUp) * 100) %>%arrange(CV)arrangedData# A tibble: 11 × 4   Plant meanUp   sdUp    CV   <ord>  <dbl>  <dbl> <dbl> 1 Mc3     18.4  0.826  4.48 2 Mc1     20.1  1.83   9.11 3 Qn1     36.1  3.42   9.48 4 Mn2     29.9  3.92  13.1  5 Mn3     26.2  3.49  13.3  6 Qc2     36.6  5.14  14.1  7 Qc1     32.6  5.03  15.4  8 Qn2     38.8  6.07  15.7  9 Mn1     29.0  5.70  19.6 10 Qc3     35.5  7.52  21.2 11 Qn3     37.6 10.3   27.5

从头到尾通读代码,每次遇到%>%操作符时,说“然后”。您可以这样理解:“获取CO2数据,然后选择这些列,然后过滤这些行,然后根据这个变量分组,然后用这些变量进行汇总,然后改变这个新变量,然后按照这个变量的顺序排列,并将输出保存为arrangedData。” 您是否明白,这就是您可能用简单的英语向同事解释数据操作过程的方式? 这就是dplyr的强大之处: 能够以一种合乎逻辑的、人类可读的方式执行复杂的数据操作。

提示: 通常在%>%操作符之后开始一个新行,以帮助使代码更容易阅读。

2.5 什么是ggplot2包和它做什么

在R中,有三种主要的绘图系统:

Base graphicslatticeggplot2

可以说,ggplot2是数据科学家中最流行的系统,由于它是整洁世界的一部分,我们将在本书中使用这个系统来绘制我们的数据。ggplot2中的“gg”代表图形语法,这是一种思想流派,它认为任何数据图形都可以通过将数据与图组件层(如轴、标记、网格线、点、条和线)组合而成。通过像这样分层绘图组件,您可以使用ggplot2以非常直观的方式创建具有交流性和吸引力的绘图。

让我们加载R附带的iris数据集,并创建它的两个变量的散点图。这些数据是由Edgar Anderson于1935年收集并发表的,包含了三种鸢尾植物花瓣和萼片的长度和宽度测量。

图2.1用ggplot2创建的散点图萼片。长度变量被映射到x美学,萼片宽度变量映射到y美学。通过添加theme_bw()层应用了一个黑白主题。

创建图2.1中的图表的代码如清单2.15所示。函数ggplot()将您提供的数据作为第一个参数,而函数aes()将作为第二个参数(稍后将详细讨论这个问题)。这将获取数据,并基于数据创建一个绘图环境、轴和轴标签。

aes()函数是美学映射的简称,如果您习惯了以R为基础的绘图,那么它可能对您来说是新的。美学是一种可以由数据中的变量控制的图形特征。美学的例子包括x轴、y轴、颜色、形状、大小,甚至绘制在图上的数据点的透明度。在清单2.15中的函数调用中,我们要求ggplot 分别映射Sepal.Length和Sepal.Width变量到x轴和y轴。

清单2.15 使用ggplot()函数绘制数据

library(ggplot2)data(iris)myPlot <- ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +geom_point() +theme_bw()myPlot

提示: 注意,我们不需要用引号括起来; ggplot很聪明!

我们用+符号完成了这条线,我们用它向我们的图形添加额外的层(我们可以添加任意多的层来创建我们想要的图形)。约定是,当我们向我们的图中添加额外的层时,我们使用+完成当前层,并将下一层放置在新的直线上。这有助于保持可读性。

重要:当向初始的ggplot()函数调用添加图层时,每一行都需要以+结尾; 你不能把+放在一个新的行上。

下一层是一个名为geom_point()的函数。geom代表几何对象,是用来表示数据的图形元素,如点、条、线、盒、须等,因此生成这些层的函数都被命名为geom_[图形元素]。例如,让我们在我们的图中添加两个新层,geom_density_2d(),它添加密度轮廓,以及geom_smooth(),它将带有置信带的平滑线贴合到数据中。

图2.2 与图中相同的散点图,添加了2D密度等高线和平滑线作为图层(分别使用geom_density_2d()和geom_smooth函数)。

图2.2中的图是相当复杂的,要在以R为基数的情况下实现同样的效果需要很多行代码。看看清单2.16,看看使用ggplot2实现这一点有多容易!

清单2.16 向ggplot对象添加额外的geom层

myPlot +geom_density_2d() +geom_smooth()

注意,您可以将ggplot保存为一个命名对象,并简单地向该对象添加新层,而不是每次都从头创建plot。

最后,突出显示数据中的分组结构通常很重要,我们可以通过添加颜色或形状美感映射来实现这一点,如图2.3所示。

图2.3与图相同的散点图,其中物种变量映射到形状和颜色美学。

生成这些图的代码如清单2.17所示。它们之间的唯一区别是物种是作为形状或颜色美学的论据而给出的。

清单2.17 将物种映射到形状和颜色美学

ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, shape = Species)) +geom_point() +theme_bw()ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width, col = Species)) +geom_point() +theme_bw()

注意,当添加除x和y之外的美学映射时,ggplot如何自动生成一个图例?对于基础图形,你必须手动生成这些内容!

关于ggplot,我想教给你们的最后一件事,是它极其强大的面向面功能。有时,我们可能希望创建数据的子图,其中每个子图或facet显示属于数据中某些组的数据。

图2.4显示了相同的数据,但不同的iris物种被绘制在不同的子图或“小平面”上。

图2 - 4显示了相同的iris数据,但这次是由“Species”变量分面。创建该图的代码如清单2. 18所示。我只是在ggplot 2调用中添加了一个facet_wrap()层,并指定我希望它按(~)Species划分面。

清单2.18 使用group_by()函数对数据进行分组

ggplot(iris, aes(x = Sepal.Length, y = Sepal.Width)) +facet_wrap(~ Species) +geom_point() +theme_bw()

虽然你可以用ggplot2做的事情比这里介绍的要多得多(包括定制几乎所有东西的外观),但我只想让你了解如何创建基本的绘图,以复制你在整本书中看到的那些绘图。如果你想把你的数据可视化技能提升到一个新的水平,我强烈推荐Hadley Wickham的ggplot2(Springer International Publishing,2016)。

提示: ggplot上绘图元素的顺序很重要!绘图元素实际上是按顺序分层的,因此稍后在ggplot调用中添加的元素将位于所有其他元素的顶部。重新排序geom_density_2d()和geom_point() 函数,用于创建图2.2,并仔细观察发生了什么(图可能看起来一样,但实际上不是!)

2.6 tidyr包是什么及其功能

在第2.1节中,我们看了一个不整洁的数据示例,然后看了以整洁格式重组后的相同数据。作为数据科学家,我们通常无法控制数据的格式,我们通常不得不将杂乱的数据重组为整齐的格式,以便将其传递到机器学习管道中。让我们做一个不整洁的tibble并将其转换为整洁的格式。

在代码表2.19中,我们有一小部分虚构的患者数据,其中患者的体重指数(BMI)是在一些虚构的干预开始后的第0个月、第3个月和第6个月测量的。数据是否整齐?不,数据里只有3个变量:

患者ID测量月份BMI测量

但是我们有4列! 此外,每行不包含单个观察的数据,它们都包含对该患者进行的所有观察。

清单2.19 凌乱的tibble

library(tibble)library(tidyr)patientData <- tibble(Patient = c("A", "B", "C"),Month0 = c(21, 17, 29),Month3 = c(20, 21, 27),Month6 = c(21, 22, 23))patientData# A tibble: 3 × 4  Patient Month0 Month3 Month6  <chr>    <dbl>  <dbl>  <dbl>1 A           21     20     212 B           17     21     223 C           29     27     23

要将这个凌乱的tibble转换成整洁的tibble,可以使用tidyr的gather()函数。

gather()函数将数据作为它的第一个参数。key参数定义了新变量的名称,该变量将表示我们正在“收集”的列。在本例中,我们收集的列被命名为Month0、Month3和Month6,因此我们将保存这些“键”的新列称为Month。value参数定义了新变量的名称,该变量将表示我们正在收集的列中保存的数据。在本例中,这些值是BMI测量值,因此我们将表示这些“值”的新列称为BMI。

最后一个参数是一个向量,它定义了要“收集”哪些变量并将其转换为键值对。通过使用-Patient in,我们告诉gather()使用除了标识变量Patient之外的所有变量。

清单2.20 使用gather()函数整理数据

tidyPatientData <- gather(patientData, key = Month,value = BMI, -Patient)tidyPatientData# A tibble: 9 × 3  Patient Month    BMI  <chr>   <chr>  <dbl>1 A       Month0    212 B       Month0    173 C       Month0    294 A       Month3    205 B       Month3    216 C       Month3    277 A       Month6    218 B       Month6    229 C       Month6    23

我们可以通过输入以下代码来实现相同的结果(注意,它们返回的tibbles是相同的):

清单2.21 选择收集列的不同方法

gather(patientData, key = Month, value = BMI, Month0:Month6)gather(patientData, key = Month, value = BMI, c(Month0, Month3, Month6))# A tibble: 9 × 3  Patient Month    BMI  <chr>   <chr>  <dbl>1 A       Month0    212 B       Month0    173 C       Month0    294 A       Month3    205 B       Month3    216 C       Month3    277 A       Month6    218 B       Month6    229 C       Month6    23

侧边栏 转换数据到宽格式

patientData tibble中的数据结构称为宽格式,其中单个病例的观察结果被放置在同一行中,跨越多个列。大多数情况下,我们希望使用整洁的数据,因为它使生活变得更简单:我们可以立即看到我们有哪些变量,分组结构很清楚,大多数函数被设计成可以轻松地使用整洁的数据。然而,在一些罕见的情况下,我们需要将整洁的数据转换为宽格式,这可能是因为我们需要的函数期望使用这种格式的数据。我们可以使用spread()函数将整洁的数据转换为宽格式:

spread(tidyPatientData, key = Month, value = BMI)# A tibble: 3 × 4  Patient Month0 Month3 Month6  <chr>    <dbl>  <dbl>  <dbl>1 A           21     20     212 B           17     21     223 C           29     27     23

它的使用与gather的用法完全相反:我们提供键和值参数作为使用gather()函数创建的键和值列的名称,该函数将这些参数转换为宽格式。

2.7 purrr包是什么和它做什么

我要向您展示的最后一个tidyverse包是purrr(带有三个“r”)。R为我们提供了将其用作函数式编程语言的工具。这意味着它为我们提供了一种工具,可以将所有的计算视为返回其值的数学函数,而不改变工作空间中的任何东西。

注:当函数不返回一个值(比如画一个图或改变一个环境),这些被称为函数。不产生任何副作用的副作用函数称为纯函数。

代码清单2.22显示了一个简单的函数示例,它可以产生副作用,也可以不产生副作用。pure()函数返回a+1的值,但不会改变全局环境中的任何东西。side_effects()函数使用超赋值操作符<<-在全局环境中重新赋值对象a。每次运行pure()函数,它都会给出相同的输出,但是运行side_effect()函数每次都会给出一个新值(并且也会影响后续pure()函数调用的输出)。

清单2.22 创建一个数字向量列表

a <- 20pure <- function() {a <- a + 1a}side_effect <- function() {a <<- a + 1a}c(pure(), pure())#[1] 21 21c(side_effect(), side_effect())#[1] 21 22

在没有副作用的情况下调用函数通常是可取的,因为这样更容易预测函数将做什么。如果函数没有副作用,可以用不同的实现替换它,而不会破坏代码中的任何东西。

这样做的一个重要后果是,for循环在单独使用时会产生不必要的副作用(如修改现有变量),因此可以将其封装在其他函数中。将for循环封装在其中的函数,允许我们迭代vector/list的每个元素(包括数据帧或tibbles的列和行),对该元素应用一个函数,并返回整个迭代过程的结果。

注如果您熟悉apply()系列的base R函数,那么purrr包中的函数可以帮助我们实现同样的功能,但是使用了一致的语法和一些方便的特性。

2.7.1 用map()替换for循环

purrr包为我们提供了一组函数,允许我们对列表的每个元素应用一个函数。我们使用哪一个purrr函数取决于输入的数量和我们想要的输出,在本节中,我将演示这个包中最常用函数的重要性。假设您有一个包含三个数值向量的列表:

清单2.23 创建数值向量列表

listOfNumerics <- list(a = rnorm(5),                       b = rnorm(9),                       c = rnorm(10))listOfNumerics

现在,假设我们想对三个列表元素分别应用一个函数,比如返回每个元素长度的函数。我们可以使用一个for循环来完成这个任务,length()迭代每个列表元素,并将长度保存为一个新列表的元素,我们预先定义了这个新列表以保存时间。

清单2.24 使用循环对列表迭代函数

elementLengths <- vector("list", length = 3)for(i in seq_along(listOfNumerics)) {elementLengths[[i]] <- length(listOfNumerics[[i]])}elementLengths[[1]][1] 5[[2]][1] 9[[3]][1] 10

这段代码很难读懂,需要我们预定义一个空向量来防止循环变慢,并且有一个副作用:如果你再次运行循环,它将覆盖elementLengths列表。

相反,我们可以用map()函数替换for循环。第一个参数是映射族函数就是我们要迭代的数据。第二个参数是我们应用于每个列表元素的函数。看一下图2.5,它展示了map()函数是如何将函数应用于列表/向量的每个元素,并返回包含输出的列表的。

图2.5 map()函数以向量或列表为输入,对每个元素单独应用函数,并返回返回值的列表

在此示例中,map()函数将length()函数应用于listOfNumerics列表,并将这些值作为列表返回。请注意,map()函数还使用输入元素的名称作为输出元素(a、b和c)的名称。

清单2.25 使用map()迭代函数遍历列表

map(listOfNumerics, length)$a[1] 5$b[1] 9$c[1] 10

注: 如果您熟悉apply系列函数,那么map()就是lapply()的purrr等价物。

我希望你能马上看到,与我们创建的for循环相比,它的代码编写起来简单多了,阅读起来也容易多了!

2.7.2 返回原子向量而不是列表

所以map()函数总是返回一个列表。但是,如果我们不想返回列表,而是想返回原子向量呢?purrr包为我们提供了许多函数来实现这一点:

map_dbl(),返回双精度(小数)向量;map_chr(),返回字符向量;map_int(),返回整数向量;map_lgl(),返回逻辑向量.

这些函数中的每一个都返回由其后缀指定的类型的原子向量。这样,我们就不得不考虑并预先确定输出数据的类型。例如,我们可以像前面一样使用map_int()函数返回每个listOfNumerics列表元素的长度。就像map()一样,map_int()函数应用length()函数添加到列表中的每个元素,但返回的输出是一个整数向量。我们可以使用map_chr()函数来做同样的事情,它将输出强制为字符向量,但是map_lgl()函数抛出错误,因为它不能将输出强制为逻辑向量。

注: 强制我们显式地声明想要返回的输出类型可以防止意外输出类型的bug。

清单2.26 使用map_int()、map_chr()和map_lgl()

map_int(listOfNumerics, length)map_chr(listOfNumerics, length)map_lgl(listOfNumerics, length)

最后,我们可以使用map_df()函数返回一个tibble,而不是一个列表。

清单2.27 使用map_df()返回tibble

map_df(listOfNumerics, length)# A tibble: 1 × 3      a     b     c  <int> <int> <int>1     5     9    10
2.7.3 在map()家族中使用匿名函数

有时候我们想对列表中的每个元素应用一个函数,但我们还没有定义这个函数.我们动态定义的函数被称为匿名函数,当我们应用的函数使用频率不高,不需要将其赋值给对象时,匿名函数会很有用。使用基数R,我们通过简单地调用function()函数来定义一个匿名函数:

清单2.28 使用'function(.)定义匿名函数

map(listOfNumerics, function(.) . + 2)

重要:注意到匿名函数中的.了吗?这只表示map()当前正在迭代的元素。

函数(.)后的表达式是函数的主体。这种语法没有任何问题,因为它工作得非常好,但是purrr为我们提供了function(.)的简写。它只是~(潮汐)符号。因此,我们可以将map()调用简化为:

map(listOfNumerics, ~. + 2)

我们可以简单地将function(.)替换为~。

2.7.4 使用walk()产生函数的副作用

有时候我们需要迭代一个函数以获得它的副作用。可能最常见的例子是当我们想要产生一系列的情节。在这种情况下,我们可以使用walk()函数对列表的每个元素应用一个函数,以产生该函数的副作用。walk()函数还返回我们传递给它的原始输入数据,因此它对于绘制一系列管道操作中的中间步骤非常有用。下面是一个walk()的例子,它被用来为列表中的每个元素创建一个单独的直方图:

par(mfrow = c(1, 3))walk(listOfNumerics, hist)

图2.6显示了结果图。

图2.6 使用walk()遍历hist()函数的结果

但是,如果我们想使用每个列表元素的名称,作为每个直方图的标题呢?我们可以使用iwalk()函数来实现这一点,该函数使每个元素的名称或索引可用。在提供给iwalk()的函数中,可以使用.x引用要遍历的列表元素,使用.y引用它的名称/索引

iwalk(listOfNumerics, ~hist(.x, main = .y))

注意:每个函数都有一个i版本,让我们引用每个map() i元素的名称/索引。

得到的图如图2.7所示。注意,现在每个直方图的标题显示了它所绘制的列表元素的名称。

图2.7 使用iwalk()对列表中的每个元素“遍历”hist()的结果

2.7.5 同时遍历多个列表

有时,我们希望遍历的数据并不包含在单个列表中。假设我们要将列表中的每个元素乘以一个不同的值。我们可以将这些值存储在一个单独的列表中,并使用map2()函数同时遍历两个列表,用第一个列表中的元素乘以第二个列表中的元素。这一次,我们不再使用.来引用数据,而是分别使用.x和.y来特别引用第一个和第二个列表。

multipliers <- list(0.5, 10, 3)map2(.x = listOfNumerics, .y = multipliers, ~.x * .y)

想象一下,您想要迭代三个或更多的列表,而不是只迭代两个列表。pmap()函数允许我们同时遍历多个列表。当我想测试一个函数的多个参数组合时,我使用pmap()。rnorm()函数从正态分布中抽取一个随机样本,它有三个参数:n(样本的数量)、mean(分布的中心)和sd(标准差)。我们可以为每个组合创建一个值列表,然后使用pmap()迭代每个列表,以在每个组合上运行该函数。我首先使用expand.grid()函数创建一个包含输入向量的每个组合的数据帧。由于数据帧实际上只是列的列表,因此为pmap()提供一个列将在数据帧中的每一列上迭代一个函数。本质上,我们要求pmap()迭代的函数将使用数据帧的每一行包含的参数运行。因此,pmap()将返回8个不同的随机样本,一个对应于数据帧中每个参数的组合。

由于所有map-family函数的第一个参数是我们希望遍历的数据,因此可以使用%>%操作符将它们链接在一起。在这里,我将pmap()返回的随机样本管道到iwalk()函数中,为每个样本绘制单独的直方图,用索引标记。

注释:该函数调用简单地将绘图设备par(mfrow = c(2,4))分割为两行四列作为基本绘图。

清单2.29 使用pmap()迭代多个列表

arguments <- expand.grid(n = c(100, 200),mean = c(1, 10),sd = c(1, 10))arguments    n mean sd1 100    1  12 200    1  13 100   10  14 200   10  15 100    1 106 200    1 107 100   10 108 200   10 10par(mfrow = c(2, 4))pmap(arguments, rnorm) %>%iwalk(~hist(.x, main = paste("Element", .y)))

清单2.29代码生成的图形如图2.7所示。

图2.8 pmap()函数用于迭代rnorm()函数在三个参数向量上。pmap()的输出通过管道传递到iwalk(),以便在每个随机样本上迭代hist()函数。

如果你还没有记住我们刚刚讲过的所有的tidyverse函数,也不要担心,我们将在整本书的机器学习过程中使用这些工具。使用tidyverse工具,我们还可以做比这里介绍的更多的事情,但是对于解决您将遇到的最常见的数据操作问题来说,这些已经足够了。现在你已经掌握了如何使用这本书的知识,让我们深入了解机器学习的理论。

2.8 小结Tidy数据是矩形数据,其中每一行是一个单独的观察,每一列是一个变量。在将数据传递到机器学习函数之前,确保数据是整洁的格式通常是很重要的。Tibbles是数据帧的现代版,它有更好的打印矩形数据的规则,永不改变变量类型,并总是返回另一个tibble时,使用[dplyr包为数据操作过程提供了人类可读的类动词函数,其中最重要的是select(),filter(),group_by(),summarize()和arrange()dplyr最强大的方面是能够使用管道%>%操作符一起执行函数,该操作符在函数的左侧传递函数的输出,作为函数右侧的第一个参数ggplot2包是一个现代且流行的R绘图系统,它允许您以一种简单、分层的方式创建有效的绘图tidyr包提供了重要的函数gather(),它允许您轻松地将不整洁的数据转换为整洁的格式(与此函数相反的是spread(),它将整洁的数据转换为宽格式purrr包为我们提供了一种简单、一致的方式,可以迭代地在列表中的每个元素上应用函数

标签: #csstidy