Missing Values

Julia предоставляет поддержку для представления отсутствующих значений в статистическом смысле. Это для ситуаций, когда для переменной в наблюдении нет доступного значения, но теоретически существует допустимое значение. Отсутствующие значения представлены через объект missing, который является единственным экземпляром типа Missing. missing эквивалентен NULL in SQL и NA in R, и ведет себя как они в большинстве ситуаций.

Propagation of Missing Values

missing значения распространяются автоматически, когда они передаются стандартным математическим операторам и функциям. Для этих функций неопределенность относительно значения одного из операндов вызывает неопределенность относительно результата. На практике это означает, что математическая операция, включающая missing значение, обычно возвращает missing:

julia> missing + 1
missing

julia> "a" * missing
missing

julia> abs(missing)
missing

Поскольку missing является обычным объектом Julia, это правило распространения работает только для функций, которые выбрали реализацию этого поведения. Это можно достичь следующим образом:

  • добавление конкретного метода, определенного для аргументов типа Missing,
  • принимая аргументы этого типа и передавая их функциям, которые их распространяют (как стандартные математические операторы).

Пакеты должны учитывать, имеет ли смысл передавать отсутствующие значения при определении новых функций, и соответственно определять методы, если это так. Передача значения missing в функцию, которая не имеет метода, принимающего аргументы типа Missing, вызывает MethodError, так же как и для любого другого типа.

Функции, которые не передают значения missing, могут быть изменены для этого, если обернуть их в функцию passmissing, предоставляемую пакетом Missings.jl. Например, f(x) становится passmissing(f)(x).

Equality and Comparison Operators

Стандартные операторы равенства и сравнения следуют правилу распространения, представленному выше: если любой из операндов является missing, результат будет missing. Вот несколько примеров:

julia> missing == 1
missing

julia> missing == missing
missing

julia> missing < 1
missing

julia> 2 >= missing
missing

В частности, обратите внимание, что missing == missing возвращает missing, поэтому == не может быть использован для проверки, является ли значение отсутствующим. Чтобы проверить, является ли x отсутствующим, используйте ismissing(x).

Специальные операторы сравнения isequal и === являются исключениями из правила распространения. Они всегда будут возвращать значение Bool, даже в присутствии значений missing, рассматривая 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

Logical operators

Логические (или булевы) операторы |, & и xor являются особым случаем, поскольку они распространяют значения missing только тогда, когда это логически необходимо. Для этих операторов, зависит от конкретной операции, будет ли результат неопределенным или нет. Это соответствует хорошо установленным правилам three-valued logic, которые реализованы, например, в NULL в SQL и NA в R. Эта абстрактная дефиниция соответствует относительно естественному поведению, которое лучше всего объясняется через конкретные примеры.

Давайте проиллюстрируем этот принцип с помощью логического оператора "или" |. Следуя правилам булевой логики, если один из операндов равен true, значение другого операнда не влияет на результат, который всегда будет true:

julia> true | true
true

julia> true | false
true

julia> false | true
true

На основе этого наблюдения мы можем заключить, что если один из операндов равен true, а другой отсутствует, мы знаем, что результат будет true, несмотря на неопределенность относительно фактического значения одного из операндов. Если бы мы смогли наблюдать фактическое значение второго операнда, оно могло бы быть только true или false, и в обоих случаях результат был бы true. Следовательно, в этом конкретном случае отсутствие значения не распространяется:

julia> true | missing
true

julia> missing | true
true

Напротив, если один из операндов равен false, результат может быть либо true, либо false в зависимости от значения другого операнда. Поэтому, если этот операнд отсутствует, результат также должен быть 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, так же как и другие унарные операторы.

Control Flow and Short-Circuiting Operators

Операторы управления потоком, включая if, while и ternary operator, не допускают отсутствующих значений. Это связано с неопределенностью относительно того, будет ли фактическое значение true или false, если мы сможем его наблюдать. Это подразумевает, что мы не знаем, как программа должна себя вести. В этом случае TypeError выбрасывается, как только в этом контексте встречается значение missing:

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

Arrays With Missing Values

Массивы, содержащие пропущенные значения, можно создавать так же, как и другие массивы:

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
Note

Использование undef или similar в настоящее время может привести к созданию массива, заполненного missing, но это не правильный способ получения такого массива. Вместо этого используйте конструктор missing, как показано выше.

Массив с типом элемента, допускающим missing записи (например, Vector{Union{Missing, T}}), который не содержит никаких missing записей, может быть преобразован в массив, который не допускает missing записи (например, Vector{T}) с использованием convert. Если массив содержит значения 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

Skipping Missing Values

Поскольку значения 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> sum(x)
6

julia> mapreduce(sqrt, +, x)
4.146264369941973

Объекты, созданные вызовом skipmissing на массиве, могут индексироваться с использованием индексов из родительского массива. Индексы, соответствующие отсутствующим значениям, не являются допустимыми для этих объектов, и при попытке их использования возникает ошибка (они также пропускаются функциями keys и eachindex):

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

Logical Operations on Arrays

Логика с тремя значениями, описанная выше для логических операторов, также используется логическими функциями, применяемыми к массивам. Таким образом, тесты на равенство массивов с использованием оператора == возвращают 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