sorting - tables - sort by 2 columns r
Cómo formar rápidamente grupos(cuartiles, deciles, etc.) ordenando columna(s) en un marco de datos (9)
Veo muchas preguntas y respuestas order
sort
y sort
. ¿Hay algo que clasifique vectores o marcos de datos en agrupaciones (como cuartiles o deciles)? Tengo una solución "manual", pero es probable que haya una mejor solución que haya sido probada en grupo.
Aquí está mi intento:
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
¿Hay un enfoque mejor (más limpio / más rápido / una línea)? ¡Gracias!
El método que uso es uno de estos o 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))
Un alternativo puede ser:
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")
))
El primero tiene el efecto secundario de etiquetar los cuartiles con los valores, que considero "algo bueno", pero si no fuera "bueno para ti", o los problemas válidos planteados en los comentarios eran una preocupación que podrías abordar. con la versión 2. Puede usar labels=
in cut
, o puede agregar esta línea a su código:
temp$quartile <- factor(temp$quartile, levels=c("1","2","3","4") )
O incluso más rápido pero un poco más oscuro en la forma en que funciona, aunque ya no es un factor, sino más bien un vector numérico:
temp$quartile <- as.numeric(temp$quartile)
Hay una función ntile
útil en el paquete dplyr
. Es flexible en el sentido de que puede definir fácilmente la cantidad de * mosaicos o "contenedores" que desea crear.
Cargue el paquete (instálelo primero si no lo hizo) y agregue la columna de cuartil:
library(dplyr)
temp$quartile <- ntile(temp$value, 4)
O bien, si desea usar la sintaxis de dplyr:
temp <- temp %>% mutate(quartile = ntile(value, 4))
El resultado en ambos casos es:
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
datos:
Tenga en cuenta que no necesita crear la columna "cuartil" de antemano y usar set.seed
para hacer la aleatorización reproducible:
set.seed(123)
temp <- data.frame(name=letters[1:12], value=rnorm(12))
La adaptación de dplyr::ntile
para aprovechar las optimizaciones de data.table
brinda una solución más rápida.
library(data.table)
setDT(temp)
temp[order(value) , quartile := floor( 1 + 4 * (.I-1) / .N)]
Probablemente no califica como más limpio, pero es más rápido y de una sola línea.
Tiempo en un conjunto de datos más grande
Comparando esta solución con ntile
y cut
para data.table
según lo propuesto por @docendo_discimus y @MichaelChirico.
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)]
)
Da:
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
Me gustaría proponer una versión, que parece ser más robusta, ya que tuve muchos problemas al usar quantile()
en el cut()
opción de cut()
en mi conjunto de datos. Estoy usando la función ntile
de plyr
, pero también funciona con ecdf
como entrada.
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)
)]
¿Es eso correcto?
Perdón por llegar un poco tarde a la fiesta. Quería agregar mi trazador de líneas usando cut2
ya que no sabía max / min para mis datos y quería que los grupos fueran idénticamente grandes. Leí acerca de cut2 en un problema que fue marcado como duplicado (enlace a continuación).
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
Resultado:
> 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)
Posiblemente hay una manera más rápida, pero yo haría:
a <- rnorm(100) # Our data
q <- quantile(a) # You can supply your own breaks, see ?quantile
# Define a simple function that checks in which quantile a number falls
getQuant <- function(x)
{
for (i in 1:(length(q)-1))
{
if (x>=q[i] && x<q[i+1])
break;
}
i
}
# Apply the function to the data
res <- unlist(lapply(as.matrix(a), getQuant))
Puede usar la función quantile()
, pero necesita manejar el redondeo / precisión al usar cut()
. Asi que
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))
Dando:
> 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
data.table
versión de data.table
para que alguien más la data.table
en Google (es decir, la solución de @ BondedDust se tradujo a data.table
y se redujo un poco):
library(data.table)
setDT(temp)
temp[ , quartile := cut(value,
breaks = quantile(value, probs = 0:4/4),
labels = 1:4, right = FALSE)]
Que es mucho mejor (más limpio, faster ) de lo que había estado haciendo:
temp[ , quartile :=
as.factor(ifelse(value < quantile(value, .25), 1,
ifelse(value < quantile(value, .5), 2,
ifelse(value < quantile(value, .75), 3, 4))]
Tenga en cuenta, sin embargo, que este enfoque requiere que los cuantiles sean distintos, por ejemplo, fallará en la rep(0:1, c(100, 1))
; qué hacer en este caso es de final abierto, así que te lo dejo a ti.
temp$quartile <- ceiling(sapply(temp$value,function(x) sum(x-temp$value>=0))/(length(temp$value)/4))