web-dev-qa-db-fra.com

Appelez une fonction semblable à une application sur chaque ligne du cadre de données avec plusieurs arguments pour chaque ligne

J'ai un dataframe avec plusieurs colonnes. Pour chaque ligne du cadre 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, supposons 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. Ainsi, pour la ligne 1, je veux 1 + 5, et pour la ligne 2, 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 que je souhaite 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 ceci (é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.

132
vasek1

Vous pouvez appliquer 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 somme utiliser la version vectorisée:

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

Si vous voulez utiliser testFunc

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

EDITPour 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']))
112
agstudy

Un data.frame est une list, donc ...

Pour fonctions vectorisées do.call est généralement un bon choix. Mais les noms des arguments entrent en jeu. Ici, votre testFunc est appelée avec les arguments 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 , mapply fonctionnera, mais vous devez respecter l'ordre des arguments ou les nommer explicitement:

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

Parfois, apply fonctionnera - comme lorsque tous les arguments sont du même type, le fait de forcer le data.frame à une matrice ne pose pas de problème 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 les arguments sont tous passés, il existe une méthode beaucoup plus sournoise que celles-ci. Étudiez les premières lignes du corps de lm() si vous voulez suivre cette voie.

99
user2087984

Utilisez 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
27
Chinmay Patil

Nouvelle réponse avec le paquet dplyr

Si la fonction que vous souhaitez appliquer est vectorisée, vous pouvez utiliser la fonction mutate du package dplyr:

> 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 le paquet plyr

À mon humble avis, l’outil le mieux adapté à cette tâche est mdply du package plyr.

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 du bloc de données dans l'appel mdply . 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)
16
I Like to Code

D'autres ont correctement souligné que mapply est créé à cette fin, mais (par souci d'exhaustivité) une méthode conceptuellement plus simple consiste simplement à utiliser une boucle for

for (row in 1:nrow(df)) { 
    df$newvar[row] <- testFunc(df$x[row], df$z[row]) 
}
10
rsoren

De nombreuses fonctions étant déjà vectorisées, aucune itération n'est nécessaire (ni les boucles for ni les fonctions *pply). Votre testFunc en est un exemple. Vous pouvez simplement appeler: 

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

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


Sinon, si vous devez passer plusieurs arguments à une fonction non vectorisée, mapply peut être ce que vous recherchez: 

  mapply(power.t.test, df[, "x"], df[, "z"])
10
Ricardo Saporta

Je suis venu ici à la recherche de tidyverse nom de la fonction - dont je savais qu’il existait. Ajout de ceci pour (ma) future référence et pour les passionnés de tidyverse: purrrlyr:invoke_rows (purrr:invoke_rows dans les versions antérieures). 

Avec la connexion aux méthodes de statistiques standard comme dans la question initiale, le paquet broom serait probablement utile.

4
liborm

Voici une approche alternative. C'est plus intuitif.

Un aspect clé que certaines des réponses ne prennent pas en compte et que je signale pour la postérité est que 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 cadres de données:

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 va faire une copie de votre tableau de données ..___, ce qui pourrait être un problème de mémoire. (C’est vraiment triste, car tdf est un programme simple, il suffit d’être un itérateur du fichier original, é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 agir sur chaque cellule individuelle dans une trame de données. 

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

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

Utiliser do.call de manière basique:

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

lapply la fonction power.t.test à chacune des lignes des 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
... ...
2
thelatemail

data.table a également une manière 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 crochets 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
2
Pete M

Si les colonnes data.frame sont de types différents, apply() a un problème . Une subtilité à propos de l'itération de ligne correspond à la manière dont apply(a.data.frame, 1, ...) effectue La conversion de type implicite en types de caractère lorsque les colonnes sont de types différents; une colonne de facteur et 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 retour 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 de vecteur. Ainsi, l'itération de la colonne Pourrait être effectuée avant la soustraction en passant un vecteur à [], Avec un code un peu plus laid:

subjects$height - unlist(mean.height[subjects$gender])
0
John Mark

Une fonction vraiment intéressante pour cela est adply à partir de plyr, surtout si vous souhaitez ajouter le résultat au cadre de données d'origine. Cette fonction et sa cousine ddply m'ont évité beaucoup de maux de tête et de lignes de code!

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

Alternativement, vous pouvez appeler la fonction que vous désirez.

df_appended <- adply(df, 1, mutate, sum=testFunc(x,z))
0
Zach S.