Chiamare una funzione simile a ogni riga di frame di dati con più argomenti per ogni riga


168

Ho un dataframe con più colonne. Per ogni riga nel frame di dati, desidero chiamare una funzione sulla riga e l'input della funzione utilizza più colonne di quella riga. Ad esempio, supponiamo che io abbia questi dati e questo testFunc che accetta due argomenti:

> 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

Diciamo che voglio applicare questo testFunc alle colonne xe z. Quindi, per la riga 1 voglio 1 + 5, e per la riga 2 voglio 2 + 6. C'è un modo per farlo senza scrivere un ciclo for, forse con la famiglia di funzioni apply?

Ho provato questo:

> 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

Ma hai ricevuto qualche idea?

EDIT: la vera funzione che voglio chiamare non è una semplice somma, ma è power.t.test. Ho usato a + b solo a scopo di esempio. L'obiettivo finale è riuscire a fare qualcosa del genere (scritto in pseudocodice):

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

dove il risultato è un vettore di output per power.t.test per ogni riga di df.


Vedi anche stackoverflow.com/a/24728107/946850 per il dplyrmodo.
krlmlr,

Risposte:


137

È possibile applicare applya un sottoinsieme dei dati originali.

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

o se la tua funzione è solo somma usa la versione vettoriale:

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

Se vuoi usare testFunc

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

MODIFICA Per accedere alle colonne in base al nome e non all'indice, puoi fare qualcosa del genere:

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

grazie @agstudy, ha funzionato! sai se esiste un modo per specificare gli arg per nome anziché per indice? quindi, per testFunc, qualcosa come apply (dat [, c ('x', 'z')], 1, [pseudocode] testFunc (a = x, b = y))? il motivo è che sto chiamando power.t.test in questo modo e mi piacerebbe poter fare riferimento ai parametri delta, power, sig.level per nome invece di inserirli in un array con posizioni predefinite e quindi riferendosi a quella posizione, per il motivo di essere più robusto. in ogni caso grazie mille!
vasek1,

scusate il commento precedente, premete invio prima di aver finito di digitare :) cancellato e pubblicato la versione completa.
vasek1,

21
Non utilizzare applysu big data.frames copierà l'intero oggetto (per convertirlo in una matrice). Ciò causerà anche problemi se si hanno oggetti di classe diversi all'interno di data.frame.
Mnel

105

A data.frameè un list, quindi ...

Per le funzioni vettoriali do.call è di solito una buona scommessa. Ma entrano in gioco i nomi degli argomenti. Qui il tuo testFuncviene chiamato con args xey al posto di aeb. Il ...permette args irrilevanti da passare senza provocare un errore:

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

Per le funzioni non vettoriali , funzionerà , mapplyma è necessario abbinare l'ordinamento degli arg o denominarli esplicitamente:

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

A volte applyfunzionerà, come quando tutti gli arg sono dello stesso tipo, quindi forzare la data.framematrice non causa problemi cambiando i tipi di dati. Il tuo esempio era di questo tipo.

Se la tua funzione deve essere chiamata all'interno di un'altra funzione in cui tutti gli argomenti vengono passati, esiste un metodo molto più semplice di questi. Studia le prime linee del corpo di lm()se vuoi percorrere quella strada.


8
+10 se potessi. Benvenuti in SO. ottima risposta - potrebbe valere la pena menzionare Vectorizecome wrapper mapplyper vettorializzare le funzioni
mnel

wow, è perfetto. La funzione originale che ho usato non è stata vettorializzata (un'estensione personalizzata sopra power.t.test), ma penso che la vettorializzerò e userò do.call (...). Grazie!
vasek1,

3
Ribadendo solo la nota che questa risposta dice già che si applica (df, 1, funzione (riga) ...) può essere un male perché si applica converte il df in una matrice !!!! Questo può essere negativo e provocare un sacco di capelli. Le alternative da applicare sono molto necessarie!
Colin D,

Grazie mille per la differenziazione tra vettorizzato / non vettorializzato, questa è assolutamente la risposta che stavo cercando
User632716

31

Uso 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

20

Nuova risposta con dplyrpacchetto

Se la funzione che si desidera applicare è vettorializzata, è possibile utilizzare la mutatefunzione dal dplyrpacchetto:

> 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

Vecchia risposta con plyrpacchetto

A mio modesto parere, lo strumento più adatto al compito è mdplydal plyrpacchetto.

Esempio:

> 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

Sfortunatamente, come ha sottolineato Bertjan Broeksema , questo approccio fallisce se non si utilizzano tutte le colonne del frame di dati nella mdplychiamata. Per esempio,

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

1
È bello quando hai solo un piccolo numero di colonne. Ho provato a fare qualcosa del tipo: mdply (df, function (col1, col3) {}) e mdply esce, lamentando che col2 non è stato usato. Ora, se hai decine o addirittura centinaia di colonne, questo approccio non è molto attraente.
Bertjan Broeksema il

1
@BertjanBroeksema per modificare molte colonne, è possibile utilizzare dplyr::mutate_each. Ad esempio: iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux,

Non potresti semplicemente passare elipses, o le centinaia nella funzione e semplicemente non usarla? Ciò dovrebbe correggere quell'errore?
Shawn

11

Altri hanno correttamente sottolineato che mapplyè stato creato per questo scopo, ma (per completezza) un metodo concettualmente più semplice è solo quello di utilizzare un forciclo.

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

1
Hai ragione. Per usare mapply in modo efficace, penso che devi capire che è solo un ciclo "for" dietro le quinte, specialmente se provieni da un background di programmazione procedurale come C ++ o C #.
Contango

10

Molte funzioni sono già in vettorializzazione, quindi non è necessario eseguire alcuna iterazione (né forloop né *pplyfunzioni). Il tuo testFuncè un esempio. Puoi semplicemente chiamare:

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

In generale, consiglierei prima di provare questi approcci di vettorializzazione e vedere se ti danno i risultati desiderati.


In alternativa, se è necessario passare più argomenti a una funzione che non è vettoriale, mapplypotrebbe essere quello che si sta cercando:

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

che dolce. Sai se esiste un modo per specificare gli argomenti per nome in modo mappato? cioè qualcosa come [pseudocodice] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1,

1
Sì, è esattamente come lo hai tu! ;)
Ricardo Saporta,

4

Ecco un approccio alternativo. È più intuitivo

Un aspetto chiave che ritengo che alcune delle risposte non abbiano preso in considerazione, che sottolineo per i posteri, è apply () consente di eseguire facilmente calcoli di riga, ma solo per dati a matrice (tutti numerici)

le operazioni sulle colonne sono ancora possibili per i frame di dati:

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

Per operare su file, eseguiamo prima la trasposizione.

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

Il rovescio della medaglia è che credo che R farà una copia della tabella dei dati. Quale potrebbe essere un problema di memoria. (Questo è veramente triste, perché è programmaticamente semplice per tdf essere solo un iteratore del df originale, risparmiando così memoria, ma R non consente il riferimento a puntatore o iteratore.)

Inoltre, una domanda correlata è come operare su ogni singola cella in un frame di dati.

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

4

Sono venuto qui alla ricerca del nome della funzione ordinata , che sapevo esistesse. Aggiungendo questo per (il mio) riferimento futuro e per gli tidyverseappassionati: purrrlyr:invoke_rows(purrr:invoke_rows nelle versioni precedenti).

Con la connessione a metodi di statistiche standard come nella domanda originale, probabilmente il pacchetto di ginestra sarebbe di aiuto.


3

La risposta di @utente20877984 è eccellente. Da quando lo hanno riassunto molto meglio della mia precedente risposta, ecco il mio (possibilmente ancora scadente) tentativo di applicare il concetto:

Usando do.callin modo semplice:

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

Lavorando su un set di dati 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

lapplyla power.t.testfunzione per ciascuna delle righe di valori specificati:

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

Haha contorto forse? ;) perché stai usando t () e applicando over 2, perché non semplicemente applicare over 1?
Ricardo Saporta,

3

data.table ha un modo davvero intuitivo di fare anche questo:

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' :=operatore può essere chiamato tra parentesi per aggiungere una nuova colonna usando una funzione

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

È anche facile accettare le costanti come argomenti e utilizzare questo metodo:

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

1

Se le colonne data.frame sono di tipi diversi, apply()si verifica un problema. Una sottigliezza sull'iterazione delle righe è come la apply(a.data.frame, 1, ...)conversione di tipo implicita in tipi di carattere quando le colonne sono di tipo diverso; per esempio. un fattore e una colonna numerica. Ecco un esempio, usando un fattore in una colonna per modificare una colonna numerica:

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 sottrazione fallisce perché le colonne vengono convertite in tipi di caratteri.

Una soluzione consiste nel riconvertire la seconda colonna in un numero:

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

Ma le conversioni possono essere evitate mantenendo le colonne separate e usando mapply():

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

mapply()è necessario perché [[ ]]non accetta un argomento vettoriale. Quindi l'iterazione della colonna potrebbe essere eseguita prima della sottrazione passando un vettore a [], con un codice un po 'più brutto:

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

1

Davvero un bel funzione di questo è adplyda plyr, soprattutto se si desidera aggiungere il risultato al dataframe originale. Questa funzione e suo cugino ddplymi hanno risparmiato un sacco di mal di testa e righe di codice!

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

In alternativa, puoi chiamare la funzione che desideri.

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

adply () può gestire funzioni che restituiscono elenchi o frame di dati? ad esempio, cosa succede se testFunc () restituisce un elenco? unestest () verrebbe utilizzato per mutarlo in colonne aggiuntive di df_appened?
val
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.