Wywołaj funkcję podobną do zastosowania w każdym wierszu ramki danych z wieloma argumentami z każdego wiersza

168

Mam ramkę danych z wieloma kolumnami. Dla każdego wiersza w ramce danych chcę wywołać funkcję w wierszu, a dane wejściowe funkcji używają wielu kolumn z tego wiersza. Na przykład, powiedzmy, że mam te dane i ten testFunc, który akceptuje dwa argumenty:

> 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

Powiedzmy, że chcę zastosować ten testFunc do kolumn x i z. Więc dla wiersza 1 chcę 1 + 5, a dla wiersza 2 chcę 2 + 6. Czy istnieje sposób na zrobienie tego bez pisania pętli for, być może z rodziną funkcji Apply?

Próbowałem tego:

> 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

Ale masz błąd, jakieś pomysły?

EDYCJA: faktyczna funkcja, którą chcę wywołać, nie jest prostą sumą, ale jest to power.t.test. Użyłem a + b tylko dla celów. Ostatecznym celem jest zrobienie czegoś takiego (napisanego w pseudokodzie):

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

gdzie wynikiem jest wektor wyjść dla power.t.test dla każdego wiersza df.

vasek1
źródło
Zobacz także stackoverflow.com/a/24728107/946850, aby dplyrdowiedzieć się więcej.
krlmlr

Odpowiedzi:

137

Możesz zastosować applydo podzbioru oryginalnych danych.

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

lub jeśli twoja funkcja jest po prostu sumą, użyj wersji wektorowej:

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

Jeśli chcesz użyć testFunc

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

EDYCJA Aby uzyskać dostęp do kolumn według nazwy, a nie indeksu, możesz zrobić coś takiego:

 testFunc <- function(a, b) a + b
 apply(dat[,c('x','z')], 1, function(y) testFunc(y['z'],y['x']))
agstudy
źródło
dzięki @agstudy, zadziałało! czy wiesz, czy istnieje sposób na określenie argumentów według nazwy zamiast indeksu? więc dla testFunc, coś takiego jak apply (dat [, c ('x', 'z')], 1, [pseudokod] testFunc (a = x, b = y))? powodem jest to, że wywołuję power.t.test w ten sposób i chciałbym móc odwoływać się do parametrów delta, potęgi, sig.level według nazwy, zamiast umieszczać je w tablicy z wcześniej określonymi pozycjami, a następnie odnosząc się do tych pozycji, aby były bardziej solidne. w każdym razie wielkie dzięki!
vasek1
przepraszam za poprzedni komentarz, wciśnij Enter przed zakończeniem pisania :) usunąłem go i opublikowałem pełną wersję.
vasek1
21
Nie używaj applyna big data.frames, spowoduje to skopiowanie całego obiektu (w celu konwersji na macierz). Spowoduje to również problemy, jeśli masz różne obiekty klas w data.frame.
mnel
105

A data.framejest list, więc ...

W przypadku funkcji wektoryzowanych do.call jest zwykle dobrym rozwiązaniem. Ale w grę wchodzą nazwy argumentów. Tutaj twój testFuncjest wywoływany z argumentami x i y zamiast a i b. ...Pozwala nieistotne args być przekazywane bez powodowania błędu:

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

Dla funkcji non-wektorowy , mapplybędzie działać, ale trzeba dopasować kolejność args lub jawnie nazwać je:

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

Czasami applyzadziała - tak jak wtedy, gdy wszystkie argumenty są tego samego typu, więc data.frameprzekształcenie macierzy w macierz nie powoduje problemów przy zmianie typów danych. Twój przykład był tego rodzaju.

Jeśli twoja funkcja ma zostać wywołana w ramach innej funkcji, do której są przekazywane wszystkie argumenty, istnieje znacznie lepsza metoda niż te. Przestudiuj pierwsze linie ciała, lm()jeśli chcesz iść tą trasą.

user2087984
źródło
8
+10, gdybym mógł. Witamy w SO. świetna odpowiedź - warto o tym wspomnieć Vectorizejako opakowanie mapplydo wektoryzacji funkcji
mnel
wow, to jest sprytne. Oryginalna funkcja, której użyłem, nie była wektoryzowana (niestandardowe rozszerzenie na szczycie power.t.test), ale myślę, że wektoryzuję ją i użyję do.call (...). Dzięki!
vasek1
3
Powtarzam tylko uwagę, że ta odpowiedź już mówi, że apply (df, 1, function (row) ...) może być złe, ponieważ Apply konwertuje df na macierz !!!! Może to być złe i skutkować wyrywaniem wielu włosów. Potrzebne są alternatywy do zastosowania!
Colin D
Dziękuję bardzo za rozróżnienie między wektoryzacją / niewektoryzacją, to jest absolutnie odpowiedź, której szukałem
User632716
31

Posługiwać się 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
źródło
20

Nowa odpowiedź z dplyrpakietem

Jeśli funkcja, którą chcesz zastosować, jest wektoryzowana, możesz użyć mutatefunkcji z dplyrpakietu:

> 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

Stara odpowiedź z plyrpakietem

Moim skromnym zdaniem, narzędzie najlepiej nadaje się do tego zadania jest mdplyod plyrpakietu.

Przykład:

> 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

Niestety, jak zauważył Bertjan Broeksema , to podejście zawodzi, jeśli nie używasz wszystkich kolumn ramki danych w mdplywywołaniu. Na przykład,

> 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)
Lubię kodować
źródło
1
Fajnie, gdy masz tylko niewielką liczbę kolumn. Próbowałem zrobić coś takiego: mdply (df, function (col1, col3) {}) i mdply wyskakuje, narzekając, że col2 jest nieużywany. Teraz, jeśli masz dziesiątki lub nawet setki kolumn, to podejście nie jest zbyt atrakcyjne.
Bertjan Broeksema
1
@BertjanBroeksema, aby zmodyfikować wiele kolumn, możesz użyć dplyr::mutate_each. Na przykład: iris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux
Czy nie mógłbyś po prostu przekazać elipsy lub setek do funkcji i po prostu jej nie używać? To powinno naprawić ten błąd?
Shawn
11

Inni słusznie wskazali, że mapplyjest on stworzony do tego celu, ale (ze względu na kompletność) koncepcyjnie prostszą metodą jest po prostu użycie forpętli.

for (row in 1:nrow(df)) { 
    df$newvar[row] <- testFunc(df$x[row], df$z[row]) 
}
rsoren
źródło
1
Masz rację. Aby efektywnie używać mapply, myślę, że musisz zrozumieć, że jest to po prostu pętla „for” za kulisami, zwłaszcza jeśli pochodzisz z programowania proceduralnego, takiego jak C ++ lub C #.
Contango
10

Wiele funkcji jest już wektoryzowanych, więc nie ma potrzeby wykonywania żadnych iteracji (ani forpętli, ani *pplyfunkcji). Twój testFuncjest jednym z takich przykładów. Możesz po prostu zadzwonić:

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

Ogólnie zalecałbym najpierw wypróbowanie takich metod wektoryzacji i sprawdzenie, czy dadzą one zamierzone rezultaty.


Alternatywnie, jeśli chcesz przekazać wiele argumentów do funkcji, która nie jest wektoryzowana, mapplymoże być tym, czego szukasz:

  mapply(power.t.test, df[, "x"], df[, "z"])
Ricardo Saporta
źródło
och, słodko. Czy wiesz, czy istnieje sposób określania argumentów według nazwy w mapply? tj. coś w rodzaju [pseudokod] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1
1
Tak, jest dokładnie taki, jaki masz! ;)
Ricardo Saporta
4

Oto alternatywne podejście. Jest bardziej intuicyjny.

Jednym z kluczowych aspektów, które, moim zdaniem, nie wzięły pod uwagę niektórych odpowiedzi, na które zwracam uwagę dla potomności, jest zastosowanie () pozwala na łatwe obliczenia wierszy, ale tylko dla danych macierzowych (wszystkich danych liczbowych)

operacje na kolumnach są nadal możliwe dla ramek danych:

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

Aby operować na wierszach, najpierw dokonujemy transpozycji.

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

Wadą jest to, że wierzę, że R zrobi kopię twojej tabeli danych. Co może być problemem z pamięcią. (Jest to naprawdę smutne, ponieważ tdf jest programistycznie prosty, aby po prostu być iteratorem do oryginalnego df, oszczędzając w ten sposób pamięć, ale R nie pozwala na odwoływanie się do wskaźnika lub iteratora).

Powiązane pytanie dotyczy również sposobu działania na każdej pojedynczej komórce w ramce danych.

newdf <- as.data.frame(lapply(df, function(x) {sapply(x, myFunctionForEachCell()}))
BAMF4bacon
źródło
4

Przyszedłem tutaj, szukając nazwy funkcji tidyverse - o której wiedziałem, że istnieje. Dodanie tego dla (moich) przyszłych odniesień i dla tidyverseentuzjastów: purrrlyr:invoke_rows( purrr:invoke_rowsw starszych wersjach).

W połączeniu ze standardowymi metodami statystyk, jak w pierwotnym pytaniu, pakiet miotły prawdopodobnie by pomógł.

liborm
źródło
3

Odpowiedź @ user20877984 jest doskonała. Ponieważ podsumowali to znacznie lepiej niż moja poprzednia odpowiedź, oto moja (prawdopodobnie wciąż tandetna) próba zastosowania tego pojęcia:

Używanie do.callw sposób podstawowy:

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

Praca na pełnym zestawie danych:

# 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

lapplypower.t.testfunkcja każdego z rzędów wymienionych wartości:

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
źródło
Haha może zawiły? ;) dlaczego używasz t () i nakładasz nad 2, dlaczego po prostu nie nakładasz 1?
Ricardo Saporta,
3

data.table ma też naprawdę intuicyjny sposób:

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

:=Operator może być wywołana w nawiasach w celu dodania nowej kolumny przy użyciu funkcji

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

Za pomocą tej metody można również łatwo zaakceptować stałe jako argumenty:

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.
źródło
1

Jeśli kolumny data.frame są różnych typów, apply()występuje problem. Subtelność dotycząca iteracji wierszy polega na tym, jak apply(a.data.frame, 1, ...)niejawna konwersja typów na typy znakowe, gdy kolumny są różnych typów; na przykład. współczynnik i kolumna liczbowa. Oto przykład użycia współczynnika w jednej kolumnie do zmodyfikowania kolumny liczbowej:

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

Odejmowanie kończy się niepowodzeniem, ponieważ kolumny są konwertowane na typy znaków.

Jedną z poprawek jest konwersja wsteczna drugiej kolumny na liczbę:

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

Ale konwersji można uniknąć, oddzielając kolumny i używając mapply():

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

mapply()jest potrzebny, ponieważ [[ ]]nie akceptuje argumentu wektorowego. Więc iteracja kolumny może być wykonana przed odejmowaniem, przekazując wektor do [], za pomocą nieco bardziej brzydkiego kodu:

subjects$height - unlist(mean.height[subjects$gender])
John Mark
źródło
1

Naprawdę fajną funkcją do tego jest adplyfrom plyr, zwłaszcza jeśli chcesz dołączyć wynik do oryginalnej ramki danych. Ta funkcja i jej kuzyn ddplyzaoszczędziły mi wielu bólów głowy i linii kodu!

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

Alternatywnie możesz wywołać żądaną funkcję.

df_appended <- adply(df, 1, mutate, sum=testFunc(x,z))
Zach S.
źródło
czy adply () może obsługiwać funkcje, które zwracają listy lub ramki danych? np. co jeśli testFunc () zwróci listę? czy unnest () zostałoby użyte do przekształcenia go w dodatkowe kolumny twojego df_appened?
Val