Анализ и визуализация данных
Работа с пропущенными данными
Работа с пропущенными данными — обычная задача при предварительной обработке данных. Julia имеет множество возможностей для работы с отсутствующими данными.
Обработка пропущенных данных
В качестве примера рассмотрим таблицу electricity.xlsx, находящуюся в папке данного курса. В этой таблице собраны помесячные данные о потреблении электричества в США с января 1990 г. по декабрь 2019 г. Данные разделяются на разные секторы (потребление жилых помещений, коммерческих потребителей, промышленности и общее потребление, в которое входят несколько неучтенных групп). В таблице приведены значения потребления этих секторов за каждый месяц в мегаватт-часах.
Импортируем данные и подготовим их для анализа:
# Загрузить данные
using DataFrames, XLSX
data = DataFrame( XLSX.readtable("electricity.xlsx", "Sheet1"))
# Отделить даты от данных о потреблении
sectors = names(data)[2:end]
usage = data[:,sectors]
dates = data.Date
print(usage)
Чтобы воспроизвести более реальную атмосферу работы с данными, среди значений отсутствуют данные за некоторые месяцы, т.е. некоторые ячейки пропущены.
С пропущенными данными (missing) можно поступить одним из трех способов:
- проигнорировать;
- удалить из массива;
- заменить другими значениями.
Первый вариант подразумевает использование функции skipmissing. При ее вызове создается итератор — объект, который индексирует исходные данные. Входной массив при этом не изменяется, также эта функция не создает копии исходных данных, так что при ее использовании экономится память. Если наглядно преобразовать итератор в массив, то мы получим массив без этих значений:
a = [45, missing, 64, missing, 73, 91, missing, 52]
collect(skipmissing(a))
✏️Задание 1
Дан массив [1, 2 missing, 4]. Создайте на его основе новый массив, в котором будут удалены пропущенные данные.
Подсказка
Воспользуйтесь функцией skipmissing. Т.к. эта функция создает не массив, а итератор (диапазон), нужно с помощью функции collect преобразовать его в массив.
Решение
a = [1, 2, missing, 4]
collect(skipmissing(a))
Полное удаление значений missing позволяет получить более чистый массив, с которым удобнее работать. Однако, для того чтобы количество переменных в каждой строке оставалось одинаковым, скорее всего, придется избавиться не только от пропущенных ячеек, но и от строк, которые их содержат. Вместе с пустой ячейкой мы выбросим и остальные ячейки из строки — те, которые содержали вполне полезные данные. При работе с объектом DataFrame избавиться от строк, содержащих пропущенные значения, можно при помощи функции dropmissing.
Построим графики потребления электроэнергии по каждому сектору, а также график общего потребления:
using Plots
plot(dates, usage.Residential, label="Residential")
plot!(dates, usage.Commercial, label="Commercial")
plot!(dates, usage.Industrial, label="Industrial")
plot!(dates, usage.Total, label="Total")
Заметьте, что графики иногда прерываются. Причина в том, что в исходной таблице присутствуют пропущенные значения.
Обнаружение недостающих значений
В Julia имеется достаточно функций для проверки принадлежности значений к тому или иному классу: функций, начинающихся на is. Они принимают на вход массив с данными, а возвращают массив такого же размера, но с логическими переменными. В каждой ячейке массива располагается ответ на вопрос, обладают ли некоторой характеристикой данные в соответствующей ячейке исходного массива.
Например, функция isnan принимает на вход данные и возвращает значение true для тех ячеек, которые содержат значение NaN (не число, not a number). Функция isfinite проверяет, содержит ли ячейка значение Inf (бесконечность, infinity), а функция ismissing — содержит ли ячейка пропущенное значение.
Пример:
X = [Inf, 4, NaN, 1, 7]
println(X)
println(ismissing.(X))
println(isnan.(X))
println(isfinite.(X))
Если на входе такой функции находится матрица, на выходе тоже будет матрица.
В случае большого набора значений, как правило, нас будет интересовать, имеется ли вообще в массиве хотя бы одно значение того или иного типа. Это можно сделать с помощью функции any:
any(isnan.(X))
Чтобы проверить, содержатся ли пропущенные значения в каждой строке или столбце исходной таблицы, нужно преобразовать ее в тип Matrix и указать размерность, вдоль которой применяется функция any, задав ее аргумент под названием dims:
any(ismissing.(X), dims=2)
В результате мы получаем вектор-строку или вектор-столбец значений логического типа Bool.
Подсчитать количество пропущенных значений можно с помощью функций count или sum. Например, с помощью функции sum подсчитаем, сколько содержится пропущенных данных в таблице usage:
sum(ismissing.(Matrix(usage)))
✏️Задание 2
С помощью функции count подсчитайте, сколько содержится пропущенных данных в таблице usage.
Подсказка
Воспользуйтесь функцией count. В качестве ее аргумента запишите функцию ismissing, проверяющую, содержит ли ячейка таблицы пропущенное значение. Аргументом функции ismissing, в свою очередь, является таблица usage, преобразованная в матрицу.
Решение
count(ismissing.(Matrix(usage)))
Устранение недостающих значений
Чтобы удалить строки с пропущенными значениями из таблицы DataFrame, нужно вызвать функцию dropmissing (она не приспособлена для работы с обычными массивами).
С ее помощью вы можете переместить подходящие строки в отдельный массив:
x = DataFrame(A=[1,2,3], B=["a",missing,"c"])
y = dropmissing(x)
Или же просто удалить неподходящие строки из исходного массива:
dropmissing!(x)
✏️Задание 3
Удалите из таблицы usage строки, в которых содержатся пропущенные данные.
Подсказка
Воспользуйтесь функцией dropmissing. Аргументом функции является таблица usage.
Решение
dropmissing(usage)
Если вас интересуют пропущенные значения лишь некоторых переменных (столбцов), то нужно передать интересующие вас столбцы в качестве второго аргумента функции dropmissing.
Например:
y = dropmissing(x, :B)
✏️Задание 4
Удалите из столбца Total таблицы usage строки, в которых содержатся пропущенные данные, и запишите результат в новую таблицу dataOK.
Подсказка
Воспользуйтесь функцией dropmissing. В качестве второго аргумента функции запишите имя столбца "Total".
Решение
dataOK = dropmissing(usage, "Total")
✏️Задание 5
Для полученной таблицы dataOK постройте графики потребления электроэнергии по каждому сектору, а также график общего потребления.
Подсказка
Один раз воспользуйтесь функцией plot и трижды — функцией plot!. В качестве первого аргумента задайте столбец, содержащий даты (dataOK.date), а в качестве второго аргумента — столбцы, содержащие потребление электроэнергии по каждому сектору, а также общее потребление (dataOK.Residential, dataOK.Commercial, dataOK.Industrial и dataOK.Total).
Решение
plot(dataOK.Date, dataOK.Residential, label="Residential")
plot!(dataOK.Date, dataOK.Commercial, label="Commercial")
plot!(dataOK.Date, dataOK.Industrial, label="Industrial")
plot!(dataOK.Date, dataOK.Total, label="Total")
Для векторизованных математических операций лучше использовать тип данных Matrix. Другая функция, которую мы уже использовали, называлась skipmissing. Она порождает итератор, который проходит по всем элементам массива, пропуская значения missing. Итератор — это объект, который не вычисляет отдельные значения последовательности, пока этого специально не потребуют.
Посмотрим, какой результат мы получим, если выполним skipmissing на входной двумерной матрице, и раскроем итератор при помощи команды collect:
x = Matrix([1 2 3; "a" missing "b"])
collect(skipmissing(x))
Заполнение недостающих значений
Можно выполнить подстановку значений вместо пропущенных данных. В результате заполнения пропусков новыми значениями с данными тоже становится легко работать, однако, поскольку подставляемые значения получены не в результате измерений, а в результате оценки, полученные данные могут быть неточными.
Для замены в массиве x значений типа missing на значения y можно воспользоваться командой coalesce.(x, y).
Например, заменим нулями пропущенные данные в массиве a:
a = [45, missing, 64, missing, 73, 91, missing, 52]
coalesce.(a, 0)
✏️Задание 6
Замените нулями пропущенные данные в таблице usage.
Подсказка
Воспользуйтесь функцией coalesce. Первым аргументом функции является имя таблицы usage, вторым — то значение, которым мы заменяем пропущенные данные, т.е. 0. После имени функции нужно поставить точку, т.к. она применяется к каждому элементу таблицы.
Решение
coalesce.(usage, 0)
Для замены пропущенных данных также можно воспользоваться функцией replace!(). Например, заменим значением 25 пропущенные данные в массиве a:
replace!(a, missing => 25)
Еще один способ — использование функции Missings.replace():
b = collect(Missings.replace(a, 25))