有没有一种简单的方法可以并行运行DataFrames::by?

有没有一种简单的方法可以并行运行DataFrames::by?,dataframe,julia,Dataframe,Julia,我有一个大的数据帧,我想并行计算。我想并行化的调用是 df = by(df, [:Chromosome], some_func) 有没有一种方法可以轻松地将其并行化?最好不要复制 此外,我猜所使用的并行化类型应该根据创建的组的大小而有所不同 答案中使用的最小可重复示例: using DataFrames, CSV, Pkg iris = CSV.read(joinpath(Pkg.dir("DataFrames"), "test/data/iris.csv")) iris_count = b

我有一个大的数据帧,我想并行计算。我想并行化的调用是

df = by(df, [:Chromosome], some_func)
有没有一种方法可以轻松地将其并行化?最好不要复制

此外,我猜所使用的并行化类型应该根据创建的组的大小而有所不同


答案中使用的最小可重复示例:

using DataFrames, CSV, Pkg
iris = CSV.read(joinpath(Pkg.dir("DataFrames"), "test/data/iris.csv"))
iris_count = by(iris, [:Species], nrow)
在Windows运行控制台上(根据您拥有的内核/线程数进行调整):

在Linux运行控制台上:

$ export JULIA_NUM_THREADS=4
$ julia
现在检查它是否工作:

julia> Threads.nthreads()
4
运行以下代码(我将更新您的代码以匹配Julia 1.0):

让我们定义一些在
数据帧的一部分上运行的函数

 function nrow2(df::AbstractDataFrame)
     val = nrow(df) 
     #do something much more complicated...
     val
 end
现在,谜题中最复杂的部分来了:

function par_by(df::AbstractDataFrame,f::Function,cols::Symbol...;block_size=40)
    #f needs to be precompiled - we precompile using the first row of the DataFrame.
    #If try to do it within @thread macro
    #Julia will crash in most ugly and unexpected ways
    #if you comment out this line you can observe a different crash with every run
    by(view(df,1:1),[cols...],f);

    nr = nrow(df)
    local dfs = DataFrame()
    blocks = Int(ceil(nr/block_size))
    s = Threads.SpinLock()
    Threads.@threads for block in 1:blocks
        startix = (block-1)*block_size+1
        endix = min(block*block_size,nr)
        rv= by(view(df,startix:endix), [cols...], f)
        Threads.lock(s)
        if nrow(dfs) == 0  
            dfs = rv
        else 
            append!(dfs,rv)
        end
        Threads.unlock(s)
    end
    dfs
end
让我们测试它并汇总结果

julia> res = par_by(iris,nrow2,:Species)
6×2 DataFrame
│ Row │ Species    │ x1    │
│     │ String     │ Int64 │
├─────┼────────────┼───────┤
│ 1   │ versicolor │ 20    │
│ 2   │ virginica  │ 20    │
│ 3   │ setosa     │ 10    │
│ 4   │ versicolor │ 30    │
│ 5   │ virginica  │ 30    │
│ 6   │ setosa     │ 40    │


julia> by(res, :Species) do df;DataFrame(x1=sum(df.x1));end
3×2 DataFrame
│ Row │ Species    │ x1    │
│     │ String     │ Int64 │
├─────┼────────────┼───────┤
│ 1   │ setosa     │ 50    │
│ 2   │ versicolor │ 50    │
│ 3   │ virginica  │ 50    │
par_by
也支持多列

julia> res = par_by(iris,nrow2,:Species,:PetalType)
8×3 DataFrame
│ Row │ Species   │ PetalType │ x1    │
│     │ String    │ Bool      │ Int64 │
├─────┼───────────┼───────────┼───────┤
│ 1   │ setosa    │ false     │ 40    │
⋮
│ 7   │ virginica │ true      │ 13    │
│ 8   │ virginica │ false     │ 17    │
@BogumiłKamiński评论说,在线程化之前使用
groupby()
是合理的。除非出于某种原因,
groupby
成本太高(需要完全扫描),否则这是推荐的方法-使聚合更简单

 ress = DataFrame(Species=String[],count=Int[])
 for group in groupby(iris,:Species)
     r = par_by(group,nrow2,:Species,block_size=15)
     push!(ress,[r.Species[1],sum(r.x1)])
 end 


 julia> ress
 3×2 DataFrame
 │ Row │ Species    │ count │
 │     │ String     │ Int64 │
 ├─────┼────────────┼───────┤
 │ 1   │ setosa     │ 50    │
 │ 2   │ versicolor │ 50    │
 │ 3   │ virginica  │ 50    │
请注意,在上面的示例中,只有三个组,因此我们对每个组进行并行处理。但是,如果你有大量的组可以考虑运行:

function par_by2(df::AbstractDataFrame,f::Function,cols::Symbol...)
    res = NamedTuple[]
    s = Threads.SpinLock()
    groups = groupby(df,[cols...])
    f(view(groups[1],1:1));
    Threads.@threads for g in 1:length(groups)
        rv= f(groups[g])
        Threads.lock(s)
        key=tuple([groups[g][cc][1] for cc in cols]...)
        push!(res,(key=key,val=rv))
        Threads.unlock(s)
    end
    res
end

julia> iris.PetalType = iris.PetalWidth .> 2;

julia> par_by2(iris,nrow2,:Species,:PetalType)
4-element Array{NamedTuple,1}:
 (key = ("setosa", false), val = 50)
 (key = ("versicolor", false), val = 50)
 (key = ("virginica", true), val = 23)
 (key = ("virginica", false), val = 27)
让我知道它是否对你有用。
由于更多的人可能会遇到类似的问题,我将把这段代码放入一个Julia包中(这就是为什么我保持这段代码非常通用)

用Julia-p4启动Julia,然后运行

using CSV, DataFrames

iris = CSV.read(joinpath(dirname(pathof(DataFrames)),"..","test/data/iris.csv"))

g = groupby(iris, :Species)

pmap(nrow, [i for i in g])

这将并行运行groupby。

实际实现将取决于您是要使用mulithreading还是多进程。通常,您可以在一个进程中运行
groupby
,然后并行应用要创建的组的函数,最后在一个进程中再次合并结果。周末后将重试,并报告是否获得成功结果:)当然,如果apply part的计算成本很高,这将有所帮助,由于当前拆分应用合并管道中的拆分和合并步骤不支持DataFrames.jl中的并行处理。一般来说,JuliaDB.jl是一个完全支持核心外工作流的包。
julia-p4
提供了四个辅助进程,而不是线程。对于您的问题,我建议您首先研究线程。线程可以在CPU上使用相同的内存工作。另一方面,多重处理要求在进程之间复制数据(或者您可以使用
SharedArrays.jl
,但这也是关于在进程之间分割数据的),它不是在复制内存(查看
view()
的用法)。我做这些例子的目的就是为了让它们成为常见问题的标准答案。现在,我更新了代码,使其包含对任意数量列的支持。让我知道它是否有效。现在par_by2不返回数据帧,不像原来的by。它返回一个
数组{NamedTuple}
。par_by仍然有效:)您的
par_by
在我的一些数据上给出了与常规
by
不同的结果。这并不奇怪,因为它似乎将数据切割成块,然后在块上运行groupby,这将产生错误的结果。这使用多处理:(1)必须在进程之间复制数据(至少对于每个组),并且(2)需要确保远程进程中存在自定义函数和数据(例如,使用
@everywhere
)。另一方面,它可扩展到集群(线程仅可扩展到一台机器)。
function par_by2(df::AbstractDataFrame,f::Function,cols::Symbol...)
    res = NamedTuple[]
    s = Threads.SpinLock()
    groups = groupby(df,[cols...])
    f(view(groups[1],1:1));
    Threads.@threads for g in 1:length(groups)
        rv= f(groups[g])
        Threads.lock(s)
        key=tuple([groups[g][cc][1] for cc in cols]...)
        push!(res,(key=key,val=rv))
        Threads.unlock(s)
    end
    res
end

julia> iris.PetalType = iris.PetalWidth .> 2;

julia> par_by2(iris,nrow2,:Species,:PetalType)
4-element Array{NamedTuple,1}:
 (key = ("setosa", false), val = 50)
 (key = ("versicolor", false), val = 50)
 (key = ("virginica", true), val = 23)
 (key = ("virginica", false), val = 27)
using CSV, DataFrames

iris = CSV.read(joinpath(dirname(pathof(DataFrames)),"..","test/data/iris.csv"))

g = groupby(iris, :Species)

pmap(nrow, [i for i in g])