R- merge dataframes based on recent dates - r

I have two dataframes:
In DF1, for every ID, the param have been recorded on various dates.
In DF2, for every ID, a number of dates are given.
For every ID, I would like to get all the corresponding param and value from DF1, depending on the dates:
either the value that corresponds to the most recent date1 (in DF1) before date2 (in DF2) for a given param or
If there is no such date1, the most recent value after date2.
DF1 is (I have marked with * the correct rows for the result ):
ID date1 param value
1 id1 1/1/2020 pA pA_1_1
2 id1 2/1/2020 pA pA_1_2 *
3 id1 17/1/2020 pA pA_1_3
4 id1 20/1/2020 pB pB_1_1 *
5 id1 21/1/2020 pB pB_1_2
6 id2 21/12/2022 pA pA_2_1 *
7 id2 22/12/2022 pA pA_2_2
8 id2 18/12/2022 pB pB_2_1 *
9 id2 19/12/2022 pB pB_2_2
DF2 is:
ID date2
1 id1 15/1/2020
2 id2 20/12/2020
The result should be:
ID date2 param value date1
1 id1 15/1/2020 pA pA_1_2 2/1/2020
2 id1 15/1/2020 pB pB_1_1 20/1/2020
3 id2 20/12/2020 pA pA_2_1 21/12/2022
4 id2 20/12/2020 pB pB_2_1 18/12/2022
Code to reproduce the DF1 and DF2:
DF1= data.frame(
stringsAsFactors = FALSE,
ID = c("id1","id1","id1","id1",
"id1","id2","id2","id2","id2"),
date1 = c("1/1/2020","2/1/2020",
"17/1/2020","20/1/2020","21/1/2020","21/12/2022",
"22/12/2022","18/12/2022","19/12/2022"),
param = c("pA", "pA", "pA", "pB", "pB", "pA", "pA", "pB", "pB"),
value = c("pA_1_1","pA_1_2","pA_1_3",
"pB_1_1","pB_1_2","pA_2_1","pA_2_2","pB_2_1","pB_2_2")
)
DF2=data.frame(
stringsAsFactors = FALSE,
ID = c("id1", "id2"),
date2 = c("15/1/2020", "20/12/2020")
)

This is my solution. I'm sure there is a way to write this with less code (using one dataframe instead of two and later merging). But I don't know righ now.
library(tidyverse)
library(lubridate)
# Get before date2
before <- DF1 %>%
left_join(DF2,by = "ID") %>%
mutate(diff = dmy(date1)-dmy(date2)) %>%
mutate(Grp = data.table::rleid(param)) %>%
filter(diff < 0) %>%
group_by(Grp) %>%
filter(diff == max(diff)) %>%
ungroup
# Get after date2
after <- DF1 %>%
left_join(DF2,by = "ID") %>%
mutate(diff = dmy(date1)-dmy(date2)) %>%
mutate(Grp = data.table::rleid(param)) %>%
filter(diff > 0) %>%
group_by(Grp) %>%
filter(! Grp %in% before$Grp, diff == min(diff)) %>%
ungroup
result <- bind_rows(before,after) %>%
select(ID,date2, param, value, date1) %>%
arrange(ID, param)
Explanation: I'm using lubridate library to compare the dates. I do the same process to create two dataframes - first one (before df) for groups which accomplish first condition (closest date in DF1 before date2 in DF2), second one (after df) is for groups which do the other way round (closest date in DF1 after date2 in DF2).
I will explain first:
# Get before date2
before <- DF1 %>%
left_join(DF2,by = "ID") %>%
mutate(diff = dmy(date1)-dmy(date2)) %>%
mutate(Grp = data.table::rleid(param)) %>%
filter(diff < 0) %>%
group_by(Grp) %>%
filter(diff == max(diff)) %>%
ungroup
Here, we merge DF1 and DF2 by ID, so rows with same ID have the same date2. Then, we calculate the differences date1-date2 - first we convert characters to date using dmy(). Therefore, dates before date2 will be a negative difference. With data.table::rleid(param) we enumerate subgroups with different ID & param, so we can know the subgroups. Then we can group by then and filter by them.
At the end:
result <- bind_rows(before,after) %>%
select(ID,date2, param, value, date1) %>%
arrange(ID, param)
We bind the two dataframe by rows and select the columns you are looking for, to delete the columns we created to operate with (group and filter).
PS: I added arrange() to make sure the final df is sorted by ID and param values.

Related

Replace values in dataframe based on other dataframe with column name and value

Let's say I have a dataframe of scores
library(dplyr)
id <- c(1 , 2)
name <- c('John', 'Ninaa')
score1 <- c(8, 6)
score2 <- c(NA, 7)
df <- data.frame(id, name, score1, score2)
Some mistakes have been made so I want to correct them. My corrections are in a different dataframe.
id <- c(2,1)
column <- c('name', 'score2')
new_value <- c('Nina', 9)
corrections <- data.frame(id, column, new_value)
I want to search the dataframe for the correct id and column and change the value.
I have tried something with match but I don't know how mutate the correct column.
df %>% mutate(corrections$column = replace(corrections$column, match(corrections$id, id), corrections$new_value))
We could join by 'id', then mutate across the columns specified in the column and replace the elements based on the matching the corresponding column name (cur_column()) with the column
library(dplyr)
df %>%
left_join(corrections) %>%
mutate(across(all_of(column), ~ replace(.x, match(cur_column(),
column), new_value[match(cur_column(), column)]))) %>%
select(names(df))
-output
id name score1 score2
1 1 John 8 9
2 2 Nina 6 7
It's an implementation of a feasible idea with dplyr::rows_update, though it involves functions of multiple packages. In practice I prefer a moderately parsimonious approach.
library(tidyverse)
corrections %>%
group_by(id) %>%
group_map(
~ pivot_wider(.x, names_from = column, values_from = new_value) %>% type_convert,
.keep = TRUE) %>%
reduce(rows_update, by = 'id', .init = df)
# id name score1 score2
# 1 1 John 8 9
# 2 2 Nina 6 7

Group by ID and Outcome and take the earliest earliest Dates and assign numbers (i.e outcome1, outcome2) AFTER specific outcome has occurred

Apologies I am not sure this warrants another question. But am basing this off of another questions (Group by ID and Outcome and take the earliest earliest Dates of specific outcomes and assign numbers (i.e outcome1, outcome2)).
mydata = data.frame (Id =c (1,1,1,1,1,1,1,1,2,2,2,2),
Date = c("2001-01-31", "2001-02-13","2001-05-31",
"2001-06-02","2018-01-31","2018-03-31","2018-07-31",
"2019-04-04","2014-01-31","2014-02-02","2014-04-31",
"2014-05-18"),Outcome = c("Relapse","CR","Relapse","Relapse",
"CR","CR","CR","Relapse","CR", "CR","Relapse","CR"))
Which outputs the below.
Id Date Outcome
1 2001-01-31 Relapse
1 2001-02-13 CR
1 2001-05-31 Relapse
1 2001-06-02 Relapse
1 2018-01-31 CR
1 2018-03-31 CR
1 2018-07-31 CR
1 2019-04-04 Relapse
2 2014-01-31 CR
2 2014-02-02 CR
2 2014-04-31 Relapse
2 2014-05-18 CR
As you can see each patient is in certain phases at different times and I would like to capture the earliest dates of when each new phase starts per patient. I would like then to rename these phases to CR1, Relapse1, CR2, Relapse2 and so forth. But I would only like to start naming AFTER patient achieves their first CR1. So if a patient has a relapse before CR, those would be ignored.
Im hoping to get an output like this:
Id CR1 Relapse1 CR2 Relapse2
1 2001-02-13 2001-05-31 2018-01-31 2019-04-04
2 2014-01-31 2014-04-31 2014-05-18 NA
As you see patient 1 had a relapse at 2001-01-31, but was ignored because it occured before the first CR1.
The following code was so helpful to the previous question, but was wondering if I can modify it so that it will start the counting after the first CR? Thank you!!
mydata %>%
group_by(Id) %>%
mutate(Grp = rleid(Outcome)) %>%
group_by(Grp, .add = T) %>%
slice(1) %>%
group_by(Id, Outcome) %>%
mutate(n = row_number()) %>%
pivot_wider(id_cols = Id, names_from = c(Outcome, n), values_from = Date)
Using data.table package
library(data.table)
mydata <- setDT(mydata)
# identify consecutive same Outcome for each Id
mydata[, flag:=rleid(Id, Outcome)]
# collapse identified consecutive values (keep min date)
tmp <- mydata[, by="flag,Id,Outcome", .(Date=min(Date))]
# mark relapse if first
tmp <- tmp[, by=Id,
flag := (Outcome[1]=="Relapse") & (seq_len(.N)==1)
]
# remove marked relapse
tmp <- tmp[(!flag)]
# Outcome numbering
tmp[, by="Id,Outcome", n:=seq_len(.N)]
# cast (widen)
dcast(tmp, Id~paste0(Outcome, n), value.var = "Date")
Got
Id CR1 CR2 Relapse1 Relapse2
1: 1 2001-02-13 2018-01-31 2001-05-31 2019-04-04
2: 2 2014-01-31 2014-05-18 2014-04-31 <NA>
using %>% notation
mydata[, flag:=rleid(Id,Outcome)] %>%
.[, by="flag,Id,Outcome", .( Date=min(Date) )] %>%
.[, by="Id", flag := (Outcome[1]=="Relapse") & (seq_len(.N)==1)] %>%
.[(!flag)] %>%
.[, by="Id,Outcome", n := seq_len(.N)] %>%
dcast(., Id ~ paste0(Outcome, n), value.var = "Date")
That is my approach :
library(data.table)
my_df <- data.frame (Id =c (1,1,1,1,1,1,1,1,2,2,2,2),
Date = c("2001-01-31", "2001-02-13","2001-05-31",
"2001-06-02","2018-01-31","2018-03-31","2018-07-31",
"2019-04-04","2014-01-31","2014-02-02","2014-04-31",
"2014-05-18"),
Outcome = c("Relapse","Relapse","Relapse","Relapse",
"CR","CR","CR","Relapse","CR", "CR","Relapse","CR"),
stringsAsFactors = FALSE)
my_df <- my_df %>% group_by(Id) %>% arrange(Id, Date)
my_df <- my_df %>%
group_by(Id) %>%
mutate(Value = seq_along(Outcome)) %>%
mutate(first_v = Outcome == "CR" & !duplicated(Outcome == "CR")) %>%
mutate(first_a = first_v == "FALSE" & Value > Value[Outcome == "CR" & !duplicated(Outcome == "CR")] | Outcome == "CR") %>%
filter(first_a == "TRUE")
my_df %>%
group_by(Id) %>%
mutate(Grp = rleid(Outcome)) %>%
group_by(Grp, .add = T) %>%
group_by(Id, Outcome) %>%
mutate(n = row_number()) %>%
pivot_wider(id_cols = Id, names_from = c(Outcome, n), values_from = Date)
You could try adding:
filter(Date >= first(Date[Outcome == "CR"]))
to filter out rows before the first "CR". This assumes your Date is sorted/arranged first.
library(tidyverse)
library(data.table)
mydata %>%
group_by(Id) %>%
filter(Date >= first(Date[Outcome == "CR"])) %>%
mutate(Grp = rleid(Outcome)) %>%
group_by(Grp, .add = T) %>%
slice(1) %>%
group_by(Id, Outcome) %>%
mutate(n = row_number()) %>%
pivot_wider(id_cols = Id, names_from = c(Outcome, n), values_from = Date)
Output
Id CR_1 Relapse_1 CR_2 Relapse_2
<dbl> <chr> <chr> <chr> <chr>
1 1 2001-02-13 2001-05-31 2018-01-31 2019-04-04
2 2 2014-01-31 2014-04-31 2014-05-18 NA

For each ID return the earliest date from the start column and the latest date from the end column in r

I have a dataset which has multiple start dates and end dates for each Id. I would like to take the earliest date from the "startDate" column and the latest date from the endDate column.
data = data.frame(ID=c(1,1,1,1,2,2,2),
startDate= c("2018-01-31", "2018-01-31", "2018-01-31", "2019-06-06",
"2002-06-07", "2002-06-07", "2002-09-12"),
endDate = c(NA,NA,NA,"2019-07-09",NA,NA, "2002-10-02"))
This is the output I was hoping to get:
data = data.frame(ID=c(1,2),
startDate= c("2018-01-31","2002-06-07"),
endDate = c("2019-07-09","2002-10-02"))
After trying I have figured out how to do this through the following code, but would prefer something more efficient if at all possible. I am continuously needing to do this and i would rather not have to create two separate dataframes. Thank you guys for your help!
data_start <- data %>%
group_by(ID) %>%
arrange(startDate) %>%
slice(1L)
data_end <- data %>%
group_by(ID) %>%
arrange(desc(endDate)) %>%
slice(1L)
data <- left_join(data_start[,c(1,2)], data_end[,c(1,3)], by="ID")
Or with first and last:
library(dplyr)
data %>%
group_by(ID) %>%
summarise(
startDate = first(startDate),
endDate = last(endDate)
)
# A tibble: 2 x 3
ID startDate endDate
* <dbl> <chr> <chr>
1 1 2018-01-31 2019-07-09
2 2 2002-06-07 2002-10-02
You can use min and max, working the variables as dates
data %>% group_by(ID) %>%
summarise(startDate = min(as.Date(startDate),na.rm = T),
endDate = max(as.Date(endDate),na.rm = T))

Finding the first row after which x rows meet some criterium in R

A data wrangling question:
I have a dataframe of hourly animal tracking points with columns for id, time, and whether the animal is on land or in water (0 = water; 1 = land). It looks something like this:
set.seed(13)
n <- 100
dat <- data.frame(id = rep(1:5, each = 10),
datetime=seq(as.POSIXct("2020-12-26 00:00:00"), as.POSIXct("2020-12-30 3:00:00"), by = "hour"),
land = sample(0:1, n, replace = TRUE))
What I need to do is flag the first row after which the animal uses land at least once for 3 straight days. I tried doing something like this:
dat$ymd <- ymd(dat$datetime[1]) # make column for year-month-day
# add land points within each id group
land.pts <- dat %>%
group_by(id, ymd) %>%
arrange(id, datetime) %>%
drop_na(land) %>%
mutate(all.land = cumsum(land))
#flag days that have any land points
flag <- land.pts %>%
group_by(id, ymd) %>%
arrange(id, datetime) %>%
slice(n()) %>%
mutate(flag = if_else(all.land == 0,0,1))
# Combine flagged dataframe with full dataframe
comb <- left_join(land.pts, flag)
comb[is.na(comb)] <- 1
and then I tried this:
x = comb %>%
group_by(id) %>%
arrange(id, datetime) %>%
mutate(time.land=ifelse(land==0 | is.na(lag(land)) | lag(land)==0 | flag==0,
0,
difftime(datetime, lag(datetime), units="days")))
But I still can't quite wrap my head around what to do to make it so that I can figure out when the animal has been on land at least once for three days straight, and then flag that first point on land. Thanks so much for any help you can provide!
Create a date column from the timestamp. Summarise the data and keep only 1 row for each id and date which shows whether the animal was on land even once in the entire day.
Use zoo's rollapply function to mark the first day as TRUE if the next 3 days the animal was on land.
library(dplyr)
library(zoo)
dat <- dat %>% mutate(date = as.Date(datetime))
dat %>%
group_by(id, date) %>%
summarise(on_land = any(land == 1)) %>%
mutate(consec_three = rollapply(on_land, 3,all, align = 'left', fill = NA)) %>%
ungroup %>%
#If you want all the rows of the data
left_join(dat, by = c('id', 'date'))

Get the No_intersection/Complementary part of several date's intervals

I want to get the missing part of several date's intervals in 2017.
here for example, each "id" of following dataframe:
df <- data.frame(id=c(rep("a",3),rep("b",2)),
start=c("2017-01-01","2017-01-10","2017-02-10","2017-03-01","2017-04-20"),
end=c("2017-01-15","2017-01-20","2017-02-20","2017-03-28","2017-04-29"))
id start end
a 2017-01-01 2017-01-15
a 2017-01-10 2017-01-20
a 2017-02-10 2017-02-20
b 2017-03-01 2017-03-28
b 2017-04-20 2017-04-29
I want to get:
df_final <- data.frame(id=c(rep("a",2),rep("b",3)),
start=c("2017-01-21","2017-02-21","2017-01-01","2017-03-29","2017-04-30"),
end=c("2017-02-09","2017-12-31","2017-02-28","2017-04-19","2017-12-31"))
id start end
a 2017-01-21 2017-02-09
a 2017-02-21 2017-12-31
b 2017-01-01 2017-02-28
b 2017-03-29 2017-04-19
b 2017-04-30 2017-12-31
Thank you!
First, confirm whether start and end are Date class.
df$start <- as.Date(df$start)
df$end <- as.Date(df$end)
Use by() to split the data into a list of two data frames according to the ids.
library(purrr)
by(df, df$id, function(x){
year <- seq(as.Date("2017-01-01"), as.Date("2017-12-31"), 1)
ind <- map2(x$start, x$end, function(start, end){
which(year < start | year > end)
}) %>% reduce(intersect)
gap <- which(diff(ind) > 1)
head <- ind[c(1, gap + 1)] ; tail <- ind[c(gap, length(ind))]
return(data.frame(id = unique(x$id), start = year[head], end = year[tail]))
}) %>% reduce(rbind)
Description:
year : All days in 2017.
ind : Get rid of the dates between start and end along the rows and the outcome represents the indices of missing dates.
gap : The discontinuous indices.
Output:
# id start end
# 1 a 2017-01-21 2017-02-09
# 2 a 2017-02-21 2017-12-31
# 3 b 2017-01-01 2017-02-28
# 4 b 2017-03-29 2017-04-19
# 5 b 2017-04-30 2017-12-31
I think my solution is still cumbersome. Hope to help you.
I encountered a similar problem recently, and I found that expanding the table to get one row for each relevant date, and then collapsing back down to ranges, was easier than trying to work out the correct logic from the range endpoints alone.
Here's how that approach would work. Alternatively, it might be possible to do something like this or this, but those approaches don't have the "not in range" issue you're dealing with.
library(dplyr)
library(fuzzyjoin)
library(lubridate)
df <- data.frame(id=c(rep("a",3),rep("b",2)),
start=c("2017-01-01","2017-01-10","2017-02-10","2017-03-01","2017-04-20"),
end=c("2017-01-15","2017-01-20","2017-02-20","2017-03-28","2017-04-29"))
# All the dates in 2017.
all.2017.dates = data.frame(date = seq.Date(as.Date("2017-01-01"), as.Date("2017-12-31"), by = "day"))
# Start by expanding the original dataframe so that we get one record for each
# id for each date in any of that id's ranges.
df.expanded = df %>%
# Convert the strings to real dates.
mutate(start.date = as.Date(start),
end.date = as.Date(end)) %>%
# Left join to 2017 dates on dates that are in the range of this record.
fuzzy_left_join(all.2017.dates,
by = c("start.date" = "date", "end.date" = "date"),
match_fun = list(`<=`, `>=`)) %>%
# Filter to distinct ids/dates.
select(id, date) %>%
distinct()
# Now, do an anti-join that gets dates NOT in an id's ranges, and collapse back
# down to ranges.
df.final = expand.grid(id = unique(df$id),
date = all.2017.dates$date) %>%
# Anti-join on id and date.
anti_join(df.expanded,
by = c("id", "date")) %>%
# Sort by id, then date, so that the lead/lag functions behave as expected.
arrange(id, date) %>%
# Check whether this record is an endpoint (i.e., is it adjacent to the
# previous/next record?).
mutate(prev.day.included = coalesce(date == lag(date) + 1 &
id == lag(id), F),
next.day.included = coalesce(date == lead(date) - 1 &
id == lag(id), F)) %>%
# Filter to just endpoint records.
filter(!prev.day.included | !next.day.included) %>%
# Fill in both start and end dates on "start" records. The start date is the
# date in the record; the end date is the date of the next record.
mutate(start.date = as.Date(ifelse(!prev.day.included, date, NA),
origin = lubridate::origin),
end.date = as.Date(ifelse(!prev.day.included, lead(date), NA),
origin = lubridate::origin)) %>%
filter(!is.na(start.date))
Here's my solution:
library(tidyverse)
library(lubridate)
library(wrapr)
df %>%
mutate_at(2:3, ymd) %>%
group_by(id) %>%
gather('start_end', 'date', start:end) %>%
mutate(date = if_else(start_end == 'start', min(date), max(date))) %>%
unique() %>%
mutate(
start = if_else(
start_end == 'start',
date %>% min() %>% year() %>% paste0('-01-01') %>% ymd(),
date
),
end = if_else(
start_end == 'end',
date %>% max() %>% year() %>% paste0('-12-31') %>% ymd(),
date
)) %>%
filter(start != end) %>%
select(id, start, end) %>%
mutate(supp = TRUE) %>%
bind_rows(mutate(df, supp = FALSE) %>% mutate_at(2:3, ymd)) %>%
arrange(id, start) %>%
mutate(rn = row_number()) %.>%
left_join(., mutate(., rn = rn - 1), by = c('id', 'rn')) %>%
na.omit() %>%
mutate(
start = case_when(
(start.y >= end.x) & !supp.x ~ end.x + 1,
(start.y >= end.x) & supp.x ~ start.x,
TRUE ~ as.Date(NA)
),
end = case_when(
(start.y >= end.x) & supp.y ~ end.y,
(start.y >= end.x) & !supp.y ~ start.y - 1,
TRUE ~ as.Date(NA)
)
) %>%
select(id, start, end) %>%
na.omit()

Resources