Para cada linha retorna o nome da coluna do maior valor


97

Tenho uma lista de funcionários e preciso saber em que departamento eles estão com mais frequência. É trivial tabular o ID do funcionário em relação ao nome do departamento, mas é mais complicado retornar o nome do departamento, em vez do número de contagens de escala, da tabela de frequência. Um exemplo simples abaixo (nomes de colunas = departamentos, nomes de linhas = ids de funcionários).

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

Agora como faço para obter

> DF2
  RE
1 V3
2 V1
3 V2

qual o tamanho dos seus dados reais?
Arun

1
@Arun> dim (teste) [1] 26746 18
dmvianna

6
Uma generalização interessante seria os maiores nomes de coluna de n valores por linha
Hack-R

Respostas:


99

Uma opção de usar seus dados (para referência futura, use set.seed()para fazer exemplos usando samplereproduzíveis):

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"

Uma solução mais rápida do que usar applypode ser max.col:

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

... onde ties.methodpode ser qualquer um "random" "first"ou"last"

Isso obviamente causa problemas se acontecer de você ter duas colunas iguais ao máximo. Não tenho certeza do que você deseja fazer nesse caso, pois terá mais de um resultado para algumas linhas. Por exemplo:

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 

Se eu tiver duas colunas iguais, geralmente escolho apenas a primeira. São casos limítrofes que não perturbam minha análise estatística.
dmvianna

1
@dmvianna - usar which.maxestará bem então.
thelatemail

Presumo que a ordem seja preservada, então posso criar uma nova coluna com esse vetor que se alinhará corretamente aos IDs dos funcionários. Isso está correto?
dmvianna

applyconverte o data.framepara matrixinternamente. Você pode não ver uma diferença de desempenho nessas dimensões.
Arun

2
@PankajKaundal - assumindo valores distintos, que tal issocolnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
thelatemail

15

Se você estiver interessado em uma data.tablesolução, aqui está uma. É um pouco complicado, pois você prefere obter o id para o primeiro máximo. É muito mais fácil se você preferir o último máximo. No entanto, não é tão complicado e é rápido!

Aqui eu gerei dados de suas dimensões (26746 * 18).

Dados

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

data.table responda:

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"]

Avaliação comparativa:

# 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

É cerca de 11 vezes mais rápido com dados dessas dimensões e também pode ser data.tabledimensionado muito bem.


Editar: se qualquer um dos ids máximos estiver correto, então:

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"]

Na verdade, não me importo se é o primeiro ou o último máximo. Vou pela simplicidade primeiro, mas tenho certeza que uma solução data.table será útil no futuro, obrigado!
dmvianna

11

Uma solução poderia ser reformular a data de ampla para longa, colocando todos os departamentos em uma coluna e as contagens em outra, agrupar pelo id do empregador (neste caso, o número da linha) e, em seguida, filtrar para o (s) departamento (s) com o Valor máximo. Existem algumas opções para lidar com laços com essa abordagem também.

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.

11

Com base nas sugestões acima, a seguinte data.tablesolução funcionou muito rápido para mim:

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

E também vem com a vantagem de sempre poder especificar quais colunas .SDdevem ser consideradas, mencionando-as em .SDcols:

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

Caso precisemos do nome da coluna de menor valor, conforme sugerido por @lwshang, basta usar -.SD:

DT[, col_min := colnames(.SD)[max.col(-.SD, ties.method = "first")]]

Eu tinha um requisito semelhante, mas quero obter o nome da coluna com o valor mínimo para cada linha ... parece que não temos min.col em R ... você saberia qual seria a solução equivalente ?
user1412

Olá @ user1412. Obrigado pela sua pergunta interessante. Eu não tenho nenhuma ideia agora além de usar o which.minem algo que se pareça com: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]ou DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]nos dados fictícios acima. Isso não considera empates e retorna apenas o primeiro mínimo. Considere fazer outra pergunta. Também estaria curioso para saber quais outras respostas você obteria.
Valentin

1
Um truque para obter coluna mínimo está enviando o negativo do data.frame em max.col, como: colnames(.SD)[max.col(-.SD, ties.method="first")].
lwshang

6

Uma dplyrsolução:

Idéia:

  • adicionar rowids como uma coluna
  • remodelar para formato longo
  • filtrar para o máximo em 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

Essa abordagem pode ser facilmente estendida para obter as ncolunas superiores . Exemplo 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

1
Você poderia comentar a diferença entre essa abordagem e a resposta de sbha acima? Eles parecem iguais para mim.
Gregor Thomas

2

Um forloop simples também pode 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

1

Uma opção dplyr 1.0.0pode 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     

Dados de amostra:

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

0

Aqui está uma resposta que funciona com data.table e é mais simples. Isso pressupõe que seu data.table tem o nome yourDF:

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

Substitua ("V1", "V2", "V3", "V4")e (V1, V2, V3, V4)pelos nomes das suas colunas

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.