#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 обрабатывает огромные наборы данных, поражает. Большое вам спасибо за помощь.