关于R:如何按多个列对数据帧排序

How to sort a dataframe by multiple column(s)

我想按多列对data.frame排序。例如,在下面的data.frame中,我希望按列z降序排序,然后按列b升序排序:

1
2
3
4
5
6
7
8
9
10
dd <- data.frame(b = factor(c("Hi","Med","Hi","Low"),
      levels = c("Low","Med","Hi"), ordered = TRUE),
      x = c("A","D","A","C"), y = c(8, 3, 9, 9),
      z = c(1, 1, 1, 2))
dd
    b x y z
1  Hi A 8 1
2 Med D 3 1
3  Hi A 9 1
4 Low C 9 2


您可以直接使用order()函数,而不必使用附加工具——请看这个简单的答案,它使用了example(order)代码顶部的一个技巧:

1
2
3
4
5
6
R> dd[with(dd, order(-z, b)), ]
    b x y z
4 Low C 9 2
2 Med D 3 1
1  Hi A 8 1
3  Hi A 9 1

编辑大约2年后:刚刚有人问如何通过列索引来实现这一点。答案是简单地将所需的排序列传递给order()函数:

1
2
3
4
5
6
7
R> dd[order(-dd[,4], dd[,1]), ]
    b x y z
4 Low C 9 2
2 Med D 3 1
1  Hi A 8 1
3  Hi A 9 1
R>

而不是使用列的名称(和with()以便更容易/更直接地访问)。


你的选择

  • 来自baseorder
  • 来自dplyrarrange
  • setordersetorderv来自data.table
  • 来自plyrarrange
  • 来自taRifxsort
  • 来自doByorderBy
  • 来自DeducersortData

大多数情况下,您应该使用dplyrdata.table解决方案,除非没有依赖关系很重要,在这种情况下,使用base::order

我最近在cran包中添加了sort.data.frame,使其与这里讨论的类兼容:为sort.data.frame创建通用/方法一致性的最佳方法?

因此,给定data.frame dd,您可以按以下方式排序:

1
2
3
4
5
6
dd <- data.frame(b = factor(c("Hi","Med","Hi","Low"),
      levels = c("Low","Med","Hi"), ordered = TRUE),
      x = c("A","D","A","C"), y = c(8, 3, 9, 9),
      z = c(1, 1, 1, 2))
library(taRifx)
sort(dd, f= ~ -z + b )

如果您是此功能的原始作者之一,请与我联系。关于公共领域的讨论如下:http://chat.stackoverflow.com/script/message/1094290 1094290

您也可以使用来自plyrarrange()函数,正如Hadley在上述线程中指出的那样:

1
2
library(plyr)
arrange(dd,desc(z),b)

基准测试:请注意,我在新的R会话中加载了每个包,因为有很多冲突。尤其是加载doby包会导致sort返回"以下对象被屏蔽在‘x(位置17)’:b、x、y、z",并且加载演绎器包会覆盖kevin wright或tarifx包的sort.data.frame

1
2
3
4
5
6
7
8
9
10
11
12
#Load each time
dd <- data.frame(b = factor(c("Hi","Med","Hi","Low"),
      levels = c("Low","Med","Hi"), ordered = TRUE),
      x = c("A","D","A","C"), y = c(8, 3, 9, 9),
      z = c(1, 1, 1, 2))
library(microbenchmark)

# Reload R between benchmarks
microbenchmark(dd[with(dd, order(-z, b)), ] ,
    dd[order(-dd$z, dd$b),],
    times=1000
)

中位数:

江东十一〔41〕778

埃多克斯1〔42〕788

1
2
library(taRifx)
microbenchmark(sort(dd, f= ~-z+b ),times=1000)

中位数时间:1567

1
2
library(plyr)
microbenchmark(arrange(dd,desc(z),b),times=1000)

中位数时间:862

1
2
library(doBy)
microbenchmark(orderBy(~-z+b, data=dd),times=1000)

中位数时间:1694

请注意,多比需要很长的时间来加载包。

1
2
library(Deducer)
microbenchmark(sortData(dd,c("z","b"),increasing= c(FALSE,TRUE)),times=1000)

无法进行演绎加载。需要Jgr控制台。

1
2
3
4
5
6
7
8
esort <- function(x, sortvar, ...) {
attach(x)
x <- x[with(x,order(sortvar,...)),]
return(x)
detach(x)
}

microbenchmark(esort(dd, -z, b),times=1000)

由于附加/分离,似乎与微基准不兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
m <- microbenchmark(
  arrange(dd,desc(z),b),
  sort(dd, f= ~-z+b ),
  dd[with(dd, order(-z, b)), ] ,
  dd[order(-dd$z, dd$b),],
  times=1000
  )

uq <- function(x) { fivenum(x)[4]}  
lq <- function(x) { fivenum(x)[2]}

y_min <- 0 # min(by(m$time,m$expr,lq))
y_max <- max(by(m$time,m$expr,uq)) * 1.05

p <- ggplot(m,aes(x=expr,y=time)) + coord_cartesian(ylim = c( y_min , y_max ))
p + stat_summary(fun.y=median,fun.ymin = lq, fun.ymax = uq, aes(fill=expr))

microbenchmark plot

(线条从下四分位数延伸到上四分位数,圆点是中间值)

考虑到这些结果和称重的简单性和速度,我不得不在plyr包中向arrange表示赞同。它有一个简单的语法,但其速度几乎与基R命令的复杂加工速度相同。典型的出色的哈德利·威克姆作品。我唯一的不满是它打破了标准的R命名法,分类对象由sort(object)调用,但我理解为什么哈德利这样做是因为在上面链接的问题中讨论的问题。


德克的回答很好。它还强调了索引data.frames和data.tables所用语法的一个关键区别:

1
2
3
4
5
## The data.frame way
dd[with(dd, order(-z, b)), ]

## The data.table way: (7 fewer characters, but that's not the important bit)
dd[order(-z, b)]

这两个电话之间的差别很小,但可能会产生重要的后果。尤其是如果您编写生产代码和/或关心您的研究中的正确性,最好避免不必要的变量名重复。data.table帮助你做到这一点。

下面是一个例子,说明变量名的重复可能会给您带来麻烦:

让我们从德克的回答中改变上下文,说这是一个更大的项目的一部分,其中有很多对象名,它们很长而且有意义;而不是dd,它被称为quarterlyreport。它变成:

1
quarterlyreport[with(quarterlyreport,order(-z,b)),]

好的,很好。没什么问题。接下来,你的老板要求你在报告中包括上个季度的报告。你通过你的代码,在不同的地方添加一个对象lastquarterlyreport,不知何故(在地球上如何?)你最终会得到这样的结果:

1
quarterlyreport[with(lastquarterlyreport,order(-z,b)),]

这不是你的意思,但你没有发现它,因为你做得很快,而且它依偎在一页类似的代码上。代码不会失效(没有警告也没有错误),因为R认为这就是您的意思。你希望阅读你的报告的人能发现它,但也许他们不会。如果你经常使用编程语言,那么这种情况可能是大家都熟悉的。你会说这是个"打字错误"。我会改正你对你老板说的"打字错误"。

data.table中,我们关心的是这样的小细节。所以我们做了一些简单的事情来避免两次输入变量名。非常简单的事情。i已经在dd的框架内自动进行了评估。你根本不需要with()

而不是

1
dd[with(dd, order(-z, b)), ]

只是

1
dd[order(-z, b)]

而不是

1
quarterlyreport[with(lastquarterlyreport,order(-z,b)),]

只是

1
quarterlyreport[order(-z,b)]

这是一个很小的区别,但有一天它可能会挽救你的脖子。在权衡这个问题的不同答案时,考虑将变量名的重复次数作为决定的标准之一。有些答案有很多重复,有些则没有。


这里有很多很好的答案,但是dplyr给出了我能快速且容易记住的唯一语法(因此现在经常使用):

1
2
3
4
5
library(dplyr)
# sort mtcars by mpg, ascending... use desc(mpg) for descending
arrange(mtcars, mpg)
# sort mtcars first by mpg, then by cyl, then by wt)
arrange(mtcars , mpg, cyl, wt)

对于OP的问题:

1
2
3
4
5
6
7
arrange(dd, desc(z),  b)

    b x y z
1 Low C 9 2
2 Med D 3 1
3  Hi A 8 1
4  Hi A 9 1


R包data.table提供了快速和内存高效的数据表排序,语法简单(Matt在他的答案中很好地强调了其中的一部分)。从那以后,有了很多改进,也有了一个新的功能setorder()。从v1.9.5+中,setorder()也可用于data.frames。

首先,我们将创建一个足够大的数据集,并对其他答案中提到的不同方法进行基准测试,然后列出data.table的特性。

数据:

1
2
3
4
5
6
7
8
9
10
11
12
require(plyr)
require(doBy)
require(data.table)
require(dplyr)
require(taRifx)

set.seed(45L)
dat = data.frame(b = as.factor(sample(c("Hi","Med","Low"), 1e8, TRUE)),
                 x = sample(c("A","D","C"), 1e8, TRUE),
                 y = sample(100, 1e8, TRUE),
                 z = sample(5, 1e8, TRUE),
                 stringsAsFactors = FALSE)

Benchmarks:

报告的计时来自于在下面显示的这些函数上运行system.time(...)。时间安排如下表所示(按最慢到最快的顺序)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
orderBy( ~ -z + b, data = dat)     ## doBy
plyr::arrange(dat, desc(z), b)     ## plyr
arrange(dat, desc(z), b)           ## dplyr
sort(dat, f = ~ -z + b)            ## taRifx
dat[with(dat, order(-z, b)), ]     ## base R

# convert to data.table, by reference
setDT(dat)

dat[order(-z, b)]                  ## data.table, base R like syntax
setorder(dat, -z, b)               ## data.table, using setorder()
                                   ## setorder() now also works with data.frames

# R-session memory usage (BEFORE) = ~2GB (size of 'dat')
# ------------------------------------------------------------
# Package      function    Time (s)  Peak memory   Memory used
# ------------------------------------------------------------
# doBy          orderBy      409.7        6.7 GB        4.7 GB
# taRifx           sort      400.8        6.7 GB        4.7 GB
# plyr          arrange      318.8        5.6 GB        3.6 GB
# base R          order      299.0        5.6 GB        3.6 GB
# dplyr         arrange       62.7        4.2 GB        2.2 GB
# ------------------------------------------------------------
# data.table      order        6.2        4.2 GB        2.2 GB
# data.table   setorder        4.5        2.4 GB        0.4 GB
# ------------------------------------------------------------
  • data.tableDT[order(...)]语法比其他方法(dplyr的最快)快约10倍,同时消耗的内存量与dplyr相同。

  • data.tablesetorder()比其它方法(dplyr快约14倍,而只占用0.4Gb的额外内存。dat现在符合我们要求的顺序(通过引用更新)。

data.table特性:

速度:

  • 表的排序非常快,因为它实现了基数排序。

  • 语法DT[order(...)]在内部进行了优化以使用数据。表的排序也很快。您可以继续使用熟悉的baser语法,但可以加快进程(并使用较少的内存)。

记忆:

  • 大多数情况下,重新排序后不需要原始data.frame或data.table。也就是说,我们通常将结果分配回相同的对象,例如:

    1
    DF <- DF[order(...)]

    问题是这至少需要原始对象的两倍(2x)内存。因此,为了节省内存,data.table还提供了一个函数setorder()

    setorder()重新排序数据。表by reference已就位,无需制作任何额外副本。它只使用等于一列大小的额外内存。

其他特点:

  • 它支持integerlogicalnumericcharacter甚至bit64::integer64类型。


    Note that factor, Date, POSIXct etc.. classes are all integer/numeric types underneath with additional attributes and are therefore supported as well.

  • 在R基中,不能使用字符向量上的-按该列的降序排序。相反,我们必须使用-xtfrm(.)

    但是,在data.table中,我们可以这样做,例如,dat[order(-x)]setorder(dat, -x)


  • KevinWright的这个(非常有用的)功能发布在R wiki的Tips部分,很容易实现。

    1
    2
    3
    4
    5
    6
    sort(dd,by = ~ -z + b)
    #     b x y z
    # 4 Low C 9 2
    # 2 Med D 3 1
    # 1  Hi A 8 1
    # 3  Hi A 9 1


    或者你可以用包裹多比

    1
    2
    library(doBy)
    dd <- orderBy(~-z+b, data=dd)

    假设您有一个data.frameA,您希望使用名为x的降序列对其进行排序。调用排序后的data.framenewdata

    1
    newdata <- A[order(-A$x),]

    如果你想要升序,那就把"-"换成空白。你可以吃点像

    1
    newdata <- A[order(-A$x, A$y, -A$z),]

    其中xzdata.frameA中的一些列。这意味着按x降序、y升序和z降序对data.frameA排序。


    如果您自然而然地会遇到SQL,那么sqldf会按照codd的预期处理order by。


    或者,使用包演绎器

    1
    2
    library(Deducer)
    dd<- sortData(dd,c("z","b"),increasing= c(FALSE,TRUE))

    我用下面的例子了解了order,这让我困惑了很长一段时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    set.seed(1234)

    ID        = 1:10
    Age       = round(rnorm(10, 50, 1))
    diag      = c("Depression","Bipolar")
    Diagnosis = sample(diag, 10, replace=TRUE)

    data = data.frame(ID, Age, Diagnosis)

    databyAge = data[order(Age),]
    databyAge

    这个例子之所以有效是因为order是由vector Age进行排序,而不是由data frame data中名为Age的列进行排序。

    要了解这一点,请使用read.table创建一个相同的数据帧,该数据帧的列名略有不同,并且不使用上述任何向量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    my.data <- read.table(text = '

      id age  diagnosis
       1  49 Depression
       2  50 Depression
       3  51 Depression
       4  48 Depression
       5  50 Depression
       6  51    Bipolar
       7  49    Bipolar
       8  49    Bipolar
       9  49    Bipolar
      10  49 Depression

    ', header = TRUE)

    由于没有名为Age的矢量,因此order的上述线结构不再起作用:

    1
    databyage = my.data[order(age),]

    下一行工作是因为ordermy.data中的Age列进行排序。

    1
    databyage = my.data[order(my.data$age),]

    考虑到我对这个例子困惑了这么久,我认为这是值得发表的。如果这篇文章不适合这个线程,我可以删除它。

    编辑:2014年5月13日

    下面是按每列对数据帧进行排序而不指定列名的通用方法。下面的代码显示了如何从左到右或从右到左排序。如果每一列都是数字的,这是有效的。我没有尝试添加字符列。

    一两个月前,我在一个不同的网站上的一篇旧文章中找到了do.call代码,但这只是在经过广泛而困难的搜索之后发现的。我不确定我现在能重新安置那个职位。当前线程是在R中订购data.frame的第一个命中。所以,我认为我原来的do.call代码的扩展版本可能有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    set.seed(1234)

    v1  <- c(0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1)
    v2  <- c(0,0,0,0, 1,1,1,1, 0,0,0,0, 1,1,1,1)
    v3  <- c(0,0,1,1, 0,0,1,1, 0,0,1,1, 0,0,1,1)
    v4  <- c(0,1,0,1, 0,1,0,1, 0,1,0,1, 0,1,0,1)

    df.1 <- data.frame(v1, v2, v3, v4)
    df.1

    rdf.1 <- df.1[sample(nrow(df.1), nrow(df.1), replace = FALSE),]
    rdf.1

    order.rdf.1 <- rdf.1[do.call(order, as.list(rdf.1)),]
    order.rdf.1

    order.rdf.2 <- rdf.1[do.call(order, rev(as.list(rdf.1))),]
    order.rdf.2

    rdf.3 <- data.frame(rdf.1$v2, rdf.1$v4, rdf.1$v3, rdf.1$v1)
    rdf.3

    order.rdf.3 <- rdf.1[do.call(order, as.list(rdf.3)),]
    order.rdf.3


    为了响应在操作中添加的关于如何以编程方式排序的注释:

    使用dplyrdata.table

    1
    2
    library(dplyr)
    library(data.table)

    DPLYR

    只需使用arrange_,这是arrange的标准评估版本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    df1 <- tbl_df(iris)
    #using strings or formula
    arrange_(df1, c('Petal.Length', 'Petal.Width'))
    arrange_(df1, ~Petal.Length, ~Petal.Width)
        Source: local data frame [150 x 5]

       Sepal.Length Sepal.Width Petal.Length Petal.Width Species
              (dbl)       (dbl)        (dbl)       (dbl)  (fctr)
    1           4.6         3.6          1.0         0.2  setosa
    2           4.3         3.0          1.1         0.1  setosa
    3           5.8         4.0          1.2         0.2  setosa
    4           5.0         3.2          1.2         0.2  setosa
    5           4.7         3.2          1.3         0.2  setosa
    6           5.4         3.9          1.3         0.4  setosa
    7           5.5         3.5          1.3         0.2  setosa
    8           4.4         3.0          1.3         0.2  setosa
    9           5.0         3.5          1.3         0.3  setosa
    10          4.5         2.3          1.3         0.3  setosa
    ..          ...         ...          ...         ...     ...


    #Or using a variable
    sortBy <- c('Petal.Length', 'Petal.Width')
    arrange_(df1, .dots = sortBy)
        Source: local data frame [150 x 5]

       Sepal.Length Sepal.Width Petal.Length Petal.Width Species
              (dbl)       (dbl)        (dbl)       (dbl)  (fctr)
    1           4.6         3.6          1.0         0.2  setosa
    2           4.3         3.0          1.1         0.1  setosa
    3           5.8         4.0          1.2         0.2  setosa
    4           5.0         3.2          1.2         0.2  setosa
    5           4.7         3.2          1.3         0.2  setosa
    6           5.5         3.5          1.3         0.2  setosa
    7           4.4         3.0          1.3         0.2  setosa
    8           4.4         3.2          1.3         0.2  setosa
    9           5.0         3.5          1.3         0.3  setosa
    10          4.5         2.3          1.3         0.3  setosa
    ..          ...         ...          ...         ...     ...

    #Doing the same operation except sorting Petal.Length in descending order
    sortByDesc <- c('desc(Petal.Length)', 'Petal.Width')
    arrange_(df1, .dots = sortByDesc)

    更多信息请访问:https://cran.r-project.org/web/packages/dplyr/vignettes/nse.html

    最好使用公式,因为它还捕获环境以在

    数据表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    dt1 <- data.table(iris) #not really required, as you can work directly on your data.frame
    sortBy <- c('Petal.Length', 'Petal.Width')
    sortType <- c(-1, 1)
    setorderv(dt1, sortBy, sortType)
    dt1
         Sepal.Length Sepal.Width Petal.Length Petal.Width   Species
      1:          7.7         2.6          6.9         2.3 virginica
      2:          7.7         2.8          6.7         2.0 virginica
      3:          7.7         3.8          6.7         2.2 virginica
      4:          7.6         3.0          6.6         2.1 virginica
      5:          7.9         3.8          6.4         2.0 virginica
     ---                                                            
    146:          5.4         3.9          1.3         0.4    setosa
    147:          5.8         4.0          1.2         0.2    setosa
    148:          5.0         3.2          1.2         0.2    setosa
    149:          4.3         3.0          1.1         0.1    setosa
    150:          4.6         3.6          1.0         0.2    setosa

    Dirk的回答很好,但是如果需要保持排序,您将希望将排序应用到该数据帧的名称上。使用示例代码:

    1
    dd <- dd[with(dd, order(-z, b)), ]

    dplyer中的arrange()是我最喜欢的选项。使用管道操作员,从最不重要的方面到最重要的方面

    1
    2
    3
    dd1 <- dd %>%
        arrange(z) %>%
        arrange(desc(x))

    为了完整起见:您还可以使用BBmisc包中的sortByCol()功能:

    1
    2
    3
    4
    5
    6
    7
    library(BBmisc)
    sortByCol(dd, c("z","b"), asc = c(FALSE, TRUE))
        b x y z
    4 Low C 9 2
    2 Med D 3 1
    1  Hi A 8 1
    3  Hi A 9 1

    性能比较:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    library(microbenchmark)
    microbenchmark(sortByCol(dd, c("z","b"), asc = c(FALSE, TRUE)), times = 100000)
    median 202.878

    library(plyr)
    microbenchmark(arrange(dd,desc(z),b),times=100000)
    median 148.758

    microbenchmark(dd[with(dd, order(-z, b)), ], times = 100000)
    median 115.872


    就像很久以前的机械卡片分类机一样,首先按最不重要的键分类,然后按次最重要的键分类,等等。不需要库,可以使用任意数量的键和任何升序和降序键的组合。

    1
     dd <- dd[order(dd$b, decreasing = FALSE),]

    现在我们准备好做最重要的事情了。排序是稳定的,最重要的键中的任何关系都已解决。

    1
    dd <- dd[order(dd$z, decreasing = TRUE),]

    这可能不是最快的,但肯定是简单可靠的


    另一种选择,使用rgr包:

    1
    2
    3
    4
    5
    6
    7
    > library(rgr)
    > gx.sort.df(dd, ~ -z+b)
        b x y z
    4 Low C 9 2
    2 Med D 3 1
    1  Hi A 8 1
    3  Hi A 9 1

    只是为了完整性,因为关于按列号排序的讨论不多…可以肯定的是,这通常是不可取的(因为列的顺序可能会改变,从而导致错误),但在某些特定情况下(例如,当您需要快速完成一项工作,并且不存在列更改顺序的风险),这可能是最明智的做法,尤其是在处理大量的F列。

    在这种情况下,do.call()来救援:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    ind <- do.call(what ="order", args = iris[,c(5,1,2,3)])
    iris[ind, ]

    ##        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
    ##    48           4.6         3.2          1.4         0.2     setosa
    ##    7            4.6         3.4          1.4         0.3     setosa
    ##    (...)

    当我想自动化N列的排序过程时,我正努力使用上述解决方案,因为N列的列名每次都可能不同。我从psych包中找到了一个非常有用的功能,可以以一种简单的方式完成这项工作:

    1
    dfOrder(myDf, columnIndices)

    其中,columnIndices是一列或多列的索引,按您希望的排序顺序排列。更多信息:

    "psych"包中的dforder函数