如何制作一个优秀的R可重现示例

2466

当与同事讨论性能、教学、发送错误报告或在邮件列表和Stack Overflow上寻求指导时,通常会要求提供可重现的示例,这总是很有帮助的。

您创建优秀示例的建议是什么?如何将的数据结构粘贴到文本格式中?还应包括哪些其他信息?

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

如何创建一个优秀的可重现示例?


34
我对问题的范围感到困惑。人们似乎在 SO 或 R-help 上询问问题时,已经理解了“重现错误”的含义。那么关于帮助页面中可重现的 R 示例呢?关于包演示?关于教程/演讲? - baptiste
15
相同之处在于减去误差。我解释的所有技术都被用在包帮助页面以及我关于R的教程和演示中。 - Joris Meys
33
有时候数据是限制因素,因为结构可能过于复杂难以模拟。生成公共数据要从私有数据开始:https://dev59.com/HGkv5IYBdhLWcg3wpCXJ#10458688 在 https://dev59.com/HGkv5IYBdhLWcg3wpCXJ#10458688 - Etienne Low-Décarie
23个回答

1969
基本上,一个最小可复现示例(MRE)应该能让其他人在他们的机器上“精确地”重现你的问题。
请不要发布数据、代码或控制台输出的图像!
简要总结
MRE包括以下内容:
- 一个必要的最小数据集,用于演示问题 - 必要的最小可运行代码,可以在给定的数据集上运行以重现问题 - 所有使用的库、R版本和操作系统的必要信息,可能还包括sessionInfo() - 对于随机过程,设置种子(通过set.seed())以使其他人能够完全复制你所得到的相同结果
要查看好的MRE示例,请参阅您正在使用的函数帮助页面底部的“示例”部分。只需在R控制台中键入例如help(mean)或简写为?mean
提供一个最小数据集
通常,共享大型数据集是不必要的,而且可能会让其他人对您的问题失去兴趣。因此,最好使用内置数据集或创建一个类似于原始数据的小型“玩具”示例,这实际上就是所谓的“最小”。如果出于某种原因您确实需要共享原始数据,您应该使用dput()等方法,以便他人可以获得您数据的精确副本。
内置数据集
您可以使用其中一个内置数据集。可以使用data()查看内置数据集的全面列表。每个数据集都有一个简短的描述,并且可以通过?iris等方式获取更多信息,例如用于R中的'iris'数据集。已安装的软件包可能包含其他数据集。
创建示例数据集

初步说明:有时您可能需要特殊格式(例如因子、日期或时间序列)的类别。对于这些情况,可以使用诸如as.factoras.Dateas.xts等函数。 示例:

d <- as.Date("2020-12-30")

哪里
class(d)
# [1] "Date"

向量

x <- rnorm(10)  ## random vector normal distributed
x <- runif(10)  ## random vector uniformly distributed    
x <- sample(1:100, 10)  ## 10 random draws out of 1, 2, ..., 100    
x <- sample(LETTERS, 10)  ## 10 random draws out of built-in latin alphabet

矩阵

m <- matrix(1:12, 3, 4, dimnames=list(LETTERS[1:3], LETTERS[1:4]))
m
#   A B C  D
# A 1 4 7 10
# B 2 5 8 11
# C 3 6 9 12

数据框架
set.seed(42)  ## for sake of reproducibility
n <- 6
dat <- data.frame(id=1:n, 
                  date=seq.Date(as.Date("2020-12-26"), as.Date("2020-12-31"), "day"),
                  group=rep(LETTERS[1:2], n/2),
                  age=sample(18:30, n, replace=TRUE),
                  type=factor(paste("type", 1:n)),
                  x=rnorm(n))
dat
#   id       date group age   type         x
# 1  1 2020-12-26     A  27 type 1 0.0356312
# 2  2 2020-12-27     B  19 type 2 1.3149588
# 3  3 2020-12-28     A  20 type 3 0.9781675
# 4  4 2020-12-29     B  26 type 4 0.8817912
# 5  5 2020-12-30     A  26 type 5 0.4822047
# 6  6 2020-12-31     B  28 type 6 0.9657529

注意:尽管广泛使用,最好不要将数据框命名为df,因为df()是R函数,用于计算F分布在点x处的密度(即曲线的高度),可能会与之冲突。
复制原始数据
如果您有特定的原因或者从中构建示例会很困难的数据,可以使用dput提供原始数据的一个小子集。
为什么使用dput()?
dput会在控制台上输出重现数据所需的所有信息。您只需复制输出并粘贴到问题中即可。
调用上面的dat会产生输出,但如果在问题中共享它,则仍缺少有关变量类别和其他特征的信息。此外,type列中的空格使其难以处理。即使我们打算使用这些数据,也无法正确获取您数据的重要特征。
  id       date group age   type         x
1  1 2020-12-26     A  27 type 1 0.0356312
2  2 2020-12-27     B  19 type 2 1.3149588
3  3 2020-12-28     A  20 type 3 0.9781675

筛选你的数据

要分享一个子集,可以使用 head()subset() 或索引 iris[1:4, ]。然后将其包装在 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")), row.names = c(NA, 
4L), class = "data.frame")

当使用时,您可能还想只包括相关列,例如: 注意:如果您的数据框中存在许多水平的factor,输出可能会很冗长,因为它仍然会列出所有可能的factor水平,即使它们不在数据子集中。为解决此问题,您可以使用函数。请注意下面的示例中,species是一个只有一个水平的factor,例如:。对于键入的对象或分组的(class )来说,无法工作。在这些情况下,在共享之前,您可以将其转换回常规数据框,。

生成最小代码

与最小数据配合使用(参见上文),您的代码应该通过简单地复制和粘贴在另一台机器上完全复现问题。
这应该是最简单的部分,但通常并非如此。以下是你不应该做的事情:
- 显示各种数据转换;确保提供的数据已经处于正确的格式中(除非这就是问题所在) - 复制粘贴一个整个脚本,在某个地方出现错误。尝试定位导致错误的具体行。往往你会发现问题所在。
以下是你应该做的事情:
- 如果使用了任何包,请添加使用的包(使用library()) - 在新的R会话中测试运行代码,以确保代码可运行。人们应该能够将你的数据和代码复制粘贴到控制台中,并得到与你相同的结果。 - 如果打开连接或创建文件,请添加一些代码来关闭它们或删除文件(使用unlink()) - 如果更改选项,请确保代码包含将其恢复为原始选项的语句。(例如:op <- par(mfrow=c(1,2)) ...some code... par(op)
提供必要的信息
在大多数情况下,只需要提供R的版本和操作系统即可。当出现包冲突时,提供sessionInfo()的输出可以非常有帮助。当谈论与其他应用程序的连接(无论是通过ODBC还是其他方式)时,还应提供这些应用程序的版本号,并尽可能提供设置的必要信息。
如果您在使用R Studio中运行R,可以通过rstudioapi::versionInfo()来报告您的RStudio版本。
如果您遇到特定包的问题,可以通过给出packageVersion("包名称")的输出来提供包的版本。

种子

使用set.seed()可以指定种子1,即R的随机数生成器的特定状态。这使得随机函数(例如sample()rnorm()runif()等)始终返回相同的结果,例如:
set.seed(42)
rnorm(3)
# [1]  1.3709584 -0.5646982  0.3631284

set.seed(42)
rnorm(3)
# [1]  1.3709584 -0.5646982  0.3631284

1注意:set.seed()的输出在R版本大于3.6.0和之前版本之间有所不同。请指定您用于随机过程的R版本,并不要感到惊讶,如果您按照旧问题进行操作时得到稍微不同的结果。为了在这种情况下得到相同的结果,您可以在set.seed()之前使用RNGversion()函数(例如:RNGversion("3.5.2"))。


1
我个人的偏好之一是:我喜欢对Knuth的文学编程进行一些变体。变量和操作需要通过代码注释或文字来描述。 - EngrStudent

652

关于如何编写可重现的示例,这是我的建议(如何编写可重现的示例)。我尝试让它简短而精炼。《R4DS》中“工作流:获取帮助”的第9.2节是一个更近期的版本,还讨论了reprex包。

如何编写可重现的示例

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

为使示例可重现,您需要包括四个内容:所需的软件包、数据、代码以及您的R环境的描述。

  • 程序包应该在脚本顶部加载,这样很容易看到示例需要哪些程序包。

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

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

    • 确保使用了空格并且变量名简洁但具有说明性

    • 使用注释指出问题所在

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

  • 在代码注释中包含sessionInfo()的输出。这概括了您的R环境,并使检查是否使用过时的程序包变得容易。

您可以通过启动一个新的 R 会话并将脚本粘贴进去来检查是否已经创建了可重现的示例。

在将所有代码放入电子邮件之前,请考虑将其放在 Gist github 上。这将为您的代码提供漂亮的语法高亮,并且您不必担心任何东西被电子邮件系统破坏。


38
tidyverse 中的 reprex 是一个很好的包,用于生成最小化的、可重现的示例代码:https://github.com/tidyverse/reprex - mt1022
29
我经常收到带有代码的电子邮件,甚至收到带有代码的附加Word文档的电子邮件。有时候我甚至会收到带有代码截图的附加Word文档的电子邮件。 - hadley
如果它是一个图形对象呢?对于图形,dput()不幸地返回了一长串向量。 - Grace
与空间数据(如“sf” tibble)相同。即使只剩下几行,根据我的经验,这些数据似乎也无法很好地与像“dput”这样的工具配合使用。 - Francis Barton
如何在Windows中将dput(mtcars)的输出直接复制到剪贴板? - Julien

346

就我个人而言,我更喜欢"一行代码"。例如:

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包处理空间数据,可以生成一些随机数据。该包的vignette中可以找到许多示例,但这里提供了一个小技巧。

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中实现的空间对象,你可以通过外部文件(如ESRI shapefile)在“空间”包中获取一些数据集(请查看任务视图中的空间视图)。
library(rgdal)
ogrDrivers()
dsn <- system.file("vectors", package = "rgdal")[1]
ogrListLayers(dsn)
ogrInfo(dsn=dsn, layer="cities")
cities <- readOGR(dsn=dsn, layer="cities")

321

受到这个帖子的启发,现在我在需要发布到Stack Overflow时使用一个方便的函数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 x 102个数据。我想要抽取10行和一些指定列。

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

Gives the following output:

输出如下:
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==

注意输出的整个内容都在一行中,而不是被分成多行段落。

这样做可以使 Stack Overflow 上的问题帖子更易于阅读,也更容易复制粘贴。


2013年10月更新:

现在您可以指定输出文本占用的行数(即,您将粘贴到 Stack Overflow 上的内容)。使用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==

1
在每个会话中使用 reproduce 函数,是否需要运行 library(devtools);source_url("https://raw.github.com/rsaporta/pubR/gitbranch/reproduce.R") - Julien
控制台中的输出数据不幸地不在一行上,至少对我来说在Windows 11上是这样。 - Julien
要将dput的输出复制到一行中,请运行writeClipboard(paste(capture.output(dput(DF)), collapse = "")) - Julien

226

这里有一份很好的指南

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

编辑:

另外,请确保您已经自己确定了问题的位置。示例不应该是一个完整的R脚本,其中包含“第200行出错”。如果您使用R中的调试工具(我喜欢browser())和Google,则应该能够真正确定问题的位置,并在其中再现相同的错误。


191

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")

)

重点是“小”。您应该尽力创建一个最小化的可复现示例,这意味着数据和代码应尽可能简单以解释问题。

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


189
自 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
") 

166
有时,无论如何努力,使用更小的数据集也不能复现问题,并且问题在合成数据中也不会发生(尽管展示如何生成未能复现问题的合成数据集是有用的,因为它排除了一些假设)。
  • 将数据发布到网络上并提供一个URL可能是必要的。
  • 如果数据不能公开发布,但可以分享给感兴趣的各方,则您可以提供发送电子邮件的方式(尽管这将减少愿意参与解决的人数)。
  • 我实际上从未见过这样做的情况,因为无法发布数据的人对以任何形式发布数据都非常敏感,但某些情况下,如果数据经过足够匿名化/混淆/稍微损坏,仍然可以发布数据。

如果您无法这样做,则可能需要聘请顾问来解决您的问题...

编辑: 匿名化/混淆的两个有用SO问题:


3
为了生成人造数据集,这个问题的答案提供了有用的示例,其中包括fitdistrfitdistrplus的应用。 - Iterator
我真的很想得到一些关于提供空间数据示例的建议,例如在一个几何列中具有大量坐标的sf tibble。即使只有少数行数据,这些数据似乎也无法完全复制到剪贴板中使用dput。虽然可以在reprex中使用内置的sf数据集,但有时需要提供自己数据的样本,因为正是该数据的某些特定方面导致了问题。 - Francis Barton

155

到目前为止,回答中肯定已经涵盖了可复现性的部分。这仅是为了澄清一个问题:可重现的示例不能且不应该是问题的唯一组成部分。不要忘记解释你想让它看起来像什么以及你问题的轮廓,而不仅仅是你已经尝试过的方法。代码是不够的,你还需要用言语表达。

以下是我遇到问题的样本数据和函数的一部分,这是一个需要避免的可重现示例(从一个真实的示例中摘录,名称已更改以保护无辜者):

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

我该如何实现这个功能?



144

我有一种非常简单高效的方法来制作一个之前未提到的R代码示例。 首先,您可以先定义您的结构。例如:

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

>fix(mydata)

当您执行 'fix' 命令时,将会弹出此窗口

然后您可以手动输入数据。对于小的例子而言,这是高效的方式。


24
那么,dput(mydata)的意思是将mydata对象转换为可复制的R语言代码并输出。 - GSee
你的前端是什么?有一个完整的回答会很好。例如,制作一个数据,你可以直接像 for (d in data) {...} 这样循环使用。 - Léo Léopold Hertz 준영

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接