ГЛАВА 5 Извлечение признаков с помощью пакета feasts
Подобно свойствам другим совокупностей, мы можем компактно охарактеризовать свойства временных рядов с помощью стандартных показателей описательной статистики (среднее значение, медиана, дисперсия, стандартное отклонение, квантили и т.п.). Поскольку наблюдения во временных рядах упорядочены, то для них можно рассчитать также и многие специфичные показатели, такие как автокорреляция (гл. 4), выраженность тренда и сезонной компоненты, стационарность ряда и т.п.
Помимо того, что описательные статистики временных рядов представляют интерес сами по себе, их используют также в качестве признаков, или предикторов, при создании предсказательных моделей и в других задачах, решаемых с помощью методов машинного обучения (кластеризация, выявление аномалий и т.п.). Извлечение таких признаков из временных рядов с помощью R не составляет труда — для этого вполне подойдут как базовые функции, так и функции из многочисленных дополнительных пакетов. Однако учитывая то, как часто возникает необходимость в подобных вычислениях, в идеале было бы удобно иметь один–два пакета, в которых реализован соответствующий функционал. К счастью, такие пакеты для R есть. С одним из них — feasts
— мы уже познакомились в гл. 4. Ниже будут рассмотрены возможности feasts
по извлечению признаков из временных рядов на примере данных по стоимости 22 криптовалют (подразд. 1.5.1).
5.1 Функция features()
В пакете feasts
извлечение признаков из временных рядов выполняется с помощью функции–менеджера features()
, которая имеет следующие аргументы:
.tbl
— таблица с данными (например, объект классаdata.frame
,tibble
илиtsibble
);.var
— имя переменной в таблице.tbl
, по значениям которой необходимо вычислить те или иные признаки;features
— список функций, которые непосредственно выполняют вычисления соответствующих признаков;...
— дополнительные аргументы, которые необходимо подать на функции, перечисленные в спискеfeatures
. Важно отметить, что такие аргументы будут переданы на ту или иную функцию, только если они входят в состав формальных аргументов этой функции, а не являются ее дополнительными аргументами (...
). Например, в случае с базовой функциейvar()
, указание аргументаna.rm = TRUE
сработает, поскольку это формальный аргументvar()
. В то же время в случае с базовой функциейmean()
это не сработает, посколькуna.rm
является ее дополнительным аргументом. Чтобы убедиться в том, что необходимые аргументы будут поданы на нужную функцию, ее следует добавить в списокfeatures
в виде анонимной функции, как показано в следующем примере:
require(feasts)
# Неправильный способ подать дополнительный аргумент на функцию,
# которая вычисляет некоторый признак (здесь - среднее значение)
# при наличии пропущенных наблюдений (NA):
## features(dat_ts, y, features = list(mean), na.rm = TRUE)
# Правильный способ (с использованием анонимной функции):
features(cryptos, y, features = list(~ mean(., na.rm = TRUE)))
## # A tibble: 22 x 2
## coin V1
## <chr> <dbl>
## 1 augur 23.2
## 2 bitcoin 7492.
## 3 cardano 0.136
## 4 chainlink 0.938
## 5 dash 223.
## 6 decred 41.8
## 7 dogecoin 0.00350
## 8 eos 6.08
## 9 ethereum 340.
## 10 iota 0.776
## # ... with 12 more rows
Заметьте, что в последней команде нам не нужно было сначала сгруппировать данные по переменной coin
— расчет средних значений для отдельных уровней этой переменной был выполнен автоматически. Такой результат стал возможным благодаря тому, что таблица dat_ts
является объектом класса tsibble
, в котором переменная coin
служит “ключом”. Функции пакета feasts
“знают” как работать с подобными объектами.
Другой момент, на который стоит обратить внимание: результатом выполнения последней команды стала таблица, в которой рассчитанная новая переменная имеет мало о чем говорящее название V1
. Чтобы изменить такое поведение feasts
, рекомендуется сначала сохранить определение анонимной функции в виде полноценной пользовательской функции. Эта функция должна возвращать вектор с одним или несколькими поименованными элементами — их имена в итоге станут именами соответствующих переменных в итоговой таблице. В приведенном ниже примере пользовательская функция my_func(x)
возвращает вектор со значениями арифметического среднего и дисперсии x
:
my_func <- function(x) {
m <- mean(x, na.rm = TRUE)
v <- var(x, na.rm = TRUE)
return(c(avg = m, var = v))
}
features(cryptos, y, features = list(~ my_func(.)))
## # A tibble: 22 x 3
## coin avg var
## <chr> <dbl> <dbl>
## 1 augur 23.2 3.28e+2
## 2 bitcoin 7492. 6.72e+6
## 3 cardano 0.136 2.61e-2
## 4 chainlink 0.938 7.71e-1
## 5 dash 223. 4.66e+4
## 6 decred 41.8 7.46e+2
## 7 dogecoin 0.00350 3.46e-6
## 8 eos 6.08 1.23e+1
## 9 ethereum 340. 7.00e+4
## 10 iota 0.776 5.82e-1
## # ... with 12 more rows
5.2 Встроенные функции для расчета признаков
Помимо подаваемых на features()
пользовательских функций можно также воспользоваться целым рядом готовых функций, входящих в состав пакета feasts
. Например, для расчета коэффициента Xёрста и спектральной энтропии ряда служат функции coef_hurst()
и feat_spectral()
:
## # A tibble: 22 x 3
## coin coef_hurst spectral_entropy
## <chr> <dbl> <dbl>
## 1 augur 1.00 0.423
## 2 bitcoin 1.00 0.444
## 3 cardano 1.00 0.422
## 4 chainlink 1.00 0.437
## 5 dash 1.00 0.398
## 6 decred 1.00 0.422
## 7 dogecoin 1.00 0.535
## 8 eos 1.00 0.494
## 9 ethereum 1.00 0.407
## 10 iota 1.00 0.417
## # ... with 12 more rows
Некоторые встроенные в feasts
функции возвращают сразу несколько признаков. Например, feat_stl()
выполняет декомпозицию временного ряда на отдельные компоненты (гл. 4) и далее на их основе вычисляет различные величины, включая выраженность тренда (trend strength) и сезонной составляющей (seasonal strength), линейность (linearity) и кривизну (curvature) ряда и т.д. (Hyndman and Athanasopoulos 2019; Kang, Hyndman, and Smith-Miles 2017):
## Observations: 22
## Variables: 10
## $ coin <chr> "augur", "bitcoin", "car...
## $ trend_strength <dbl> 0.9907166, 0.9908439, 0....
## $ seasonal_strength_week <dbl> 0.14638033, 0.21990784, ...
## $ seasonal_peak_week <dbl> 6, 6, 6, 6, 6, 6, 6, 6, ...
## $ seasonal_trough_week <dbl> 2, 2, 2, 2, 1, 4, 2, 2, ...
## $ spikiness <dbl> 4.733049e-04, 6.274527e+...
## $ linearity <dbl> -3.523179e+02, -4.067290...
## $ curvature <dbl> 2.204012e+02, 5.028015e+...
## $ stl_e_acf1 <dbl> 0.40641299, 0.29884561, ...
## $ stl_e_acf10 <dbl> 0.4719425, 0.2286323, 0....
На момент написания этой книги пакет feasts
позволял рассчитать 48 признаков, характеризующих различные свойства временных рядов. Для одновременного расчета всех этих признаков можно применить вспомогательную функцию feature_set()
из пакета fabletools
(гл. 4) следующим образом:
## # A tibble: 22 x 49
## coin trend_strength seasonal_streng~ seasonal_peak_w~ seasonal_trough~
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 augur 0.991 0.146 6 2
## 2 bitc~ 0.991 0.220 6 2
## 3 card~ 0.990 0.249 6 2
## 4 chai~ 0.996 0.226 6 2
## 5 dash 0.996 0.312 6 1
## 6 decr~ 0.993 0.232 6 4
## 7 doge~ 0.983 0.303 6 2
## 8 eos 0.988 0.274 6 2
## 9 ethe~ 0.995 0.159 6 4
## 10 iota 0.995 0.228 6 2
## # ... with 12 more rows, and 44 more variables: spikiness <dbl>,
## # linearity <dbl>, curvature <dbl>, stl_e_acf1 <dbl>, stl_e_acf10 <dbl>,
## # acf1 <dbl>, acf10 <dbl>, diff1_acf1 <dbl>, diff1_acf10 <dbl>,
## # diff2_acf1 <dbl>, diff2_acf10 <dbl>, season_acf1 <dbl>, pacf5 <dbl>,
## # diff1_pacf5 <dbl>, diff2_pacf5 <dbl>, season_pacf <dbl>,
## # zero_run_mean <dbl>, nonzero_squared_cv <dbl>, zero_start_prop <dbl>,
## # zero_end_prop <dbl>, lambda_guerrero <dbl>, kpss_stat <dbl>,
## # kpss_pvalue <dbl>, pp_stat <dbl>, pp_pvalue <dbl>, ndiffs <int>,
## # nsdiffs <int>, bp_stat <dbl>, bp_pvalue <dbl>, lb_stat <dbl>,
## # lb_pvalue <dbl>, var_tiled_var <dbl>, var_tiled_mean <dbl>,
## # shift_level_max <dbl>, shift_level_index <dbl>, shift_var_max <dbl>,
## # shift_var_index <dbl>, shift_kl_max <dbl>, shift_kl_index <dbl>,
## # spectral_entropy <dbl>, n_crossing_points <int>, n_flat_spots <int>,
## # coef_hurst <dbl>, stat_arch_lm <dbl>
Функция feature_set()
имеет два аргумента: pkgs
— вектор с названиями пакетов, функции из которых вычисляют те или иные признаки, и tags
— вектор с “метками”, обозначающими группы тематически сходных признаков. В приведенной выше команде мы запросили вычисление всех признаков, реализованных в пакете feasts
(feature_set(pkgs = "feasts")
). Однако часто более удобным и гибким подходом для формирования списка признаков будет использование аргумента tags
. Например, команда feature_set(tags = c("stl", "spectral"))
приведет к вычислению всех признаков, возвращаемых функциями feat_stl()
и feat_spectral()
(см. выше). Список меток всех признаков, реализованных в пакете feasts
, можно просмотреть с помощью команды ?features_by_tag
. Этот список и краткие пояснения к нему приведены ниже (названия меток выделены жирным шрифтом; после названия каждой метки идут краткое пояснение и список функций, непосредственно выполняющих соответствующие вычисления):
acf
Набор признаков, рассчитываемых на основе коэффициентов автокорреляции.
feat_acf()
autocorrelation
Набор признаков, рассчитываемых на основе коэффициентов автокорреляции и частных коэффициентов автокорреляции.
feat_acf()
feat_pacf()
coefficients
Коэффициент Хёрста.
coef_hurst()
count
Количество раз, когда временной ряд пересекает свою собственную медиану, и максимальная длина временного отрезка, в пределах которого наблюдения имеют одинаковые значения:
n_crossing_points()
n_flat_spots()
decomposition
Признаки, рассчитываемые на основе компонент временного ряда.
feat_stl()
intermittent
Признаки, отражающие наличие и структуру временных отрезков, в пределах которых все значения ряда равны нулю.
feat_intermittent()
lumpiness
Дисперсия дисперсий, рассчитанных по наблюдениям из непересекающихся временных отрезков (окон).
var_tiled_var()
optimisation
То же, что и метка boxcox
выше, т.е. оптимальное значение параметра \(\lambda\) для преобразования Бокса–Кокса.
guerrero()
pacf
Набор признаков, рассчитываемых на основе частных коэффициентов автокорреляции.
feat_pacf()
portmanteau
Статистики, рассчитываемые в результате применения тестов Льюнг–Бокса и Бокса–Пирса на наличие автокорреляции в данных.
box_pierce()
ljung_box()
rle
Максимальная длина временного отрезка, в пределах которого значения ряда принадлежат к одному из децилей.
n_flat_spots()
roll
Признаки, рассчитываемые по данным из пересекающихся временных отрезков (окон), в частности максимальный сдвиг среднего уровня, дисперсии и расстояния Кульбака–Лейблера в следующих друг за другом отрезках.
shift_level_max()
shift_var_max()
shift_kl_max()
seasonal
Признаки, рассчитываемые на основе компонент временного ряда, а также минимальное число дифференцирований ряда, необходимое для достижения его стационарности.
feat_stl()
unitroot_nsdiffs()
slide
То же, что и метка roll
(см. выше).
spectral
Спектральная энтропия ряда.
feat_spectral()
stability
Максимальный сдвиг среднего уровня, рассчитанного по данным из двух пересекающихся временных отрезков (окон).
var_tiled_mean()
stl
Признаки, рассчитываемые на основе компонент временного ряда.
feat_stl()
test
Статистики, рассчитываемые в ходе различных тестов (на наличие автокорреляции, единичного корня, авторегрессионной условной гетероскедастичности и др.).
unitroot_kpss()
unitroot_pp()
unitroot_ndiffs()
unitroot_nsdiffs()
box_pierce()
ljung_box()
stat_arch_lm()
tile
Признаки, рассчитываемые по данным из непересекающихся временных отрезков (окон), в частности дисперсия дисперсий и дисперсия средних значений.
var_tile_mean()
var_tile_var()
trend
То же, что и метка stl
.
unitroot
Статистики, рассчитываемые в ходе тестов на наличие в данных единичного корня, а также минимальное число дифференцирований ряда, необходимое для достижения его стационарности.
unitroot_kpss()
unitroot_pp()
unitroot_ndiffs()
unitroot_nsdiffs()
Пользовательские функции тоже можно сделать “видимыми” для feature_set()
. Для этого их необходимо зарегистрировать с помощью вспомогательной функции register_feature()
:
register_feature(my_func, tags = c("avg_and_var"))
cryptos %>%
features(., y, feature_set(tags = "avg_and_var")) %>%
tibble::glimpse(width = 60)
## Observations: 22
## Variables: 3
## $ coin <chr> "augur", "bitcoin", "cardano", "chainlink"...
## $ avg <dbl> 2.315007e+01, 7.491721e+03, 1.359970e-01, ...
## $ var <dbl> 3.283892e+02, 6.722352e+06, 2.614519e-02, ...
5.3 Примеры использования извлеченных признаков
Описанные в предыдущем разделе признаки позволяют проанализировать и понять свойства сразу нескольких временных рядов. Представим, например, что перед нами стоит задача выявить временные ряды с наименьшей и наибольшей выраженностью тренда. Это можно было бы сделать следующим образом (рис. 5.1):
require(ggplot2)
require(gridExtra)
cryptos_trend <- cryptos %>%
features(., y, feature_set(tags = "stl"))
min_trend <- cryptos_trend %>%
filter(trend_strength == min(trend_strength)) %>%
dplyr::select(coin) %>%
left_join(., cryptos, by = "coin") %>%
ggplot(., aes(ds, y)) +
geom_line() + facet_grid(~coin) +
theme_minimal()
max_trend <- cryptos_trend %>%
filter(trend_strength == max(trend_strength)) %>%
dplyr::select(coin) %>%
left_join(., cryptos, by = "coin") %>%
ggplot(., aes(ds, y)) +
geom_line() + facet_grid(~coin) +
theme_minimal()
grid.arrange(min_trend, max_trend, ncol = 2)
Другой вопрос, на который было бы интересно ответить: какие ряды необычны по сочетанию нескольких признаков одновременно? Посмотрим, например, как выглядит распределение анализируемых рядов по выраженности тренда и недельной сезонности (рис. 5.2):
require(ggrepel)
cryptos_trend %>%
ggplot(., aes(trend_strength,
seasonal_strength_week, label = coin)) +
geom_point() +
geom_text_repel(force = 10, segment.color = "gray60") +
theme_minimal()
На рис. 5.2 четко выделяется временной ряд tether
, для которого характерны низкие значения обоих показателей. Мы уже видели, как выглядит этот временной ряд (см. рис. 5.1 слева). Изобразим теперь и данные по криптовалюте qtum
, которая характеризуется необычно высокой выраженностью как тренда, так и недельной сезонности (рис. 5.3):
Мы можем пойти еще дальше и проанализировать данные из таблицы cryptos
с помощью метода главных компонент (PCA) одновременно по всем признакам с ненулевой дисперсией (обратите внимание на логарифмирование исходных значений y
, выполненное для снижения дисперсии в отдельных рядах и различий между рядами):
# Функция для нахождения признаков c нулевой дисперсией:
zero_var_cols <- function(dat) {
checks <- lapply(dat, function(x) length(unique(x)))
keep <- which(!checks > 1)
unlist(keep) %>% names(.)
}
set.seed(1984)
pca <- all_crypto_features %>%
dplyr::select(-c(coin, zero_var_cols(.))) %>%
prcomp(scale = TRUE)
# summary(pca)
Согласно полученному результату (не приведен для экономии места), первые две главные компоненты объясняют примерно 56% всей дисперсии в данных. Изобразим все временные ряды в системе координат, образованной этими двумя главными компонентами (рис. 5.4):
pc <- all_crypto_features %>%
dplyr::select(coin) %>%
bind_cols(., as_tibble(pca$x))
pc %>%
ggplot(., aes(PC1, PC2, label = coin)) +
geom_point() +
geom_text_repel(force = 10, segment.color = "gray60") +
theme_minimal()
PCA–анализ по совокупности нескольких десятков признаков еще раз подтвердил аномальность временного ряда tether
, а также подчеркнул необычность ряда bitcoin
. Следует отметить, что этот анализ был выполнен в демонстрационных целях: аномальность tether
легко заметить и без применения PCA — достаточно изобразить все имеющиеся временные ряды на одном графике (см. рис. 1.2, где этот ряд выглядит практически как горизонтальная линия). Тем не менее, во многих реальных ситуациях приходится иметь дело с сотнями и даже тысячами временных рядов, простое визуальное исследование которых невозможно. Описанный выше подход в сочетании с определенным методом обнаружения аномалий помог бы выявить необычные временные ряды в автоматическом режиме.