在 pivot_longer 之前清理标题

Posted

技术标签:

【中文标题】在 pivot_longer 之前清理标题【英文标题】:Clean headers before pivot_longer 【发布时间】:2022-01-13 09:57:36 【问题描述】:

我有一个包含 40 传感器的数据集,这些传感器的名称很奇怪(例如 A_B_Loc_1)。我需要将这些数据转换为长格式来绘制它们。我需要拆分名称,以便知道传感器名称(例如来自A_B_Loc_1, name=AB)和传感器位置(例如来自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))

大约有 5000 万行,所以很慢:

编辑:哎哟!有些名称没有“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"))

这可以加速吗? left join 有一个根据传感器名称添加列的查找表?

【问题讨论】:

make_clean_namespivot_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", "")]

【讨论】:

tstrsplit(variable, "_Loc_") 可能会更好 那么你的名字仍然以 A_B 结尾,拆分完全取决于列名的健壮程度,正如 OP 在另一条评论中提到的那样,Loc 突然并不总是存在。我根据 Loc 的存在选择了一种简单的方法来首先删除所有下划线。 啊哈,你是对的。【参考方案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 行):

由reprex package (v2.0.1) 于 2021-12-08 创建

【讨论】:

非常感谢。如果名称中没有“Loc”会怎样?抱歉,我刚刚看到有些名称没有 Loc,而只有 A_B_1 或 B_2。但是这个数字总是存在的。 啊,那么我认为上面的分隔符 ((?=[0-9])) 可能效果最好? separate 可能会更慢,但不会那么慢 ;) 并且您的示例数据集没有分配这么多内存。在您的单独示例中,您包含超过 4.25 秒的虚假垃圾收集。通过清理你自己的 R 内存(在测试之前)来惩罚这种方法是不公平的。另一个注意事项,对于 data.table 方法,包含到 data.table 的转换是不公平的,也没有必要重新分配 dt,因为 dt 通过引用更新。 非常真实!这是一个相当快速和肮脏的比较,试图将我们注意到的时间与 1m+ 行数据集进行比较。它让我意识到data.table 的相对有用性:)

以上是关于在 pivot_longer 之前清理标题的主要内容,如果未能解决你的问题,请参考以下文章

laravel :在验证之前清理请求数据

在 .NET 中执行测试之前清理数据库

如何在重用之前清理 ListViewItem 的状态?

在 Node.js 退出之前执行清理操作

在存储到数据库之前或渲染之前清理 HTML? (ASP.NET 中的反 XSS 库)

电脑怎么设置关机自动清理某个指定文件夹内多天之前的文件?近期的文件不要清理!