Đối với mỗi hàng, trả về tên cột có giá trị lớn nhất


97

Tôi có một danh sách nhân viên và tôi cần biết họ thường làm ở bộ phận nào nhất. Việc lập bảng mã số nhân viên so với tên bộ phận là việc đơn giản, nhưng việc trả lại tên bộ phận, thay vì số lượng danh sách, từ bảng tần suất sẽ khó hơn. Một ví dụ đơn giản dưới đây (tên cột = phòng ban, tên hàng = id nhân viên).

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

Bây giờ làm thế nào để tôi có được

> DF2
  RE
1 V3
2 V1
3 V2

dữ liệu thực tế của bạn lớn đến mức nào?
Arun

1
@Arun> dim (thử nghiệm) [1] 26.746 18
dmvianna

6
Một sự tổng quát hóa thú vị sẽ là tên cột của n giá trị lớn nhất trên mỗi hàng
Hack-R

Câu trả lời:


99

Một tùy chọn sử dụng dữ liệu của bạn (để tham khảo trong tương lai, sử dụng set.seed()để làm ví dụ bằng cách sử dụng có thể sampletái tạo):

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"

Một giải pháp nhanh hơn việc sử dụng applycó thể là max.col:

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

... nơi ties.methodcó thể là bất kỳ "random" "first"hoặc"last"

Tất nhiên, điều này gây ra sự cố nếu bạn tình cờ có hai cột bằng giá trị tối đa. Tôi không chắc bạn muốn làm gì trong trường hợp đó vì bạn sẽ có nhiều hơn một kết quả cho một số hàng. Ví dụ:

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 

Nếu tôi có hai cột bằng nhau, tôi thường chỉ chọn cột đầu tiên. Đây là những trường hợp biên giới không làm đảo lộn phân tích thống kê của tôi.
dmvianna

1
@dmvianna - sử dụng which.maxsẽ ổn sau đó.
thelatemail

Tôi giả sử đơn đặt hàng được giữ nguyên, vì vậy tôi có thể tạo một cột mới với vectơ này sẽ căn chỉnh chính xác với ID nhân viên. Đúng không?
dmvianna 19/07/13

applychuyển đổi data.framethành matrixnội bộ. Tuy nhiên, bạn có thể không thấy sự khác biệt về hiệu suất trên các thứ nguyên này.
Arun

2
@PankajKaundal - giả sử giá trị khác biệt, làm thế nào về vấn đề nàycolnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
thelatemail

15

Nếu bạn quan tâm đến một data.tablegiải pháp, đây là một giải pháp. Nó hơi phức tạp vì bạn muốn lấy id ở mức tối đa đầu tiên. Sẽ dễ dàng hơn nhiều nếu bạn muốn có mức tối đa cuối cùng. Tuy nhiên, nó không phức tạp và nhanh chóng!

Tại đây, tôi đã tạo dữ liệu về kích thước của bạn (26746 * 18).

Dữ liệu

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

data.table câu trả lời:

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

Điểm chuẩn:

# 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

Nó nhanh hơn khoảng 11 lần đối với dữ liệu của các thứ nguyên này và cũng mở rộng data.tablequy mô khá tốt.


Chỉnh sửa: nếu bất kỳ id tối đa nào ổn, thì:

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

Tôi thực sự không quan tâm nếu đó là mức tối đa đầu tiên hay cuối cùng. Trước tiên, tôi muốn đơn giản hóa, nhưng tôi chắc chắn rằng giải pháp data.table sẽ hữu ích trong tương lai, cảm ơn!
dmvianna

11

Một giải pháp có thể là định hình lại ngày từ rộng thành dài, đặt tất cả các phòng ban vào một cột và đếm trong cột khác, nhóm theo id nhà tuyển dụng (trong trường hợp này là số hàng), sau đó lọc thành (các) phòng ban với giá trị tối đa. Có một số tùy chọn để xử lý các mối quan hệ với cách tiếp cận này.

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

Dựa trên các đề xuất ở trên, data.tablegiải pháp sau đây hoạt động rất nhanh đối với tôi:

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

Và cũng đi kèm với lợi thế là luôn có thể chỉ định những cột nào .SDnên xem xét bằng cách đề cập chúng trong .SDcols:

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

Trong trường hợp chúng ta cần tên cột có giá trị nhỏ nhất, theo đề xuất của @lwshang, người ta chỉ cần sử dụng -.SD:

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

Tôi đã có một yêu cầu tương tự nhưng muốn tên cột có giá trị nhỏ nhất cho mỗi hàng ..... dường như chúng tôi không có min.col trong R ..... bạn có biết đâu sẽ là giải pháp tương đương ?
user1412

Xin chào @ user1412. Cảm ơn câu hỏi thú vị của bạn. Tôi không có bất kỳ ý tưởng nào ngay bây giờ ngoài việc sử dụng which.mintrong một cái gì đó trông giống như: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]hoặc DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]trên dữ liệu giả ở trên. Điều này không coi là ràng buộc và chỉ trả về mức tối thiểu đầu tiên. Có thể cân nhắc đặt một câu hỏi riêng. Tôi cũng rất tò mò về những câu trả lời khác mà bạn sẽ nhận được.
Valentin

1
Một thủ thuật để có được cột tối thiểu là gửi các tiêu cực của data.frame thành max.col, như: colnames(.SD)[max.col(-.SD, ties.method="first")].
lwshang

6

Một dplyrgiải pháp:

Ý tưởng:

  • thêm rowids dưới dạng một cột
  • định hình lại thành định dạng dài
  • lọc cho tối đa trong mỗi nhóm

Mã:

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) 

Kết quả:

# 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

Cách tiếp cận này có thể dễ dàng mở rộng để lấy các ncột trên cùng . Ví dụ cho n=2:

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

Kết quả:

# 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
Bạn có thể nhận xét về sự khác biệt giữa cách tiếp cận này và câu trả lời của sbha ở trên? Họ trông giống nhau đối với tôi.
Gregor Thomas

2

Một forvòng lặp đơn giản cũng có thể hữu ích:

> 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

Một tùy chọn từ dplyr 1.0.0có thể là:

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     

Dữ liệu mẫu:

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

Đây là một câu trả lời hoạt động với data.table và đơn giản hơn. Điều này giả sử data.table của bạn có tên yourDF:

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

Thay thế ("V1", "V2", "V3", "V4")(V1, V2, V3, V4)bằng tên cột của bạn

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.