【问题标题】:A sensible function to sort dataframes对数据帧进行排序的明智功能
【发布时间】:2022-01-20 22:46:27
【问题描述】:

我正在创建一个函数来对 data.frames 进行排序(为什么?因为原因)。一些标准:

  • 适用于 data.frames
  • 针对非交互式使用
  • 仅使用基础 R
  • 不依赖任何非基础包

函数现在看起来像这样:

#' @title Sort a data.frame
#' @description Sort a data.frame based on one or more columns
#' @param x A data.frame class object
#' @param by A column in the data.frame. Defaults to NULL, which sorts by all columns.
#' @param decreasing A logical indicating the direction of sorting.
#' @return A data.frame.
#' 
sortdf <- function(x,by=NULL,decreasing=FALSE) {
  if(!is.data.frame(x)) stop("Input is not a data.frame.")

  if(is.null(by)) {
    ord <- do.call(order,x)
  } else {
    if(any(!by %in% colnames(x))) stop("One or more items in 'by' was not found.")
    if(length(by) == 1) ord <- order(x[ , by])
    if(length(by) > 1) ord <- do.call(order, x[ , by])
  }
  
  if(decreasing) ord <- rev(ord)
  return(x[ord, , drop=FALSE])
}

例子

sortdf(iris)
sortdf(iris,"Petal.Length")
sortdf(iris,"Petal.Length",decreasing=TRUE)
sortdf(iris,c("Petal.Length","Sepal.Length"))
sortdf(iris,"Petal.Length",decreasing=TRUE)

目前有什么效果

  • 按一列或多列对 data.frame 进行排序
  • 调整整体排序方向

但是,我还需要一个功能:通过传递 by 中指定的每一列的方向向量,分别为每一列设置排序方向的能力。例如;

sortdf(iris,by=c("Sepal.Width","Petal.Width"),dir=c("up","down"))

关于如何实现这一点的任何想法/建议?

更新

以下答案的基准:

library(microbenchmark)
library(ggplot2)

m <- microbenchmark::microbenchmark(
  "base 1u"=iris[order(iris$Petal.Length),],
  "Maël 1u"=sortdf(iris,"Petal.Length"),
  "Mikko 1u"=sortdf1(iris,"Petal.Length"),
  "arrange 1u"=dplyr::arrange(iris,Petal.Length),
  "base 1d"=iris[order(iris$Petal.Length,decreasing=TRUE),],
  "Maël 1d"=sortdf(iris,"Petal.Length",dir="down"),
  "Mikko 1d"=sortdf1(iris,"Petal.Length",decreasing=T),
  "arrange 1d"=dplyr::arrange(iris,-Petal.Length),
  "base 2d"=iris[order(iris$Petal.Length,iris$Sepal.Length,decreasing=TRUE),],
  "Maël 2d"=sortdf(iris,c("Petal.Length","Sepal.Length"),dir=c("down","down")),
  "Mikko 2d"=sortdf1(iris,c("Petal.Length","Sepal.Length"),decreasing=T),
  "arrange 2d"=dplyr::arrange(iris,-Petal.Length,-Sepal.Length),
  "base 1u1d"=iris[order(iris$Petal.Length,rev(iris$Sepal.Length)),],
  "Maël 1u1d"=sortdf(iris,c("Petal.Length","Sepal.Length"),dir=c("up","down")),
  "Mikko 1u1d"=sortdf1(iris,c("Petal.Length","Sepal.Length"),decreasing=c(T,F)),
  "arrange 1u1d"=dplyr::arrange(iris,Petal.Length,-Sepal.Length),
  times=1000
)
autoplot(m)+theme_bw()

R 4.1.0
dplyr 1.0.7

【问题讨论】:

  • dirdecreasing 的区别仅在于它可以从不同的列中以不同的方式指定吗?
  • 我会使用 forloop。
  • @Maël 我想我们可以摆脱 decreasing 一次 dir 工作。如果未指定任何列,则 dir 可以默认为 rep("up",ncol(x))。也许dir 不是最好的名字,听起来有点像目录。
  • 你可以看看rgr packagegx.sort.df函数。我喜欢它的语法,带有公式:gx.sort.df(dat, ~ colA + colB)。并且使用 - 而不是 + 它按降序排序
  • @Greg 正如我所提到的,这种特殊情况是用于非交互式使用(即;在其他函数中使用它),所以desc() 可能不会工作得那么好。但是,对于交互式使用,dplyr::arrange() 可能是最好的选择,我完全同意你的观点,如果有一个类似的基本 R 等效项会很好。

标签: r


【解决方案1】:

这是我的尝试,使用取自 this answer 的函数,并假设 up 是上升的,down 是下降的。 dir 默认设置为“向上”。

sortdf <- function(x, by=NULL, dir=NULL) {
  if(!is.data.frame(x)) stop("Input is not a data.frame.")
  
  if(is.null(by) & is.null(dir)) {
    dir <- rep("up", ncol(x))
  } else if (is.null(dir)) {
    dir <- rep("up", length(by))
  }
  
  sort_asc = by[which(dir == "up")]
  sort_desc = by[which(dir == "down")]

  if(is.null(by)) {
    ord <- do.call(order,x)
  } else {
    if(any(!by %in% colnames(x))) stop("One or more items in 'by' was not found.")
    if(length(by) == 1) ord <- order(x[ , by])
    if(length(by) > 1) ord <- do.call(order, c(as.list(iris[sort_asc]), lapply(iris[sort_desc], 
                                                                               function(x) -xtfrm(x))))
  }
  
   if(length(dir) == 1 & all(dir == "down")) ord <- rev(ord)

  x[ord, , drop=FALSE]
}

然后您可以有多个不同的方向进行排序:

sortdf(iris, by=c("Sepal.Width","Petal.Width"), dir=c("up","down")) |>
  head()

    Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
61           5.0         2.0          3.5         1.0 versicolor
69           6.2         2.2          4.5         1.5 versicolor
120          6.0         2.2          5.0         1.5  virginica
63           6.0         2.2          4.0         1.0 versicolor
54           5.5         2.3          4.0         1.3 versicolor
88           6.3         2.3          4.4         1.3 versicolor

其他示例也按预期工作:

sortdf(iris)
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
14          4.3         3.0          1.1         0.1  setosa
9           4.4         2.9          1.4         0.2  setosa
39          4.4         3.0          1.3         0.2  setosa
43          4.4         3.2          1.3         0.2  setosa
42          4.5         2.3          1.3         0.3  setosa
4           4.6         3.1          1.5         0.2  setosa

sortdf(iris, c("Petal.Length","Sepal.Length"))
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
23          4.6         3.6          1.0         0.2  setosa
14          4.3         3.0          1.1         0.1  setosa
36          5.0         3.2          1.2         0.2  setosa
15          5.8         4.0          1.2         0.2  setosa
39          4.4         3.0          1.3         0.2  setosa
43          4.4         3.2          1.3         0.2  setosa

sortdf(iris, "Petal.Length", "down")
    Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
119          7.7         2.6          6.9         2.3 virginica
123          7.7         2.8          6.7         2.0 virginica
118          7.7         3.8          6.7         2.2 virginica
106          7.6         3.0          6.6         2.1 virginica
132          7.9         3.8          6.4         2.0 virginica
108          7.3         2.9          6.3         1.8 virginica

【讨论】:

  • 谢谢!有趣的是它不保留行号的顺序。 all.equal(rownames(sortdf(iris,"Petal.Length")),rownames(dplyr::arrange(iris,Petal.Length)))
  • 我所说的“它”是指dplyr::arrange(),而我以前从未注意到它。
【解决方案2】:

这是另一个摆脱所有分支逻辑的替代方案 确保您始终为每个 by 列找到一个代理进行排序 xtfrm()。为了与基础保持一致,而不是使用“新”dir 参数,最好保留 decreasing 参数,但是 只允许它是一个被回收以匹配 by 长度的向量。

sortdf <- function(x, by = colnames(x), decreasing = FALSE, ...) {
  if (!is.data.frame(x)) {
    stop("Input is not a data.frame.")
  }

  if (!all(by %in% colnames(x))) {
    stop("One or more items in 'by' was not found.")
  }
  
  # Recycle `decreasing` to ensure it matches `by`
  decreasing <- rep_len(as.logical(decreasing), length(by))
  
  # Find a sorting proxy for each `by` column, according to `decreasing`
  pxy <- Map(function(x, decr) (-1)^decr * xtfrm(x), x[by], decreasing)
  ord <- do.call(order, c(pxy, list(...)))
  
  x[ord, , drop = FALSE]
}

再想一想,我什至可能会进一步简化:

  • Map() 处理bydecreasing 的回收。
  • [ 处理因索引不正确而引发的错误(因此也接受 列的数字索引,而不仅仅是名称)。
  • 不通过...(遵循YAGNI原则)。

这可以归结为两个单行函数:

sortdf <- function(x, by = colnames(x), decreasing = FALSE) {
  x[do.call(order, Map(sortproxy, x[by], decreasing)), , drop = FALSE]
}

sortproxy <- function(x, decreasing = FALSE) {
  as.integer((-1)^as.logical(decreasing)) * xtfrm(x)
}

例子:

sortdf(iris) |> head()
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 14          4.3         3.0          1.1         0.1  setosa
#> 9           4.4         2.9          1.4         0.2  setosa
#> 39          4.4         3.0          1.3         0.2  setosa
#> 43          4.4         3.2          1.3         0.2  setosa
#> 42          4.5         2.3          1.3         0.3  setosa
#> 4           4.6         3.1          1.5         0.2  setosa

sortdf(iris, by = c("Sepal.Length", "Sepal.Width")) |> head()
#>    Sepal.Length Sepal.Width Petal.Length Petal.Width Species
#> 14          4.3         3.0          1.1         0.1  setosa
#> 9           4.4         2.9          1.4         0.2  setosa
#> 39          4.4         3.0          1.3         0.2  setosa
#> 43          4.4         3.2          1.3         0.2  setosa
#> 42          4.5         2.3          1.3         0.3  setosa
#> 4           4.6         3.1          1.5         0.2  setosa

sortdf(iris, by = c("Sepal.Length", "Sepal.Width"), decreasing = TRUE) |> head()
#>     Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
#> 132          7.9         3.8          6.4         2.0 virginica
#> 118          7.7         3.8          6.7         2.2 virginica
#> 136          7.7         3.0          6.1         2.3 virginica
#> 123          7.7         2.8          6.7         2.0 virginica
#> 119          7.7         2.6          6.9         2.3 virginica
#> 106          7.6         3.0          6.6         2.1 virginica

sortdf(iris, by = c("Sepal.Length", "Sepal.Width"), decreasing = c(TRUE, FALSE)) |> head()
#>     Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
#> 132          7.9         3.8          6.4         2.0 virginica
#> 119          7.7         2.6          6.9         2.3 virginica
#> 123          7.7         2.8          6.7         2.0 virginica
#> 136          7.7         3.0          6.1         2.3 virginica
#> 118          7.7         3.8          6.7         2.2 virginica
#> 106          7.6         3.0          6.6         2.1 virginica

【讨论】:

  • 不错!我喜欢减少的简单性和使用。我很惊讶这并没有转化为更快的执行时间。
  • 是的,说实话,我很惊讶地发现性能上有任何差异。我的第一个想法是,这可能是因为 xtfrm() 甚至在非递减列上的调用,但现在我不确定了。无论如何,我怀疑使用更大的数据会很快消除差异。
  • @rmf 哦,我意识到我的方法对于仅使用 1 列进行排序的情况特别慢。在@Maël 的回答中,这是为了避免 do.call() 的特殊情况,这就是差异的来源。
  • 您介意详细说明* xtfrm(x)(-1)^decr * xtfrm(x) 中应该做什么吗?
  • @tmfmnk 当然:xtfrm(x) 给出了一个数字向量,其排序方式与原始 x 的排序方式相同。这让您可以通过取负值来进行递减排序。当然,对于数字输入,这无关紧要,但对于因子/字符输入,它是必需的,让您统一处理所有输入。
猜你喜欢
  • 2017-06-30
  • 2020-07-10
  • 1970-01-01
  • 1970-01-01
  • 2020-08-15
  • 2021-07-25
  • 1970-01-01
  • 1970-01-01
  • 2017-03-08
相关资源
最近更新 更多