Вызовите функцию, подобную apply, в каждой строке информационного кадра с несколькими аргументами из каждой строки


168

У меня есть датафрейм с несколькими столбцами. Для каждой строки в кадре данных я хочу вызвать функцию в строке, и для ввода функции используются несколько столбцов из этой строки. Например, допустим, у меня есть эти данные и этот testFunc, который принимает два аргумента:

> 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

Допустим, я хочу применить этот testFunc к столбцам x и z. Итак, для строки 1 я хочу 1 + 5, а для строки 2 я хочу 2 + 6. Есть ли способ сделать это без написания цикла for, может быть, с помощью семейства apply?

Я попробовал это:

> 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

Но есть ошибки, есть идеи?

РЕДАКТИРОВАТЬ: фактическая функция, которую я хочу вызвать, это не простая сумма, но это power.t.test. Я использовал + B только для примера. Конечная цель - сделать что-то вроде этого (написано в псевдокоде):

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

где результатом является вектор выходных данных для power.t.test для каждой строки df.


См. Также stackoverflow.com/a/24728107/946850 для dplyrспособа.
krlmlr

Ответы:


137

Вы можете применить applyк подмножеству исходных данных.

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

или если ваша функция просто сумма, используйте векторизованную версию:

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

Если вы хотите использовать testFunc

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

РЕДАКТИРОВАТЬ Чтобы получить доступ к столбцам по имени, а не по индексу, вы можете сделать что-то вроде этого:

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

спасибо @agstudy, это сработало! Вы знаете, есть ли способ указать аргументы по имени, а не по индексу? Итак, для testFunc что-то вроде apply (dat [, c ('x', 'z')], 1, [псевдокод] testFunc (a = x, b = y))? причина в том, что я вызываю power.t.test таким образом, и я хотел бы иметь возможность ссылаться на параметры delta, power, sig.level по имени, а не вставлять их в массив с предварительно заданными позициями, а затем ссылаясь на эту позицию, для того, чтобы быть более устойчивым. в любом случае большое спасибо!
vasek1

извините за предыдущий комментарий, нажмите ввод, прежде чем закончить ввод :) удалил его и разместил полную версию.
vasek1

21
Не используйте applyна больших data.frames, он скопирует весь объект (для преобразования в матрицу). Это также вызовет проблемы, если у вас есть различные объекты класса в data.frame.
mnel

105

data.frameЭто list, так что ...

Для векторизованных функций do.call обычно хорошая ставка. Но имена аргументов вступают в игру. Здесь ваш testFuncвызывается с аргументами x и y вместо a и b. Параметр ...позволяет передавать нерелевантные аргументы, не вызывая ошибки:

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

Для не-векторных функций , mapplyбудет работать, но вы должны соответствовать упорядочиванию арга или явно назвать их:

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

Иногда applyбудет работать - например, когда все аргументы имеют одинаковый тип, поэтому приведение data.frameк матрице не вызывает проблем при изменении типов данных. Ваш пример был такого рода.

Если ваша функция должна вызываться в другой функции, в которую передаются все аргументы, существует гораздо более приятный метод, чем этот. Изучите первые строки тела, lm()если вы хотите пойти по этому пути.


8
+10 если бы мог. Добро пожаловать в ТАК. отличный ответ - возможно, стоит упомянуть Vectorizeв качестве оболочки mapplyдля векторизации функций
mnel

вау, это гладко Исходная функция, которую я использовал, не была векторизована (пользовательское расширение поверх power.t.test), но я думаю, что я буду векторизовать ее и использовать do.call (...). Спасибо!
vasek1

3
Просто повторяя примечание, что этот ответ уже говорит, что apply (df, 1, function (row) ...) может быть плохим, потому что apply преобразует df в матрицу !!!! Это может быть плохо и привести к большому вытягиванию волос. Альтернативы для применения очень нужны!
Колин Д.

Большое спасибо за то, что вы
сделали

31

использование 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

Новый ответ с dplyrпакетом

Если функция, которую вы хотите применить, векторизована, то вы можете использовать mutateфункцию из 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

Старый ответ с plyrпакетом

По моему скромному мнению, инструмент, который лучше всего подходит для этой задачи, взят mdplyиз plyrпакета.

Пример:

> 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

К сожалению, как отметил Бертжан Бруксема , этот подход не работает, если вы не используете все столбцы фрейма данных в mdplyвызове. Например,

> 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
Приятно, когда у вас есть только небольшое количество столбцов. Я пытался сделать что-то вроде: mdply (df, function (col1, col3) {}) и mdply выручает, жалуясь, что col2 не используется. Теперь, если у вас есть десятки или даже сотни столбцов, этот подход не очень привлекателен.
Бертьян Бруксема

1
@BertjanBroeksema, чтобы изменить много столбцов, вы можете использовать dplyr::mutate_each. Например: iris %>% mutate_each(funs(half = . / 2),-Species).
Поль Ружье,

Не могли бы вы просто передать elipses или сотни в функцию и просто не использовать ее? Это должно исправить эту ошибку?
Шон

11

Другие правильно указали, что mapplyсделано для этой цели, но (для полноты картины) концептуально более простым методом является просто использование forцикла.

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

1
Ты прав. Чтобы эффективно использовать mapply, я думаю, вы должны понимать, что это просто «for» цикл за кулисами, особенно если вы пришли из процедурного фона программирования, такого как C ++ или C #.
Контанго

10

Многие функции уже векторизованы, и поэтому нет необходимости в каких-либо итерациях (ни forциклах, ни *pplyфункциях). Вы testFuncодин из таких примеров. Вы можете просто позвонить:

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

В общем, я бы рекомендовал сначала попробовать такие подходы к векторизации и посмотреть, принесут ли они вам ожидаемые результаты.


В качестве альтернативы, если вам нужно передать несколько аргументов в функцию, которая не является векторизованной, mapplyможет быть то, что вы ищете:

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

о сладкий. Знаете ли вы, есть ли способ указать аргументы по имени в mapply? то есть что-то вроде [псевдокод] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ...)?
vasek1

1
Да, это именно так, как у вас есть! ;)
Рикардо Сапорта

4

Вот альтернативный подход. Это более интуитивно понятно.

Я считаю, что один из ключевых аспектов, который я не учел в некоторых ответах, на которые я обращаю внимание в потомках, - apply () позволяет легко выполнять вычисления строк, но только для матричных (все числовые) данных.

операции над столбцами возможны еще для датафреймов:

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

Чтобы оперировать строками, мы сначала делаем транспонирование.

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

Недостатком является то, что я верю, что R сделает копию вашей таблицы данных. Что может быть проблемой памяти. (Это действительно грустно, потому что программно просто для tdf просто быть итератором исходного df, тем самым экономя память, но R не допускает ссылки на указатель или итератор.)

Кроме того, связанный вопрос, как работать с каждой отдельной ячейкой в ​​кадре данных.

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

4

Я пришел сюда в поисках названия функции tidyverse, которое, как я знал, существовало. Добавление этого для (моей) будущей ссылки и для tidyverseэнтузиастов: purrrlyr:invoke_rows( purrr:invoke_rowsв более старых версиях).

С подключением к стандартным методам статистики, как в первоначальном вопросе, пакет метлы , вероятно, поможет.


3

@ user20877984 ответ отличный. Так как они суммировали это намного лучше, чем мой предыдущий ответ, вот моя (возможно, все еще дрянная) попытка применения концепции:

Используя do.callв основном:

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

Работа над полным набором данных:

# 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.testфункции к каждому из рядов заданных значений:

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

Хаха, возможно, запутанный? ;) почему вы используете t () и применяете более 2, а не просто применяете поверх 1?
Рикардо Сапорта

3

data.table имеет действительно интуитивный способ сделать это:

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

:=Оператор может быть вызван в скобках , чтобы добавить новый столбец , используя функцию

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

Также легко принять константы в качестве аргументов, используя этот метод:

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

Если столбцы data.frame имеют разные типы, apply()возникает проблема. Тонкость итераций строки заключается в том, как apply(a.data.frame, 1, ...)происходит неявное преобразование типов в символьные типы, когда столбцы имеют разные типы; например. коэффициент и числовой столбец. Вот пример использования коэффициента в одном столбце для изменения числового столбца:

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

Вычитание не выполняется, поскольку столбцы преобразуются в типы символов.

Одним из исправлений является обратное преобразование второго столбца в число:

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

Но преобразований можно избежать, если разделить столбцы и использовать mapply():

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

mapply()необходим, потому [[ ]]что не принимает векторный аргумент. Таким образом, итерация столбца может быть выполнена до вычитания путем передачи вектора в []немного более уродливый код:

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

1

Действительно хорошая функция для этого adplyиз plyr, особенно если вы хотите , чтобы добавить результат к исходному dataframe. Эта функция и ее двоюродный брат ddplyизбавили меня от многих головных болей и строк кода!

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

Кроме того, вы можете вызвать функцию, которую вы хотите.

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

может adply () иметь дело с функциями, которые возвращают списки или кадры данных? например, что если testFunc () возвращает список? Будет ли unnest () использоваться для преобразования его в дополнительные столбцы вашего df_appened?
вал
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.