#julia
#Джулия
Вопрос:
Как я могу получить доступ к столбцам фрейма данных стабильным по типу способом?
Предположим, у меня есть следующие данные:
df = DataFrame(x = fill(1.0, 1000000), y = fill(1, 1000000), z = fill("1", 1000000))
И теперь я хочу выполнить некоторые рекурсивные вычисления (поэтому я не могу использовать transform
)
function foo!(df::DataFrame)
for i in 1:nrow(df)
if (i > 1) df.x[i] = df.x[i-1] end
end
end
Это имеет ужасную производительность:
julia> @time foo!(df)
0.144921 seconds (6.00 M allocations: 91.529 MiB)
Быстрое исправление в этом упрощенном примере будет следующим:
function bar!(df::DataFrame)
x::Vector{Float64} = df.x
for i in length(x)
if (i > 1) x[i] = x[i-1] end
end
end
julia> @time bar!(df)
0.000004 seconds
Тем не менее, я ищу решение, которое можно обобщить, например, когда рекурсивное вычисление просто задается как функция
function foo2!(df::DataFrame, fn::Function)
for i in 1:nrow(df)
if (i > 1) fn(df, i) end
end
end
function my_fn(df::DataFrame, i::Int64)
x::Vector{Float64} = df.x
x[i] = x[i-1]
end
Хотя это (почти) не выделяется, оно все равно очень медленное.
julia> @time foo2!(df, my_fn)
0.050465 seconds (1 allocation: 16 bytes)
Существует ли эффективный подход, обеспечивающий такую гибкость / обобщаемость?
РЕДАКТИРОВАТЬ: я должен также упомянуть, что на практике априори неизвестно, от каких столбцов fn
зависит функция. Т.е. я ищу подход, который обеспечивает эффективный доступ / обновление произвольных столбцов внутри fn
. Необходимые столбцы могут быть указаны вместе с fn
как Vector{Symbol}
, например, при необходимости.
РЕДАКТИРОВАТЬ 2: я пытался использовать барьерные функции следующим образом, но это неэффективно
function foo3!(df::DataFrame, fn::Function, colnames::Vector{Symbol})
cols = map(cname -> df[!,cname], colnames)
for i in 1:nrow(df)
if (i > 1) fn(cols..., i) end
end
end
function my_fn1(x::Vector{Float64}, i::Int64)
x[i] = x[i-1]
end
function my_fn2(x::Vector{Float64}, y::Vector{Int64}, i::Int64)
x[i] = x[i-1] * y[i-1]
end
@time foo3!(df, my_fn1, [:x])
@time foo3!(df, my_fn2, [:x, :y])
Ответ №1:
Эта проблема предназначена (чтобы избежать чрезмерной компиляции для широких фреймов данных), и способы ее решения описаны в https://github.com/bkamins/Julia-DataFrames-Tutorial/blob/master/11_performance .ipynb.
В общем, вам следует уменьшить количество раз, когда вы индексируете фрейм данных. Так что в этом случае сделайте:
julia> function foo3!(x::AbstractVector, fn::Function)
for i in 2:length(x)
fn(x, i)
end
end
foo3! (generic function with 1 method)
julia> function my_fn(x::AbstractVector, i::Int64)
x[i] = x[i-1]
end
my_fn (generic function with 1 method)
julia> @time foo3!(df.x, my_fn)
0.010746 seconds (16.60 k allocations: 926.036 KiB)
julia> @time foo3!(df.x, my_fn)
0.002301 seconds
(Я использую версию, в которой вы хотите передать пользовательскую функцию)
Комментарии:
1. К сожалению, это все еще недостаточно общее для моего варианта использования, поскольку затем приходится жестко кодировать столбцы, переданные
fn
вfoo3
. По сути, я ищу способ отделить логику рекурсии (которая на практике также содержит много бухгалтерского учета, вычисления показателей и т.д.) От логики обновления состояния (которая может зависеть от любого столбца в фрейме данных).2. Вы можете легко передать имя столбца как a
Symbol
, а затем выполнитьdf[!, colname]
, а затем вызвать рабочую функцию. Таким образом, вам не нужно ничего жестко кодировать.3. Как бы я сделал это с произвольным количеством столбцов? Например, у меня есть
fn
иVector{Symbol}
.4. Просто повторите
Vector{Symbol}
. Если вы приведете мне конкретный пример того, что вам нужно, я могу предложить вам решение (в вашем вопросе его нет).5. Я обновил вопрос другим примером, в котором подчеркивается, что я хотел бы сделать.
Ответ №2:
Мой текущий подход включает в себя обертывание фрейма данных в структуру и перегрузку getindex
/ setindex!
. Для получения возможности доступа к столбцам по имени требуется некоторая дополнительная хитрость с использованием сгенерированных функций. Хотя это эффективно, это также довольно сложно, и я надеялся, что есть более элегантное решение, использующее только фреймы данных.
Для простоты предполагается, что все (соответствующие) столбцы имеют Float64
тип.
struct DataFrameWrapper{colnames}
cols::Vector{Vector{Float64}}
end
function df_to_vectors(df::AbstractDataFrame, colnames::Vector{Symbol})::Vector{Vector{Float64}}
res = Vector{Vector{Float64}}(undef, length(colnames))
for i in 1:length(colnames)
res[i] = df[!,colnames[i]]
end
res
end
function DataFrameWrapper{colnames}(df::AbstractDataFrame) where colnames
DataFrameWrapper{colnames}(df_to_vectors(df, collect(colnames)))
end
get_colnames(::Type{DataFrameWrapper{colnames}}) where colnames = colnames
@generated function get_col_index(x::DataFrameWrapper, ::Val{col})::Int64 where col
id = findfirst(y -> y == col, get_colnames(x))
:($id)
end
Base.@propagate_inbounds Base.getindex(x::DataFrameWrapper, col::Val)::Vector{Float64} = x.cols[get_col_index(x, col)]
Base.@propagate_inbounds Base.getindex(x::DataFrameWrapper, col::Symbol)::Vector{Float64} = getindex(x, Val(col))
Base.@propagate_inbounds Base.setindex!(x::DataFrameWrapper, value::Float64, row::Int64, col::Val) = setindex!(x.cols[get_col_index(x, col)], value, row)
Base.@propagate_inbounds Base.setindex!(x::DataFrameWrapper, value::Float64, row::Int64, col::Symbol) = setindex!(x, value, row, Val(col))