Appelez la fonction de type apply sur chaque ligne de dataframe avec plusieurs arguments de chaque ligne

168

J'ai un dataframe avec plusieurs colonnes. Pour chaque ligne de la trame de données, je souhaite appeler une fonction sur la ligne et l'entrée de la fonction utilise plusieurs colonnes de cette ligne. Par exemple, disons que j'ai ces données et ce testFunc qui accepte deux arguments:

> df <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
> df
  x y z
1 1 3 5
2 2 4 6
> testFunc <- function(a, b) a + b

Disons que je veux appliquer ce testFunc aux colonnes x et z. Donc, pour la ligne 1, je veux 1 + 5, et pour la ligne 2, je veux 2 + 6. Y a-t-il un moyen de faire cela sans écrire une boucle for, peut-être avec la famille de fonctions apply?

J'ai essayé ceci:

> df[,c('x','z')]
  x z
1 1 5
2 2 6
> lapply(df[,c('x','z')], testFunc)
Error in a + b : 'b' is missing

Mais vous avez une erreur, des idées?

EDIT: la fonction réelle que je veux appeler n'est pas une simple somme, mais c'est power.t.test. J'ai utilisé a + b juste à titre d'exemple. Le but final est de pouvoir faire quelque chose comme ça (écrit en pseudocode):

df = data.frame(
    delta=c(delta_values), 
    power=c(power_values), 
    sig.level=c(sig.level_values)
)

lapply(df, power.t.test(delta_from_each_row_of_df, 
                        power_from_each_row_of_df, 
                        sig.level_from_each_row_of_df
))

où le résultat est un vecteur de sorties pour power.t.test pour chaque ligne de df.

vasek1
la source
Voir également stackoverflow.com/a/24728107/946850 pour le dplyrchemin.
krlmlr

Réponses:

137

Vous pouvez postuler applyà un sous-ensemble des données d'origine.

 dat <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
 apply(dat[,c('x','z')], 1, function(x) sum(x) )

ou si votre fonction est juste une somme, utilisez la version vectorisée:

rowSums(dat[,c('x','z')])
[1] 6 8

Si vous souhaitez utiliser testFunc

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(x) testFunc(x[1],x[2]))

EDIT Pour accéder aux colonnes par nom et non par index, vous pouvez faire quelque chose comme ceci:

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(y) testFunc(y['z'],y['x']))
étude
la source
merci @agstudy, cela a fonctionné! savez-vous s'il existe un moyen de spécifier les arguments par nom plutôt que par index? donc, pour testFunc, quelque chose comme apply (dat [, c ('x', 'z')], 1, [pseudocode] testFunc (a = x, b = y))? la raison est que j'appelle power.t.test de cette manière, et j'aimerais pouvoir référencer les paramètres delta, power, sig.level par nom au lieu de les coller dans un tableau avec des positions pré-spécifiées, puis référencer ces positions, pour la raison d'être plus robuste. en tout cas merci beaucoup!
vasek1
désolé pour le commentaire précédent, appuyez sur Entrée avant d'avoir fini de taper :) l'a supprimé et publié la version complète.
vasek1
21
Ne pas utiliser applysur les big data.frames, cela copiera l'objet entier (pour le convertir en matrice). Cela posera également des problèmes si vous avez différents objets de classe dans le data.frame.
mnel
105

A data.frameest unlist , donc ...

Pour les fonctions vectorisées, do.call c'est généralement un bon pari. Mais les noms des arguments entrent en jeu. Ici, votre testFuncest appelé avec args x et y à la place de a et b. Le ...permet de passer des arguments non pertinents sans provoquer d'erreur:

do.call( function(x,z,...) testFunc(x,z), df )

Pour les fonctions non vectorisées , mapplycela fonctionnera, mais vous devez faire correspondre l'ordre des arguments ou les nommer explicitement:

mapply(testFunc, df$x, df$z)

Cela applyfonctionnera parfois - comme lorsque tous les arguments sont du même type, ce qui contraint ledata.frame à une matrice ne pose pas de problèmes en modifiant les types de données. Votre exemple était de ce genre.

Si votre fonction doit être appelée dans une autre fonction dans laquelle tous les arguments sont passés, il existe une méthode beaucoup plus astucieuse que celles-ci. Étudiez les premières lignes du corps pour savoir lm()si vous souhaitez emprunter cette voie.

user2087984
la source
8
+10 si je pouvais. Bienvenue à SO. excellente réponse - cela vaut peut-être la peine de le mentionner Vectorizecomme wrapper mapplypour vectoriser les fonctions
mnel
wow, c'est lisse. La fonction d'origine que j'ai utilisée n'était pas vectorisée (une extension personnalisée en plus de power.t.test), mais je pense que je vais la vectoriser et utiliser do.call (...). Merci!
vasek1
3
Réitérant simplement la note que cette réponse dit déjà que apply (df, 1, function (row) ...) peut être mauvais car apply convertit le df en une matrice !!!! Cela peut être mauvais et entraîner beaucoup de cheveux. Les alternatives à appliquer sont bien nécessaires!
Colin D
Merci beaucoup d'avoir fait la différence entre vectorisé / non vectorisé, c'est absolument la réponse que je recherchais
User632716
31

Utilisation mapply

> df <- data.frame(x=c(1,2), y=c(3,4), z=c(5,6))
> df
  x y z
1 1 3 5
2 2 4 6
> mapply(function(x,y) x+y, df$x, df$z)
[1] 6 8

> cbind(df,f = mapply(function(x,y) x+y, df$x, df$z) )
  x y z f
1 1 3 5 6
2 2 4 6 8
Chinmay Patil
la source
20

Nouvelle réponse avec dplyrpackage

Si la fonction que vous souhaitez appliquer est vectorisée, vous pouvez utiliser la mutatefonction du dplyrpackage:

> library(dplyr)
> myf <- function(tens, ones) { 10 * tens + ones }
> x <- data.frame(hundreds = 7:9, tens = 1:3, ones = 4:6)
> mutate(x, value = myf(tens, ones))
  hundreds tens ones value
1        7    1    4    14
2        8    2    5    25
3        9    3    6    36

Ancienne réponse avec plyrpackage

À mon humble avis, l'outil le mieux adapté à la tâche est celui mdplydu plyrpackage.

Exemple:

> library(plyr)
> x <- data.frame(tens = 1:3, ones = 4:6)
> mdply(x, function(tens, ones) { 10 * tens + ones })
  tens ones V1
1    1    4 14
2    2    5 25
3    3    6 36

Malheureusement, comme l'a souligné Bertjan Broeksema , cette approche échoue si vous n'utilisez pas toutes les colonnes de la trame de données dans l' mdplyappel. Par exemple,

> library(plyr)
> x <- data.frame(hundreds = 7:9, tens = 1:3, ones = 4:6)
> mdply(x, function(tens, ones) { 10 * tens + ones })
Error in (function (tens, ones)  : unused argument (hundreds = 7)
J'aime coder
la source
1
C'est bien quand vous n'avez qu'un petit nombre de colonnes. J'ai essayé de faire quelque chose comme: mdply (df, function (col1, col3) {}) et mdply renfloue, me plaignant que col2 n'est pas utilisé. Maintenant, si vous avez des dizaines voire des centaines de colonnes, cette approche n'est pas très attractive.
Bertjan Broeksema
1
@BertjanBroeksema pour modifier beaucoup de colonnes, vous pouvez utiliser dplyr::mutate_each. Par exemple: iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux
Ne pourriez-vous pas simplement passer des elipses ou des centaines dans la fonction et ne pas l'utiliser? Cela devrait corriger cette erreur?
Shawn
11

D'autres ont correctement souligné que mapplyc'est fait à cet effet, mais (par souci d'exhaustivité) une méthode conceptuellement plus simple consiste simplement à utiliser une forboucle.

for (row in 1:nrow(df)) { 
    df$newvar[row] <- testFunc(df$x[row], df$z[row]) 
}
rsoren
la source
1
Vous avez raison. Pour utiliser mapply efficacement, je pense que vous devez comprendre que c'est juste une boucle «for» dans les coulisses, surtout si vous venez d'un fond de programmation procédurale comme C ++ ou C #.
Contango
10

De nombreuses fonctions sont déjà vectorisées, et il n'y a donc pas besoin d'itérations (ni forboucles ni *pplyfonctions). Votre en testFuncest un exemple. Vous pouvez simplement appeler:

  testFunc(df[, "x"], df[, "z"])

En général, je recommanderais d'essayer d'abord de telles approches de vectorisation et de voir si elles vous donnent les résultats escomptés.


Alternativement, si vous devez passer plusieurs arguments à une fonction qui n'est pas vectorisée, mapplypeut-être ce que vous recherchez:

  mapply(power.t.test, df[, "x"], df[, "z"])
Ricardo Saporta
la source
oh, doux. Savez-vous s'il existe un moyen de spécifier des arguments par nom dans mapply? c'est-à-dire quelque chose comme [pseudocode] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1
1
Oui, c'est exactement comme vous l'avez! ;)
Ricardo Saporta
4

Voici une autre approche. C'est plus intuitif.

Un aspect clé que je pense que certaines des réponses n'ont pas pris en compte, que je souligne pour la postérité, est apply () vous permet de faire des calculs de lignes facilement, mais uniquement pour les données matricielles (toutes numériques)

les opérations sur les colonnes sont encore possibles pour les dataframes:

as.data.frame(lapply(df, myFunctionForColumn()))

Pour opérer sur les lignes, nous faisons d'abord la transposition.

tdf<-as.data.frame(t(df))
as.data.frame(lapply(tdf, myFunctionForRow()))

L'inconvénient est que je pense que R fera une copie de votre tableau de données. Ce qui pourrait être un problème de mémoire. (C'est vraiment triste, car il est simple par programme pour tdf d'être simplement un itérateur du df d'origine, économisant ainsi de la mémoire, mais R ne permet pas le référencement de pointeur ou d'itérateur.)

En outre, une question connexe est de savoir comment opérer sur chaque cellule individuelle dans une trame de données.

newdf <- as.data.frame(lapply(df, function(x) {sapply(x, myFunctionForEachCell()}))
BAMF4bacon
la source
4

Je suis venu ici à la recherche du nom de la fonction tidyverse - dont je savais qu'il existait. En ajoutant ceci pour (ma) future référence et pour les tidyversepassionnés: purrrlyr:invoke_rows(purrr:invoke_rows dans les anciennes versions).

Avec la connexion aux méthodes de statistiques standard comme dans la question originale, le paquet balai aiderait probablement.

liborm
la source
3

La réponse de @ user20877984 est excellente. Puisqu'ils l'ont résumé bien mieux que ma réponse précédente, voici ma tentative (peut-être encore de mauvaise qualité) d'application du concept:

Utilisation do.callde base:

powvalues <- list(power=0.9,delta=2)
do.call(power.t.test,powvalues)

Travailler sur un ensemble de données complet:

# get the example data
df <- data.frame(delta=c(1,1,2,2), power=c(.90,.85,.75,.45))

#> df
#  delta power
#1     1  0.90
#2     1  0.85
#3     2  0.75
#4     2  0.45

lapplyla power.t.testfonction à chacune des lignes de valeurs spécifiées:

result <- lapply(
  split(df,1:nrow(df)),
  function(x) do.call(power.t.test,x)
)

> str(result)
List of 4
 $ 1:List of 8
  ..$ n          : num 22
  ..$ delta      : num 1
  ..$ sd         : num 1
  ..$ sig.level  : num 0.05
  ..$ power      : num 0.9
  ..$ alternative: chr "two.sided"
  ..$ note       : chr "n is number in *each* group"
  ..$ method     : chr "Two-sample t test power calculation"
  ..- attr(*, "class")= chr "power.htest"
 $ 2:List of 8
  ..$ n          : num 19
  ..$ delta      : num 1
  ..$ sd         : num 1
  ..$ sig.level  : num 0.05
  ..$ power      : num 0.85
... ...
le courrier électronique
la source
Haha alambiqué peut-être? ;) pourquoi utilisez-vous t () et appliquez-vous sur 2, pourquoi ne pas simplement postuler 1?
Ricardo Saporta
3

data.table a également une façon très intuitive de le faire:

library(data.table)

sample_fxn = function(x,y,z){
    return((x+y)*z)
}

df = data.table(A = 1:5,B=seq(2,10,2),C = 6:10)
> df
   A  B  C
1: 1  2  6
2: 2  4  7
3: 3  6  8
4: 4  8  9
5: 5 10 10

L' :=opérateur peut être appelé entre parenthèses pour ajouter une nouvelle colonne à l'aide d'une fonction

df[,new_column := sample_fxn(A,B,C)]
> df
   A  B  C new_column
1: 1  2  6         18
2: 2  4  7         42
3: 3  6  8         72
4: 4  8  9        108
5: 5 10 10        150

Il est également facile d'accepter des constantes comme arguments en utilisant cette méthode:

df[,new_column2 := sample_fxn(A,B,2)]

> df
   A  B  C new_column new_column2
1: 1  2  6         18           6
2: 2  4  7         42          12
3: 3  6  8         72          18
4: 4  8  9        108          24
5: 5 10 10        150          30
Pete M
la source
1

Si les colonnes data.frame sont de types différents, apply()a un problème. Une subtilité à propos de l'itération de ligne est de savoir comment apply(a.data.frame, 1, ...)la conversion de type implicite en types de caractères lorsque les colonnes sont de types différents; par exemple. un facteur et une colonne numérique. Voici un exemple, en utilisant un facteur dans une colonne pour modifier une colonne numérique:

mean.height = list(BOY=69.5, GIRL=64.0)

subjects = data.frame(gender = factor(c("BOY", "GIRL", "GIRL", "BOY"))
         , height = c(71.0, 59.3, 62.1, 62.1))

apply(height, 1, function(x) x[2] - mean.height[[x[1]]])

La soustraction échoue car les colonnes sont converties en types de caractères.

Un correctif consiste à convertir en arrière la deuxième colonne en un nombre:

apply(subjects, 1, function(x) as.numeric(x[2]) - mean.height[[x[1]]])

Mais les conversions peuvent être évitées en gardant les colonnes séparées et en utilisant mapply():

mapply(function(x,y) y - mean.height[[x]], subjects$gender, subjects$height)

mapply()est nécessaire car [[ ]]n'accepte pas un argument vectoriel. Ainsi, l'itération de colonne pourrait être faite avant la soustraction en passant un vecteur à [], par un code un peu plus laid:

subjects$height - unlist(mean.height[subjects$gender])
John Mark
la source
1

Une fonction vraiment intéressante pour cela est adplyde plyr, surtout si vous souhaitez ajouter le résultat au dataframe d'origine. Cette fonction et sa cousine ddplym'ont sauvé beaucoup de maux de tête et de lignes de code!

df_appended <- adply(df, 1, mutate, sum=x+z)

Vous pouvez également appeler la fonction souhaitée.

df_appended <- adply(df, 1, mutate, sum=testFunc(x,z))
Zach S.
la source
adply () peut-il gérer les fonctions qui renvoient des listes ou des dataframes? par exemple, et si testFunc () renvoie une liste? unnest () serait-il utilisé pour le transformer en colonnes supplémentaires de votre df_appened?
val