【问题标题】:Set column value using the first next row in the same group that meets a condition使用同一组中满足条件的第一行设置列值
【发布时间】:2019-07-30 22:28:22
【问题描述】:

我是 R 新手,这是我关于 stackoverflow 的第一个问题。

我在努力

  • 通过引用分配给新列
  • 每一行
  • 使用同一组行中第一下一行的值
  • 满足条件。

示例数据:

    id code  date_down    date_up
 1:  1    p 2019-01-01 2019-01-02
 2:  1    f 2019-01-02 2019-01-03
 3:  2    f 2019-01-02 2019-01-02
 4:  2    p 2019-01-03       <NA>
 5:  3    p 2019-01-04       <NA>
 6:  4 <NA> 2019-01-05 2019-01-05
 7:  5    f 2019-01-07 2019-01-08
 8:  5    p 2019-01-07 2019-01-08
 9:  5    p 2019-01-09 2019-01-09
10:  6    f 2019-01-10 2019-01-10
11:  6    p 2019-01-10 2019-01-10
12:  6    p 2019-01-10 2019-01-11

我想做的是

  • 子集(组)id
  • 对于每一行
  • 找到date_up 以获取更下方的第一行,
  • 其中code = 'p'date-up(找到的行)大于我正在更新的行的date-down

我的预期结果应该是:

    id code  date_down    date_up  founddate
 1:  1    p 2019-01-01 2019-01-02       <NA>
 2:  1    f 2019-01-02 2019-01-03       <NA>
 3:  2    f 2019-01-02 2019-01-02       <NA>
 4:  2    p 2019-01-03       <NA>       <NA>
 5:  3    p 2019-01-04       <NA>       <NA>
 6:  4 <NA> 2019-01-05 2019-01-05       <NA>
 7:  5    f 2019-01-07 2019-01-08 2019-01-08
 8:  5    p 2019-01-07 2019-01-08 2019-01-09
 9:  5    p 2019-01-09 2019-01-09       <NA>
10:  6    f 2019-01-10 2019-01-10 2019-01-11
11:  6    p 2019-01-10 2019-01-10 2019-01-11
12:  6    p 2019-01-10 2019-01-11       <NA>

我尝试了很多变体,使用.SD.N,创建一个新列 DT[, idcount:= seq_leg(.N),by=id],但并没有真正到达任何地方。非常感谢任何帮助。

还有对 data.table 的任何好的参考 :) 非常感谢

编辑: 我已经编辑了提供的原始数据以给出一个更微妙的示例,其中第 10 行使用第 12 行的数据进行更新,因为第 12 行在 id 子集中并且符合资格标准。第 11 行不符合资格标准,因此数据不用于更新第 10 行。还包括我第一次使用dput

示例数据为dput 代码:

dt <- structure(list(
id        = c(1L, 1L, 2L, 2L, 3L, 4L, 5L, 5L, 5L, 6L, 6L, 6L),
code      = c("p", "f", "f", "p", "p", "<NA>", "f", "p", "p", "f", "p", "p"),
date_down = structure(c(17897, 17898, 17898, 17899, 17900, 17901, 17903, 17903, 17905, 17906, 17906, 17906), class = "Date"),
date_up   = structure(c(17898, 17899, 17898, NA, NA, 17901, 17904, 17904, 17905, 17906, 17906, 17907), class = "Date")),
class     = c("data.table", "data.frame"),
row.names = c(NA, -12L))
setDT(dt)  # to reinit the internal self ref pointer (known issue)

【问题讨论】:

  • 您的示例数据中有一些日期是 2017 年。我是不是读错了问题,还是您的示例数据都应该是 2019 年?
  • 是的,感谢您发现错别字。真实数据有更多的列。我会更正数据。
  • 欢迎来到 SO!请始终将示例数据添加为 R 代码,以便我们更容易回答(例如,使用 dput)。 TXH!
  • @R Yoda 是的,数据是排序的,按id升序,如果id相同则按date_down升序,如果前两个相同则按date_up升序。虽然我手工制作了示例数据,但我认为它是这样表示的。
  • 感谢您有兴趣帮助我 - 答案如下 founddate 是否包含找到的“p”行的 date_up 数据?是的,你更新所有行还是只更新非“p”行? - 如果找到与条件匹配的后续行,则应更新所有行,无论代码的“p”状态如何如果更新所有行(包括“p”):自“p”的创建日期以来是否需要再次更新-rows 可以解释为新的 date_up?不,此元数据用于其他目的

标签: r data.table


【解决方案1】:

按组将 data.table 加入其自身的子集,以从匹配不相等条件的行中获取值。

总结:

  • 下面我展示了 5 个有效的 data.table 解决方案,它们是候选的 针对 OP 的实际数据集(140 万条记录)进行性能测试。

  • 所有 5 种解决方案都使用“非 equi”连接(使用不等式进行比较 on 子句中的连接列)。

  • 每个解决方案都只是一个小的渐进式代码更改,因此应该是 易于遵循以比较不同的data.table 选项和语法选择。

方法

为了解决data.table 的语法问题,我将其分解为以下步骤来解决 OP 的问题:

  1. 将 dt 加入其自身的子集(或另一个 data.table )。
  2. 从 dt 或子集中选择(并重命名)您想要的列。
  3. 根据 dt 中的列与子集中列的比较来定义连接条件,包括使用“non-equi”(不相等)比较。
  4. (可选)定义在子集中找到多个匹配记录时是选择第一个匹配还是最后一个匹配。

解决方案 1:

# Add row numbers to all records in dt (only because you 
# have criteria based on comparing sequential rows):
dt[, row := .I] 

# Compute result columns (  then standard assignment into dt using <-  )
dt$found_date  <- 
            dt[code=='p'][dt,   # join dt to the data.table matching your criteria, in this case dt[code=='p']
                          .( x.date_up ),   # columns to select, x. prefix means columns from dt[code=='p'] 
                          on = .(id==id, row > row, date_up > date_down),   # join criteria: dt[code=='p'] fields on LHS, main dt fields on RHS
                          mult = "first"]   # get only the first match if multiple matches

注意上面的连接表达式:

  • i 在这种情况下是您的主要 dt。这样您就可以从主 data.table 中获取所有记录。
  • x 是您要从中查找匹配值的子集(或任何其他 data.table)。

结果匹配请求的输出:

dt

    id code  date_down    date_up row found_date
 1:  1    p 2019-01-01 2019-01-02   1       <NA>
 2:  1    f 2019-01-02 2019-01-03   2       <NA>
 3:  2    f 2019-01-02 2019-01-02   3       <NA>
 4:  2    p 2019-01-03       <NA>   4       <NA>
 5:  3    p 2019-01-04       <NA>   5       <NA>
 6:  4 <NA> 2019-01-05 2019-01-05   6       <NA>
 7:  5    f 2019-01-07 2019-01-08   7 2019-01-08
 8:  5    p 2019-01-07 2019-01-08   8 2019-01-09
 9:  5    p 2019-01-09 2019-01-09   9       <NA>
10:  6    f 2019-01-10 2019-01-10  10 2019-01-11
11:  6    p 2019-01-10 2019-01-10  11 2019-01-11
12:  6    p 2019-01-10 2019-01-11  12       <NA>

注意:如果您愿意,可以通过 dt[, row := NULL] 删除 row 列。

解决方案 2:

加入和查找结果列的逻辑与上述相同,但现在使用“通过引用分配”:=dt 中创建found_date

dt[, row := .I] # add row numbers (as in all the solutions)

# Compute result columns (  then assign by reference into dt using :=  

# dt$found_date  <- 
dt[, found_date :=   # assign by reference to dt$found_date 
            dt[code=='p'][dt, 
                          .( x.date_up ), 
                          on = .(id==id, row > row, date_up > date_down),
                          mult = "first"]]

在解决方案 2 中,将我们的结果“通过引用”分配给 dt 的细微变化应该比解决方案 1 更有效。解决方案 1 以完全相同的方式计算结果 - 唯一的区别是解决方案 1 使用标准分配 &lt;- 到创建dt$found_date(效率较低)。

解决方案 3:

类似于解决方案 2,但现在使用 .(.SD) 代替 dt 来引用原始 dt 而无需直接命名。

dt[, row := .I] # add row numbers (as in all the solutions)
setkey(dt, id, row, date_down)  #set key for dt 

# For all rows of dt, create found_date by reference :=
dt[, found_date := 
            # dt[code=='p'][dt, 
            dt[code=='p'][.(.SD),   # our subset (or another data.table), joined to .SD (referring to original dt)
                          .( x.date_up ), 
                          on = .(id==id, row > row, date_up > date_down),  
                          mult = "first"] ]  

.SD 上面引用了我们分配回的原始 dt。它对应于 data.table 的子集,其中包含在第一个 dt[, 中选择的行,这是所有行,因为我们没有对其进行过滤。

注意:在解决方案 3 中,我使用 setkey() 设置密钥。我应该在解决方案 1 和解决方案 2 中这样做 - 但是在 @OllieB 成功测试它们之后我不想更改这些解决方案。

解决方案 4:

与解决方案 3 类似,但 使用 .SD 比以前多了一次。我们的主要 data.table 名称 dt 现在在整个表达式中只出现一次

# add row column and setkey() as previous solutions

dt[, found_date :=
            # dt[code=='p'][.(.SD), 
            .SD[code=='p'][.SD,   # .SD in place of dt at left!  Also, removed .() at right (not sure on this second change)
                           .(found_date = x.date_up),
                           on = .(id==id, row > row, date_up > date_down),
                           mult = "first"]]

通过上面的更改,我们的 data.table 名称 dt 只出现一次。我非常喜欢它,因为它可以很容易地在其他地方复制、改编和重用。

另请注意:我以前使用 .(SD) 的地方现在删除了 .SD 周围的 .(),因为它似乎不需要它。但是对于该更改,我不确定它是否具有任何性能优势,或者它是否是 data.table 首选语法。如果有人可以就这一点添加评论以提供建议,我将不胜感激。

解决方案 5:

与之前的解决方案类似,但在加入时使用by 明确分组操作的子集

# add row column and setkey() as previous solutions

dt[, found_date :=
       .SD[code=='p'][.SD,
                      .(found_date = x.date_up),
                      # on = .(id==id, row > row, date_up > date_down),
                      on = .(row > row, date_up > date_down),  # removed the id column from here
                      mult = "first"]
   , by = id]   # added by = id to group the .SD subsets 

在最后一个解决方案中,我将其更改为使用by 子句将id 上的.SD 子集显式分组。

注意: 与解决方案 1 - 4 相比,解决方案 5 与 OllieB 的实际数据相比表现不佳。但是,测试我自己的模拟数据时,我发现解决方案 5 在唯一组数的情况下表现良好id 列中的低:
- 在 150 万条记录中只有 6 个组,此解决方案的运行速度与其他解决方案一样快。
- 在 150 万条记录中有 40k 组,我看到了与 OllieB 报告的类似的糟糕表现。

结果

解决方案 1 - 4 表现良好:

  • 根据 OllieB 的反馈,对于 OllieB 的实际数据中的 145 万条记录,解决方案 1 到 4 中的每一个都是 2.42 秒或更短的“经过”时间。对于具有“elapsed=1.22”秒的 OllieB 来说,解决方案 3 似乎运行得最快。

  • 我个人更喜欢解决方案 4,因为它的语法更简单。

解决方案 5

  • 解决方案 5(使用 by 子句)在 OllieB 对其真实数据的测试中耗时 577 秒表现不佳。

使用的版本

data.table 版本:1.12.0

R 版本 3.5.3 (2019-03-11)


可能的进一步改进:

  • 将日期字段更改为整数可能有助于更有效地加入。请参阅 as.IDate() 将日期转换为 data.tables 中的整数。
  • 可能不再需要 setkey() 步骤:As explained here by @Arun 由于on 调用 [经常] 更有效的二级索引和自动索引。

对 data.table 的引用

作为您问题的一部分,您要求“对 data.table 的任何好的参考”。我发现以下内容很有帮助:

请注意this answer by @Arun,其中解释了“实施 on= 参数的原因”,表明可能不再需要设置键:

因此有必要弄清楚是否花费在 重新排序整个 data.table 值得花时间去做 缓存高效的连接/聚合。通常,除非有重复 在同一个键控上执行分组/连接操作 data.table,应该没有明显的区别。

因此,在大多数情况下,不需要设置键 不再。我们建议尽可能使用 on=,除非设置键 具有您想要利用的性能的显着改进。


一如既往,如果有人提出建议,我将不胜感激,因为也许这可以进一步改进。

如果您可以添加任何内容,请随时发表评论、更正或发布其他解决方案。

【讨论】:

  • 感谢 Krads。但我认为解决方案存在一些问题,一个很容易修复的问题 - 没有检查找到行的 date_up 的子句是>原始行的 date_down,易于修复。大问题可能与我的原始措辞不够清楚有关。我要做的是首先找到子集 - 即。如果满足条件,则在子集中第一次出现,即在我们的行之后正在更新。我认为这个解决方案只发现数据是下一行满足条件,但不是如果它不满足条件,即使子集中有后一行。
  • 我想就是这样,直到今晚才能检查 - 但在第一次阅读时会看到。一如既往地感谢您花时间在这上面。稍后再回来。
  • @RYoda:正如您所建议的,我已更新解决方案以仅添加一个派生列(删除了如何从子集中返回多个列的不必要示例)。使这个问题更清晰/更清晰,所以谢谢!仍在寻找通过引用更新的方法...
  • @krads - 谢谢 - 这绝对是我一直在寻找的解决方案(我都可以)。它让我开始做一些我也需要的其他时髦的东西,例如找到的列的计数和串联:) 我已经在这两个解决方案上运行 system.time,针对 1.45m 行和 30 列 data.table 和结果就在我需要它们的地方。第一个是:user=1.70 system=0.86 elapsed=2.42,第二个是:user=1.49 system=0.64 elapsed=1.94 对于上下文,我已经(或者更确切地说是)学习/学习 R 以使我能够编写代码以超越 ....
  • ...“高性能数据处理、分析和报告”需要一个多小时来运行我需要运行的子集数量。我预计它在 R 中需要不到一分钟的时间。谢谢大家....非常感谢!
【解决方案2】:

一种非数据表方式的方法:

> df <- structure(list(
+   id        = c(1L, 1L, 2L, 2L, 3L, 4L, 5L, 5L, 5L, 6L, 6L, 6L),
+   code      = c("p", "f", "f", "p", "p", "<NA>", "f", "p", "p", "f", "p", "p"),
+   date_down = structure(c(17897, 17898, 17898, 17899, 17900, 17901, 17903, 17903, 17905, 17906, 17906, 17906), class = "Date"),
+   date_up   = structure(c(17898, 17899, 17898, NA, NA, 17901, 17904, 17904, 17905, 17906, 17906, 17907), class = "Date")),
+   class     = c("data.frame"),
+   row.names = c(NA, -12L))
> 
> 
> Lista <- lapply(split(df, df$id), function(x){
+   x$founddate <- 
+     sapply(c(1:nrow(x)), function(y){
+       na.omit(sapply(y:nrow(x), function(i){
+         ifelse(x[i + 1, "code"] == "p" & x[i + 1, "date_up"] > x[y, "date_down"],
+                x[i + 1, "date_up"], NA)
+       }))[1]
+     })
+   x$founddate <- as.Date(x$founddate, origin = "1970-01-01")
+   return(x)
+ })
> 
> 
> df <- do.call(rbind.data.frame, Lista)
> 
> df
     id code  date_down    date_up  founddate
1.1   1    p 2019-01-01 2019-01-02       <NA>
1.2   1    f 2019-01-02 2019-01-03       <NA>
2.3   2    f 2019-01-02 2019-01-02       <NA>
2.4   2    p 2019-01-03       <NA>       <NA>
3     3    p 2019-01-04       <NA>       <NA>
4     4 <NA> 2019-01-05 2019-01-05       <NA>
5.7   5    f 2019-01-07 2019-01-08 2019-01-08
5.8   5    p 2019-01-07 2019-01-08 2019-01-09
5.9   5    p 2019-01-09 2019-01-09       <NA>
6.10  6    f 2019-01-10 2019-01-10 2019-01-11
6.11  6    p 2019-01-10 2019-01-10 2019-01-11
6.12  6    p 2019-01-10 2019-01-11       <NA>
> 

在给定条件下,每行有多个匹配项。建议的答案获得第一个匹配项,但这可以修改。

希望对你有帮助。

【讨论】:

  • 我已经让这个运行了一个小时,有 150 万行。但没有返回 - 我认为针对大数据集的 data.table 方式是前进的方向。感谢您的时间和帮助 - 所有这些答案都对我很有帮助。
  • @OllieB 欢迎您!当然,对于同一个问题,总是有很多方法。此外,基本解决方案可以通过 parallel / doParallel 包进行一些并行化来加速。
【解决方案3】:

这是一种快速而肮脏的方法,不需要您进行太多思考,它会捕获子集中的第一个可行选项,如果不存在则留下NA

do(f(.)) 调用在group_by 语句定义的dt 的每个子集上评估预定义函数f。我会把那个简单的脚本翻译成Rcpp 以供认真使用。

library(dplyr)
f <- function(x){
  x <- x %>% mutate(founddate = as.Date(NA))

  for(i in 1:nrow(x)){
    y <- x[i, "date_down"]
    x[i, "founddate"] <-(x[-c(1:i),] %>% filter(code == "p", date_up > y) %>% select(date_up))[1, ]
  }

  return(x)
}

dt %>% group_by(id) %>% do(f(.))

# A tibble: 12 x 5
# Groups:   id [6]
      id code  date_down  date_up    founddate 
   <int> <chr> <date>     <date>     <date>    
 1     1 p     2019-01-01 2019-01-02 NA        
 2     1 f     2019-01-02 2019-01-03 NA        
 3     2 f     2019-01-02 2019-01-02 NA        
 4     2 p     2019-01-03 NA         NA        
 5     3 p     2019-01-04 NA         NA        
 6     4 <NA>  2019-01-05 2019-01-05 NA        
 7     5 f     2019-01-07 2019-01-08 2019-01-08
 8     5 p     2019-01-07 2019-01-08 2019-01-09
 9     5 p     2019-01-09 2019-01-09 NA        
10     6 f     2019-01-10 2019-01-10 2019-01-11
11     6 p     2019-01-10 2019-01-10 2019-01-11
12     6 p     2019-01-10 2019-01-11 NA 

您对糟糕表现的评论不足为奇。如果我知道怎么做,我会私信这个,但下面是 Rcpp::cppFunction 来做同样的事情。

Rcpp::cppFunction('DataFrame fC(DataFrame x) {
                    int i, j;
                    int n = x.nrows();
                    CharacterVector code = x["code"];
                    DateVector date_up = x["date_up"];
                    DateVector date_down = x["date_down"];
                    DateVector founddate = rep(NA_REAL, n);

                    for(i = 0; i < n; i++){
                      for(j = i + 1; j < n; j++){
                        if(code(j) == "p"){
                          if(date_up(j) > date_down(i)){
                            founddate(i) = date_up(j);
                            break;
                          } else{
                            continue;
                          }
                        } else{
                          continue;
                        }
                      }
                    }
                    x.push_back(founddate, "founddate");
                    return x;
                    }')

dt %>% group_by(id) %>% do(fC(.))

【讨论】:

  • 嗨 Croote - 你是对的,这是非常可读的 - 但看起来性能方面它与 data.table 方式不匹配。在上面运行 system.time ,它目前告诉我它已经完成了 2%,还剩 1 小时。 .... 大约 1.5m 行。感谢您花时间帮助我 - 我正在学习每一个答案
  • 嗨 OllieB,是的,它非常慢。当我遇到这类问题时,我倾向于通过Rcpp 用c++ 编写函数,以跟上您在data.table 中看到的性能,因为我对data.table 不太熟悉。
  • @OllieB 如果你能告诉我cppFunction 在你的 150 万行上的表现,我将不胜感激。我发现 data.table 的竞争速度快得离谱。
  • 抱歉,我试图通过 RStudios 运行您的代码,但我的笔记本电脑锁定无法让我完成代码,它启动并下载了一些编译器包,但随后因为权限问题将我踢了出去。这有点超出了我现在的位置:)
  • 别担心!我会尝试模拟一些数据:)
猜你喜欢
  • 1970-01-01
  • 2020-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多