当字符数据实际上是因子时,是否有更快的重新编码字符数据的方法?

3

我经常处理需要一些重编码的字符数据。一个常见场景是,一个被记录的字符向量本质上是一个因子,但并不一定是一个类别。例如,考虑以下 chr 向量 vec

set.seed(2021)
vec <- sample(rep(c("animal_dog_xyz", "animal_cat_abc", "animal_alligator_tyl"), 10))
vec
#>  [1] "animal_dog_xyz"       "animal_alligator_tyl" "animal_cat_abc"      
#>  [4] "animal_cat_abc"       "animal_alligator_tyl" "animal_alligator_tyl"
#>  [7] "animal_cat_abc"       "animal_cat_abc"       "animal_cat_abc"      
#> [10] "animal_dog_xyz"       "animal_dog_xyz"       "animal_cat_abc"      
#> [13] "animal_alligator_tyl" "animal_alligator_tyl" "animal_alligator_tyl"
#> [16] "animal_cat_abc"       "animal_dog_xyz"       "animal_alligator_tyl"
#> [19] "animal_alligator_tyl" "animal_cat_abc"       "animal_dog_xyz"      
#> [22] "animal_cat_abc"       "animal_cat_abc"       "animal_dog_xyz"      
#> [25] "animal_dog_xyz"       "animal_dog_xyz"       "animal_dog_xyz"      
#> [28] "animal_dog_xyz"       "animal_alligator_tyl" "animal_alligator_tyl"

本文创建于2021-07-19,使用reprex包(v2.0.0)

如果我想重新编码这个向量并提取出动物名称,我会选择适用于字符数据的解决方案:

library(stringr)

sapply(str_split(vec, "_",  n = 3), `[`, 2)
#>  [1] "dog"       "alligator" "cat"       "cat"       "alligator" "alligator"
#>  [7] "cat"       "cat"       "cat"       "dog"       "dog"       "cat"      
#> [13] "alligator" "alligator" "alligator" "cat"       "dog"       "alligator"
#> [19] "alligator" "cat"       "dog"       "cat"       "cat"       "dog"      
#> [25] "dog"       "dog"       "dog"       "dog"       "alligator" "alligator"

问题

如果向量非常长,进行这样的重新编码过程需要很长时间。R会迭代每个向量元素并应用该过程。鉴于向量中只有3个唯一值,这似乎效率低下。换句话说,我们不需要逐个检查元素并确定重新编码的值。

在这里,vec_long 长度为30000。这是在我的计算机上重新编码所需的时间。

vec_long <- sample(rep(c("animal_dog_xyz", "animal_cat_abc", "animal_alligator_tyl"), 10000))
length(vec_long)
#> [1] 30000

library(microbenchmark)

microbenchmark(sapply(str_split(vec_long, "_",  n = 3), `[`, 2))
#> Unit: milliseconds
#>                                             expr     min       lq     mean
#>  sapply(str_split(vec_long, "_", n = 3), `[`, 2) 51.6972 52.66918 57.42299
#>    median      uq     max neval
#>  54.47867 58.7653 115.754   100

有没有一种方法可以利用这个向量实际上是一个因子的事实?因此识别唯一值(“级别”),重新编码它们,并重新部署到整个向量长度中?是否有这样的过程可以加快处理时间?

谢谢!


编辑


我想总结一下基于@GKi的答案,@ThomasIsCoding的答案和@user20650的评论所做的测试。

## The Data
set.seed(2021)

unique_vals <- c("animal_dog_xyz", "animal_cat_abc", "animal_alligator_tyl")

vec <- sample(rep(unique_vals, 10))
vec_long <- sample(rep(unique_vals, 1000))
vec_very_long <- sample(unique_vals, 100000))

## The functions

## function #1 -- as @user20650 proposed
via_fac_levels <- function(x) {
  x_factor <- factor(x)
  levels(x_factor) <- sapply(str_split(levels(x_factor), "_",  n = 3), `[`, 2)
  as.character(x_factor)
}
####################

## function #2 --  as @GKi proposed
via_fac_no_levels <- function(x) {
  x_factor <- as.factor(x)
  x_factor <- sapply(strsplit(levels(x_factor), "_", TRUE), `[`, 2)[x_factor]
  as.character(x_factor)
}
####################

## function #3 -- the original slow method shown in the question
via_chr_only <- function(x) {
  sapply(str_split(x, "_",  n = 3), `[`, 2)
}

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

## function #4 -- as @ThomasIsCoding proposed
via_read_table <- function(x) {
  read.table(text = paste0(x, collapse = "\n"), sep = "_", header = FALSE)$V2
}

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

## function #5 -- forcats::fct_relabel()
via_fct_relabel <- function(x) {
  x_factor <- as.factor(x)
  x_factor <- fct_relabel(x_factor, ~sapply(str_split(.x, "_",  n = 3), `[`, 2))
  as.character(x_factor)
}

## Performance assessment
### I ran it on Rstudio cloud
bm_short <- bench::mark(fac_levels = via_fac_levels(vec),
                  fac_no_levels = via_fac_no_levels(vec),
                  chr = via_chr_only(vec), 
                  read_t = via_read_table(vec),
                  fct_relabel = via_fct_relabel(vec),
                  iterations = 1000)

bm_long <- bench::mark(fac_levels = via_fac_levels(vec_long),
                        fac_no_levels = via_fac_no_levels(vec_long),
                        chr = via_chr_only(vec_long), 
                       read_t = via_read_table(vec_long),
                       fct_relabel = via_fct_relabel(vec_long),
                       iterations = 1000)

bm_very_long <- bench::mark(fac_levels = via_fac_levels(vec_very_long),
                  fac_no_levels = via_fac_no_levels(vec_very_long),
                  chr = via_chr_only(vec_very_long),
                  read_t = via_read_table(vec_very_long),
                  fct_relabel = via_fct_relabel(vec_very_long),
                  iterations = 1000)

## visualize
library(ggplot2)
library(tidyr)
library(ggbeeswarm)
library(beeswarm)

autoplot(bm_short) + ggtitle("data of length 30")
autoplot(bm_long) + ggtitle("data of length 3000")
autoplot(bm_very_long) + ggtitle("data of length 300000")

bm_short bm_long bm_very_long

## verify all functions give the same output
v1 <- via_fac_levels(vec_long)
v2 <- via_fac_no_levels(vec_long)
v3 <- via_chr_only(vec_long)
v4 <- via_read_table(vec_long)
v5 <- via_fct_relabel(vec_long)

all(sapply(list(v1, v2, v3, v4), FUN = identical, v5)) # https://dev59.com/p10Z5IYBdhLWcg3w-UV-#30850654
## [1] TRUE

2
vec_long = factor(vec_long) ; levels(vec_long) <- c("all", "cat", "dog") - user20650
4个回答

2
如果您创建一个因子,这也需要一些时间,它可能看起来像这样:

vec_fac <- as.factor(vec_long)
sapply(strsplit(levels(vec_fac), "_", TRUE), `[`, 2)[vec_fac]

如果需要对数据进行重新编码:

levels(vec_fac) <- sapply(strsplit(levels(vec_fac), "_", TRUE), `[`, 2)
vec_fac

基准测试:

bench::mark(
         Question = sapply(strsplit(vec_long, "_",  TRUE), `[`, 2)
       , HaveFactor = sapply(strsplit(levels(vec_fac), "_", TRUE), `[`, 2)[vec_fac]
       , CreateFactor = {vec_fac <- as.factor(vec_long)
         sapply(strsplit(levels(vec_fac), "_", TRUE), `[`, 2)[vec_fac]}
       )
#  expression        min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc
#  <bch:expr>   <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>
#1 Question       16.2ms   16.4ms      59.6    1.17MB     51.7    15    13
#2 HaveFactor    233.9µs    251µs    3757.   234.42KB     16.8  1788     8
#3 CreateFactor  882.8µs  920.2µs    1073.   959.36KB     19.2   503     9

谢谢。我并不明白你所描述的第一个场景(“如果你创建了一个因子”)和第二个场景(“在数据也需要重新编码的情况下”)之间的区别。你所说的“如果数据需要重新编码”,是什么意思? - Emman
1
在第一种情况下,您只需获取值并保留数据不变。在第二种情况下,您需要覆盖数据以获得所需的输出。 - GKi

1
你可以尝试以下代码使用 read.table
read.table(text = vec, sep = "_", header = FALSE)$V2

这提供了

> read.table(text = vec, sep = "_", header = FALSE)$V2
 [1] "dog"       "alligator" "cat"       "cat"       "alligator" "alligator"
 [7] "cat"       "cat"       "cat"       "dog"       "dog"       "cat"
[13] "alligator" "alligator" "alligator" "cat"       "dog"       "alligator"
[19] "alligator" "cat"       "dog"       "cat"       "cat"       "dog"
[25] "dog"       "dog"       "dog"       "dog"       "alligator" "alligator"

这很聪明,但它过于特定于此示例中数据的模式。如果我们想应用不同的字符串处理,我们该如何使用这种方法? - Emman
@Emman 在 strsplit 中使用的分隔符可以作为 read.tablesep = 的参数应用。 - ThomasIsCoding
是的,但在不同的情况下,处理类型可能是 - 例如 - 从字母数字值中删除数字:stringr :: str_replace_all(c(“44dog123”,“lion896”,“1zebra5”),“[: digit:]”,“”),它返回 [1]“dog”“lion”“zebra”。我们能否将 read.table() 用于此类字符串处理? - Emman
在这种情况下,您可以尝试使用gsub("\\d+","",vec) - ThomasIsCoding
为了完整起见,您的意思是:vec <- c("44dog123", "lion896", "1zebra5"); read.table(text = gsub("\\d+", "", vec), header = FALSE)$V1,它确实返回 [1] "dog" "lion" "zebra"。确实很有创意! - Emman
我已更新帖子并将您的方法添加到基准测试中。 - Emman

0
我意识到解决这个问题的一种方法是使用 forcats::fct_relabel()
library(forcats)
library(stringr)

via_fct_relabel <- function(x) {
  x_factor <- as.factor(x)
  x_factor <- fct_relabel(x_factor, ~sapply(str_split(.x, "_",  n = 3), `[`, 2))
  as.character(x_factor)
}


vec_long <- sample(rep(c("animal_dog_xyz", "animal_cat_abc", "animal_alligator_tyl"), 10000))

result <- via_fct_relabel(vec_long)
head(result, n = 20)
#>  [1] "cat"       "dog"       "dog"       "cat"       "cat"       "alligator"
#>  [7] "cat"       "alligator" "alligator" "cat"       "alligator" "dog"      
#> [13] "cat"       "alligator" "dog"       "cat"       "cat"       "dog"      
#> [19] "cat"       "dog"

reprex包(v2.0.0)于2021-07-21创建


0

我也曾经玩过一个想法,即编写一个允许使用正则表达式重新编码因子的函数。具体请查看https://dev59.com/hpfga4cB1Zd3GeqPB9pB#37800944

简而言之

remotes::install_github("jwilliman/xfactor")

as.character(
    xfactor::xfactor(vec_very_long, levels = c(dog = "dog", cat = "cat", alligator = "alligator"))
)

引号中的位是正则表达式,名称是要替换它们的级别。

这比上面最快的答案慢了将近两倍,但在允许为每个结果因子级别应用不同的正则表达式模式方面具有更大的灵活性。也就是说,适用于清理混乱的数据。


谢谢。但是,我不完全理解。我该如何将字符串处理函数(例如sapply(str_split(.x, "_", n = 3), [, 2))传递给xfactor() - Emman
@Emman 你不会这样做的。你需要为每个要生成的输出级别定义一个新的正则表达式,例如在示例中,你需要为每个动物编写一个新的正则表达式。xfactor 不适用于遵循固定模式的明确定义数据。然而,它非常适合重新编码混乱且无法使用单个处理函数重新编码的字符数据,例如手动输入的带有拼写错误和不一致格式的数据。 - JWilliman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接