Вызовите функцию, подобную 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.