【问题标题】:For each row return the column name of the largest value对于每一行,返回最大值的列名
【发布时间】:2021-12-06 04:25:04
【问题描述】:

我有一份员工名册,我需要知道他们最常在哪个部门工作。将员工 ID 与部门名称制成表格很简单,但从频率表中返回部门名称而不是花名册计数的数量则比较棘手。下面是一个简单的示例(列名 = 部门,行名 = 员工 ID)。

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

现在我该如何获得

> DF2
  RE
1 V3
2 V1
3 V2

【问题讨论】:

  • 您的实际数据有多大?
  • @Arun > 暗淡(测试)[1] 26746 18
  • 一个有趣的概括是每行最大 n 个值的列名

标签: r


【解决方案1】:

使用您的数据的一个选项(为了将来参考,使用set.seed() 来制作使用sample 可重现的示例):

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"

比使用apply 更快的解决方案可能是max.col

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

...其中ties.method 可以是"random" "first""last" 中的任何一个

如果您碰巧有两列等于最大值,这当然会导致问题。我不确定在这种情况下您想做什么,因为某些行会有多个结果。例如:

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 

【讨论】:

  • 如果我有两个相等的列,我通常只选择第一个。这些是边境案例,不会影响我的统计分析。
  • @dmvianna - 使用which.max 就可以了。
  • 我假设订单被保留,因此我可以使用此向量创建一个新列,该列将与员工 ID 正确对齐。对吗?
  • apply 在内部将data.frame 转换为matrix。不过,您可能看不到这些维度的性能差异。
  • @PankajKaundal - 假设不同的值,colnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
【解决方案2】:

如果您对data.table 解决方案感兴趣,这里有一个。这有点棘手,因为您更喜欢获取第一个最大值的 id。如果您想要最后一个最大值,这会容易得多。不过,它并不复杂,而且速度很快!

在这里,我生成了您的尺寸数据 (26746 * 18)。

数据

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

data.table答:

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

基准测试:

# 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

在这些维度的数据上大约快 11 倍,data.table 的扩展性也很好。


编辑:如果任何最大 id 都可以,那么:

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

【讨论】:

  • 我实际上并不关心它是第一个最大值还是最后一个最大值。我首先要简单,但我相信 data.table 解决方案将来会派上用场,谢谢!
【解决方案3】:

基于上述建议,以下data.table 解决方案对我来说非常有效:

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

而且还具有一个优势,即始终可以通过在.SDcols 中提及它们来指定.SD 应考虑的列:

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

如果我们需要最小值的列名,正如@lwshang所建议的,只需要使用-.SD

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

【讨论】:

  • 我有类似的要求,但想获得每行具有最小值的列名.....我们似乎在 R 中没有 min.col ......会你知道什么是等效的解决方案吗?
  • 嗨@user1412。感谢您提出有趣的问题。除了在上面的虚拟数据上使用which.min 之外,我现在没有任何想法:DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]。这不考虑平局,只返回第一个最小值。也许考虑问一个单独的问题。我也很好奇你会得到什么其他答案。
  • 获得最小列的技巧是将 data.frame 的负数发送到 max.col,例如:colnames(.SD)[max.col(-.SD, ties.method="first")]
【解决方案4】:

一种解决方案可能是将日期从宽更改为长,将所有部门放在一列中并在另一列中计数,按雇主 ID(在本例中为行号)分组,然后过滤到部门) 具有最大值。使用这种方法处理关系也有几种选择。

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.

【讨论】:

    【解决方案5】:

    一个简单的for 循环也可以很方便:

    > 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
    

    【讨论】:

      【解决方案6】:

      dplyr 解决方案:

      想法:

      • 将 rowids 添加为列
      • 重塑为长格式
      • 过滤每组中的最大值

      代码:

      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) 
      

      结果:

      # 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
      

      此方法可以轻松扩展以获取顶部的n 列。 n=2 的示例:

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

      结果:

      # 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
      

      【讨论】:

      • 您能否评论一下这种方法与上述 sbha 的答案之间的区别?它们在我看来差不多。
      【解决方案7】:

      这是一个适用于 data.table 并且更简单的答案。这假设您的 data.table 名为 yourDF:

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

      ("V1", "V2", "V3", "V4")(V1, V2, V3, V4) 替换为您的列名

      【讨论】:

      • 如果NA值存在于列值中,有人可以帮助我们如何忽略NA值
      【解决方案8】:

      dplyr 1.0.0 的一个选项可能是:

      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     
      

      在某些情况下,使用pmap() 可能更安全(需要purrr):

      DF %>%
          mutate(row_max = pmap(across(everything()), ~ names(c(...)[which.max(c(...))])))
      

      样本数据:

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

      【讨论】:

        【解决方案9】:

        这个很快:

        with(DF, {
          names(DF)[(V1 > V2 & V1 > V3) * 1 + (V2 > V3 & V2 > V1) * 2 + (V3 > V1 & V3 > V2)*3]
        })
        

        【讨论】:

          【解决方案10】:

          这是一个快速简单的 tidyverse 解决方案,可以轻松应用于 data.frame 中的任何列子集。下面的版本还使用ifelse 在所有列都为 0 的情况下添加缺失值。例如,如果有人想使用它来重新组合 one-hot 编码的列,则缺失值将很有用。它适用于问题中的数据,但这里是一个单热编码数据集的示例,它也适用。

          data <- data.frame(
             oh_a = c(1,0,0,1,0,0)
            ,oh_b = c(0,1,1,0,0,0)
            ,oh_c = c(0,0,0,0,1,0)
            ,d = c("l","m","n","o","p","q"))
          
          f <- function(x){ifelse(rowSums(x)==0, NA, names(x)[max.col(x, "first")])}
          data %>% 
            mutate(transformed = f(across(starts_with("oh"))))
          

          输出:

            oh_a oh_b oh_c d transformed
          1    1    0    0 l        oh_a
          2    0    1    0 m        oh_b
          3    0    1    0 n        oh_b
          4    1    0    0 o        oh_a
          5    0    0    1 p        oh_c
          6    0    0    0 q        <NA>
          

          【讨论】:

            猜你喜欢
            • 2020-12-16
            • 2015-11-01
            • 1970-01-01
            • 2016-01-06
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多