作者: Zhang Ao

  • R语言工作流程:脚本和项目

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

    脚本

    到目前为止,你一直使用控制台来运行代码。这是一个很好的起点,但当你创建更复杂的 ggplot2 图形以及更长的 dplyr 管道时,你会发现这里很快就会变得非常拥挤。为了给自己更多的工作空间,请使用脚本编辑器。你可以通过点击“File(文件)”菜单、选择“New File(新建文件)”,然后选择“R script(R 脚本)”,或者使用键盘快捷键 Cmd/Ctrl + Shift + N 来打开它。现在你会看到四个面板,就像图 6.1 所示一样。脚本编辑器非常适合用来试验你的代码。当你想要修改某些内容时,你不必重新输入整段内容;你只需要编辑脚本并重新运行即可。而当你已经编写出能够正常运行、并实现你想要效果的代码之后,你就可以将其保存为脚本文件,之后随时方便地返回继续使用。

    运行代码

    脚本编辑器是构建复杂的 ggplot2 图形或进行长串 dplyr 处理的绝佳场所。要高效地使用脚本编辑器,关键在于记住最重要的一个键盘快捷键:Cmd/Ctrl + Enter。这个快捷键会在控制台中执行当前的 R 表达式。例如,看看下面这段代码。

    # 1) 加载需要的 R 包
    library(dplyr) # 提供数据处理/管道(filter、group_by、summarize 等)函数
    library(nycflights13) # 提供 nycflights13 数据集(航班数据)
    # 2) 筛选:只保留“未被取消”的航班记录
    # - dep_delay:起飞延误(如果航班被取消,通常会对应为 NA)
    # - arr_delay:到达延误(如果航班被取消,通常也会对应为 NA)
    # - 这里的作用是:排除 dep_delay 或 arr_delay 为 NA 的行
    not_cancelled <- flights |>
    filter(!is.na(dep_delay)█, !is.na(arr_delay))
    # 3) 分组:按航班日期(年、月、日)分成多个组
    # group_by(year, month, day) 让后续的 summarize 在每个日期组内分别计算
    not_cancelled |>
    group_by(year, month, day) |>
    summarize(
    # 4) 汇总:对每个日期组计算平均起飞延误
    # - mean(dep_delay):该日期组内所有 dep_delay 的平均值
    # - summarize 会返回一个“每组一行”的结果数据框
    mean = mean(dep_delay)
    )

    如果你的光标位于 █ 处,按下 Cmd/Ctrl + Enter 将运行生成 not_cancelled 的完整命令。它还会把光标移动到下一条语句(以 not_cancelled |> 开头)。因此,你可以通过反复按 Cmd/Ctrl + Enter,轻松地一步步浏览/执行你的完整脚本。

    我们建议你始终在脚本的开头先写好你需要的包。这样,当你把代码分享给别人时,他们就能很容易地看出需要安装哪些包。不过请注意:你在分享的脚本中绝对不要包含 install.packages()。因为如果别人没有足够小心,你把脚本交给他们后,它可能会在他们的电脑上做出一些改变——这是不太体贴的做法。

    在后续章节中学习时,我们强烈建议你从脚本编辑器开始,并练习你的键盘快捷键。久而久之,你用这种方式把代码发送到控制台会变得非常自然,甚至都不用再去刻意思考。

    保存与命名

    RStudio 会在你退出时自动保存脚本编辑器中的内容,并在你重新打开时自动重新加载。然而,还是建议你避免使用 Untitled1、Untitled2、Untitled3 之类的文件名,而是要把你的脚本保存起来,并给它们起有信息量的名字。

    给文件起名为 code.R 或 myscript.R 可能会很诱人,但在选择文件名之前,你应该多想一想。文件命名有三个重要原则如下:

    • 文件名应当可被机器读取:避免使用空格、符号和特殊字符。不要依赖大小写敏感来区分不同文件。
    • 文件名应当便于人类阅读:用文件名来描述文件里包含的内容。
    • 文件名应当与默认排序方式友好兼容:让文件名以数字开头,这样按字母顺序排序时,它们就会按照实际使用的顺序排列。

    例如,假设你的项目文件夹中有如下这些文件。

    alternative model.R
    code for exploratory analysis.r
    finalreport.qmd
    FinalReport.qmd
    fig 1.png
    Figure_02.png
    model_first_try.R
    run-first.r
    temp.txt

    这里有多种问题:很难弄清应该先运行哪个文件,文件名里包含空格,有两个文件虽然名字相同但大小写不同(finalreport vs. FinalReport1),另外还有一些名字不能很好地描述它们的内容(run-first 和 temp)。

    下面是为同一组文件起名和整理的更好方法:

    01-load-data.R
    02-exploratory-analysis.R
    03-model-approach-1.R
    04-model-approach-2.R
    fig-01.png
    fig-02.png
    report-2022-03-20.qmd
    report-2022-04-02.qmd
    report-draft-notes.txt

    对关键脚本进行编号可以清楚地表明运行它们的顺序,而一致的命名规则也让你更容易看出哪些地方是变化的。此外,图形的标注也采用了类似的方式,报告通过文件名中包含的日期来区分,temp 也被重命名为 report-draft-notes,以更准确地描述其内容。如果某个目录里有很多文件,建议在组织方面再更进一步:把不同类型的文件(脚本、图形等)放到不同的目录中。

    项目

    总有一天,你需要退出 R,去做别的事情,然后稍后再回来继续分析。总有一天,你会同时进行多个分析,并希望把它们彼此分开。总有一天,你需要把外部世界的数据导入 R,并把数值结果和图形从 R 再输出到外部世界。

    为了应对这些现实生活中的情况,你需要做出两个决定:

    1. 什么是事实来源?你会把什么保存为对所发生事情的持久记录?
    2. 你的分析存放在哪里?

    什么是权威来源?

    作为初学者,你可以依赖当前的 Environment 来保存你在分析过程中创建的所有对象。不过,为了更方便地处理更大的项目或与他人协作,你的事实来源应该是 R 脚本。有了 R 脚本(以及你的数据文件),你就可以重建 Environment。只有 Environment 的话,要重建你的 R 脚本就困难得多:你要么得凭记忆重写大量代码(这一路上难免会出错),要么就得仔细翻查你的 R 历史记录。

    为了帮助你把 R 脚本作为分析的事实来源,我们强烈建议你指示 RStudio 不要在会话之间保留工作区。你可以通过运行 usethis::use_blank_slate(),或者按照图 6.2 中显示的选项进行设置。这样做在短期内会让你有些不便,因为当你重新启动 RStudio 时,它将不再记得你上次运行的代码,你创建的对象或读取的数据集也不会再可用。但这种短期的不便会为你节省长期的痛苦,因为它迫使你把所有重要的步骤都记录在代码中。没有什么比在三个月后发现你只把一个重要计算的结果保存在 Environment 里,而没有把计算过程本身写进代码中,更糟糕的了。

    有一组非常实用的键盘快捷键,可以配合使用,确保你已经把代码中的重要部分保存在编辑器里:

    1. 按 Cmd/Ctrl + Shift + 0/F10 重新启动 R。
    2. 按 Cmd/Ctrl + Shift + S 重新运行当前脚本。

    我们每周都会把这个操作模式一起使用上百次。

      或者,如果你不使用键盘快捷键,也可以依次选择 Session > Restart R,然后高亮并重新运行当前脚本。

      如果你使用的是 RStudio Server,那么默认情况下你的 R 会话永远不会被重启。当你关闭 RStudio Server 的标签页时,感觉上像是在关闭 R,但实际上服务器仍会在后台继续运行。等你下次回来时,你会回到和离开时完全一样的位置。这也就使得定期重启 R 变得更加重要,这样你才能从一个干净的状态开始。

      你的分析存放在哪里?

      你可以通过运行 getwd() 在 R 代码中将其打印出来:

      getwd()
      #> [1] "/Users/hadley/Documents/r4ds"

      在这个 R 会话中,当前工作目录(可以把它看作“家”)位于 hadley 的 Documents 文件夹中,名为 r4ds 的子文件夹里。当你运行这段代码时,它会返回不同的结果,因为你的电脑的目录结构和 Hadley 的不一样!

      作为初学者,把你的工作目录设为主目录、文档目录,或者电脑上的任何其他奇怪目录都没问题。很快你就应该逐渐把项目整理到各自的目录中,并且在处理某个项目时,把 R 的工作目录设置为对应的目录。

      你可以在 R 中设置工作目录,但我们不建议这样做:

      setwd("/path/to/my/CoolProject")

      有一种更好的方法;一种还能让你走上像专家一样管理 R 工作之路的方法。那就是 RStudio 项目。

      RStudio 项目

      将与某个项目相关的所有文件(输入数据、R 脚本、分析结果和图表)都放在同一个目录中,是一种非常明智且常见的做法,因此 RStudio 通过 project 提供了内置支持。让我们为你创建一个 project,供你在阅读本书其余部分时使用。点击 File > New Project,然后按照图 6.3 中所示的步骤进行操作。

      将你的项目命名为 r4ds,并仔细考虑把项目放在哪个子目录中。如果你不把它存放在一个合适的位置,将来就很难找到它!

      一旦这个过程完成,你就会得到一个新 RStudio 项目。请检查你的项目“home”是否就是当前工作目录:

      getwd()
      #> [1] /Users/hadley/Documents/r4ds

      现在在脚本编辑器中输入以下命令,并将文件保存为“diamonds.R”。然后,新建一个名为“data”的文件夹。你可以通过点击 RStudio 中 Files 面板里的“New Folder”按钮来完成。最后,运行整个脚本,这会将一个 PNG 文件和一个 CSV 文件保存到你的项目目录中。

      # 加载 tidyverse 套件
      library(tidyverse)
      # 使用钻石数据集创建 carat 与 price 的六边形分箱图
      ggplot(diamonds, aes(x = carat, y = price)) +
      geom_hex()
      # 将图形保存为 PNG 文件
      ggsave("diamonds.png")
      # 将 diamonds 数据集写入 CSV 文件
      write_csv(diamonds, "data/diamonds.csv")

      退出 RStudio。查看与你的项目相关联的文件夹——注意 .Rproj 文件。双击该文件以重新打开项目。注意你会回到离开时的状态:相同的工作目录和命令历史记录,所有正在处理的文件也仍然打开着。不过,由于你遵循了我们上面的说明,你将拥有一个完全全新的环境,这保证你是从一个干净的起点开始的。

      用你最喜欢的、针对特定操作系统的方式,在电脑中搜索 diamonds.png,你不仅会找到这个 PNG 文件(不奇怪),还会找到创建它的脚本(diamonds.R)。这可是一个巨大的收获!总有一天,你会想重新制作某个图形,或者只是想弄清楚它是从哪里来的。如果你严格地用 R 代码把图形保存到文件中,而不是使用鼠标或剪贴板,那么你就能轻松地重现旧的工作成果!

      相对路径和绝对路径

      一旦进入项目中,你应该始终只使用相对路径,而不要使用绝对路径。它们有什么区别?相对路径是相对于工作目录而言的,也就是项目的 home 目录。比如,Hadley 上面写的 data/diamonds.csv,其实是 /Users/hadley/Documents/r4ds/data/diamonds.csv 的简写。但更重要的是,如果 Mine 在她自己的电脑上运行这段代码,它指向的就会是 /Users/Mine/Documents/r4ds/data/diamonds.csv。这就是相对路径很重要的原因:无论 R 项目文件夹最终放在哪里,它们都能正常工作。

      绝对路径则无论你的工作目录是什么,都会指向同一个位置。它们的形式会因操作系统不同而略有差异。在 Windows 上,它们以驱动器字母开头(例如 C:),或者以两个反斜杠开头(例如 \\servername);而在 Mac/Linux 上,它们以斜杠 / 开头(例如 /users/hadley)。你不应该在脚本中使用绝对路径,因为这会妨碍共享:没有人的目录配置会和你完全一样。

      不同操作系统之间还有一个重要区别:如何分隔路径中的各个部分。Mac 和 Linux 使用正斜杠(例如 data/diamonds.csv),而 Windows 使用反斜杠(例如 data\diamonds.csv)。R 可以处理这两种写法(无论你当前使用的是哪个平台),但不幸的是,反斜杠在 R 中有特殊含义,因此如果你想在路径中输入一个反斜杠,就必须输入两个反斜杠!这会让人很烦,所以我们建议始终使用 Linux/Mac 风格的正斜杠。

      总结

      在这一章中,你已经学会了如何在脚本(文件)和项目(目录)中组织你的 R 代码。和代码风格一样,这一开始可能会让你觉得像是在做琐事。但随着你在多个项目中积累越来越多的代码,你会逐渐体会到,前期做一点组织工作能在后面节省大量时间。

      总之,脚本和项目能为你提供一套可靠的工作流程,并且在未来也会非常有用:

      • 为每个数据分析项目创建一个 RStudio 项目。
      • 将你的脚本(用有意义的名称命名)保存在项目中,编辑它们,并分段或整体运行它们。要经常重启 R,以确保你已经把脚本中的内容都保存好了。
      • 只使用相对路径,不要使用绝对路径。
      • 这样,你所需的一切都会集中在一个地方,并且与其他你正在处理的项目清晰分开。
    1. R语言数据清理

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

      引言

      在本章中,你将学习一种在 R 里以一致方式组织数据的方法,也就是所谓的“整洁数据(tidy data)”体系。要把数据整理成这种格式,前期需要付出一些工作,但从长远来看这些付出会带来回报。等你拥有了整洁数据,你将花更少的时间把数据从一种表示形式“搓”到另一种表示形式,从而让你能够把更多时间投入到你真正关心的数据问题上。

      在本章中,你将首先学习整洁数据的定义,并用一个简单的玩具数据集来加以说明。然后,我们会深入讲解你在整理数据时将使用的主要工具:pivot(透视/旋转)。pivot 允许你在不改变任何数值的情况下,改变数据的形态。

      整洁数据

      你可以用多种方式来表示同一份底层数据。下面的例子展示了同一份数据以三种不同的方式组织起来。每个数据集都包含四个变量的相同取值:country(国家)、year(年份)、population(人口)以及 TB(结核病)的已记录病例数(number of documented cases of TB),但每个数据集对这些取值的组织方式都不同。

      library(tidyr)
      table1
      #> # A tibble: 6 × 4
      #> country year cases population
      #> <chr> <dbl> <dbl> <dbl>
      #> 1 Afghanistan 1999 745 19987071
      #> 2 Afghanistan 2000 2666 20595360
      #> 3 Brazil 1999 37737 172006362
      #> 4 Brazil 2000 80488 174504898
      #> 5 China 1999 212258 1272915272
      #> 6 China 2000 213766 1280428583
      table2
      #> # A tibble: 12 × 4
      #> country year type count
      #> <chr> <dbl> <chr> <dbl>
      #> 1 Afghanistan 1999 cases 745
      #> 2 Afghanistan 1999 population 19987071
      #> 3 Afghanistan 2000 cases 2666
      #> 4 Afghanistan 2000 population 20595360
      #> 5 Brazil 1999 cases 37737
      #> 6 Brazil 1999 population 172006362
      #> # ℹ 6 more rows
      table3
      #> # A tibble: 6 × 3
      #> country year rate
      #> <chr> <dbl> <chr>
      #> 1 Afghanistan 1999 745/19987071
      #> 2 Afghanistan 2000 2666/20595360
      #> 3 Brazil 1999 37737/172006362
      #> 4 Brazil 2000 80488/174504898
      #> 5 China 1999 212258/1272915272
      #> 6 China 2000 213766/1280428583

      这些都是对同一份底层数据的不同表示方式,但它们并不一样容易使用。其中一种表示(table1)会更容易进行处理,因为它是“整洁的(tidy)”。

      有三条相互关联的规则决定了数据集是否整洁:

      1. 每个变量都是一列;每一列都是一个变量。
      2. 每个观测值都是一行;每一行都是一个观测值。
      3. 每个值都是一个单元格;每个单元格都是一个单一的值。

      为什么要确保你的数据是整洁的(tidy)?主要有两个好处:

      1. 选择一种一致的数据存储方式带来的总体优势。如果你的数据结构是统一的,那么学习那些与之配套的工具会更容易,因为它们背后有一种共同的、相对统一的规律。
      2. 把变量放在列中带来的特定优势:这能让 R 的向量化特性充分发挥。大多数内置的 R 函数都能作用在一组值的向量上。因此,对整洁数据进行转换会感觉特别自然。

      dplyr、ggplot2包,都被设计成可以与整洁数据(tidy data)协同工作。下面是几个小例子,展示你可能会如何使用 table1。

      # Compute rate per 10,000
      table1 |>
      mutate(rate = cases / population * 10000)
      #> # A tibble: 6 × 5
      #> country year cases population rate
      #> <chr> <dbl> <dbl> <dbl> <dbl>
      #> 1 Afghanistan 1999 745 19987071 0.373
      #> 2 Afghanistan 2000 2666 20595360 1.29
      #> 3 Brazil 1999 37737 172006362 2.19
      #> 4 Brazil 2000 80488 174504898 4.61
      #> 5 China 1999 212258 1272915272 1.67
      #> 6 China 2000 213766 1280428583 1.67
      # Compute total cases per year
      table1 |>
      group_by(year) |>
      summarize(total_cases = sum(cases))
      #> # A tibble: 2 × 2
      #> year total_cases
      #> <dbl> <dbl>
      #> 1 1999 250740
      #> 2 2000 296920
      # Visualize changes over time
      ggplot(table1, aes(x = year, y = cases)) +
      geom_line(aes(group = country), color = "grey50") +
      geom_point(aes(color = country, shape = country)) +
      scale_x_continuous(breaks = c(1999, 2000)) # x-axis breaks at 1999 and 2000

      拉长数据

      整洁数据(tidy data)的原则看起来似乎如此显而易见,以至于你可能会想:你是不是再也不会遇到不整洁的数据集了。然而,不幸的是,大多数真实数据都是不整洁的。有两个主要原因:

      数据往往是为实现某种目标(而不是为了分析)而进行组织的。例如,数据被设计成便于数据录入而不是便于分析,这是很常见的。

      大多数人并不熟悉整洁数据的原则,而且除非你花很多时间亲自处理数据,否则很难自己推导出这些原则。

      这意味着,大多数真实分析都至少需要做一点整理(tidying)。你将从弄清楚底层的变量和观测值是什么开始。有时这很容易;但有时你需要去查阅最初生成这些数据的人。接下来,你会把数据转换成整洁的形式:把变量放在列中,把观测值放在行中。

      tidyr 提供了两个用于数据透视(pivot)的函数:pivot_longer() 和 pivot_wider()。我们会先从最常见的情况 pivot_longer() 开始。下面我们进入一些例子。

      列名中的数据

      广告牌数据集记录了2000年歌曲的广告牌排名:

      billboard
      #> # A tibble: 317 × 79
      #> artist track date.entered wk1 wk2 wk3 wk4 wk5
      #> <chr> <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
      #> 1 2 Pac Baby Don't Cry (Ke… 2000-02-26 87 82 72 77 87
      #> 2 2Ge+her The Hardest Part O… 2000-09-02 91 87 92 NA NA
      #> 3 3 Doors Down Kryptonite 2000-04-08 81 70 68 67 66
      #> 4 3 Doors Down Loser 2000-10-21 76 76 72 69 67
      #> 5 504 Boyz Wobble Wobble 2000-04-15 57 34 25 17 17
      #> 6 98^0 Give Me Just One N… 2000-08-19 51 39 34 26 26
      #> # ℹ 311 more rows
      #> # ℹ 71 more variables: wk6 <dbl>, wk7 <dbl>, wk8 <dbl>, wk9 <dbl>, …

      在该数据集中,每个观测值都是一首歌曲。前三列(artist,track 和 date.entered)是描述这首歌曲的变量。接着我们有 76 列(wk1-wk76),用于描述该歌曲在每周的排名。这里,列名是一个变量(周次),单元格的值是另一个变量(排名)。

      为了整理这份数据,我们将使用 pivot_longer():

      billboard |>
      pivot_longer(
      cols = starts_with("wk"), # 选择所有列名以“wk”开头的列(例如 wk1, wk2, ... wk76)
      names_to = "week", # 把这些列的列名(wk1, wk2, ...)整理到新变量 week 中
      values_to = "rank" # 把这些列对应的单元格值(排名数字)整理到新变量 rank 中
      )
      #> # A tibble: 24,092 × 5
      #> artist track date.entered week rank
      #> <chr> <chr> <date> <chr> <dbl>
      #> 1 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk1 87
      #> 2 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk2 82
      #> 3 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk3 72
      #> 4 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk4 77
      #> 5 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk5 87
      #> 6 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk6 94
      #> 7 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk7 99
      #> 8 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk8 NA
      #> 9 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk9 NA
      #> 10 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk10 NA
      #> # ℹ 24,082 more rows

      在数据之后,有三个关键参数:

      • cols 指定需要进行 pivot 的列,也就是哪些列不是变量。该参数使用与 select() 相同的语法,因此这里我们可以使用 !c(artist, track, date.entered) 或 starts_with("wk")。
      • names_to 指定将存储在列名中的变量,我们把该变量命名为 week。
      • values_to 指定将存储在单元格值中的变量,我们把该变量命名为 rank。

      注意:在代码中之所以给 "week" 和 "rank" 加上引号,是因为它们是我们要新创建的变量;在运行 pivot_longer() 调用时,这些变量在原始数据中还不存在。

      现在让我们把注意力转向整理后的、更长的数据框。如果一首歌进入了前 100 名的时间少于 76 周,会发生什么?例如,2 Pac 的 “Baby Don’t Cry”。上面的输出表明它只在前 100 名中出现了 7 周,而其余周都会填入缺失值。这些 NA 并不真正表示未知的观测值;它们是由数据集的结构“被迫”存在的,所以我们可以通过设置 values_drop_na = TRUE 让 pivot_longer() 把它们去掉:

      billboard |>
      pivot_longer(
      cols = starts_with("wk"), # 选择所有列名以“wk”开头的列(即 wk1-wk76),把它们从宽表转换为长表
      names_to = "week", # 将这些列名(wk1、wk2、...)存到新变量 week 中
      values_to = "rank", # 将每个单元格的值(排名数字)存到新变量 rank 中
      values_drop_na = TRUE # 删除生成结果中的缺失值(NA);避免为不足 76 周的歌曲补出的无效观测
      )
      #> # A tibble: 5,307 × 5
      #> artist track date.entered week rank
      #> <chr> <chr> <date> <chr> <dbl>
      #> 1 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk1 87
      #> 2 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk2 82
      #> 3 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk3 72
      #> 4 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk4 77
      #> 5 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk5 87
      #> 6 2 Pac Baby Don't Cry (Keep... 2000-02-26 wk6 94
      #> # ℹ 5,301 more rows

      行数现在大幅减少,说明丢掉了很多包含 NA 的行。

      你可能还会好奇:如果一首歌在前 100 名中超过了 76 周会怎样?仅凭这份数据我们无法得知,但你可能会猜想,数据集中会新增更多列,如 wk77、wk78、……。

      这份数据现在已经整理得很整齐了,不过我们可以通过使用 mutate() 和 readr::parse_number(),把将 week 的值从字符字符串转换为数字,从而让后续计算更容易一些。parse_number() 是一个很方便的函数:它会从字符串中提取第一个数字,并忽略字符串中的其他所有文本。

      library(readr)
      billboard_longer <- billboard |>
      pivot_longer(
      cols = starts_with("wk"), # 选择所有以“wk”开头的列(wk1-wk76)进行透视展开
      names_to = "week", # 把原列名(wk1、wk2、...)存入新变量 week
      values_to = "rank", # 把原单元格值(排名)存入新变量 rank
      values_drop_na = TRUE # 删除由“未覆盖到所有周”产生的 NA,避免无效观测
      ) |>
      mutate(
      week = parse_number(week) # 将 week 由字符串(例如 "wk1")转换为数字(例如 1)
      )
      billboard_longer # 输出整理后的长表数据框
      #> # A tibble: 5,307 × 5
      #> artist track date.entered week rank
      #> <chr> <chr> <date> <dbl> <dbl>
      #> 1 2 Pac Baby Don't Cry (Keep... 2000-02-26 1 87
      #> 2 2 Pac Baby Don't Cry (Keep... 2000-02-26 2 82
      #> 3 2 Pac Baby Don't Cry (Keep... 2000-02-26 3 72
      #> 4 2 Pac Baby Don't Cry (Keep... 2000-02-26 4 77
      #> 5 2 Pac Baby Don't Cry (Keep... 2000-02-26 5 87
      #> 6 2 Pac Baby Don't Cry (Keep... 2000-02-26 6 94
      #> # ℹ 5,301 more rows

      现在我们已经把所有的周次编号放在同一个变量里、把所有的排名数值放在另一个变量里了,因此我们很适合用可视化来展示歌曲排名如何随时间变化。下面给出了代码,运行结果如图 5.2 所示。我们可以看到,很少有歌曲会在前 100 名中停留超过 20 周。

      billboard_longer |>
      ggplot(aes(x = week, y = rank, group = track)) + # 指定绘图数据与美学映射:横轴 week,纵轴 rank;按 track 分组以便连线
      geom_line(alpha = 0.25) + # 用折线图连接同一首歌(同一 track)在不同周的排名;alpha=0.25 设置透明度,便于观察重叠
      scale_y_reverse() # 反转 y 轴方向:排名 1(最好)显示在最上方

      透视(pivot)是如何工作的?

      现在你已经看到了我们如何使用透视(pivoting)来重塑数据,让我们花一点时间来建立直观理解:pivoting 到底会如何改变数据。我们先从一个非常简单的数据集开始,这样更容易看清发生了什么。假设我们有三位患者,id 分别是 A、B 和 C,并且每位患者都有两次血压测量。我们将使用 tribble() 来创建这些数据——tribble() 是一个很方便的函数,可以手动构造小的 tibble:

      df <- tribble(
      ~id, ~bp1, ~bp2,
      "A", 100, 120,
      "B", 140, 115,
      "C", 120, 125
      )

      我们希望新的数据集包含三个变量:id(已经存在)、measurement(列名)以及 value(单元格里的值)。为实现这一点,我们需要把 df 透视为更长的格式(pivot df longer):

      df |>
      pivot_longer(
      cols = bp1:bp2, # 选择要透视展开的列范围(bp1 到 bp2),这些列名会被转成新的变量 measurement
      names_to = "measurement",# 将被透视的列名(如 bp1、bp2)放入新列 measurement
      values_to = "value" # 将对应单元格的数值放入新列 value
      )
      #> # A tibble: 6 × 3
      #> id measurement value
      #> <chr> <chr> <dbl>
      #> 1 A bp1 100
      #> 2 A bp2 120
      #> 3 B bp1 140
      #> 4 B bp2 115
      #> 5 C bp1 120
      #> 6 C bp2 125

      重塑(reshaping)是如何工作的?如果我们按列来想,会更容易理解。正如图 5.3 所示:在原始数据集中已经是变量的某一列(id)里的值,需要为每一个被透视展开(pivoted)的列各重复一次。

      列名会变成一个新变量中的取值,而该新变量的名称由 names_to 定义,如图 5.4 所示。它们需要针对原始数据集中的每一行重复一次。

      单元格里的值也会变成新变量中的值,其名称由 values_to 指定。它们会按行逐行“解开”(unwound)。图 5.5 展示了这个过程。

      列名中有很多变量

      当列名中塞进了多条信息,而且你希望把这些信息分别存到若干个新的变量中时,就会遇到更具挑战性的情况。比如,来看一下 who2 数据集——也就是你在上面看到的 table1 及其相关内容的来源:

      who2
      #> # A tibble: 7,240 × 58
      #> country year sp_m_014 sp_m_1524 sp_m_2534 sp_m_3544 sp_m_4554
      #> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
      #> 1 Afghanistan 1980 NA NA NA NA NA
      #> 2 Afghanistan 1981 NA NA NA NA NA
      #> 3 Afghanistan 1982 NA NA NA NA NA
      #> 4 Afghanistan 1983 NA NA NA NA NA
      #> 5 Afghanistan 1984 NA NA NA NA NA
      #> 6 Afghanistan 1985 NA NA NA NA NA
      #> # ℹ 7,234 more rows
      #> # ℹ 51 more variables: sp_m_5564 <dbl>, sp_m_65 <dbl>, sp_f_014 <dbl>, …

      该数据集由世界卫生组织收集,记录了关于结核病(tuberculosis)诊断的信息。已有两列变量,且很容易理解:country 和 year。它们之后还有 56 列,例如 sp_m_014、ep_m_4554 和 rel_m_3544。只要盯着这些列名看够久,你就会发现其中存在规律。每个列名由三部分组成,它们之间用 _ 分隔:第一部分 sp/rel/ep 描述用于诊断的方法;第二部分 m/f 是性别(在该数据集中以二元变量编码);第三部分 014/1524/2534/3544/4554/5564/65 表示年龄范围(例如 014 代表 0-14)。

      所以在这个例子中,who2 里记录了六条信息:国家和年份(已经是列);诊断方法、性别类别和年龄段类别(包含在其他列名中);以及该类别下患者的数量(单元格的值)。为了把这六条信息组织到六个独立的列中,我们使用 pivot_longer():对 names_to 使用一个列名向量,并对 instructors 使用用于拆分原始变量名为若干片段的设置,同时还需要一个用于指定 values_to 的列名:

      who2 |>
      pivot_longer(
      cols = !(country:year), # 选择除 country 和 year 之外的所有列作为要被“转成长表”的列
      names_to = c("diagnosis", "gender", "age"), # 将列名按分隔符拆成三部分,分别存入这三个新变量
      names_sep = "_", # 三部分之间用 "_" 分隔(对应列名格式:diagnosis_gender_age)
      values_to = "count" # 被透视后的数值放到新列 count 中
      )
      #> # A tibble: 405,440 × 6
      #> country year diagnosis gender age count
      #> <chr> <dbl> <chr> <chr> <chr> <dbl>
      #> 1 Afghanistan 1980 sp m 014 NA
      #> 2 Afghanistan 1980 sp m 1524 NA
      #> 3 Afghanistan 1980 sp m 2534 NA
      #> 4 Afghanistan 1980 sp m 3544 NA
      #> 5 Afghanistan 1980 sp m 4554 NA
      #> 6 Afghanistan 1980 sp m 5564 NA
      #> # ℹ 405,434 more rows

      从概念上来说,这只是对你之前已经见过的更简单情况的一种小幅变化。图 5.6 展示了这个基本思路:现在,不再是把列名透视(pivot)到一个单独的列中,而是透视到多个列中。你可以把这个过程想象为两步完成(先透视,再分离),但在底层它会在一步内完成,因为这样更快。

      数据与变量名出现在列标题中

      复杂度更进一步时,列名中会同时包含变量值和变量名。例如,来看家庭数据集:

      library(tidyr)
      household
      #> # A tibble: 5 × 5
      #> family dob_child1 dob_child2 name_child1 name_child2
      #> <int> <date> <date> <chr> <chr>
      #> 1 1 1998-11-26 2000-01-29 Susan Jose
      #> 2 2 1996-06-22 NA Mark <NA>
      #> 3 3 2002-07-11 2004-04-05 Sam Seth
      #> 4 4 2004-10-10 2009-08-27 Craig Khai
      #> 5 5 2000-12-05 2005-02-28 Parker Gracie

      这个数据集包含关于五个家庭的数据,以及最多两个孩子的姓名和出生日期。该数据集中的新挑战在于,列名同时包含两个变量(dob、name)的名称,以及另一个变量(child)的取值(其值为 1 或 2)。为了解决这个问题,我们同样需要向 names_to 提供一个向量,但这次使用特殊的 “.value” 哨兵;这并不是某个变量的名称,而是一个独特的值,用来告诉 pivot_longer() 采取不同的处理方式。它会覆盖通常的 values_to 参数,把透视后列名的第一个组成部分作为输出中的变量名。

      household |> # 以 household 数据框为起点,接着把结果传给 pivot_longer()
      pivot_longer( # 将数据从宽格式转换为长格式
      cols = !family, # 除了 family 列以外,其余列都作为要整理的列
      names_to = c(".value", "child"), # 将列名拆成两部分:前半部分作为输出列名,后半部分存入 child
      names_sep = "_", # 按下划线 "_" 拆分列名
      values_drop_na = TRUE # 删除转换后产生的缺失值行
      ) # 结束 pivot_longer() 调用
      #> # A tibble: 9 × 4
      #> family child dob name
      #> <int> <chr> <date> <chr>
      #> 1 1 child1 1998-11-26 Susan
      #> 2 1 child2 2000-01-29 Jose
      #> 3 2 child1 1996-06-22 Mark
      #> 4 3 child1 2002-07-11 Sam
      #> 5 3 child2 2004-04-05 Seth
      #> 6 4 child1 2004-10-10 Craig
      #> # ℹ 3 more rows

      我们再次使用 values_drop_na = TRUE,因为输入数据的形状会强制创建显式的缺失变量(例如,对于只有一个孩子的家庭)。

      图 5.7 用一个更简单的例子说明了这一基本思路。当你在 names_to 中使用 “.value” 时,输入中的列名会同时参与输出中的值和变量名。

      数据变宽

      到目前为止,我们一直使用 pivot_longer() 来解决一种常见问题:值被放到了列名中。接下来我们转向 pivot_wider(),它通过增加列数、减少行数来让数据集变宽,并且在一条观察被分散在多行中时很有帮助。这种情况在实际中似乎没那么常见,但在处理政府数据时似乎经常出现。

      我们先来看 cms_patient_experience,这是来自医疗保险和医疗补助服务中心(Centers of Medicare and Medicaid Services)的一个数据集,用于收集患者体验方面的数据:

      cms_patient_experience
      #> # A tibble: 500 × 5
      #> org_pac_id org_nm measure_cd measure_title prf_rate
      #> <chr> <chr> <chr> <chr> <dbl>
      #> 1 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_1 CAHPS for MIPS… 63
      #> 2 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_2 CAHPS for MIPS… 87
      #> 3 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_3 CAHPS for MIPS… 86
      #> 4 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_5 CAHPS for MIPS… 57
      #> 5 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_8 CAHPS for MIPS… 85
      #> 6 0446157747 USC CARE MEDICAL GROUP INC CAHPS_GRP_12 CAHPS for MIPS… 24
      #> # ℹ 494 more rows

      研究的核心单位是一个组织,但每个组织分布在六行中,调查中对组织所做的每一次测量各占一行。我们可以通过使用 distinct() 来查看 measure_cd 和 measure_title 的完整取值集合:

      cms_patient_experience |> # 以 cms_patient_experience 数据框为起点
      distinct(measure_cd, measure_title) # 提取 measure_cd 和 measure_title 的唯一组合
      #> # A tibble: 6 × 2
      #> measure_cd measure_title
      #> <chr> <chr>
      #> 1 CAHPS_GRP_1 CAHPS for MIPS SSM: Getting Timely Care, Appointments, and In…
      #> 2 CAHPS_GRP_2 CAHPS for MIPS SSM: How Well Providers Communicate
      #> 3 CAHPS_GRP_3 CAHPS for MIPS SSM: Patient's Rating of Provider
      #> 4 CAHPS_GRP_5 CAHPS for MIPS SSM: Health Promotion and Education
      #> 5 CAHPS_GRP_8 CAHPS for MIPS SSM: Courteous and Helpful Office Staff
      #> 6 CAHPS_GRP_12 CAHPS for MIPS SSM: Stewardship of Patient Resources

      这两个列名都不太适合作为变量名:measure_cd 不能说明变量含义,而 measure_title 是一个包含空格的长句子。现在我们先用 measure_cd 作为新列名的来源,但在真正的分析中,你可能会想自己创建既简短又有意义的变量名。

      pivot_wider() 的接口与 pivot_longer() 正好相反:我们不是选择新的列名,而是需要提供用于定义值的现有列(values_from)以及用于定义列名的列(names_from):

      cms_patient_experience |> # 以 cms_patient_experience 数据框为起点
      pivot_wider( # 将数据从长格式转换为宽格式
      names_from = measure_cd, # 使用 measure_cd 的值作为新列名
      values_from = prf_rate # 使用 prf_rate 的值填充新生成的列
      ) # 结束 pivot_wider() 调用
      #> # A tibble: 500 × 9
      #> org_pac_id org_nm measure_title CAHPS_GRP_1 CAHPS_GRP_2
      #> <chr> <chr> <chr> <dbl> <dbl>
      #> 1 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… 63 NA
      #> 2 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA 87
      #> 3 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA
      #> 4 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA
      #> 5 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA
      #> 6 0446157747 USC CARE MEDICAL GROUP … CAHPS for MIPS… NA NA
      #> # ℹ 494 more rows
      #> # ℹ 4 more variables: CAHPS_GRP_3 <dbl>, CAHPS_GRP_5 <dbl>, …

      输出结果看起来不太对;我们似乎仍然为每个组织保留了多行。这是因为,我们还需要告诉 pivot_wider() 哪一列或哪些列的值能够唯一标识每一行;在这个例子中,这些列是以 “org” 开头的变量:

      cms_patient_experience |> # 以 cms_patient_experience 数据框为起点
      pivot_wider( # 将数据从长格式转换为宽格式
      id_cols = starts_with("org"), # 使用以 "org" 开头的列作为标识列
      names_from = measure_cd, # 使用 measure_cd 的值作为新列名
      values_from = prf_rate # 使用 prf_rate 的值填充新生成的列
      ) # 结束 pivot_wider() 调用
      #> # A tibble: 95 × 8
      #> org_pac_id org_nm CAHPS_GRP_1 CAHPS_GRP_2 CAHPS_GRP_3 CAHPS_GRP_5
      #> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
      #> 1 0446157747 USC CARE MEDICA… 63 87 86 57
      #> 2 0446162697 ASSOCIATION OF … 59 85 83 63
      #> 3 0547164295 BEAVER MEDICAL … 49 NA 75 44
      #> 4 0749333730 CAPE PHYSICIANS… 67 84 85 65
      #> 5 0840104360 ALLIANCE PHYSIC… 66 87 87 64
      #> 6 0840109864 REX HOSPITAL INC 73 87 84 67
      #> # ℹ 89 more rows
      #> # ℹ 2 more variables: CAHPS_GRP_8 <dbl>, CAHPS_GRP_12 <dbl>

      这就得到了我们想要的输出。

      pivot_wider() 是如何工作的?

      为了理解 pivot_wider() 的工作方式,我们再次从一个非常简单的数据集开始。这次我们有两位患者,ID 分别为 A 和 B,患者 A 有三次血压测量,患者 B 有两次:

      df <- tribble(
      ~id, ~measurement, ~value,
      "A", "bp1", 100,
      "B", "bp1", 140,
      "B", "bp2", 115,
      "A", "bp2", 120,
      "A", "bp3", 105
      )

      我们将从 value 列中取值,并从 measurement 列中取列名:

      df |>
      pivot_wider(
      names_from = measurement,
      values_from = value
      )
      #> # A tibble: 2 × 4
      #> id bp1 bp2 bp3
      #> <chr> <dbl> <dbl> <dbl>
      #> 1 A 100 120 105
      #> 2 B 140 115 NA

      要开始这个过程,pivot_wider() 需要先确定哪些内容放在行里、哪些内容放在列里。新的列名将取自 measurement 的唯一值。

      df |>
      distinct(measurement) |>
      pull()
      #> [1] "bp1" "bp2" "bp3"

      默认情况下,输出中的行由所有不用于生成新名称或新值的变量决定。这些变量称为 id_cols。这里只有一列,但一般来说可以有任意数量。

      df |>
      select(!measurement & !value) |>
      distinct()
      #> # A tibble: 2 × 1
      #> id
      #> <chr>
      #> 1 A
      #> 2 B

      pivot_wider() 然后将这些结果组合起来,生成一个空的数据框:

      df |>
      select(!measurement & !value) |>
      distinct() |>
      mutate(x = NA, y = NA, z = NA)
      #> # A tibble: 2 × 4
      #> id x y z
      #> <chr> <lgl> <lgl> <lgl>
      #> 1 A NA NA NA
      #> 2 B NA NA NA

      然后,它会使用输入中的数据填充所有缺失的值。在这种情况下,输出中的每个单元格都不一定在输入中有对应值,因为患者 B 没有第三次血压测量,所以那个单元格仍然是缺失的。

      你可能还会想,如果输入中有多行对应输出中的一个单元格,会发生什么。下面的例子中有两行对应于 id “A” 和 measurement “bp1”:

      df <- tribble(
      ~id, ~measurement, ~value,
      "A", "bp1", 100,
      "A", "bp1", 102, # 这行重复了
      "A", "bp2", 120,
      "B", "bp1", 140,
      "B", "bp2", 115
      )

      如果我们尝试对此进行透视,就会得到一个包含列表列的输出

      df2 <- df |>
      pivot_wider(
      names_from = measurement,
      values_from = value
      )

      由于你还不知道如何处理这类数据,你需要按照警告中的提示来找出问题所在:

      # 按 id 和 measurement 分组,统计每组的行数
      df |>
      group_by(id, measurement) |>
      summarize(n = n(), .groups = "drop") |>
      # 只保留重复出现的组合(n > 1)
      filter(n > 1)
      #> # A tibble: 1 × 3
      #> id measurement n
      #> <chr> <chr> <int>
      #> 1 A bp1 2

      接下来就要由你来弄清楚数据出了什么问题,并修复底层损坏,或者运用分组和汇总技巧,确保每一种行值和列值的组合都只有一行。

      总结

      在这一章中,你学习了整洁数据:即变量放在列中、观测放在行中的数据。整洁数据能让你更轻松地工作,因为它是一种大多数函数都能理解的一致结构;而主要的挑战,是把你接收到的任何结构的数据转换成整洁格式。为此,你学习了 pivot_longer() 和 pivot_wider(),它们可以帮助你整理许多不整洁的数据集。我们这里展示的示例,是 vignette("pivot", package = "tidyr") 中示例的一部分,所以如果你遇到本章无法帮助你解决的问题,那篇 vignette 是下一个很好的尝试地点。

      另一个挑战是,对于某个数据集来说,长格式或宽格式版本有时都无法被明确称为“整洁”的那个。这在一定程度上反映了我们对整洁数据的定义:我们说整洁数据是每一列对应一个变量,但我们实际上并没有定义什么是变量(而且这件事出乎意料地难以界定)。实用一点地说,把变量定义为“最有利于你的分析的东西”完全没问题。所以如果你在弄清楚如何进行某个计算时遇到困难,不妨尝试重新组织数据;不要害怕在需要时先弄得不整洁、再转换、然后再整理回整洁!

      既然你已经开始编写大量的 R 代码,现在是时候进一步学习如何把代码组织到文件和目录中了。

    2. R语言堆叠面积图

      library(gcookbook) # Load gcookbook for the uspopage data set
      library(ggplot2)
      # 使用 ggplot2:以 uspopage 为数据源,设置美学映射
      ggplot(
      uspopage,
      aes(
      x = Year, # x轴:年份
      y = Thousands, # y轴:Thousands
      fill = AgeGroup, # 用 AgeGroup 决定填充颜色
      order = dplyr::desc(AgeGroup) # 设置堆叠/绘制顺序:AgeGroup 倒序
      )
      ) +
      # 画堆叠面积图
      # colour = NA:不画面积边界线(避免边界干扰)
      # alpha = .4:填充透明度 0.4
      geom_area(colour = NA, alpha = .4) +
      # 使用 RColorBrewer 的 Blues 配色方案为 fill 上色
      scale_fill_brewer(palette = "Blues") +
      # 在“堆叠结构一致”的基础上叠加折线
      # position = "stack":线的位置按堆叠方式计算
      # size = .2:线宽很细
      geom_line(position = "stack", size = .2)
    3. R语言工作流:代码风格

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

      良好的代码风格就像正确的标点:你可以不依赖它来完成事情,但它确实会让事情更容易阅读。即使你还是非常新手程序员,也建议你着手培养自己的代码风格。使用一致的风格能让他人(包括未来的你!)更容易阅读你的作品;如果你需要向别人求助,这一点尤其重要。

      给代码做样式设置一开始可能会觉得有点繁琐,但只要你多加练习,很快就会变成自然而然的习惯。此外,还有一些很棒的工具可以让你快速地为已有代码重新设置风格,比如 Lorenz Walthert 开发的 styler 包。你安装完成后(用 install.packages("styler")),使用它的一个简便方法是通过 RStudio 的命令面板。命令面板允许你使用任意内置的 RStudio 命令,以及许多由各个包提供的扩展功能(addins)。按下 Cmd/Ctrl + Shift + P 打开面板,然后输入“styler”就能查看 styler 提供的所有快捷方式。图 4.1 展示了结果。

      命名

      我们之前简要讨论过命名规则。请记住,变量名(用 <- 创建的,以及用 mutate() 创建的)只能使用小写字母、数字和 _。使用 _ 在名称内部分隔单词。

      # Strive for:
      short_flights <- flights |> filter(air_time < 60)

      一般经验是:最好优先使用较长、描述性强且容易理解的名称,而不是简短但需要快速输入的名称。短名称在编写代码时节省的时间相对不多(尤其是因为自动补全会帮你把字补全),但当你回到旧代码时就可能会花更久的时间:你不得不绞尽脑汁去破解那些令人费解的缩写。

      如果你有一堆用于表示相关事物的名称,尽量做到前后一致。当你忘记了先前的命名约定时,不一致很容易出现,所以如果你需要回头去重命名也不要感到难为情。总体而言:如果你有一组变量都围绕同一个主题变化,更适合给它们一个共同的前缀,而不是共同的后缀——因为自动补全在变量名的开头处效果最好。

      空格

      在数学运算符两侧放置空格(除了 ^;即 +、-、==、<、…),并且在赋值运算符(<-)周围也放置空格。

      # Strive for
      z <- (a + b)^2 / d

      不要在常规函数调用的括号内部或括号外加空格。逗号后面始终要加一个空格,就像标准英语一样。

      # Strive for
      mean(x, na.rm = TRUE) # mean和()之间不要有空格

      如果这样做能提升对齐效果,添加一些额外空格是可以的。例如,如果你在 mutate() 中创建多个变量,你可能希望在它们的等号 = 周围加上空格,以便让所有的 = 能对齐。这样做会让你更容易快速浏览代码。

      flights |>
      mutate(
      speed = distance / air_time,
      dep_hour = dep_time %/% 100,
      dep_minute = dep_time %% 100
      )

      管道

      |> 前面始终应当有一个空格,并且通常应当放在一行的末尾。这样做能让你更容易添加新的步骤、重新排列已有步骤、修改某个步骤中的元素,并通过在左侧快速浏览动词来获得总体的(约 10,000 英尺高空)视角。

      # Strive for
      flights |>
      filter(!is.na(arr_delay), !is.na(tailnum)) |>
      count(dest)

      如果你要通过管道(pipe)传入的函数带有命名参数(例如 mutate() 或 summarize()),那么把每个参数都放在新的一行上。如果函数没有命名参数(例如 select() 或 filter()),就把所有内容保持在同一行;除非放不下,在这种情况下你应该把每个参数都放在各自独立的行上。

      # Strive for
      flights |>
      group_by(tailnum) |>
      summarize(
      delay = mean(arr_delay, na.rm = TRUE),
      n = n()
      )

      在管道的第一步之后,给每一行缩进两个空格。RStudio 会在你于 |> 后换行时自动帮你补上这些空格。如果你把每个参数都各自放在单独的一行上,那么再额外缩进两个空格。确保 ) 单独占一行,并且取消缩进以与函数名的水平位置保持一致。

      # Strive for
      flights |>
      group_by(tailnum) |>
      summarize(
      delay = mean(arr_delay, na.rm = TRUE),
      n = n()
      )

      如果你的管道(pipeline)能轻松放在一行里,那么你可以在一定程度上放宽对这些规则的遵守。但根据我们的共同经验,短小的片段往往会逐渐变得更长,因此你通常可以通过一开始就预留你所需要的所有垂直空间,反而在长期来看节省时间。

      # more variables and more steps in the future
      df |>
      mutate(
      y = x + 1
      )

      最后,要当心编写过长的管道(pipelines),比如超过 10–15 行。试着把它们拆分成更小的子任务,并给每个任务起一个信息量足够、具有说明性的名字。这些名字能帮助读者快速把握正在发生什么,也让你更容易核对中间结果是否符合预期。只要你有机会给某个东西取一个信息量足够的名字,就应该这么做——例如当你从根本上改变了数据的结构时,比如在 pivot(透视)或 summarizing(汇总)之后。不要指望一开始就写对!如果管道中存在一些中间状态,而这些状态可以很好地被命名,那么就应该把长管道拆开。

      ggplot2

      适用于管道(pipe)的相同基本规则也同样适用于 ggplot2;只要把 + 当作 |> 来处理就行。

      flights |>
      group_by(month) |>
      summarize(
      delay = mean(arr_delay, na.rm = TRUE)
      ) |>
      ggplot(aes(x = month, y = delay)) +
      geom_point() +
      geom_line()

      另外,如果你无法把某个函数的所有参数都放进同一行,那么就把每个参数都放在各自独立的行上:

      flights |>
      group_by(dest) |>
      summarize(
      distance = mean(distance),
      speed = mean(distance / air_time, na.rm = TRUE)
      ) |>
      ggplot(aes(x = distance, y = speed)) +
      geom_smooth(
      method = "loess",
      span = 0.5,
      se = FALSE,
      color = "white",
      linewidth = 4
      ) +
      geom_point()

      注意从 |> 转到 + 的衔接。我们希望这种转换并不需要,但遗憾的是,ggplot2 在发现管道(pipe)之前就已经写好了。

      分节注释

      随着你的脚本变得更长,你可以使用分区注释(sectioning comments)把文件拆分成便于管理的多个部分:

      # Load data --------------------------------------
      # Plot data --------------------------------------

      RStudio 提供了一个键盘快捷键来创建这些标题(Cmd/Ctrl + Shift + R),并且会在编辑器左下角的代码导航下拉菜单中显示它们,如图 所示。

      总结

      在本章中,你已经学到了代码风格中最重要的原则。起初,这些可能会让人觉得是一套任意的规则(确实是!),但随着你写更多代码、并把代码分享给更多的人,你就会明白一致的风格有多重要。别忘了 styler 包:它是快速提升格式不够规范的代码质量的好方法。

    4. R语言画区域图

      library(ggplot2)
      sunspotyear <- data.frame(
      Year = as.numeric(time(sunspot.year)),
      Sunspots = as.numeric(sunspot.year)
      )
      ggplot(sunspotyear, aes(x = Year, y = Sunspots)) +
      geom_area(colour = "black", fill = "blue", alpha = .2)
    5. R语言画多条折线

      library(ggplot2)
      library(gcookbook) # Load gcookbook for the tg data set
      ggplot(tg, aes(x = dose, y = length, shape = supp)) +
      geom_line() +
      geom_point(size = 4) # Make the points a little larger
      ggplot(tg, aes(x = dose, y = length, colour = supp)) +
      geom_line(linetype = "dashed") +
      geom_point(shape = 22, size = 3, fill = "white")
      # 创建一个“并排偏移”的位置调整器:
      # 用于在同一剂量 dose 下,不同组 supp 的线/点彼此错开,避免重叠
      pd <- position_dodge(0.2)
      # 开始作图:数据集是 tg
      # x 轴:dose
      # y 轴:length
      # 填充颜色(用于区分组):supp
      ggplot(tg, aes(x = dose, y = length, fill = supp)) +
      # 画折线:每个组会根据 position = pd 做并排偏移
      geom_line(position = pd) +
      # 画散点:shape = 21 表示点形状为可填充的点(边框+填充)
      # size = 3 控制点大小
      # position = pd 让散点也跟折线使用相同的并排规则
      geom_point(shape = 21, size = 3, position = pd) +
      # 手动指定填充颜色:supp 的取值将对应到这些颜色
      # 这里把两组分别设为黑色和白色
      scale_fill_manual(values = c("black","white"))
    6. R语言向折线图添加点

      ggplot(BOD, aes(x = Time, y = demand)) +
      geom_line() +
      geom_point()
    7. R语言制作克利夫兰点阵图

      # install.packages("gcookbook")
      # 加载 gcookbook 包(其中包含数据集 cabbage_exp)
      library(gcookbook) # Load gcookbook for the cabbage_exp data set
      library(ggplot2)
      # 基于 tophit 数据绘图:
      # - 横轴:avg
      # - 纵轴:对 name 按 avg 进行重排(让显示顺序与 avg 大小一致)
      ggplot(tophit, aes(x = avg, y = reorder(name, avg))) +
      # 用散点图表示每个观测值
      # size = 3:增大点的大小,便于观察
      geom_point(size = 3) +
      # 使用黑白主题(更简洁、对比度更强)
      theme_bw() +
      # 自定义网格线样式
      theme(
      # 去掉横轴方向的主要网格线
      panel.grid.major.x = element_blank(),
      # 去掉横轴方向的次要网格线
      panel.grid.minor.x = element_blank(),
      # 保留纵轴方向的主要网格线,并设置为灰色虚线
      panel.grid.major.y = element_line(colour = "grey60", linetype = "dashed")
      )
      # install.packages("gcookbook")
      # 加载 gcookbook 包(其中包含数据集 cabbage_exp)
      library(gcookbook) # Load gcookbook for the cabbage_exp data set
      library(ggplot2)
      # 绘制“平均值 avg”与分类变量 name 之间的关系图,并按 lg 分面显示
      ggplot(tophit, aes(x = avg, y = reorder(name, avg))) +
      # 从 x=0 到 x=avg 画水平线段(y 坐标在对应的 name 上)
      # aes(yend = name):线段的终点 y 仍为该 name(因此是水平线)
      # xend = 0:线段从 0 开始
      # colour = "grey50":线段统一为灰色
      geom_segment(aes(yend = name), xend = 0, colour = "grey50") +
      # 在每个 name 位置叠加散点
      # size = 3:点变大
      # aes(colour = lg):点颜色由 lg 决定
      geom_point(size = 3, aes(colour = lg)) +
      # 使用 Set1 调色板,并限制颜色映射的取值范围为 NL 和 AL
      # guide = FALSE:不显示颜色图例(不生成 legend)
      scale_colour_brewer(palette = "Set1", limits = c("NL", "AL"), guide = FALSE) +
      # 使用黑白主题
      theme_bw() +
      # 去掉横向(y 方向)的主要网格线
      theme(panel.grid.major.y = element_blank()) +
      # 按 lg 进行分面:
      # - 行维度:lg ~ . (lg 分面到列或行,具体取决于 facet 的默认布局)
      # - scales = "free_y":每个分面 y 轴范围可不同(适合 name 个数/顺序变化)
      # - space = "free_y":每个分面的绘图区域大小随 y 的内容自适应
      facet_grid(lg ~ ., scales = "free_y", space = "free_y")
    8. R语言向条形图加标签

      # install.packages("gcookbook")
      # 加载 gcookbook 包(其中包含数据集 cabbage_exp)
      library(gcookbook) # Load gcookbook for the cabbage_exp data set
      library(ggplot2)
      # 绘制图形(在“柱子图”上标注每个柱子的数值)
      # x:把 Date 和 Cultivar 组合成一个交互(类别)变量,用于形成分组/并列的柱子
      # y:柱子的高度由 Weight 决定
      ggplot(cabbage_exp, aes(x = interaction(Date, Cultivar), y = Weight)) +
      # geom_col():画柱状图(列高度直接对应 y=Weight)
      geom_col(fill = "darkblue") +
      # 在每根柱子上方/内部添加文本标签
      # label:文本内容取 Weight
      # vjust:调整文字的垂直对齐位置(1.5 通常用于把文字往下推,避免遮挡)
      # colour:文字颜色设为白色,方便在深色柱子上更清晰
      geom_text(aes(label = Weight), vjust = 1.5, colour = "white")
    9. R语言画箱线图

      plot(
      x = ToothGrowth$supp,
      y = ToothGrowth$len,
      xlab = "supp(给药方式)",
      ylab = "len(牙齿长度)",
      main = "ToothGrowth:len 随 supp 的变化",
      pch = 19, # 点形状:实心圆
      col = "steelblue"
      )
      library(ggplot2)
      ggplot(ToothGrowth, aes(x = supp, y = len)) +
      geom_boxplot(fill="skyblue") +
      labs(
      title = "Tooth length by supplement type",
      x = "supp",
      y = "len"
      )