如何制作出色的 R 可重现示例

与同事讨论性能,教学,发送错误报告或在邮件列表以及堆栈溢出此处寻求指导时,经常会问到一个可重复的示例,并且总是很有帮助。

您建立出色范例的秘诀是什么?如何以文本格式粘贴中的数据结构?您还应该包括哪些其他信息?

除了使用dput()dump()structure()之外,还有其他技巧吗?什么时候应该包含library()require()语句?除了cdfdata等之外,还应避免使用哪些保留字?

怎样才能成为一位伟大的重复的例子?

答案

一个最小的可复制示例包括以下各项:

  • 一个最小的数据集,是证明问题所必需的
  • 再现错误所需的最小可运行代码,可以在给定数据集上运行
  • 有关所用软件包,R 版本和运行它的系统的必要信息。
  • 在随机过程的情况下,可重现性的种子(由set.seed()设置) 1

有关良好的最小可复制示例的示例 ,请参见所使用功能的帮助文件。通常,此处给出的所有代码都满足最小可重现示例的要求:提供数据,提供最少的代码并且一切都可运行。还要查看有关堆栈溢出的问题,其中要有很多支持。

产生最小数据集

在大多数情况下,只需提供带有某些值的向量 / 数据帧即可轻松完成此操作。或者,您可以使用大多数软件包随附的内置数据集之一。
可以使用library(help = "datasets")查看内置数据集的完整列表。每个数据集都有一个简短的描述,例如,可以使用?mtcars获得更多信息,其中 “mtcars” 是列表中的数据集之一。其他软件包可能包含其他数据集。

制作矢量很容易。有时有必要在其中添加一些随机性,并且有大量的函数可以实现此目的。 sample()可以将向量随机化,或者给出仅包含几个值的随机向量。 letters是包含字母的有用向量。这可以用来做因素。

一些例子:

  • 随机值: x <- rnorm(10)表示正态分布, x <- runif(10)表示均匀分布,...
  • 一些值的排列: x <- sample(1:10)对于矢量 1:10 以随机顺序排列。
  • 一个随机因子: x <- sample(letters[1:4], 20, replace = TRUE)

对于矩阵,可以使用matrix() ,例如:

matrix(1:10, ncol = 2)

可以使用data.frame()制作数据帧。应该注意命名数据框中的条目,并且不要使其过于复杂。

一个例子 :

set.seed(1)
Data <- data.frame(
    X = sample(1:10),
    Y = sample(c("yes", "no"), 10, replace = TRUE)
)

对于某些问题,可能需要特定的格式。对于这些,可以使用任何提供的as.someType函数: as.factoras.Dateas.xts ,... 这些与矢量和 / 或数据帧技巧结合使用。

复制您的数据

如果您有一些数据很难使用这些技巧来构造,那么您始终可以使用head()subset()或索引来制作原始数据的subset() 。然后使用dput()给我们可以立即放入 R 中的内容:

> dput(iris[1:4, ]) # first four rows of the iris data set
structure(list(Sepal.Length = c(5.1, 4.9, 4.7, 4.6), Sepal.Width = c(3.5, 
3, 3.2, 3.1), Petal.Length = c(1.4, 1.4, 1.3, 1.5), Petal.Width = c(0.2, 
0.2, 0.2, 0.2), Species = structure(c(1L, 1L, 1L, 1L), .Label = c("setosa", 
"versicolor", "virginica"), class = "factor")), .Names = c("Sepal.Length", 
"Sepal.Width", "Petal.Length", "Petal.Width", "Species"), row.names = c(NA, 
4L), class = "data.frame")

如果您的数据框具有多个级别的因数,则dput输出可能会很笨拙,因为即使它们在数据子集中不存在,它仍会列出所有可能的因素级别。要解决此问题,可以使用droplevels()函数。请注意以下物种是如何仅具有一个水平的因素:

> dput(droplevels(iris[1:4, ]))
structure(list(Sepal.Length = c(5.1, 4.9, 4.7, 4.6), Sepal.Width = c(3.5, 
3, 3.2, 3.1), Petal.Length = c(1.4, 1.4, 1.3, 1.5), Petal.Width = c(0.2, 
0.2, 0.2, 0.2), Species = structure(c(1L, 1L, 1L, 1L), .Label = "setosa",
class = "factor")), .Names = c("Sepal.Length", "Sepal.Width", 
"Petal.Length", "Petal.Width", "Species"), row.names = c(NA, 
4L), class = "data.frame")

使用dput ,您可能还希望仅包括相关列:

> dput(mtcars[1:3, c(2, 5, 6)]) # first three rows of columns 2, 5, and 6
structure(list(cyl = c(6, 6, 4), drat = c(3.9, 3.9, 3.85), wt = c(2.62, 
2.875, 2.32)), row.names = c("Mazda RX4", "Mazda RX4 Wag", "Datsun 710"
), class = "data.frame")

另外一个需要注意的dput是,它不会对键工作data.table对象或分组tbl_df (类grouped_df从) dplyr 。在这些情况下,您可以在共享dput(as.data.frame(my_data))之前转换回常规数据帧。

最坏的情况是,您可以提供一个文本表示形式,可以使用read.tabletext参数来读取它:

zz <- "Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa"

Data <- read.table(text=zz, header = TRUE)

产生最少的代码

这应该是容易的部分,但通常不是。您不应该做的是:

  • 添加所有类型的数据转换。确保提供的数据已经采用正确的格式(当然,除非是问题所在)
  • 复制并粘贴会给出错误的整个函数 / 代码块。首先,尝试找出哪些行完全导致错误。通常,您会发现问题出在哪里。

您应该做的是:

  • 添加使用任何软件包(如果使用)(使用library()
  • 如果您打开连接或创建文件,请添加一些代码以将其关闭或删除文件(使用unlink()
  • 如果更改选项,请确保代码包含一条语句以将其还原为原始选项。 (例如op <- par(mfrow=c(1,2)) ...some code... par(op)
  • 在一个新的空 R 会话中测试您的代码,以确保该代码可运行。人们应该能够在控制台中复制粘贴您的数据和代码,并获得与您完全相同的信息。

提供更多信息

在大多数情况下,仅 R 版本和操作系统就足够了。当软件包发生冲突时,提供sessionInfo()的输出确实可以提供帮助。在谈论与其他应用程序的连接(通过 ODBC 或其他方式)时,还应提供这些应用程序的版本号,并在可能的情况下还提供有关安装程序的必要信息。

如果使用rstudioapi::versionInfo()R Studio中运行 R,则有助于报告 RStudio 版本。

如果您对特定软件包有packageVersion("name of the package") ,则可能需要通过提供packageVersion("name of the package")的输出来提供软件包的版本。


1 注意:在 R> 3.6.0 与以前的版本之间, set.seed()的输出有所不同。请务必指定用于随机过程的 R 版本,如果在遵循旧问题时得到的结果略有不同,请不要感到惊讶。为了得到同样的结果在这种情况下,你可以使用RNGversion() - 函数之前set.seed()如: RNGversion("3.5.2")

(这是我从 “ 如何编写可重现的示例” 中获得的建议 。我试图使它简短但精巧)

如何编写可复制的示例。

如果您提供一个可复制的示例,则很可能会在 R 问题上获得良好的帮助。一个可复制的示例允许其他人仅通过复制和粘贴 R 代码来重新创建您的问题。

要使示例可重现,需要包括四件事:所需的程序包,数据,代码和对 R 环境的描述。

  • 程序包应加载到脚本的顶部,因此很容易看到示例所需的程序包。

  • 在电子邮件或堆栈溢出问题中包含数据的最简单方法是使用dput()生成 R 代码以重新创建它。例如,要在 R 中重新创建mtcars数据集,我将执行以下步骤:

    1. 在 R 中运行dput(mtcars)
    2. 复制输出
    3. 在我的可复制脚本中,键入mtcars <-然后粘贴。
  • 花一点时间来确保您的代码易于他人阅读:

    • 确保您使用了空格,并且变量名简洁明了,但内容丰富

    • 使用注释指示您的问题所在

    • 尽力删除与该问题无关的所有内容。
      您的代码越短,越容易理解。

  • 在代码的注释中包含sessionInfo()的输出。这概括了您的R 环境 ,并使您可以轻松检查是否使用了过时的软件包。

您可以通过启动一个新的 R 会话并粘贴您的脚本来检查您是否确实制作了一个可复制的示例。

在将所有代码放入电子邮件之前,请考虑将其放在Gist github 上 。它将为您的代码提供出色的语法高亮显示,并且您不必担心电子邮件系统会干扰任何事情。

就个人而言,我更喜欢 “一个” 衬垫。大致情况:

my.df <- data.frame(col1 = sample(c(1,2), 10, replace = TRUE),
        col2 = as.factor(sample(10)), col3 = letters[1:10],
        col4 = sample(c(TRUE, FALSE), 10, replace = TRUE))
my.list <- list(list1 = my.df, list2 = my.df[3], list3 = letters)

数据结构应该模仿作者问题的概念,而不是精确的逐字记录结构。当变量不覆盖我自己的变量或禁止使用函数(例如df )时,我真的很感激。

或者,可以偷偷摸摸地指出一个预先存在的数据集,例如:

library(vegan)
data(varespec)
ord <- metaMDS(varespec)

不要忘记提及您可能正在使用的任何特殊软件包。

如果要在较大的物体上演示某些东西,可以尝试

my.df2 <- data.frame(a = sample(10e6), b = sample(letters, 10e6, replace = TRUE))

如果通过raster包处理空间数据,则可以生成一些随机数据。在包装插图中可以找到很多示例,但这是一个小块。

library(raster)
r1 <- r2 <- r3 <- raster(nrow=10, ncol=10)
values(r1) <- runif(ncell(r1))
values(r2) <- runif(ncell(r2))
values(r3) <- runif(ncell(r3))
s <- stack(r1, r2, r3)

如果需要sp实现的某些空间对象,则可以通过 “spatial” 包中的外部文件(例如 ESRI shapefile)获得一些数据集(请参阅 “任务视图” 中的 Spatial 视图)。

library(rgdal)
ogrDrivers()
dsn <- system.file("vectors", package = "rgdal")[1]
ogrListLayers(dsn)
ogrInfo(dsn=dsn, layer="cities")
cities <- readOGR(dsn=dsn, layer="cities")

受此启发,我现在使用了一个方便的功能
当我需要发布到 StackOverflow 时reproduce(<mydata>)


快速说明

如果myData是要复制的对象的名称,请在 R 中运行以下命令:

install.packages("devtools")
library(devtools)
source_url("https://raw.github.com/rsaporta/pubR/gitbranch/reproduce.R")

reproduce(myData)

细节:

此函数是dput的智能包装器,它执行以下操作:

  • 自动对大型数据集进行采样(基于大小和类别。可以调整样本大小)
  • 创建一个dput输出
  • 允许您指定要导出的列
  • 追加到它的前面objName <- ...以便可以轻松地复制粘贴,但是...
  • 如果在 Mac 上运行,则输出会自动复制到剪贴板,以便您可以简单地运行它,然后粘贴到您的问题中。

来源在这里可用:


例:

# sample data
DF <- data.frame(id=rep(LETTERS, each=4)[1:100], replicate(100, sample(1001, 100)), Class=sample(c("Yes", "No"), 100, TRUE))

DF 为 100 x102。我想对 10 行采样,并指定几列

reproduce(DF, cols=c("id", "X1", "X73", "Class"))  # I could also specify the column number.

提供以下输出:

This is what the sample looks like: 

    id  X1 X73 Class
1    A 266 960   Yes
2    A 373 315    No            Notice the selection split 
3    A 573 208    No           (which can be turned off)
4    A 907 850   Yes
5    B 202  46   Yes         
6    B 895 969   Yes   <~~~ 70 % of selection is from the top rows
7    B 940 928    No
98   Y 371 171   Yes          
99   Y 733 364   Yes   <~~~ 30 % of selection is from the bottom rows.  
100  Y 546 641    No        


    ==X==============================================================X==
         Copy+Paste this part. (If on a Mac, it is already copied!)
    ==X==============================================================X==

 DF <- structure(list(id = structure(c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 25L, 25L, 25L), .Label = c("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y"), class = "factor"), X1 = c(266L, 373L, 573L, 907L, 202L, 895L, 940L, 371L, 733L, 546L), X73 = c(960L, 315L, 208L, 850L, 46L, 969L, 928L, 171L, 364L, 641L), Class = structure(c(2L, 1L, 1L, 2L, 2L, 2L, 1L, 2L, 2L, 1L), .Label = c("No", "Yes"), class = "factor")), .Names = c("id", "X1", "X73", "Class"), class = "data.frame", row.names = c(1L, 2L, 3L, 4L, 5L, 6L, 7L, 98L, 99L, 100L)) 

    ==X==============================================================X==

还要注意,输出的整体都在一条漂亮的长行中,而不是一整段的短线。这样可以更轻松地阅读 SO 问题帖子,也更易于复制和粘贴。


2013 年 10 月更新:

现在,您可以指定要占用多少行文本输出(即,将粘贴到 StackOverflow 中的内容)。 lines.out=n请使用lines.out=n参数。例:

reproduce(DF, cols=c(1:3, 17, 23), lines.out=7)得出:

==X==============================================================X==
         Copy+Paste this part. (If on a Mac, it is already copied!)
    ==X==============================================================X==

 DF <- structure(list(id = structure(c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 25L,25L, 25L), .Label
      = c("A", "B", "C", "D", "E", "F", "G", "H","I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U","V", "W", "X", "Y"), class = "factor"),
      X1 = c(809L, 81L, 862L,747L, 224L, 721L, 310L, 53L, 853L, 642L),
      X2 = c(926L, 409L,825L, 702L, 803L, 63L, 319L, 941L, 598L, 830L),
      X16 = c(447L,164L, 8L, 775L, 471L, 196L, 30L, 420L, 47L, 327L),
      X22 = c(335L,164L, 503L, 407L, 662L, 139L, 111L, 721L, 340L, 178L)), .Names = c("id","X1",
      "X2", "X16", "X22"), class = "data.frame", row.names = c(1L,2L, 3L, 4L, 5L, 6L, 7L, 98L, 99L, 100L))

    ==X==============================================================X==

这是一个很好的指南

最重要的一点是: 只需确保您编写一小段代码即可运行以查看问题所在 。一个有用的函数是dput() ,但是如果您有非常大的数据,则可能要制作一个小的样本数据集或仅使用前 10 行左右。

编辑:

还要确保您确定问题出在哪里。该示例不应是带有 “在线 200 处存在错误” 的整个 R 脚本。如果您使用 R(我喜欢browser() )和 Google 中的调试工具,则您应该能够真正确定问题出在哪里,并重现一个简单的例子,其中同样的事情出了问题。

R-help 邮件列表的发布指南涵盖了提问和回答问题,包括生成数据的示例:

示例:有时它有助于提供一个可以使某人实际运行的小示例。例如:

如果我有一个矩阵 x,如下所示:

> x <- matrix(1:8, nrow=4, ncol=2,
                dimnames=list(c("A","B","C","D"), c("x","y"))
  > x
    x y
  A 1 5
  B 2 6
  C 3 7
  D 4 8
  >

我如何将其转换为具有 8 行,名为 “row”,“col” 和 “value” 的三列的数据框,它们的维名称分别为 “row” 和 “col” 的值,如下所示:

> x.df
     row col value
  1    A   x      1

...
(答案可能是:

> x.df <- reshape(data.frame(row=rownames(x), x), direction="long",
                    varying=list(colnames(x)), times=colnames(x),
                    v.names="value", timevar="col", idvar="row")

这个词特别重要。您应该以最小的可复制示例为目标,这意味着数据和代码应尽可能简单以解释问题。

编辑:漂亮的代码比丑陋的代码更容易阅读。使用样式指南

从 R.2.14 开始(我想),您可以将数据文本表示形式直接提供给read.table

df <- read.table(header=TRUE, 
  text="Sepal.Length Sepal.Width Petal.Length Petal.Width Species
1          5.1         3.5          1.4         0.2  setosa
2          4.9         3.0          1.4         0.2  setosa
3          4.7         3.2          1.3         0.2  setosa
4          4.6         3.1          1.5         0.2  setosa
5          5.0         3.6          1.4         0.2  setosa
6          5.4         3.9          1.7         0.4  setosa
")

有时,无论您如何努力,使用较小的数据实际上都无法重现该问题,并且对于合成数据也不会发生此问题(尽管显示如何生成重现该问题的合成数据集很有用,因为它排除了一些假设)。

  • 可能需要将数据发布到某个地方的 Web 并提供 URL。
  • 如果这些数据不能公开发布,但可以共享,那么您可以通过电子邮件将其发送给感兴趣的各方(尽管这样可以减少工作人员的数量)在上面)。
  • 我实际上还没有看到这样做,因为无法发布数据的人对发布任何形式的数据都很敏感,但是在某些情况下,如果数据被充分匿名 / 扰乱 / 损坏,仍然可以发布数据似乎是合理的。某种程度上来说。

如果您不能做任何一个,那么您可能需要聘请顾问来解决您的问题...

编辑 :两个有用的匿名 / 加扰的问题:

到目前为止,对于可重复性部分,答案显然是非常好的。这仅仅是为了阐明可复制的示例不能也不应该成为问题的唯一组成部分。不要忘了解释您希望它看起来像什么以及问题的轮廓,而不仅仅是解释您到目前为止如何尝试实现。代码是不够的。您还需要文字。

以下是可避免的操作的可重现示例(从真实示例中得出,为了保护无辜者,更改了名称):


以下是示例数据和我遇到的部分功能。

code
code
code
code
code (40 or so lines of it)

我该如何实现?


我有一个非常简单有效的方法来制作上面没有提到的 R 示例。您可以先定义您的结构。例如,

mydata <- data.frame(a=character(0), b=numeric(0),  c=numeric(0), d=numeric(0))

>fix(mydata)

当您执行“修复”命令时,您将获得此弹出框

然后,您可以手动输入数据。对于较小的示例而不是较大的示例,这是有效的。