对于每一行,返回最大值的列名

我有一份员工名册,我需要知道他们最常在哪个部门工作。将员工 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
128735 次浏览

使用您的数据的一个选项(为了将来的参考,使用 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

如果你对 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解决方案对我来说非常有效:

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

一个解决方案可以是将日期从宽到长,将所有部门放在一列中,并在另一列中计数,按雇主 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.

一个简单的 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

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

下面是一个使用 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)

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

这个很快:

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

这是一个快速而简单的整合解决方案,可以很容易地应用到 data.frame中的任何列子集。如果所有列都为0,则下面的版本还使用 ifelse添加缺少的值。如果有人希望使用缺少的值重新组合一个热编码的列,那么这些值将非常有用。它可以处理问题中的数据,但这里有一个示例,它也可以处理一个热编码的数据集。

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>