เรียกใช้ฟังก์ชั่นคล้ายกับการเรียกใช้ในแต่ละแถวของ dataframe ที่มีหลายอาร์กิวเมนต์จากแต่ละแถว


168

ฉันมีชื่อไฟล์ที่มีหลายคอลัมน์ สำหรับแต่ละแถวใน dataframe ฉันต้องการเรียกใช้ฟังก์ชันบนแถวและอินพุตของฟังก์ชันใช้หลายคอลัมน์จากแถวนั้น ตัวอย่างเช่นสมมติว่าฉันมีข้อมูลนี้และ testFunc ซึ่งยอมรับสอง args:

> 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 มีวิธีทำเช่นนี้โดยไม่เขียนลูปสำหรับบางทีอาจจะมีครอบครัวใช้ฟังก์ชั่น?

ฉันลองสิ่งนี้:

> 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 เพื่อเป็นตัวอย่างเท่านั้น เป้าหมายสุดท้ายคือสามารถทำสิ่งนี้ (เขียนใน pseudocode):

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

หรือถ้าฟังก์ชันของคุณเป็นเพียงผลรวมใช้เวอร์ชัน vectorized:

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 มันใช้ได้ผล! คุณรู้หรือไม่ว่ามีวิธีใดที่จะระบุ args ตามชื่อแทน by index? ดังนั้นสำหรับ testFunc สิ่งที่ต้องการใช้ (dat [, c ('x', 'z')], 1, [pseudocode] testFunc (a = x, b = y))? เหตุผลก็คือฉันเรียก power.t.test ในลักษณะนี้และฉันชอบที่จะสามารถอ้างอิงเดลต้าพลังงาน sig.level params โดยใช้ชื่อแทนที่จะติดไว้ในอาร์เรย์ที่มีตำแหน่งที่กำหนดไว้ล่วงหน้าแล้ว อ้างอิงตำแหน่งเหล่านั้นเพื่อเหตุผลที่แข็งแกร่งมากขึ้น ในกรณีใด ๆ ขอบคุณมาก!
vasek1

ขออภัยเกี่ยวกับความคิดเห็นก่อนหน้ากด Enter ก่อนพิมพ์เสร็จ :) ลบและโพสต์เวอร์ชันเต็ม
vasek1

21
อย่าใช้applyกับ data.frames ขนาดใหญ่มันจะคัดลอกวัตถุทั้งหมด (เพื่อแปลงเป็นเมทริกซ์) สิ่งนี้จะทำให้เกิดปัญหาหากคุณมีคลาสวัตถุที่แตกต่างกันภายใน data.frame
mnel

105

A data.frameคือlist...

สำหรับฟังก์ชั่น vectorized do.callมักจะเป็นทางออกที่ดี แต่ชื่อของการโต้แย้งเข้ามาเล่น ที่นี่คุณtestFuncถูกเรียกด้วย args x และ y แทนที่ a และ b ...ช่วยให้ args ไม่เกี่ยวข้องกับถูกส่งผ่านโดยไม่ก่อให้เกิดข้อผิดพลาด:

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

สำหรับฟังก์ชั่นที่ไม่ vectorized , mapplyจะทำงาน แต่คุณจำเป็นต้องตรงกับการสั่งซื้อของ args หรือชื่อพวกเขาอย่างชัดเจน:

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

บางครั้งapplyจะทำงาน - เมื่อทุก args เป็นประเภทเดียวกันดังนั้นการบังคับdata.frameให้เมทริกซ์ไม่ทำให้เกิดปัญหาโดยการเปลี่ยนชนิดข้อมูล ตัวอย่างของคุณเป็นแบบนี้

หากฟังก์ชั่นของคุณจะถูกเรียกภายในฟังก์ชั่นอื่นซึ่งการส่งผ่านข้อโต้แย้งทั้งหมดจะมีวิธีการลดทอนมากกว่าสิ่งเหล่านี้ ศึกษาบรรทัดแรกของเนื้อหาlm()ถ้าคุณต้องการไปเส้นทางนั้น


8
+10 ถ้าทำได้ ยินดีต้อนรับสู่ SO คำตอบที่ดี - มันอาจจะคุ้มค่าที่จะกล่าวถึงVectorizeในฐานะ wrapper สำหรับmapplyฟังก์ชั่น vectorize
mnel

ว้าวนั่นมันลื่น ฟังก์ชั่นดั้งเดิมที่ฉันใช้ไม่ได้เป็นแบบเวกเตอร์ (ส่วนขยายที่กำหนดเองที่ด้านบนของ power.t.test) แต่ฉันคิดว่าฉันจะทำให้เป็นแบบเวกเตอร์และใช้ do.call (... ) ขอบคุณ!
vasek1

3
เพียงแค่ย้ำข้อความที่คำตอบนี้แล้วบอกว่าใช้ (df, 1, ฟังก์ชั่น (แถว) ... ) อาจไม่ดีเพราะใช้แปลง df ลงในเมทริกซ์ !!!! สิ่งนี้อาจไม่ดีและทำให้เกิดการดึงผมได้มากมาย ทางเลือกในการสมัครมีความจำเป็นมาก!
โคลิน D

ขอบคุณมากสำหรับความแตกต่างระหว่าง Vectorized / non-vectorized นี่คือคำตอบที่ฉันกำลังมองหา
User632716

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

น่าเสียดายที่Bertjan Broeksemaชี้ให้เห็นวิธีการนี้จะล้มเหลวหากคุณไม่ใช้คอลัมน์ทั้งหมดของกรอบข้อมูลในการ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, ฟังก์ชั่น (col1, col3) {}) และ mdply bail out, บ่น col2 ไม่ได้ใช้ ตอนนี้ถ้าคุณมีหลายสิบหรือหลายร้อยคอลัมน์วิธีนี้ไม่น่าสนใจมาก
Bertjan Broeksema

1
@BertjanBroeksema dplyr::mutate_eachการปรับเปลี่ยนจำนวนมากของคอลัมน์คุณสามารถใช้ ตัวอย่างเช่นiris %>% mutate_each(funs(half = . / 2),-Species).
Paul Rougieux

คุณไม่สามารถส่งผ่าน elipses หรือร้อยเข้าไปในฟังก์ชันและไม่ใช้ ที่ควรแก้ไขข้อผิดพลาดนั้น
Shawn

11

คนอื่น ๆ ได้ชี้ให้เห็นอย่างถูกต้องว่าmapplyทำขึ้นเพื่อจุดประสงค์นี้ แต่ (เพื่อประโยชน์ของความสมบูรณ์) วิธีที่ง่ายกว่าในแนวคิดคือใช้forลูป

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

1
คุณถูก. ในการใช้งานแผนที่อย่างมีประสิทธิภาพฉันคิดว่าคุณต้องเข้าใจว่ามันเป็นเพียง "สำหรับ" วนรอบฉากโดยเฉพาะอย่างยิ่งถ้าคุณมาจากพื้นหลังการเขียนโปรแกรมขั้นตอนเช่น C ++ หรือ C #
Contango

10

มีฟังก์ชั่นมากมายที่ใช้การแปลงเป็นเวกเตอร์อยู่แล้วดังนั้นจึงไม่จำเป็นต้องทำซ้ำ (ทั้งforลูปหรือ*pplyฟังก์ชั่น) testFuncตัวอย่างหนึ่งของคุณคือ คุณสามารถโทร:

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

โดยทั่วไปฉันขอแนะนำให้ลองใช้วิธีการเวกเตอร์ดังกล่าวก่อนและดูว่าพวกเขาทำให้คุณได้ผลลัพธ์ตามที่ต้องการหรือไม่


อีกทางเลือกหนึ่งถ้าคุณต้องการส่งอาร์กิวเมนต์จำนวนมากไปยังฟังก์ชั่นที่ไม่ได้เป็นเวกเตอร์mapplyอาจเป็นสิ่งที่คุณกำลังมองหา:

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

โอ้ที่รัก คุณรู้หรือไม่ว่ามีวิธีในการระบุข้อโต้แย้งตามชื่อในรูปแบบ? เช่นบางสิ่งเช่น [pseudocode] mapply (power.t.test, delta = df [, 'delta'], power = df [, 'power'], ... )?
vasek1

1
ใช่มันเป็นอย่างที่คุณมีมัน! ;)
Ricardo Saporta

4

นี่คือวิธีการอื่น มันใช้งานง่ายมากขึ้น

ประเด็นสำคัญอย่างหนึ่งที่ฉันรู้สึกว่าคำตอบบางคำไม่ได้นำมาพิจารณาซึ่งฉันชี้ให้เห็นถึงลูกหลานนั้นมีการใช้ () ช่วยให้คุณทำการคำนวณแถวได้อย่างง่ายดาย แต่สำหรับข้อมูลเมทริกซ์ (ตัวเลขทั้งหมด) เท่านั้น

การดำเนินการกับคอลัมน์ยังคงเป็นไปได้สำหรับดาต้าเฟรม:

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?
Ricardo Saporta

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โดยเฉพาะอย่างยิ่งถ้าคุณต้องการผนวกผลลัพธ์ไปที่ดาต้าเฟรมดั้งเดิม ฟังก์ชั่นนี้และลูกพี่ลูกน้องของมันddplyช่วยลดอาการปวดหัวและรหัสได้มากมาย!

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

หรือคุณสามารถเรียกฟังก์ชั่นที่คุณต้องการ

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

adply () สามารถจัดการกับฟังก์ชันที่ส่งคืนรายการหรือ dataframes ได้หรือไม่ เช่นจะเกิดอะไรขึ้นถ้า testFunc () ส่งคืนรายการ จะใช้คำสั่งไม่ได้ () เพื่อแปลงเป็นคอลัมน์เพิ่มเติมของ df_appened ของคุณหรือไม่
Val
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.