【问题标题】:Import unusually formatted text data using R使用 R 导入格式异常的文本数据
【发布时间】:2016-12-29 09:08:28
【问题描述】:

我有一台设备的输出数据。不幸的是,输出数据组织得不是很好,我一直在用 R 编写代码来分解它。本质上,数据是粘贴到一个长文档中的每个主题的单独信息列表(基本描述信息,以及每个时间间隔的两个不同测量 A 和 B 的原始数据)。例如:

Date: 01/01/2016
Time: 12:00:00 
Subject: Subject1
A: 
1: 1  2  4  1 
2: 2  1  2  3
3: 1  0  2  7
B:
1: 2  3  0  1
2: 4  1  1  2 
3: 3  5  2  8

Date: 01/01/2016
Time: 12:00:00 
Subject: Subject2   
A: 
1: 8  2  0  1 
2: 9  1  2  7
3: 1  6  2  7
B:
1: 2  3  2  0
2: 6  7  1  2
3: 3  3  2  4

我使用 split(seq_along)、for-loops 和 do.call(主要基于 this stack overflow questionthis blog post)在 R 中编写了一个有效但不是很优雅的代码。

# First read text file in as a character vector called ‘example’

    scan("example_file.txt", what="character", strip.white=T, sep="\n") -> example

# Separate the header text (before the colon) from the proceeding data
# and make that text name the components of the vector

    regmatches(example, regexpr(example, pattern="[[:alnum:]]+:", useBytes = F)) -> names(example)
    gsub(example, pattern="[[:print:]]+: ", replacement="", useBytes = F)-> example.2

# Then, split character vector into a list based on how many lines are
# dedicated to each subject (in this example, 11 lines); based on SE
# answer cited above

    strsplit(example.2, "([A-Z]:)") -> example.3
    split(as.list(example.3), ceiling(seq_along(example.2)/11)) -> example.4

# Use a for-loop to systematically add the data together for subjects 1
# and 2 for time interval 1, using the method detailed from a blog post
# (cited above)

    my.list <- list()

    for(i in 1:2){
            strsplit(as.character(example.4[[i]][5]), split="[[:blank:]]+") -> A
            strsplit(as.character(example.4[[i]][9]), split="[[:blank:]]+")-> B

            as.vector(c(as.character(example.4[[i]][3]), "A", unlist(A))) -> A_char
            as.vector(c(as.character(example.4[[i]][3]), "B", unlist(B))) -> B_char

            paste(as.character(example.4[[i]][3]), "Measure_A") -> a_name
            paste(as.character(example.4[[i]][3]), "Measure_B") -> b_name

            my.list[[a_name]] <- A_char
            my.list[[b_name]] <- B_char
    }

    final.data <- do.call(rbind, my.list)
    as.data.frame(final.data) -> final.data

    names(final.data) <- c("Subject", "Measure", "V1", "V2", "V3", "V4")

我可以使用我的代码(例如,上面的“1: 1 2 4 1”和“1: 2 3 0 1”行)在所有受试者中提取 A 和 B 的单个时间间隔的数据并放入将所有信息放在一个数据框中。当我想为 所有 的时间间隔而不只是一个时间间隔执行此操作时,哪里会变得混乱。如果不为每个时间间隔运行单独的 for 循环,我无法弄清楚如何做到这一点。我尝试在 for 循环中执行 for 循环,但没有奏效。我也不知道如何使用 apply() 类型的函数来做到这一点。

如果我只有 3 个时间间隔,按照这个例子,这个问题不会那么糟糕,但我的实际数据要长得多。任何关于更优雅和简洁方法的建议都将不胜感激!

附:我知道上面代码给出的最终数据框有多余的行名。但是,这是确保最终数据框的主题和度量信息与我应用于早期 R 对象的标签一致的有用方法。

【问题讨论】:

  • 您是使用&gt; 来实现块引用效果还是数据本身在行首有&gt;?而且,行之间有空格吗?
  • 我只使用了&gt; 的块引用效果。实际数据没有这些。行之间没有空格,但由于strip.white=T 参数,我的代码仍应使用空行。
  • 不要使用块引用效果 - 格式为代码。会清晰很多。
  • 我将其重新格式化为代码;感谢您的建议

标签: r list for-loop dataframe


【解决方案1】:

不清楚数据框是否是表示这些数据的最方便的方式。下面显示了三个备用输出:

  • 三个数组——一个矩阵,每个主题有一行,包含日期时间和主题列,A 数组,A[,,i] 是第 i 个主题的 A 矩阵,B 数组,这样B[,,i] 是第 i 个主题的 B 矩阵。 没有使用任何包。

  • 宽格式的数据框

  • 长格式数据帧

没有使用任何包。

对于所有三个,将文件读入字符向量Lines。然后使用grep 删除任何空行——如果我们知道没有空行,我们可以省略这一步。然后将Lines 拆分为主题组s。然后lapply 覆盖主题组,并在每个主题组中从前三行获取日期、时间和主题,从第 5:7 行和第 9:11 行获取两个矩阵,每个主题一个组件。生成列表L 的关键代码很容易改造成不同的格式:

Lines <- readLines("example_file.txt")
Lines <- grep("^\\s*$", Lines, value = TRUE, invert = TRUE)
s <- split(Lines, cumsum(grepl("^Date:", Lines)))
L <- lapply(s, function(x) list(read.dcf(textConnection(x[1:3])), 
           A = as.matrix(read.table(text = sub(":", "", x[5:7]), row.names = 1)),
           B = as.matrix(read.table(text = sub(":", "", x[9:11]), row.names = 1))))
names(L) <- sapply(L, function(x) x[[1]][, "Subject"])

给定L,我们可以通过在其上使用lapply 轻松创建各种输出格式。这三种格式中的每一种都显示在下面的单独部分中。输出显示在最后,以免破坏代码。

三个数组

我们可以按原样使用L,但将L 转换为三个数组可能更方便:(1) ident 这是一个3 列矩阵,具有与主题一样多的行,包括日期、时间和每个主题,(2) A 这是一个 3d 数组,A[,,i] 是第 i 个主题的 A 矩阵,(3) B 这是一个 3d 数组,所以 B[,,i] 是 @第 i 个主题的 987654342@ 矩阵。

ident <- do.call(rbind, lapply(L, "[[", 1))
A <- simplify2array(lapply(L, "[[", 2))
B <- simplify2array(lapply(L, "[[", 3))

data.frame - 宽格式

DF <- do.call(rbind, lapply(L, function(x) data.frame(x[[1]], x[[2]], x[[3]])))
names(DF)[4:7] <- "A"
names(DF)[8:11] <- "B"
rownames(DF) <- NULL

data.frame - 长格式

DF2 <- do.call(rbind, lapply(L, function(x)
          data.frame(x[[1]], rbind(cbind(AB = "A", x[[2]]), cbind(AB = "B", x[[3]])))))
rownames(DF2) <- NULL

输出——三个数组

> ident
     Date         Time       Subject   
[1,] "01/01/2016" "12:00:00" "Subject1"
[2,] "01/01/2016" "12:00:00" "2"       
> A
, , Subject1

  V2 V3 V4 V5
1  1  2  4  1
2  2  1  2  3
3  1  0  2  7

, , 2

  V2 V3 V4 V5
1  8  2  0  1
2  9  1  2  7
3  1  6  2  7

> B
, , Subject1

  V2 V3 V4 V5
1  2  3  0  1
2  4  1  1  2
3  3  5  2  8

, , 2

  V2 V3 V4 V5
1  2  3  2  0
2  6  7  1  2
3  3  3  2  4

输出 -- 数据框宽格式

> DF

        Date     Time  Subject A A A A B B B B
1 01/01/2016 12:00:00 Subject1 1 2 4 1 2 3 0 1
2 01/01/2016 12:00:00 Subject1 2 1 2 3 4 1 1 2
3 01/01/2016 12:00:00 Subject1 1 0 2 7 3 5 2 8
4 01/01/2016 12:00:00        2 8 2 0 1 2 3 2 0
5 01/01/2016 12:00:00        2 9 1 2 7 6 7 1 2
6 01/01/2016 12:00:00        2 1 6 2 7 3 3 2 4

输出 - 数据帧长格式

> DF2
         Date     Time  Subject AB V2 V3 V4 V5
1  01/01/2016 12:00:00 Subject1  A  1  2  4  1
2  01/01/2016 12:00:00 Subject1  A  2  1  2  3
3  01/01/2016 12:00:00 Subject1  A  1  0  2  7
4  01/01/2016 12:00:00 Subject1  B  2  3  0  1
5  01/01/2016 12:00:00 Subject1  B  4  1  1  2
6  01/01/2016 12:00:00 Subject1  B  3  5  2  8
7  01/01/2016 12:00:00        2  A  8  2  0  1
8  01/01/2016 12:00:00        2  A  9  1  2  7
9  01/01/2016 12:00:00        2  A  1  6  2  7
10 01/01/2016 12:00:00        2  B  2  3  2  0
11 01/01/2016 12:00:00        2  B  6  7  1  2
12 01/01/2016 12:00:00        2  B  3  3  2  4

【讨论】:

  • 出于好奇,您建议如何将此输出转换为合并数据框?特别是如果你只使用基础 R?
  • 在帖子末尾添加。
  • 除了宽格式外,还添加了长格式。
【解决方案2】:

除了行名之外的所有事情:

lines <- readLines(textConnection("Date: 01/01/2016
Time: 12:00:00
Subject: Subject1
A:
1: 1 2 4 1
2: 2 1 2 3
3: 1 0 2 7
B:
1: 2 3 0 1
2: 4 1 1 2
3: 3 5 2 8
Date: 01/01/2016
Time: 12:00:00
Subject: 2
A:
1: 8 2 0 1
2: 9 1 2 7
3: 1 6 2 7
B:
1: 2 3 2 0
2: 6 7 1 2
3: 3 3 2 4
Date: 01/01/2016
Time: 12:00:00
Subject: 2
A:
1: 8 2 0 1
2: 9 1 2 7
3: 1 6 2 7
B:
1: 2 3 2 0
2: 6 7 1 2

3: 3 3 2 4
3: 3 3 2 4"))

非基础 R 解决方案需要的一些库:

library(purrr)
library(tibble)
library(tidyr)
library(dplyr)

修剪空白并过滤掉空行:

trimws(lines) %>% discard(`==`, "") -> lines

这会在lines 中创建记录开始的索引向量(通过在行首找到Date: 来指定):

starts <- which(grepl("^Date:", lines))

现在,我们开始寻找Date:下一个 项(即下一条记录)。它会找到所有的,所以我们只关心第一个。要计算该索引,我们添加起始索引并减去 1。理论上只会有一个NA(即最后一条记录),但我们懒惰地使用ifelse,而只是改变他的最后一条。

ends <- map_dbl(starts, function(i) {
  which(grepl("^Date:", lines[(i+1):length(lines)]))[1]+i-1
})
ends <- ifelse(is.na(ends), length(lines), ends)

所以,现在starts 包含每条记录的开头索引,ends 包含每条记录结尾的索引。

map2_df()mapply()do.call(rbind,…) 的超级方便的伪包装器。我们使用这些是 DCF 格式 (key: value) 并使用 read.dcf() 的事实。这样就形成了一个矩阵,然后我们将其重新定向并将其转换为 data.frame。

然后,我们将值分开,添加行名称以创建 time_interval 列,添加日期、时间和主题,并确保列的类型正确。

如果我们告诉map2_df() 将使用命名列表“keys”作为列,我们也会使用这一事实。

最后,我们重新排列列。

因此,这将遍历 startsends 并将每次迭代传递给 startend

map2_df(starts, ends, function(start, end) {

  # now, we extract just the current record into `record` by pulling
  # out lines by the indexes.

  record <- lines[start:end]

  # we then use `read.dcf` to read in the date/subject/time values:

  header <- as.data.frame(read.dcf(textConnection(record[1:3])))

  # Since we do not have blank lines and you said the records were
  # uniform we can use the fact that they'll be at known index
  # positions in this `record`. So, we make a list of two vectors
  # which are the indexes. Each becomes `i` (two total iterations)
  # and we use the value in `i` to extract out the three lines from
  # `record` and read those via `read.dcf`.

  # But that reads things into a matrix and in an unhelpful order
  # so we transpose it into shape and make it a data frame since
  # we'll ultimately need that.

  # We use `separate` to take the single character space-separated
  # `V1` column and turn it into 4 columns. `read.dcf` gave us
  # named rows for each time interval so we promote that to a 
  # full-on column and then add in date/time/subject, ensuring
  # they are characters and not factors, then ensure that the 
  # values we split out from `V1` are numeric and not character or
  # factor.

  # `map_df` can add in the `A` and `B` from the named list we passed
  # in for us and we have it call that column `measure`.

  # finally, we put the columns in a better order.

  map_df(list(A=5:7, B=9:11), function(i) {
    read.dcf(textConnection(record[i])) %>%  
      t() %>% as_data_frame() %>%
      separate(V1, sprintf("V%d", 1:4)) %>%
      rownames_to_column("time_interval") %>%
      mutate(date=as.character(header$Date),
             time=as.character(header$Time),
             subject=header$Subject) %>%
      mutate_at(vars(starts_with("V")), as.numeric)

  }, .id="measure")

}) %>% 
  select(date, time, subject, measure, time_interval, V1, V2, V3, V4)

产生以下输出:

## # A tibble: 18 x 9
##          date     time  subject measure time_interval    V1    V2    V3    V4
##         <chr>    <chr>    <chr>   <chr>         <chr> <dbl> <dbl> <dbl> <dbl>
## 1  01/01/2016 12:00:00 Subject1       A             1     1     2     4     1
## 2  01/01/2016 12:00:00 Subject1       A             2     2     1     2     3
## 3  01/01/2016 12:00:00 Subject1       A             3     1     0     2     7
## 4  01/01/2016 12:00:00 Subject1       B             1     2     3     0     1
## 5  01/01/2016 12:00:00 Subject1       B             2     4     1     1     2
## 6  01/01/2016 12:00:00 Subject1       B             3     3     5     2     8
## 7  01/01/2016 12:00:00        2       A             1     8     2     0     1
## 8  01/01/2016 12:00:00        2       A             2     9     1     2     7
## 9  01/01/2016 12:00:00        2       A             3     1     6     2     7
## 10 01/01/2016 12:00:00        2       B             1     2     3     2     0
## 11 01/01/2016 12:00:00        2       B             2     6     7     1     2
## 12 01/01/2016 12:00:00        2       B             3     3     3     2     4
## 13 01/01/2016 12:00:00        2       A             1     8     2     0     1
## 14 01/01/2016 12:00:00        2       A             2     9     1     2     7
## 15 01/01/2016 12:00:00        2       A             3     1     6     2     7
## 16 01/01/2016 12:00:00        2       B             1     2     3     2     0
## 17 01/01/2016 12:00:00        2       B             2     6     7     1     2
## 18 01/01/2016 12:00:00        2       B             3     3     3     2     4

如果您真的需要基本 R 解决方案,那么:

do.call(rbind, mapply(function(start, end) {

  record <- lines[start:end]
  header <- as.data.frame(read.dcf(textConnection(record[1:3])))

  do.call(rbind, lapply(list(A=5:7, B=9:11), function(i) {
    mat <- as.data.frame(t(read.dcf(textConnection(record[i]))))
    mat <- matrix(unlist(apply(mat, 1, strsplit, split=" "), use.names=FALSE), ncol=4, byrow=TRUE)
    mat <- as.data.frame(mat)
    mat$time_interval <- 1:3
    mat$date <- as.character(header$Date)
    mat$time <- as.character(header$Time)
    mat$subject <- as.character(header$Subject)
    mat
  })) -> df

  df$measure <- gsub("\\..*$", "", rownames(df))
  rownames(df) <- NULL
  df

}, starts, ends, SIMPLIFY=FALSE)) -> out_df
out_df[,c("date", "time", "subject", "measure", "time_interval", "V1", "V2", "V3", "V4")]

##          date     time  subject measure time_interval V1 V2 V3 V4
## 1  01/01/2016 12:00:00 Subject1       A             1  1  2  4  1
## 2  01/01/2016 12:00:00 Subject1       A             2  2  1  2  3
## 3  01/01/2016 12:00:00 Subject1       A             3  1  0  2  7
## 4  01/01/2016 12:00:00 Subject1       B             1  1  2  4  1
## 5  01/01/2016 12:00:00 Subject1       B             2  2  1  2  3
## 6  01/01/2016 12:00:00 Subject1       B             3  1  0  2  7
## 7  01/01/2016 12:00:00        2       A             1  8  2  0  1
## 8  01/01/2016 12:00:00        2       A             2  9  1  2  7
## 9  01/01/2016 12:00:00        2       A             3  1  6  2  7
## 10 01/01/2016 12:00:00        2       B             1  8  2  0  1
## 11 01/01/2016 12:00:00        2       B             2  9  1  2  7
## 12 01/01/2016 12:00:00        2       B             3  1  6  2  7
## 13 01/01/2016 12:00:00        2       A             1  8  2  0  1
## 14 01/01/2016 12:00:00        2       A             2  9  1  2  7
## 15 01/01/2016 12:00:00        2       A             3  1  6  2  7
## 16 01/01/2016 12:00:00        2       B             1  8  2  0  1
## 17 01/01/2016 12:00:00        2       B             2  9  1  2  7
## 18 01/01/2016 12:00:00        2       B             3  1  6  2  7

【讨论】:

  • 谢谢,这看起来很有希望;我会进一步检查。然而,理想情况下,我最终会得到一个数据框,让人想起我用上面的原始代码实现的效果。我会修改你的代码,看看我是否能弄清楚如何将它改造成一个数据框。 do.call() 合适吗?
  • 我完成了数据框的创建。如果你真的需要那些微不足道的行名,如果你真的不想要它们,就删除多余的列。
  • 您的代码需要tibbletidyr 包,对吧?
  • library() 呼叫已添加到原始解决方案中。添加了基础 R 解决方案。
  • 感谢您提供额外的解决方案!我仍然对一些事情感到困惑。
猜你喜欢
  • 2023-03-30
  • 2020-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多