Як додати рядки до кадру даних R


121

Я оглянув StackOverflow, але не можу знайти рішення, специфічне для моєї проблеми, яке передбачає додавання рядків до кадру даних R.

Я ініціалізую порожній кадр даних з двох стовпців, як описано нижче.

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.

Продовжуючи Джуліана 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і через те, що вирощування об'єктів у Р., як правило, повільно 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
Дякую! Це вирішує мою проблему. Чому це "насправді не гарна ідея"? І яким чином х і у змішуються в циклі for?
Gyan Veda

5
@ user2932774, Це неймовірно неефективно вирощувати об’єкт таким чином у Р. Поліпшенням (але все ж не обов'язково найкращим способом) було б попередньо виділити data.frameкінцевий розмір, який ви очікуєте, і додати значення в результаті [вилучення / заміни.
A5C1D2H2I1M1N2O1R2T1

1
Спасибі, Ананда. Я зазвичай йду з попередньою розподілом, але я не згоден, що це насправді не дуже гарна ідея. Це залежить від ситуації. У моєму випадку я маю справу з невеликими даними, і альтернатива буде більш трудомісткою для кодування. Крім того, це більш елегантний код порівняно з тим, необхідний для оновлення числових індексів для заповнення відповідних частин попередньо виділеного кадру даних на кожній ітерації. Просто цікаво, який "найкращий спосіб" виконати це завдання, на вашу думку? Я б подумав, що попереднє розміщення було б найкращим.
Gyan Veda

2
@ user2932774, це здорово. Я також ціную вашу перспективу - я теж майже ніколи не працюю з великими наборами даних. Це означає, що якщо я буду працювати над написанням функції чи чогось іншого, я б зазвичай витрачав трохи додаткових зусиль, намагаючись налаштувати код, щоб досягти кращої швидкості, коли це можливо. Дивіться моє оновлення на прикладі досить величезної різниці швидкостей.
A5C1D2H2I1M1N2O1R2T1

1
Ого, це величезна різниця! Дякую за те, що запустив таке моделювання та навчав мене про пакет мікробірок. Я безумовно згоден з вами, що приємно докладати додаткових зусиль. У моєму конкретному випадку, я думаю, я просто хотів чогось швидкого і брудного в якомусь коді, який мені, можливо, більше ніколи не доведеться запускати. :)
Gyan Veda

34

Давайте порівняємо три запропоновані рішення:

# 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). Наступним найкращим рішенням є використання list, і, здається , найгірше рішення (принаймні, виходячи з цих результатів) rbind.


Дякую! Хоча я не згоден з пропозицією Ананда. Хочу я перетворювати символи на рівні коефіцієнта чи ні, залежатиме від того, що я хочу зробити з результатом. Хоча я здогадуюсь, що із запропонованим вами рішенням потрібно встановити stringsAsFactors на FALSE.
Gyan Veda

Дякую за моделювання. Я усвідомлюю, що попереднє розміщення найкраще з точки зору швидкості обробки, але це не єдиний фактор, який я враховував при прийнятті цього рішення щодо кодування.
Gyan Veda

1
У f1 ви переплуталися, призначивши рядок числовому вектору x. Правильна лінія:df <- rbind(df, data.frame(x = i, y = toString(i)))
Ельдар Агаларов

14

Припустимо, ви просто не знаєте розмір data.frame заздалегідь. Це може бути кілька рядів, або кілька мільйонів. Потрібно мати якийсь контейнер, який динамічно зростає. Беручи до уваги мій досвід та всі відповіді на відповіді, я маю чотири різних рішення:

  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 = 1000 рядів

Для рядків 1E5 (вимірювання зроблені на процесорі Intel (R) Core (TM) i7-4710HQ CPU @ 2,50 ГГц):

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 одразу, але це не найкраще в зростанні по черзі.
Джуліан Цукер

5

Оновлення за допомогою purrr, tidyr & dplyr

Оскільки питання вже датоване (6 років), у відповідях відсутнє рішення з новішими пакетами tidyr і purrr. Тож людям, що працюють з цими пакунками, я хочу додати рішення до попередніх відповідей - все досить цікаво, особливо.

Найбільша перевага purrr та tidyr - краща читабельність IMHO. purrr замінює настільки гнучку сімейство 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 

що зробило б це місце на другому місці після таблиці даних (якщо ви ігноруєте плацебо) у еталоні від @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)) }).
користувач3808394

@ user3808394 дякую, це цікава альтернатива! якщо хтось хоче створити кадр даних з нуля, ваш коротший, тому краще рішення. якщо ви вже маєте кадр даних, моє рішення, звичайно, краще.
Agile Bean

Якщо у вас вже є кадр даних, ви б зробили це bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))замість цього add_row.
користувач3808394

2

Давайте візьмемо векторну 'точку', яка має числа від 1 до 5

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

якщо ми хочемо додати число 6 де-небудь всередині вектора, тоді команда нижче може стати в нагоді

і) Вектори

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
}

Функція exteDf () розширює кадр даних з 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))
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.