Джулия: стабильность типа с фреймами данных

#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))