Rufen Sie die Apply-ähnliche Funktion für jede Zeile des Datenrahmens mit mehreren Argumenten aus jeder Zeile auf


168

Ich habe einen Datenrahmen mit mehreren Spalten. Für jede Zeile im Datenrahmen möchte ich eine Funktion in der Zeile aufrufen, und die Eingabe der Funktion verwendet mehrere Spalten aus dieser Zeile. Nehmen wir zum Beispiel an, ich habe diese Daten und diese testFunc, die zwei Argumente akzeptiert:

> 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

Angenommen, ich möchte diesen TestFunc auf die Spalten x und z anwenden. Für Zeile 1 möchte ich 1 + 5 und für Zeile 2 möchte ich 2 + 6. Gibt es eine Möglichkeit, dies zu tun, ohne eine for-Schleife zu schreiben, möglicherweise mit der Apply-Funktionsfamilie?

Ich habe es versucht:

> 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

Aber Fehler, irgendwelche Ideen?

BEARBEITEN: Die eigentliche Funktion, die ich aufrufen möchte, ist keine einfache Summe, sondern power.t.test. Ich habe a + b nur zum Beispiel verwendet. Das Endziel ist es, in der Lage zu sein, so etwas zu tun (in Pseudocode geschrieben):

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

wobei das Ergebnis ein Vektor von Ausgaben für power.t.test für jede Zeile von df ist.


Siehe auch stackoverflow.com/a/24728107/946850 für den dplyrWeg.
krlmlr

Antworten:


137

Sie können applyeine Teilmenge der Originaldaten anwenden .

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

oder wenn Ihre Funktion nur eine Summe ist, verwenden Sie die vektorisierte Version:

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

Wenn Sie verwenden möchten testFunc

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

BEARBEITEN Um auf Spalten nach Namen und nicht nach Index zuzugreifen, können Sie Folgendes tun:

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

danke @agstudy, das hat funktioniert! Wissen Sie, ob es eine Möglichkeit gibt, die Argumente nach Namen anstatt nach Index anzugeben? Also, für testFunc, so etwas wie anwenden (dat [, c ('x', 'z')], 1, [Pseudocode] testFunc (a = x, b = y))? Der Grund dafür ist, dass ich power.t.test auf diese Weise aufrufe und gerne in der Lage wäre, die Parameter Delta, Power und Sig.level namentlich zu referenzieren, anstatt sie in ein Array mit vorgegebenen Positionen zu stecken und dann Verweisen auf diese Position aus Gründen der Robustheit. auf jeden fall vielen dank!
Vasek1

Entschuldigung für den vorherigen Kommentar, drücke die Eingabetaste, bevor du mit der Eingabe fertig bist :) habe ihn gelöscht und die Vollversion gepostet.
Vasek1

21
Nicht applyfür Big Data verwenden. Frames kopiert das gesamte Objekt (um es in eine Matrix zu konvertieren). Dies führt auch zu Problemen, wenn Sie unterschiedliche Klassenobjekte im data.frame haben.
mnel

105

A data.frameist a list, also ...

Für vektorisierte Funktionen do.call ist normalerweise eine gute Wahl. Aber die Namen der Argumente kommen ins Spiel. Hier wird Ihr testFuncmit den Argumenten x und y anstelle von a und b aufgerufen. Das ...ermöglicht es irrelevant args , ohne dass es einen Fehler übergeben werden:

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

Funktioniert für nicht vektorisierte Funktionen , mapplySie müssen jedoch die Reihenfolge der Argumente anpassen oder sie explizit benennen:

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

Manchmal applyfunktioniert es - als ob alle Argumente vom gleichen Typ sind, was das erzwingtdata.frame einer Matrix keine Probleme durch Ändern des Datentyps verursacht. Ihr Beispiel war von dieser Art.

Wenn Ihre Funktion innerhalb einer anderen Funktion aufgerufen werden soll, an die alle Argumente übergeben werden, gibt es eine viel einfachere Methode als diese. Studieren Sie die ersten Zeilen des Körpers, lm()wenn Sie diesen Weg gehen möchten.


8
+10 wenn ich könnte. Willkommen bei SO. gute Antwort - es könnte erwähnenswert sein, Vectorizeals Wrapper mapplyFunktionen zu vektorisieren
mnel

wow, das ist schlau. Die ursprüngliche Funktion, die ich verwendet habe, wurde nicht vektorisiert (eine benutzerdefinierte Erweiterung zusätzlich zu power.t.test), aber ich denke, ich werde sie vektorisieren und do.call (...) verwenden. Vielen Dank!
Vasek1

3
Nur die Anmerkung wiederholen, dass diese Antwort bereits besagt, dass apply (df, 1, function (row) ...) schlecht sein kann, weil apply den df in eine Matrix konvertiert !!!! Dies kann schlecht sein und zu viel Haarziehen führen. Die Alternativen zur Bewerbung werden dringend benötigt!
Colin D

Vielen Dank für die Unterscheidung zwischen vektorisiert / nicht vektorisiert, dies ist absolut die Antwort, die ich gesucht habe
User632716

31

Verwenden 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

Neue Antwort mit dplyrPaket

Wenn die Funktion, die Sie anwenden möchten, vektorisiert ist, können Sie die mutateFunktion aus dem dplyrPaket verwenden:

> 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

Alte Antwort mit plyrPaket

Meiner bescheidenen Meinung nach stammt das für die Aufgabe am besten geeignete Tool mdplyaus dem plyrPaket.

Beispiel:

> 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

Wie Bertjan Broeksema betonte, schlägt dieser Ansatz leider fehl, wenn Sie nicht alle Spalten des Datenrahmens im mdplyAufruf verwenden. Beispielsweise,

> 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
Es ist schön, wenn Sie nur eine kleine Anzahl von Spalten haben. Ich habe versucht, etwas zu tun wie: mdply (df, function (col1, col3) {}) und mdply rettet, beschwert sich, dass col2 nicht verwendet wird. Wenn Sie zehn oder sogar Hunderte von Spalten haben, ist dieser Ansatz nicht sehr attraktiv.
Bertjan Broeksema

1
@BertjanBroeksema, um viele Spalten zu ändern, können Sie verwenden dplyr::mutate_each. Zum Beispiel : iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux

Könnten Sie nicht einfach Elipsen oder Hunderte in die Funktion einfügen und sie einfach nicht verwenden? Das sollte diesen Fehler beheben?
Shawn

11

Andere haben richtig darauf hingewiesen, dass mapplydies zu diesem Zweck gemacht wurde, aber der Vollständigkeit halber besteht eine konzeptionell einfachere Methode darin, nur eine forSchleife zu verwenden.

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

1
Du hast recht. Um Mapply effektiv zu nutzen, muss man verstehen, dass es sich nur um eine "for" -Schleife hinter den Kulissen handelt, insbesondere wenn Sie aus einem prozeduralen Programmierhintergrund wie C ++ oder C # stammen.
Contango

10

Viele Funktionen sind bereits vektorisiert, sodass keine Iterationen erforderlich sind (weder forSchleifen noch *pplyFunktionen). Ihr testFuncist ein solches Beispiel. Sie können einfach anrufen:

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

Im Allgemeinen würde ich empfehlen, zuerst solche Vektorisierungsansätze auszuprobieren und zu prüfen, ob sie Ihnen die beabsichtigten Ergebnisse liefern.


Wenn Sie alternativ mehrere Argumente an eine nicht vektorisierte Funktion übergeben müssen, ist dies mapplymöglicherweise das, wonach Sie suchen:

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

Oh wie süß. Wissen Sie, ob es eine Möglichkeit gibt, Argumente in mapply nach Namen anzugeben? dh so etwas wie [Pseudocode] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
Vasek1

1
Ja, es ist genau so, wie du es hast! ;)
Ricardo Saporta

4

Hier ist ein alternativer Ansatz. Es ist intuitiver.

Ein wichtiger Aspekt, den einige der Antworten meines Erachtens nicht berücksichtigt haben, auf den ich für die Nachwelt hinweise, ist, dass Sie mit apply () problemlos Zeilenberechnungen durchführen können, jedoch nur für Matrixdaten (alle numerischen Daten)

Operationen an Spalten sind für Datenrahmen weiterhin möglich:

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

Um Zeilen zu bearbeiten, machen wir zuerst die Transponierung.

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

Der Nachteil ist, dass ich glaube, dass R eine Kopie Ihrer Datentabelle erstellen wird. Welches könnte ein Speicherproblem sein. (Dies ist wirklich traurig, da es für tdf programmatisch einfach ist, nur ein Iterator für das ursprüngliche df zu sein, wodurch Speicherplatz gespart wird, aber R keine Zeiger- oder Iteratorreferenzierung zulässt.)

Eine verwandte Frage ist auch, wie jede einzelne Zelle in einem Datenrahmen bearbeitet werden soll.

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

4

Ich kam hierher, um nach dem Namen der Tidyverse- Funktion zu suchen - von dem ich wusste, dass er existiert. Hinzufügen für (meine) zukünftige Referenz und für tidyverseEnthusiasten: purrrlyr:invoke_rows(purrr:invoke_rows in älteren Versionen).

Bei Verbindung mit Standardstatistikmethoden wie in der ursprünglichen Frage würde das Besenpaket wahrscheinlich helfen.


3

Die Antwort von @ user20877984 ist ausgezeichnet. Da sie es weitaus besser zusammengefasst haben als meine vorherige Antwort, ist hier mein (möglicherweise immer noch mieser) Versuch, das Konzept anzuwenden:

Unter Verwendung do.callin einer grundlegenden Art und Weise:

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

Arbeiten an einem vollständigen Datensatz:

# 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

lapplydie power.t.testFunktion für jede der Zeilen mit den angegebenen Werten:

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 vielleicht verwickelt? ;) warum benutzt du t () und bewirbst dich über 2, warum bewirbst du dich nicht einfach über 1?
Ricardo Saporta

3

data.table hat auch eine sehr intuitive Möglichkeit, dies zu tun:

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

Der :=Operator kann in Klammern aufgerufen werden, um mithilfe einer Funktion eine neue Spalte hinzuzufügen

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

Mit dieser Methode können Konstanten auch leicht als Argumente akzeptiert werden:

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

Wenn data.frame-Spalten unterschiedliche Typen haben, apply()liegt ein Problem vor. Eine Feinheit in Bezug auf die Zeileniteration ist, wie apply(a.data.frame, 1, ...)die implizite Typkonvertierung in Zeichentypen erfolgt, wenn Spalten unterschiedliche Typen sind. z.B. ein Faktor und eine numerische Spalte. Hier ist ein Beispiel, bei dem ein Faktor in einer Spalte zum Ändern einer numerischen Spalte verwendet wird:

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

Die Subtraktion schlägt fehl, da die Spalten in Zeichentypen konvertiert werden.

Eine Lösung besteht darin, die zweite Spalte in eine Zahl umzuwandeln:

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

Die Konvertierungen können jedoch vermieden werden, indem die Spalten getrennt bleiben und Folgendes verwendet wird mapply():

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

mapply()wird benötigt, weil [[ ]]kein Vektorargument akzeptiert wird. Die Spalteniteration könnte also vor der Subtraktion durchgeführt werden, indem ein Vektor an []einen etwas hässlicheren Code übergeben wird:

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

1

Eine wirklich schöne Funktion hierfür ist adplyvon plyr, insbesondere wenn Sie das Ergebnis an den ursprünglichen Datenrahmen anhängen möchten. Diese Funktion und ihr Cousin ddplyhaben mir viele Kopfschmerzen und Codezeilen erspart!

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

Alternativ können Sie die gewünschte Funktion aufrufen.

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

kann adply () mit Funktionen umgehen, die Listen oder Datenrahmen zurückgeben? Was ist, wenn testFunc () eine Liste zurückgibt? würde unnest () verwendet, um es in zusätzliche Spalten Ihres df_appened zu mutieren?
Val
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.