R: «аккуратная» версия функции намного медленнее, чем оригинал, и мне интересно, почему - PullRequest
3 голосов
/ 06 июля 2019

У меня есть данные от субъектов с уникальными идентификаторами, которые появляются при нескольких посещениях, каждый из которых находится в отдельной строке фрейма данных. Некоторая информация, такая как пол или год рождения, может быть собрана только за одно посещение, но она имеет отношение к любому из посещений. Для посещений, где информация не была собрана, это поле будет NA. Итак, я создал функцию, которая будет копировать информацию субъекта для данного поля во все посещения, заменив NA. Это сработало, но код неуклюжий, и теперь, когда я изучаю аккуратную обработку данных, я хотел включить это, чтобы сделать код чище. Я также ожидал, что это ускорит процесс, но этого не произошло.

Во-первых, вот некоторые данные о игрушках:

data <- tibble(record_id = c(rep(LETTERS[1:4], 3)), 
               year1 = c(NA, NA, 2000, 2001, 2002, rep(NA, 7)),
               year2 = c(rep(NA, 5), 2003, 2004, 2005, 2006, rep(NA, 3)))

Следующее даст желаемый результат:

data %>% 
  group_by(id) %>% 
  arrange(year1, .by_group = T) %>% 
  fill(year1) %>%
  arrange(year2) %>%
  fill(year2)

Перед тем, как привести в порядок, я создал этот код, и он отлично работал.

mash.old <- function(data, variable){
  x <- data[!is.na(data[,variable]),] %>%
    distinct(record_id, .keep_all = T)
  x <- as.data.frame(x)
  for(i in 1:nrow(data)){
    if(is.na(data[i,variable]) &
       data[i, "record_id"] %in% x$record_id){
      id <- data[i, "record_id"]
      data[i,variable] <- x[x$record_id == as.character(id),
                            variable]
    }else{
      next
    }
  }
  rm(x, id, i)
  return(data)
}

Я мог бы бежать

data <- mash.old(data, 'year1')
data <- mash.old(data, 'year2')

и получите желаемый результат.

Я хотел улучшить его, позволив ему принимать вектор переменных для выполнения функции, иметь возможность выбрать группирующую переменную (имя переменной субъектного идентификатора) и использовать dplyr / tidyr. Итак, я создал это:

mash.new <- function(data, variables, grouping.var = record_id){
  for(i in variables){
    data <- data %>%
      group_by(!!enquo(grouping.var)) %>%
      arrange((!!sym(i)), .by_group = T) %>%
      fill(!!sym(i)) %>%
      ungroup()
  }
  return(data)
}

Теперь mash.new(data, c('year1, 'year2')) вернет ожидаемые результаты. Нет проблем для этого небольшого фрейма данных.

Мой фактический фрейм данных содержит 15762 строки, и я хотел запустить функцию для двенадцати переменных. mash.old() На это ушло около четырех минут. mash.new() сказал, что это займет около трех часов, поэтому я остановил это примерно через пять минут или около того.

Мой вопрос, почему огромная разница? Я смотрел на свою первую функцию как на любительскую и неуклюжую, я думал, что улучшаюсь Это просто, что процесс группировки, упорядочения, заполнения и группировки требует гораздо больше вычислительной мощности? Есть ли лучший способ написать это? Я самоучка и просто пытаюсь улучшить свои навыки.

Редактировать

Спасибо за помощь. Вот функция, которую я в конечном итоге использовал. Хотя версия Коул data.table быстрее, я решил придерживаться метода dplyr, просто потому, что это то, что я знаю.

mash <- function(data, variables, grouping.var = record_id){
   data <- data %>%
      arrange(!!enquo(grouping.var)) %>%
      group_by(!!enquo(grouping.var)) %>%
      mutate_at(vars(!!!variables), 
                function(x) zoo::na.locf(x[order(x)], na.rm = F)) %>%
      ungroup()
   return(data)
}
#Note that if there are two different entries for a given subject in a 
#variable, this will fill with the data that comes last in the sort order

1 Ответ

2 голосов
/ 06 июля 2019

Самым большим улучшением будет group_by() один раз.Прямо сейчас вы делаете 12 группировок и разгруппировок, что добавляет много ненужных накладных расходов.Плюс, новая функция переназначает все обратно на себя - если мы находимся на year1, нет причин возиться с памятью year2 или report_id

library(dplyr)
library(zoo)

data%>%
  arrange(record_id)%>%
  group_by(record_id)%>%
  mutate_at(vars(-group_cols()), function(x) zoo::na.locf(x[order(x)], na.rm = F))%>%
  ungroup()

# A tibble: 12 x 3
   record_id year1 year2
   <chr>     <dbl> <dbl>
 1 A          2002  2006
 2 A          2002  2006
 3 A          2002  2006
 4 B            NA  2003
 5 B            NA  2003
 6 B            NA  2003
 7 C          2000  2004
 8 C          2000  2004
 9 C          2000  2004
10 D          2001  2005
11 D          2001  2005
12 D          2001  2005

Кроме того, мой самый любимыйбудет data.table.Это коротко и мило:

library(data.table)
library(zoo)

dt <- as.data.table(data)

vars_n <- names(dt)[-1] #included if you want to make a function later
dt[,lapply(.SD, function(x) zoo::na.locf(x[order(x)], na.rm = F)), keyby = record_id, .SDcols = vars_n]

Это также самый быстрый

Unit: milliseconds
           expr     min       lq      mean   median       uq      max neval
     cole_dplyr  3.2388  3.39800  3.588391  3.47175  3.62610   6.6420   100
       cole_dt2  1.6135  1.83535  2.082963  1.96230  2.07435   6.7179   100
    mashing_old  4.6119  4.86305  5.175244  4.94930  5.10220   9.1026   100
    mashing_new 16.1860 16.82445 18.610696 17.30585 18.01270 101.6192   100
 OP_non_mashing 15.1633 15.57970 16.914889 16.10400 16.97860  46.5837   100

И весь мой код - тесты внизу:

library(tidyverse)

data <- tibble(record_id = c(rep(LETTERS[1:4], 3)), 
               year1 = c(NA, NA, 2000, 2001, 2002, rep(NA, 7)),
               year2 = c(rep(NA, 5), 2003, 2004, 2005, 2006, rep(NA, 3)))

data <- tibble(record_id = c(rep(LETTERS[1:4], 3)), 
               year1 = c(NA, NA, 2000, 2001, 2002, rep(NA, 7)),
               year2 = c(rep(NA, 5), 2003, 2004, 2005, 2006, 2002, rep(NA, 2)))

data

library(data.table)
dt <- as.data.table(data)

vars_n <- names(dt)[-1] #included if you want to make a function later
dt[,lapply(.SD, function(x) zoo::na.locf(x[order(x)], na.rm = F)), keyby = record_id, .SDcols = vars_n]


data%>%
  arrange(record_id)%>%
  group_by(record_id)%>%
  mutate_at(vars(-group_cols()), function(x) zoo::na.locf(x[order(x)], na.rm = F))%>%
  ungroup()

mash.old <- function(data, variable){
  x <- data[!is.na(data[,variable]),] %>%
    distinct(record_id, .keep_all = T)
  x <- as.data.frame(x)
  for(i in 1:nrow(data)){
    if(is.na(data[i,variable]) &
       data[i, "record_id"] %in% x$record_id){
      id <- data[i, "record_id"]
      data[i,variable] <- x[x$record_id == as.character(id),
                            variable]
    }else{
      next
    }
  }
  rm(x, id, i)
  return(data)
}

mash.new <- function(data, variables, grouping.var = record_id){
  for(i in variables){
    data <- data %>%
      group_by(!!enquo(grouping.var)) %>%
      arrange((!!sym(i)), .by_group = T) %>%
      fill(!!sym(i)) %>%
      ungroup()
  }
  return(data)
}

library(microbenchmark)

microbenchmark(
  cole_dplyr = {
    data %>%
      arrange(record_id)%>%
      group_by(record_id)%>%
      mutate_at(vars(-group_cols()), function(x) zoo::na.locf(x[order(x)], na.rm = F))%>%
      ungroup()
  }
  ,
  # cole_dt = {
  #   dt1 <- copy(dt)
  #   
  #   vars_n <- names(dt1)[-1]
  #   dt1[, (vars_n) := lapply(.SD, function(x) zoo::na.locf(sort(x))), keyby = record_id]
  # },
  cole_dt2 = {
    dt[,lapply(.SD, function(x) zoo::na.locf(x[order(x)], na.rm = F)), keyby = record_id]
    },
  mashing_old = {
    data1 <- data
    data1 <- mash.old(data1, 'year1')
    data1 <- mash.old(data1, 'year2')
  }
  ,
  mashing_new = {
    mash.new(data, c('year1', 'year2'))
  }
  , OP_non_mashing = {
    data %>%
      group_by(record_id) %>%
      arrange(year1, .by_group = T) %>%
      fill(year1) %>%
      arrange(year2) %>%
      fill(year2)
  }
)
...