Mix values from 2 dataframes in a mutate function with conditions

Viewed 68

I have 2 dataframes with these forms:

DF1 <- data.frame(
  idCarte = c('a', 'a', 'b', 'b', 'b'),
  idPack = c('1', '2', '2', '3', '3'),
  timeIn = c('10:00:02', '12:00:50', '11:40:00', '12:10:35', '15:15:00'),
  timeOut = c('12:00:00', '14:00:00', '11:50:00', '15:00:00', '16:00:00')
)

DF1
idCarte  idPack      timeIn   timeOut
      a      1     10:00:02  12:00:00
      a      2     12:00:50  14:00:00
      b      2     11:40:00  11:50:00
      b      3     12:10:35  15:00:00
      b      3     15:15:35  16:00:00
DF2 <- data.frame(
  idCarte = c('a', 'a', 'b', 'b', 'b'),
  idPack = c('1', '2', '3', '3', '2'),
  timeBetween = c('11:00:02', '13:00:50', '14:10:35', '15:20:00', '18:00:00')
)

DF2
idCarte  idPack    timeBetween
      a       1       11:00:02
      a       2       13:00:50
      b       3       14:10:35
      b       3       15:20:00
      b       2       18:00:00

And I want to get this result

idCarte  idPack      timeIn   timeOut  timeBetween
      a      1     10:00:02  12:00:00     11:00:02 
      a      2     12:00:50  14:00:00     13:00:50
      b      2     11:40:00  11:50:00           NA
      b      3     12:10:35  15:00:00     14:10:35
      b      3     15:15:00  16:00:00     15:20:00

I can do it with a for loop like this but it's really slow

for (i in 1:nrow(DF1)) {
  timeBetweenLocal <- DF2 %>%
    filter(
      idCarte == DF1[i,"idCarte"] &
      idPack == DF1[i,"idPack"] &
      timeBetween >= DF1[i,"timeIn"] &
      timeBetween <= DF1[i,"timeOut"]
    )
  if (nrow(timeBetweenLocal) > 0) {
    DF1[i, "timeBetween"] <- timeBetweenLocal[1, "timeBetween"]
  } else {
    DF1[i, "timeBetween"] <- NA
  }
}

I want to do it in a vectorised way with dplyr::mutate to go faster but it seems a little bit tricky.

DF1 %>%
  mutate (
    timeBetween = ifelse (
      nrow(DF2 %>%
             dplyr::filter(
               idCarte == .$idCarte &
               idPack == .$idPack &
               timeBetween >=.$timeIn &
               timeBetween <= .$timeOut
             )
      ) > 0,
      DF2 %>%
        dplyr::filter(
          idCarte == .$idCarte &
          idPack == .$idPack &
          timeBetween >=.$timeIn &
          timeBetween <= .$timeOut
        ),
      NA
    )
  )

# Error : Result must have length 4, not 0

My problem is that I need test the matching time because there are multiple idCarte, idPack Is anyone have an idea to vectorise this algorithm ? Thanks

3 Answers

Here is a solution with left_join and case_when. left_join can lead to duplicated rows, you can use na.omit or filter(!duplicated(...)) if you want remove some duplications.

library(lubridate)
library(dplyr)

# Yours data
DF1 <- data.frame(stringsAsFactors = F,
  idCarte = c('a', 'a', 'b', 'b', 'b'),
  idPack = c('1', '2', '2', '3', '3'),
  timeIn = c('10:00:02', '12:00:50', '11:40:00', '12:10:35', '15:15:00'),
  timeOut = c('12:00:00', '14:00:00', '11:50:00', '15:00:00', '16:00:00')
)


DF2 <- data.frame(stringsAsFactors = F,
  idCarte = c('a', 'a', 'b', 'b', 'b'),
  idPack = c('1', '2', '3', '3', '2'),
  timeBetween = c('11:00:02', '13:00:50', '14:10:35', '15:20:00', '18:00:00')
)


# Solution with left_join lead to duplicate rows

df = left_join(x = DF1, y = DF2, by = c("idCarte"="idCarte","idPack"="idPack")) %>% 
  mutate(timeBetween = case_when(hms(timeBetween)>= hms(timeIn) & hms(timeBetween)<= hms(timeOut) ~ timeBetween,
                                  T ~ NA_character_
                                  )
         )

# The output
# 
#   idCarte idPack   timeIn  timeOut timeBetween
# 1       a      1 10:00:02 12:00:00    11:00:02
# 2       a      2 12:00:50 14:00:00    13:00:50
# 3       b      2 11:40:00 11:50:00        <NA>
# 4       b      3 12:10:35 15:00:00    14:10:35
# 5       b      3 12:10:35 15:00:00        <NA>
# 6       b      3 15:15:00 16:00:00        <NA>
# 7       b      3 15:15:00 16:00:00    15:20:00

Here is the dplyr solution as mentioned in the comments:

library(dplyr)
library(lubridate)

DF1 %>% 
  left_join(DF2) %>% 
  mutate(timeIn = as_datetime(hms(timeIn)),
         timeOut = as_datetime(hms(timeOut)),
         timeBetween = as_datetime(hms(timeBetween))) %>% 
  filter(timeBetween > timeIn & timeBetween < timeOut | is.na(timeBetween))
#Joining, by = c("idCarte", "idPack")
#  idCarte idPack              timeIn             timeOut         timeBetween
#1       a      1 1970-01-01 10:00:02 1970-01-01 12:00:00 1970-01-01 11:00:02
#2       a      2 1970-01-01 12:00:50 1970-01-01 14:00:00 1970-01-01 13:00:50
#3       b      2 1970-01-01 11:40:00 1970-01-01 11:50:00                <NA>
#4       b      3 1970-01-01 12:10:35 1970-01-01 15:00:00 1970-01-01 14:10:35

To check for matches in the first two columns we may use outer. For multiple matches we want to check whether the time is between timeIn and timeOut. Therefore it's advantageous to convert the times into POSIXct format.

DF1[3:4] <- lapply(DF1[3:4], as.POSIXct, format="%H:%M:%S")
DF2[3] <- as.POSIXct(DF2[[3]], format="%H:%M:%S")

For the outer we code a convenience function.

rp <- function(x) Reduce(paste, x)

Now we create a list w with indices which of the first two columns of both data frames do match using outer.

w <- apply(outer(rp(DF1[1:2]), rp(DF2[1:2]), `==`), 1, which)

Look at the lapply(... in following line; we call each list entry of w, throw either NA if it's empty or chose that entry that falls within the time frame of DF1. Empty elements we again turn to NA. The do.call("c", ...) concatenates the resulting list into a vector, that we can cbind to DF1.

res <- cbind(DF1, timeBetween=do.call("c", lapply(seq(w), function(i) {
  r <- DF2[w[[i]], 3]
  if (length(r) == 0) r <- NA
  else r <- r[r > DF1[i, 3] & r < DF1[i, 4]]
  if (length(r) == 0) r <- NA
  return(r)
  })))

Optionally, we can strip of the dates at the end.

res[3:5] <- lapply(res[3:5], strftime, format="%H:%M:%S")

Result

res    
#   idCarte idPack   timeIn  timeOut timeBetween
# 1       a      1 10:00:02 12:00:00    11:00:02
# 2       a      2 12:00:50 14:00:00    13:00:50
# 3       b      2 11:40:00 11:50:00        <NA>
# 4       b      3 12:10:35 15:00:00    14:10:35
# 5       b      3 15:15:00 16:00:00    15:20:00
Related