Julia DataFrame如何不显式指定变量生成分组汇总统计量
问题场景
给定一个包含多个数值变量、最后一列为类别列cl的Julia DataFrame,示例输入如下:
julia> df 5×3 DataFrame Row │ v1 v2 cl │ Int64? Int64 Int64 ─────┼─────────────────────── 1 │ 10 1 2 2 │ 20 2 2 3 │ 300 10 1 4 │ 400 20 1 5 │ missing 30 1
需要生成按类别分组的汇总统计DataFrame,要求类别作为列、各变量及对应统计指标作为行,期望输出格式如下:
julia> dfByCl 11×3 DataFrame Row │ var cl_1 cl_2 │ String Float64? Float64? ─────┼─────────────────────────────── 1 │ nrow 3.0 2.0 2 │ v1_mean 350.0 15.0 3 │ v1_std 70.7107 7.07107 4 │ v1_lb 252.002 5.20018 5 │ v1_ub 447.998 24.7998 6 │ v1_nm 2.0 2.0 7 │ v2_mean 20.0 1.5 8 │ v2_std 10.0 0.707107 9 │ v2_lb 8.68414 0.520018 10 │ v2_ub 31.3159 2.47998 11 │ v2_nm 3.0 2.0
要求代码中不需要显式枚举所有变量名,实现比原有示例代码更简洁优雅。
原有示例代码:
using Statistics, DataFrames, Distributions meansk(data) = mean(skipmissing(data)) stdsk(data) = std(skipmissing(data)) nm(data) = sum(.! ismissing.(data)) ci(data::AbstractVector,α=0.05) = meansk(data) - quantile(Normal(),1-α/2)*stdsk(data)/sqrt(nm(data)), meansk(data) + quantile(Normal(),1-α/2)*stdsk(data)/sqrt(nm(data)) cilb(data) = ci(data)[1] ciub(data) = ci(data)[2] df = DataFrame(v1=[10,20,300,400,missing],v2=[1,2,10,20,30],cl=[2,2,1,1,1]) dfByCl_w = combine(groupby(df,["cl"]), nrow, names(df) .=> meansk .=> col -> col * "_mean", names(df) .=> stdsk .=> col -> col * "_std", names(df) .=> cilb .=> col -> col * "_lb", names(df) .=> ciub .=> col -> col * "_ub", names(df) .=> nm .=> col -> col * "_nm", ) orderedNames = vcat("cl","nrow",[ ["$(n)_mean", "$(n)_std", "$(n)_lb", "$(n)_ub", "$(n)_nm"] for n in names(df)[1:end-1]]...) dfByCl_w = dfByCl_w[:, orderedNames] toStack = vcat("nrow",[ ["$(n)_mean", "$(n)_std", "$(n)_lb", "$(n)_ub", "$(n)_nm"] for n in names(df)[1:end-1]]...) dfByCl_l = stack(dfByCl_w,toStack) dfByCl = unstack(dfByCl_l,"cl","value") rename!(dfByCl,vcat("var",["cl_$(c)" for c in unique(dfByCl_w.cl)]))
更简洁的实现方案
通过提前定义统计配置列表、自动识别非类别列的方式消除重复代码,不需要手动指定列顺序、重复构造列名列表,后续新增/修改统计指标、新增数值变量时不需要改动核心逻辑:
using Statistics, DataFrames, Distributions # 基础统计函数 meansk(x) = mean(skipmissing(x)) stdsk(x) = std(skipmissing(x)) nm(x) = count(!ismissing, x) # 置信区间上下限 function cilb(x, α=0.05) m, s, n = meansk(x), stdsk(x), nm(x) m - quantile(Normal(), 1-α/2) * s / sqrt(n) end function ciub(x, α=0.05) m, s, n = meansk(x), stdsk(x), nm(x) m + quantile(Normal(), 1-α/2) * s / sqrt(n) end # 统计规则配置:(统计函数, 列名后缀),新增指标直接在这里加即可 stats = [ meansk => "_mean", stdsk => "_std", cilb => "_lb", ciub => "_ub", nm => "_nm" ] df = DataFrame(v1=[10,20,300,400,missing],v2=[1,2,10,20,30],cl=[2,2,1,1,1]) # 自动提取所有非类别列,不需要手动指定变量名 val_cols = setdiff(names(df), ["cl"]) # 自动生成所有聚合操作 agg_ops = Any[nrow] for col in val_cols, (fn, suffix) in stats push!(agg_ops, col => fn => Symbol(col, suffix)) end # 分组聚合得到宽表 wide = combine(groupby(df, :cl), agg_ops...) # 长宽表转换自动得到目标格式 long = stack(wide, Not(:cl), variable_name=:var) res = unstack(long, :var, :cl, :value, renamecols=x -> Symbol("cl_", x)) # 调整行顺序:nrow在前,后续按变量+指标顺序排列 row_order = vcat("nrow", vec([string(col, suffix) for col in val_cols, (_, suffix) in stats])) res = res[indexin(row_order, string.(res.var)), :]
运行得到的res和期望输出完全一致,代码可维护性更高:新增统计指标只需要修改stats配置列表,新增数值列时代码会自动识别处理,不需要修改任何聚合逻辑。
内容的提问来源于stack exchange,提问作者Antonello
相关产品推荐
相关产品推荐

