如何通过对数据框列进行排序来快速形成分组(四分位数、十分位数等)?

95

我看到很多关于ordersort的问题和答案。是否有任何方法可以将向量或数据框按组进行排序(例如四分位数或十分位数)?我有一个“手动”解决方案,但可能有更好的经过群体测试的解决方案。

temp <- data.frame(name=letters[1:12], value=rnorm(12), quartile=rep(NA, 12))
temp
#    name       value quartile
# 1     a  2.55118169       NA
# 2     b  0.79755259       NA
# 3     c  0.16918905       NA
# 4     d  1.73359245       NA
# 5     e  0.41027113       NA
# 6     f  0.73012966       NA
# 7     g -1.35901658       NA
# 8     h -0.80591167       NA
# 9     i  0.48966739       NA
# 10    j  0.88856758       NA
# 11    k  0.05146856       NA
# 12    l -0.12310229       NA
temp.sorted <- temp[order(temp$value), ]
temp.sorted$quartile <- rep(1:4, each=12/4)
temp <- temp.sorted[order(as.numeric(rownames(temp.sorted))), ]
temp
#    name       value quartile
# 1     a  2.55118169        4
# 2     b  0.79755259        3
# 3     c  0.16918905        2
# 4     d  1.73359245        4
# 5     e  0.41027113        2
# 6     f  0.73012966        3
# 7     g -1.35901658        1
# 8     h -0.80591167        1
# 9     i  0.48966739        3
# 10    j  0.88856758        4
# 11    k  0.05146856        2
# 12    l -0.12310229        1

有更好的(更清晰/更快/一行代码)方法吗?谢谢!

11个回答

125

dplyr包中有一个方便的ntile函数。它非常灵活,因为你可以很容易地定义要创建的瓷砖或“箱子”的数量。

加载该包(如果尚未安装,请先安装),并添加四分位数列:

library(dplyr)
temp$quartile <- ntile(temp$value, 4)  

或者,如果你想要使用dplyr语法:

temp <- temp %>% mutate(quartile = ntile(value, 4))

两种情况下的结果都是:

temp
#   name       value quartile
#1     a -0.56047565        1
#2     b -0.23017749        2
#3     c  1.55870831        4
#4     d  0.07050839        2
#5     e  0.12928774        3
#6     f  1.71506499        4
#7     g  0.46091621        3
#8     h -1.26506123        1
#9     i -0.68685285        1
#10    j -0.44566197        2
#11    k  1.22408180        4
#12    l  0.35981383        3

数据:

请注意,您不需要提前创建“四分位”列,并使用set.seed使随机化可重现:

set.seed(123)
temp <- data.frame(name=letters[1:12], value=rnorm(12))

很好的替代方案,但你的回答缺少关于ntile使用的断点信息(包括最低、最高、并列)。 - EDC
3
那应该能解决端点的问题了吧? temp <- temp %>% mutate(quartile = cut(x = ntile(value, 100), breaks = seq(25,100,25) , include.lowest = TRUE, right = FALSE , labels = FALSE)) (注意:该翻译仅供参考,若需用于正式场合,请进行必要的校对和修改。) - hannes101

85

我使用的方法之一是以下其中一个或者Hmisc::cut2(value, g=4)

temp$quartile <- with(temp, cut(value, 
                                breaks=quantile(value, probs=seq(0,1, by=0.25), na.rm=TRUE), 
                                include.lowest=TRUE))

另一种选择可能是:

temp$quartile <- with(temp, factor(
                            findInterval( val, c(-Inf,
                               quantile(val, probs=c(0.25, .5, .75)), Inf) , na.rm=TRUE), 
                            labels=c("Q1","Q2","Q3","Q4")
      ))
第一个方法的副作用是用值标记四分位数,我认为这是一件“好事情”,但如果对您不好,或者评论中提出的有效问题是一个问题,那么您可以选择第二种方法。您可以在 `cut` 中使用 `labels=` ,或者您可以将以下代码添加到您的代码中:
temp$quartile <- factor(temp$quartile, levels=c("1","2","3","4") )

或者更快但稍微有些晦涩的方法是将其转换为数字向量,尽管这已经不再是一个因素,而是一个数字向量:

temp$quartile <- as.numeric(temp$quartile)

13
cut() 函数有一个名为 labels 的参数,可以用来避免使用 factor() 函数 - 只需在第一行的 cut() 调用中添加 labels = 1:4 即可。 - Gavin Simpson
3
Hmisc包中的cut2函数具有一个“m”参数,可将数据切割成大约相等的“m”个部分。 - IRTFM
1
我想补充一下,如果你对一个带有重复值的时间序列计算分位数,可能会出现错误:“'breaks' are not unique”。例如,最低的分位数(0%)可能等于稍高的分位数(10%)。在这种情况下,上面使用的findInterval函数似乎更好。 - user3032689
@42- 你能否建议如何处理分位数和带有NA值的数据。 - Aquarius
对于分位数使用 probs=c((0:9)/10), Inf) 结合 findInterval 函数,或者使用 probs=seq(0,1, by=0.1)) 结合 cut 函数。这两个函数之间一个重要的区别是默认情况下,findInterval 函数左闭右开,而 cut 函数左开右闭。关于 NA 的好处就像 sum、mean 或 max 一样,应该在使用 quantile 函数时添加 na.rm=TRUE 参数。 - IRTFM
显示剩余3条评论

28

我将为其他谷歌搜索的人添加data.table版本(即,将@BondedDust的解决方案翻译为data.table并略微减少了一些内容):

我会为其他搜寻data.table版本的人提供代码(也就是说,将@BondedDust的解决方案翻译成data.table并稍作修改):

library(data.table)
setDT(temp)
temp[ , quartile := cut(value,
                        breaks = quantile(value, probs = 0:4/4),
                        labels = 1:4, right = FALSE)]

这比我之前做的要好得多(更干净,更快):

temp[ , quartile := 
        as.factor(ifelse(value < quantile(value, .25), 1,
                         ifelse(value < quantile(value, .5), 2,
                                ifelse(value < quantile(value, .75), 3, 4))]

需要注意的是,此方法要求分位数必须不同,例如rep(0:1, c(100, 1))会失败;如果出现这种情况应该怎么做是开放性问题,所以由您自行决定。


4
顺便说一下,使用data.table版本是最快的方法。感谢@MichaelChirico。 - rafa.pereira
1
我认为这里的 right = F 是不正确的。不仅最大值没有分组,而且如果你的数据是 1:21,中位数是 11,但会被分到 .75 组中。 - 00schneider

14

通过适应 dplyr::ntile 以利用 data.table 的优化,可提供更快的解决方案。

library(data.table)
setDT(temp)
temp[order(value) , quartile := floor( 1 + 4 * (.I-1) / .N)]

可能不能算作清洁的方法,但它更快且只需一行。

在较大数据集上的时间

将此解决方案与@docendo_discimus和@MichaelChirico提出的data.tablentilecut进行比较。

library(microbenchmark)
library(dplyr)

set.seed(123)

n <- 1e6
temp <- data.frame(name=sample(letters, size=n, replace=TRUE), value=rnorm(n))
setDT(temp)

microbenchmark(
    "ntile" = temp[, quartile_ntile := ntile(value, 4)],
    "cut" = temp[, quartile_cut := cut(value,
                                       breaks = quantile(value, probs = seq(0, 1, by=1/4)),
                                       labels = 1:4, right=FALSE)],
    "dt_ntile" = temp[order(value), quartile_ntile_dt := floor( 1 + 4 * (.I-1)/.N)]
)

给出:

Unit: milliseconds
     expr      min       lq     mean   median       uq      max neval
    ntile 608.1126 647.4994 670.3160 686.5103 691.4846 712.4267   100
      cut 369.5391 373.3457 375.0913 374.3107 376.5512 385.8142   100
 dt_ntile 117.5736 119.5802 124.5397 120.5043 124.5902 145.7894   100

11

您可以使用quantile()函数,但在使用cut()时需要处理四舍五入/精度问题。 因此

set.seed(123)
temp <- data.frame(name=letters[1:12], value=rnorm(12), quartile=rep(NA, 12))
brks <- with(temp, quantile(value, probs = c(0, 0.25, 0.5, 0.75, 1)))
temp <- within(temp, quartile <- cut(value, breaks = brks, labels = 1:4, 
                                     include.lowest = TRUE))

给定:

> head(temp)
  name       value quartile
1    a -0.56047565        1
2    b -0.23017749        2
3    c  1.55870831        4
4    d  0.07050839        2
5    e  0.12928774        3
6    f  1.71506499        4

8

对不起,我来晚了一点。我想用cut2添加我的一行代码,因为我不知道数据的最大/最小值,并且希望分组具有相同的大小。我在标记为重复的问题中读到了关于cut2的信息(链接如下)。

library(Hmisc)   #For cut2
set.seed(123)    #To keep answers below identical to my random run

temp <- data.frame(name=letters[1:12], value=rnorm(12), quartile=rep(NA, 12))

temp$quartile <- as.numeric(cut2(temp$value, g=4))   #as.numeric to number the factors
temp$quartileBounds <- cut2(temp$value, g=4)

temp

结果:

> temp
   name       value quartile  quartileBounds
1     a -0.56047565        1 [-1.265,-0.446)
2     b -0.23017749        2 [-0.446, 0.129)
3     c  1.55870831        4 [ 1.224, 1.715]
4     d  0.07050839        2 [-0.446, 0.129)
5     e  0.12928774        3 [ 0.129, 1.224)
6     f  1.71506499        4 [ 1.224, 1.715]
7     g  0.46091621        3 [ 0.129, 1.224)
8     h -1.26506123        1 [-1.265,-0.446)
9     i -0.68685285        1 [-1.265,-0.446)
10    j -0.44566197        2 [-0.446, 0.129)
11    k  1.22408180        4 [ 1.224, 1.715]
12    l  0.35981383        3 [ 0.129, 1.224)

这是一个类似的问题,我在里面详细阅读了cut2的相关内容


1
temp$quartile <- ceiling(sapply(temp$value,function(x) sum(x-temp$value>=0))/(length(temp$value)/4))

1

当你的原始值聚集在某些数值上时,使用ntile()要小心。 为了创建大小相等的组,它会将具有相同原始值的行分配到不同的组中。这可能不是理想的。

我曾经遇到过这样一种情况,即个人的得分聚集在某些值上,重要的是将具有相同原始得分的个体放入同一组中(例如基于考试成绩为学生分组)。 ntile() 将具有相同得分的人分配到不同的组中(在这种情况下不公平),但是 cut() 与 quantile() 不会这样做(但是组的大小仅大约相等)。

library(dplyr)
library(reshape2)
library(ggplot2)


# awkward data: cannot be fairly and equally divided into quartiles or quintiles
# (similar results are obtained from more realistic cases of clustered values)
example <- data.frame(id = 1:49, x = c(rep(1:7, each=7))) %>%
  mutate(ntileQuartile = ntile(x, 4),
         cutQuartile = cut(x, breaks=quantile(x, seq(0, 1, by=1/4)),
                           include.lowest=T, label=1:4),
         ntileQuintile = ntile(x, 5),
         cutQuintile = cut(x, breaks=quantile(x, seq(0, 1, by=1/5)),
                           include.lowest=T, label=1:5))


# graph: x axis is original score, colour is group allocation
# ntile creates equal groups, but some values of original score are split
# into separate groups.  cut creates different sized groups, but score 
# exactly determines the group.
melt(example, id.vars=c("id", "x"), 
     variable.name = "method", value.name="groupNumber") %>%
  ggplot(aes(x, fill=groupNumber)) +
  geom_histogram(colour="black", bins=13) +
  facet_wrap(vars(method))

1
尝试使用这个函数。
getQuantileGroupNum <- function(vec, group_num, decreasing=FALSE) {
  if(decreasing) {
    abs(cut(vec, quantile(vec, probs=seq(0, 1, 1 / group_num), type=8, na.rm=TRUE), labels=FALSE, include.lowest=T) - group_num - 1)
  } else {
    cut(vec, quantile(vec, probs=seq(0, 1, 1 / group_num), type=8, na.rm=TRUE), labels=FALSE, include.lowest=T)
  }
}

> t1 <- runif(7)
> t1
[1] 0.4336094 0.2842928 0.5578876 0.2678694 0.6495285 0.3706474 0.5976223
> getQuantileGroupNum(t1, 4)
[1] 2 1 3 1 4 2 4
> getQuantileGroupNum(t1, 4, decreasing=T)
[1] 3 4 2 4 1 3 1

0

我想提出一个版本,这个版本似乎更加健壮,因为在我的数据集上使用cut()quantile()选项时遇到了很多问题。 我正在使用plyrntile函数,但它也可以使用ecdf作为输入。

temp[, `:=`(quartile = .bincode(x = ntile(value, 100), breaks = seq(0,100,25), right = TRUE, include.lowest = TRUE)
            decile = .bincode(x = ntile(value, 100), breaks = seq(0,100,10), right = TRUE, include.lowest = TRUE)
)]

temp[, `:=`(quartile = .bincode(x = ecdf(value)(value), breaks = seq(0,1,0.25), right = TRUE, include.lowest = TRUE)
            decile = .bincode(x = ecdf(value)(value), breaks = seq(0,1,0.1), right = TRUE, include.lowest = TRUE)
)]

这是正确的吗?


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