Как добавить строки во фрейм данных 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, но я хотел игнорировать дополнительную строку кода, которая потребовалась бы для обновления индексов на каждой итерации, и я не могу легко предварительно выделить размер фрейма данных, потому что я не Не знаю, сколько строк это займет в итоге. Помните, что это всего лишь игрушечный пример, предназначенный для воспроизведения. В любом случае, спасибо за ваше предложение!
Гьян Веда

Ответы:

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и из-за того, что рост объектов таким образом обычно происходит медленно в 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)
A5C1D2H2I1M1N2O1R2T1
источник
6
Спасибо! Это решает мою проблему. Почему это «действительно плохая идея»? И каким образом x и y смешиваются в цикле for?
Гьян Веда
5
@ user2932774, Невероятно неэффективно выращивать объект таким образом в R. Улучшение (но все же не обязательно лучший способ) заключалось бы в том, чтобы предварительно выделить a data.frameмаксимального размера, который вы ожидаете, и добавить значения с [извлечением / заменой.
A5C1D2H2I1M1N2O1R2T1
1
Спасибо, Ананда. Обычно я использую предварительное распределение, но я не согласен с тем, что это действительно не очень хорошая идея. Это зависит от ситуации. В моем случае я имею дело с небольшими данными, и кодирование альтернативы займет больше времени. Кроме того, это более элегантный код по сравнению с кодом, который требуется для обновления числовых индексов для заполнения соответствующих частей предварительно выделенного кадра данных на каждой итерации. Просто любопытно, какой, на ваш взгляд, «лучший способ» выполнить эту задачу? Я бы подумал, что предварительное выделение было бы лучше всего.
Гьян Веда
2
@ user2932774, это круто. Я тоже ценю вашу точку зрения - я почти никогда не работаю с большими наборами данных. Тем не менее, если я собираюсь поработать над написанием функции или чего-то еще, я обычно трачу немного больше усилий, пытаясь настроить код, чтобы получить лучшую скорость, когда это возможно. См. Мое обновление для примера довольно большой разницы в скорости.
A5C1D2H2I1M1N2O1R2T1
1
Ого, это огромная разница! Спасибо, что запустили это моделирование и рассказали мне о пакете микробенчмарков. Я определенно согласен с вами, что приятно приложить дополнительные усилия. В моем конкретном случае, я думаю, мне просто нужно было что-то быстрое и грязное в каком-то коде, который мне, возможно, никогда не придется запускать снова. :)
Гьян Веда
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). Следующим лучшим решением является использование list, а наихудшее решение (по крайней мере, на основе этих временных результатов) представляется rbind.

Хулиан Урбано
источник
Спасибо! Хотя я не согласен с предложением Ананды. Хочу ли я преобразовать символы в уровни фактора или нет, будет зависеть от того, что я хочу сделать с выводом. Хотя я предполагаю, что с предложенным вами решением необходимо установить stringsAsFactors в значение FALSE.
Гьян Веда
Спасибо за симуляцию. Я понимаю, что предварительное выделение лучше всего с точки зрения скорости обработки, но это не единственный фактор, который я учел при принятии решения о кодировании.
Гьян Веда
1
В f1 вы запутались, присвоив строку числовому вектору x. Правильная строка:df <- rbind(df, data.frame(x = i, y = toString(i)))
Эльдар Агаларов
14

Предположим, вы просто не знаете заранее размер data.frame. Это может быть несколько строк или несколько миллионов. Вам нужен какой-то контейнер, который динамично растет. Принимая во внимание мой опыт и все связанные с ним ответы в SO, у меня есть 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 решение

Это в основном копирование и вставка ответа Карстена В. в аналогичной теме.

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 @ 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и увеличивайте data.table экспоненциально (например, используя мой код).

Адам Рычковски
источник
2
Причина, по которой SQLite работает медленно, заключается в том, что при каждом INSERT INTO он должен выполнять REINDEX, который равен O (n), где n - количество строк. Это означает, что вставка в базу данных SQL по одной строке за раз составляет O (n ^ 2). SQLite может быть очень быстрым, если вы вставляете сразу весь data.frame, но он не лучший вариант для построчного роста.
Джулиан Цукер
5

Обновите с помощью purrr, tidyr & dplyr

Поскольку вопрос уже датирован (6 лет), в ответах отсутствует решение с более новыми пакетами tidyr и purrr. Итак, для людей, работающих с этими пакетами, я хочу добавить решение к предыдущим ответам - все очень интересно, особенно.

ИМХО самое большое преимущество purrr и tidyr - лучшая читаемость. 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
Agile Bean
источник
Вам не нужно использовать add_row. Например: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394
@ user3808394 спасибо, это интересная альтернатива! если кто-то хочет создать фрейм данных с нуля, ваш будет короче, поэтому лучшее решение. Если у вас уже есть фреймворк, мое решение, конечно, лучше.
Agile Bean
Если у вас уже есть фрейм данных, 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
}

Функция extendDf () расширяет фрейм данных на 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
Pisca46
источник
0

Мое решение почти такое же, как и исходный ответ, но у меня оно не сработало.

Итак, я дал названия столбцам, и это работает:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
Брун Иджбх
источник