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

Использование CategoricalArrays

Базовое использование

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

julia> using CategoricalArrays

julia> x = CategoricalArray(["Old", "Young", "Middle", "Young"], ordered=true)
4-element CategoricalArray{String,1,UInt32}:
 "Old"
 "Young"
 "Middle"
 "Young"

По умолчанию уровни сортируются по лексическому принципу, что явно не является правильным в нашем случае и выдаст неверные результаты при проверке на порядок. Это легко исправить с помощью функции levels! для переупорядочения уровней:

julia> levels(x)
3-element Vector{String}:
 "Middle"
 "Old"
 "Young"

julia> levels!(x, ["Young", "Middle", "Old"])
4-element CategoricalArray{String,1,UInt32}:
 "Old"
 "Young"
 "Middle"
 "Young"

Благодаря такому порядку можно не только проверить равенство двух значений, но и сравнить возраст, например 1-го и 2-го человека.

julia> x[1]
CategoricalValue{String, UInt32} "Old" (3/3)

julia> x[2]
CategoricalValue{String, UInt32} "Young" (1/3)

julia> x[2] == x[4]
true

julia> x[1] > x[2]
true

Теперь представим, что первый человек входит в группу «Молодые». Давайте зафиксируем это (обратите внимание, как строка "Young" автоматически преобразуется в CategoricalValue):

julia> x[1] = "Young"
"Young"

julia> x[1]
CategoricalValue{String, UInt32} "Young" (1/3)

CategoricalArray по-прежнему рассматривает "Old" как возможный уровень, даже если сейчас он не используется. Это необходимо для эффективного доступа к уровням и установки значений элементов в массиве: действительно, для отбрасывания неиспользуемых уровней требуется итерация каждого элемента в массиве, что дорого. Это свойство также может быть полезно для отслеживания возможных уровней, даже если они не встречаются на практике.

Чтобы избавиться от группы "Old", просто вызовите функцию droplevels!:

julia> levels(x)
3-element Vector{String}:
 "Young"
 "Middle"
 "Old"

julia> droplevels!(x)
4-element CategoricalArray{String,1,UInt32}:
 "Young"
 "Young"
 "Middle"
 "Young"

julia> levels(x)
2-element Vector{String}:
 "Young"
 "Middle"

Другим решением был бы вызов levels!(x, ["Young", "Middle"]) вручную. Эта команда также безопасна, поскольку при попытке удалить уровни, которые используются в данный момент, будет выдана ошибка:

julia> levels!(x, ["Young", "Midle"])
ERROR: ArgumentError: cannot remove level "Middle" as it is used at position 3. Change the array element type to Union{String, Missing} using convert if you want to transform some levels to missing values.
[...]

Обратите внимание, что записи в массиве x не могут считаться строками. Их нужно преобразовать в строки с помощью String(x[i]):

julia> lowercase(String(x[3]))
"middle"

julia> replace(String(x[3]), 'M'=>'R')
"Riddle"

Обратите внимание, что вызов String не снижает производительность по сравнению с работой с Vector{String}, так как он просто возвращает строковый объект, который хранится в пуле.

Целочисленные коды, указывающие индекс каждого значения в уровнях, можно получить с помощью функции levelcode:

julia> levelcode(x[1])
1

julia> levelcode.(x)
4-element Vector{Int64}:
 1
 1
 2
 1

Работа с отсутствующими значениями

В приведенных выше примерах предполагалось, что данные не содержат отсутствующих значений. Как правило, это не так для реальных данных. Именно здесь в игру вступает CategoricalArray{Union{T, Missing}}. По сути, это эквивалент категориальных данных Array{Union{T, Missing}}. Он действует точно так же, как CategoricalArray{T}, за исключением того, что при индексировании возвращает либо объект CategoricalValue{T}, либо missing, если значение отсутствует. Дополнительные сведения о типе Missing см. в руководстве по Julia.

Адаптируем пример, разработанный выше, для поддержки отсутствующих значений. Поскольку во входном векторе нет отсутствующих значений, нам нужно указать, что массив должен содержать либо String, либо missing:

julia> y = CategoricalArray{Union{Missing, String}}(["Old", "Young", "Middle", "Young"], ordered=true)
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 "Old"
 "Young"
 "Middle"
 "Young"

Уровни все равно нужно переупорядочивать вручную:

julia> levels(y)
3-element Vector{String}:
 "Middle"
 "Old"
 "Young"

julia> levels!(y, ["Young", "Middle", "Old"])
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 "Old"
 "Young"
 "Middle"
 "Young"

На этом этапе индексирование массива дает точно такой же результат

julia> y[1]
CategoricalValue{String, UInt32} "Old" (3/3)

Отсутствующие значения можно вводить либо вручную, либо ограничив набор возможных уровней. На этот раз представим, что возраст первого человека нам неизвестен. Мы можем задать для него отсутствующее значение следующим образом.

julia> y[1] = missing
missing

julia> y
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 missing
 "Young"
 "Middle"
 "Young"

julia> y[1]
missing

Можно также преобразовать все значения, принадлежащие некоторым уровням, в отсутствующие значения, что дает тот же результат, что и в данном случае, поскольку в группе "Old" есть только один человек. Сначала восстановим исходное значение для первого элемента, а затем снова зададим ему отсутствующее значение, используя аргумент allowmissing для levels!:

julia> y[1] = "Old"
"Old"

julia> y
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 "Old"
 "Young"
 "Middle"
 "Young"

julia> levels!(y, ["Young", "Middle"]; allowmissing=true)
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 missing
 "Young"
 "Middle"
 "Young"

И наоборот, все отсутствующие значения можно преобразовать в «нормальные» с помощью функции replace (или recode, синтаксис которой идентичен для этой операции):

julia> replace(y, missing => "missing value")
4-element CategoricalArray{String,1,UInt32}:
 "missing value"
 "Young"
 "Middle"
 "Young"

Обратите внимание, что возвращаемый массив больше не допускает отсутствующие значения (что обычно и ожидается). Этот синтаксис работает для типов массивов, отличных от CategoricalArray.

Также доступен вариант replace! на месте (соответственно recode!). Обратите внимание, что y по-прежнему допускает отсутствующие значения (поскольку тип объекта не может быть изменен).

julia> replace!(y, missing => "missing value");

julia> y
4-element CategoricalArray{Union{Missing, String},1,UInt32}:
 "missing value"
 "Young"
 "Middle"
 "Young"

Объединение уровней

Некоторые операции подразумевают объединение уровней двух категориальных массивов: это происходит при конкатенации массивов (vcat, hcat и cat) и при присвоении CategoricalValue из другого категориального массива.

Например, представим, что у нас есть два набора наблюдений: один только с молодой частью населения и другой — с более старшей:

julia> x = categorical(["Middle", "Old", "Middle"], ordered=true);

julia> y = categorical(["Young", "Middle", "Middle"], ordered=true);

julia> levels!(y, ["Young", "Middle"]);

При конкатенации двух наборов уровни результирующего категориального вектора выбираются таким образом, чтобы относительные порядки уровней в x и y по возможности сохранялись. В этом случае сравнения с < и > остаются действительными, а результирующий вектор помечается как упорядоченный:

julia> xy = vcat(x, y)
6-element CategoricalArray{String,1,UInt32}:
 "Middle"
 "Old"
 "Middle"
 "Young"
 "Middle"
 "Middle"

julia> levels(xy)
3-element Vector{String}:
 "Young"
 "Middle"
 "Old"

julia> isordered(xy)
true

Аналогично, присвоение CategoricalValue из y записи в x расширяет уровни x во всеми уровнями из y с соблюдением по возможности порядка уровней обоих векторов:

julia> levels(x)
2-element Vector{String}:
 "Middle"
 "Old"

julia> x[1] = y[1]
CategoricalValue{String, UInt32} "Young" (1/2)

julia> levels(x)
3-element Vector{String}:
 "Young"
 "Middle"
 "Old"

julia> x[1]
CategoricalValue{String, UInt32} "Young" (1/3)

В случаях, когда объединяются уровни с несовместимыми порядками, побеждает порядок массива назначения, и массив назначения помечается как неупорядоченный. То же самое происходит при конкатенации массивов, и в случае конфликта побеждает порядок первого массива:

julia> a = categorical(["a", "b", "c"], ordered=true);

julia> b = categorical(["a", "b", "c"], ordered=true);

julia> ab = vcat(a, b)
6-element CategoricalArray{String,1,UInt32}:
 "a"
 "b"
 "c"
 "a"
 "b"
 "c"

julia> levels(ab)
3-element Vector{String}:
 "a"
 "b"
 "c"

julia> isordered(ab)
true

julia> levels!(b, ["c", "b", "a"])
3-element CategoricalArray{String,1,UInt32}:
 "a"
 "b"
 "c"

julia> ab2 = vcat(a, b)
6-element CategoricalArray{String,1,UInt32}:
 "a"
 "b"
 "c"
 "a"
 "b"
 "c"

julia> levels(ab2)
3-element Vector{String}:
 "a"
 "b"
 "c"

julia> isordered(ab2)
false

Результирующий массив помечается как упорядоченный только в том случае, если все исходные массивы упорядочены, за исключением того, что неупорядоченные массивы без уровней не требуют пометки результата как неупорядоченного. В частности, это позволяет присвоить CategoricalValue пустому CategoricalArray с помощью функции setindex!, чтобы скопировать уровни исходного значения и пометить результат как упорядоченный.

Обратите внимание, что в некоторых случаях два набора уровней могут иметь совместимый порядок, но невозможно определить, в каком порядке уровни должны появляться в объединенном наборе. Так обстоит дело, например, с ["a, "b", "d"] и ["c", "d", "e"]: невозможно определить, что "c" должен быть вставлен точно после "b" (лексикографическое упорядочение здесь не имеет значения). В таких случаях результирующий массив помечается как неупорядоченный. Такая ситуация может возникнуть только при работе с подмножествами данных, выбранными на основе несмежных подмножеств уровней.

Экспортируемые функции

categorical(A) — строит категориальный массив со значениями из A

compress(A) — возвращает копию категориального массива A, используя минимально возможный ссылочный тип

cut(x) — делит числовой массив на интервалы и возвращает упорядоченный CategoricalArray

decompress(A) — возвращает копию категориального массива A, используя заданный по умолчанию ссылочный тип

isordered(A) — проверяет, можно ли сравнивать записи в A с помощью <, > и подобных операторов

ordered!(A, ordered) — задает, можно ли сравнивать записи в A с помощью <, > и подобных операторов

recode(a[, default], pairs...) — возвращает копию a после замены одного или нескольких значений

recode!(a[, default], pairs...) — заменяет одно или несколько значений в a на месте

unwrap(x) — возвращает значение, содержащееся в категориальном значении x; если x равно Missing , возвращает missing

levelcode(x) — возвращает код категориального значения x, т. е. его индекс в наборе возможных значений, возвращаемых levels(x).

Дополнительные сведения см. в документе API Index.