2012-10-08 28 views
24

Empecé a usar el paquete data.table en R para mejorar el rendimiento de mi código. Estoy utilizando el siguiente código:¿Por qué es tan lenta la fecha en un vector de caracteres?

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)] 

me di cuenta de que la conversión realizada por la función as.Date es muy lento, en comparación con otras funciones que crean los días de semana, etc. ¿Por qué? ¿Hay una solución mejor/más rápida, cómo convertir a formato de fecha? (Si le pregunta si realmente necesito el formato de fecha, probablemente sí, porque entonces utilizar ggplot2 para hacer gráficos, que funcionan a las mil maravillas con este tipo de datos.)

Para ser más precisos

> 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 

En MacAir i5 con un poco menos de 3000000 observaciones.

Gracias

+0

Es bastante extraño que sea mucho más lento. Debería estar haciendo el equivalente a: dPl <- as.POSIXlt; with (paste (mon, mdate, year, sep = "/") –

+0

Agregué el tiempo por encima de ... – krhlk

+0

¿Has mirado los resultados de la conversión? –

Respuesta

18

creo que es justo que as.Date convierte character a través DatePOSIXlt, utilizando strptime. Y strptime es muy lento, creo.

Para rastrearlo a través de usted, escriba as.Date, luego methods(as.Date), luego observe el método 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> 
> 

¿Por qué es as.POSIXlt(Date)$year+1900 relativamente rápido? De nuevo, rastrearlo a través de:

> 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> 
> 

Intrigado, profundicemos en Date2POSIXlt. Para este bit, necesitamos grep main/src para saber qué archivo .c debemos ver.

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

Ahora sabemos que necesitamos buscar 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}}, 
$ 

Oh, que podría tener datetime.c adivinado. De todos modos, así que mirando copia más reciente en vivo:

datetime.c

Búsqueda de allí para D2POSIXlt y verá lo fácil que es pasar de la fecha (numérico) para POSIXlt. También verá cómo POSIXlt es un vector real (8 bytes) más siete vectores enteros (4 bytes cada uno). ¡Eso es 40 bytes, por fecha!

Así que el quid de la cuestión (creo) es por qué strptime es tan lento, y tal vez eso se puede mejorar en R. O simplemente evitar POSIXlt, ya sea directa o indirectamente.


Aquí está un ejemplo reproducible usando el número de elementos indicados en cuestión (3000000):

> 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 

Pasando tz parece acelerar strptime, lo que hace as.Date.character. Entonces tal vez depende de tu localidad. Pero strptime parece ser el culpable, no data.table. Quizás vuelva a ejecutar este ejemplo y vea si le lleva 90 segundos en su equipo.

+0

system.time (as.Date (Date, "% m /% d /% Y")) toma 48.594s de tiempo transcurrido. – krhlk

2

principio pensé: "El argumento para as.Date anterior no tiene el formato especificado."

Ahora pienso: asumí que el valor de Fecha que codificabas estaba en un formato estándar. Supongo que no. Entonces estás haciendo dos procesos. Está formateando de nuevo el formato de fecha y está reorganizando en función de los nuevos valores que tienen una secuencia de clasificación completamente diferente.

+0

Re-sorting? ¿Qué significa exactamente? – krhlk

+0

Para establecer una clave, necesita crear un índice ... una tabla hash, creo ... que le permite hacer las búsquedas rápidas que proporciona data.table. Si cambia los valores de la clave, supongo que debe volver a hacer la creación de la tabla de búsqueda. –

+0

Aha, pero entonces supongo que lo hace con cada valor cambiado (que me parece contradictorio, de hecho creo que la tabla hash solo actualizaciones después de la actualización, corrígeme si estoy equivocado). La conversión de data.frame a data.table va rápido> system.time (sp500 <- data.table (sp500, key = "Date")) sistema de usuario transcurrido 0.168 0.024 0.192 – krhlk

8

Gracias por las sugerencias. Lo resolví escribiendo el algoritmo gaussiano para las fechas y obtuve mejores resultados, ver abajo.

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 

Ejecutando todo el bloque anterior da (incluida la lectura de la fecha de csv) ... Data.table es realmente impresionante.

user system elapsed 
19.074 0.803 20.284 

El tiempo de la conversión en sí es 3.49 transcurrido.

+0

Me alegro de que estés contento, pero 20 años para esto todavía parece relativamente lento. Aquí hay algunas preguntas relacionadas [search for + strptime + slow] (http://stackoverflow.com/search?q=%5Br%5D+%2Bstrptime+%2Bslow) que pueden ser útiles. –

+0

@Matthew Dowle El tiempo de la función en sí es de 3,5 segundos, lo cual me parece bien. La mayoría del tiempo de los 20 se consume al leer los datos del disco. (Actualicé arriba.) – krhlk

+0

Ah, me perdí ese pedacito. Eso es mejor. –

24

Como mencionan otros, strptime (conversión de caracteres a POSIXlt) es el cuello de botella aquí. Otra solución simple es utilizar el paquete lubridate y su método fast_strptime.

Esto es lo que parece en mis datos:

> 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, with = F] 
       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 

Para la comparación:

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

Eso es ~ 316 veces más rápido!

+0

Este es un consejo fue increíble. Espero que la gente de data.table incorpore tal velocidad. Ahora hice funciones de fecha/hora "rápidas" y las puse en la mayoría de mis paquetes, p. fast.as.IDate <- function (x, format = "% Y-% m-% d", ...) data.table :: as.IDate (lubridate :: fast_strptime (x, format = format, ..) .)) –

5

Esta es una vieja pregunta, pero creo que este pequeño truco podría ser útil. Si tiene varias filas con la misma fecha, se puede hacer

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

Es mucho más rápido, ya que sólo procesa cada día una vez (en mi conjunto de datos de 40 millones de filas que va de 25 segundos a 0,5 segundos).

+0

Para ampliar por qué esto es más rápido, porque estaba confundido: supongamos que tiene información para la venta de un millón de artículos, y rastreó el número de ventas durante 1 semana. Tendría 7 millones de filas. Sin la parte "por", calcularía el día de la semana para una fila, luego pasaría a la siguiente y calculará, luego la siguiente, etc. Usando la parte "por", primero agrupará sus filas en días específicos (tan 7 días) y entonces solo calcula el día de la semana para cada uno de esos 7 días (7 cálculos en lugar de 7 millones). Luego toma el resultado y lo asigna como una nueva columna para cada grupo respectivo –

Cuestiones relacionadas