Effiziente Methode zum Zählen offener Fälle zum Zeitpunkt der Einreichung jedes Falls in einem großen Datensatz

In einem großen Datensatz (~ 1 Million Fälle) hat jeder Fall ein "erstelltes" und ein "zensiertes"dateTime. Ich möchte die Anzahl der anderen Fälle zählen, die zum Zeitpunkt der Erstellung der einzelnen Fälle geöffnet waren. Fälle sind offen zwischen "erstellt" und "zensiert"dataTimes.

Mehrere Lösungen eignen sich gut für kleine Datensätze (<100.000 Fälle), aber die Rechenzeit nimmt exponentiell zu. Meine Schätzung ist, dass sich die Rechenzeit als Funktion 3n ^ 2 erhöht. In n = 100.000 Fällen beträgt die Rechenzeit auf meinem Server mit 6 * 4-GHz-Kernen und 64 GB RAM> 20 Minuten. Selbst mit Multi-Core-Bibliotheken könnte ich die Zeit bestenfalls um den Faktor 8 oder 10 verkürzen. Nicht genug, um ~ 1 Million Fälle zu bearbeiten.

Ich suche nach einer effizienteren Methode, um diese Berechnung durchzuführen. Im Folgenden habe ich eine Funktion bereitgestellt, mit der Sie auf einfache Weise eine große Anzahl von "erstellten" und "zensierten" @ erstellen könnedateTime Paare zusammen mit zwei bisher ausprobierten Lösungen unter Verwendung vondplyr unddata.table Bibliotheken. Die Timings werden dem Benutzer zur Vereinfachung mitgeteilt. Sie können einfach die Variable "CASE_COUNT" oben ändern, um die Zeiten erneut auszuführen und anzuzeigen und das Timing anderer Lösungen zu vergleichen, die Sie möglicherweise vorschlagen müssen.

Ich werde den ursprünglichen Beitrag mit anderen Lösungen aktualisieren, um den Autoren die gebührende Ehre zu erweisen. Vielen Dank im Voraus für Ihre Hilfe!

# Load libraries used in this example
library(dplyr);
library(data.table);
# Not on CRAN. See: http://bioconductor.org/packages/release/bioc/html/IRanges.html
library(IRanges);

# Set seed for reproducibility 
set.seed(123)

# Set number of cases & date range variables
CASE_COUNT  <<- 1000;
RANGE_START <- as.POSIXct("2000-01-01 00:00:00", 
                          format="%Y-%m-%d %H:%M:%S", 
                          tz="UTC", origin="1970-01-01");
RANGE_END   <- as.POSIXct("2012-01-01 00:00:00", 
                          format="%Y-%m-%d %H:%M:%S", 
                          tz="UTC", origin="1970-01-01");

# Select which solutions you want to run in this test           
RUN_SOLUTION_1 <- TRUE;     # dplyr::summarize() + comparisons
RUN_SOLUTION_2 <- TRUE;     # data.table:foverlaps()
RUN_SOLUTION_3 <- TRUE;     # data.table aggregation + comparisons
RUN_SOLUTION_4 <- TRUE;     # IRanges::IRanges + countOverlaps()
RUN_SOLUTION_5 <- TRUE;     # data.table::frank()

# Function to generate random creation & censor dateTime pairs
# The censor time always has to be after the creation time
# Credit to @DirkEddelbuettel for this smart function
# (https://stackoverflow.com/users/143305/dirk-eddelbuettel)

generate_cases_table <- function(n = CASE_COUNT, start_val=RANGE_START, end_val=RANGE_END) {
    # Measure duration between start_val & end_val
    duration <- as.numeric(difftime(end_val, start_val, unit="secs"));

    # Select random values in duration to create start_offset
    start_offset   <- runif(n, 0, duration);

    # Calculate the creation time list
    created_list  <- start_offset + start_val;

    # Calculate acceptable time range for censored values
    # since they must always be after their respective creation value
    censored_range <- as.numeric(difftime(RANGE_END, created_list, unit="secs"));

    # Select random values in duration to create end_offset
    creation_to_censored_times <- runif(n, 0, censored_range);

    censored_list <- created_list + creation_to_censored_times;

    # Create and return a data.table with creation & censor values
    # calculated from start or end with random offsets
    return_table  <- data.table(id       = 1:n,
                                created  = created_list,
                                censored = censored_list);

    return(return_table);
}

# Create the data table with the desired number of cases specified by CASE_COUNT above
cases_table <- generate_cases_table();

solution_1_function <- function (cases_table) { 
    # SOLUTION 1: Using dplyr::summarize:

    # Group by id to set parameters for summarize() function 
    cases_table_grouped <- group_by(cases_table, id);

    # Count the instances where other cases were created before
    # and censored after each case using vectorized sum() within summarize()

    cases_table_summary <- summarize(cases_table_grouped, 
                           open_cases_at_creation = sum((cases_table$created  < created & 
                                                         cases_table$censored > created)));
    solution_1_table <<- as.data.table(cases_table_summary, key="id");        
} # End solution_1_function

solution_2_function <- function (cases_table) {
    # SOLUTION 2: Using data.table::foverlaps:

    # Adapted from solution provided by @Davidarenburg
    # (https://stackoverflow.com/users/3001626/david-arenburg)

    # The foverlaps() solution tends to crash R with large case counts
    # I suspect it has to do with memory assignment of the very large objects
    # It maxes RAM on my system (64GB) before crashing, possibly attempting
    # to write beyond its assigned memory limits.
    # I'll submit a reproduceable bug to the data.table team since
    # foverlaps() is pretty new and known to be occasionally unstable

    if (CASE_COUNT > 50000) {
        stop("The foverlaps() solution tends to crash R with large case counts. Not running.");
    }

    setDT(cases_table)[, created_dupe := created];
    setkey(cases_table, created, censored);

    foverlaps_table  <- foverlaps(cases_table[,c("id","created","created_dupe"), with=FALSE],
                                  cases_table[,c("id","created","censored"),    with=FALSE], 
                                  by.x=c("created","created_dupe"))[order(i.id),.N-1,by=i.id];

    foverlaps_table  <- dplyr::rename(foverlaps_table, id=i.id, open_cases_at_creation=V1);

    solution_2_table <<- as.data.table(foverlaps_table, key="id");
} # End solution_2_function

solution_3_function <- function (cases_table) {    
    # SOLUTION 3: Using data.table aggregation instead of dplyr::summarize

    # Idea suggested by @jangorecki
    # (https://stackoverflow.com/users/2490497/jangorecki)

    # Count the instances where other cases were created before
    # and censored after each case using vectorized sum() with data.table aggregation

    cases_table_aggregated <- cases_table[order(id), sum((cases_table$created  < created & 
                                                     cases_table$censored > created)),by=id];   

    solution_3_table <<- as.data.table(dplyr::rename(cases_table_aggregated, open_cases_at_creation=V1), key="id");

} # End solution_3_function

solution_4_function <- function (cases_table) { 
    # SOLUTION 4: Using IRanges package

    # Adapted from solution suggested by @alexis_laz
    # (https://stackoverflow.com/users/2414948/alexis-laz)

    # The IRanges package generates ranges efficiently, intended for genome sequencing
    # but working perfectly well on this data, since POSIXct values are numeric-representable
    solution_4_table <<- data.table(id      = cases_table$id,
                     open_cases_at_creation = countOverlaps(IRanges(cases_table$created, 
                                                                    cases_table$created), 
                                                            IRanges(cases_table$created, 
                                                                    cases_table$censored))-1, key="id");

} # End solution_4_function

solution_5_function <- function (cases_table) {
    # SOLUTION 5: Using data.table::frank()

    # Adapted from solution suggested by @danas.zuokas
    # (https://stackoverflow.com/users/1249481/danas-zuokas)

    n <- CASE_COUNT;

    # For every case compute the number of other cases
    # with `created` less than `created` of other cases
    r1 <- data.table::frank(c(cases_table[, created], cases_table[, created]), ties.method = 'first')[1:n];

    # For every case compute the number of other cases
    # with `censored` less than `created`
    r2 <- data.table::frank(c(cases_table[, created], cases_table[, censored]), ties.method = 'first')[1:n];

    solution_5_table <<- data.table(id      = cases_table$id,
                     open_cases_at_creation = r1 - r2, key="id");

} # End solution_5_function;

# Execute user specified functions;
if (RUN_SOLUTION_1)
    solution_1_timing <- system.time(solution_1_function(cases_table)); 
if (RUN_SOLUTION_2) {
    solution_2_timing <- try(system.time(solution_2_function(cases_table)));
    cases_table <- select(cases_table, -created_dupe);
}
if (RUN_SOLUTION_3)
    solution_3_timing <- system.time(solution_3_function(cases_table)); 
if (RUN_SOLUTION_4)
    solution_4_timing <- system.time(solution_4_function(cases_table));
if (RUN_SOLUTION_5)
    solution_5_timing <- system.time(solution_5_function(cases_table));         

# Check generated tables for comparison
if (RUN_SOLUTION_1 && RUN_SOLUTION_2 && class(solution_2_timing)!="try-error") {
    same_check1_2 <- all(solution_1_table$open_cases_at_creation == solution_2_table$open_cases_at_creation);
} else {same_check1_2 <- TRUE;}
if (RUN_SOLUTION_1 && RUN_SOLUTION_3) {
    same_check1_3 <- all(solution_1_table$open_cases_at_creation == solution_3_table$open_cases_at_creation);
} else {same_check1_3 <- TRUE;}
if (RUN_SOLUTION_1 && RUN_SOLUTION_4) {
    same_check1_4 <- all(solution_1_table$open_cases_at_creation == solution_4_table$open_cases_at_creation);
} else {same_check1_4 <- TRUE;}
if (RUN_SOLUTION_1 && RUN_SOLUTION_5) {
    same_check1_5 <- all(solution_1_table$open_cases_at_creation == solution_5_table$open_cases_at_creation);
} else {same_check1_5 <- TRUE;}
if (RUN_SOLUTION_2 && RUN_SOLUTION_3 && class(solution_2_timing)!="try-error") {
    same_check2_3 <- all(solution_2_table$open_cases_at_creation == solution_3_table$open_cases_at_creation);
} else {same_check2_3 <- TRUE;}
if (RUN_SOLUTION_2 && RUN_SOLUTION_4 && class(solution_2_timing)!="try-error") {
    same_check2_4 <- all(solution_2_table$open_cases_at_creation == solution_4_table$open_cases_at_creation);
} else {same_check2_4 <- TRUE;}
if (RUN_SOLUTION_2 && RUN_SOLUTION_5 && class(solution_2_timing)!="try-error") {
    same_check2_5 <- all(solution_2_table$open_cases_at_creation == solution_5_table$open_cases_at_creation);
} else {same_check2_5 <- TRUE;}
if (RUN_SOLUTION_3 && RUN_SOLUTION_4) {
    same_check3_4 <- all(solution_3_table$open_cases_at_creation == solution_4_table$open_cases_at_creation);
} else {same_check3_4 <- TRUE;}
if (RUN_SOLUTION_3 && RUN_SOLUTION_5) {
    same_check3_5 <- all(solution_3_table$open_cases_at_creation == solution_5_table$open_cases_at_creation);
} else {same_check3_5 <- TRUE;}
if (RUN_SOLUTION_4 && RUN_SOLUTION_5) {
    same_check4_5 <- all(solution_4_table$open_cases_at_creation == solution_5_table$open_cases_at_creation);
} else {same_check4_5 <- TRUE;}


same_check    <- all(same_check1_2, same_check1_3, same_check1_4, same_check1_5,
                     same_check2_3, same_check2_4, same_check2_5, same_check3_4,
                     same_check3_5, same_check4_5);

# Report summary of results to user
cat("This execution was for", CASE_COUNT, "cases.\n",
    "It is", same_check, "that all solutions match.\n");
if (RUN_SOLUTION_1)
    cat("The dplyr::summarize() solution took", solution_1_timing[3], "seconds.\n");
if (RUN_SOLUTION_2 && class(solution_2_timing)!="try-error")
    cat("The data.table::foverlaps() solution took", solution_2_timing[3], "seconds.\n");
if (RUN_SOLUTION_3)
    cat("The data.table aggregation solution took", solution_3_timing[3], "seconds.\n");
if (RUN_SOLUTION_4)
    cat("The IRanges solution solution took", solution_4_timing[3], "seconds.\n");
if (RUN_SOLUTION_5)
    cat("The data.table:frank() solution solution took", solution_5_timing[3], "seconds.\n\n");

Dasdata.table::foverlaps()ie @ -Lösung ist in weniger Fällen schneller (<5000 oder so; hängt von der Zufälligkeit zusätzlich zu n ab, da die Optimierung über die binäre Suche erfolgt). Dasdplyr::summarize()ie @ -Lösung ist schneller für mehr Fälle (> 5.000 oder so). Bei weitem über 100.000 ist keine der beiden Lösungen realisierbar, da beide zu langsam sind.

EDIT: Es wurde eine dritte Lösung hinzugefügt, die auf der von @jangorecki vorgeschlagenen Idee basiert und @ verwendedata.table Aggregation anstelle vondplyr::summarize() und ist ansonsten dem @ ähnlidplyr Lösung. Für bis zu 50.000 Fälle ist es die schnellste Lösung. Über 50.000 Fälle hinaus hat dasdplyr::summarize() Lösung ist etwas schneller, aber nicht viel. Leider ist dies für 1 Million Fälle immer noch nicht praktikabel.

EDIT2: Es wurde eine vierte Lösung hinzugefügt, die von der von @alexis_laz vorgeschlagenen Lösung abgeleitet ist und die @ verwendeIRanges package und seincountOverlaps Funktion. Es ist deutlich schneller als die 3 anderen Lösungen. Mit 50.000 Fällen war es fast 400% schneller als die Lösungen 1 und 3.

EDIT3: Die Funktion zur Erzeugung von Groß- und Kleinschreibung wurde geändert, um die "zensierte" Bedingung ordnungsgemäß zu erfüllen. Vielen Dank an @jangorecki, der die Einschränkungen der Vorgängerversion abgefangen hat.

EDIT4: Neu geschrieben, um dem Benutzer die Auswahl der auszuführenden und zu verwendenden Lösungen zu ermöglichen.system.time() für den zeitlichen Vergleich mit der Speicherbereinigung vor jeder Ausführung zur genaueren zeitlichen Steuerung (gemäß @ jangoreckis scharfsinniger Beobachtung) - Außerdem wurden einige Bedingungsprüfungen für Absturzfälle hinzugefügt.

EDIT5: Es wurde eine fünfte Lösung hinzugefügt, die der von @ danas.zuokas vorgeschlagenen Lösung mit @ angepasst wurdrank(). Meine Experimente legen nahe, dass es immer mindestens eine Größenordnung langsamer ist als die anderen Lösungen. Bei 10.000 Fällen dauert es 44 Sekunden gegenüber 3,5 Sekunden fürdplyr::summarize und 0,36 Sekunden fürIRanges solutions.

FINAL EDIT: Ich habe geringfügige Änderungen an Lösung 5 vorgenommen, die von @ danas.zuokas vorgeschlagen wurden, und die der Beobachtung von @ Khashaa über Typen entsprechen. Ich habe den Typ @ eingestelas.numeric in demdataTime Generierungsfunktion, die drastisch beschleunigtrank wie es funktioniert aufintegers oderdoubles Anstatt vondateTime objects (erhöht die Geschwindigkeit anderer Funktionen, aber nicht so drastisch). Bei einigen Tests wird das Setzen vonties.method='first' ergibt Ergebnisse, die mit der Absicht übereinstimmen.data.table::frank ist schneller als beidebase::rank undIRanges::rank. bit64::rank ist am schnellsten, scheint aber anders mit Bindungen umzugehen alsdata.table::frank und ich kann es nicht dazu bringen, sie wie gewünscht zu behandeln. Einmalbit64 wird geladen, maskiert eine große Anzahl von Typen und Funktionen und ändert die Ergebnisse vondata.table::frank nach dem Weg. Die konkreten Gründe dafür sprengen den Rahmen dieser Frage.

POST END HINWEIS: Stellt sich heraus, dassdata.table::frank behandeltPOSIXct dateTimes effizient, während wederbase::rank NochIRanges::rank scheint. Selbst dasas.numeric (oderas.integer) Typeinstellung ist bei @ nicht erforderlidata.table::frank und es gibt keinen Präzisionsverlust durch die Konvertierung, daher gibt es wenigerties.method Unstimmigkeiten. Vielen Dank an alle, die dazu beigetragen haben! Ich habe viel gelernt! Sehr geschätzt! :) Das Guthaben wird in meinen Quellcode aufgenommen.

ENDNOTE: Diese Frage ist eine verfeinerte und geklärte Version von @, die benutzerfreundlicher und lesbarer isMehr effiziente Methode zum Zählen offener Fälle ab dem Erstellungszeitpunkt jedes Falls - Ich habe es hier getrennt, um den ursprünglichen Beitrag nicht mit zu vielen Bearbeitungen zu überfordern und die Erstellung einer großen Anzahl von @ zu vereinfachedataTime Paare im Beispielcode. Auf diese Weise müssen Sie nicht so hart arbeiten, um zu antworten. Danke noch einmal

Antworten auf die Frage(4)

Ihre Antwort auf die Frage