Pour chaque ligne, renvoyez le nom de colonne de la plus grande valeur

97

J'ai une liste d'employés et j'ai besoin de savoir dans quel département ils travaillent le plus souvent. Il est simple de classer l'ID d'employé par rapport au nom du service, mais il est plus difficile de renvoyer le nom du service, plutôt que le nombre de comptes de liste, à partir de la table de fréquence. Un exemple simple ci-dessous (noms de colonnes = départements, noms de lignes = identifiants d'employés).

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

Maintenant comment puis-je obtenir

> DF2
  RE
1 V3
2 V1
3 V2
dmvianna
la source
Quelle est la taille réelle de vos données?
Arun
1
@Arun> dim (test) [1] 26746 18
dmvianna
6
Une généralisation intéressante serait les noms de colonnes de n valeurs les plus grandes par ligne
Hack-R

Réponses:

99

Une option utilisant vos données (pour référence future, utilisez set.seed()pour faire des exemples en utilisant samplereproductible):

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"

Une solution plus rapide que l'utilisation applypourrait être max.col:

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

... où ties.methodpeut être l'un des "random" "first"ou"last"

Cela pose bien sûr des problèmes si vous avez deux colonnes égales au maximum. Je ne suis pas sûr de ce que vous voulez faire dans ce cas car vous aurez plus d'un résultat pour certaines lignes. Par exemple:

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 
le courrier électronique
la source
Si j'ai deux colonnes égales, je choisis généralement la première. Ce sont des cas frontières qui ne bouleversent pas mon analyse statistique.
dmvianna
1
@dmvianna - utiliser which.maxsera alors très bien.
thelatemail
Je suppose que l'ordre est conservé, je peux donc créer une nouvelle colonne avec ce vecteur qui s'alignera correctement sur les identifiants des employés. Est-ce exact?
dmvianna
applyconvertit le data.frameen matrixinterne. Cependant, vous ne verrez peut-être pas de différence de performances sur ces dimensions.
Arun
2
@PankajKaundal - en supposant des valeurs distinctes, que diriez-vous de celacolnames(DF)[max.col(replace(DF, cbind(seq_len(nrow(DF)), max.col(DF,ties.method="first")), -Inf), "first")]
thelatemail
15

Si vous êtes intéressé par une data.tablesolution, en voici une. C'est un peu délicat car vous préférez obtenir l'id pour le premier maximum. C'est beaucoup plus facile si vous préférez le dernier maximum. Néanmoins, ce n'est pas si compliqué et c'est rapide!

Ici, j'ai généré des données de vos dimensions (26746 * 18).

Les données

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

data.table répondre:

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

Analyse comparative:

# 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

C'est environ 11 fois plus rapide sur les données de ces dimensions et data.tableévolue assez bien aussi.


Edit: si l'un des identifiants max est correct, alors:

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"]
Arun
la source
En fait, je ne me soucie pas de savoir si c'est le premier ou le dernier maximum. Je vais d'abord pour la simplicité, mais je suis sûr qu'une solution data.table sera utile à l'avenir, merci!
dmvianna
11

Une solution pourrait être de remodeler la date de large à long en plaçant tous les départements dans une colonne et en comptant dans une autre, en les regroupant par identifiant d'employeur (dans ce cas, le numéro de ligne), puis en filtrant sur le (s) département (s) avec le Valeur max. Il existe également plusieurs options pour gérer les liens avec cette approche.

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.
sbha
la source
11

Sur la base des suggestions ci-dessus, la data.tablesolution suivante a fonctionné très rapidement pour moi:

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

Et vient également avec l'avantage de pouvoir toujours spécifier les colonnes à .SDprendre en compte en les mentionnant dans .SDcols:

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

Au cas où nous aurions besoin du nom de colonne de la plus petite valeur, comme suggéré par @lwshang, il suffit d'utiliser -.SD:

DT[, col_min := colnames(.SD)[max.col(-.SD, ties.method = "first")]]
Valentin
la source
J'avais une exigence similaire mais je veux obtenir le nom de la colonne ayant la valeur minimale pour chaque ligne ..... nous ne semblons pas avoir min.col dans R ..... sauriez-vous quelle serait la solution équivalente ?
user1412
Salut @ user1412. Merci pour votre question intéressante. Je n'ai aucune idée pour le moment autre que d'utiliser le which.mindans quelque chose qui ressemblerait à: DT[, MIN := colnames(.SD)[apply(.SD,1,which.min)]]ou DT[, MIN2 := colnames(.SD)[which.min(.SD)], by = 1:nrow(DT)]sur les données factices ci-dessus. Cela ne prend pas en compte les liens et ne renvoie que le premier minimum. Pensez peut-être à poser une question distincte. Je serais également curieux de savoir quelles autres réponses vous obtiendriez.
Valentin
1
Une astuce pour obtenir la colonne minimum envoie le négatif du data.frame dans max.col, comme: colnames(.SD)[max.col(-.SD, ties.method="first")].
lwshang
6

Une dplyrsolution:

Idée:

  • ajouter des rowids en tant que colonne
  • remodeler au format long
  • filtre pour max dans chaque groupe

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) 

Résultat:

# 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

Cette approche peut être facilement étendue pour obtenir les ncolonnes du haut . Exemple pour n=2:

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

Résultat:

# 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
Gregor Sturm
la source
1
Pourriez-vous commenter la différence entre cette approche et la réponse de sbha ci-dessus? Ils me ressemblent à peu près.
Gregor Thomas
2

Une simple forboucle peut également être pratique:

> 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
rar
la source
1

Une option dplyr 1.0.0pourrait être:

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     

Exemple de données:

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))
tmfmnk
la source
0

Voici une réponse qui fonctionne avec data.table et qui est plus simple. Cela suppose que votre data.table est nommé yourDF:

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

Remplacez ("V1", "V2", "V3", "V4")et (V1, V2, V3, V4)par vos noms de colonnes

Apprentissage des statistiques par exemple
la source