Per ogni riga restituisci il nome della colonna del valore più grande


97

Ho un elenco di dipendenti e ho bisogno di sapere in quale reparto si trovano più spesso. È banale tabulare l'ID del dipendente rispetto al nome del reparto, ma è più complicato restituire il nome del reparto, piuttosto che il numero di conteggi del registro, dalla tabella delle frequenze. Un semplice esempio di seguito (nomi di colonne = reparti, nomi di righe = ID dipendenti).

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

Ora come ottengo

> DF2
  RE
1 V3
2 V1
3 V2

quanto sono grandi i tuoi dati effettivi?
Arun

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

6
Una generalizzazione interessante sarebbe i nomi delle colonne dei valori n più grandi per riga
Hack-R

Risposte:


99

Un'opzione che utilizza i tuoi dati (per riferimento futuro, utilizza set.seed()per fare esempi utilizzando sampleriproducibile):

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 soluzione più veloce dell'utilizzo applypotrebbe essere max.col:

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

... dove ties.methodpuò essere uno qualsiasi di "random" "first"o"last"

Questo ovviamente causa problemi se ti capita di avere due colonne che sono uguali al massimo. Non sono sicuro di cosa vuoi fare in quel caso poiché avrai più di un risultato per alcune righe. Per esempio:

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 ho due colonne uguali di solito scelgo solo la prima. Sono casi limite che non sconvolgono la mia analisi statistica.
dmvianna

1
@dmvianna - l'utilizzo which.maxandrà bene allora.
recapito

Presumo che l'ordine sia mantenuto, quindi posso creare una nuova colonna con questo vettore che si allineerà correttamente agli ID dei dipendenti. È corretto?
dmvianna

applyconverte il data.framein matrixinternamente. Tuttavia, potresti non notare una differenza di prestazioni su queste dimensioni.
Arun

2
@PankajKaundal - assumendo valori distinti, che ne dici di questocolnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
thelatemail

15

Se sei interessato a un file data.table soluzione, eccone una. È un po 'complicato poiché preferisci ottenere l'id per il primo massimo. È molto più facile se preferisci l'ultimo massimo. Tuttavia, non è così complicato ed è veloce!

Qui ho generato i dati delle tue dimensioni (26746 * 18).

Dati

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

data.table risposta:

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

Analisi 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

È circa 11 volte più veloce con i dati di queste dimensioni e si data.tableridimensiona abbastanza bene.


Modifica: se uno qualsiasi degli ID massimi va bene, allora:

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

In realtà non mi interessa se è il primo o l'ultimo massimo. Vado innanzitutto per la semplicità, ma sono sicuro che una soluzione data.table tornerà utile in futuro, grazie!
dmvianna

11

Una soluzione potrebbe essere quella di rimodellare la data da ampia a lunga mettendo tutti i reparti in una colonna e conteggia in un'altra, raggruppandoli per id datore di lavoro (in questo caso, il numero di riga), quindi filtrando i reparti con il valore massimo. Ci sono anche un paio di opzioni per gestire i legami con questo approccio.

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

Sulla base dei suggerimenti di cui sopra, la seguente data.tablesoluzione ha funzionato molto velocemente per me:

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 ha anche il vantaggio di poter sempre specificare quali colonne .SDdovrebbero considerare menzionandole in .SDcols:

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

Nel caso in cui abbiamo bisogno del nome della colonna del valore più piccolo, come suggerito da @lwshang, è sufficiente utilizzare -.SD:

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

Avevo un requisito simile ma voglio ottenere il nome della colonna con il valore minimo per ogni riga ..... non sembra avere min.col in R ..... sapresti quale sarebbe la soluzione equivalente ?
user1412

Ciao @ user1412. Grazie per la tua interessante domanda. Non ho alcuna idea in questo momento oltre a utilizzare which.minin qualcosa che potrebbe assomigliare: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]o DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]sui dati fittizi sopra. Questo non considera i legami e restituisce solo il primo minimo. Forse considera di porre una domanda separata. Sarei anche curioso di sapere quali altre risposte potresti ottenere.
Valentin

1
Un trucco per ottenere colonna minima sta inviando il negativo della data.frame in max.col, come: colnames(.SD)[max.col(-.SD, ties.method="first")].
lwshang

6

Una dplyrsoluzione:

Idea:

  • aggiungi rowids come colonna
  • rimodellare in formato lungo
  • filtro per max in ogni gruppo

Codice:

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) 

Risultato:

# 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

Questo approccio può essere facilmente esteso per ottenere le prime ncolonne. Esempio per n=2:

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

Risultato:

# 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
Potresti commentare la differenza tra questo approccio e la risposta di sbha sopra? A me sembrano più o meno lo stesso.
Gregor Thomas

2

Anche un semplice forloop può essere utile:

> 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

Un'opzione da dplyr 1.0.0potrebbe essere:

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     

Dati di esempio:

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

Ecco una risposta che funziona con data.table ed è più semplice. Ciò presuppone che data.table sia denominato yourDF:

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

Sostituisci ("V1", "V2", "V3", "V4")e (V1, V2, V3, V4)con i nomi delle colonne

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.