0
votes

I have written the function below that takes a non-standard time format e.g. '730' (7:30) and converts it to a decimal number of hours e.g. '7.5'.

decimal_time <- function(x) {
  x <- as.character(x)
  tmp <- nchar(x)

  if (tmp < 4 & !is.na(tmp)){
    x <- paste0(strrep('0',4-tmp),as.character(x))
  }

  x <-  sub("([[:digit:]]{2,2})$", ":\\1", x)
  x <- strsplit(x,':')[[1]]
  x <- as.numeric(x)
  x[1]+x[2]/60
}

To apply it to one column I do the following...

dt_times[, New_Time := lapply(Time, decimal_time)]

However I can't figure out how to apply this same function to many columns that share the same format. Of course, if it was a vectorised function (like 'mean') then I could just write...

dt_times[, lapply(.SD, mean), .SDcols = c('col1', 'col2')]

... but what do I do if my function uses lapply in the first place?! Help please!

4
dt_times[, c('col1', 'col2') := lapply(.SD, decimal_time), .SDcols = c('col1', 'col2')]? - Jaap
Thanks but that doesn't seem to work - I get the results for the first row on every row... - astro person
" In if (tmp < 4 & !is.na(tmp)) { : the condition has length > 1 and only the first element will be used " - astro person

4 Answers

2
votes

If your problem is that you have not a vectorised function, then you could use sapply inside the function

decimal_time <- function(y) {
  sapply(y,function(x) {
    x <- as.character(x)
    tmp <- nchar(x)

    if (tmp < 4 & !is.na(tmp)){
      x <- paste0(strrep('0',4-tmp),as.character(x))
    }

    x <-  sub("([[:digit:]]{2,2})$", ":\\1", x)
    x <- strsplit(x,':')[[1]]
    x <- as.numeric(x)
    x[1]+x[2]/60
  })
}
2
votes

You do not need any loops (outside or inside the function). You can fully vectorize your function:

decimal_time <- function(x) {
  x <- as.character(x)
  tmp <- nchar(x)
  ii <- tmp < 4 & !is.na(tmp)
  x[ii] <- paste0(strrep('0',4-tmp[ii]), x[ii])

   x <-  sub("([[:digit:]]{2,2})$", ":\\1", x)
  x <-  strsplit(x,':')
  x <- do.call(rbind, x)
  mode(x) <- "numeric"
  x[,1]+x[,2]/60
}

x <- c("1", "730")
decimal_time(x)
#[1] 0.01666667 7.50000000

Using integer division it's even easier than with text processing:

decimal_time <- function(x) {
  x <- as.integer(x)
  if (any(x >= 2400)) warning("input >= 24 h")
  x %/% 100 + (x %% 100) / 60
}

x <- c("1", "730")
decimal_time(x)
#[1] 0.01666667 7.50000000
0
votes

This is an issue I've encountered in the past. My solution is typically to just run a for loop:

for(col in c('col1', 'col2'){
 dt_times[, (col):= vapply(col, function(x) decimal_time(get(x)), FUN.VALUE = numeric(1))]
}

Maybe not the most elegant solution, but it should get the job done.

-1
votes

I'd recommend you to use the map_dfr function from the purrr package to apply a function over data.frame, returning a data.frame as well. Under the hood, the map_* family of functions iterate the same as using for loops, but in a more readable and tidy way.

Also, if you'd like to map this function to specific column names you could as well use the dplyr package as well, combining the use of the filter and contains functions, you can modify those specific variables. Combining these features:

library(dplyr)
library(purrr)

df %>%
  filter(contains("some_string")) %>%
  map_dfr(decimal_time)