Jak dołączyć wiersze do ramki danych R.


121

Rozejrzałem się po StackOverflow, ale nie mogę znaleźć rozwiązania specyficznego dla mojego problemu, który obejmuje dołączanie wierszy do ramki danych R.

Inicjalizuję pustą 2-kolumnową ramkę danych w następujący sposób.

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

Następnie moim celem jest iteracyjne przejrzenie listy wartości i dodanie wartości na końcu listy w każdej iteracji. Zacząłem od następującego kodu.

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

Ja również próbował funkcje c, appendi mergebez powodzenia. Daj mi znać, jeśli masz jakieś sugestie.


2
Nie zakładam, że wiem, jak miało być używane R, ale chciałem zignorować dodatkową linię kodu, która byłaby wymagana do aktualizacji indeksów w każdej iteracji i nie mogę łatwo wstępnie przydzielić rozmiaru ramki danych, ponieważ nie nie wiem, ile ostatecznie zajmie wierszy. Pamiętaj, że powyższe jest jedynie przykładem zabawki, która ma być odtwarzalna. Tak czy inaczej, dziękuję za sugestię!
Gyan Veda

Odpowiedzi:


115

Aktualizacja

Nie wiedząc, co próbujesz zrobić, podzielę się jeszcze jedną sugestią: wstępnie przydziel wektory żądanego typu dla każdej kolumny, wstaw wartości do tych wektorów, a na końcu utwórz własne data.frame.

Kontynuacja z Julian's f3(wstępnie przydzielona data.frame) jako najszybsza jak dotąd opcja, zdefiniowana jako:

# 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
}

Oto podobne podejście, ale takie, w którym data.framejest tworzony jako ostatni krok.

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

microbenchmarkz pakietu „microbenchmark” da nam bardziej wszechstronny wgląd niż 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()(podejście poniżej) jest niewiarygodnie nieefektywne ze względu na to, jak często wywołuje data.framei ponieważ rosnący w ten sposób obiektów jest generalnie powolny w R. f3()jest znacznie ulepszony dzięki wstępnej alokacji, ale data.framesama struktura może być tutaj częścią wąskiego gardła. f4()próbuje ominąć to wąskie gardło, nie rezygnując z przyjętego podejścia.


Oryginalna odpowiedź

To naprawdę nie jest dobry pomysł, ale jeśli chcesz to zrobić w ten sposób, myślę, że możesz spróbować:

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

Zwróć uwagę, że w Twoim kodzie jest jeszcze jeden problem:

  • Powinieneś użyć, stringsAsFactorsjeśli chcesz, aby znaki nie były konwertowane na czynniki. Posługiwać się:df = data.frame(x = numeric(), y = character(), stringsAsFactors = FALSE)

6
Dzięki! To rozwiązuje mój problem. Dlaczego to „naprawdę nie jest dobry pomysł”? A w jaki sposób x i y są mieszane w pętli for?
Gyan Veda,

5
@ user2932774, hodowanie obiektu w ten sposób w R. jest niewiarygodnie nieefektywne. Poprawa (ale niekoniecznie najlepsza) polegałaby na wstępnym przydzieleniu data.frameostatecznego rozmiaru, jakiego oczekujesz, i dodaniu wartości podczas [ekstrakcji / wymiany.
A5C1D2H2I1M1N2O1R2T1

1
Dzięki, Anando. Zwykle korzystam z wstępnej alokacji, ale nie zgadzam się, że to naprawdę nie jest dobry pomysł. To zależy od sytuacji. W moim przypadku mam do czynienia z małymi danymi i alternatywa będzie bardziej czasochłonna w kodowaniu. Ponadto jest to bardziej elegancki kod w porównaniu z kodem wymaganym do aktualizacji indeksów numerycznych w celu wypełnienia odpowiednich części wstępnie przydzielonej ramki danych w każdej iteracji. Ciekawe, jaki jest według Ciebie „najlepszy sposób” na wykonanie tego zadania? Pomyślałbym, że najlepsze byłoby wstępne przydzielenie.
Gyan Veda

2
@ user2932774, jest super. Doceniam również twoją perspektywę - prawie nigdy tak naprawdę nie pracuję z dużymi zbiorami danych. To powiedziawszy, jeśli zamierzam pracować nad napisaniem funkcji lub czegoś podobnego, zwykle poświęcałbym trochę więcej wysiłku, próbując dostroić kod, aby uzyskać lepsze prędkości, gdy tylko jest to możliwe. Zobacz moją aktualizację, aby zobaczyć przykład dość dużej różnicy prędkości.
A5C1D2H2I1M1N2O1R2T1

1
Whoa, to ogromna różnica! Dziękuję za uruchomienie tej symulacji i nauczenie mnie o pakiecie microbenchmark. Zdecydowanie zgadzam się z tobą, że miło jest włożyć ten dodatkowy wysiłek. Wydaje mi się, że w moim konkretnym przypadku chciałem po prostu czegoś dziwnego i brudnego w jakimś kodzie, którego być może nigdy więcej nie będę musiał uruchamiać. :)
Gyan Veda,

35

Porównajmy trzy proponowane rozwiązania:

# 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

Najlepszym rozwiązaniem jest wstępne przydzielenie miejsca (zgodnie z zamierzeniami w R). Następnym najlepszym rozwiązaniem jest użycie list, a najgorszym rozwiązaniem (przynajmniej w oparciu o te wyniki czasowe) wydaje się być rbind.


Dzięki! Chociaż nie zgadzam się z sugestią Anandy. To, czy chcę, aby postacie były konwertowane na poziomy współczynnika, czy nie, zależy od tego, co chcę zrobić z wynikiem. Chociaż wydaje mi się, że w proponowanym przez Ciebie rozwiązaniu konieczne jest ustawienie stringówAsFactors na FALSE.
Gyan Veda,

Dzięki za symulację. Zdaję sobie sprawę, że wstępna alokacja jest najlepsza pod względem szybkości przetwarzania, ale nie jest to jedyny czynnik, który wziąłem pod uwagę przy podejmowaniu decyzji o kodowaniu.
Gyan Veda

1
W f1 pomyliłeś przypisując łańcuch do numerycznego wektora x. Prawidłowa linia to:df <- rbind(df, data.frame(x = i, y = toString(i)))
Eldar Agalarov

14

Załóżmy, że po prostu nie znasz wcześniej rozmiaru ramki data.frame. Może to być kilka rzędów lub kilka milionów. Trzeba mieć jakiś pojemnik, który będzie się dynamicznie rozrastał. Biorąc pod uwagę moje doświadczenie i wszystkie powiązane odpowiedzi w SO, przedstawiam 4 różne rozwiązania:

  1. rbindlist do data.frame

  2. Skorzystaj data.tablez szybkiej setobsługi i połącz ją z ręcznym podwojeniem stołu w razie potrzeby.

  3. Użyj RSQLitei dołącz do tabeli przechowywanej w pamięci.

  4. data.framewłasna zdolność do rozwijania i używania niestandardowego środowiska (które ma semantykę referencyjną) do przechowywania data.frame, aby nie było kopiowane po zwróceniu.

Oto test wszystkich metod zarówno dla małej, jak i dużej liczby dołączonych wierszy. Z każdą metodą są powiązane 3 funkcje:

  • create(first_element)która zwraca odpowiedni obiekt podkładowy z first_elementwstawioną.

  • append(object, element)który dołącza znak elementdo końca tabeli (reprezentowany przez object).

  • access(object)pobiera data.frameze wszystkimi wstawionymi elementami.

rbindlist do data.frame

To dość łatwe i proste:

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 + w razie potrzeby ręczne podwojenie tabeli.

Będę przechowywać prawdziwą długość tabeli w rowcountatrybucie.

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 powinien być zoptymalizowany pod kątem szybkiego wstawiania rekordów, więc początkowo miałem duże nadzieje na RSQLiterozwiązanie

To jest po prostu skopiuj i wklej odpowiedź Karstena W. w podobnym wątku.

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.framewłasne dołączanie wierszy + środowisko niestandardowe.

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

Zestaw testów:

Dla wygody użyję jednej funkcji testowej, aby objąć je wszystkie połączeniami pośrednimi. (Sprawdziłem: używanie do.callzamiast bezpośredniego wywoływania funkcji nie powoduje, że kod działa dłużej).

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

Zobaczmy wydajność dla n = 10 wstawień.

Dodałem również funkcje „placebo” (z sufiksem 0), które nic nie wykonują - tylko po to, aby zmierzyć narzut konfiguracji testowej.

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

Czasy dodawania n = 10 wierszy

Czasy dla n = 100 rzędów Czasy dla n = 1000 wierszy

Dla rzędów 1E5 (pomiary wykonane na procesorze Intel (R) Core (TM) i7-4710HQ @ 2,50 GHz):

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

Wygląda na to, że sulution oparty na SQLite, chociaż odzyskuje pewną prędkość na dużych danych, nie jest nigdzie w pobliżu data.table + ręczny wzrost wykładniczy. Różnica wynosi prawie dwa rzędy wielkości!

Podsumowanie

Jeśli wiesz, że dodasz niewielką liczbę wierszy (n <= 100), użyj najprostszego możliwego rozwiązania: po prostu przypisz wiersze do data.frame za pomocą notacji nawiasowej i zignoruj ​​fakt, że data.frame jest nie wypełnione wstępnie.

Do wszystkiego innego używaj data.table::seti rozwijaj data.table wykładniczo (np. Używając mojego kodu).


2
Powodem, dla którego SQLite jest powolny, jest to, że na każdym INSERT INTO musi wykonać REINDEX, czyli O (n), gdzie n to liczba wierszy. Oznacza to, że wstawianie do bazy danych SQL po jednym wierszu na raz to O (n ^ 2). SQLite może być bardzo szybki, jeśli wstawisz całą ramkę danych na raz, ale nie jest najlepszy w powiększaniu linia po linii.
Julian Zucker

5

Zaktualizuj za pomocą purrr, tidyr i dplyr

Ponieważ pytanie jest już przestarzałe (6 lat), w odpowiedziach brakuje rozwiązania z nowszymi pakietami tidyr i purrr. Dlatego dla osób pracujących z tymi pakietami chcę dodać rozwiązanie do poprzednich odpowiedzi - wszystkie, szczególnie interesujące.

Największą zaletą purrr i tidyr jest lepsza czytelność IMHO. purrr zastępuje lapply bardziej elastyczną rodziną map (), tidyr oferuje superintuicyjną metodę add_row - po prostu robi to, co mówi :)

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

To rozwiązanie jest krótkie i intuicyjne w czytaniu oraz stosunkowo szybkie:

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

Skaluje się prawie liniowo, więc dla wierszy 1e5 wydajność jest następująca:

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 

co dałoby mu drugie miejsce zaraz po data.table (jeśli zignorujesz placebo) w benchmarku @Adama Ryczkowskiego:

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

Nie musisz używać add_row. Na przykład: map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }).
user3808394

@ user3808394 dzięki, to ciekawa alternatywa! jeśli ktoś chce stworzyć ramkę danych od podstaw, twoja jest krótsza, więc lepsze rozwiązanie. w przypadku, gdy masz już ramkę danych, moje rozwiązanie jest oczywiście lepsze.
Agile Bean

Jeśli masz już ramkę danych, zrobiłbyś to bind_rows(df, map_dfr(1:1e5, function(x) { tibble(x = x, y = toString(x)) }))zamiast używać add_row.
user3808394

2

Weźmy wektor „punkt”, który ma liczby od 1 do 5

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

jeśli chcemy dodać liczbę 6 w dowolnym miejscu wewnątrz wektora, poniższe polecenie może się przydać

i) Wektory

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

ii) kolumny tabeli

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

Polecenie appendprzyjmuje trzy argumenty:

  1. wektor / kolumna do zmodyfikowania.
  2. wartość, która ma być zawarta w zmodyfikowanym wektorze.
  3. indeks dolny, po którym należy dołączyć wartości.

prosty...!! Przepraszamy w przypadku ...!


1

Bardziej ogólne rozwiązanie może wyglądać następująco.

    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
}

Funkcja extensionDf () rozszerza ramkę danych o n wierszy.

Jako przykład:

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

Moje rozwiązanie jest prawie takie samo jak oryginalna odpowiedź, ale nie zadziałało.

Więc nadałem nazwy kolumnom i działa:

painel <- rbind(painel, data.frame("col1" = xtweets$created_at,
                                   "col2" = xtweets$text))
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.