Julia: 如何从表达式创建 DataFrame 列?

3

考虑到这一点:

dict = Dict(("y" => ":x / 2"))

df = DataFrame(x = [1, 2, 3, 4])

df
4×1 DataFrame
│ Row │ x     │
│     │ Int64 │
├─────┼───────┤
│ 11     │
│ 22     │
│ 33     │
│ 44

我想制作这个:

4×2 DataFrame
│ Row │ x     │ y       │
│     │ Int64 │ Float64 │
├─────┼───────┼─────────┤
│ 1   │ 1     │ 0.5     │
│ 2   │ 2     │ 1.0     │
│ 3   │ 3     │ 1.5     │
│ 4   │ 4     │ 2.0     │

这似乎是一个完美的应用程序,可以使用 DataFramesMeta 的 @with 或 @eachrow,但我尚未能够在存在 :x 的环境中按预期评估我的表达式。
基本上,我想能够迭代字典中的 (k, v) 对,并为每个 Symbol(k) 创建一个新列,其相应值为 eval(Meta.parse(v)) 或类似的内容,其中求值发生在诸如 :x 之类的 Symbols 在评估时存在的情况下。
我没指望这会起作用,而且它确实没有。
[df[Symbol(k)] = eval(Meta.parse(v)) for (k, v) in dict]

ERROR: MethodError: no method matching /(::Symbol, ::Int64)

但这说明了问题:我需要在包含这些符号的环境中计算这些表达式。

但是,将其移到@with内部并不能解决问题:

using DataFramesMeta

@with(df, [eval(Meta.parse(v)) for (k, v) in dict])

ERROR: MethodError: no method matching /(::Symbol, ::Int64)

使用@eachrow会出现相同的失败情况:

using DataFramesMeta

@eachrow df begin
           for (k, v) in dict
               @newcol tmp::Vector{Float32}
               tmp = eval(Meta.parse(v))
           end
       end

ERROR: MethodError: no method matching /(::Symbol, ::Int64)

我猜我对于DataFramesMeta如何在DataFrame中创建环境的关键要素理解不清晰。我也不一定非得使用DataFramesMeta,因为任何合理简洁的选项都可以,因为我可以将其封装在一个包函数中。
注:我控制要解析为表达式的字符串的格式,但我想避免复杂性,例如在字符串中指定DataFrame对象的名称或广播每个操作。我希望初始字符串中的表达式语法对于非Julia程序员来说相对清晰。
更新:我在这个问题的评论中尝试了所有三个解决方案,它们有一个问题:它们在函数内部不起作用。
dict = Dict(("y" => ":x / 2"))

data = DataFrame(x = [1, 2, 3, 4])


function transform_from_dict(df, dict)

    new = eval(Meta.parse("@transform(df, " * join(join.(collect(dict), " = "), ", ") * ")"))

    return new

end

transform_from_dict(data, dict)

ERROR: UndefVarError: df not defined

或者:
function transform_from_dict!(df, dict)

    [df[!, Symbol(k)] = eval(:(@with(df, $(Meta.parse(v))))) for (k, v) in dict]

    return nothing

end

transform_from_dict!(data, dict)

ERROR: UndefVarError: df not defined

1
尝试使用以下代码:[df[!,Symbol(k)] = eval(DataFramesMeta.with_helper(df, Meta.parse(v))) for (k, v) in dict] - 张实唯
2
请仅返回翻译后的文本:或者 [df[!,Symbol(k)] = eval(:(@with(df, $(Meta.parse(v))))) for (k, v) in dict]。这两个都有些棘手。关键是不要直接 eval v,而是让 DataFramesMeta 用它的魔力来评估它。 - 张实唯
2
一般情况下,我不建议执行此类操作,因为它们是不安全的。如果您确实需要这样做,那么例如这个代码可以工作:eval(Meta.parse("@transform(df, " * join(join.(collect(dict), " = "), ", ") * ")")) - Bogumił Kamiński
2
它们将在函数内失败,因为eval在模块的全局作用域中被评估。DataFramesMeta.jl不会创建eval环境,但其宏在编译时而非运行时解析。 - Bogumił Kamiński
1
在你的脚本开头添加 global new,并在函数(内部)的开头也添加 global new。这是 Python 中的一个通用技巧,适用于任何需要在运行时读取并随时间增长的对象。请参阅 https://thispointer.com/python-how-to-use-global-variables-in-a-function/。也许它也适用于 Julia。然后,df 可以在任何列表 / 字典推导式中增长。未经测试:也许它也可以与 eval() + Meta.parse() 结合使用。顺便说一下:我没有看到这是 Julia 而不是 Python,请将这个想法转移到 Julia。 - questionto42
显示剩余7条评论
2个回答

1

好的,将所有评论者的答案结合起来就可以了!

using DataFrames
using DataFramesMeta

dict = Dict(("y" => ":x / 2"))

data = DataFrame(x = [1, 2, 3, 4])

@张实唯使用@with的方法:

# using @with
function transform_from_dict1(df, dict)

    global df

    [df[!, Symbol(k)] = eval(:(@with(df, $(Meta.parse(v))))) for (k, v) in dict]

    return df

end

transform_from_dict1(data, dict)
# 4×2 DataFrame
# │ Row │ x     │ y       │
# │     │ Int64 │ Float64 │
# ├─────┼───────┼─────────┤
# │ 1   │ 1     │ 0.5     │
# │ 2   │ 2     │ 1.0     │
# │ 3   │ 3     │ 1.5     │
# │ 4   │ 4     │ 2.0     │

@Bogumił Kamiński使用@transform的方法:

# using @transform
function transform_from_dict2(df, dict)

    global df

    new_df = eval(Meta.parse("@transform(df, " * join(join.(collect(dict), " = "), ", ") * ")"))

    return new_df

end

transform_from_dict2(data, dict)
# 4×2 DataFrame
# │ Row │ x     │ y       │
# │     │ Int64 │ Float64 │
# ├─────┼───────┼─────────┤
# │ 1   │ 1     │ 0.5     │
# │ 2   │ 2     │ 1.0     │
# │ 3   │ 3     │ 1.5     │
# │ 4   │ 4     │ 2.0     │

两者都包含了来自@Lorenz的修复,使用global

请注意,第二种形式使用的内存比第一种形式多约2.5倍,可能是由于创建了第二个DataFrame所致:

julia> @allocated transform_from_dict1(data, dict)
853948

julia> @allocated transform_from_dict2(data, dict)
22009111

我也认为第一个表单更清晰,所以我在内部使用它。

请注意,如果您的转换中有逻辑运算符,则可能需要广播这些运算符,并且通常需要事先处理任何缺失数据问题。


非常感谢您接受我的答案。同时也要感谢您的肯定,因为我完全不了解Julia,您并没有期望我能够回答。感谢您的公平竞赛。另外,时间测试也会得到我的点赞。 - questionto42

1

我和@Ajar一起独立完成了这个答案,没有复制那个答案的任何内容,也不知道它的存在。因为我对Julia完全是陌生的,所以我必须安装它(因为我认为在线编译器甚至不知道DataFrame),后来我明白了这些包无论是在线还是离线都必须在开始时调用。我添加了初学者可能需要了解的包信息。

using Pkg 
Pkg.add("DataFrames")
Pkg.add("DataFramesMeta")

using DataFrames
using DataFramesMeta 
dict = Dict(("y" => ":x / 2"))
df = DataFrame(x = [1, 2, 3, 4])
使用@with解决方案:
julia> function transform_from_dict!(k, v)
           global df
           df[!, Symbol(k)] = eval(:(@with(df, $(Meta.parse(v)))))
           return nothing
       end
transform_from_dict! (generic function with 2 methods)
julia> [transform_from_dict!(k, v) for (k, v) in dict]
1-element Array{Nothing,1}:
 nothing
julia> df
4×2 DataFrame
 Row │ x      y
     │ Int64  Float64
─────┼────────────────
   1 │     1      0.5
   2 │     2      1.0
   3 │     3      1.5
   4 │     4      2.0
@transform解决方案:
julia> function transform_from_dict(df, dict)
           global new
           new = eval(Meta.parse("@transform(df, " * join(join.(collect(dict), " = "), ", ") * ")"))

           return new

       end
transform_from_dict (generic function with 1 method)
julia>

julia> transform_from_dict(data, dict)
4×2 DataFrame
 Row │ x      y
     │ Int64  Float64
─────┼────────────────
   11      0.5
   22      1.0
   33      1.5
   44      2.0

感谢其他评论者,以及 @Ajar 答案中列出的基本思想。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接