Очистить заголовки перед pivot_longer

#r #dplyr #data.table

#r #dplyr #данные.таблица

Вопрос:

У меня есть набор данных с 40 датчиками с нечетными именами (например A_B_Loc_1 , ). Мне нужно преобразовать эти данные в длинный формат, чтобы отобразить их. Мне нужно разделить имена, чтобы я знал имя датчика (например, from A_B_Loc_1, name=AB ) и местоположение датчика (например, from A_B_Loc_1, location=1 ).

 require(dplyr)
require(janitor)
require(tidyfast)
require(tidyr)

df<-data.frame(time=c("2021-02-27 22:06:20","2021-02-27 23:06:20"),A_Loc_1=c(500,600),A_Loc_2=c(500,600),A_B_Loc_1=c(500,600),A_B_Loc_2=c(500,600),B_Loc_1=c(500,600),B_3=c(500,600))
 

Это около 50 миллионов строк, поэтому это очень медленно:

РЕДАКТИРОВАТЬ: Ой! В некоторых именах нет «Loc» (например, B_3 — это датчик B, местоположение 3).

#Поворот его:

 df %>% 
   tidyfast::dt_pivot_longer( #tidyfast package uses data.table instead of tidyr, so much faster
     cols = -time,
     names_to = "name",
     values_to = "value"
  ) %>% drop_na()->df
 

#Разделить имена

 df %>% 
  separate(name, 
           into = c("sensor", "location"), 
           sep = "(?=[0-9])"
           ) %>% 
  mutate(sensor=janitor::make_clean_names(sensor, case = "big_camel"))
 

Можно ли это ускорить? A left join с таблицей подстановки, которая добавляет столбцы на основе имен датчиков?

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

1. make_clean_names делает то же самое много раз после pivot_longer . Я бы сделал это раньше pivot_longer , чтобы ускорить процесс

Ответ №1:

 library(data.table)
setDT(df)

dt <- melt(df, id.vars = c("time"))
dt[, c("name", "location") := tstrsplit(str_replace_all(variable, "_", ""), "Loc")]

dt
#                    time  variable value name location
#  1: 2021-02-27 22:06:20   A_Loc_1   500    A        1
#  2: 2021-02-27 23:06:20   A_Loc_1   600    A        1
#  3: 2021-02-27 22:06:20   A_Loc_2   500    A        2
#  4: 2021-02-27 23:06:20   A_Loc_2   600    A        2
#  5: 2021-02-27 22:06:20 A_B_Loc_1   500   AB        1
#  6: 2021-02-27 23:06:20 A_B_Loc_1   600   AB        1
#  7: 2021-02-27 22:06:20 A_B_Loc_2   500   AB        2
#  8: 2021-02-27 23:06:20 A_B_Loc_2   600   AB        2
#  9: 2021-02-27 22:06:20   B_Loc_1   500    B        1
# 10: 2021-02-27 23:06:20   B_Loc_1   600    B        1
 

Редактировать: OP упоминает, что Loc присутствует не всегда, поэтому мы разделяем последнее подчеркивание, чтобы получить число. Затем мы очищаем имя на втором шаге, чтобы удалить подчеркивания и — если присутствует — «Loc»

 dt <- melt(df, id.vars = c("time"))
dt[, c("name", "location") := tstrsplit(variable, "_(?!.*_)", perl = T)]
dt[, name := str_replace_all(name, "_|Loc", "")]
 

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

1. tstrsplit(variable, "_Loc_") может быть, будет лучше

2. Тогда ваши имена все равно заканчиваются A_B , разделение зависит от того, насколько надежны имена столбцов, хотя, как упоминает OP в другом комментарии, Loc внезапно отсутствует всегда. Я выбрал простой способ, основанный на наличии Loc, чтобы сначала удалить все подчеркивания.

3. Ага, вы правы.

Ответ №2:

Мы поэкспериментировали с несколькими подходами к разделению столбцов с помощью регулярных выражений. separate был очень медленным, но, по-видимому, самым быстрым было stringr::str_split(..., simplify=TRUE) создание новых столбцов (для tibble).:

 require(dplyr)
require(janitor)
require(tidyr)
require(stringr)

df <-
  data.frame(
    time = c("2021-02-27 22:06:20", "2021-02-27 23:06:20"),
    A_Loc_1 = c(500, 600),
    A_Loc_2 = c(500, 600),
    A_B_Loc_1 = c(500, 600),
    A_B_Loc_2 = c(500, 600),
    B_Loc_1 = c(500, 600)
  )

df1 <- df %>%
  # Suggestion from above about cleaning names first?
  clean_names(case = "big_camel") %>%
  tidyfast::dt_pivot_longer(
    cols = -Time,
    names_to = "name",
    values_to = "value") %>%
  drop_na() %>%
  as_tibble

df1[c("sensor", "location")] <-
  str_split(df1$name, "Loc", simplify = TRUE)
 

Это предполагает, что ваш самый большой расход времени — это разделительная часть столбцов!

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

Существует как минимум четыре способа разделения, и в зависимости от сложности разделения может быть быстрее использовать другие методы (например, data.table::tstrsplit ), но некоторые из них потребуют последовательного «разделения» по всем строкам:

 library(tidyverse)
library(data.table)


# a sample of 100,000 pivoted rows
n <- 1e5

df  <-  data.frame(condition = c(rep("ABLoc1", times = n),
                                 rep("ABLoc2", times = n),
                                 rep("ACLoc1", times = n),
                                 rep("ACLoc2", times = n),
                                 rep("AALoc4", times = n)))


(speeds <- bench::mark(
  separate = {
    df_sep <- df %>%
      separate(condition,sep = "Loc", into = c("part1", "part2"), remove = FALSE)
  },
  dt = {
    df_dt <- data.table::data.table(df)
    df_dt <-
      df_dt[, c("part1" , "part2") := tstrsplit(condition, split = "Loc", fixed = TRUE)] 
    
    
  },
  stringr = {
    
    df_str <- df
    df_str[c("part1", "part2")] <- str_split(df_str$condition, "Loc", simplify = TRUE)
    
  },
  
  gsub = {
      df_vec <- df
      df_vec$part1 <- gsub("(^.*)Loc.*", "\1",  df$condition)
      df_vec$part2 <- gsub(".*Loc(.*$)", "\1",  df$condition)
  },
  iterations = 10,
  check = FALSE
))


#> # A tibble: 4 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 separate      4.63s    5.19s     0.191    3.89GB    4.25 
#> 2 dt          99.44ms 112.32ms     8.95    28.91MB    0.895
#> 3 stringr    296.11ms  306.5ms     3.16    59.53MB    0.632
#> 4 gsub       502.85ms 528.69ms     1.63     7.63MB    0.163


plot(speeds, type = "beeswarm")
 

Скорость построения графиков для каждого подхода (для перебора более 100 000 строк):

Создано 2021-12-08 пакетом reprex (v2.0.1)

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

1. Большое вам спасибо. Что произойдет, если в имени нет «Loc»? Извините, я только что увидел, что в некоторых именах нет Loc, а есть только A_B_1 или B_2, например. Однако число всегда присутствует.

2. Ах, тогда я думаю, что ваш разделитель выше ( (?=[0-9]) ) может работать лучше всего?

3. разделение может быть медленнее, но НЕ настолько медленным 😉 и ваш образец набора данных не выделил так много памяти. В вашем отдельном примере вы включаете сбор ложных данных, который занимает более 4,25 секунды. Несправедливо наказывать этот метод очисткой вашей собственной R-памяти (перед тестированием). Еще одно замечание: для метода data.table несправедливо включать преобразование в data.table и нет необходимости переназначать dt, поскольку dt обновляется по ссылке.

4. Очень верно! это было довольно быстрое и грязное сравнение, чтобы попытаться сравнить время, которое мы заметили, с наборами данных 1m row. Это заставило меня осознать относительную полезность data.table though 🙂