有没有一种简单的方法可以并行运行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])