29

我开始在 R 中使用 data.table 包来提高代码的性能。我正在使用以下代码:

sp500 <- read.csv('../rawdata/GMTSP.csv')
days <- c("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday")

# Using data.table to get the things much much faster
sp500 <- data.table(sp500, key="Date")
sp500 <- sp500[,Date:=as.Date(Date, "%m/%d/%Y")]
sp500 <- sp500[,Weekday:=factor(weekdays(sp500[,Date]), levels=days, ordered=T)]
sp500 <- sp500[,Year:=(as.POSIXlt(Date)$year+1900)]
sp500 <- sp500[,Month:=(as.POSIXlt(Date)$mon+1)]

我注意到,与创建工作日等的其他函数相比,as.Date 函数完成的转换非常慢。这是为什么呢?有没有更好/更快的解决方案,如何转换为日期格式?(如果你问我是否真的需要日期格式,可能是的,因为然后使用 ggplot2 来制作绘图,这对于这种类型的数据来说就像一个魅力。)

更准确地说

> system.time(sp500 <- sp500[,Date:=as.Date(Date, "%m/%d/%Y")])
   user  system elapsed 
 92.603   0.289  93.014 
> system.time(sp500 <- sp500[,Weekday:=factor(weekdays(sp500[,Date]), levels=days, ordered=T)])
   user  system elapsed 
  1.938   0.062   2.001 
> system.time(sp500 <- sp500[,Year:=(as.POSIXlt(Date)$year+1900)])
   user  system elapsed 
  0.304   0.001   0.305 

在 MacAir i5 上,观测值略低于 3000000。

4

6 回答 6

31

正如其他人提到的,strptime(从字符转换为 POSIXlt)是这里的瓶颈。另一个简单的解决方案是使用lubridate包及其fast_strptime方法。

这是我的数据上的样子:

> tables()
     NAME      NROW  MB COLS                                     
[1,] pp   3,718,339 126 session_id,date,user_id,path,num_sessions
     KEY         
[1,] user_id,date
Total: 126MB

> pp[, 2]
               date
      1: 2013-09-25
      2: 2013-09-25
      3: 2013-09-25
      4: 2013-09-25
      5: 2013-09-25
     ---           
3718335: 2013-09-25
3718336: 2013-09-25
3718337: 2013-09-25
3718338: 2013-10-11
3718339: 2013-10-11

> system.time(pp[, date := as.Date(fast_strptime(date, "%Y-%m-%d"))])
   user  system elapsed 
  0.315   0.026   0.344  

为了比较:

> system.time(pp[, date := as.Date(date, "%Y-%m-%d")])
   user  system elapsed 
108.193   0.399 108.844 

这大约快 316 倍!

于 2014-06-10T18:38:34.890 回答
24

我认为这只是as.Date转换characterDatevia POSIXlt,使用strptime. 而且strptime很慢,我相信。

要自己跟踪它,请键入as.Date,然后methods(as.Date),然后查看character方法。

> as.Date
function (x, ...) 
UseMethod("as.Date")
<bytecode: 0x2cf4b20>
<environment: namespace:base>

> methods(as.Date)
[1] as.Date.character as.Date.date      as.Date.dates     as.Date.default  
[5] as.Date.factor    as.Date.IDate*    as.Date.numeric   as.Date.POSIXct  
[9] as.Date.POSIXlt  
   Non-visible functions are asterisked

> as.Date.character
function (x, format = "", ...) 
{
    charToDate <- function(x) {
        xx <- x[1L]
        if (is.na(xx)) {
            j <- 1L
            while (is.na(xx) && (j <- j + 1L) <= length(x)) xx <- x[j]
            if (is.na(xx)) 
                f <- "%Y-%m-%d"
        }
        if (is.na(xx) || !is.na(strptime(xx, f <- "%Y-%m-%d", 
            tz = "GMT")) || !is.na(strptime(xx, f <- "%Y/%m/%d", 
            tz = "GMT"))) 
            return(strptime(x, f))
        stop("character string is not in a standard unambiguous format")
    }
    res <- if (missing(format)) 
        charToDate(x)
    else strptime(x, format, tz = "GMT")       ####  slow part, I think  ####
    as.Date(res)
}
<bytecode: 0x2cf6da0>
<environment: namespace:base>
> 

为什么as.POSIXlt(Date)$year+1900比较快?再次,通过以下方式追踪它:

> as.POSIXct
function (x, tz = "", ...) 
UseMethod("as.POSIXct")
<bytecode: 0x2936de8>
<environment: namespace:base>

> methods(as.POSIXct)
[1] as.POSIXct.date    as.POSIXct.Date    as.POSIXct.dates   as.POSIXct.default
[5] as.POSIXct.IDate*  as.POSIXct.ITime*  as.POSIXct.numeric as.POSIXct.POSIXlt
   Non-visible functions are asterisked

> as.POSIXlt.Date
function (x, ...) 
{
    y <- .Internal(Date2POSIXlt(x))
    names(y$year) <- names(x)
    y
}
<bytecode: 0x395e328>
<environment: namespace:base>
> 

感兴趣,让我们深入研究 Date2POSIXlt。对于这一点,我们需要 grep main/src 来知道要查看哪个 .c 文件。

~/R/Rtrunk/src/main$ grep Date2POSIXlt *
names.c:{"Date2POSIXlt",do_D2POSIXlt,   0,  11, 1,  {PP_FUNCALL, PREC_FN,   0}},
$

现在我们知道我们需要寻找 D2POSIXlt :

~/R/Rtrunk/src/main$ grep D2POSIXlt *
datetime.c:SEXP attribute_hidden do_D2POSIXlt(SEXP call, SEXP op, SEXP args, SEXP env)
names.c:{"Date2POSIXlt",do_D2POSIXlt,   0,  11, 1,  {PP_FUNCALL, PREC_FN,   0}},
$

哦,我们可以猜到 datetime.c。无论如何,看看最新的实时副本:

日期时间.c

在那里搜索D2POSIXlt,您会发现从 Date(数字)到 POSIXlt 是多么简单。您还将看到 POSIXlt 如何是一个实数向量(8 个字节)加上七个整数向量(每个 4 个字节)。这是 40 个字节,每个日期!

所以问题的症结(我认为)是为什​​么strptime这么慢,也许可以在 R 中改进。或者POSIXlt直接或间接地避免。


这是一个使用相关项目数 (3,000,000) 的可重现示例:

> Range = seq(as.Date("2000-01-01"),as.Date("2012-01-01"),by="days")
> Date = format(sample(Range,3000000,replace=TRUE),"%m/%d/%Y")
> system.time(as.Date(Date, "%m/%d/%Y"))
   user  system elapsed 
 21.681   0.060  21.760 
> system.time(strptime(Date, "%m/%d/%Y"))
   user  system elapsed 
 29.594   8.633  38.270 
> system.time(strptime(Date, "%m/%d/%Y", tz="GMT"))
   user  system elapsed 
 19.785   0.000  19.802 

通过tz似乎加速strptimeas.Date.character确实如此。所以也许这取决于你的语言环境。但strptime似乎是罪魁祸首,不是data.table。也许重新运行这个例子,看看你的机器上是否需要 90 秒?

于 2012-10-08T20:28:05.033 回答
8

感谢您的建议。我通过自己为日期编写高斯算法解决了这个问题,并得到了更好的结果,见下文。

getWeekDay <- function(year, month, day) {
  # Implementation of the Gaussian algorithm to get weekday 0 - Sunday, ... , 7 - Saturday
  Y <- year
  Y[month<3] <- (Y[month<3] - 1)

  d <- day
  m <- ((month + 9)%%12) + 1
  c <- floor(Y/100)
  y <- Y-c*100
  dayofweek <- (d + floor(2.6*m - 0.2) + y + floor(y/4) + floor(c/4) - 2*c) %% 7
  return(dayofweek)
}

sp500 <- read.csv('../rawdata/GMTSP.csv')
days <- c("Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday")

# Using data.table to get the things much much faster
sp500 <- data.table(sp500, key="Date")
sp500 <- sp500[,Month:=as.integer(substr(Date,1,2))]
sp500 <- sp500[,Day:=as.integer(substr(Date,4,5))]
sp500 <- sp500[,Year:=as.integer(substr(Date,7,10))]
#sp500 <- sp500[,Date:=as.Date(Date, "%m/%d/%Y")]
#sp500 <- sp500[,Weekday:=factor(weekdays(sp500[,Date]), levels=days, ordered=T)]
sp500 <- sp500[,Weekday:=factor(getWeekDay(Year, Month, Day))]
levels(sp500$Weekday) <- days

运行上面的整个块给出(包括从 csv 读取日期)...... Data.table 确实令人印象深刻。

user  system elapsed 
 19.074   0.803  20.284 

转换本身的时间是 3.49 过去。

于 2012-10-09T06:56:30.013 回答
7

这是一个老问题,但我认为这个小技巧可能很有用。如果您有多行具有相同的日期,您可以这样做

data[, date := as.Date(date[1]), by = date]

它要快得多,因为它只处理每个日期一次(在我的 4000 万行数据集中,它从 25 秒变为 0.5 秒)。

于 2017-04-06T21:16:38.043 回答
2

我原本以为:“上面 as.Date 的参数没有指定格式。”

我现在想:我假设您键入的日期值是标准格式。我猜不会。所以你正在做两个过程。您正在从字符格式重新格式化为日期格式,并且您正在根据具有完全不同排序顺序的新值重新排序。

于 2012-10-08T18:27:22.657 回答
0

我在 CRAN 上创建了一个名为 calcUnique 的小(单功能)包,它应该可以加快速度。改编自马特·道尔...

> Range = seq(as.Date("2000-01-01"), as.Date("2012-01-01"), by = "days")
> Date = format(sample(Range, 3000000, replace = TRUE), "%m/%d/%Y")
> system.time(as.Date(Date, "%m/%d/%Y"))
   user  system elapsed 
  1.160   0.000   1.157 
> system.time(strptime(Date, "%m/%d/%Y"))
   user  system elapsed 
  2.628   2.385   5.008 
> system.time(strptime(Date, "%m/%d/%Y", tz = "GMT"))
   user  system elapsed 
  0.849   0.001   0.851 
> system.time(calcUnique::calcUnique(Date, strptime, format = "%m/%d/%Y"))
   user  system elapsed 
  0.244   0.000   0.244 
> system.time(calcUnique::calcUnique(Date, strptime, format = "%m/%d/%Y", tz = "GMT"))
   user  system elapsed 
  0.173   0.000   0.172 
> 
于 2021-12-07T23:08:53.247 回答