【问题标题】:R loops - is there a more efficient way?R循环 - 有没有更有效的方法?
【发布时间】:2016-02-01 00:00:15
【问题描述】:

我有一个数据框,其中每一行都有一个唯一的 ID。我需要根据开始日期与结束日期和批准日期的最大值之间的天数复制这些行中的每一行。

ID <- c(1,2)

Value <- c(10,20)

StartDate <- c(as.Date("01/01/2015", '%d/%m/%Y'),
    as.Date("01/01/2015", '%d/%m/%Y'))

EndDate <- c(as.Date("31/01/2015", '%d/%m/%Y'), 
    as.Date("15/01/2015", '%d/%m/%Y'))

AppDate <- c(as.Date("15/01/2015", '%d/%m/%Y'), 
    as.Date("15/02/2015", '%d/%m/%Y'))

df <- data.frame(ID, Value, StartDate, EndDate, AppDate)

df <- df[rep(row.names(df), ifelse(as.numeric(df$AppDate) >
    as.numeric(df$EndDate),as.numeric(df$AppDate-df$StartDate),
    as.numeric(df$EndDate-df$StartDate)) + 1),]

然后我需要添加从开始日期到结束日期或批准日期的最大值的顺序日期列表。

我已经通过 2 个循环完成了这项工作。外部循环遍历每个唯一 ID 的数据帧。然后第二个循环遍历 ID 并添加日期。一旦第二个循环完成,它会将行传递到外部循环作为新的起点。

IDs <- unique(df$ID)
df$Days <- rep(as.Date("01/01/1999",'%d/%m/%Y'), nrow(df))
counter <- 1
for (i in 1:length(IDs)) {
    ref <- IDs[i]
    start <- 1
        while (df$ID[counter] == ref) {
            ifelse(start == 1, df$Days[counter] <- df$StartDate[counter],
                df$Days[counter] <- df$StartDate[counter] + start -1)
            ifelse (counter > nrow(df), break, counter <- counter + 1)
            ifelse (counter > nrow(df), break, start <- start + 1)
        }
 }

我的实际数据集有超过 6,000 个 ID,一旦我复制了行,它最终会超过 500,000 行。循环运行时间超过 15 分钟,因此显然效率很低。

所以我想我有 2 个问题。

1)。在R中执行此操作的最有效方法是什么

2)。一般来说,这样做最有效的方法是什么,即说像 C++ 之类的东西

谢谢

【问题讨论】:

  • 我认为本问答中的data.table 答案提供了有效解决方案的一般原则:Expanding a sequence in a data frame。谷歌“R expand date range data.table”应该提供几个类似的例子。

标签: r loops for-loop nested-loops


【解决方案1】:

这是一种矢量化的解决方案。注意:您的代码与我尝试的取 EndDate 和 AppDate 最大值的概念不匹配,但如果这不是您想要的,您可以相应地修改代码。

library(dplyr)
df <- df %>% group_by(ID) %>% mutate(Days = rep(seq(min(StartDate), max(EndDate, df$AppDate), 'days'), ceiling(nrow(df) / n()))[1:n()])

输出将如下(仅前几行):

head(df)
Source: local data frame [6 x 6]
Groups: ID [1]

     ID Value  StartDate    EndDate    AppDate       Days
  (dbl) (dbl)     (date)     (date)     (date)     (date)
1     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-01
2     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-02
3     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-03
4     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-04
5     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-05
6     1    10 2015-01-01 2015-01-31 2015-01-15 2015-01-06

tail(df)
Source: local data frame [6 x 6]
Groups: ID [1]

     ID Value  StartDate    EndDate    AppDate       Days
  (dbl) (dbl)     (date)     (date)     (date)     (date)
1     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-10
2     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-11
3     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-12
4     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-13
5     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-14
6     2    20 2015-01-01 2015-01-15 2015-02-15 2015-02-15

【讨论】:

  • 感谢它完美运行。我喜欢矢量化版本与我编写的代码的简单性。虽然它比我预期的要慢很多,这是最快的方法吗?
  • 取决于您正在处理的数据大小、组数和日期范围。此外,如果您定义“慢”,它可能会有所帮助。有一个名为data.table 的不同包被认为比dplyr 更有效。我不使用它是因为我发现dplyr 更加通用和直观(个人意见)。其他人也许可以帮助您将代码转换为 data.table 版本。
【解决方案2】:

通常,我会推荐返回笛卡尔积(两组之间的所有组合)的交叉连接 SQL 查询。但是,您可以使用 merge() 复制 R 中的交叉连接,而无需任何 by 参数和 all=True。从那里过滤EndDate 截止:

# CALCULATE CONDITIONAL END DATE
df$TrueEndDate <- as.Date(ifelse(df$AppDate > df$EndDate,
                                 df$AppDate,
                                 df$EndDate), origin="1970-01-01")

# CREATE A SEQUENTIAL DATES DATA FRAME (HERE IS 60 DAYS FROM 2015-01-01)
dates <- data.frame(Date=as.Date(unlist(lapply(0:60, function(x) 
                                      as.Date("2015-01-01") + x)),                    
                                 origin="1970-01-01"))   

# RUN CROSS JOIN MERGE, PULLING ONLY NEEDED FIELDS
mergedf <- merge(df[c('ID', 'StartDate', 'TrueEndDate')], dates, all=TRUE)

# FILTER OUT DATES PAST ROW'S TRUE END DATE
mergedf <- mergedf[(mergedf$Date <= mergedf$TrueEndDate),]

# CLEANUP
mergedf <- mergedf[with(mergedf, order(ID)), ]     # ORDER BY ID
row.names(mergedf) <- 1:nrow(mergedf)              # RESET ROW NAMES

您是否对等效的交叉连接 SQL 感到好奇(您可以在 RDMS 引擎上调用 R 并作为最终数据帧导入,这可能有助于解决性能问题):

SELECT ID.ID, ID.Value, ID.StartDate, 
       CASE WHEN ID.AppDate > ID.EndDate 
            THEN ID.AppDate 
            ELSE ID.EndDate 
       END As TrueEndDate, 
Dates.Dates
FROM ID, Dates
WHERE  Dates.Dates <= CASE WHEN ID.AppDate > ID.EndDate 
                           THEN ID.AppDate ELSE ID.EndDate 
                      END
ORDER BY ID.ID, Dates.Dates

【讨论】:

    猜你喜欢
    • 2015-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-07
    • 2015-04-28
    • 1970-01-01
    • 2016-04-14
    相关资源
    最近更新 更多