2016-05-05 10 views
8

Ich habe eine große, breite data.table (20m Reihen) von einer Person ID aber mit vielen Spalten (~ 150), die viele Null-Werte haben. Jede Spalte ist ein aufgezeichneter Status/Attribut, das ich für jede Person mitnehmen möchte. Jede Person kann zwischen 10 und 10.000 Beobachtungen haben und es sind etwa 500.000 Menschen im Set. Werte von einer Person können nicht in die folgende Person "bluten". Daher muss meine Lösung die Spalte ID der Personenkennung und die Gruppe entsprechend berücksichtigen.effizient locf nach Gruppen in einem einzigen R data.table

Zu Demonstrationszwecken - hier ist eine sehr kleine Probeneingang:

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

Es sieht wie folgt aus:

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 

Meine erwartete Ausgabe sieht wie folgt aus:

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‘ Ich habe eine data.table Lösung gefunden, die funktioniert, aber es ist schrecklich langsam auf meinen großen Datensätzen:

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

Ich habe gleichwertige Lösungen mit dplyr gefunden, die gleichermaßen langsam sind.

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

Ich war zuversichtlich, dass ich mit einem Roll ‚Selbst‘ kommen könnte kommen die data.table-Funktionalität, aber ich kann es einfach nicht richtig zu machen scheint (ich vermute, ich würde .N verwenden müssen, aber ich nur habe es nicht herausgefunden).

An diesem Punkt denke ich, dass ich etwas in Rcpp schreiben muss, um den gruppierten locf effizient anzuwenden.

Ich bin neu in R, aber ich bin nicht neu in C++ - also bin ich zuversichtlich, dass ich es schaffen kann. Ich habe einfach das Gefühl, dass es einen effizienten Weg geben sollte, dies in R mit data.table zu tun.

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 

Dies repliziert na.locf mit einem na.rm = TRUE Argument, na.rm = FALSE Verhalten zu bekommen, müssen wir einfach:

+0

Ich bin mir ziemlich sicher, dass 'DT [, lapply (.SD, na.locf, F), durch = id]' wird schneller sein – eddi

+0

ich damit tatsächlich begonnen und fand die Leistung schlechter. –

+0

Rolling Self Join scheint hier ein Punkt zu sein, ich erinnere mich an einige Fragen, die sowohl 'na.locf'- als auch Rolling-Joins-Antworten haben, also denke ich, dass Sie die Antwort in der aktuellen SO-Wissensdatenbank finden können. – jangorecki

Antwort

14

Eine sehr einfache na.locf kann durch Weiterleiten (cummax) die nicht NA Indizes ((!is.na(x)) * seq_along(x)) und subsetting entsprechend gebaut werden um sicherzustellen, dass das erste Element in der cummaxTRUE:

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 

In diesem Fall müssen wir berücksichtigen, nicht nur die nicht NA Indizes aber auch, der Indizes, wo die (bestellt oder bestellt werden) „id“ Spalte ändert Wert:

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 

Kombination die oben:

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 

Beachten sie, dass wir hier das erste Element mit TRUEOR, das heißt es gleich TRUE, wodurch das na.rm = FALSE Verhalten zu bekommen.

Und für dieses Beispiel:

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

der Downvote ist mir sehr nicht offensichtlich, und einige Erklärung würde geschätzt werden – eddi

+1

Große Antwort imo - nicht nur ist dies eine viel schnellere Version eines regulären 'na.locf', sondern es fügt auch eine Änderung hinzu, um es pro Gruppe zu tun (angenommen sortierte Gruppen), ** ohne ** tatsächlich eine 'by'-Schleife (die zusätzliche' eval's pro Gruppe einführen würde und es verlangsamen würde). Es sei denn, ich verpasse etwas - das sollte die Standard-'na.locf'-Implementierung sein, anstelle der' rle'-Sachen, die 'zoo' tut. – eddi

+0

@eddi: danke für die Änderungen. Ich denke, der 'zoo :: na.locf' ist flexibler, obwohl ich glaube, dass für einfache Fälle die' 4-5 * length (x) 'Scans der' cummax' Version ziemlich einfach sein müssen. Und es hat sich auch als zweckmäßig erwiesen, jeden Spaltenzeiger einmal in der Funktion zu übergeben und virtuell "by" -Gruppe anzuwenden. –