在保留组位置的同时进行排序 [英] Sort While Preserving Locations of Groups

查看:22
本文介绍了在保留组位置的同时进行排序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我这里有一个tibbleLiketb_1

# A tibble: 7 x 2
  Grp     Srt
  <chr> <int>
1 A        10
2 B         4
3 B         7
4 A         5
5 A         1
6 A         3
7 B         2

我转载如下:

tb_1 <- structure(
  list(
    Grp = c("A", "B", "B", "A", "A", "A", "B"),
    Srt = c(10L, 4L, 7L, 5L, 1L, 3L, 2L)
  ),
  class = c("tbl_df", "tbl", "data.frame"),
  row.names = c(NA, -7L)
)

我希望dplyrstyle中有一个性能函数arrange_groups();该函数将(按给定变量)对每个现有组内的观察结果进行排序,同时保留该组的分布位置。

library(dplyr)


tb_2 <- tb_1 %>%

  # Group by 'Grp'.
  group_by(Grp) %>%

  # Sort by 'Srt' WITHIN each group.
  arrange_groups(Srt)
在得到的tb_2中,来自"A"组的4个观测值应该保持分布在1456行之间;在它们之间按Srt排序之后。同样,来自"B"组的3个观测值应保持分布在237行中。

# A tibble: 7 x 2
# Groups:   Grp [2]
  Grp     Srt
  <chr> <int>
1 A         1
2 B         2
3 B         4
4 A         3
5 A         5
6 A        10
7 B         7

我已经复制了以下tb_2

tb_2 <- structure(
  list(
    Grp = c("A", "B", "B", "A", "A", "A", "B"),
    Srt = c(1L, 2L, 4L, 3L, 5L, 10L, 7L)
  ),
  class = c("grouped_df", "tbl_df", "tbl", "data.frame"),
  row.names = c(NA, -7L),
  groups = structure(
    list(
      Grp = c("A", "B"),
      .rows = structure(
        list(
          c(1L, 4L, 5L, 6L),
          c(2L, 3L, 7L)
        ),
        ptype = integer(0),
        class = c("vctrs_list_of", "vctrs_vctr", "list")
      )
    ),
    class = c("tbl_df", "tbl", "data.frame"),
    row.names = c(NA, -2L),
    .drop = TRUE
  )
)

更新

虽然我能够answer我自己的问题,但我会留出机会讨论其他解决方案。我很想看看有哪些alternatives存在,特别是那些性能更好、更有创造力或与data.table这样的不同生态系统协同工作的产品。

走向优化

理想情况下,进一步的解决方案应该

  1. 避免为df中的每一列重新计算order(Srt_1, Srt_2, ...)
  2. 不得低于data.table中的existing suggestions

推荐答案

解决方案

tidyverse中,可以通过简单的工作流或(除其他外)以下两个功能来实现该目标。

工作流

您可以简单地忽略arrange_groups(),转而使用mutate()实现dplyr工作流,因为操作(如order())无论如何都会在组内应用。

library(dplyr)

tb_1 %>%
    group_by(Grp) %>%

    # Arbitrary sorting variables go HERE:
    mutate(across(everything(), ~.[order(Srt)]))
    #                                    ^^^

重新排序函数

arrange_groups_1()函数首先按现有组排序,然后按...中给出的变量排序。将数据在其组内进行排序后,arrange_groups_1()然后将这些组映射回其原始位置。

arrange_groups_1 <- function(.data, ...) {
  # Arrange into group "regions", and sort within each region; then...
  dplyr::arrange(.data = .data, ... = ..., .by_group = TRUE)[
    # ...map the results back to the original structure.
    order(order(dplyr::group_indices(.data = .data))),
  ]
}

兼容dplyr

library(dplyr)

tb_1 %>%
    group_by(Grp) %>%
    arrange_groups_1(Srt)

变异函数

不如arrange_groups_1()聪明,但比arrange_groups_1()更直接,arrange_groups_2()解决方案只是以函数形式实现工作流。

arrange_groups_2 <- function(.data, ...) {
  # Capture the symbols for the sorting variables.
  dots <- dplyr::enquos(...)
  
  dplyr::mutate(
    .data = .data,
    dplyr::across(
      # Sort across the entire dataset.
      .cols = dplyr::everything(),

      # Sort each group "in place"; by variables captured from the proper scope.
      .fns = ~.[order(!!!dots)]
    )
  )
}

也兼容dplyr

library(dplyr)

tb_1 %>%
    group_by(Grp) %>%
    arrange_groups_2(Srt)

结果

给定像您这样的tb_1,所有这些解决方案都将产生预期的结果:

# A tibble: 7 x 2
# Groups:   Grp [2]
  Grp     Srt
  <chr> <int>
1 A         1
2 B         2
3 B         4
4 A         3
5 A         5
6 A        10
7 B         7

性能

在大型数据集上,性能差异可能会很大。给定具有100万个观察值的df和用于分组(Grp_*)和排序(Srt_*)的几个变量)

set.seed(0)

df <- data.frame(
    Record_ID = 1:1000000,

    Grp_1 = sample(x = letters[ 1:6 ] , size = 1000000, replace = TRUE ),
    Grp_2 = sample(x = letters[ 7:12] , size = 1000000, replace = TRUE ),
    Grp_3 = sample(x = letters[13:18] , size = 1000000, replace = TRUE ),
    Grp_4 = sample(x = letters[19:26] , size = 1000000, replace = TRUE ),
    
    Srt_1 = sample(x =       1:1000000, size = 1000000, replace = FALSE),
    Srt_2 = sample(x = 1000001:2000000, size = 1000000, replace = FALSE),
    Srt_3 = sample(x = 2000001:3000000, size = 1000000, replace = FALSE),
    Srt_4 = sample(x = 3000001:4000000, size = 1000000, replace = FALSE)
)

这里计算了每种解决方案的相对性能:

library(dplyr)
library(microbenchmark)

performances <- list(
  one_var = microbenchmark(
    arrange_groups_1 = df %>%
      group_by(Grp_1) %>%
      arrange_groups_1(Srt_1), 
    arrange_groups_2 = df %>%
      group_by(Grp_1) %>%
      arrange_groups_2(Srt_1), 
    workflow = df %>%
      group_by(Grp_1) %>%
      mutate(across(everything(), ~.[order(Srt_1)])),
    times = 50
  ),
  two_vars = microbenchmark(
    arrange_groups_1 = df %>%
      group_by(Grp_1, Grp_2) %>%
      arrange_groups_1(Srt_1, Srt_2),
    arrange_groups_2 = df %>%
      group_by(Grp_1, Grp_2) %>%
      arrange_groups_2(Srt_1, Srt_2),
    workflow = df %>%
      group_by(Grp_1, Grp_2) %>%
      mutate(across(everything(), ~.[order(Srt_1, Srt_2)])),
    times = 50
  ),
  three_vars = microbenchmark(
    arrange_groups_1 = df %>%
      group_by(Grp_1, Grp_2, Grp_3) %>%
      arrange_groups_1(Srt_1, Srt_2, Srt_3),
    arrange_groups_2 = df %>%
      group_by(Grp_1, Grp_2, Grp_3) %>%
      arrange_groups_2(Srt_1, Srt_2, Srt_3),
    workflow = df %>%
      group_by(Grp_1, Grp_2, Grp_3) %>%
      mutate(across(everything(), ~.[order(Srt_1, Srt_2, Srt_3)])),
    times = 50
  ),
  four_vars = microbenchmark(
    arrange_groups_1 = df %>%
      group_by(Grp_1, Grp_2, Grp_3, Grp_4) %>%
      arrange_groups_1(Srt_1, Srt_2, Srt_3, Srt_4),
    arrange_groups_2 = df %>%
      group_by(Grp_1, Grp_2, Grp_3, Grp_4) %>%
      arrange_groups_2(Srt_1, Srt_2, Srt_3, Srt_4),
    workflow = df %>%
      group_by(Grp_1, Grp_2, Grp_3, Grp_4) %>%
      mutate(across(everything(), ~.[order(Srt_1, Srt_2, Srt_3, Srt_4)])),
    times = 50
  )
)

显然arrange_groups_1()处于劣势。我怀疑arrange_groups_2()可以在工作流程中站稳脚跟,并保持在后者的视线之内,同时提供更符合人体工程学的用法。但是,对于更大的分组和排序变量集,应该在其他(更好的)计算机上验证这种怀疑。

#> performances

$one_var
Unit: milliseconds
             expr       min        lq      mean    median        uq       max neval
 arrange_groups_1 2066.4674 2155.8859 2231.3547 2199.7442 2283.5782 2565.0542    50
 arrange_groups_2  352.3775  385.1829  435.2595  444.8746  464.1493  607.0927    50
         workflow  337.2756  391.0174  428.9049  435.8385  454.7347  546.4498    50

$two_vars
Unit: milliseconds
             expr       min        lq      mean    median        uq       max neval
 arrange_groups_1 3580.5395 3688.1506 3842.2048 3799.5430 3979.9716 4317.7100    50
 arrange_groups_2  230.1166  239.9141  265.0786  249.3640  287.1006  359.1822    50
         workflow  221.6627  234.2732  256.6200  243.3707  281.2269  365.9102    50

$three_vars
Unit: milliseconds
             expr       min        lq      mean    median        uq       max neval
 arrange_groups_1 5113.6341 5340.5483 5441.3399 5443.5068 5535.0578 5946.6958    50
 arrange_groups_2  261.9329  274.1785  295.6854  282.4638  323.5710  412.0139    50
         workflow  224.8709  236.9958  263.2440  252.6042  292.7043  339.6351    50

$four_vars
Unit: milliseconds
             expr       min        lq      mean    median        uq       max neval
 arrange_groups_1 6810.3864 7035.7077 7237.6941 7156.7051 7314.4667 8051.8558    50
 arrange_groups_2  581.9000  603.7822  640.8977  626.4116  672.6488  859.8239    50
         workflow  349.7786  361.6454  391.7517  375.1532  429.3643  485.9227    50

更新

混合函数

@akrunanswer启发,这里有一个集成了data.table.

功能的函数
arrange_groups_3 <- function(.data, ...) {
  # Name the variables for grouping, and their complement in '.data'.
  group_vars <- dplyr::group_vars(.data)
  other_vars <- setdiff(names(.data), group_vars)

  # For proper scoping, generate here the expression for sorting.
  sort_expr <- substitute(order(...))
  
  dplyr::as_tibble(data.table::as.data.table(.data)[,
    (other_vars) := lapply(
      # Sort each column, using an index...
      .SD, (x, i) x[i],

      # ...which we need calculate only once.
      i = eval(sort_expr)
    ),
    group_vars
  ])
}

.人体工程学dplyr

library(dplyr)

tb_1 %>%
    group_by(Grp) %>%
    arrange_groups_3(Srt)

但是,我的实现删除了.data中的原始分组,因此仍是正在进行的工作

快速变异

此相当快的实现受@Henriksuggestion使用dtplyrdata.table后端dplyr的启发。

arrange_groups_4 <- function(.data, ...) {
  # Capture the symbols for the sorting and grouping variables.
  sort_syms <- dplyr::enquos(...)
  group_syms <- dplyr::groups(.data)
  
  .data |>

    # Use a "data.table" backend.
    dtplyr::lazy_dt() |>

    # Preserve the grouping.
    dplyr::group_by(!!!group_syms) |>

    # Perform the sorting.
    dplyr::mutate(
      dplyr::across(
        # Sort across the entire dataset.
        .cols = dplyr::everything(),
        
        # Sort each group "in place": subscript using the index...
        .fns = `[`,
        
        # ...generated when ordering by the sorting variables.
        i = order(!!!sort_syms)
      )
    )
}

虽然我还没有对超过4个变量进行分组和排序测试,但似乎可以在线性时间内完成:

$one_var
Unit: milliseconds
             expr    min      lq     mean  median      uq     max neval
 arrange_groups_4 30.738 31.8028 46.81692 37.6586 59.8274 95.4703    50

$two_vars
Unit: milliseconds
             expr     min      lq     mean  median      uq    max neval
 arrange_groups_4 41.4364 41.9118 52.91332 46.4306 66.1674 80.171    50

$three_vars
Unit: milliseconds
             expr     min      lq     mean  median     uq      max neval
 arrange_groups_4 47.8605 48.6225 62.06675 51.9562 71.487 237.0102    50

$four_vars
Unit: milliseconds
             expr    min      lq     mean   median      uq      max neval
 arrange_groups_4 67.306 69.1426 78.68869 73.81695 88.7874 108.2624    50

这篇关于在保留组位置的同时进行排序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆