Splicing/cleaning 数据放入整齐的数据框

Splicing/cleaning data into neat dataframe

我是 R 的新手(以及 Whosebug,因此项目符号仅代表新行)并且被指派从事一个项目,在该项目中我需要将 MEDLINE 数据清理成一个整洁的数据框。原始 .txt 文件的示例如下:

 PMID- 28152974 
 OWN - NLM 
 IS  - 1471-230X (Electronic) 
 IS  - 1471-230X (Linking) 
 PMID- 28098115 
 OWN - NLM 
 IP  - 1 
 VI  - 28 

等等

每个新的观察值都以PMID开头,并不是所有的变量都包含在每个观察值中,并且需要合并同一个观察值中具有相同列名的一些单元格(即IS)。最终数据框应如下所示:

 PMID      OWN  IS                                          VI
 28152974  NLM  1471-230X (Electronic) 1471-230X (Linking)  N/A 
 28098115  NLM  N/A                                         28 

等等

目前我以多种方式操纵我的数据。第一个是原始数据文件的格式,但在两列中,没有“-”。例如:

 PMID 28152974
 OWN  NLM
 IS   1471-230X (Electronic) 
 IS   1471-230X (Linking) 
 PMID 28098115 
 OWN  NLM 
 IP   1 
 VI   28 

等等

第二个是所有观察结果都在一行中,每个变量有数千列。例如:

 PMID      OWN   IS                      IS                   PMID      OWN
 28152974  NLM   1471-230X (Electronic)  1471-230X (Linking)  28098115  NLM

等等

第三个与第二个类似,但它没有数千列,它仅具有与第一个 PMID 值不同的列类型。例如:

 PMID               OWN      IS 
 28152974 28098115  NLM NLM  1471-230X (Electronic) 1471-230X (Linking)

等等

请帮忙。我不知道如何拼接我的数据,也不知道我应该使用哪种操作。

可重现的数据:

d <- c("PMID- 28152974", "OWN - NLM", "IS  - 1471-230X (Electronic)", 
       "IS  - 1471-230X (Linking)", "PMID- 28098115", "OWN - NLM", "IP  - 1", 
       "VI  - 28")

来自文件的输入:

d <- readLines('/path/to/file')

一个想法:

# split into records
i <- grepl("^PMID", d)
i <- cumsum(i)
d <- split(d, i)

# split into key-value pairs
d <- lapply(d, strsplit, "\ {0,2}-\ ")
d <- lapply(d, function (x) setNames(sapply(x, '[[', 2), sapply(x, '[[', 1)))

# merge IS variables
d <- lapply(d, function (x) {
  i <- names(x) == "IS"
  if (any(i))
     x <- c(x[!i], IS = paste(x[i], collapse = " "))
  return(x)
})

# merge records to data.frame
library(data.table)
d <- lapply(d, as.list)
d <- lapply(d, as.data.table)
d <- rbindlist(d, fill = T)
d <- as.data.frame(d)

不同种类的数据混合在数据文件的一列或两列中的情况并不少见。只要可以通过某种方式(例如通过正则表达式)识别不同类型的数据,就可以将行的内容移动到不同的列。

以下解决方案使用包readr中的read_fwf()从文本文件中读取固定宽度的数据(这里通过读取字符串来模拟)。 data.table 包中的 dcast() 用于从长格式重塑为宽格式,从而产生每行一条记录的 data.frame:

读取数据

library(data.table)
# read data 
dt <- readr::read_fwf(
  " PMID- 28152974 
 OWN - NLM 
 IS  - 1471-230X (Electronic) 
 IS  - 1471-230X (Linking) 
 PMID- 28098115 
 OWN - NLM 
 IP  - 1 
 VI  - 28 ", 
  readr::fwf_positions(c(2, 8), c(5, Inf), c("variable", "value")),
  col_types = "cc")
# coerce tibble to data.table
setDT(dt)

从长格式重塑为宽格式

# create new column PMID with the record id
dt[variable == "PMID", PMID := value]
# fill missing values in subsequent rows to mark all rows belonging to one record
dt[, PMID := zoo::na.locf(PMID)]
dt
#   variable                  value     PMID
#1:     PMID               28152974 28152974
#2:      OWN                    NLM 28152974
#3:       IS 1471-230X (Electronic) 28152974
#4:       IS    1471-230X (Linking) 28152974
#5:     PMID               28098115 28098115
#6:      OWN                    NLM 28098115
#7:       IP                      1 28098115
#8:       VI                     28 28098115

# reshape from wide to long, thereby collapsing strings if necessary
dcast(dt[variable != "PMID"], PMID ~ ..., fun = paste, collapse = " ")
#       PMID IP                                         IS OWN VI
#1: 28098115  1                                            NLM 28
#2: 28152974    1471-230X (Electronic) 1471-230X (Linking) NLM   

请注意,这种方法非常灵活,因为它折叠 所有重复的 数据字段,如果它们出现在数据中,无论它们如何命名,而不仅仅是 IS 列。

来自 base R 的

Thanks to G. Grothendieck, I learned about the read.dcf() function 大大简化了这项任务。只需要进行一些小的调整。

# use connection to avoid warnings
con <- file("test.dat")
# read file row-wise and adjust for dcf format
dat <- sub("PMID", "\nPMID", sub("- ", ": ", trimws(readLines(con))))
# close connection to avoid warnings
close(con)

# re-read from variable using dcf format, collapse multiple entries
result <- read.dcf(textConnection(dat), all = TRUE)
result
      PMID OWN                                         IS   IP   VI  
1 28152974  NLM 1471-230X (Electronic), 1471-230X (Linking) <NA> <NA>
2 28098115  NLM                                          NA    1   28