Cómo formar rápidamente grupos (cuartiles, deciles, etc.) ordenando columna (s) en un dataframe

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 bash:

 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 syntax 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)) 

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, más rápido ) 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.

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 

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 

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) 

Problema similar donde leo sobre cut2 en detalle

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

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?

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