«сглаживание» временных данных - можно ли сделать это более эффективно? - PullRequest
0 голосов
/ 21 июня 2011

У меня есть фрейм данных, содержащий идентификатор, дату начала и дату окончания.Мои данные упорядочены по идентификатору, началу, концу (в этой последовательности).

Теперь я хочу, чтобы все строки с одинаковым идентификатором имели перекрывающийся промежуток времени (или имели дату начала, которая является правильной на следующий день после окончания)дата другой строки) для объединения.

Объединение их означает, что они заканчиваются в одной строке с одинаковым идентификатором, min (дата начала) и max (дата окончания) (надеюсь, вы понимаете, чтоЯ имею в виду).

Я написал функцию для этого (она не полностью протестирована, но на данный момент выглядит хорошо).Проблема в том, что мой фрейм данных содержит почти 100 000 наблюдений, функция очень медленная.

Можете ли вы помочь мне улучшить мою функцию с точки зрения эффективности?

Вот функция

smoothingEpisodes <- function (theData) {
    theOutput <- data.frame()

    curId <- theData[1, "ID"]
    curStart <- theData[1, "START"]
    curEnd <- theData[1, "END"]

    for(i in 2:nrow(theData)) {
        nextId <- theData[i, "ID"]
        nextStart <- theData[i, "START"]
        nextEnd <- theData[i, "END"]

        if (curId != nextId | (curEnd + 1) < nextStart) {
            theOutput <- rbind(theOutput, data.frame("ID" = curId, "START" = curStart, "END" = curEnd))

            curId <- nextId
            curStart <- nextStart
            curEnd <- nextEnd
        } else {
            curEnd <- max(curEnd, nextEnd, na.rm = TRUE)
        }
    }
    theOutput <- rbind(theOutput, data.frame("ID" = curId, "START" = curStart, "END" = curEnd))

    theOutput
}

Спасибо!

[править]

данные испытаний:

    ID      START        END
1    1 2000-01-01 2000-03-31
2    1 2000-04-01 2000-05-31
3    1 2000-04-15 2000-07-31
4    1 2000-09-01 2000-10-31
5    2 2000-01-15 2000-03-31
6    2 2000-02-01 2000-03-15
7    2 2000-04-01 2000-04-15
8    3 2000-06-01 2000-06-15
9    3 2000-07-01 2000-07-15

(START и END имеют тип данных "Дата ", ID - это числовое значение)

Скорость передачи данных:

structure(list(ID = c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 3L, 3L), START = structure(c(10957, 
11048, 11062, 11201, 10971, 10988, 11048, 11109, 11139), class = "Date"), 
    END = structure(c(11047, 11108, 11169, 11261, 11047, 11031, 
    11062, 11123, 11153), class = "Date")), .Names = c("ID", 
"START", "END"), class = "data.frame", row.names = c(NA, 9L))

Ответы [ 3 ]

2 голосов
/ 21 июня 2011

Первая [не особо задумываясь о том, что вы пытаетесь сделать] оптимизация, которую я бы предложил, - выделить хранилище для theOutput.В данный момент вы увеличиваете theOutput на каждой итерации цикла.В R это абсолютное нет нет !!Это то, что вы никогда не делаете, если вам не нравится ужасно медленный код.R должен копировать объект и расширять его во время каждой итерации, и это медленно.

Глядя на код, мы знаем, что theOutput должно иметь nrow(theData) - 1 строк и 3 столбца.Поэтому создайте это до начала цикла:

theOutput <- data.frame(matrix(ncol = 3, nrow = nrow(theData) - 1))

, затем заполните этот объект во время цикла:

theOutput[i, ] <- data.frame("ID" = curId, "START" = curStart, "END" = curEnd))

, например.

Не ясно, чтоSTART и END есть?если это числовые значения, то работа с матрицей, а не с фреймом данных также может повысить эффективность использования скорости.

Кроме того, создание фрейма данных на каждой итерации будет медленным.Я не могу рассчитать время, не тратя много своего времени, но вы можете просто заполнить нужные биты напрямую, не вызывая data.frame() вызов во время каждой итерации:

theOutput[i, "ID"] <- curId
theOutput[i, "START"] <- curStart
theOutput[i, "END"] <- curEnd

Лучший советОднако я могу дать вам, чтобы профилировать ваш код.Посмотрите, где узкие места и ускорить их.Запустите вашу функцию на небольшом подмножестве данных;размер которого достаточен, чтобы дать вам немного времени для сбора полезных данных профилирования без необходимости ждать целую вечность, чтобы завершить прогон профилирования.Чтобы профилировать в R, используйте Rprof():

Rprof(filename = "my_fun_profile.Rprof")
## run your function call here on a subset of the data
Rprof(NULL)

. Вы можете посмотреть на вывод, используя

summaryRprof("my_fun_profile.Rprof")

У Хэдли Уикхэма (@hadley) есть пакет, чтобы сделать это немногоПолегче.Он называется profr .И, как Дирк напоминает мне в комментариях, есть также пакет proftools Люка Тирни.

Редактировать: , так как ОП предоставил некоторые тестовые данныеЯ быстро добавил что-то, чтобы показать ускорение, достигнутое просто следуя хорошей практике цикла:

smoothingEpisodes2 <- function (theData) {
    curId <- theData[1, "ID"]
    curStart <- theData[1, "START"]
    curEnd <- theData[1, "END"]
    nr <- nrow(theData)
    out1 <- integer(length = nr)
    out2 <- out3 <- numeric(length = nr)
    for(i in 2:nrow(theData)) {
        nextId <- theData[i, "ID"]
        nextStart <- theData[i, "START"]
        nextEnd <- theData[i, "END"]
        if (curId != nextId | (curEnd + 1) < nextStart) {
            out1[i-1] <- curId
            out2[i-1] <- curStart
            out3[i-1] <- curEnd
            curId <- nextId
            curStart <- nextStart
            curEnd <- nextEnd
        } else {
            curEnd <- max(curEnd, nextEnd, na.rm = TRUE)
        }
    }
    out1[i] <- curId
    out2[i] <- curStart
    out3[i] <- curEnd
    theOutput <- data.frame(ID = out1,
                            START = as.Date(out2, origin = "1970-01-01"),
                            END = as.Date(out3, origin = "1970-01-01"))
    ## drop empty
    theOutput <- theOutput[-which(theOutput$ID == 0), ]
    theOutput
}

Используя набор тестовых данных в объекте testData, я получаю:

> res1 <- smoothingEpisodes(testData)
> system.time(replicate(100, smoothingEpisodes(testData)))
   user  system elapsed 
  1.091   0.000   1.131 
> res2 <- smoothingEpisodes2(testData)
> system.time(replicate(100, smoothingEpisodes2(testData)))
   user  system elapsed 
  0.506   0.004   0.517

ускорение на 50%.Не драматично, но просто достичь, просто не увеличивая объект на каждой итерации.

1 голос
/ 22 июня 2011

Марсель, я подумал, что просто попробую немного улучшить твой код. Версия ниже примерно в 30 раз быстрее (от 3 секунд до 0,1 секунды) ... Хитрость заключается в том, чтобы сначала извлечь три столбца в целые и двойные векторы.

В качестве примечания, я стараюсь использовать [[, где это применимо, и стараюсь сохранять целые числа как целые, записывая j <- j + 1L и т. Д. Это не имеет никакого значения, но иногда приведение между целыми числами и двойными числами может занять некоторое время.

smoothingEpisodes3 <- function (theData) {
    theLength <- nrow(theData)
    if (theLength < 2L) return(theData)

    id <- as.integer(theData[["ID"]])
    start <- as.numeric(theData[["START"]])
    end <- as.numeric(theData[["END"]])

    curId <- id[[1L]]
    curStart <- start[[1L]]
    curEnd <- end[[1L]]

    out.1 <- integer(length = theLength)
    out.2 <- out.3 <- numeric(length = theLength)

    j <- 1L

    for(i in 2:nrow(theData)) {
        nextId <- id[[i]]
        nextStart <- start[[i]]
        nextEnd <- end[[i]]

        if (curId != nextId | (curEnd + 1) < nextStart) {
            out.1[[j]] <- curId
            out.2[[j]] <- curStart
            out.3[[j]] <- curEnd

            j <- j + 1L

            curId <- nextId
            curStart <- nextStart
            curEnd <- nextEnd
        } else {
            curEnd <- max(curEnd, nextEnd, na.rm = TRUE)
        }
    }

    out.1[[j]] <- curId
    out.2[[j]] <- curStart
    out.3[[j]] <- curEnd

    theOutput <- data.frame(ID = out.1[1:j], START = as.Date(out.2[1:j], origin = "1970-01-01"), END = as.Date(out.3[1:j], origin = "1970-01-01"))

    theOutput
}

Тогда следующий код покажет разницу в скорости. Я просто взял ваши данные и повторил их 1000 раз ...

x <- structure(list(ID = c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 3L, 3L), START = structure(c(10957, 
11048, 11062, 11201, 10971, 10988, 11048, 11109, 11139), class = "Date"), 
    END = structure(c(11047, 11108, 11169, 11261, 11047, 11031, 
    11062, 11123, 11153), class = "Date")), .Names = c("ID", 
"START", "END"), class = "data.frame", row.names = c(NA, 9L))

r <- 1000
y <- data.frame(ID=rep(x$ID, r) + rep(1:r, each=nrow(x))-1, START=rep(x$START, r), END=rep(x$END, r))

system.time( a1 <- smoothingEpisodes(y) )   # 2.95 seconds
system.time( a2 <- smoothingEpisodes3(y) )  # 0.10 seconds
all.equal( a1, a2 )
1 голос
/ 21 июня 2011

Я сделал это немного по-другому, чтобы избежать удаления пустых строк в конце:

smoothingEpisodes <- function (theData) {
    curId <- theData[1, "ID"]
    curStart <- theData[1, "START"]
    curEnd <- theData[1, "END"]

    theLength <- nrow(theData)

    out.1 <- integer(length = theLength)
    out.2 <- out.3 <- numeric(length = theLength)

    j <- 1

    for(i in 2:nrow(theData)) {
        nextId <- theData[i, "ID"]
        nextStart <- theData[i, "START"]
        nextEnd <- theData[i, "END"]

        if (curId != nextId | (curEnd + 1) < nextStart) {
            out.1[j] <- curId
            out.2[j] <- curStart
            out.3[j] <- curEnd

            j <- j + 1

            curId <- nextId
            curStart <- nextStart
            curEnd <- nextEnd
        } else {
            curEnd <- max(curEnd, nextEnd, na.rm = TRUE)
        }
    }

    out.1[j] <- curId
    out.2[j] <- curStart
    out.3[j] <- curEnd

    theOutput <- data.frame(ID = out.1[1:j], START = as.Date(out.2[1:j], origin = "1970-01-01"), END = as.Date(out.3[1:j], origin = "1970-01-01"))

    theOutput
}

довольно большое улучшение по сравнению с моей первоначальной версией!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...