web-dev-qa-db-fra.com

R: dplyr :: mutate par ligne utilisant une fonction qui prend une ligne de trame de données et retourne un entier

J'essaie d'utiliser l'instruction pipe mutate à l'aide d'une fonction personnalisée. J'ai regardé ce quelque peu similaire SO post mais en vain. Supposons que j'ai un bloc de données comme celui-ci (où blob est une variable non liée à la tâche spécifique mais faisant partie de l'ensemble des données):

df <- 
  data.frame(exclude=c('B','B','D'), 
             B=c(1,0,0), 
             C=c(3,4,9), 
             D=c(1,1,0), 
             blob=c('fd', 'fs', 'sa'), 
             stringsAsFactors = F)

J'ai une fonction qui utilise les noms de variables, alors sélectionnez-en certaines en fonction de la valeur dans la colonne exclude et par exemple calcule une somme sur les variables non spécifiées dans exclude (qui est toujours un seul caractère).

FUN <- function(df){
  sum(df[c('B', 'C', 'D')] [!names(df[c('B', 'C', 'D')]) %in% df['exclude']] )
}

Lorsque je donne une seule ligne (ligne 1) à FUN j'obtiens la somme attendue de C et D (celles qui ne sont pas mentionnées par exclude), à savoir 4:

FUN(df[1,])

Comment puis-je faire de même dans un tube avec mutate (en ajoutant le résultat à une variable s). Ces deux essais ne fonctionnent pas:

df %>% mutate(s=FUN(.))
df %>% group_by(1:n()) %>% mutate(s=FUN(.))

[~ # ~] mise à jour [~ # ~] Cela ne fonctionne pas non plus comme prévu:

df %>% rowwise(.) %>% mutate(s=FUN(.))

Cela fonctionne de la cause mais n'est pas dans le mutate de dplyr (et les tuyaux):

df$s <- sapply(1:nrow(df), function(x) FUN(df[x,]))
8
user3375672

Si vous souhaitez utiliser dplyr vous pouvez le faire en utilisant rowwise et votre fonction FUN.

df %>% 
    rowwise %>% 
    do({
        result = as_data_frame(.)
        result$s = FUN(result)
        result
    })

La même chose peut être obtenue en utilisant group_by Au lieu de rowwise (comme vous l'avez déjà essayé) mais avec do au lieu de mutate

df %>% 
    group_by(1:n()) %>% 
    do({
        result = as_data_frame(.)
        result$s = FUN(result)
        result
    })

La raison pour laquelle mutate ne fonctionne pas dans ce cas, c'est que vous lui passez l'intégralité du tibble, c'est donc comme appeler FUN(df).

Une manière beaucoup plus efficace de faire la même chose est de créer une matrice de colonnes à inclure, puis d'utiliser rowSums.

cols <- c('B', 'C', 'D')
include_mat <- outer(function(x, y) x != y, X = df$exclude, Y = cols)
# or outer(`!=`, X = df$exclude, Y = cols) if it's more readable to you
df$s <- rowSums(df[cols] * include_mat)
8
konvas

purrr approche

Nous pouvons utiliser une combinaison de nest et map_dbl Pour cela:

library(tidyverse)
df %>% 
  rowwise %>% 
  nest(-blob) %>% 
  mutate(s = map_dbl(data, FUN)) %>% 
  unnest

Décomposons cela un peu. Premièrement, rowwise nous permet d'appliquer chaque fonction suivante pour prendre en charge des opérations complexes arbitraires qui doivent être appliquées à chaque ligne.

Ensuite, nest créera une nouvelle colonne qui est une liste de nos données à alimenter dans FUN (la beauté des tibbles vs data.frames!). Puisque nous appliquons ce rowwise, chaque ligne contient un tibble d'une ligne de exclude:D.

Enfin, nous utilisons map_dbl Pour mapper notre FUN à chacun de ces tibbles. map_dbl Est utilisé par rapport à la famille d'autres fonctions map_* Car notre sortie prévue est numérique (c'est-à-dire double).

unnest renvoie notre tibble dans la structure plus standard.

purrrlyr approche

Alors que purrrlyr n'est peut-être pas aussi "populaire" que ses parents dplyr et purrr, sa fonction by_row A ici une certaine utilité.

Dans votre exemple ci-dessus, nous utiliserions votre bloc de données df et la fonction définie par l'utilisateur FUN de la manière suivante:

df %>% 
  by_row(..f = FUN, .to = "s", .collate = "cols")

C'est tout! Te donne:

# tibble [3 x 6]
  exclude     B     C     D  blob     s
    <chr> <dbl> <dbl> <dbl> <chr> <dbl>
1       B     1     3     1    fd     4
2       B     0     4     1    fs     5
3       D     0     9     0    sa     9

Certes, la syntaxe est un peu étrange, mais voici comment elle se décompose:

  • ..f = La fonction à appliquer à chaque ligne
  • .to = Le nom de la colonne de sortie, dans ce cas s
  • .collate = La façon dont les résultats doivent être regroupés, par liste, ligne ou colonne. Étant donné que FUN n'a qu'une seule sortie, nous serions d'accord pour utiliser "cols" Ou "rows"

Voir ici pour plus d'informations sur l'utilisation de purrrlyr...


Performance

Attention, même si j'aime la fonctionnalité de by_row, Ce n'est pas toujours la meilleure approche pour la performance! purrr est plus intuitif, mais aussi à une perte de vitesse assez importante. Voir le test microbenchmark suivant:

library(microbenchmark)
mbm <- microbenchmark(
  purrr.test = df %>% rowwise %>% nest(-blob) %>% 
    mutate(s = map_dbl(data, FUN)) %>% unnest,
  purrrlyr.test = df %>% by_row(..f = FUN, .to = "s", .collate = "cols"),
  rowwise.test = df %>% 
    rowwise %>% 
    do({
      result = as_tibble(.)
      result$s = FUN(result)
      result
    }),
  group_by.test = df %>% 
    group_by(1:n()) %>% 
    do({
      result = as_tibble(.)
      result$s = FUN(result)
      result
    }),
  sapply.test = {df$s <- sapply(1:nrow(df), function(x) FUN(df[x,]))}, 
  times = 1000
)
autoplot(mbm)

enter image description here

Vous pouvez voir que l'approche purrrlyr est plus rapide que l'approche consistant à utiliser une combinaison de do avec rowwise ou group_by(1:n()) (voir la réponse @konvas), et plutôt à égalité avec l'approche sapply. Cependant, le package n'est certes pas le plus intuitif. L'approche standard purrr semble être la plus lente, mais peut-être aussi plus facile à utiliser. Différentes fonctions définies par l'utilisateur peuvent modifier l'ordre de vitesse.

6
Dave Gruenewald