Вопрос: Как сделать отличный R воспроизводимый пример?


При обсуждении работы с коллегами, преподавании, отправке отчета об ошибке или поиске инструкций в списках рассылки, и здесь, на SO, воспроизводимый пример часто спрашивают и всегда помогают.

Каковы ваши советы по созданию отличного примера? Как вставить структуры данных из в текстовом формате? Какую еще информацию вы должны включить?

Существуют ли другие приемы в дополнение к использованию dput(), dump()или structure()? Когда вы должны включить library()или require()заявления? Какие зарезервированные слова следует избегать, в дополнение к c, df, data, и т.д?

Как сделать отличный воспроизводимый пример?


2339


источник


Ответы:


Минимальный воспроизводимый пример состоит из следующих элементов:

  • минимальный набор данных, необходимый для воспроизведения ошибки
  • минимальный работоспособный код, необходимый для воспроизведения ошибки, которая может быть запущена в данном наборе данных.
  • необходимую информацию о используемых пакетах, версию R и систему, на которой она запущена.
  • в случае случайных процессов семя (установленное set.seed()) для воспроизводимости

Часто полезно ознакомиться с примерами в файлах справки используемых функций. В общем, весь приведенный здесь код удовлетворяет требованиям минимального воспроизводимого примера: предоставляются данные, предоставляется минимальный код, и все выполняется.

Создание минимального набора данных

В большинстве случаев это можно легко сделать, просто предоставив вектор / фрейм данных с некоторыми значениями. Или вы можете использовать один из встроенных наборов данных, которые предоставляются с большинством пакетов.
Полный список встроенных наборов данных можно увидеть с помощью 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(), Следует обратить внимание на имена записей в кадре данных и не сделать их чрезмерно сложными.

Пример :

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

Для некоторых вопросов могут потребоваться конкретные форматы. Для этого можно использовать любой из предоставленных as.someTypeфункции: as.factor, as.Date, as.xts, ... Они в сочетании с векторными и / или фреймами данных.

Скопируйте данные

Если у вас есть некоторые данные, которые было бы слишком сложно построить с помощью этих советов, вы всегда можете сделать подмножество исходных данных, используя, например, head(), subset()или индексы. Затем используйте, например. dput()чтобы дать нам что-то, что можно сразу положить в R:

> dput(head(iris,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 = 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(head(iris, 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заключается в том, что он не будет работать для ключей data.tableобъектов или для сгруппированных tbl_df(класс grouped_df) из dplyr, В этих случаях вы можете конвертировать обратно в обычный фрейм данных перед совместным использованием, dput(as.data.frame(my_data)),

В худшем случае вы можете предоставить текстовое представление, которое можно прочитать при использовании textпараметр read.table:

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)

Создание минимального кода

Это должно быть легкой частью, но часто это не так. То, что вы не должны делать, это:

  • добавьте все виды преобразований данных. Убедитесь, что предоставленные данные уже находятся в правильном формате (если это не проблема, конечно)
  • копировать-вставить целую функцию / кусок кода, который дает ошибку. Сначала попробуйте найти, какие строки точно приводят к ошибке. Чаще всего вы узнаете, в чем проблема.

Что вы должны делать, это:

  • добавьте, какие пакеты следует использовать, если вы используете их.
  • если вы открываете подключения или make-файлы, добавьте код, чтобы закрыть их или удалить файлы (используя unlink())
  • если вы измените параметры, убедитесь, что код содержит инструкцию, чтобы вернуть их обратно к исходным. (например op <- par(mfrow=c(1,2)) ...some code... par(op))
  • проверите свой код в новом, пустом сеансе R, чтобы убедиться, что код запущен. Люди должны иметь возможность просто скопировать ваши данные и ваш код в консоли и получить то же самое, что и у вас.

Предоставьте дополнительную информацию

В большинстве случаев достаточно только версии R и операционной системы. Когда возникают конфликты с пакетами, выводятся результаты sessionInfo()может действительно помочь. Говоря о подключении к другим приложениям (будь то через ODBC или что-то еще), нужно также указать номера версий для них и, если возможно, также необходимую информацию об установке.

Если вы используете R в R Studio с помощью rstudioapi::versionInfo()может помочь сообщить о вашей версии RStudio.

Если у вас возникла проблема с конкретным пакетом, вы можете предоставить версию пакета, указав вывод packageVersion("name of the package"),


1427



(Вот мой совет от Как написать воспроизводимый пример , Я пытался сделать это коротким, но сладким)

Как написать воспроизводимый пример.

Вы, скорее всего, получите хорошую помощь с вашей проблемой R, если вы предоставите воспроизводимый пример. Воспроизводимый пример позволяет кому-то еще воссоздать вашу проблему, просто скопировав и вставив R-код.

Вам необходимо включить четыре варианта, чтобы сделать ваш пример воспроизводимым: требуемые пакеты, данные, код и описание вашей среды R.

  • пакеты должен быть загружен в верхней части сценария, поэтому легко посмотрите, какие из них нужны в этом примере.

  • Самый простой способ включить данные в элементе электронной почты или переполнении стека используется dput()генерировать R-код для его воссоздания. Например, чтобы воссоздать mtcarsнабор данных в R, Я бы выполнил следующие шаги:

    1. Бег dput(mtcars)в R
    2. Скопируйте вывод
    3. В моем воспроизводимом скрипте введите mtcars <-затем вставьте.
  • Потратьте немного времени, чтобы код для других легко читать:

    • убедитесь, что вы использовали пробелы, а имена переменных краткие, но информативный

    • используйте комментарии, чтобы указать, где находится ваша проблема.

    • сделайте все возможное, чтобы удалить все, что не связано с проблемой.
      Чем короче ваш код, тем легче его понимать.

  • Включить вывод sessionInfo()в комментарии в вашем коде. Это суммирует ваш р Окружающая среда и позволяет легко проверить, используете ли вы устаревшие пакет.

Вы можете проверить, что вы действительно сделали воспроизводимый пример, запустив новый сеанс R и вставив свой скрипт.

Прежде чем поместить весь свой код в электронное письмо, подумайте о его включении Gist github , Это придаст вашему коду приятный синтаксический подсветка, и вам не придется беспокоиться о чем-либо, искаженном системой электронной почты.


507



Лично я предпочитаю «один» лайнер. Что-то вроде строк:

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, вы можете получить некоторые наборы данных через внешние файлы (например, шейп-файл ESRI) в «пространственных» пакетах (см. Пространственный вид в представлении задач).

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

256



Inspired by this very post, I now use a handy function
reproduce(<mydata>) when I need to post to StackOverflow.


QUICK INSTRUCTIONS

If myData is the name of your object to reproduce, run the following in R:

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

reproduce(myData)

Details:

This function is an intelligent wrapper to dput and does the following:

  • automatically samples a large data set (based on size and class. Sample size can be adjusted)
  • creates a dput output
  • allows you to specify which columns to export
  • appends to the front of it objName <- ... so that it can be easily copy+pasted, but...
  • If working on a mac, the output is automagically copied to the clipboard, so that you can simply run it and then paste to your question.

The source is available here:


Example:

# 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 is about 100 x 102. I want to sample 10 rows, and a few specific columns

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==

Notice also that the entirety of the output is in a nice single, long line, not a tall paragraph of chopped up lines. This makes it easier to read on SO questions posts and also easier to copy+paste.


Update Oct 2013:

You can now specify how many lines of text output will take up (ie, what you will paste into StackOverflow). Use the lines.out=n argument for this. Example:

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

    ==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==

239



Here is a good guide:

http://www.r-bloggers.com/three-tips-for-posting-good-questions-to-r-help-and-stack-overflow/

But the most important is: Just make sure that you make a small piece of code that we can run to see what the problem is. A usefull function for this is dput(), but if you have very large data you might want to make a small sample dataset or only use the first 10 lines or so.

EDIT:

Also make sure that you identified where the problem is yourself. The example should not be an entire R script with "On line 200 there is an error". If you use the debugging tools in R (I love browser()) and google you should be able to really identify where the problem is and reproduce a trivial example in which the same thing goes wrong.


167



The R-help mailing list has a posting guide which covers both asking and answering questions, including an example of generating data:

Examples: Sometimes it helps to provide a small example that someone can actually run. For example:

If I have a matrix x as follows:

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

how can I turn it into a dataframe with 8 rows, and three columns named 'row', 'col', and 'value', which have the dimension names as the values of 'row' and 'col', like this:

  > x.df
     row col value
  1    A   x      1

...
(To which the answer might be:

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

)

The word small is especially important. You should be aiming for a minimal reproducible example, which means that the data and the code should be as simple as possible to explain the problem.

EDIT: Pretty code is easier to read than ugly code. Use a style guide.


141



Since R.2.14 (I guess) you can feed your data text representation directly to read.table:

df <- read.table(header=T, 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
") 

136



Sometimes the problem really isn't reproducible with a smaller piece of data, no matter how hard you try, and doesn't happen with synthetic data (although it's useful to show how you produced synthetic data sets that did not reproduce the problem, because it rules out some hypotheses).

  • Posting the data to the web somewhere and providing a URL may be necessary.
  • If the data can't be released to the public at large but could be shared at all, then you may be able to offer to e-mail it to interested parties (although this will cut down the number of people who will bother to work on it).
  • I haven't actually seen this done, because people who can't release their data are sensitive about releasing it any form, but it would seem plausible that in some cases one could still post data if it were sufficiently anonymized/scrambled/corrupted slightly in some way.

If you can't do either of these then you probably need to hire a consultant to solve your problem ...

edit: Two useful SO questions for anonymization/scrambling:


126