缺失值

Julia 提供了支持,用于表示统计意义上的缺失值。这适用于在观测中,某个变量没有可用值,但理论上存在有效值的情况。缺失值通过 missing 对象表示,它是 Missing 类型 的单例实例。missing 等同于 SQL 中的 NULLR 中的 NA,并且在大多数情况下表现得像它们一样。

缺失值的传播

missing 值在传递给标准数学运算符和函数时会自动传播。对于这些函数,关于某个操作数的值的不确定性会引起关于结果的不确定性。在实践中,这意味着涉及 missing 值的数学运算通常会返回 missing

julia> missing + 1
missing

julia> "a" * missing
missing

julia> abs(missing)
missing

由于 missing 是一个普通的 Julia 对象,因此这种传播规则只对已选择实施此行为的函数有效。可以通过以下方法实现这一点:

  • 添加针对类型为 Missing 的参数定义的特定方法,
  • 接受这种类型的参数,并将它们传递给传播它们的函数(如标准数学运算符)。

包在定义新函数时应该考虑是否需要传播缺失值,并在必要时定义相应的方法。将 missing 值传递给没有接受类型为 Missing 的参数的方法的函数会抛出一个 MethodError,就像其他任何类型一样。

可以通过将不传播 missing 值的函数包装在 Missings.jl 包提供的 passmissing 函数中,使它们传播 missing 值。例如,f(x) 变为 passmissing(f)(x)

相等和比较运算符

标准相等和比较运算符遵循上面介绍的传播规则:如果任何一个操作数是 missing,则结果为 missing。以下是一些示例

julia> missing == 1
missing

julia> missing == missing
missing

julia> missing < 1
missing

julia> 2 >= missing
missing

特别是,请注意 missing == missing 返回 missing,因此 == 不能用于测试某个值是否缺失。要测试 x 是否为 missing,请使用 ismissing(x)

特殊比较运算符 isequal=== 是传播规则的例外。即使存在 missing 值,它们也始终会返回 Bool 值,将 missing 视为等于 missing 并且与任何其他值不同。因此,它们可用于测试某个值是否为 missing

julia> missing === 1
false

julia> isequal(missing, 1)
false

julia> missing === missing
true

julia> isequal(missing, missing)
true

isless 运算符是另一个例外:missing 被视为大于任何其他值。此运算符由 sort! 使用,因此它将 missing 值放在所有其他值之后

julia> isless(1, missing)
true

julia> isless(missing, Inf)
false

julia> isless(missing, missing)
false

逻辑运算符

逻辑(或布尔)运算符 |&xor 是另一种特殊情况,因为它们只在逻辑上需要时才会传播 missing 值。对于这些运算符,结果是否不确定取决于具体的运算。这遵循由例如 SQL 中的 NULL 和 R 中的 NA 实现的 三值逻辑 的既定规则。这个抽象定义对应于一个相对自然的行为,通过具体的示例可以最好地解释。

让我们用逻辑“或”运算符 | 来说明这个原理。遵循布尔逻辑的规则,如果其中一个操作数是 true,则另一个操作数的值不会影响结果,结果始终为 true

julia> true | true
true

julia> true | false
true

julia> false | true
true

基于此观察结果,我们可以得出结论,如果其中一个操作数是 true,另一个操作数是 missing,我们知道结果是 true,尽管对其中一个操作数的实际值存在不确定性。如果我们能够观察到第二个操作数的实际值,它只能是 truefalse,在这两种情况下,结果都将是 true。因此,在这种特定情况下,缺失性不会传播

julia> true | missing
true

julia> missing | true
true

相反,如果其中一个操作数是 false,则结果可能为 truefalse,具体取决于另一个操作数的值。因此,如果该操作数是 missing,则结果也必须是 missing

julia> false | true
true

julia> true | false
true

julia> false | false
false

julia> false | missing
missing

julia> missing | false
missing

逻辑“与”运算符 & 的行为类似于 | 运算符,不同之处在于当其中一个操作数是 false 时,缺失性不会传播。例如,当第一个操作数是这种情况时

julia> false & false
false

julia> false & true
false

julia> false & missing
false

另一方面,当其中一个操作数是 true 时,缺失性会传播,例如第一个操作数

julia> true & true
true

julia> true & false
false

julia> true & missing
missing

最后,“异或”逻辑运算符 xor 始终传播 missing 值,因为两个操作数始终对结果有影响。还要注意,否定运算符 ! 在操作数为 missing 时返回 missing,就像其他一元运算符一样。

控制流和短路运算符

控制流运算符,包括 ifwhile三元运算符 x ? y : z 不允许缺失值。这是因为关于如果我们能够观察到实际值,它将是 true 还是 false 存在不确定性。这意味着我们不知道程序应该如何运行。在这种情况下,一遇到 missing 值,就会抛出一个 TypeError

julia> if missing
           println("here")
       end
ERROR: TypeError: non-boolean (Missing) used in boolean context

出于同样的原因,与上面介绍的逻辑运算符相反,短路布尔运算符 &&|| 不允许在以下情况下出现 missing 值:操作数的值决定是否评估下一个操作数。例如

julia> missing || false
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> missing && false
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> true && missing && false
ERROR: TypeError: non-boolean (Missing) used in boolean context

相反,当结果可以在不使用 missing 值的情况下确定时,不会抛出错误。当代码在评估 missing 操作数之前短路,以及当 missing 操作数是最后一个操作数时,就会出现这种情况

julia> true && missing
missing

julia> false && missing
false

包含缺失值的数组

可以像其他数组一样创建包含缺失值的数组

julia> [1, missing]
2-element Vector{Union{Missing, Int64}}:
 1
  missing

如本例所示,此类数组的元素类型为 Union{Missing, T},其中 T 为非缺失值的类型。这反映了数组条目可以是类型 T(此处为 Int64)或类型 Missing。这种类型的数组使用与 Array{T} 等效的有效内存存储,存储实际值,并结合使用 Array{UInt8} 指示条目的类型(即,它是 Missing 还是 T)。

允许缺失值的数组可以使用标准语法创建。使用 Array{Union{Missing, T}}(missing, dims) 创建填充缺失值的数组。

julia> Array{Union{Missing, String}}(missing, 2, 3)
2×3 Matrix{Union{Missing, String}}:
 missing  missing  missing
 missing  missing  missing
注意

当前使用 undefsimilar 可能会得到填充 missing 的数组,但这并不是获取此类数组的正确方法。请改用如上所示的 missing 构造函数。

元素类型允许 missing 条目(例如 Vector{Union{Missing, T}})且不包含任何 missing 条目的数组可以使用 convert 转换为不允许 missing 条目的数组类型(例如 Vector{T})。如果数组包含 missing 值,则在转换期间会抛出 MethodError

julia> x = Union{Missing, String}["a", "b"]
2-element Vector{Union{Missing, String}}:
 "a"
 "b"

julia> convert(Array{String}, x)
2-element Vector{String}:
 "a"
 "b"

julia> y = Union{Missing, String}[missing, "b"]
2-element Vector{Union{Missing, String}}:
 missing
 "b"

julia> convert(Array{String}, y)
ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type String

跳过缺失值

由于 missing 值会随着标准数学运算符传播,因此当在包含缺失值的数组上调用约简函数时,它们会返回 missing

julia> sum([1, missing])
missing

在这种情况下,请使用 skipmissing 函数跳过缺失值。

julia> sum(skipmissing([1, missing]))
1

此便捷函数返回一个迭代器,该迭代器有效地过滤掉了 missing 值。因此,它可以与支持迭代器的任何函数一起使用。

julia> x = skipmissing([3, missing, 2, 1])
skipmissing(Union{Missing, Int64}[3, missing, 2, 1])

julia> maximum(x)
3

julia> sum(x)
6

julia> mapreduce(sqrt, +, x)
4.146264369941973

通过对数组调用 skipmissing 创建的对象可以使用来自父数组的索引进行索引。与缺失值对应的索引对于这些对象无效,并且尝试使用它们时会抛出错误(它们也会被 keyseachindex 跳过)。

julia> x[1]
3

julia> x[2]
ERROR: MissingException: the value at index (2,) is missing
[...]

这允许对索引进行操作的函数与 skipmissing 结合使用。这在搜索和查找函数中尤其明显。这些函数返回对 skipmissing 返回的对象有效的索引,并且也是父数组中匹配条目的索引。

julia> findall(==(1), x)
1-element Vector{Int64}:
 4

julia> findfirst(!iszero, x)
1

julia> argmax(x)
1

使用 collect 提取非 missing 值并将它们存储在数组中。

julia> collect(x)
3-element Vector{Int64}:
 3
 2
 1

数组上的逻辑运算

上面针对逻辑运算符描述的三值逻辑也由应用于数组的逻辑函数使用。因此,使用 == 运算符进行的数组相等性测试,只要结果无法确定而不了解 missing 条目的实际值,就会返回 missing。实际上,这意味着如果比较数组的所有非缺失值相等,但一个或两个数组包含缺失值(可能在不同的位置),则返回 missing

julia> [1, missing] == [2, missing]
false

julia> [1, missing] == [1, missing]
missing

julia> [1, 2, missing] == [1, missing, 2]
missing

与单个值一样,使用 isequalmissing 值视为等于其他 missing 值,但不等于非缺失值。

julia> isequal([1, missing], [1, missing])
true

julia> isequal([1, 2, missing], [1, missing, 2])
false

函数 anyall 也遵循三值逻辑规则。因此,当结果无法确定时,返回 missing

julia> all([true, missing])
missing

julia> all([false, missing])
false

julia> any([true, missing])
true

julia> any([false, missing])
missing