Julia:将 DataFrame 传递给函数会创建指向 DataFrame 的指针?

Posted

技术标签:

【中文标题】Julia:将 DataFrame 传递给函数会创建指向 DataFrame 的指针?【英文标题】:Julia: Passing a DataFrame to a function creates a pointer to the DataFrame? 【发布时间】:2015-03-18 05:01:23 【问题描述】:

我有一个函数可以标准化 DataFrame 的前 N ​​列。我想返回规范化的 DataFrame,但不要理会原始数据。然而,该函数似乎也改变了传递的 DataFrame!

using DataFrames

function normalize(input_df::DataFrame, cols::ArrayInt)
    norm_df = input_df
    for i in cols
        norm_df[i] = (input_df[i] - minimum(input_df[i])) / 
            (maximum(input_df[i]) - minimum(input_df[i]))
    end
    norm_df
end

using RDatasets
iris = dataset("datasets", "iris")
println("original df:\n", head(iris))

norm_df = normalize(iris, [1:4]);
println("should be the same:\n", head(iris))

输出:

original df:
6x5 DataFrame
| Row | SepalLength | SepalWidth | PetalLength | PetalWidth | Species  |
|-----|-------------|------------|-------------|------------|----------|
| 1   | 5.1         | 3.5        | 1.4         | 0.2        | "setosa" |
| 2   | 4.9         | 3.0        | 1.4         | 0.2        | "setosa" |
| 3   | 4.7         | 3.2        | 1.3         | 0.2        | "setosa" |
| 4   | 4.6         | 3.1        | 1.5         | 0.2        | "setosa" |
| 5   | 5.0         | 3.6        | 1.4         | 0.2        | "setosa" |
| 6   | 5.4         | 3.9        | 1.7         | 0.4        | "setosa" |

should be the same:
6x5 DataFrame
| Row | SepalLength | SepalWidth | PetalLength | PetalWidth | Species  |
|-----|-------------|------------|-------------|------------|----------|
| 1   | 0.222222    | 0.625      | 0.0677966   | 0.0416667  | "setosa" |
| 2   | 0.166667    | 0.416667   | 0.0677966   | 0.0416667  | "setosa" |
| 3   | 0.111111    | 0.5        | 0.0508475   | 0.0416667  | "setosa" |
| 4   | 0.0833333   | 0.458333   | 0.0847458   | 0.0416667  | "setosa" |
| 5   | 0.194444    | 0.666667   | 0.0677966   | 0.0416667  | "setosa" |
| 6   | 0.305556    | 0.791667   | 0.118644    | 0.125      | "setosa" |

【问题讨论】:

【参考方案1】:

Julia 使用一种称为“传递共享”的行为。来自文档(强调我的):

Julia 函数参数遵循有时称为“传递共享”的约定,这意味着将值传递给函数时不会复制它们。函数参数本身充当新的变量绑定(可以引用值的新位置),但它们引用的值与传递的值相同。 在函数中对可变值(例如数组)所做的修改将对调用者可见。 这与在 Scheme、大多数 Lisps、Python、Ruby 和 Perl 以及其他动态语言中发现的行为相同。

在您的特定情况下,您似乎想要做的是为您的规范化操作创建一个全新且独立的 DataFrame。有两个操作可以做到这一点:copydeepcopy。如果所有 DataFrame 列的元素类型都是不可变的(例如 IntFloat64String 等),那么 copy 就足够了。但是,如果其中一列包含可变类型,则需要使用deepcopy。函数调用如下所示:

norm_df = copy(input_df)     # Column types are immutable
norm_df = deepcopy(input_df) # At least one column type is mutable

Julia 通常会要求您明确地执行这些操作,因为创建大型数据框的独立副本可能会耗费大量计算资源,而 Julia 是一种面向性能的语言。

对于那些想要了解更多关于 copydeepcopy 之间区别的详细信息的人,请再次从文档中注意以下几点:

copy(x):创建 x 的浅拷贝:复制外部结构,但不复制所有内部值。例如,复制一个数组会生成一个新数组,其元素与原始数组相同。

deepcopy(x):创建 x 的深层副本:一切都被递归复制,从而产生一个完全独立的对象。例如,深拷贝一个数组会产生一个新数组,其元素是原始元素的深拷贝。

类型DataFrame 类似于数组,因此如果元素是可变的,则deepcopy 是必需的。如果您不确定,请使用deepcopy(虽然它会更慢)。

一个相关的 SO 问题是here。

【讨论】:

谢谢!那么在哪些情况下,对象是复制还是深度复制?例如,如果我这样做,iris2 = iris 然后将iris2 传递给normalize()iris 是否也应该被修改? @AlexanderFlyax 是的,iris 会被修改。通常y=x 形式的任何操作不会 创建一个新的独立对象,无论类型如何(尽管有趣的是,对于向量,y=x[1:end](我认为)创建一个独立的对象)。对于像Int64这样的具体类型,copy创建一个新的独立对象,但是对于ArrayInt64copy只创建一个独立的外部结构,而不是独立的内部元素,因此需要deepcopy 处理数组时。 DataFrame 类似于数组,因此需要deepcopy 感谢您的解释。我并不是要批评 Julia,只是想了解:如果修改副本会修改原始数组,为什么我需要制作数组的副本?如果我想修改原始数组,我会这样做,不是吗?即,它似乎充其量是多余的,最坏的情况是适得其反。我错过了什么吗? @AlexanderFlyax 好问题。我不知道!我最好的猜测是它围绕范围旋转,即在某些情况下,在一个范围内拥有一组引用(例如调用函数)同时在不同的范围内拥有一组独立的引用(例如称为函数),但两组引用都指向内存中的同一个对象。请不要把这当成福音。也许您可以将该问题作为一个新的 SO 问题提出(不一定是 Julia 特定的)并获得更明智的答复...

以上是关于Julia:将 DataFrame 传递给函数会创建指向 DataFrame 的指针?的主要内容,如果未能解决你的问题,请参考以下文章

在 Julia 中加入 DataFrame

R语言ggplot2可视化:将dataframe和数据列名称传递给函数通过函数进行ggplot2可视化输出

使用 JULIA RCall 包将 NA 传递给 R

Julia 将 DataFrame 写入 csv 失败 UndefRefError: access to undefined reference

Julia 中 @distributed 宏下的混淆范围规则

更改 DataFrame 中的列数据类型并将其传递到 UDF - PySpark