автоматически определять столбцы даты при чтении файла в data.frame

При чтении файла функция read.table использует type.convert для различения логических, целочисленных, числовых, комплексных или факторных столбцов и сохранения их соответствующим образом.

Я хотел бы добавить даты в смесь, чтобы столбцы, содержащие даты, могли автоматически распознаваться и анализироваться в Date объектах. Следует распознавать только несколько форматов даты, например.

date.formats <- c("%m/%d/%Y", "%Y/%m/%d")

Вот пример:

fh <- textConnection(

 "num  char date-format1  date-format2  not-all-dates  not-same-formats
   10     a     1/1/2013    2013/01/01     2013/01/01          1/1/2013
   20     b     2/1/2013    2013/02/01              a        2013/02/01 
   30     c     3/1/2013            NA              b          3/1/2013"
)

И вывод

dat <- my.read.table(fh, header = TRUE, stringsAsFactors = FALSE,
                     date.formats = date.formats)
sapply(dat, class)

даст:

num              => numeric
char             => character
date-format1     => Date
date-format2     => Date
not-all-dates    => character
not-same-formats => character   # not a typo: date format must be consistent

Прежде чем я начну реализовывать его с нуля, есть ли что-то подобное уже в пакете? Или, может быть, кто-то уже взломал его (или собирается) и готов поделиться своим кодом здесь? Спасибо.


person flodel    schedule 22.08.2013    source источник
comment
Связано с Укажите формат даты для аргумента colClasses в read.table/read.csv.   -  person Joshua Ulrich    schedule 23.08.2013
comment
Связанный, да, и он может быть полезен людям, изучающим эту тему. Однако в моем случае мне нужно, чтобы столбцы даты автоматически определялись.   -  person flodel    schedule 23.08.2013
comment
по тому же формату важен ли точный формат или порядок ymd (например, «2013/01/01» и «2013-01-01» в одном столбце будут в порядке?)   -  person mnel    schedule 23.08.2013
comment
Для моих нужд я предпочитаю точно такой же формат, поскольку я предполагаю, что входные файлы поступают из доверенного процесса. Но ради помощи будущим посетителям не стесняйтесь нарушать правила.   -  person flodel    schedule 23.08.2013
comment
Но можем ли мы преобразовать четыре балла и семь лет назад в POSIXct?   -  person geneorama    schedule 29.05.2019
comment
Проверьте tryFormats в ?as.POSIXct. Это хороший список форматов строк для проверки.   -  person geneorama    schedule 29.05.2019


Ответы (3)


Вы можете использовать lubridate::parse_date_time, который является более строгим (и создает POSIXlt) данных.

Я также добавил дополнительную проверку существующих значений NA (может быть, это и не нужно).

eg

library(lubridate)
my.read.table <- function(..., date.formats = c("%m/%d/%Y", "%Y/%m/%d")) {
  dat <- read.table(...)
  for (col.idx in seq_len(ncol(dat))) {
    x <- dat[, col.idx]
    if(!is.character(x) | is.factor(x)) next
    if (all(is.na(x))) next
    for (format in date.formats) {
      complete.x <- !(is.na(x))
      d <- as.Date(parse_date_time(as.character(x), format, quiet = TRUE))
      d.na <- d[complete.x]
      if (any(is.na(d.na))) next
      dat[, col.idx] <- d         
    }
  }
  dat

}

 dat <- my.read.table(fh, stringsAsFactors = FALSE,header=TRUE)

str(dat)
'data.frame':   3 obs. of  6 variables:
 $ num             : int  10 20 30
 $ char            : chr  "a" "b" "c"
 $ date.format1    : Date, format: "2013-01-01" "2013-02-01" "2013-03-01"
 $ date.format2    : Date, format: "2013-01-01" "2013-02-01" NA
 $ not.all.dates   : chr  "2013/01/01" "a" "b"
 $ not.same.formats: chr  "1/1/2013" "2013/02/01" "3/1/2013"

Альтернативой может быть использование options(warn = 2) внутри функции и перенос parse_date_time(...) в оператор try.

my.read.table <- function(..., date.formats = c("%m/%d/%Y", "%Y/%m/%d")) {
  dat <- read.table(...)
  owarn <-getOption('warn')
  on.exit(options(warn = owarn))
  options(warn = 2)
  for (col.idx in seq_len(ncol(dat))) {
    x <- dat[, col.idx]
    if(!is.character(x) | is.factor(x)) next
    if (all(is.na(x))) next
    for (format in date.formats) {
      d <- try(as.Date(parse_date_time(as.character(x), format)), silent= TRUE)

      if (inherits(d, 'try-error')) next
      dat[, col.idx] <- d         
    }
  }
  dat

}
person mnel    schedule 23.08.2013
comment
@flodel - я только что добавил опцию, используя try вместо бита any(is.na(...)). - person mnel; 23.08.2013
comment
и преимущество в том, что он выйдет, как только не сможет разобрать дату? так быстрее в целом? - person flodel; 23.08.2013
comment
@flodel parse_date_time в целом не особенно быстр (поэтому, хотя могут быть некоторые ускорения, движок может быть медленнее). См. stackoverflow.com/questions/10645815/ для дальнейшего обсуждения (и, возможно, подходов, которые могут помочь) - person mnel; 23.08.2013

Можно попробовать с регулярными выражениями.

my.read.table <- function(..., date.formats = c("%m/%d/%Y", "%Y/%m/%d")) {
   require(stringr)
   formats <- c(
     "%m" = "[0-9]{1,2}",
     "%d" = "[0-9]{1,2}",
     "%Y" = "[0-9]{4}"
   )
   dat <- read.table(...)
   for (col.idx in seq_len(ncol(dat))) {
      for (format in date.formats) {
         x <- dat[, col.idx]
         if(!is.character(x) | is.factor(x)) break
         if (all(is.na(x))) break
         x <- as.character(x)
         # Convert the format into a regular expression
         for( k in names(formats) ) {
           format <- str_replace_all( format, k, formats[k] )
         }
         # Check if it matches on the non-NA elements
         if( all( str_detect( x, format ) | is.na(x) ) ) {
           dat[, col.idx] <- as.Date(x, format)
           break
         }
      }
   }
   dat
}

dat <- my.read.table(fh, header = TRUE, stringsAsFactors = FALSE)
as.data.frame(sapply(dat, class))
#                  sapply(dat, class)
# num                         integer
# char                      character
# date.format1                   Date
# date.format2                   Date
# not.all.dates             character
# not.same.formats          character
person Vincent Zoonekynd    schedule 22.08.2013

Здесь я собрал один быстро. Он не обрабатывает последний столбец должным образом, потому что функция as.Date недостаточно строгая (см., например, что as.Date("1/1/2013", "%Y/%m/%d") анализирует нормально...)

my.read.table <- function(..., date.formats = c("%m/%d/%Y", "%Y/%m/%d")) {
   dat <- read.table(...)
   for (col.idx in seq_len(ncol(dat))) {
      x <- dat[, col.idx]
      if(!is.character(x) | is.factor(x)) next
      if (all(is.na(x))) next
      for (f in date.formats) {
         d <- as.Date(as.character(x), f)
         if (any(is.na(d[!is.na(x)]))) next
         dat[, col.idx] <- d         
      }
   }
   dat
}

dat <- my.read.table(fh, header = TRUE, stringsAsFactors = FALSE)
as.data.frame(sapply(dat, class))

#                  sapply(dat, class)
# num                         integer
# char                      character
# date.format1                   Date
# date.format2                   Date
# not.all.dates             character
# not.same.formats               Date

Если вы знаете способ парсинга дат, который более строг в отношении форматов, чем as.Date (см. пример выше), сообщите мне об этом.

Изменить: чтобы сделать синтаксический анализ даты очень строгим, я могу добавить

if (!identical(x, format(d, f))) next

Чтобы это работало, мне нужно, чтобы все мои входные даты имели начальные нули там, где это необходимо, то есть 01/01/2013, а не 1/1/2013. Я могу жить с этим, если это стандартный способ.

person flodel    schedule 22.08.2013