Chame a função de aplicação em cada linha do quadro de dados com vários argumentos de cada linha

168

Eu tenho um quadro de dados com várias colunas. Para cada linha do quadro de dados, desejo chamar uma função na linha, e a entrada da função está usando várias colunas dessa linha. Por exemplo, digamos que eu tenho esses dados e esse testFunc que aceita dois argumentos:

> 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

Digamos que eu queira aplicar este testFunc às colunas x e z. Então, para a linha 1, eu quero 1 + 5, e para a linha 2, eu quero 2 + 6. Existe uma maneira de fazer isso sem escrever um loop for, talvez com a família de funções apply?

Eu tentei isso:

> 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

Mas tem erro, alguma idéia?

EDIT: a função real que eu quero chamar não é uma soma simples, mas é power.t.test. Eu usei a + b apenas para fins de exemplo. O objetivo final é poder fazer algo assim (escrito em pseudocódigo):

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

onde o resultado é um vetor de saídas para power.t.test para cada linha de df.

vasek1
fonte
Consulte também stackoverflow.com/a/24728107/946850 para saber o dplyrcaminho.
Krlmlr

Respostas:

137

Você pode aplicar applya um subconjunto dos dados originais.

 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 se sua função é apenas soma, use a versão vetorizada:

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

Se você quiser usar testFunc

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

EDIT Para acessar colunas por nome e não por índice, você pode fazer algo assim:

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(y) testFunc(y['z'],y['x']))
estudo
fonte
obrigado @agstudy, que funcionou! você sabe se existe alguma maneira de especificar os argumentos por nome em vez de por índice? então, para testFunc, algo como apply (dat [, c ('x', 'z')], 1, [pseudocódigo] testFunc (a = x, b = y))? o motivo é que estou chamando power.t.test dessa maneira, e gostaria de poder fazer referência aos parâmetros delta, power, sig.level por nome, em vez de colocá-los em uma matriz com posições pré-especificadas e, em seguida, referenciar essas posições, por ser mais robusto. de qualquer forma, muito obrigado!
vasek1
desculpe pelo comentário anterior, pressione enter antes de terminar de digitar :) excluiu e postou a versão completa.
vasek1
21
Não use applyno big data.frames, ele copiará o objeto inteiro (para converter em uma matriz). Isso também causará problemas Se você tiver diferentes objetos de classe no data.frame.
mnel
105

A data.frameé um list, então ...

Para funções vetorizadas do.call é geralmente uma boa aposta. Mas os nomes dos argumentos entram em jogo. Aqui você testFuncé chamado com args x e y no lugar de a e b. O ...permite argumentos irrelevantes para ser passado sem causar um erro:

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

Para funções não vetorizadas , mapplyfuncionará, mas você precisa corresponder à ordem dos argumentos ou nomeá-los explicitamente:

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

Às vezes applyfuncionará - como quando todos os argumentos são do mesmo tipo, portanto, coagir a data.frameuma matriz não causa problemas alterando os tipos de dados. Seu exemplo foi desse tipo.

Se sua função deve ser chamada dentro de outra função na qual todos os argumentos são passados, existe um método muito mais preciso que esse. Estude as primeiras linhas do corpo de lm()se você deseja seguir esse caminho.

user2087984
fonte
8
+10 se eu pudesse. Bem-vindo ao SO. grande resposta - pode valer a pena mencionar Vectorizecomo um wrapper para mapplyvetorizar funções
mnel
uau, isso é liso. A função original que usei não foi vetorizada (uma extensão personalizada em cima do power.t.test), mas acho que vou vetorizá-la e usar do.call (...). Obrigado!
vasek1
3
Apenas reiterando a nota de que esta resposta já diz que apply (df, 1, function (row) ...) pode ser ruim porque apply converte o df em uma matriz !!!! Isso pode ser ruim e resultar em muito puxão de cabelo. As alternativas a aplicar são muito necessárias!
Colin D
Muito obrigado para a diferenciação entre Vectorized / não-vetorizadas, isso é absolutamente a resposta que eu estava procurando
User632716
31

Usar 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
fonte
20

Nova resposta com dplyrpacote

Se a função que você deseja aplicar for vetorizada, você poderá usar a mutatefunção do dplyrpacote:

> 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

Resposta antiga com plyrpacote

Na minha humilde opinião, a ferramenta mais adequada para a tarefa é mdplydo plyrpacote.

Exemplo:

> 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

Infelizmente, como Bertjan Broeksema apontou, essa abordagem falha se você não usar todas as colunas do quadro de dados na mdplychamada. Por exemplo,

> 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)
Eu gosto de codificar
fonte
1
É bom quando você tem apenas um pequeno número de colunas. Eu tentei fazer algo como: mdply (df, function (col1, col3) {}) e mdply diminui, reclamando col2 não é usado. Agora, se você tem dezenas ou mesmo centenas de colunas, essa abordagem não é muito atraente.
Bertjan Broeksema
1
@BertjanBroeksema para modificar muitas colunas, você pode usar dplyr::mutate_each. Por exemplo: iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux
Você não poderia simplesmente passar elipses ou centenas para a função e simplesmente não usá-la? Isso deve corrigir esse erro?
Shawn
11

Outros apontaram corretamente que mapplyé feito para esse fim, mas (por uma questão de completude), um método conceitualmente mais simples é apenas usar um forloop.

for (row in 1:nrow(df)) { 
    df$newvar[row] <- testFunc(df$x[row], df$z[row]) 
}
Rsoren
fonte
1
Você está certo. Para usar o mapply de maneira eficaz, acho que você precisa entender que é apenas um loop "for" nos bastidores, especialmente se você é oriundo de programação processual como C ++ ou C #.
318 Contango
10

Muitas funções já são vetorizadas e, portanto, não há necessidade de iterações ( forloops ou *pplyfunções). O seu testFuncé um exemplo. Você pode simplesmente ligar para:

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

Em geral, eu recomendaria tentar essas abordagens de vetorização primeiro e ver se elas obtêm os resultados pretendidos.


Como alternativa, se você precisar passar vários argumentos para uma função que não é vetorizada, mapplypode ser o que você está procurando:

  mapply(power.t.test, df[, "x"], df[, "z"])
Ricardo Saporta
fonte
oh querida. Você sabe se existe uma maneira de especificar argumentos por nome no mapply? ou seja, algo como [pseudocódigo] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1
1
Sim, é exatamente como você o tem! ;)
Ricardo Saporta 25/02
4

Aqui está uma abordagem alternativa. É mais intuitivo.

Um aspecto-chave que considero que algumas das respostas não levaram em consideração, as quais aponto para a posteridade, é o aplicativo apply (), que permite fazer cálculos de linha com facilidade, mas apenas para dados matriciais (todos numéricos)

operações em colunas ainda são possíveis para quadros de dados:

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

Para operar em linhas, fazemos a transposição primeiro.

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

A desvantagem é que acredito que o R fará uma cópia da sua tabela de dados. O que poderia ser um problema de memória. (Isso é realmente triste, porque é programaticamente simples para o tdf ser apenas um iterador do df original, economizando memória, mas R não permite referência a ponteiro ou iterador.)

Além disso, uma pergunta relacionada é como operar em cada célula individual em um quadro de dados.

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

Eu vim aqui procurando o nome da função arrumada - que eu sabia que existia. Adicionando isso para (minha) referência futura e para tidyverseentusiastas: purrrlyr:invoke_rows( purrr:invoke_rowsem versões mais antigas).

Com a conexão aos métodos estatísticos padrão, como na pergunta original, o pacote da vassoura provavelmente ajudaria.

liborm
fonte
3

A resposta de @ user20877984 é excelente. Como eles resumiram muito melhor do que minha resposta anterior, aqui está minha tentativa (possivelmente ainda péssima) de uma aplicação do conceito:

Usando de do.callmaneira básica:

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

Trabalhando em um conjunto de dados completo:

# 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

lapplya power.t.testfunção para cada uma das linhas de valores especificados:

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
... ...
thelatemail
fonte
Haha talvez complicado? ;) por que você está usando t () e aplicando sobre 2, por que não aplicar apenas sobre 1?
Ricardo Saporta
3

data.table tem uma maneira realmente intuitiva de fazer isso:

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

O :=operador pode ser chamado entre colchetes para adicionar uma nova coluna usando uma função

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

Também é fácil aceitar constantes como argumentos, usando também este método:

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
fonte
1

Se as colunas data.frame forem de tipos diferentes, apply()há um problema. Uma sutileza sobre a iteração de linha é como a apply(a.data.frame, 1, ...)conversão implícita de tipos para tipos de caracteres quando colunas são tipos diferentes; por exemplo. uma coluna fator e numérica. Aqui está um exemplo, usando um fator em uma coluna para modificar uma coluna numérica:

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

A subtração falha porque as colunas são convertidas em tipos de caracteres.

Uma correção é converter novamente a segunda coluna em um número:

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

Mas as conversões podem ser evitadas mantendo as colunas separadas e usando mapply():

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

mapply()é necessário porque [[ ]]não aceita um argumento de vetor. Portanto, a iteração da coluna poderia ser feita antes da subtração passando um vetor para [], por um código um pouco mais feio:

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

Uma função muito bom para isso é adplya partir plyr, especialmente se você deseja anexar o resultado para a trama de dados de origem. Esta função e seu primo ddplyme salvaram muitas dores de cabeça e linhas de código!

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

Como alternativa, você pode chamar a função que deseja.

df_appended <- adply(df, 1, mutate, sum=testFunc(x,z))
Zach S.
fonte
pode adply () lidar com funções que retornam listas ou quadros de dados? por exemplo, e se testFunc () retornar uma lista? unnest () seria usado para transformá-lo em colunas adicionais do seu df_appened?
Val