Документация Engee
Notebook

Отсутствующие данные в Julia

Введение

Работа с отсутствующими данными - обычная задача при предварительной обработке данных. Хотя иногда отсутствующие значения указывают на значимое событие в данных, они часто представляют собой ненадежные или непригодные для использования точки данных. В любом случае Julia имеет множество возможностей для работы с отсутствующими данными.

Создание отсутствующих данных

Отсутствующие данные в Julia можно представить в нескольких формах. Например, значение NaN (NaN64) (не число)

In [ ]:
x_64 = [NaN, 8, 15, 16, 23, 42]
println("Type of $(x_64[1]): ", typeof(x_64[1]))

x_64 = [NaN64, 8, 15, 16, 23, 42]
print("Type of $(x_64[1]): ", typeof(x_64[1]))
Type of NaN: Float64
Type of NaN: Float64

Значение NaN при необходимости может быть задано как число с плавающей точкой меньшей разрядности:

In [ ]:
x_32 = [NaN32, 8, 15, 16, 23, 42]
Out[0]:
6-element Vector{Float32}:
 NaN
   8.0
  15.0
  16.0
  23.0
  42.0
In [ ]:
x_16 = [NaN16, 8, 15, 16, 23, 42]
Out[0]:
6-element Vector{Float16}:
 NaN
   8.0
  15.0
  16.0
  23.0
  42.0

Говоря о сути такой формы представления отсутствующих данных, можно заключить, что это конкретные значения типа данных с плавающей точкой, которые имеют возможность распространяться в вычислениях. Зачастую такие значения появляются в результате неопределённых операций:

In [ ]:
x_nan = Inf*0
Out[0]:
NaN

Как правило, в Julia значения NaN используются для распространения в вычислениях неопределённости расчёта числовых значений. Для работы именно с отсутствующими данными корректнее будет использовать специальный объект missing, который также распространяется в вычислениях. Такая форма представления является единственным экземпляром типа Missing:

In [ ]:
typeof(missing)
Out[0]:
Missing

Это означает, в частности, что массивы, содержащие missing среди других значений, неоднородны по типу:

In [ ]:
x_missing = [missing, 8, 15, 16, 23, 42]
Out[0]:
6-element Vector{Union{Missing, Int64}}:
   missing
  8
 15
 16
 23
 42

Для работы с missing может пригодиться библиотека Missings.jl. Так, при помощи неё можно создавать массивы с такими объектами:

In [ ]:
import Pkg; Pkg.add("Missings") # загрузка библиотеки
In [ ]:
using Missings # подключение библиотеки

# создание массивов с отсутствующими значениями:
# массивы типа Missing
@show missings(1)
@show missings(3)
@show missings(3,1)

# массив объединения типов
@show missings(Int,3,3); 
missings(1) = [missing]
missings(3) = [missing, missing, missing]
missings(3, 1) = [missing; missing; missing;;]
missings(Int, 3, 3) = Union{Missing, Int64}[missing missing missing; missing missing missing; missing missing missing]

Систематизация отсутствующих данных

При сортировке массивов с отсутствующими данными необходимо учитывать, что объект missing считается больше любого объекта, с которым он сравнивается:

In [ ]:
isless(Inf, missing)
Out[0]:
true

Поэтому, при сортировке пропущенные значения будут автоматически отделены, и будут находиться в конце возрастающей сортировки. В случае, если при сортировке требуется изменить порядок включения отсутствующих значений, достаточно применить атрибут lt = missingsmallest:

In [ ]:
sort(x_missing, rev=true, lt = missingsmallest)
Out[0]:
6-element Vector{Union{Missing, Int64}}:
 42
 23
 16
 15
  8
   missing

Нормализация отсутствующих данных

Для рассмотрения примеров работы с пропусками в табличных данных подключим библиотеки DataFrames.jl и Statistics.jl.

In [ ]:
Pkg.add(["DataFrames", "Statistics"])
using DataFrames, Statistics

Создадим таблицу тестовых данных для обработки:

In [ ]:
df_missing = DataFrame(
    имя     = ["NULL", "Коля", "Юра", "Миша"],
    возраст = [16, NaN, missing, 15],
    рост    = [171, 162, 999, 165],
)
Out[0]:
4×3 DataFrame
Rowимявозрастрост
StringFloat64?Int64
1NULL16.0171
2КоляNaN162
3Юраmissing999
4Миша15.0165

При работе с данными разных форматов, объединенными в одну таблицу, могут возникнуть случаи, когда отсутствующие данные принимают различные значения. Для стандартизации значений отсутствующих данных удобно использовать функцию declaremissings() из библиотеки Impute.jl:

In [ ]:
Pkg.add("Impute")
In [ ]:
using Impute

df_missing = Impute.declaremissings(df_missing; values=(NaN, 999, "NULL"))
Out[0]:
4×3 DataFrame
Rowимявозрастрост
String?Float64?Int64?
1missing16.0171
2Коляmissing162
3Юраmissingmissing
4Миша15.0165

Теперь, как мы видим, отсутствующие данные приведены к одному виду - объекту missing.

Поиск отсутствующих данных

Для определения, является ли значение NaN, удобно воспользоваться функцией isnan()

In [ ]:
isnan.(x_64)
Out[0]:
6-element BitVector:
 1
 0
 0
 0
 0
 0

Существует и аналогичная функция для определения объектов missing.

In [ ]:
ismissing.(x_missing)
Out[0]:
6-element BitVector:
 1
 0
 0
 0
 0
 0

Определим расположение отсутствующих значений в таблице:

In [ ]:
df_mask = ismissing.(df_missing)
Out[0]:
4×3 DataFrame
Rowимявозрастрост
BoolBoolBool
1truefalsefalse
2falsetruefalse
3falsetruetrue
4falsefalsefalse

Часто объекты missing могут вызывать проблемы при обработке данных. Для этого их можно пропустить, исключить или заменить.

Пропуск отсутствующих данных

Для фильтрации вектора от значений NaN достаточно воспользоваться функцией filter() базовой библиотеки Julia.

In [ ]:
filter(!isnan, x_64)
Out[0]:
5-element Vector{Float64}:
  8.0
 15.0
 16.0
 23.0
 42.0

В случае фильтрации массива с объектом missing тип результирующего массива не изменится. Для приведения типа массива к типу отфильтрованных значений можно воспользоваться функцией disallowmissing() библиотеки Missings.jl.

In [ ]:
@show x = filter(!ismissing, x_missing)
disallowmissing(x)
x = filter(!ismissing, x_missing) = Union{Missing, Int64}[8, 15, 16, 23, 42]
Out[0]:
5-element Vector{Int64}:
  8
 15
 16
 23
 42

Следующая строка кода показывает, как отфильтровать missing из табличных данных с помощью функции filter().

In [ ]:
filter(:имя => !ismissing, df_missing)
Out[0]:
3×3 DataFrame
Rowимявозрастрост
String?Float64?Int64?
1Коляmissing162
2Юраmissingmissing
3Миша15.0165

Аналогичный результат даёт применение функции skipmissing() базовой библиотеки:

In [ ]:
collect(skipmissing(x_missing))
Out[0]:
5-element Vector{Int64}:
  8
 15
 16
 23
 42

Для пропуска missing в табличных данных в библиотеке DataFrames.jl есть более удобная функция - dropmissing():

In [ ]:
dropmissing(df_missing)
Out[0]:
1×3 DataFrame
Rowимявозрастрост
StringFloat64Int64
1Миша15.0165

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

In [ ]:
dropmissing(df_missing, :имя)
Out[0]:
3×3 DataFrame
Rowимявозрастрост
StringFloat64?Int64?
1Коляmissing162
2Юраmissingmissing
3Миша15.0165

В этом случае функция dropmissing() возвращает новую таблицу. Если для решаемой задачи не нужно сохранять исходную таблицу, можно удалить строки с отсутствующими данными при помощи dropmissing!().

Замена отсутствующих данных

В случае необходимости замены отсутствующих данных в массиве удобно воспользоваться функцией Missings.replace():

In [ ]:
рост = collect(Missings.replace(df_missing.рост, 170))
Out[0]:
4-element Vector{Int64}:
 171
 162
 170
 165

Для замены отсутствующих данных в таблицах можно воспользоваться функцией replace!():

In [ ]:
replace!(df_missing.возраст, missing => 15)
Out[0]:
4-element Vector{Union{Missing, Float64}}:
 16.0
 15.0
 15.0
 15.0

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

In [ ]:
df_missing.имя = coalesce.(df_missing.имя, "Ваня")
df_missing.рост = coalesce.(df_missing.рост, mean(skipmissing(df_missing.рост)))
df_missing
Out[0]:
4×3 DataFrame
Rowимявозрастрост
StringFloat64?Real
1Ваня16.0171
2Коля15.0162
3Юра15.0166.0
4Миша15.0165

Заключение

В этом примере были рассмотрены способы создания, систематизации, нормализации, поиска, пропуска и замены отсутствующих данных в Julia.