Para cada fila, devuelva el nombre de la columna del valor más grande

97

Tengo una lista de empleados y necesito saber en qué departamento se encuentran con más frecuencia. Es trivial tabular la identificación del empleado con el nombre del departamento, pero es más complicado devolver el nombre del departamento, en lugar del número de recuentos de la lista, de la tabla de frecuencia. Un ejemplo simple a continuación (nombres de columna = departamentos, nombres de fila = ID de empleado).

DF <- matrix(sample(1:9,9),ncol=3,nrow=3)
DF <- as.data.frame.matrix(DF)
> DF
  V1 V2 V3
1  2  7  9
2  8  3  6
3  1  5  4

Ahora como consigo

> DF2
  RE
1 V3
2 V1
3 V2
dmvianna
fuente
¿Qué tan grandes son sus datos reales?
Arun
1
@Arun> dim (prueba) [1] 26746 18
dmvianna
6
Una generalización interesante serían los nombres de columna de los valores n más grandes por fila
Hack-R

Respuestas:

99

Una opción usando sus datos (para referencia futura, use set.seed()para hacer ejemplos usando samplereproducibles):

DF <- data.frame(V1=c(2,8,1),V2=c(7,3,5),V3=c(9,6,4))

colnames(DF)[apply(DF,1,which.max)]
[1] "V3" "V1" "V2"

Una solución más rápida que usar applypodría ser max.col:

colnames(DF)[max.col(DF,ties.method="first")]
#[1] "V3" "V1" "V2"

... donde ties.methodpuede estar cualquiera de "random" "first"o"last"

Esto, por supuesto, causa problemas si tiene dos columnas que son iguales al máximo. No estoy seguro de lo que quiere hacer en esa instancia, ya que tendrá más de un resultado para algunas filas. P.ej:

DF <- data.frame(V1=c(2,8,1),V2=c(7,3,5),V3=c(7,6,4))
apply(DF,1,function(x) which(x==max(x)))

[[1]]
V2 V3 
 2  3 

[[2]]
V1 
 1 

[[3]]
V2 
 2 
thelatemail
fuente
Si tengo dos columnas iguales, normalmente elijo la primera. Estos son casos fronterizos que no perturban mi análisis estadístico.
dmvianna
1
@dmvianna - entonces el uso which.maxestará bien.
thelatemail
Supongo que el orden se conserva, por lo que puedo crear una nueva columna con este vector que se alineará correctamente con las ID de los empleados. ¿Es eso correcto?
dmvianna
applyconvierte el data.framea matrixinternamente. Sin embargo, es posible que no vea una diferencia de rendimiento en estas dimensiones.
Arun
2
@PankajKaundal - asumiendo valores distintos, ¿qué tal esto?colnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
thelatemail
15

Si está interesado en una data.tablesolución, aquí tiene una. Es un poco complicado ya que prefiere obtener la identificación para el primer máximo. Es mucho más fácil si prefiere el último máximo. Sin embargo, ¡no es tan complicado y es rápido!

Aquí he generado datos de sus dimensiones (26746 * 18).

Datos

set.seed(45)
DF <- data.frame(matrix(sample(10, 26746*18, TRUE), ncol=18))

data.table responder:

require(data.table)
DT <- data.table(value=unlist(DF, use.names=FALSE), 
            colid = 1:nrow(DF), rowid = rep(names(DF), each=nrow(DF)))
setkey(DT, colid, value)
t1 <- DT[J(unique(colid), DT[J(unique(colid)), value, mult="last"]), rowid, mult="first"]

Benchmarking:

# data.table solution
system.time({
DT <- data.table(value=unlist(DF, use.names=FALSE), 
            colid = 1:nrow(DF), rowid = rep(names(DF), each=nrow(DF)))
setkey(DT, colid, value)
t1 <- DT[J(unique(colid), DT[J(unique(colid)), value, mult="last"]), rowid, mult="first"]
})
#   user  system elapsed 
#  0.174   0.029   0.227 

# apply solution from @thelatemail
system.time(t2 <- colnames(DF)[apply(DF,1,which.max)])
#   user  system elapsed 
#  2.322   0.036   2.602 

identical(t1, t2)
# [1] TRUE

Es aproximadamente 11 veces más rápido en datos de estas dimensiones y también data.tableescala bastante bien.


Editar: si alguno de los ID máximos está bien, entonces:

DT <- data.table(value=unlist(DF, use.names=FALSE), 
            colid = 1:nrow(DF), rowid = rep(names(DF), each=nrow(DF)))
setkey(DT, colid, value)
t1 <- DT[J(unique(colid)), rowid, mult="last"]
Arun
fuente
De hecho, no me importa si es el primer o el último máximo. Primero voy por la simplicidad, pero estoy seguro de que una solución de data.table será útil en el futuro, ¡gracias!
dmvianna
11

Una solución podría ser remodelar la fecha de amplia a larga colocando todos los departamentos en una columna y los recuentos en otra, agrupar por la identificación del empleador (en este caso, el número de fila) y luego filtrar a los departamentos con el valor máximo. También hay un par de opciones para manejar los vínculos con este enfoque.

library(tidyverse)

# sample data frame with a tie
df <- data_frame(V1=c(2,8,1),V2=c(7,3,5),V3=c(9,6,5))

# If you aren't worried about ties:  
df %>% 
  rownames_to_column('id') %>%  # creates an ID number
  gather(dept, cnt, V1:V3) %>% 
  group_by(id) %>% 
  slice(which.max(cnt)) 

# A tibble: 3 x 3
# Groups:   id [3]
  id    dept    cnt
  <chr> <chr> <dbl>
1 1     V3       9.
2 2     V1       8.
3 3     V2       5.


# If you're worried about keeping ties:
df %>% 
  rownames_to_column('id') %>%
  gather(dept, cnt, V1:V3) %>% 
  group_by(id) %>% 
  filter(cnt == max(cnt)) %>% # top_n(cnt, n = 1) also works
  arrange(id)

# A tibble: 4 x 3
# Groups:   id [3]
  id    dept    cnt
  <chr> <chr> <dbl>
1 1     V3       9.
2 2     V1       8.
3 3     V2       5.
4 3     V3       5.


# If you're worried about ties, but only want a certain department, you could use rank() and choose 'first' or 'last'
df %>% 
  rownames_to_column('id') %>%
  gather(dept, cnt, V1:V3) %>% 
  group_by(id) %>% 
  mutate(dept_rank  = rank(-cnt, ties.method = "first")) %>% # or 'last'
  filter(dept_rank == 1) %>% 
  select(-dept_rank) 

# A tibble: 3 x 3
# Groups:   id [3]
  id    dept    cnt
  <chr> <chr> <dbl>
1 2     V1       8.
2 3     V2       5.
3 1     V3       9.

# if you wanted to keep the original wide data frame
df %>% 
  rownames_to_column('id') %>%
  left_join(
    df %>% 
      rownames_to_column('id') %>%
      gather(max_dept, max_cnt, V1:V3) %>% 
      group_by(id) %>% 
      slice(which.max(max_cnt)), 
    by = 'id'
  )

# A tibble: 3 x 6
  id       V1    V2    V3 max_dept max_cnt
  <chr> <dbl> <dbl> <dbl> <chr>      <dbl>
1 1        2.    7.    9. V3            9.
2 2        8.    3.    6. V1            8.
3 3        1.    5.    5. V2            5.
sbha
fuente
11

Según las sugerencias anteriores, la siguiente data.tablesolución funcionó muy rápido para mí:

library(data.table)

set.seed(45)
DT <- data.table(matrix(sample(10, 10^7, TRUE), ncol=10))

system.time(
  DT[, col_max := colnames(.SD)[max.col(.SD, ties.method = "first")]]
)
#>    user  system elapsed 
#>    0.15    0.06    0.21
DT[]
#>          V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 col_max
#>       1:  7  4  1  2  3  7  6  6  6   1      V1
#>       2:  4  6  9 10  6  2  7  7  1   3      V4
#>       3:  3  4  9  8  9  9  8  8  6   7      V3
#>       4:  4  8  8  9  7  5  9  2  7   1      V4
#>       5:  4  3  9 10  2  7  9  6  6   9      V4
#>      ---                                       
#>  999996:  4  6 10  5  4  7  3  8  2   8      V3
#>  999997:  8  7  6  6  3 10  2  3 10   1      V6
#>  999998:  2  3  2  7  4  7  5  2  7   3      V4
#>  999999:  8 10  3  2  3  4  5  1  1   4      V2
#> 1000000: 10  4  2  6  6  2  8  4  7   4      V1

Y también viene con la ventaja de que siempre se puede especificar qué columnas se .SDdeben considerar mencionándolas en .SDcols:

DT[, MAX2 := colnames(.SD)[max.col(.SD, ties.method="first")], .SDcols = c("V9", "V10")]

En caso de que necesitemos el nombre de columna del valor más pequeño, como lo sugiere @lwshang, solo necesita usar -.SD:

DT[, col_min := colnames(.SD)[max.col(-.SD, ties.method = "first")]]
Valentin
fuente
Tenía un requisito similar, pero quiero que el nombre de la columna tenga el valor mínimo para cada fila ... parece que no tenemos min.col en R ... ¿Sabrías cuál sería la solución equivalente? ?
user1412
Hola @ user1412. Gracias por tu interesante pregunta. No tengo ninguna idea en este momento más que usar which.minen algo que se vería así: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]o DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]en los datos ficticios de arriba. Esto no considera empates y devoluciones solo el primer mínimo. Tal vez considere hacer una pregunta separada. También tendría curiosidad por saber qué otras respuestas obtendría.
Valentin
1
Un truco para conseguir la columna mínimo está enviando el negativo de la hoja.de.datos en max.col, como: colnames(.SD)[max.col(-.SD, ties.method="first")].
lwshang
6

Una dplyrsolucion:

Idea:

  • agregar filas como una columna
  • remodelar a formato largo
  • filtrar por máximo en cada grupo

Código:

DF = data.frame(V1=c(2,8,1),V2=c(7,3,5),V3=c(9,6,4))
DF %>% 
  rownames_to_column() %>%
  gather(column, value, -rowname) %>%
  group_by(rowname) %>% 
  filter(rank(-value) == 1) 

Resultado:

# A tibble: 3 x 3
# Groups:   rowname [3]
  rowname column value
  <chr>   <chr>  <dbl>
1 2       V1         8
2 3       V2         5
3 1       V3         9

Este enfoque se puede ampliar fácilmente para obtener las ncolumnas superiores . Ejemplo para n=2:

DF %>% 
  rownames_to_column() %>%
  gather(column, value, -rowname) %>%
  group_by(rowname) %>% 
  mutate(rk = rank(-value)) %>%
  filter(rk <= 2) %>% 
  arrange(rowname, rk) 

Resultado:

# A tibble: 6 x 4
# Groups:   rowname [3]
  rowname column value    rk
  <chr>   <chr>  <dbl> <dbl>
1 1       V3         9     1
2 1       V2         7     2
3 2       V1         8     1
4 2       V3         6     2
5 3       V2         5     1
6 3       V3         4     2
Gregor Sturm
fuente
1
¿Podría comentar sobre la diferencia entre este enfoque y la respuesta de sbha anterior? A mí me parecen iguales.
Gregor Thomas
2

Un forbucle simple también puede ser útil:

> df<-data.frame(V1=c(2,8,1),V2=c(7,3,5),V3=c(9,6,4))
> df
  V1 V2 V3
1  2  7  9
2  8  3  6
3  1  5  4
> df2<-data.frame()
> for (i in 1:nrow(df)){
+   df2[i,1]<-colnames(df[which.max(df[i,])])
+ }
> df2
  V1
1 V3
2 V1
3 V2
rar
fuente
1

Una opción de dplyr 1.0.0podría ser:

DF %>%
 rowwise() %>%
 mutate(row_max = names(.)[which.max(c_across(everything()))])

     V1    V2    V3 row_max
  <dbl> <dbl> <dbl> <chr>  
1     2     7     9 V3     
2     8     3     6 V1     
3     1     5     4 V2     

Data de muestra:

DF <- structure(list(V1 = c(2, 8, 1), V2 = c(7, 3, 5), V3 = c(9, 6, 
4)), class = "data.frame", row.names = c(NA, -3L))
tmfmnk
fuente
0

Aquí hay una respuesta que funciona con data.table y es más simple. Esto asume que su data.table se llama yourDF:

j1 <- max.col(yourDF[, .(V1, V2, V3, V4)], "first")
yourDF$newCol <- c("V1", "V2", "V3", "V4")[j1]

Reemplazar ("V1", "V2", "V3", "V4")y (V1, V2, V3, V4)con los nombres de sus columnas

Aprendiendo estadísticas por ejemplo
fuente