【问题标题】:Efficiently convert a date column in data.table有效地转换 data.table 中的日期列
【发布时间】:2018-02-15 02:49:38
【问题描述】:

我有一个大型数据集,其中包含许多包含两种不同格式日期的列:

"1996-01-04" "1996-01-05" "1996-01-08" "1996-01-09" "1996-01-10" "1996-01-11"

"02/01/1996" "03/01/1996" "04/01/1996" "05/01/1996" "08/01/1996" "09/01/1996"

在这两种情况下,class() 都是“字符”。由于数据集有很多行(450万),我正在寻找一种高效的data.table转换方法。现在,我使用这个自建函数:

convert_to_date <- function(in_array){
  tmp <- try(as.Date(in_array, format = "%d/%m/%Y"),TRUE)
  if (all(!is.na(tmp)) & class(tmp) != "try-error"){
    return(tmp)
  } else{
    tmp2 <- try(as.Date(in_array),TRUE)
    if (all(!is.na(tmp2)) & class(tmp2) != "try-error"){
      return(tmp2)
    } else{
      return(in_array)
    }
  }
}

然后我用它转换我需要的列(data.table DF)

DF[,date:=convert_to_date(date)]

但是,这仍然非常慢(每列将近 45 秒)。

有什么方法可以通过 data.table 方法优化它吗?到目前为止,我还没有找到更好的方法,所以我会感谢任何提示。

P.S:为了更好的可读性,我将函数“外包”到了第二个文件,并在我的主程序中获取了它。这对 R 中的计算速度有(负面)显着影响吗?

【问题讨论】:

  • data.tableas.IDate()
  • 有趣的问题,但您必须为此发布可重现的示例。
  • This answer 包含将字符日期转换为 Date 类的各种方法和包的基准。
  • @docendodiscimus 谢谢,但根据我的个人经验 as.Date() 和 as.IDate() 对于 data.table 对象的工作速度相当快,我还没有运行基准测试,但它确实没有“感觉”更快,所以我试图在其他地方破解我的代码。
  • 仅供参考,IDate 的加速不在转换中;它存在于之后的所有内容中(过滤、加入)。通常使用整数来做这类事情会比浮点数更快。

标签: r date data.table


【解决方案1】:

根据this benchmark,将标准明确格式(YYYY-MM-DD)的字符日期转换为Date 类的最快方法是使用as.Date(fasttime::fastPOSIXct())

不幸的是,这需要事先测试格式,因为您的其他格式 DD/MM/YYYYfasttime::fastPOSIXct() 误解了。

所以,如果您不想担心每个日期列的格式,您可以使用anytime::anydate() 函数:

# sample data
df <- data.frame(
    X1 = c("1996-01-04", "1996-01-05", "1996-01-08", "1996-01-09", "1996-01-10", "1996-01-11"), 
    X2 = c("02/01/1996", "03/01/1996", "04/01/1996", "05/01/1996", "08/01/1996", "09/01/1996"), 
    stringsAsFactors = FALSE)

library(data.table)
# convert date columns
date_cols <- c("X1", "X2")
setDT(df)[, (date_cols) := lapply(.SD, anytime::anydate), .SDcols = date_cols]
df
           X1         X2
1: 1996-01-04 1996-02-01
2: 1996-01-05 1996-03-01
3: 1996-01-08 1996-04-01
4: 1996-01-09 1996-05-01
5: 1996-01-10 1996-08-01
6: 1996-01-11 1996-09-01

benchmark timings 表明anytime 包提供的便利性和性能之间存在折衷。因此,如果速度至关重要,则没有其他方法可以测试每列的格式并使用该格式可用的最快转换方法。

OP 为此目的使用了try() 函数。下面的解决方案使用 正则表达式 来查找与给定格式匹配的所有列(仅第 1 行用于节省时间)。这样做的另一个好处是相关列的名称是自动确定的,无需输入。

# enhanced sample data with additional columns
df <- data.frame(
    X1 = c("1996-01-04", "1996-01-05", "1996-01-08", "1996-01-09", "1996-01-10", "1996-01-11"), 
    X2 = c("02/01/1996", "03/01/1996", "04/01/1996", "05/01/1996", "08/01/1996", "09/01/1996"), 
    X3 = "other data",
    X4 = 1:6,
    stringsAsFactors = FALSE)

library(data.table)
options(datatable.print.class = TRUE)

# coerce to data.table
setDT(df)[]
# convert date columns in standard unambiguous format YYYY-MM-DD
date_cols1 <- na.omit(names(df)[
  df[1, sapply(.SD, stringr::str_detect, pattern = "\\d{4}-\\d{2}-\\d{2}"),]])
# use fasttime package
df[, (date_cols1) := lapply(.SD, function(x) as.Date(fasttime::fastPOSIXct(x))), 
   .SDcols = date_cols1]
# convert date columns in DD/MM/YYYY format
date_cols2 <- na.omit(names(df)[
  df[1, sapply(.SD, stringr::str_detect, pattern = "\\d{2}/\\d{2}/\\d{4}"),]])
# use lubridate package
df[, (date_cols2) := lapply(.SD, lubridate::dmy), .SDcols = date_cols2]
df
           X1         X2         X3    X4
       <Date>     <Date>     <char> <int>
1: 1996-01-04 1996-01-02 other data     1
2: 1996-01-05 1996-01-03 other data     2
3: 1996-01-08 1996-01-04 other data     3
4: 1996-01-09 1996-01-05 other data     4
5: 1996-01-10 1996-01-08 other data     5
6: 1996-01-11 1996-01-09 other data     6

警告

如果其中一个日期列确实包含NA在第一行,则此列可能会未经转换而转义。为了处理这些情况,需要修改上面的代码。

【讨论】:

  • 谢谢 Uwe!这正是我想要的!我不知道 str_detect/pattern 组合,它看起来比我的“尝试”方法好得多。 Fasttime 对我来说也是新的,看起来很有趣。到目前为止,我远离 Posix,因为我相信它总是会转换为数字,而我想保留日期字符串以便于调试,但我会看看你的代码如何处理它。我将在今晚晚些时候实现它,但您的代码显然符合我的要求!非常感谢
  • 只有一件事:df[, (date_cols2) := lapply(.SD, lubridate::mdy), .SDcols = date_cols2] 为我产生以下错误:警告消息:所有格式都无法解析。未找到任何格式。 因此,由于我不熟悉该软件包,因此我已切换回 df[, (date_cols2) := lapply(.SD, as.Date,format = "%d/%m/%Y"), .SDcols = date_cols2] 但如果您知道问题所在,如果 lubridate 有解决方案,我会很感兴趣。日期格式为 DD/MM/YYYY。
  • 我的错,我选择了错误的日期格式 MM/DD/YYYY 而不是 DD/MM/YYYY,因此选择了错误的 lubridate 函数。请尝试lubridate::dmy()。 (请注意,函数以它们支持的格式命名,例如,dmy(), mdy(), ymd() 等。)我已经相应地更新了我的答案。
  • 不幸的是,fasttime::fastPOSIXct("1969-05-05") 返回 NA,因为它不会解析 1970 年之前的年份,所以这是使用 anytime::anydate() 的另一个原因。
【解决方案2】:

您的数据

df <- data.frame(X1 = c("1996-01-04", "1996-01-05", "1996-01-08", "1996-01-09", "1996-01-10", "1996-01-11"), X2 = c("02/01/1996", "03/01/1996", "04/01/1996", "05/01/1996", "08/01/1996", "09/01/1996"), stringsAsFactors=F)

'data.frame':   6 obs. of  2 variables:
 $ X1: chr  "1996-01-04" "1996-01-05" "1996-01-08" "1996-01-09" ...
 $ X2: chr  "02/01/1996" "03/01/1996" "04/01/1996" "05/01/1996" ...

解决方案

library(dplyr)
library(lubridate)
ans <- df %>%
         mutate(X1 = ymd(X1), X2 = mdy(X2))

          X1         X2
1 1996-01-04 1996-02-01
2 1996-01-05 1996-03-01
3 1996-01-08 1996-04-01
4 1996-01-09 1996-05-01
5 1996-01-10 1996-08-01
6 1996-01-11 1996-09-01

str(ans)

'data.frame':   6 obs. of  2 variables:
 $ X1: Date, format: "1996-01-04" "1996-01-05" ...
 $ X2: Date, format: "1996-02-01" "1996-03-01" ...

【讨论】:

  • 感谢 CPak,这看起来也很有效,但考虑到我正在做的大规模计算,我想尽可能地坚持使用 data.table。尽管如此,还是非常感谢您的工作!
【解决方案3】:

因为您事先知道只有两种日期格式,所以这很容易。 as.Dateformat 参数是矢量化的:

as_date_either <- function(x) {
    format_vec <- rep_len("%Y-%m-%d", length(x))
    format_vec[grep("/", x, fixed = TRUE)] <- "%m/%d/%Y"
    as.Date(x, format = format_vec)
}

已编辑:将 ifelse 替换为子集分配,这样更快

【讨论】:

    【解决方案4】:

    如果您的数据集中有任何重复的日期字段,那么您可以做的一种方法是设置去重复的参考表,然后在较小的数据集上进行映射。这将比转换所有记录上的日期字段更快。

    数据

    df <- data.frame(
      X1 = c("1996-01-04", "1996-01-05", "1996-01-08", "1996-01-09", "1996-01-10", rep("1996-01-11", 100)), 
      X2 = c("02/01/1996", "03/01/1996", "04/01/1996", "05/01/1996", "08/01/1996", rep("09/01/1996", 100)), 
      stringsAsFactors = FALSE)
    

    为映射创建唯一的日期行

    date_mapping <- function(date_col){
    
      ref_df <- data.frame(date1 = unique(date_col), stringsAsFactors = FALSE)
    
      if(all(grepl("/", ref_df$date1))) {
        ref_df$date2 <- as.Date(ref_df$date1, format = "%d/%m/%Y")
    
      } else {
        ref_df$date2 <- as.Date(ref_df$date1)  
      }
    
      date_col_mapped <- ref_df[match(date_col, ref_df$date1), "date2"]
    
      return(date_col_mapped)
    
    }
    
    
    date_mapping(df$X1)
    date_mapping(df$X2)
    

    【讨论】:

      猜你喜欢
      • 2020-11-15
      • 1970-01-01
      • 1970-01-01
      • 2014-06-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-07
      相关资源
      最近更新 更多