【问题标题】:How to use functional-style programming in R when the list of inputs depends upon one another?当输入列表相互依赖时,如何在 R 中使用函数式编程?
【发布时间】:2019-11-29 08:03:47
【问题描述】:

我有一个这样组织的数据框:

df <- data.frame(id = c(1, 1, 1),
                 startDate = c("1990-01-01", "1990-01-23", "1990-01-30"),
                 endDate = c("1990-01-24", "1990-01-25", "1990-01-31"))

每一行代表开始日期和结束日期。尽管我想确定数据中存在一些重叠。例如,第一个日期范围从 1990 年 1 月 1 日到 1990 年 1 月 24 日,然后第二行日期从 1990 年 1 月 23 日到 1990 年 1 月 24 日。

我想要的是创建一个像这样的新数据框... (通过创建一个新的 R 数据框来说明)。

df <- data.frame(id = c(1, 1, 1),
                 startDate = c("1990-01-01", "1990-01-23", "1990-01-30"),
                 endDate = c("1990-01-24", "1990-01-25", "1990-01-31"),
                 overlap = c(TRUE, TRUE, FALSE),
                 newStartDate = c("1990-01-01", "1990-01-01", "1990-01-30"),
                 newEndDate = c("1990-01-25", "1990-01-25", "1990-01-31"))

首先,确定与另一行重叠的每一行。然后创建新列 (newStartDate, newEndDate),这些列将采用所有重叠日期中的最早开始日期和最晚结束日期。

我已经想到了使用伪代码将如何工作。但是,我想知道是否有一种方法可以使用“R”风格的编程、使用向量和函数等等来完成这项工作。我在概念化它的工作方式时遇到了麻烦,因为您需要跟踪很多变量以及不需要跟踪的变量,而且我不确定如何使用 map、apply 等实现它。

希望我的问题很清楚!

【问题讨论】:

标签: r functional-programming apply purrr do.call


【解决方案1】:

您可以构造一个 data.table - 依赖函数,称为 find_overlaps,如下所示:

library(data.table)

find_overlaps <- function(df, 
                          groups = NULL, 
                          start_var = NULL, 
                          end_var = NULL, 
                          fmt = "%Y-%m-%d") {

  calc_cummax_Date <- function(x) setattr(cummax(unclass(x)), "class", c("Date", "IDate"))

  df_overlap <- setDT(copy(df))

  rangevars <- c(start_var, end_var)
  groupsidx <- c(groups, "overlap_idx")

  df_overlap <- df_overlap[
    , (rangevars) := lapply(.SD, function(x) as.Date(as.character(x), format = fmt)), .SDcols = rangevars][
      , max_until_now := shift(calc_cummax_Date(get(end_var)), fill = get(end_var)[1]), by = mget(groups)][
        (max_until_now + 1L) < get(start_var), gap_between := 1][
          is.na(gap_between), gap_between := 0][
            , overlap_idx := cumsum(gap_between), by = mget(groups)][
              , `:=` (overlap = .N > 1,
                      newStartDate = min(get(start_var)),
                      newEndDate = max(get(end_var))), by = groupsidx][
                        , c("gap_between", "max_until_now") := NULL
                        ]

  return(df_overlap)

}

调用此函数(在末尾带有[] 用于打印输出)将为您提供所需的输出:

# Below code will only print the output, you have to save it by e.g. df <- find_overlaps(df, groups = "id", start_var = "startDate", end_var = "endDate")

find_overlaps(df, groups = "id", start_var = "startDate", end_var = "endDate")[]

   id  startDate    endDate overlap_idx overlap newStartDate newEndDate
1:  1 1990-01-01 1990-01-24           0    TRUE   1990-01-01 1990-01-25
2:  1 1990-01-23 1990-01-25           0    TRUE   1990-01-01 1990-01-25
3:  1 1990-01-30 1990-01-31           1   FALSE   1990-01-30 1990-01-31

如您所见,我还添加了一个名为 overlap_idx 的列,因为我相信为每个 id 的每个非重叠范​​围设置一个单独的索引可能会很有用。

该函数可以处理多个组。由于它检查结束日期变量中的累积最大值,因此它也适用于行具有最低开始日期但也有最高结束日期的情况。可以轻松添加其他参数(如max_days_between,即您认为连续的定义 - 1 天或更多)。


如果您有兴趣,上述函数与我的包neatRanges 中的一个名为collapse_ranges 的函数部分相似(在CRAN 上可用,但仍处于实验状态)。

它会给你一个折叠的输出,类似于你想要的,但作为每个非重叠范​​围的第一个和最后一个开始/结束日期的摘要:

install.packages('neatRanges')
library(neatRanges)

collapse_ranges(df, groups = "id", start_var = "startDate", end_var = "endDate")[]

  id  startDate    endDate
1  1 1990-01-01 1990-01-25
2  1 1990-01-30 1990-01-31

【讨论】:

    【解决方案2】:

    以下是一些替代方法。

    (1) 和 (1a) 仅使用 Base R。除了 (1) 使用邻接矩阵和 (1a) 使用邻域列表以避免形成潜在的大邻接矩阵之外,它们是相同的。

    (2) 是使用 sqldf 包的 SQL 解决方案。

    (3) 使用 igraph 包并可能给出与上述替代方案不同的答案,尽管在问题中的示例的情况下答案是相同的。 (3a) 与 (3) 类似,但与 (1a) 类似,避免了形成邻接矩阵。

    最后我们提供了一些图形。

    替代方案

    1) Base R 首先我们将日期转换为Date 类,给出df2。然后我们定义一个函数betw,它检查它的第一个参数是否在第二个和第三个之间,并使用它来定义一个函数overlap,它给df2中的两个行索引确定它们是否重叠(TRUE)或不(FALSE )。

    如果V 是从1 到df2 中行数的序列,那么我们可以形成一个邻接矩阵adj,这样adj[i,j] 在第i 行和第j 行重叠时为1。使用它可以直接计算overlapnewStartDatenewEndDate 列。

    这种方法不使用任何包。

    df2 <- transform(df, startDate = as.Date(startDate), endDate = as.Date(endDate))
    
    betw <- function(x, a, b) x >= a & x <= b
    overlap <- function(i, j) {
      betw(df2[i, "startDate"], df2[j, "startDate"], df2[j, "endDate"]) ||
      betw(df2[j, "startDate"], df2[i, "startDate"], df2[i, "endDate"])
    }
    
    # form adjacency matrix of graph having vertices V
    V <- 1:nrow(df2)
    adj <- sapply(V, function(u) sapply(V, overlap, u)) + 0
    
    orig <- "1970-01-01"
    transform(df2, overlap = colSums(adj) > 1,
      newStartDate = as.Date(apply(adj, 1, function(ix) min(startDate[ix == 1])), orig),
      newEndDate = as.Date(apply(adj, 1, function(ix) max(endDate[ix == 1])), orig))
    

    给予:

      id  startDate    endDate overlap newStartDate newEndDate
    1  1 1990-01-01 1990-01-24    TRUE   1990-01-01 1990-01-25
    2  1 1990-01-23 1990-01-25    TRUE   1990-01-01 1990-01-25
    3  1 1990-01-30 1990-01-31   FALSE   1990-01-30 1990-01-31
    

    1a) 避免形成adj 邻接矩阵的(1) 的一种变体是创建一个邻居列表,使得nbrs[[i]] 是第i 行重叠的行号的向量。

    nbrs <- lapply(1:nr, function(j) Filter(function(i) overlap(i, j), 1:nr))
    names(nbrs) <- 1:nr
    
    orig <- "1970-01-01"
    transform(df2, overlap = lengths(nbrs) > 1,
      newStartDate = as.Date(sapply(nbrs, function(ix) min(startDate[ix])), orig),
      newEndDate = as.Date(sapply(nbrs, function(ix) max(endDate[ix])), orig))
    

    2) sqldf 使用df2,我们可以使用 SQL 在单个 SQL 语句中计算所需的输出:

    library(sqldf)
    
    sqldf("select 
        a.id, 
        a.startDate as startDate__Date,
        a.endDate as endDate__Date,
        count(b.rowid) > 1 as overlap__logical, 
        min(b.startDate) as newStartDate__Date,
        max(b.endDate) as newEndDate__Date
      from df2 as a
      left join df2 as b on (a.startDate between b.startDate and b.endDate) or
                            (b.startDate between a.startDate and a.endDate)
      group by a.rowid
      order by a.rowid", method = "name__class")
    

    给予:

      id  startDate    endDate overlap newStartDate newEndDate
    1  1 1990-01-01 1990-01-24    TRUE   1990-01-01 1990-01-25
    2  1 1990-01-23 1990-01-25    TRUE   1990-01-01 1990-01-25
    3  1 1990-01-30 1990-01-31   FALSE   1990-01-30 1990-01-31
    

    3) igraph 另一种不等同于 (1) 或 (2) 但可能更受欢迎的方法是使用重叠关系的传递完成将行划分为连接的组件。这里类似于这个问题:R: Find groups of vectors that have a > 80% overlap in their elements

    使用 (1) 中的 adj 使用 igraph 包形成图形 g。然后,在其连接组件中没有其他行的行不会重叠。如果我们将连通分量编号为 1、2、...,那么 memb 是这样的,memb[i] 是包含第 i 行的连通分量的编号,因此对于每一行,我们可以找到连通分量上的最小和最大日期它属于。尽管对于问题中的输入,这给出了与 (1) 相同的答案,但总的来说,这与 (1) 不同,因为例如,如果行 i 和 j 不重叠但每个重叠行 k 则 i, j和 k 都在同一个连通分量中,用于计算输出的列。

    library(igraph)
    
    g <- graph_from_adjacency_matrix(adj, mode = "undirected", diag = FALSE)
    memb <- components(g)$membership
    
    # assemble desired output data frame
    transform(df2, 
      overlap = ave(memb, memb, FUN = length) > 1,
      newStartDate = ave(startDate, memb, FUN = min),
      newEndDate = ave(endDate, memb, FUN = max))
    

    给予:

      id  startDate    endDate overlap newStartDate newEndDate
    1  1 1990-01-01 1990-01-24    TRUE   1990-01-01 1990-01-25
    2  1 1990-01-23 1990-01-25    TRUE   1990-01-01 1990-01-25
    3  1 1990-01-30 1990-01-31   FALSE   1990-01-30 1990-01-31
    

    3a) 或者,我们可以从 nbrs 形成 g 以避免像这样形成 adj

    g0 <- graph_from_edgelist(as.matrix(stack(nbrs)), directed = FALSE)
    g <- simplify(g0) # remove self loops
    

    图形

    顺便说一句,使用g,我们可以显示一个图形表示,其中节点 i 表示第 i 行,边表示重叠。

    plot(g)
    

    【讨论】:

      【解决方案3】:

      我为类似问题创建了一个解决方案。由于我需要将相同的逻辑应用于大型数据集,因此我的方法是 Rcppdata.table(排序速度的真正原因)。也适用于多个组 - id。 conti() 函数生成时间段的聚合范围,在这种情况下,没有中断一天(可以通过容差进行调整):

      conti <- function(
        data = df, 
        group = "id", #the group variable by which to aggregate the dates
        dateFrom = "startDate",
        dateTo = "endDate",
        tolerance = 0, #what gap shall be seen as uninterupted range on dates, here 0 tollerance
        dateFormat = "%Y-%m-%d" #date format in df
        ) {
        if(!require(Rcpp)){install.packages("Rcpp"); library(Rcpp)}
        if(!require(data.table)){install.packages("data.table"); library(data.table)}
        cppFunction('DataFrame BezRcpp(DataFrame dtable) {
                    int marker = 0;
                    IntegerVector ID = dtable["group"];
                    DateVector From = dtable["dateFrom"];
                    DateVector To = dtable["dateTo"];
                    IntegerVector Difference(ID.size(), 9999);
      
                    for (int i = 1; i < ID.size(); i++) {
                      if(ID[i] != ID[i-1]) {
                        marker = i;
                      } else {
                        Difference[i] = From[i] - To[marker];
                        if(Difference[i]>1) marker = i;
                          else if(To[i]>To[marker]){
                            To[marker] = To[i];
                    }}}
                    return DataFrame::create(
                        _["group"] = ID,
                        _["Difference"] = Difference,
                        _["dateFrom"] = From,
                        _["dateTo"] = To,
                        _["stringsAsFactors"] = false);
                  }'
        )
        conti_Rcpp_ <- function(data){
          A <- Sys.time()
          if(!"data.table" %in% class(data)) dtable <- as.data.table(data) else dtable <- copy(data)
          setnames(dtable, old = c("id", "startDate", "endDate"), new = c("group", "dateFrom", "dateTo"))
          if(class(dtable[["dateFrom"]])!="Date" || class(dtable[["dateTo"]])!="Date") for (j in c("dateFrom", "dateTo")) set(dtable, j = j, value = as.Date(dtable[[j]], dateFormat)) 
          setorderv(dtable, c("group", "dateFrom"))
          dt <- setDT(BezRcpp(dtable))
          dt <- dt[Difference>(tolerance+1), c("group", "dateFrom", "dateTo"), with = F]
          setnames(dt, new = c("id", "startDate", "endDate"), old = c("group", "dateFrom", "dateTo"))
          B <- Sys.time()
          print(paste0("Done in ", round(difftime(B, A, units = "secs"), 1), " secs. A data.table was produced."))
          return(dt)
        }
        return(conti_Rcpp_(data))
      }
      

      然后

      df <- data.frame(id = c(1L, 1L, 1L),
                       startDate = c("1990-01-01", "1990-01-23", "1990-01-30"),
                       endDate = c("1990-01-24", "1990-01-25", "1990-01-31"), stringsAsFactors = F)
      
      conti(df)
      #[1] "Done in 0 secs. A data.table was produced."
      #   id  startDate    endDate
      #1:  1 1990-01-01 1990-01-25
      #2:  1 1990-01-30 1990-01-31
      

      【讨论】:

        猜你喜欢
        • 2021-03-23
        • 2021-01-21
        • 2021-04-18
        • 2019-09-03
        • 2019-10-05
        • 1970-01-01
        • 2017-06-13
        • 2017-05-05
        • 2023-01-03
        相关资源
        最近更新 更多