วิธีผนวกแถวเข้ากับกรอบข้อมูล R


121

ฉันได้ดู StackOverflow แล้ว แต่ฉันไม่พบวิธีแก้ปัญหาเฉพาะสำหรับปัญหาของฉันซึ่งเกี่ยวข้องกับการต่อท้ายแถวเข้ากับเฟรมข้อมูล R

ฉันกำลังเริ่มต้นกรอบข้อมูล 2 คอลัมน์ที่ว่างเปล่าดังนี้

df = data.frame(x = numeric(), y = character())

จากนั้นเป้าหมายของฉันคือการวนซ้ำตามรายการค่าและในการวนซ้ำแต่ละครั้งจะเพิ่มค่าต่อท้ายรายการ ฉันเริ่มต้นด้วยรหัสต่อไปนี้

for (i in 1:10) {
    df$x = rbind(df$x, i)
    df$y = rbind(df$y, toString(i))
}

ฉันยังพยายามฟังก์ชั่นc, appendและmergeไม่ประสบความสำเร็จ โปรดแจ้งให้เราทราบหากคุณมีข้อเสนอแนะ


2
ฉันไม่คิดว่าจะรู้ว่า R ถูกนำมาใช้อย่างไร แต่ฉันต้องการละเว้นบรรทัดรหัสเพิ่มเติมที่จะต้องใช้ในการอัปเดตดัชนีในการทำซ้ำทุกครั้งและฉันไม่สามารถกำหนดขนาดของกรอบข้อมูลล่วงหน้าได้อย่างง่ายดายเพราะฉันไม่ ไม่รู้ว่าสุดท้ายแล้วจะต้องใช้กี่แถว โปรดจำไว้ว่าข้างต้นเป็นเพียงตัวอย่างของเล่นที่ทำซ้ำได้ ไม่ว่าจะด้วยวิธีใดขอบคุณสำหรับคำแนะนำของคุณ!
Gyan Veda

คำตอบ:


115

ปรับปรุง

ไม่ทราบว่าคุณกำลังพยายามทำอะไรฉันจะแบ่งปันคำแนะนำอีกหนึ่งข้อ: จัดสรรเวกเตอร์ล่วงหน้าของประเภทที่คุณต้องการสำหรับแต่ละคอลัมน์แทรกค่าลงในเวกเตอร์เหล่านั้นจากนั้นในตอนท้ายสร้างdata.frameไฟล์.

ดำเนินการต่อโดย Julian's f3(การจัดสรรล่วงหน้าdata.frame) เป็นตัวเลือกที่เร็วที่สุดที่กำหนดไว้:

# pre-allocate space
f3 <- function(n){
  df <- data.frame(x = numeric(n), y = character(n), stringsAsFactors = FALSE)
  for(i in 1:n){
    df$x[i] <- i
    df$y[i] <- toString(i)
  }
  df
}

นี่เป็นแนวทางที่คล้ายกัน แต่เป็นแนวทางที่data.frameสร้างขึ้นเป็นขั้นตอนสุดท้าย

# Use preallocated vectors
f4 <- function(n) {
  x <- numeric(n)
  y <- character(n)
  for (i in 1:n) {
    x[i] <- i
    y[i] <- i
  }
  data.frame(x, y, stringsAsFactors=FALSE)
}

microbenchmarkจากแพ็คเกจ "microbenchmark" จะให้ข้อมูลเชิงลึกที่ครอบคลุมมากกว่าsystem.time:

library(microbenchmark)
microbenchmark(f1(1000), f3(1000), f4(1000), times = 5)
# Unit: milliseconds
#      expr         min          lq      median         uq         max neval
#  f1(1000) 1024.539618 1029.693877 1045.972666 1055.25931 1112.769176     5
#  f3(1000)  149.417636  150.529011  150.827393  151.02230  160.637845     5
#  f4(1000)    7.872647    7.892395    7.901151    7.95077    8.049581     5

f1()(แนวทางด้านล่าง) ไม่มีประสิทธิภาพอย่างไม่น่าเชื่อเนื่องจากมีการเรียกใช้บ่อยเพียงใดdata.frameและเนื่องจากการเติบโตของวัตถุโดยทั่วไปจะช้าใน R. f3()จึงได้รับการปรับปรุงให้ดีขึ้นมากเนื่องจากการจัดสรรล่วงหน้า แต่data.frameโครงสร้างเองอาจเป็นส่วนหนึ่งของคอขวดที่นี่ f4()พยายามหลีกเลี่ยงปัญหาคอขวดโดยไม่กระทบกับแนวทางที่คุณต้องการ


คำตอบเดิม

นี่ไม่ใช่ความคิดที่ดีจริงๆ แต่ถ้าคุณต้องการทำด้วยวิธีนี้ฉันเดาว่าคุณสามารถลอง:

for (i in 1:10) {
  df <- rbind(df, data.frame(x = i, y = toString(i)))
}

โปรดทราบว่าในรหัสของคุณมีปัญหาอื่นอีกอย่างหนึ่ง:

  • คุณควรใช้stringsAsFactorsหากคุณต้องการให้อักขระไม่ถูกแปลงเป็นปัจจัย ใช้:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)

6
ขอบคุณ! นั่นช่วยแก้ปัญหาของฉันได้ เหตุใดจึง "ไม่ใช่ความคิดที่ดี" จริงๆ แล้ว x กับ y ผสมกันใน for loop อย่างไร?
Gyan Veda

5
@ user2932774 มันไม่มีประสิทธิภาพอย่างไม่น่าเชื่อในการขยายวัตถุด้วยวิธีนี้ใน R การปรับปรุง (แต่ก็ยังไม่จำเป็นต้องเป็นวิธีที่ดีที่สุด) คือการจัดสรรdata.frameขนาดสูงสุดที่คุณคาดหวังไว้ล่วงหน้าและเพิ่มค่าด้วยการ[แยก / การแทนที่
A5C1D2H2I1M1N2O1R2T1

1
ขอบคุณอนันดา ปกติฉันใช้การจัดสรรล่วงหน้า แต่ฉันไม่เห็นด้วยว่านี่ไม่ใช่ความคิดที่ดีจริงๆ มันขึ้นอยู่กับสถานการณ์ ในกรณีของฉันฉันกำลังจัดการกับข้อมูลขนาดเล็กและทางเลือกอื่นจะใช้เวลานานกว่าในการเขียนโค้ด นอกจากนี้นี่เป็นรหัสที่สวยงามกว่าเมื่อเทียบกับรหัสที่ต้องใช้ในการอัปเดตดัชนีตัวเลขเพื่อเติมเต็มส่วนที่เหมาะสมของกรอบข้อมูลที่จัดสรรไว้ล่วงหน้าในการทำซ้ำทุกครั้ง แค่อยากรู้ว่าอะไรคือ "วิธีที่ดีที่สุด" ในการทำงานนี้ให้สำเร็จในความคิดของคุณ? ฉันคิดว่าการจัดสรรล่วงหน้าน่าจะดีที่สุด
Gyan Veda

2
@ user2932774 มันเจ๋งมาก ฉันชื่นชมมุมมองของคุณเช่นกัน - ฉันไม่เคยทำงานกับชุดข้อมูลขนาดใหญ่มาก่อน ที่กล่าวว่าถ้าฉันจะเขียนฟังก์ชั่นหรืออะไรบางอย่างฉันมักจะใช้ความพยายามเพิ่มขึ้นเล็กน้อยในการปรับแต่งโค้ดเพื่อให้ได้ความเร็วที่ดีขึ้นทุกครั้งที่ทำได้ ดูการอัปเดตของฉันสำหรับตัวอย่างของความแตกต่างของความเร็วที่ค่อนข้างมาก
A5C1D2H2I1M1N2O1R2T1

1
โอ้โหแตกต่างกันมาก! ขอบคุณที่เรียกใช้การจำลองนั้นและสอนฉันเกี่ยวกับแพ็คเกจไมโครเบนช์มาร์ก ฉันเห็นด้วยกับคุณอย่างแน่นอนว่าเป็นเรื่องดีที่ได้ทุ่มเทอย่างเต็มที่ ในกรณีเฉพาะของฉันฉันคิดว่าฉันแค่ต้องการบางอย่างที่รวดเร็วและสกปรกในโค้ดบางอย่างที่ฉันอาจไม่ต้องเรียกใช้อีก :)
Gyan Veda

35

ลองเปรียบเทียบสามโซลูชันที่เสนอ:

# use rbind
f1 <- function(n){
  df <- data.frame(x = numeric(), y = character())
  for(i in 1:n){
    df <- rbind(df, data.frame(x = i, y = toString(i)))
  }
  df
}
# use list
f2 <- function(n){
  df <- data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)
  for(i in 1:n){
    df[i,] <- list(i, toString(i))
  }
  df
}
# pre-allocate space
f3 <- function(n){
  df <- data.frame(x = numeric(1000), y = character(1000), stringsAsFactors = FALSE)
  for(i in 1:n){
    df$x[i] <- i
    df$y[i] <- toString(i)
  }
  df
}
system.time(f1(1000))
#   user  system elapsed 
#   1.33    0.00    1.32 
system.time(f2(1000))
#   user  system elapsed 
#   0.19    0.00    0.19 
system.time(f3(1000))
#   user  system elapsed 
#   0.14    0.00    0.14

ทางออกที่ดีที่สุดคือจัดสรรพื้นที่ไว้ล่วงหน้า (ตามที่ตั้งใจไว้ใน R) Next-ทางออกที่ดีที่สุดคือการใช้งานlistและการแก้ปัญหาที่เลวร้ายที่สุด (อย่างน้อยก็ขึ้นอยู่กับผลการกำหนดเวลาเหล่านี้) rbindที่ดูเหมือนจะเป็น


ขอบคุณ! แม้ว่าฉันจะไม่เห็นด้วยกับข้อเสนอแนะของอนันดา ไม่ว่าฉันต้องการให้ตัวละครถูกแปลงเป็นระดับของปัจจัยหรือไม่จะขึ้นอยู่กับสิ่งที่ฉันต้องการทำกับผลลัพธ์ แม้ว่าฉันเดาว่าด้วยโซลูชันที่คุณเสนอ แต่จำเป็นต้องตั้งค่า stringsAsFactors เป็น FALSE
Gyan Veda

ขอบคุณสำหรับการจำลอง ฉันตระหนักดีว่าการจัดสรรล่วงหน้าดีที่สุดในแง่ของความเร็วในการประมวลผล แต่นั่นไม่ใช่ปัจจัยเดียวที่ฉันพิจารณาในการตัดสินใจเข้ารหัสนี้
Gyan Veda

1
ใน f1 คุณสับสนโดยกำหนดสตริงให้กับเวกเตอร์ตัวเลข x บรรทัดที่ถูกต้องคือdf <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov

14

สมมติว่าคุณไม่ทราบขนาดของ data.frame ล่วงหน้า อาจเป็นไม่กี่แถวหรือไม่กี่ล้าน คุณต้องมีคอนเทนเนอร์บางประเภทที่เติบโตอย่างไม่หยุดนิ่ง โดยคำนึงถึงประสบการณ์ของฉันและคำตอบที่เกี่ยวข้องทั้งหมดในดังนั้นฉันจึงมาพร้อมกับ 4 โซลูชันที่แตกต่างกัน:

  1. rbindlist ไปยัง data.frame

  2. ใช้การทำงานที่data.tableรวดเร็วsetและจับคู่กับการเพิ่มตารางด้วยตนเองเมื่อจำเป็น

  3. ใช้RSQLiteและต่อท้ายตารางที่จัดไว้ในหน่วยความจำ

  4. data.frameความสามารถของตัวเองในการเติบโตและใช้สภาพแวดล้อมที่กำหนดเอง (ซึ่งมีความหมายอ้างอิง) ในการจัดเก็บ data.frame ดังนั้นจึงจะไม่ถูกคัดลอกในทางกลับกัน

นี่คือการทดสอบวิธีการทั้งหมดสำหรับแถวต่อท้ายทั้งขนาดเล็กและจำนวนมาก แต่ละวิธีมี 3 ฟังก์ชันที่เกี่ยวข้อง:

  • create(first_element)ที่ส่งคืนวัตถุสำรองที่เหมาะสมพร้อมfirst_elementใส่

  • append(object, element)ที่elementต่อท้ายตาราง (แสดงโดยobject)

  • access(object)รับdata.frameองค์ประกอบที่แทรกทั้งหมด

rbindlist ไปยัง data.frame

มันค่อนข้างง่ายและตรงไปตรงมา:

create.1<-function(elems)
{
  return(as.data.table(elems))
}

append.1<-function(dt, elems)
{ 
  return(rbindlist(list(dt,  elems),use.names = TRUE))
}

access.1<-function(dt)
{
  return(dt)
}

data.table::set + เพิ่มตารางสองเท่าด้วยตนเองเมื่อจำเป็น

ฉันจะเก็บความยาวที่แท้จริงของตารางไว้ในrowcountแอตทริบิวต์

create.2<-function(elems)
{
  return(as.data.table(elems))
}

append.2<-function(dt, elems)
{
  n<-attr(dt, 'rowcount')
  if (is.null(n))
    n<-nrow(dt)
  if (n==nrow(dt))
  {
    tmp<-elems[1]
    tmp[[1]]<-rep(NA,n)
    dt<-rbindlist(list(dt, tmp), fill=TRUE, use.names=TRUE)
    setattr(dt,'rowcount', n)
  }
  pos<-as.integer(match(names(elems), colnames(dt)))
  for (j in seq_along(pos))
  {
    set(dt, i=as.integer(n+1), pos[[j]], elems[[j]])
  }
  setattr(dt,'rowcount',n+1)
  return(dt)
}

access.2<-function(elems)
{
  n<-attr(elems, 'rowcount')
  return(as.data.table(elems[1:n,]))
}

SQL ควรได้รับการปรับให้เหมาะสมสำหรับการแทรกบันทึกอย่างรวดเร็วดังนั้นในตอนแรกฉันมีความหวังสูงสำหรับRSQLiteการแก้ปัญหา

นี่คือการคัดลอกและวางคำตอบของKarsten W.ในหัวข้อที่คล้ายกัน

create.3<-function(elems)
{
  con <- RSQLite::dbConnect(RSQLite::SQLite(), ":memory:")
  RSQLite::dbWriteTable(con, 't', as.data.frame(elems))
  return(con)
}

append.3<-function(con, elems)
{ 
  RSQLite::dbWriteTable(con, 't', as.data.frame(elems), append=TRUE)
  return(con)
}

access.3<-function(con)
{
  return(RSQLite::dbReadTable(con, "t", row.names=NULL))
}

data.frameแถวต่อท้าย + สภาพแวดล้อมที่กำหนดเอง

create.4<-function(elems)
{
  env<-new.env()
  env$dt<-as.data.frame(elems)
  return(env)
}

append.4<-function(env, elems)
{ 
  env$dt[nrow(env$dt)+1,]<-elems
  return(env)
}

access.4<-function(env)
{
  return(env$dt)
}

ชุดทดสอบ:

เพื่อความสะดวกฉันจะใช้ฟังก์ชั่นการทดสอบเดียวเพื่อครอบคลุมทั้งหมดด้วยการโทรทางอ้อม (ฉันตรวจสอบแล้ว: การใช้do.callแทนการเรียกใช้ฟังก์ชันโดยตรงไม่ได้ทำให้โค้ดทำงานวัดผลได้นานขึ้น)

test<-function(id, n=1000)
{
  n<-n-1
  el<-list(a=1,b=2,c=3,d=4)
  o<-do.call(paste0('create.',id),list(el))
  s<-paste0('append.',id)
  for (i in 1:n)
  {
    o<-do.call(s,list(o,el))
  }
  return(do.call(paste0('access.', id), list(o)))
}

มาดูประสิทธิภาพของการแทรก n = 10 กัน

ฉันยังเพิ่มฟังก์ชัน 'ยาหลอก' (พร้อมคำต่อท้าย0) ที่ไม่ได้ทำอะไรเลย - เพียงเพื่อวัดค่าโสหุ้ยของการตั้งค่าการทดสอบ

r<-microbenchmark(test(0,n=10), test(1,n=10),test(2,n=10),test(3,n=10), test(4,n=10))
autoplot(r)

การกำหนดเวลาสำหรับการเพิ่ม n = 10 แถว

การกำหนดเวลาสำหรับ n = 100 แถว การกำหนดเวลาสำหรับ n = 1,000 แถว

สำหรับแถว 1E5 (การวัดที่ทำบน Intel (R) Core (TM) i7-4710HQ CPU @ 2.50GHz):

nr  function      time
4   data.frame    228.251 
3   sqlite        133.716
2   data.table      3.059
1   rbindlist     169.998 
0   placebo         0.202

ดูเหมือนว่าการเจือจางที่ใช้ SQLite แม้ว่าจะคืนความเร็วให้กับข้อมูลขนาดใหญ่ แต่ก็ไม่มีที่ไหนใกล้ data.table + การเติบโตแบบเอ็กซ์โพเนนเชียลด้วยตนเอง ความแตกต่างเกือบสองคำสั่งขนาด!

สรุป

หากคุณรู้ว่าคุณจะต่อท้ายแถวจำนวนค่อนข้างน้อย (n <= 100) ให้ดำเนินการต่อและใช้วิธีแก้ไขปัญหาที่ง่ายที่สุด: เพียงกำหนดแถวให้กับ data.frame โดยใช้เครื่องหมายวงเล็บและไม่สนใจข้อเท็จจริงที่ว่า data.frame คือ ไม่ได้เติมข้อมูลไว้ล่วงหน้า

สำหรับสิ่งอื่น ๆ ให้ใช้data.table::setและขยายข้อมูลตารางแบบทวีคูณ (เช่นการใช้รหัสของฉัน)


2
สาเหตุที่ SQLite ทำงานช้าคือใน INSERT INTO แต่ละอันต้อง REINDEX ซึ่งก็คือ O (n) โดยที่ n คือจำนวนแถว ซึ่งหมายความว่าการแทรกลงในฐานข้อมูล SQL ทีละแถวคือ O (n ^ 2) SQLite สามารถทำงานได้เร็วมากหากคุณแทรก data.frame ทั้งหมดในครั้งเดียว แต่ไม่ใช่วิธีที่ดีที่สุดในการเติบโตทีละบรรทัด
Julian Zucker

5

อัปเดตด้วย purrr, tidyr & dplyr

เนื่องจากคำถามนี้ลงวันที่แล้ว (6 ปี) คำตอบจึงไม่มีวิธีแก้ปัญหาด้วยแพ็คเกจที่ใหม่กว่า tidyr และ purrr ดังนั้นสำหรับคนที่ทำงานกับแพ็คเกจเหล่านี้ฉันต้องการเพิ่มวิธีแก้ปัญหาให้กับคำตอบก่อนหน้า - ทั้งหมดนี้น่าสนใจมากโดยเฉพาะ

ข้อได้เปรียบที่ใหญ่ที่สุดของ purrr และ tidyr คือ IMHO อ่านได้ดีกว่า purrr แทนที่ lapply ด้วยตระกูล map () ที่ยืดหยุ่นกว่า tidyr นำเสนอวิธีการ add_row ที่ใช้งานง่ายสุด ๆ - เพียงแค่ทำตามที่มันบอก :)

map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })

โซลูชันนี้สั้นและอ่านง่ายและค่อนข้างเร็ว:

system.time(
   map_df(1:1000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
   0.756   0.006   0.766

มันสเกลเกือบเป็นเส้นตรงดังนั้นสำหรับ 1e5 แถวประสิทธิภาพคือ:

system.time(
  map_df(1:100000, function(x) { df %>% add_row(x = x, y = toString(x)) })
)
   user  system elapsed 
 76.035   0.259  76.489 

ซึ่งจะทำให้อยู่ในอันดับที่สองรองจาก data.table (ถ้าคุณไม่สนใจยาหลอก) ในเกณฑ์มาตรฐานโดย @Adam Ryczkowski:

nr  function      time
4   data.frame    228.251 
3   sqlite        133.716
2   data.table      3.059
1   rbindlist     169.998 
0   placebo         0.202

add_rowคุณไม่จำเป็นต้องใช้ ตัวอย่างเช่น: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394

@ user3808394 ขอบคุณนั่นเป็นทางเลือกที่น่าสนใจ! หากมีคนต้องการสร้าง dataframe ตั้งแต่เริ่มต้นคุณจะสั้นกว่าดังนั้นจึงเป็นทางออกที่ดีกว่า ในกรณีที่คุณมี dataframe อยู่แล้วทางออกของฉันดีกว่าแน่นอน
Agile Bean

หากคุณมี dataframe อยู่แล้วคุณจะทำbind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))แทนการใช้add_row.
user3808394

2

ให้ใช้เวกเตอร์ 'จุด' ซึ่งมีตัวเลขตั้งแต่ 1 ถึง 5

point = c(1,2,3,4,5)

หากเราต้องการต่อท้ายตัวเลข 6 ที่ใดก็ได้ภายในเวกเตอร์คำสั่งด้านล่างอาจมีประโยชน์

i) เวกเตอร์

new_var = append(point, 6 ,after = length(point))

ii) คอลัมน์ของตาราง

new_var = append(point, 6 ,after = length(mtcars$mpg))

คำสั่งappendรับสามอาร์กิวเมนต์:

  1. เวกเตอร์ / คอลัมน์ที่จะแก้ไข
  2. ค่าที่จะรวมไว้ในเวกเตอร์ที่แก้ไข
  3. ตัวห้อยหลังจากนั้นค่าจะถูกต่อท้าย

ง่าย ... !! ขออภัยในกรณีใด ๆ ... !


1

วิธีแก้ปัญหาทั่วไปเพิ่มเติมสำหรับอาจมีดังต่อไปนี้

    extendDf <- function (df, n) {
    withFactors <- sum(sapply (df, function(X) (is.factor(X)) )) > 0
    nr          <- nrow (df)
    colNames    <- names(df)
    for (c in 1:length(colNames)) {
        if (is.factor(df[,c])) {
            col         <- vector (mode='character', length = nr+n) 
            col[1:nr]   <- as.character(df[,c])
            col[(nr+1):(n+nr)]<- rep(col[1], n)  # to avoid extra levels
            col         <- as.factor(col)
        } else {
            col         <- vector (mode=mode(df[1,c]), length = nr+n)
            class(col)  <- class (df[1,c])
            col[1:nr]   <- df[,c] 
        }
        if (c==1) {
            newDf       <- data.frame (col ,stringsAsFactors=withFactors)
        } else {
            newDf[,c]   <- col 
        }
    }
    names(newDf) <- colNames
    newDf
}

ฟังก์ชัน expandDf () ขยายกรอบข้อมูลด้วย n แถว

ตัวอย่างเช่น:

aDf <- data.frame (l=TRUE, i=1L, n=1, c='a', t=Sys.time(), stringsAsFactors = TRUE)
extendDf (aDf, 2)
#      l i n c                   t
# 1  TRUE 1 1 a 2016-07-06 17:12:30
# 2 FALSE 0 0 a 1970-01-01 01:00:00
# 3 FALSE 0 0 a 1970-01-01 01:00:00

system.time (eDf <- extendDf (aDf, 100000))
#    user  system elapsed 
#   0.009   0.002   0.010
system.time (eDf <- extendDf (eDf, 100000))
#    user  system elapsed 
#   0.068   0.002   0.070

0

วิธีแก้ปัญหาของฉันเกือบจะเหมือนกับคำตอบเดิม แต่ไม่ได้ผลสำหรับฉัน

ดังนั้นฉันตั้งชื่อคอลัมน์และใช้งานได้:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.