2016-05-05 37 views
8

Mam duży, szeroki data.table (20m wierszy) z kluczem identyfikatora osoby, ale z dużą ilością kolumn (~ 150), które mają wiele wartości null. Każda kolumna jest zarejestrowanym stanem/atrybutem, który chcę przenieść dla każdej osoby. Każda osoba może mieć od 10 do 10 000 obserwacji, a na planie jest około 500 000 osób. Wartości od jednej osoby nie mogą przeniknąć do następnej osoby, więc moje rozwiązanie musi odpowiednio respektować kolumnę identyfikatora osoby i grupę.Efektywnie lokalizuj według grup w jednym R data.table

Dla celów demonstracyjnych - tu jest bardzo małe wejście próbki:

DT = data.table(
    id=c(1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3), 
    aa=c("A", NA, "B", "C", NA, NA, "D", "E", "F", NA, NA, NA), 
    bb=c(NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA), 
    cc=c(1, NA, NA, NA, NA, 4, NA, 5, 6, NA, 7, NA) 
) 

Wygląda to tak:

id aa bb cc 
1: 1 A NA 1 
2: 1 NA NA NA 
3: 1 B NA NA 
4: 1 C NA NA 
5: 2 NA NA NA 
6: 2 NA NA 4 
7: 2 D NA NA 
8: 2 E NA 5 
9: 3 F NA 6 
10: 3 NA NA NA 
11: 3 NA NA 7 
12: 3 NA NA NA 

Moje oczekiwany wynik wygląda tak:

id aa bb cc 
1: 1 A NA 1 
2: 1 A NA 1 
3: 1 B NA 1 
4: 1 C NA 1 
5: 2 NA NA NA 
6: 2 NA NA 4 
7: 2 D NA 4 
8: 2 E NA 5 
9: 3 F NA 6 
10: 3 F NA 6 
11: 3 F NA 7 
12: 3 F NA 7 

I znalazłem rozwiązanie, które działa, ale jest strasznie wolne w moich dużych zbiorach danych:

DT[, na.locf(.SD, na.rm=FALSE), by=id] 

Znalazłem równoważne rozwiązania za pomocą dplyr, które są równie powolne.

GRP = DT %>% group_by(id) 
data.table(GRP %>% mutate_each(funs(blah=na.locf(., na.rm=FALSE)))) 

miałem nadzieję, że mogę wymyślić toczenia „ja” przyłączyć korzystania z funkcjonalności data.table, ale ja po prostu nie wydają się uzyskać to prawo (podejrzewam, że muszę korzystać .N ale po prostu nie wymyśliłem tego).

W tym momencie myślę, że będę musiał napisać coś w Rcpp, aby skutecznie zastosować zgrupowany locf.

Jestem nowy dla R, ale nie jestem nowy w C++ - więc jestem przekonany, że mogę to zrobić. Po prostu czuję, że powinien istnieć skuteczny sposób robienia tego w R przy użyciu data.table.

+0

jestem całkiem pewny 'DT [, lapply (.SD, na.locf, F), przez = id]' będzie szybciej – eddi

+0

I rzeczywiście zaczęło się, że i stwierdzili, że wydajność jest gorsza. –

+0

Wygląda na to, że toczenia samo łączenie jest tutaj punktem, pamiętam kilka pytań, które mają odpowiedzi "na.locf" i rolling joins, więc myślę, że możesz znaleźć odpowiedź w aktualnej bazie wiedzy SO. – jangorecki

Odpowiedz

14

Bardzo prosty na.locf mogą być budowane przez Forwarding (cummax) Niezasychające NA indeksów ((!is.na(x)) * seq_along(x)) i podrzędnego Odpowiednio:

x = c(1, NA, NA, 6, 4, 5, 4, NA, NA, 2) 
x[cummax((!is.na(x)) * seq_along(x))] 
# [1] 1 1 1 6 4 5 4 4 4 2 

ten replikuje na.locf z na.rm = TRUE argumentu, aby uzyskać na.rm = FALSE zachowanie po prostu potrzeba aby upewnić się, że pierwszy element w cummax jest TRUE:

x = c(NA, NA, 1, NA, 2) 
x[cummax(c(TRUE, tail((!is.na(x)) * seq_along(x), -1)))] 
#[1] NA NA 1 1 2 

W tym przypadku, musimy wziąć pod uwagę nie tylko innych niż NA indeksów, ale także, indeksów, gdzie (zamówionych lub zamówić) „id” kolumna zmienia wartość:

id = c(10, 10, 11, 11, 11, 12, 12, 12, 13, 13) 
c(TRUE, id[-1] != id[-length(id)]) 
# [1] TRUE FALSE TRUE FALSE FALSE TRUE FALSE FALSE TRUE FALSE 

Łącząc powyżej:

id = c(10, 10, 11, 11, 11, 12, 12, 12, 13, 13) 
x = c(1, NA, NA, 6, 4, 5, 4, NA, NA, 2) 

x[cummax(((!is.na(x)) | c(TRUE, id[-1] != id[-length(id)])) * seq_along(x))] 
# [1] 1 1 NA 6 4 5 4 4 NA 2 

Zauważ, że tutaj mamy OR pierwszy element z TRUE, czyli zrobić to równa TRUE, a tym samym uzyskanie zachowanie na.rm = FALSE.

I na tym przykładzie:

id_change = DT[, c(TRUE, id[-1] != id[-.N])] 
DT[, lapply(.SD, function(x) x[cummax(((!is.na(x)) | id_change) * .I)])] 
# id aa bb cc 
# 1: 1 A NA 1 
# 2: 1 A NA 1 
# 3: 1 B NA 1 
# 4: 1 C NA 1 
# 5: 2 NA NA NA 
# 6: 2 NA NA 4 
# 7: 2 D NA 4 
# 8: 2 E NA 5 
# 9: 3 F NA 6 
#10: 3 F NA 6 
#11: 3 F NA 7 
#12: 3 F NA 7 
+5

DownVote nie jest dla mnie oczywisty, a pewne wyjaśnienie byłoby docenione – eddi

+1

Świetna odpowiedź imo - nie tylko jest to znacznie szybsza wersja zwykłego 'na.locf', ale także dodaje modyfikację, aby zrobić to na grupę (zakładając posortowane grupy), ** bez ** faktycznie wykonując pętlę 'by' (która wprowadziłaby dodatkowe' eval' na grupę i spowolniłaby to). Chyba że czegoś mi brakuje - to powinna być standardowa implementacja 'na.locf', zamiast rzeczy' rle', które robi 'zoo'. – eddi

+0

@eddi: dzięki za zmiany. Myślę, że 'zoo :: na.locf' jest bardziej elastyczny, jednak uważam, że dla prostych przypadków, skany' 4-5 * długości (x) 'wersji' cummax' będą całkiem proste. I rzeczywiście, okazało się, że wygodnie jest przekazać każdy wskaźnik kolumny raz w funkcji i zastosować go praktycznie "z" grupy. –