• R语言工作流:代码风格

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

    Table of Contents

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

    给代码做样式设置一开始可能会觉得有点繁琐,但只要你多加练习,很快就会变成自然而然的习惯。此外,还有一些很棒的工具可以让你快速地为已有代码重新设置风格,比如 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 包:它是快速提升格式不够规范的代码质量的好方法。

  • 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)
  • 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"))
  • R语言向折线图添加点

    ggplot(BOD, aes(x = Time, y = demand)) +
    geom_line() +
    geom_point()
  • 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")
  • 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")
  • 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"
    )
  • R语言画直方图

    # 画直方图:用来查看 mtcars$mpg(每加仑英里数)的分布情况
    par(las = 1) # 纵坐标横写
    # hist() 会把连续变量按区间(bins)分组,并统计每个区间出现的频数(频率)
    hist(
    mtcars$mpg, # x:要绘制分布的变量(这里是 mtcars 数据框的 mpg 列)
    breaks = 5, # bins 数量/分组区间数:把数据分成 10 个区间
    col = "skyblue", # 柱子填充颜色:浅天蓝色
    border = "white", # 柱子边框颜色:白色(让柱子之间更清晰)
    main = "Distribution of mpg", # 图标题:直方图的主标题
    xlab = "mpg", # x 轴标签:变量名 mpg
    ylab = "Frequency" # y 轴标签:频数(每个区间里数据出现的次数)
    )
    library(ggplot2)
    ggplot(mtcars, aes(x = mpg)) +
    geom_histogram(binwidth = 4)

  • R语言画柱状图

    # First, take a look at the BOD data
    BOD
    #> Time demand
    #> 1 1 8.3
    #> 2 2 10.3
    #> 3 3 19.0
    #> 4 4 16.0
    #> 5 5 15.6
    #> 6 7 19.8
    par(las = 1) # 纵坐标横排显示
    barplot(
    height = BOD$demand, # x轴对应每个柱子的高度数据(BOD 表中 demand 列)
    names.arg = BOD$Time # 指定每根柱子对应的标签(x轴刻度文字)
    )
    library(ggplot2)
    ggplot(BOD, aes(x = factor(Time), y = demand)) +
    geom_col()
  • R语言画折线图

    # 1) 先画基础曲线:temperature(横轴) vs pressure(纵轴)
    plot(
    x = pressure$temperature, # 横轴:温度
    y = pressure$pressure, # 纵轴:压力
    type = "l", # type="l" 表示先画“线”(不画点)
    xlab = "temperature", # 横轴标签(可选)
    ylab = "pressure", # 纵轴标签(可选)
    main = "Pressure vs Temperature" # 图标题(可选)
    )
    # 2) 在原始曲线上叠加点(与上面那条线对应)
    points(
    x = pressure$temperature, # 横轴
    y = pressure$pressure, # 纵轴
    pch = 19, # 点形状:19 为实心圆(可调整)
    col = "black" # 点颜色(可调整)
    )
    # 3) 再叠加一条“缩放后的线”:压力的一半(pressure/2)
    # 这会画在与原曲线不同的纵轴位置(y 变为 pressure 的一半)
    lines(
    x = pressure$temperature, # 横轴
    y = pressure$pressure / 2, # 纵轴:压力的一半
    col = "red", # 线颜色
    lwd = 2 # 线宽(可调整)
    )
    # 4) 在红色那条线对应的位置再叠加点
    points(
    x = pressure$temperature, # 横轴
    y = pressure$pressure / 2, # 纵轴:压力的一半
    pch = 19, # 点形状
    col = "red" # 点颜色
    )
    
    ggplot(pressure, aes(x = temperature, y = pressure)) +
      geom_line() +
      geom_point() +
      geom_line(aes(x = temperature, y = pressure/2), color="red") +
      geom_point(aes(y = pressure/2), color = "red")