欠損値

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 == missingmissing を返すことに注目してください。これは == では欠損値を見つけられないことを意味します。xmissing かどうかを判定するには ismissing(x) を使ってください。

特別な比較演算子 isequal=== は伝播規則の例外です。この二つの関数はオペランドに missing があっても必ず Bool 型の値を返します。そのときの比較では missingmissing と等しく、他の任意の値と異なるものとみなされます。このため、これらの関数は値が 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} 型の配列と値の種類 (TMissing のどちらなのか) を収めた 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 が一つも含まれないなら、convertmissing を許さない通常の配列に変換できます。変換するとき配列に 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
[...]

また keyseachindex は欠損値を飛ばします。この機能により、添え字に対して処理を行う関数でも 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

isequalmissing 同士を等しいとみなし、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
日本語 Julia 書籍 (Amazon アソシエイト)
1 から始める Julia プログラミング
Julia プログラミングクックブック―言語仕様からデータ分析、機械学習、数値計算まで
スタンフォード ベクトル・行列からはじめる最適化数学