Geben Sie für jede Zeile den Spaltennamen des größten Werts zurück


94

Ich habe eine Liste von Mitarbeitern, und ich muss wissen, in welcher Abteilung sie sich am häufigsten befinden. Es ist trivial, die Mitarbeiter-ID anhand des Abteilungsnamens zu tabellieren, aber es ist schwieriger, den Abteilungsnamen und nicht die Anzahl der Dienstplanzählungen aus der Häufigkeitstabelle zurückzugeben. Ein einfaches Beispiel unten (Spaltennamen = Abteilungen, Zeilennamen = Mitarbeiter-IDs).

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

Wie komme ich jetzt?

> DF2
  RE
1 V3
2 V1
3 V2

Wie groß sind Ihre tatsächlichen Daten?
Arun

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

6
Eine interessante Verallgemeinerung wären die Spaltennamen der größten n Werte pro Zeile
Hack-R

Antworten:


98

Eine Option, bei der Ihre Daten verwendet werden (zum späteren Nachschlagen verwenden Sie diese set.seed(), um Beispiele mithilfe samplereproduzierbarer Daten zu erstellen ):

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"

Eine schnellere Lösung als die Verwendung applykönnte sein max.col:

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

... wo ties.methodkann einer von "random" "first"oder sein"last"

Dies verursacht natürlich Probleme, wenn Sie zufällig zwei Spalten haben, die dem Maximum entsprechen. Ich bin nicht sicher, was Sie in diesem Fall tun möchten, da Sie für einige Zeilen mehr als ein Ergebnis haben. Z.B:

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 

Wenn ich zwei gleiche Spalten habe, wähle ich normalerweise nur die erste aus. Dies sind Grenzfälle, die meine statistische Analyse nicht stören.
dmvianna

1
@dmvianna - Verwendung which.maxwird dann in Ordnung sein.
E-Mail

Ich gehe davon aus, dass die Reihenfolge beibehalten wird, damit ich mit diesem Vektor eine neue Spalte erstellen kann, die korrekt an den Mitarbeiter-IDs ausgerichtet ist. Ist das korrekt?
dmvianna

applykonvertiert das data.framein matrixintern. Möglicherweise sehen Sie bei diesen Dimensionen jedoch keinen Leistungsunterschied.
Arun

2
@PankajKaundal - unter der Annahme unterschiedlicher Werte, wie wäre es damitcolnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
E-Mail

15

Wenn Sie an einer data.tableLösung interessiert sind, finden Sie hier eine. Es ist etwas schwierig, da Sie es vorziehen, die ID für das erste Maximum zu erhalten. Es ist viel einfacher, wenn Sie lieber das letzte Maximum wollen. Trotzdem ist es nicht so kompliziert und schnell!

Hier habe ich Daten Ihrer Dimensionen generiert (26746 * 18).

Daten

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

data.table Antworten:

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

Bei Daten dieser Dimensionen ist es ungefähr elfmal schneller und lässt sich auch data.tableziemlich gut skalieren.


Bearbeiten: Wenn eine der maximalen IDs in Ordnung ist, dann:

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

Es ist mir eigentlich egal, ob es das erste oder letzte Maximum ist. Ich werde mich zunächst der Einfachheit widmen, aber ich bin sicher, dass eine data.table-Lösung in Zukunft nützlich sein wird, danke!
dmvianna

11

Eine Lösung könnte darin bestehen, das Datum von breit auf lang umzustellen, alle Abteilungen in eine Spalte zu setzen und in einer anderen zu zählen, nach der Arbeitgeber-ID (in diesem Fall der Zeilennummer) zu gruppieren und dann mit dem zu den Abteilungen zu filtern Maximalwert. Es gibt auch einige Optionen für den Umgang mit Verbindungen mit diesem Ansatz.

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.

10

Basierend auf den obigen Vorschlägen hat die folgende data.tableLösung für mich sehr schnell funktioniert:

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

Und hat auch den Vorteil, dass Sie immer angeben können, welche Spalten .SDberücksichtigt werden sollen, indem Sie sie erwähnen in .SDcols:

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

Wenn wir den Spaltennamen des kleinsten Werts benötigen, wie von @lwshang vorgeschlagen, muss man nur Folgendes verwenden -.SD:

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

Ich hatte eine ähnliche Anforderung, möchte aber den Spaltennamen mit dem Mindestwert für jede Zeile erhalten ..... wir scheinen nicht min.col in R zu haben ..... würden Sie wissen, was die äquivalente Lösung wäre ?
user1412

Hi @ user1412. Vielen Dank für Ihre interessante Frage. Ich habe momentan keine andere Idee, als das which.minin etwas zu verwenden, das aussehen würde: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]oder DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]auf den Dummy-Daten oben. Dies berücksichtigt keine Bindungen und gibt nur das erste Minimum zurück. Vielleicht sollten Sie eine separate Frage stellen. Ich wäre auch neugierig, welche anderen Antworten Sie bekommen würden.
Valentin

1
Ein Trick, um die minimale Spalte zu erhalten, besteht darin, das Negativ des data.frame in max.col zu senden, wie : colnames(.SD)[max.col(-.SD, ties.method="first")].
Lwshang

6

Eine dplyrLösung:

Idee:

  • Fügen Sie Rowids als Spalte hinzu
  • in Langformat umformen
  • Filter für max in jeder Gruppe

Code:

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) 

Ergebnis:

# 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

Dieser Ansatz kann leicht erweitert werden, um die oberen nSpalten zu erhalten. Beispiel für n=2:

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

Ergebnis:

# 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
Könnten Sie den Unterschied zwischen diesem Ansatz und der obigen Antwort von sbha kommentieren? Sie sehen für mich ungefähr gleich aus.
Gregor Thomas

2

Eine einfache forSchleife kann auch nützlich sein:

> 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

0

Hier ist eine Antwort, die mit data.table funktioniert und einfacher ist. Dies setzt voraus, dass Ihre data.table den Namen hat yourDF:

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

Ersetzen Sie ("V1", "V2", "V3", "V4")und (V1, V2, V3, V4)durch Ihre Spaltennamen


0

Eine Option von dplyr 1.0.0könnte sein:

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     

Beispieldaten:

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))
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.