欠損値
Julia は統計的な意味での欠損値 (missing value) を表現する方法を提供します。欠損値とは正当な値が理論上は存在するものの観測に失敗したことを表す値です。Missing
型のシングルトンインスタンス missing
が欠損値を表します。この missing
は SQL の NULL
や R の NA
と同様であり、多くの場合で振る舞いも同じです。
欠損値の伝播
標準の数学演算子と数学関数は missing
を自動的に伝播します。こういった関数のオペランドが不確かだと結果も不確かになるということです。実際のコードを使って説明すると、一般に missing
が絡む数学演算は missing
となります:
julia> missing + 1
missing
julia> "a" * missing
missing
julia> abs(missing)
missing
missing
は通常の Julia オブジェクトなので、この伝播規則は missing
が絡む振る舞いを個別に実装した関数でのみ効果を持ちます。この振る舞いを実装するには Missing
型の引数に対する特定的なメソッドを定義するか、Missing
型の引数を伝播する関数 (標準の数学演算子など) だけを使って返り値を計算するメソッドを定義します。パッケージで新しい関数を定義するときは欠損値を伝播する意味があるかどうかを考え、必要なら適切にメソッドを定義するべきです。Missing
型の値を受け取るメソッドが存在しない関数に missing
を渡すと、他の対応しない型を渡したときと同様の MethodError
が発生します。
missing
を伝播しない関数であっても、Missings.jl が提供する passmissing
関数に包めば伝播するようにできます。例えば 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
で使われるので、sort
によるソートでは missing
が最後に並びます。isless
の使用例を示します:
julia> isless(1, missing)
true
julia> isless(missing, Inf)
false
julia> isless(missing, missing)
false
論理演算子
論理 (真偽) 演算子 |
, &
, xor
は「missing
が絡む関数や演算子は必ず missing
を返す」という規則の例外であり、論理的に必要な場合に限って missing
を伝播します。これらの演算子では、結果が不確かどうかは演算ごとに決定されます。このときに使われる規則には三値論理という名前が付いており、SQL の NULL
や R の NA
でも同様の実装となっています。三値論理による定義は抽象的ですが、具体的な例を見れば比較的自然であることが理解できるでしょう。
論理 OR 演算子 |
の規則をここで説明します。ブール論理の規則に従うと、オペランドの片方が true
のときもう一方のオペランドは返り値に影響を及ぼさず、返り値は必ず true
となります:
julia> true | true
true
julia> true | false
true
julia> false | true
true
この観察から、もしオペランドの片方が true
でもう一方が missing
なら、オペランドの片方が不正確にもかかわらず返り値は true
であると結論できます。missing
のオペランドに実際の値を代入したとすると、そのオペランドは true
または false
にしかならず、どちらの値に対しても返り値は true
となるためです。よってこの場合 missing
は伝播しません:
julia> true | missing
true
julia> missing | true
true
これに対してオペランドの一つが false
のときは、返り値はもう一つの値に応じて true
にも false
にもなります。このため、もう一つのオペランドが missing
のときは返り値も missing
でなければなりません:
julia> false | true
true
julia> true | false
true
julia> false | false
false
julia> false | missing
missing
julia> missing | false
missing
論理 AND 演算子 &
の振る舞いは |
演算子と同様ですが、片方のオペランドが false
のときに限って欠損値が伝播しない点が異なります。一つ目のオペランドが false
である場合の例を示します:
julia> false & false
false
julia> false & true
false
julia> false & missing
false
これに対して、片方のオペランドが true
のときは欠損値が伝播します。一つ目のオペランドが true
である場合の実行例を示します:
julia> true & true
true
julia> true & false
false
julia> true & missing
missing
最後に、論理排他 OR 演算子 xor
ではどんな場合でも両方のオペランドが返り値に影響するので、missing
が必ず伝播されます。また否定演算子 !
も他の単項演算子と同様 missing
に対して missing
を返します。
制御構造と短絡評価
if
, while
および三項演算子 x ? y : z
といった制御構造演算子 (キーワード) では欠損値を利用できません。観測が可能であった場合の実際の値は true
または false
なので、プログラムがどちらを実行すべきか定まらないためです。この文脈で missing
を使うとすぐに TypeError
が発生します:
julia> if missing
println("here")
end
ERROR: TypeError: non-boolean (Missing) used in boolean context
同様の理由で、短絡評価を行う真偽演算子 &&
と ||
で missing
を使うときは、missing
以降のオペランドが評価されるかどうかが 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
に到達する前に短絡評価で結果が確定するとき、および missing
が最後のオペランドのときです:
julia> true && missing
missing
julia> false && missing
false
欠損値を持つ配列
欠損値を持つ配列は通常の配列と同じように作成します:
julia> [1, missing]
2-element Array{Union{Missing, Int64},1}:
1
missing
この例から分かるように、欠損値を持つ配列の要素型は Union{Missing, T}
となります (T
は欠損していない値の型)。これはただ配列の各要素が T
型 (ここでは Int64
) と Missing
型のどちらかである事実を表しているだけです。この種の配列は存在する値を収めた Array{T}
型の配列と値の種類 (T
か Missing
のどちらなのか) を収めた Array{UInt8}
型の配列を保持することで効率的にデータを格納します (参照: isbits 型共用体の配列)。
欠損値を許す配列は標準の構文を使って構築できます。例えば全ての要素が欠損値の配列を作成するには Array{Union{Missing, T}}(missing, dims)
を使います:
julia> Array{Union{Missing, String}}(missing, 2, 3)
2×3 Array{Union{Missing, String},2}:
missing missing missing
missing missing missing
missing
を許す配列に missing
が一つも含まれないなら、convert
で missing
を許さない通常の配列に変換できます。変換するとき配列に missing
があれば MethodError
が発生します:
julia> x = Union{Missing, String}["a", "b"]
2-element Array{Union{Missing, String},1}:
"a"
"b"
julia> convert(Array{String}, x)
2-element Array{String,1}:
"a"
"b"
julia> y = Union{Missing, String}[missing, "b"]
2-element Array{Union{Missing, String},1}:
missing
"b"
julia> convert(Array{String}, y)
ERROR: MethodError: Cannot `convert` an object of type Missing to an object of type String
欠損値を飛ばす反復
missing
は標準の数学演算で伝播されるので、配列全体を反復して値を計算する関数に 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> mean(x)
2.0
julia> mapreduce(sqrt, +, x)
4.146264369941973
配列に対する skipmissing
が返すオブジェクトには元の配列に対する添え字を使ってアクセスできます。欠損値に対する添え字アクセスは無効であり、次に示すように MissingException
が発生します:
julia> x[1]
3
julia> x[2]
ERROR: MissingException: the value at index (2,) is missing
[...]
また keys
や eachindex
は欠損値を飛ばします。この機能により、添え字に対して処理を行う関数でも skipmissing
を利用できるようになります。特に探索や検索を行う関数では、返り値の添え字が skipmissing
が返したオブジェクトと元の (欠損値を含む) 配列の両方に対して正当な添え字となります:
julia> findall(==(1), x)
1-element Array{Int64,1}:
4
julia> findfirst(!iszero, x)
1
julia> argmax(x)
1
collect
を使うと配列に含まれる missing
でない値を全て取り出せます:
julia> collect(x)
3-element Array{Int64,1}:
3
2
1
配列に対する論理演算
上述した論理演算子における三値論理は配列に対する論理関数でも使われます。例えば ==
演算子で配列の等価性を判定するときは、missing
の本当の値を知らなければ返り値が決まらないとき missing
が返ります。具体的に言うと、missing
でない要素が全て等しく、いずれかの配列に missing
があるとき等価判定は missing
を返します:
julia> [1, missing] == [2, missing]
false
julia> [1, missing] == [1, missing]
missing
julia> [1, 2, missing] == [1, missing, 2]
missing
isequal
は missing
同士を等しいとみなし、missing
と数値を異なるとみなします:
julia> isequal([1, missing], [1, missing])
true
julia> isequal([1, 2, missing], [1, missing, 2])
false
any
関数と all
関数も三値論理の規則に従い、返り値が定まらないとき missing
を返します:
julia> all([true, missing])
missing
julia> all([false, missing])
false
julia> any([true, missing])
true
julia> any([false, missing])
missing