在R中简化dput()函数

49

我希望能以透明的方式为 SO 回答添加数据。我的经验是,dput() 函数产生的 structure 对象有时会使初学者感到困惑。然而,我又没有耐心每次都将它复制/粘贴到一个简单的数据框中,并希望自动化这个过程。需要类似于 dput(),但是更简化的版本。

比方说,我通过复制/粘贴或其他方式拥有以下数据:

Df <- data.frame(A = c(2, 2, 2, 6, 7, 8),
                 B = c("A", "G", "N", NA, "L", "L"),
                 C = c(1L, 3L, 5L, NA, NA, NA))

看起来就像这样,

Df
#>   A    B  C
#> 1 2    A  1
#> 2 2    G  3
#> 3 2    N  5
#> 4 6 <NA> NA
#> 5 7    L NA
#> 6 8    L NA

在一个整数、一个因子和一个数值向量之内,

str(Df)
#> 'data.frame':    6 obs. of  3 variables:
#>  $ A: num  2 2 2 6 7 8
#>  $ B: Factor w/ 4 levels "A","G","L","N": 1 2 4 NA 3 3
#>  $ C: int  1 3 5 NA NA NA

现在,我想在SO上分享这个内容,但我并不总是有它来自的原始数据框。更多时候我使用pipe()从SO导入,并且我知道唯一将其导出的方法是dput()。例如:

dput(Df)
#> structure(list(A = c(2, 2, 2, 6, 7, 8), B = structure(c(1L, 2L, 
#> 4L, NA, 3L, 3L), .Label = c("A", "G", "L", "N"), class = "factor"), 
#> C = c(1L, 3L, 5L, NA, NA, NA)), .Names = c("A", "B", "C"), row.names = c(NA, 
#> -6L), class = "data.frame")

但是,正如我在开头所说的,这些结构体看起来可能相当令人困惑。出于这个原因,我正在寻找一种以某种方式压缩dput()输出的方法。我想象一种类似于下面这样的输出:

dput_small(Df)
#> data.frame(A = c(2, 2, 2, 6, 7, 8), B = c("A", "G", "N", NA, "L", "L"),
#> C = c(1L, 3L, 5L, NA, NA, NA))

这是可能的吗?我知道还有其他类别,比如liststbltbl_df等。


我们可以使用 dput 将其输出到文件中,然后再使用 readLines 函数读取并进行一些 正则表达式 处理。 - zx8754
7个回答

41

编辑:将旧解决方案保留在底部,因为它获得了悬赏和许多投票,但提出了改进的答案

你可以使用{constructive} package

# install.packages("constructive")
# or dev version:
# remotes::install_github("cynkra/constructive")
Df <- data.frame(A = c(2, 2, 2, 6, 7, 8),
                 B = c("A", "G", "N", NA, "L", "L"),
                 C = c(1L, 3L, 5L, NA, NA, NA))

constructive::construct(Df)
#> data.frame(
#>   A = c(2, 2, 2, 6, 7, 8),
#>   B = c("A", "G", "N", NA, "L", "L"),
#>   C = c(1L, 3L, 5L, NA, NA, NA)
#> )

它具有许多常见类的自定义构造函数,因此应该能够以人类可读的方式忠实地复制大多数对象。

旧解决方案:

3个解决方案:

  • 一个围绕dput的包装器(处理标准的data.framestibbleslists

  • 一个read.table的解决方案(适用于data.frames

  • 一个tibble::tribble的解决方案(适用于data.frames,返回一个tibble

所有解决方案都包括nrandom参数,允许只dput数据的头部或在运行时对其进行抽样。

dput_small1(Df)
# Df <- data.frame(
#   A = c(2, 2, 2, 6, 7, 8),
#   B = structure(c(1L, 2L, 4L, NA, 3L, 3L), .Label = c("A", "G", "L", 
#     "N"), class = "factor"),
#   C = c(1L, 3L, 5L, NA, NA, NA) ,
#   stringsAsFactors=FALSE)

dput_small2(Df,stringsAsFactors=TRUE)
# Df <- read.table(sep="\t", text="
#   A   B   C
#   2   A    1
#   2   G    3
#   2   N    5
#   6   NA  NA
#   7   L   NA
#   8   L   NA", header=TRUE, stringsAsFactors=TRUE)

dput_small3(Df)
# Df <- tibble::tribble(
#   ~A, ~B, ~C,
#   2,           "A",          1L,
#   2,           "G",          3L,
#   2,           "N",          5L,
#   6, NA_character_, NA_integer_,
#   7,           "L", NA_integer_,
#   8,           "L", NA_integer_
# )
# Df$B <- factor(Df$B)

包装器 dput

这个选项的输出结果非常接近问题中提出的建议。它非常通用,因为实际上是在dput周围进行了包装,但是分别应用于列。

multiline意味着'保持dput的默认输出布局为多行'

dput_small1<- function(x,
                       name=as.character(substitute(x)),
                       multiline = TRUE,
                       n=if ('list' %in% class(x)) length(x) else nrow(x),
                       random=FALSE,
                       seed = 1){
  name
  if('tbl_df' %in% class(x)) create_fun <- "tibble::tibble" else
    if('list' %in% class(x)) create_fun <- "list" else
      if('data.table' %in% class(x)) create_fun <- "data.table::data.table" else
        create_fun <- "data.frame"
    
    if(random) {
      set.seed(seed)
      if(create_fun == "list") x <- x[sample(1:length(x),n)] else 
        x <- x[sample(1:nrow(x),n),]
    } else {
      x <- head(x,n)
    }
    
    line_sep <- if (multiline) "\n    " else ""
    cat(sep='',name," <- ",create_fun,"(\n  ",
        paste0(unlist(
          Map(function(item,nm) paste0(nm,if(nm=="") "" else " = ",paste(capture.output(dput(item)),collapse=line_sep)),
              x,if(is.null(names(x))) rep("",length(x)) else names(x))),
          collapse=",\n  "),
        if(create_fun == "data.frame") ",\n  stringsAsFactors = FALSE)" else "\n)")
}

dput_small1(list(1,2,c=3,d=4),"my_list",random=TRUE,n=3)
# my_list <- list(
#   2,
#   d = 4,
#   c = 3
# )

read.table解决方案

对于data.frames,我发现以更明确/表格化的格式输入更舒适。

可以使用read.table实现这一点,然后自动重新格式化read.table无法正确获取的列类型。虽然不如第一个解决方案通用,但对于在SO上找到的95%的情况都能顺利工作。

dput_small2 <- function(df,
                        name=as.character(substitute(df)),
                        sep='\t',
                        header=TRUE,
                        stringsAsFactors = FALSE,
                        n= nrow(df),
                        random=FALSE,
                        seed = 1){
    name
    if(random) {
      set.seed(seed)
      df <- df[sample(1:nrow(df),n),]
    } else {
      df <- head(df,n)
    }
  cat(sep='',name,' <- read.table(sep="',sub('\t','\\\\t',sep),'", text="\n  ',
      paste(colnames(df),collapse=sep))
  df <- head(df,n)
  apply(df,1,function(x) cat(sep='','\n  ',paste(x,collapse=sep)))
  cat(sep='','", header=',header,', stringsAsFactors=',stringsAsFactors,')')
  
  sapply(names(df), function(x){
    if(is.character(df[[x]]) & suppressWarnings(identical(as.character(as.numeric(df[[x]])),df[[x]]))){ # if it's a character column containing numbers
      cat(sep='','\n',name,'$',x,' <- as.character(', name,'$',x,')')
    } else if(is.factor(df[[x]]) & !stringsAsFactors) { # if it's a factor and conversion is not automated
      cat(sep='','\n',name,'$',x,' <- factor(', name,'$',x,')')
    } else if(inherits(df[[x]], "POSIXct")){
      cat(sep='','\n',name,'$',x,' <- as.POSIXct(', name,'$',x,')')
    } else if(inherits(df[[x]], "Date")){
      cat(sep='','\n',name,'$',x,' <- as.Date(', name,'$',x,')')
    }})
  invisible(NULL)
}

最简单的情况

dput_small2(iris,n=6)

将打印:
iris <- read.table(sep="\t", text="
  Sepal.Length  Sepal.Width Petal.Length    Petal.Width Species
  5.1   3.5 1.4 0.2  setosa
  4.9   3.0 1.4 0.2  setosa
  4.7   3.2 1.3 0.2  setosa
  4.6   3.1 1.5 0.2  setosa
  5.0   3.6 1.4 0.2  setosa
  5.4   3.9 1.7 0.4  setosa", header=TRUE, stringsAsFactors=FALSE)

执行时将返回:

#   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

str(iris)
# 'data.frame': 6 obs. of  5 variables:
# $ Sepal.Length: num  5.1 4.9 4.7 4.6 5 5.4
# $ Sepal.Width : num  3.5 3 3.2 3.1 3.6 3.9
# $ Petal.Length: num  1.4 1.4 1.3 1.5 1.4 1.7
# $ Petal.Width : num  0.2 0.2 0.2 0.2 0.2 0.4
# $ Species     : chr  " setosa" " setosa" " setosa" " setosa" ...

更复杂
虚拟数据:
test <- data.frame(a=1:5,
                   b=as.character(6:10),
                   c=letters[1:5],
                   d=factor(letters[6:10]),
                   e=Sys.time()+(1:5),
                   stringsAsFactors = FALSE)

这个:

dput_small2(test,'df2')

将打印:

df2 <- read.table(sep="\t", text="
  a b   c   d   e
  1 6   a   f   2018-02-15 11:53:17
  2 7   b   g   2018-02-15 11:53:18
  3 8   c   h   2018-02-15 11:53:19
  4 9   d   i   2018-02-15 11:53:20
  5 10  e   j   2018-02-15 11:53:21", header=TRUE, stringsAsFactors=FALSE)
df2$b <- as.character(df2$b)
df2$d <- factor(df2$d)
df2$e <- as.POSIXct(df2$e)

当执行时,它将返回:
#   a  b c d                   e
# 1 1  6 a f 2018-02-15 11:53:17
# 2 2  7 b g 2018-02-15 11:53:18
# 3 3  8 c h 2018-02-15 11:53:19
# 4 4  9 d i 2018-02-15 11:53:20
# 5 5 10 e j 2018-02-15 11:53:21

str(df2)    
# 'data.frame': 5 obs. of  5 variables:
# $ a: int  1 2 3 4 5
# $ b: chr  "6" "7" "8" "9" ...
# $ c: chr  "a" "b" "c" "d" ...
# $ d: Factor w/ 5 levels "f","g","h","i",..: 1 2 3 4 5
# $ e: POSIXct, format: "2018-02-15 11:53:17" "2018-02-15 11:53:18" "2018-02-15 11:53:19" "2018-02-15 11:53:20" ...

all.equal(df2,test)
# [1] "Component “e”: Mean absolute difference: 0.4574251" # only some rounding error

tribble解决方案

read.table选项非常易读,但不太通用。使用tribble几乎可以处理任何数据类型(尽管因子需要临时修复)。

这个解决方案对于OP的示例可能不太有用,但对于列表列非常好用(请参见下面的示例)。要使用输出结果,需要加载tibble库。

就像我的第一个解决方案一样,它是对dput的包装,但不是“dputting”列,而是“dputting”元素。

dput_small3 <- function(df,
                        name=as.character(substitute(df)),
                        n= nrow(df),
                        random=FALSE,
                        seed = 1){
  name
  if(random) {
    set.seed(seed)
    df <- df[sample(1:nrow(df),n),]
  } else {
    df <- head(df,n)
  }
  df1 <- lapply(df,function(col) if(is.factor(col)) as.character(col) else col)
  dputs   <- sapply(df1,function(col){
    col_dputs <- sapply(col,function(elt) paste(capture.output(dput(elt)),collapse=""))
    max_char <- max(nchar(unlist(col_dputs)))
    sapply(col_dputs,function(elt) paste(c(rep(" ",max_char-nchar(elt)),elt),collapse=""))
  })
  lines   <- paste(apply(dputs,1,paste,collapse=", "),collapse=",\n  ")
  output  <- paste0(name," <- tibble::tribble(\n  ",
                    paste0("~",names(df),collapse=", "),
                    ",\n  ",lines,"\n)")
  cat(output)
  sapply(names(df), function(x) if(is.factor(df[[x]])) cat(sep='','\n',name,'$',x,' <- factor(', name,'$',x,')'))
  invisible(NULL)
}

dput_small3(dplyr::starwars[c(1:3,11)],"sw",n=6,random=TRUE)
# sw <- tibble::tribble(
#   ~name, ~height, ~mass, ~films,
#   "Lando Calrissian", 177L,       79,                     c("Return of the Jedi", "The Empire Strikes Back"),
#      "Finis Valorum", 170L, NA_real_,                                                   "The Phantom Menace",
#       "Ki-Adi-Mundi", 198L,       82, c("Attack of the Clones", "The Phantom Menace", "Revenge of the Sith"),
#           "Grievous", 216L,      159,                                                  "Revenge of the Sith",
#     "Wedge Antilles", 170L,       77,       c("Return of the Jedi", "The Empire Strikes Back", "A New Hope"),
#         "Wat Tambor", 193L,       48,                                                 "Attack of the Clones"
# )

3
不错的解决方案... 如果能够随机抽取行而不仅是前n行就更好了(例如对于鸢尾花数据集...)。可以很容易地通过使用sample = NULL函数参数,然后在函数中使用if (!is.null(sample)) { df <- df[sample(1:nrow(df), sample),] } else { df <- head(df,n) }来实现。 - Gilles San Martin
1
完成了,我为两个解决方案实现了“head”和“sample”功能,并使第一个解决方案处理“lists”和“tibbles”。 - moodymudskipper
1
我觉得这个整洁的代码应该属于 [tag:tidyverse] 中的某个包。也许 David Robinson 愿意将其添加到他的 stackr 包或类似的包中。 - Eric Fail
1
tidyverse 让我想起了 tibble::tribble,所以我添加了第三个解决方案,并在第一个解决方案中支持了 data.table - moodymudskipper

23

这个包datapasta不会总是完美地工作,因为它目前不支持所有类型,但它很干净和容易使用,即

# install.packages(c("datapasta"), dependencies = TRUE)    
datapasta::dpasta(Df)
#> data.frame(
#>            A = c(2, 2, 2, 6, 7, 8),
#>            C = c(1L, 3L, 5L, NA, NA, NA),
#>            B = as.factor(c("A", "G", "N", NA, "L", "L"))
#> )

2
还有datapasta::dmdclip()函数,它可以将相同的输出放在剪贴板上,并且每行前面都有4个空格。;) - MilesMcBain
1
非常有趣。我不知道 [tag:datapasta] 包。谢谢! - Eric Fail
1
好的包和好的名称 - Giacomo
1
不是很相关,但是能否直接将输出复制到剪贴板中呢? - Julien
@Julien --> 当然(使用datapasta::dmdclip() - undefined

11

我们可以将控制器设为NULL以简化:

dput(Df, control = NULL)
# list(A = c(2, 2, 2, 6, 7, 8), B = c(NA, NA, NA, NA, 7, 9), C = c(1, 3, 5, NA, NA, NA))

然后将其包装在data.frame内:

data.frame(dput(Df, control = NULL))

编辑:为了避免因因子列被转换为数字而引起的问题,我们在调用dput之前可以将它们转换为字符格式:


dput_small <- function(d){
  ix <- sapply(d, is.factor)
  d[ix] <- lapply(d[ix], as.character)
  dput(d, control = NULL)
  }

1
如果数据量较小,可以使用dput;如果数据量较大,则使用df1 <- read.table(text = "my delimited data")。但是使用read.table会丢失属性,因此需要检查输出是否与预期相同。 - zx8754
@EricFail 看一下编辑,还有一个 reprex 包。 - zx8754
确实很不错。我不知道 [tag:reprex] 包。它应该有一个像这样的函数。 - Eric Fail
1
好主意。我会看看接下来几天会出现什么,考虑提交一些东西到 [tag:reprex]。我想一个 [tag:reprex] 函数也必须与 tbltbl_dfgrouped_df 以及可能更多兼容。 - Eric Fail
1
tidyverse网站建议使用dputheadsample函数。http://reprex.tidyverse.org/articles/reprex.html - moodymudskipper
显示剩余4条评论

11

你可以直接向一个压缩连接写入。

gz <- gzfile("foo.gz", open="wt")
dput(Df, gz)
close(gz)

我不确定我理解这个答案。您可以演示一下它提供的输出类型吗? - Eric Fail
您5年前的问题是:“我想让dput()以某种方式压缩输出。” 我的回答使用标准的gzip压缩方法来压缩输出。不清楚您所说的“以某种方式压缩”是否意味着“更改文本表示以使其更易于人类理解”。 - Joshua Ulrich
1
好观点!非常好的观点!在考虑原始措辞之后,我决定重新措辞并设置赏金。我想五年前我太害怕向您询问澄清问题了。无论如何,我当时想说的是,我正在寻找更清晰的输出来在SO(和其他地方)共享结构。感谢您的反馈! - Eric Fail
我为我最初发布时措辞不清而道歉。 - Eric Fail

3

通常在SO或其他地方,较大的dput难以处理。相反,您可以直接将结构保存到Rda文件中:

save(Df, file='foo.Rda')

然后将其读回:

load('foo.Rda')

请参考这个问题,以获取更多信息和应得的荣誉:如何在R中保存数据框? 您也可以查看 sink 函数...
如果我误解了您的问题,请随时解释一下为什么 dput 是您唯一的机制。

3

这里可能值得提及的是 memCompressmemDecompress。对于内存中的对象,它可以按照指定的方式压缩大型对象以减少其大小。而后者则将压缩逆转。它们在包对象中实际上非常有用。

sum(nchar(dput(DF)))
# [1] 64
( mDF <- memCompress(as.character(DF)) )
# [1] 78 9c 4b d6 30 d2 51 80 20 33 1d 05 73 1d 05 0b 4d ae 64 0d 3f 47 1d 05 64 0c 14 b7 04 89 1b ea 28 18 eb 28 98 22 4b 6a 02 00 a8 ba 0c d2
length(mDF)
# [1] 46
cat(mdDF <- memDecompress(mDF, "gzip", TRUE))
# c(2, 2, 2, 6, 7, 8)
# c(NA, NA, NA, NA, 7, 9)
# c(1, 3, 5, NA, NA, NA)
nchar(mdDF)
# [1] 66

我还没有确定数据框是否可以轻松地重新组装,但我相信它是可以的。


1
谢谢,很有趣。我希望你意识到我在2013年9月就问过这个问题了。不过,我很感激你的回答。 - Eric Fail
我做到了。我在寻找其他东西时偶然发现了这篇文章,这是一个好问题。此外,我一直在使用memCompress来处理一些包数据,所以我想分享一下。 - Rich Scriven
感谢您抽出时间分享,也感谢您的赞美之词。 - Eric Fail

3

还有一个我非常喜欢的包是read.so,特别适用于读取stackoverflow上的数据。它也可以用来读取tibbles。

#devtools::install_github("alistaire47/read.so")
Df <- data.frame(A = c(2, 2, 2, 6, 7, 8),
                 B = c("A", "G", "N", NA, "L", "L"),
                 C = c(1L, 3L, 5L, NA, NA, NA))

read.so::write.so(Df)

#> Df <- data.frame(
#>   A = c(2, 2, 2, 6, 7, 8),
#>   B = c("A", "G", "N", NA, "L", "L"),
#>   C = c(1L, 3L, 5L, NA, NA, NA)
#> )

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