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

Oll*_*ieB 10 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
Run Code Online (Sandbox Code Playgroud)

我想做的是

  • 子集(组) 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>
Run Code Online (Sandbox Code Playgroud)

我尝试了多种变体,使用.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)
Run Code Online (Sandbox Code Playgroud)

kra*_*ads 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
Run Code Online (Sandbox Code Playgroud)

请注意上面的联接表达式:

  • 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>
Run Code Online (Sandbox Code Playgroud)

注意:您可以根据需要删除rowdt[, row := NULL]

解决方案2:

相同的逻辑,上述的加入,找到的结果列,但现在使用“分配参照”:=创建found_datedt

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"]]
Run Code Online (Sandbox Code Playgroud)

在解决方案2中,将我们的结果“通过引用”分配给dt的细微变化应该比解决方案1更有效。解决方案1的计算结果完全相同,唯一的区别是解决方案1使用标准分配<-创建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"] ]  
Run Code Online (Sandbox Code Playgroud)

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

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

解决方案4:

与解决方案3类似,比以前多使用一次.SD。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"]]
Run Code Online (Sandbox Code Playgroud)

更改之后,我们的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 
Run Code Online (Sandbox Code Playgroud)

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

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

结果

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

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

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

解决方案5

  • by对于OllieB的真实数据测试,解决方案5(using 子句)的执行效果很差,需要577秒。

使用的版本

data.table版本:1.12.0

R版本3.5.3(2019-03-11)


可能的进一步改进:

  • 将日期字段更改为整数可能有助于更有效地联接。请参见as.IDate()以将日期转换为data.tables中的整数。
  • 可能不再需要setkey()步骤:@Arun在此进行了解释,原因是调用on [通常]效率更高的辅助索引和自动索引。

对data.table的引用

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

重要的是,请注意@Arun的答案,该答案解释“实现on =参数的原因”表明不再需要设置键:

因此,必须弄清楚重新排序整个数据表所花费的时间是否值得进行高速缓存有效的连接/聚合。通常,除非对同一个键控数据表执行重复的分组/联接操作,否则应该没有明显的区别。

因此,在大多数情况下,无需再设置密钥。我们建议在可能的情况下使用on =,除非您希望利用key来提高性能。


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

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

  • @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以使我能够编写代码以使其表现出色.... (3认同)
  • ...“高性能数据处理,分析和报告”需要一个多小时才能运行我需要运行的子集数量。我希望在R上花费不到一分钟。谢谢大家..非常感谢! (3认同)
  • 谢谢Krads,但是我认为解决方案有两个问题,一个很容易解决的问题-没有子句来检查找到的行的date_up是&gt;原始行的date_down,很容易解决。最大的问题可能与我的原始措词不够清晰。我要做的是首先找到子集-即,如果满足条件,即在我们要更新的行之后,子集中的第一次出现。我认为此解决方案仅在下一行满足条件的情况下找到数据,即使不满足,即使子集中的下一行满足条件,也不会找到数据。 (2认同)
  • 我认为就是这样,直到今天晚上才能够检查-但在第一次阅读时会看到它。一如既往,感谢您为此花费的时间。晚点回来。 (2认同)
  • @RYoda:正如您所建议的那样,我已更新解决方案以仅添加一个派生列(删除了如何从子集中返回多个列的不必要示例)。使它更干净/更容易解决此问题,谢谢!仍在寻找通过引用进行更新的方式... (2认同)