使用同一组中满足条件的下一个第一行设置列值

时间:2019-03-08 20:53:33

标签: r data.table

我是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的任何良好引用:)非常感谢

编辑: 我已经编辑了提供的原始数据,以给出一个更微妙的示例,其中用第12行的数据更新第10行,因为第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)

3 个答案:

答案 0 :(得分:12)

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

摘要:

  • 下面,我显示5个可行的data.table解决方案,这些解决方案是 针对OP的实际数据集(140万条记录)进行性能测试。

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

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

方法

为此,我使用data.table语法来解决OP的问题,分为以下步骤:

  1. 将dt加入其自身的子集(或与此相关的另一个data.table)。
  2. 从dt或子集中选择(并重命名)所需的列。
  3. 基于dt中的列与子集中的列进行比较来定义连接条件,包括使用“非等值”(非等值)比较。
  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是您的主要目标。这样,您将从主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>

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

解决方案2:

与上述相同的逻辑可以加入并找到结果列,但现在使用“按引用分配” := 在以下位置创建found_date dt

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使用了标准分配{{1} }创建<-(效率较低)。

解决方案3:

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

dt
上面的

.SD引用了我们要分配回的原始dt。它对应于data.table的子集,该子集包含在第一个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"] ] 中选择的行,这是所有行,因为我们没有对其进行过滤。

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

解决方案4:

类似于解决方案3 ,但使用.SD的次数比以前多了。现在,我们的主要data.table名称setkey()在整个表达式中仅一次出现!

dt

在我们的data.table名称# 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"]] 以上的更改仅出现一次。我非常喜欢它,因为它可以轻松地在其他地方复制,改编和重用。

还要注意:我以前使用过dt的地方,现在已经删除了.(SD)周围的。(),因为它似乎不需要它。但是对于该更改,我不确定它是否具有任何性能优势或它是否是data.table首选语法。如果有人可以对此发表意见以提出建议,我将不胜感激。

解决方案5:

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

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上的.SD子集显式分组。

注意:与解决方案1-4相比,解决方案5在OllieB的实际数据上的表现不佳。但是,通过测试我自己的模拟数据,我发现解决方案5可以在唯一组数量方面表现良好id列中的值很低:
 -150万条记录中只有6个组,此解决方案的工作速度与其他组一样快。
 -在150万条记录中有4万个小组,我看到了与OllieB报告的类似的不良表现。

结果

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

  • 对于OllieB的实际数据中的145万条记录,根据OllieB的反馈,解决方案1至4的每个“经过”时间都在2.42秒或更短。对于OllieB,“解决方案3”的运行时间最快,为“ elapsed = 1.22”秒。

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

解决方案5

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

使用的版本

data.table版本:1.12.0

R版本3.5.3(2019-03-11)


可能的进一步改进:

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

对data.table的引用

作为您的问题的一部分,您要求“对data.table的任何良好引用”。我发现以下帮助:

重要说明this answer by @Arun解释了“实现on =参数的原因”表明不再需要设置键了:

  

因此必须弄清楚是否花费了时间   重新排序整个data.table是值得的时间   高速缓存有效的连接/聚合。通常,除非有重复   对同一键进行分组/连接操作   data.table,应该没有明显的差异。

     
    

因此,在大多数情况下,无需设置键     还有。我们建议尽可能使用on =,除非设置密钥     您想利用的性能有了显着提高。

  

一如既往,我很感谢任何人提出建议,因为这可能会得到进一步改善。

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

答案 1 :(得分: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>
> 

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

希望有帮助。

答案 2 :(得分:2)

这是一种快速而肮脏的方法,不需要您多加考虑,它会捕获子集中的第一个可行选项,并在不存在的情况下留下NA

do(f(.))调用对由f语句定义的dt的每个子集求值预定义函数group_by。我会将该简单脚本翻译成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(.))