r: перестановка фрейма данных на основе времени

#r #dataframe #datetime #transform

#r #фрейм данных #дата-время #преобразование

Вопрос:

У меня большая база данных об употреблении наркотиков:

 library(data.table)
df <- data.frame("ID" = c(1,1,1,1,2,2,2,3,3), "IndexDate" = c("2019-01-01", "2019-01-01", "2019-01-01", "2019-01-01", "2019-05-01", "2019-05-01", "2019-05-01", "2019-07-01", "2019-07-01"), "CensorDate" = c("2019-06-30", "2019-06-30", "2019-06-30", "2019-06-30", "2019-07-30", "2019-07-30", "2019-07-30", "2019-12-31", "2019-12-31"), "DrugStart" = c("2019-02-01", "2019-03-01", "2019-04-01", "2019-06-01", "2019-03-01", "2019-04-15", "2019-05-16", "2019-07-05", "2020-01-01"), "DrugEnd" = c("2019-02-15", "2019-04-15", "2019-04-30", "2019-06-05", "2019-03-15", "2019-05-15", "2019-05-30", "2019-07-15", "2020-01-15"),"Notes" = c("", "", "Overlap 15 days", "", "All days before IndexDate", "15 days before IndexDate", "", "", "15 days after CensorDate"))
df
  ID  IndexDate CensorDate  DrugStart    DrugEnd                     Notes
1  1 2019-01-01 2019-06-30 2019-02-01 2019-02-15                          
2  1 2019-01-01 2019-06-30 2019-03-01 2019-04-15                          
3  1 2019-01-01 2019-06-30 2019-04-01 2019-04-30           Overlap 15 days
4  1 2019-01-01 2019-06-30 2019-06-01 2019-06-05                          
5  2 2019-05-01 2019-07-30 2019-03-01 2019-03-15 All days before IndexDate
6  2 2019-05-01 2019-07-30 2019-04-15 2019-05-15  15 days before IndexDate
7  2 2019-05-01 2019-07-30 2019-05-16 2019-05-30                          
8  3 2019-07-01 2019-12-31 2019-07-05 2019-07-15                          
9  3 2019-07-01 2019-12-31 2020-01-01 2020-01-15  15 days after CensorDate
 

IndexDate И CensorDate все одинаковы для каждого ID . Период наблюдения составляет от IndexDate до CensorDate .
Я хотел бы изменить его по следующим критериям:

  1. Связан ID
  2. Пренебречь днями до IndexDate или после CensorDate ;
  3. Перекрывающиеся периоды времени учитываются только один раз;
  4. df это база данных об употреблении наркотиков. Все периоды в df (от DrugStart до DrugEnd ) означают использование препарата. Пропущенный период в df , но в течение периода наблюдения (от IndexDate до CensorDate ) означает, что препарат не используется.
  5. Употребление наркотиков помечено как 2 (употребление) и 1 (не употребление);
  6. IndexDate определяется как день 0 (означает, что все время начала « TimeStart » равно 0).

Я ожидаю следующих результатов:

 > df2 <- data.frame("ID" = c(1,1,1,1,1,1,1,2,2,3,3,3), "TimeStart" = c("0", "31", "46", "59", "120", "151", "156", "0", "30", "0", "4", "15"), "TimeEnd" = c("30", "45", "58", "119", "150", "155", "180", "29", "90", "3", "14", "183"), "DrugUse" = c("1", "2", "1", "2", "1", "2", "1", "2", "1", "1", "2", "1"))
> df2
   ID TimeStart TimeEnd DrugUse
1   1         0      30       1
2   1        31      45       2
3   1        46      58       1
4   1        59     119       2
5   1       120     150       1
6   1       151     155       2
7   1       156     180       1
8   2         0      29       2
9   2        30      90       1
10  3         0       3       1
11  3         4      14       2
12  3        15     183       1
 

Теперь я знаю, как генерировать TimeStart и TimeEnd с помощью « DrugStart IndexDate » и « DrugEnd IndexDate » следующим образом:

 df$TimeStart<- as.Date(df$DrugStart, format="%Y-%m-%d")-as.Date(df$IndexDate, format="%Y-%m-%d")
df$TimeEnd<- as.Date(df$DrugEnd, format="%Y-%m-%d")-as.Date(df$IndexDate, format="%Y-%m-%d")
df
  ID  IndexDate CensorDate  DrugStart    DrugEnd            Notes_Drug.use.days TimeStart  TimeEnd
1  1 2019-01-01 2019-06-30 2019-02-01 2019-02-15                         15days   31 days  45 days
2  1 2019-01-01 2019-06-30 2019-03-01 2019-04-15                         46days   59 days 104 days
3  1 2019-01-01 2019-06-30 2019-04-01 2019-04-30        Overlap 15days   15days   90 days 119 days
4  1 2019-01-01 2019-06-30 2019-06-01 2019-06-05                          5days  151 days 155 days
5  2 2019-05-01 2019-07-30 2019-03-01 2019-03-15        15days before IndexDate  -61 days -47 days
6  2 2019-05-01 2019-07-30 2019-04-15 2019-05-15 15days before IndexDate 15days  -16 days  14 days
7  2 2019-05-01 2019-07-30 2019-05-16 2019-05-30                         15days   15 days  29 days
8  3 2019-07-01 2019-12-31 2019-07-05 2019-07-15                         11days    4 days  14 days
9  3 2019-07-01 2019-12-31 2020-01-01 2020-01-15        15days after CensorDate  184 days 198 days
 

Но я не знаю, как обращаться с перекрывающимися периодами и этими непрерывными периодами, как показано ниже:

 # Overlapped periods: 
# Transform
  ID TimeStart  TimeEnd
2  1  59 days 104 days
3  1  90 days 119 days
# to
  ID TimeStart  TimeEnd
2  1  59 days 119 days


# And Continous periods:
# Transform
  ID TimeStart  TimeEnd
6  2 -16 days  14 days
7  2  15 days  29 days
# To
  ID TimeStart  TimeEnd
6  2  0 days   29 days
 

Also, how to add those periods that we do not use the drug (those DrugUse=1 )? such as these lines:

    ID TimeStart TimeEnd DrugUse
1   1         0      30       1
3   1        46      58       1
5   1       120     150       1
7   1       156     180       1
9   2        30      90       1
10  3         0       3       1
12  3        15     183       1
 

Кто-нибудь мне поможет? Большое вам спасибо!

#####################################################

Обновлено: спасибо за ответ Bas!! Я внес незначительные изменения в ответ Bas. Следующий код может быть окончательной версией!!

 library(data.table)
df <- data.frame("ID" = c(1,1,1,1,2,2,2,3,3), "IndexDate" = c("2019-01-01", "2019-01-01", "2019-01-01", "2019-01-01", "2019-05-01", "2019-05-01", "2019-05-01", "2019-07-01", "2019-07-01"), "CensorDate" = c("2019-06-30", "2019-06-30", "2019-06-30", "2019-06-30", "2019-07-30", "2019-07-30", "2019-07-30", "2019-12-31", "2019-12-31"), "DrugStart" = c("2019-02-01", "2019-03-01", "2019-04-01", "2019-06-01", "2019-03-01", "2019-04-15", "2019-05-16", "2019-07-05", "2020-01-01"), "DrugEnd" = c("2019-02-15", "2019-04-15", "2019-04-30", "2019-06-05", "2019-03-15", "2019-05-15", "2019-05-30", "2019-07-15", "2020-01-15"),"Notes" = c("", "", "Overlap 15 days", "", "All days before IndexDate", "15 days before IndexDate", "", "", "15 days after CensorDate"))
df$DrugEnd <- as.Date(df$DrugEnd, format="%Y-%m-%d")   1
df$CensorDate <- as.Date(df$CensorDate, format="%Y-%m-%d")   1

library(dplyr)
library(tidyr)
library(lubridate)
df2 <- df %>% 
mutate(across(IndexDate:DrugEnd, as.Date)) %>%
filter(DrugStart <= CensorDate, # Neglect days before IndexDate or after CensorDate
DrugEnd >= IndexDate) %>%
group_by(ID) %>% 
mutate(interval = list(int_diff(sort(unique(c(IndexDate, CensorDate, DrugStart, DrugEnd)))))) %>%
unnest(interval) %>% 
mutate(DrugUse = DrugStart < int_end(interval) amp; DrugEnd > int_start(interval)) %>%
group_by(ID, interval) %>% 
summarise(IndexDate = first(IndexDate),
CensorDate = first(CensorDate),
DrugUse = if_else(sum(DrugUse) > 0, 2, 1)) %>% 
ungroup() %>% 
filter(int_end(interval) <= CensorDate,
int_start(interval) >= IndexDate) %>% 
mutate(TimeStart = as.numeric(difftime(int_start(interval), IndexDate, units = "days")),
TimeEnd = as.numeric(difftime(int_end(interval), IndexDate, units = "days"))-1) %>% 
group_by(ID, data.table::rleid(DrugUse)) %>% 
summarise(TimeStart = min(TimeStart),
TimeEnd = max(TimeEnd),
DrugUse = first(DrugUse)) %>% 
select(ID, TimeStart, TimeEnd, DrugUse)

> df2
# A tibble: 12 x 4
# Groups:   ID [3]
      ID TimeStart TimeEnd DrugUse
   <dbl>     <dbl>   <dbl>   <dbl>
 1     1         0      30       1
 2     1        31      45       2
 3     1        46      58       1
 4     1        59     119       2
 5     1       120     150       1
 6     1       151     155       2
 7     1       156     180       1
 8     2         0      29       2
 9     2        30      90       1
10     3         0       3       1
11     3         4      14       2
12     3        15     183       1
 

#####################################################

2-е обновление: если ваш набор данных слишком велик (например, более миллиона записей), использование приведенных выше кодов может быть очень медленным. Следующий файл unnest() очень большой, и этот шаг выполняется очень медленно.

В этом случае мы можем разделить файл, используя split() (лучше не более 10 тысяч записей в каждом файле). Выполняется с помощью синтаксиса цикла ( for(i in sequence){statement} ). Затем объедините файлы с помощью rbind() .

Удачи!

Комментарии:

1. df это база данных об употреблении наркотиков. Все периоды в df (от DrugStart до DrugEnd ) означают DrugUse=2 . Пропущенный период в df , но в пределах периода наблюдения (от IndexDate до CensorDate ) означает DrugUse=1

2. Я думаю lubridate::int_diff() , может пригодиться

3. @Bas Я пока не знаю, как его использовать. Не могли бы вы привести мне пример?

Ответ №1:

Используя dplyr , tidyr и lubridate , это приближает вас к цели, но не совсем туда:

 df %>% 
  mutate(across(IndexDate:DrugEnd, as.Date)) %>%
  filter(DrugStart <= CensorDate, # Neglect days before IndexDate or after CensorDate
         DrugEnd >= IndexDate) %>%
  group_by(ID) %>% 
  mutate(interval = list(int_diff(sort(unique(c(IndexDate, CensorDate, DrugStart, DrugEnd)))))) %>%
  unnest(interval) %>% 
  mutate(DrugUse = DrugStart < int_end(interval) amp; DrugEnd > int_start(interval)) %>%
  group_by(ID, interval) %>% 
  summarise(IndexDate = first(IndexDate),
            CensorDate = first(CensorDate),
            DrugUse = if_else(sum(DrugUse) > 0, 2, 1)) %>% 
  ungroup() %>% 
  filter(int_end(interval) <= CensorDate,
         int_start(interval) >= IndexDate) %>% 
  mutate(TimeStart = as.numeric(difftime(int_start(interval), IndexDate, units = "days")),
         TimeEnd = as.numeric(difftime(int_end(interval), IndexDate, units = "days"))) %>% 
  group_by(ID, data.table::rleid(DrugUse)) %>% 
  summarise(TimeStart = min(TimeStart),
            TimeEnd = max(TimeEnd),
            DrugUse = first(DrugUse)) %>% 
  select(ID, TimeStart, TimeEnd, DrugUse)
 

что дает

       ID TimeStart TimeEnd DrugUse
   <dbl>     <dbl>   <dbl>   <dbl>
 1     1         0      31       1
 2     1        31      45       2
 3     1        45      59       1
 4     1        59     119       2
 5     1       119     151       1
 6     1       151     155       2
 7     1       155     180       1
 8     2         0      14       2
 9     2        14      15       1
10     2        15      29       2
11     2        29      90       1
12     3         0       4       1
13     3         4      14       2
14     3        14     183       1
 

Комментарии:

1. В 3-й записи, 2019-02-15 UTC--2019-06-30 UTC , DrugUse=1 . Это кажется неправильным. Для субъекта имеются записи об употреблении наркотиков от 2019-04-01 до 2019-04-30 и от 2019-06-01 до 2019-06-05 ID=1 .

2. Вы правы, я отредактировал свой ответ. Это все еще не совсем то, что вы хотите, но мы ближе. Иногда возникает разовая ошибка в TimeStart или timeEnd.