Эффективный способ расширения фрейма данных в Julia

#dataframe #julia

#фрейм данных #джулия

Вопрос:

У меня есть фрейм данных с эпизодами экспозиции для каждого случая:

 using DataFrames
using Dates
df = DataFrame(id = [1,1,2,3], startdate = [Date(2018,3,1),Date(2019,4,2),Date(2018,6,4),Date(2018,5,1)], enddate = [Date(2019,4,4),Date(2019,8,5),Date(2019,3,1),Date(2019,4,15)])
 

Я хочу расширить каждый эпизод до его составляющих дней, исключив любые повторяющиеся дни для каждого случая, возникающие в результате перекрытия эпизодов (случай 1 в примере фрейма данных):

     s = similar(df, 0)
    for row in eachrow(df)
        tf = DataFrame(row)
        ttf = repeat(tf, Dates.value.(row.enddate - row.startdate)   1)
        ttf.daydate = ttf.startdate .  Dates.Day.(0:nrow(ttf) - 1) #a record for each day between start and end days (inclusive)
        ttf.start = ttf.daydate .== ttf.startdate                  #a flag to indicate this record was at the start of an episode
        ttf.end = ttf.daydate .== ttf.enddate                      #a flag to indicate this record was at the end of an episode
        append!(s, ttf, cols=:union)
    end
    sort!(s, [:id,:daydate,:startdate, order(:enddate, rev=true)])
    unique!(s,[:id,:daydate]) #to eliminate duplicate dates in the case of episode overlaps (e.g. case 1)
 

У меня есть сильное подозрение, что есть более эффективный способ сделать это, чем метод грубой силы, который я придумал, и любая помощь будет оценена.

Примечание по реализации: В фактической реализации имеется несколько сотен тысяч случаев, в каждом из которых относительно мало эпизодов (медиана = 1,75 процентиля 3, максимум 20), но охватывающих 20 или более лет воздействия, что приводит к очень большому набору данных (несколько 100 миллионов записей). миллионов записей). Чтобы поместиться в доступной памяти, я разделил набор данных по идентификатору и использовал потоки.макрос @threads для параллельного перебора разделов. Основная цель этого разбиения на дни — не просто устранить перекрытия, но и объединить данные с другими данными экспозиции, которые доступны на ежедневной основе.

Комментарии:

1. Ваша спецификация неполная. Чтобы помочь вам, мне нужно было бы знать в случае, если два или более эпизодов пересекаются: 1) какой эпизод startdate и enddatae вы хотите сохранить? 2) для каких эпизодов вы хотите сохранить start и end значение true (поскольку теперь вы сохраняете только одну строку, поэтому то, что остается в этих столбцах, является случайным и зависит от порядка отображения эпизодов в исходном фрейме данных).

2. Прошу прощения, я пропустил шаг сортировки перед этим unique!(.... шагом. sort!(s, [:id,:DayDate,:startdate, order(:enddate, rev=true)]) . Таким образом, выбрана дата из эпизода, который начался первым или закончился последним, если есть ничья. Флаги начала и конца могут быть опущены, на самом деле только для обозначения разрыва в датах дня, когда последовательные расширенные эпизоды не соприкасаются друг с другом.

Ответ №1:

Ниже приведено более полное решение, которое учитывает некоторые существенные детали. Каждый эпизод связан с дополнительными атрибутами, в качестве примера я использовал locationid (место, где имело место разоблачение) и необходимость указывать, был ли промежуток между последующими эпизодами. Оригинальное решение также не учитывало особый случай, когда эпизод полностью содержится в другом эпизоде — такие эпизоды не следует расширять.

 using Dates
using DataFrames

function process(startdate, enddate, locationid)
    start = startdate[1]
    stop = enddate[1]
    location = locationid[1]
   res_daydate = collect(start:Day(1):stop)
    res_startdate = fill(start, length(res_daydate))
    res_enddate = fill(stop, length(res_daydate))
    res_location = fill(location, length(res_daydate))
    gap = 0
    res_gap = fill(0, length(res_daydate))
    for i in 2:length(startdate)
        if startdate[i] > res_daydate[end]
            start = startdate[i]
        elseif enddate[i] > res_daydate[end]
            start = res_daydate[end]   Day(1)
        else
            continue #this episode is contained within the previous episode
        end
        if  start - res_daydate[end] > Day(1)
            gap = gap==0 ? 1 : 0
        end 
        stop = enddate[i]
        location = locationid[i]
        new_daydate = start:Day(1):stop
        append!(res_daydate, new_daydate)
        append!(res_startdate, fill(startdate[i], length(new_daydate)))
        append!(res_enddate, fill(stop, length(new_daydate)))
        append!(res_location, fill(location, length(new_daydate)))
        append!(res_gap, fill(gap, length(new_daydate)))
    end

    return (daydate=res_daydate, startdate=res_startdate, enddate=res_enddate, locationid=res_location, gap = res_gap)
end

function eliminateoverlap()
    df = DataFrame(id = [1,1,2,3,3,4,4], startdate = [Date(2018,3,1),Date(2019,4,2),Date(2018,6,4),Date(2018,5,1), Date(2019,5,1), Date(2012,1,1), Date(2012,2,2)], 
                   enddate = [Date(2019,4,4),Date(2019,8,5),Date(2019,3,1),Date(2019,4,15),Date(2019,6,15),Date(2012,6,30), Date(2012,2,10)], locationid=[10,11,21,30,30,40,41])
    dfs = sort(df, [:startdate, order(:enddate, rev=true)])
    gdf = groupby(dfs, :id, sort=true)
    r = combine(gdf, [:startdate, :enddate, :locationid] => process => AsTable)
    df = combine(groupby(r, [:id,:gap,:locationid]), :daydate => minimum => :StartDate, :daydate => maximum => :EndDate)
    return df
end

df = eliminateoverlap()
 

Ответ №2:

Вот что должно быть эффективным:

 dfs = sort(df, [:startdate, order(:enddate, rev=true)])
gdf = groupby(dfs, :id, sort=true)

function process(startdate, enddate)
    start = startdate[1]
    stop = enddate[1]
    res_daydate = collect(start:Day(1):stop)
    res_startdate = fill(start, length(res_daydate))
    res_enddate = fill(stop, length(res_daydate))

    for i in 2:length(startdate)
        if startdate[i] > res_daydate[end]
            start = startdate[i]
            stop = enddate[i]
        elseif enddate[i] > res_daydate[end]
            start = res_daydate[end]   Day(1)
            stop = enddate[i]
        end
        new_daydate = start:Day(1):stop
        append!(res_daydate, new_daydate)
        append!(res_startdate, fill(startdate[i], length(new_daydate)))
        append!(res_enddate, fill(stop, length(new_daydate)))
    end

    return (startdate=res_startdate, enddate=res_enddate, daydate=res_daydate)
end

combine(gdf, [:startdate, :enddate] => process => AsTable)
 

(но, пожалуйста, сверьте его с большими данными с вашей реализацией, если оно правильное, поскольку я только что быстро написал его, чтобы показать вам, как выполнять эффективные реализации с помощью DataFrames.jl)

Комментарии:

1. Я использовал два набора данных (A: 127 тыс. эпизодов => 160 млн дней и B: 280 тыс. эпизодов => 800 млн дней). Для исходного алгоритма перебора потребовалось 47 секунд против 81 секунды. B занял 4m23s против 4m24s. Фактическая реализация показала большую разницу: 52 против 195 секунд для A и 320 против 1446 секунд для B. Была большая разница в распределении памяти: 973 Млн выделений общим объемом 52 ГБ против 5G выделений общим объемом 108 ГБ для A и 5G выделений общим объемом 268 ГБ против 26G выделений общим объемом 521 ГБ. Я использовал AMD Ryzen 3800X с 64 ГБ оперативной памяти под управлением Win10. Скорость, с которой Julia обрабатывает огромные наборы данных, поражает. Большое вам спасибо за помощь.