Jak usunąć wiersz przez odwołanie w data.table?

150

Moje pytanie dotyczy przydzielania zadań przez referencje a kopiowania data.table. Chcę wiedzieć, czy można usuwać wiersze przez odniesienie, podobnie do

DT[ , someCol := NULL]

Chcę wiedzieć

DT[someRow := NULL, ]

Wydaje mi się, że istnieje dobry powód, dla którego ta funkcja nie istnieje, więc może po prostu możesz wskazać dobrą alternatywę dla zwykłego podejścia do kopiowania, jak poniżej. W szczególności idąc z moim ulubionym z przykładu (data.table),

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
#      x y v
# [1,] a 1 1
# [2,] a 3 2
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Powiedzmy, że chcę usunąć pierwszy wiersz z tej tabeli data.table. Wiem, że potrafię to zrobić:

DT <- DT[-1, ]

ale często możemy chcieć tego uniknąć, ponieważ kopiujemy obiekt (a to wymaga około 3 * N pamięci, jeśli N object.size(DT), jak wskazano tutaj . Teraz znalazłem set(DT, i, j, value). Wiem, jak ustawić określone wartości (jak tutaj: ustaw wszystko wartości w wierszach 1 i 2 oraz kolumnach 2 i 3 do zera)

set(DT, 1:2, 2:3, 0) 
DT
#      x y v
# [1,] a 0 0
# [2,] a 0 0
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Ale jak mam usunąć, powiedzmy, pierwsze dwa wiersze? Robić

set(DT, 1:2, 1:3, NULL)

ustawia cały ID na NULL.

Moja znajomość SQL jest bardzo ograniczona, więc mówicie mi: podane dane. Tabela używa technologii SQL, czy istnieje odpowiednik polecenia SQL

DELETE FROM table_name
WHERE some_column=some_value

w data.table?

Florian Oswald
źródło
17
Nie sądzę, że chodzi o to, że data.table()wykorzystuje technologię SQL tak bardzo, jak można narysować paralelę między różnymi operacjami w SQL i różnymi argumentami w pliku data.table. Dla mnie odniesienie do „technologii” w pewnym stopniu sugeruje, że data.tablesiedzi gdzieś na szczycie bazy danych SQL, czego AFAIK nie ma.
Chase
1
dzięki pościg. tak, myślę, że analogia sql była szalona.
Florian Oswald
1
Często powinno wystarczyć zdefiniowanie flagi do przechowywania wierszy, na przykład DT[ , keep := .I > 1]podzbioru dla późniejszych operacji: DT[(keep), ...]być może nawet setindex(DT, keep)szybkości tego podzbioru. Nie jest to panaceum, ale warto rozważyć wybór projektu w swoim przepływie pracy - czy naprawdę chcesz usunąć wszystkie te wiersze z pamięci , czy wolisz je wykluczyć? Odpowiedź różni się w zależności od przypadku użycia.
MichaelChirico

Odpowiedzi:

125

Dobre pytanie. data.tablenie można jeszcze usunąć wierszy za pomocą odwołania.

data.tablemoże dodawać i usuwać kolumny przez odniesienie, ponieważ jak wiesz, nadmiernie przydziela wektor wskaźników kolumn. Plan jest taki, aby zrobić coś podobnego dla rzędów i pozwolić na szybkie inserti delete. Usunięcie wiersza spowoduje użycie memmovew C do przesunięcia elementów (w każdej kolumnie) po usuniętych wierszach. Usunięcie wiersza w środku tabeli byłoby nadal dość nieefektywne w porównaniu z bazą danych magazynu wierszy, taką jak SQL, która jest bardziej odpowiednia do szybkiego wstawiania i usuwania wierszy, gdziekolwiek te wiersze znajdują się w tabeli. Mimo wszystko byłoby to dużo szybsze niż kopiowanie nowego dużego obiektu bez usuniętych wierszy.

Z drugiej strony, ponieważ wektory kolumn byłyby nadmiernie alokowane, wiersze mogłyby być wstawiane (i usuwane) na końcu , natychmiast; np. rosnący szereg czasowy.


Został zgłoszony jako problem: Usuń wiersze przez odniesienie .

Matt Dowle
źródło
1
@Matthew Dowle Czy są jakieś wieści na ten temat?
statquant
15
@statquant Myślę, że powinienem naprawić 37 błędów i zakończyć jako freadpierwszy. Potem jest już dość wysoko.
Matt Dowle,
15
@MatthewDowle, oczywiście, jeszcze raz dziękuję za wszystko, co robisz.
statquant
1
@rbatt Correct. DT[b<8 & a>3]zwraca nowy data.table. Chcielibyśmy dodać delete(DT, b>=8 | a<=3)i DT[b>=8 | a<=8, .ROW:=NULL]. Zaletą tego ostatniego byłoby połączenie z innymi cechami, []takimi jak numery wierszy i, dołączanie ii rollczerpanie korzyści z [i,j,by]optymalizacji.
Matt Dowle
2
@charliealpha Brak aktualizacji. Składki mile widziane. Jestem gotów poprowadzić. Wymaga umiejętności C - znowu jestem gotów poprowadzić.
Matt Dowle,
29

podejście, które obrałem, aby wykorzystanie pamięci było podobne do usuwania na miejscu, polega na podziale kolumny na raz i usuwaniu. nie tak szybko, jak właściwe rozwiązanie memmove w C, ale zależy mi tylko na wykorzystaniu pamięci. coś takiego:

DT = data.table(col1 = 1:1e6)
cols = paste0('col', 2:100)
for (col in cols){ DT[, (col) := 1:1e6] }
keep.idxs = sample(1e6, 9e5, FALSE) # keep 90% of entries
DT.subset = data.table(col1 = DT[['col1']][keep.idxs]) # this is the subsetted table
for (col in cols){
  DT.subset[, (col) := DT[[col]][keep.idxs]]
  DT[, (col) := NULL] #delete
}
vc273
źródło
5
+1 Niezłe podejście do wydajnej pamięci. Więc najlepiej byłoby, gdybyśmy usunęli zestaw wierszy przez odniesienie, a nie my, nie pomyślałem o tym. Będzie to musiało memmovezająć kilka sekund, aby usunąć luki, ale to jest w porządku.
Matt Dowle
Czy zadziałałoby to jako funkcja, czy też użycie w funkcji i zwrócenie wymusza wykonanie kopii pamięci?
russellpierce
1
działałoby w funkcji, ponieważ data.tables są zawsze referencjami.
vc273,
1
dzięki, niezłe. Aby przyspieszyć trochę (zwłaszcza z wielu kolumn) zmiany DT[, col:= NULL, with = F]wset(DT, NULL, col, NULL)
Michele
2
Aktualizacja w świetle zmieniającego się idiomu i ostrzeżenia "z = FALSE razem z: = została wycofana w wersji 1.9.4 wydanej w październiku 2014 r. Proszę owinąć LHS: = nawiasami, np. DT [, (myVar): = sum (b) , by = a], aby przypisać do nazw kolumn przechowywanych w zmiennej myVar. Zobacz? ': =' dla innych przykładów. Zgodnie z ostrzeżeniem w 2014 roku jest to teraz ostrzeżenie. "
Frank
6

Oto działająca funkcja oparta na odpowiedzi @ vc273 i opinii @ Franka.

delete <- function(DT, del.idxs) {           # pls note 'del.idxs' vs. 'keep.idxs'
  keep.idxs <- setdiff(DT[, .I], del.idxs);  # select row indexes to keep
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs]); # this is the subsetted table
  setnames(DT.subset, cols[1]);
  for (col in cols[2:length(cols)]) {
    DT.subset[, (col) := DT[[col]][keep.idxs]];
    DT[, (col) := NULL];  # delete
  }
   return(DT.subset);
}

I przykład jego zastosowania:

dat <- delete(dat,del.idxs)   ## Pls note 'del.idxs' instead of 'keep.idxs'

Gdzie „dat” to data.table. Usunięcie 14 tys. Wierszy z 1,4 mln wierszy zajmuje na moim laptopie 0,25 sekundy.

> dim(dat)
[1] 1419393      25
> system.time(dat <- delete(dat,del.idxs))
   user  system elapsed 
   0.23    0.02    0.25 
> dim(dat)
[1] 1404715      25
> 

PS. Ponieważ jestem nowy w SO, nie mogłem dodać komentarza do wątku @ vc273 :-(

Jarno P.
źródło
Skomentowałem pod odpowiedzią vc, wyjaśniając zmienioną składnię dla (col): =. Trochę dziwne mieć funkcję o nazwie "delete", ale argument związany z tym, co zachować. Przy okazji, generalnie lepiej jest używać odtwarzalnego przykładu, niż pokazywać ciemność dla własnych danych. Możesz na przykład ponownie użyć ID z pytania.
Frank
Nie rozumiem, dlaczego robisz to przez odniesienie, ale później użyj przydziału dat <-
skan
1
@skan, To przypisanie przypisuje "dat" do wskazania zmodyfikowanej tabeli data.table, która sama została utworzona przez podzbiór oryginalnych data.table. <- assingment nie kopiuje zwracanych danych, po prostu nadaje im nową nazwę. link
Jarno P.
@Frank, zaktualizowałem funkcję pod kątem dziwności, którą wskazałeś.
Jarno P.
Ok dzięki. Zostawiam komentarz, ponieważ nadal uważam, że warto zauważyć, że nie zaleca się tutaj wyświetlania wyjścia konsoli zamiast powtarzalnego przykładu. Ponadto pojedynczy test porównawczy nie jest tak pouczający. Jeśli zmierzysz również czas potrzebny na podzbiór, będzie to bardziej pouczające (ponieważ większość z nas nie intuicyjnie wie, ile to trwa, a tym bardziej, ile czasu zajmuje twój komp). W każdym razie nie chcę sugerować, że to zła odpowiedź; Jestem jednym z jego zwolenników.
Frank
4

Zamiast tego lub próbując ustawić na NULL, spróbuj ustawić NA (pasujące do typu NA dla pierwszej kolumny)

set(DT,1:2, 1:3 ,NA_character_)
IRTFM
źródło
3
tak, to chyba działa. Mój problem polega na tym, że mam dużo danych i chcę się pozbyć dokładnie tych wierszy za pomocą NA, prawdopodobnie bez konieczności kopiowania DT, aby pozbyć się tych wierszy. i tak dziękuję za komentarz!
Florian Oswald
4

Temat jest nadal interesujący dla wielu osób (w tym mnie).

Co z tym? Kiedyś assignzastępowałem glovalenvi kod opisany wcześniej. Byłoby lepiej uchwycić oryginalne środowisko, ale przynajmniej w globalenvnim jest wydajne pamięć i działa jak zmiana przez odniesienie.

delete <- function(DT, del.idxs) 
{ 
  varname = deparse(substitute(DT))

  keep.idxs <- setdiff(DT[, .I], del.idxs)
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs])
  setnames(DT.subset, cols[1])

  for (col in cols[2:length(cols)]) 
  {
    DT.subset[, (col) := DT[[col]][keep.idxs]]
    DT[, (col) := NULL];  # delete
  }

  assign(varname, DT.subset, envir = globalenv())
  return(invisible())
}

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
delete(DT, 3)
JRR
źródło
Żeby było jasne, nie usuwa to przez odniesienie (na podstawie address(DT); delete(DT, 3); address(DT)), chociaż może być w pewnym sensie skuteczne.
Frank
1
Nie. Emuluje zachowanie i jest wydajna w pamięci. Dlatego powiedziałem: tak działa . Ale mówiąc ściśle, masz rację, adres się zmienił.
JRR
3

Oto kilka strategii, których użyłem. Myślę, że funkcja .ROW może się pojawić. Żadne z poniższych podejść nie jest szybkie. Oto kilka strategii wykraczających nieco poza podzbiory lub filtrowanie. Próbowałem myśleć jak dba, po prostu próbując wyczyścić dane. Jak wspomniano powyżej, możesz wybrać lub usunąć wiersze w data.table:

data(iris)
iris <- data.table(iris)

iris[3] # Select row three

iris[-3] # Remove row three

You can also use .SD to select or remove rows:

iris[,.SD[3]] # Select row three

iris[,.SD[3:6],by=,.(Species)] # Select row 3 - 6 for each Species

iris[,.SD[-3]] # Remove row three

iris[,.SD[-3:-6],by=,.(Species)] # Remove row 3 - 6 for each Species

Uwaga: .SD tworzy podzbiór oryginalnych danych i pozwala na wykonanie dużej ilości pracy w j lub kolejnych data.table. Zobacz https://stackoverflow.com/a/47406952/305675 . Tutaj zamówiłem moje tęczówki według długości działki, weź określoną długość działki jako minimum, wybierz trzy pierwsze (według długości działki) wszystkich gatunków i zwróć wszystkie towarzyszące dane:

iris[order(-Sepal.Length)][Sepal.Length > 3,.SD[1:3],by=,.(Species)]

Podejścia przede wszystkim do zmiany kolejności data.table sekwencyjnie podczas usuwania wierszy. Możesz transponować dane.table i usunąć lub zastąpić stare wiersze, które są teraz transponowane jako kolumny. Gdy używasz „: = NULL” do usunięcia transponowanego wiersza, następna nazwa kolumny również jest usuwana:

m_iris <- data.table(t(iris))[,V3:=NULL] # V3 column removed

d_iris <- data.table(t(iris))[,V3:=V2] # V3 column replaced with V2

Podczas transpozycji data.frame z powrotem do data.table, możesz chcieć zmienić nazwę z oryginalnego data.table i przywrócić atrybuty klasy w przypadku usunięcia. Zastosowanie „: = NULL” do transponowanego pliku data.table tworzy wszystkie klasy znaków.

m_iris <- data.table(t(d_iris));
setnames(d_iris,names(iris))

d_iris <- data.table(t(m_iris));
setnames(m_iris,names(iris))

Możesz po prostu usunąć zduplikowane wiersze, które możesz zrobić z kluczem lub bez:

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]     

d_iris[!duplicated(Key),]

d_iris[!duplicated(paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)),]  

Możliwe jest również dodanie licznika przyrostowego z „.I”. Następnie możesz wyszukać zduplikowane klucze lub pola i usunąć je, usuwając rekord z licznikiem. Jest to kosztowne obliczeniowo, ale ma pewne zalety, ponieważ można wydrukować linie do usunięcia.

d_iris[,I:=.I,] # add a counter field

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]

for(i in d_iris[duplicated(Key),I]) {print(i)} # See lines with duplicated Key or Field

for(i in d_iris[duplicated(Key),I]) {d_iris <- d_iris[!I == i,]} # Remove lines with duplicated Key or any particular field.

Możesz również po prostu wypełnić wiersz 0 lub NA, a następnie użyć zapytania i, aby je usunąć:

 X 
   x v foo
1: c 8   4
2: b 7   2

X[1] <- c(0)

X
   x v foo
1: 0 0   0
2: b 7   2

X[2] <- c(NA)
X
    x  v foo
1:  0  0   0
2: NA NA  NA

X <- X[x != 0,]
X <- X[!is.na(x),]
rferrisx
źródło
To tak naprawdę nie odpowiada na pytanie (o usuwanie przez odniesienie), a używanie tna data.frame zwykle nie jest dobrym pomysłem; sprawdź, str(m_iris)czy wszystkie dane stały się ciągiem / znakiem. Przy okazji, możesz również uzyskać numery wierszy, używając d_iris[duplicated(Key), which = TRUE]bez tworzenia kolumny licznika.
Frank,
1
Tak masz rację. Nie odpowiadam konkretnie na pytanie. Ale usunięcie wiersza przez odniesienie nie ma jeszcze oficjalnej funkcjonalności ani dokumentacji, a wiele osób przyjdzie do tego posta w poszukiwaniu ogólnej funkcji, która dokładnie to zrobi. Moglibyśmy stworzyć post, który po prostu odpowie na pytanie, jak usunąć wiersz. Przepełnienie stosu jest bardzo przydatne i naprawdę rozumiem konieczność zachowania dokładnych odpowiedzi na pytanie. Czasami jednak myślę, że SO może być pod tym względem trochę faszystowskim ... ale może jest ku temu dobry powód.
rferrisx
Ok, dzięki za wyjaśnienie. Myślę, że na razie nasza dyskusja jest wystarczającym drogowskazem dla każdego, kto się w tej sprawie zdezorientował.
Frank