UPDATE: 2022-12-28 11:52:30
modeltime
パッケージを使いながら時系列データの予測を行なうノートをいくつかに分けてまとめている。modeltime
パッケージの目的は、時系列モデルの構築を効率よく行うためのパッケージで、tidymodels
パッケージと組み合わせて時系列モデルを作成する際に使用される。時系列予測のための体系的なワークフローを提供し、時系列ブーストモデルなど新しいアルゴリズムもカバーしている点が特徴的。モデルの数理的な側面や機械学習の用語などは、このノートでは扱わない。
とりあえず、modeltime
パッケージについて一通り使い方はまとめてたので、総括として、グローバルモデルを利用し、クロスバリデーション、ハイパーパラメータチューニングを行い、予測を複数の系列に対して行なう。
必要なパッケージを読み込んでおく。
library(tsibbledata)
library(tsibble)
library(timetk)
library(tidyverse)
library(tidymodels)
library(modeltime)
オーストラリアの小売の売上データセット(tsibbledata::aus_retail
)を利用する。データの各時系列は下記のキーで一意に識別される。
State
: オーストラリアの州Industry
: 業種ここではAustralian Capital Territory
の州の2つの業種restaurants and catering services
、Clothing, footwear and personal accessory retailing
に注目する。
<- aus_retail %>%
monthly_retail_tbl tk_tbl() %>%
filter(State == "Australian Capital Territory") %>%
filter(Industry %in% unique(aus_retail$Industry)[c(1,4)]) %>%
filter(Month >= as.Date("2013-01-01")) %>%
mutate(Month = as.Date(Month),
Industry = if_else(Industry == "Cafes, restaurants and catering services",
"catering", "clothing")
%>%
) mutate(Industry = as_factor(Industry)) %>%
select(date = Month, id = Industry, value = Turnover)
<- unique(monthly_retail_tbl$id)
ids ::datatable(monthly_retail_tbl) DT
2013-01-01
から2018-12-01
の72ヶ月分のデータで、2つの業種はきれいなパネルデータになっているのでこのまま利用する。
%>%
monthly_retail_tbl ::group_by(id) %>%
dplyr::summarise(
dplyrn.obs = n(),
start_date = min(date),
end_date = max(date),
min_value = min(value),
mean_value = mean(value),
max_value = max(value)
)
## # A tibble: 2 × 7
## id n.obs start_date end_date min_value mean_value max_value
## <fct> <int> <date> <date> <dbl> <dbl> <dbl>
## 1 catering 72 2013-01-01 2018-12-01 30.7 40.0 49.1
## 2 clothing 72 2013-01-01 2018-12-01 24.4 33.8 61.3
このノートの全体像は下記のイメージである。イメージなので厳密ではない。
image
tidymodels
パッケージのrecipe
では(たぶん)グループごとに処理を行なうことが現状できないので、そのような処理はレシピを作成する前に行なう。ラグやウインドウ計算などは業種ごとにグループ化(フィルタリング)して処理しておく。ここで予測期間のデータにも一部の特徴量を混ぜることができる。
<- map_dfr(
df .x = 1:length(ids),
.f = function(x){
%>%
monthly_retail_tbl ::filter(id == ids[x]) %>%
dplyrarrange(date) %>%
future_frame(date, .length_out = "12 months", .bind_data = TRUE) %>%
mutate(id = ids[x]) %>%
tk_augment_fourier(.date_var = date, .periods = 12, .K = 1) %>%
tk_augment_lags(.value = value, .lags = 12) %>%
tk_augment_slidify(.value = value_lag12,
.f = ~ mean(.x, na.rm = TRUE),
.period = c(3, 6, 9, 12),
.partial = TRUE,
.align = "center")
}
)::datatable(df) DT
予測期間は2019-01-01
から2019-12-01
までの1年とする。
<- df %>%
future_df filter(is.na(value))
%>%
future_df ::group_by(id) %>%
dplyr::summarise(
dplyrn.obs = n(),
start_date = min(date),
end_date = max(date)
)
## # A tibble: 2 × 4
## id n.obs start_date end_date
## <fct> <int> <date> <date>
## 1 catering 12 2019-01-01 2019-12-01
## 2 clothing 12 2019-01-01 2019-12-01
学習データは、2014-01-01
から2018-12-01
の60ヶ月分のデータ。予測期間にラグやウインドウ計算の特徴量を入れた際に、2013
年は欠損するので除外した。
# training
<- df %>%
preped_df filter(!is.na(value)) %>%
drop_na()
%>%
preped_df ::group_by(id) %>%
dplyr::summarise(
dplyrn.obs = n(),
start_date = min(date),
end_date = max(date),
min_value = min(value),
mean_value = mean(value),
max_value = max(value)
)
## # A tibble: 2 × 7
## id n.obs start_date end_date min_value mean_value max_value
## <fct> <int> <date> <date> <dbl> <dbl> <dbl>
## 1 catering 60 2014-01-01 2018-12-01 30.7 39.9 49
## 2 clothing 60 2014-01-01 2018-12-01 24.9 34.5 61.3
学習データを可視化しておく。catering
は緩やかに上昇し、そこからは停滞しており、takeaway
では特定期間に突発的なスパイクが存在している。
plot_time_series(
.data = preped_df,
.date_var = date,
.value = value,
.color_var = id,
.smooth = TRUE,
.interactive = FALSE) +
scale_x_date(limits = c(min(preped_df$date), max((preped_df$date))),
labels = date_format("%Y/%m"),
breaks = date_breaks("3 month")) +
theme(axis.text.x = element_text(angle = 45, vjust = 0.2, hjust=0.2))
学習データを分割する。ここではcatering
を対象に学習データとテストデータを可視化しているが、takeaway
でも同様である。
<- time_series_split(
splits data = preped_df,
assess = 12,
cumulative = TRUE
)
%>%
splits tk_time_series_cv_plan() %>%
filter(id == "catering") %>%
plot_time_series_cv_plan(date, value, .interactive = FALSE) +
scale_x_date(limits = c(min(preped_df$date), max((preped_df$date))),
labels = date_format("%Y/%m"),
breaks = date_breaks("3 month")) +
theme(axis.text.x = element_text(angle = 45, vjust = 0.2, hjust=0.2))
モデルのパラメタを調整するために、学習データを学習データと評価データに分割する。ここでは4スライス分の時系列クロスバリデーションデータを作成する。
<- training(splits) %>%
resamples_kfold time_series_cv(
date_var = date,
assess = "12 months",
cumulative = TRUE,
skip = "3 months",
slice_limit = 4
)
resamples_kfold
## # Time Series Cross Validation Plan
## # A tibble: 4 × 2
## splits id
## <list> <chr>
## 1 <split [72/24]> Slice1
## 2 <split [66/24]> Slice2
## 3 <split [60/24]> Slice3
## 4 <split [54/24]> Slice4
時系列クロスバリデーションデータを可視化しておく。
%>%
resamples_kfold tk_time_series_cv_plan() %>%
filter(id == "catering") %>%
plot_time_series_cv_plan(date, value, .interactive = FALSE) +
scale_x_date(limits = c(min(preped_df$date), max(assessment(resamples_kfold$splits[[1]])$date)),
labels = date_format("%Y/%m"),
breaks = date_breaks("3 month")) +
theme(axis.text.x = element_text(angle = 45, vjust = 0.2, hjust=0.2))
時系列クロスバリデーションデータは2014-01-01
を起点に、学習データと評価データを調整している。
%>%
resamples_kfold mutate(
analysis = map(.x = splits, .f = function(x){analysis(x)}),
assessment = map(.x = splits, .f = function(x){assessment(x)}),
analysis_min_date = map(.x = analysis, .f = function(x){min(x$date)}),
analysis_max_date = map(.x = analysis, .f = function(x){max(x$date)}),
assessment_min_date = map(.x = assessment, .f = function(x){min(x$date)}),
assessment_max_date = map(.x = assessment, .f = function(x){max(x$date)}),
%>%
) unnest(c(analysis_min_date, analysis_max_date, assessment_min_date, assessment_max_date)) %>%
select(id, analysis_min_date, analysis_max_date, assessment_min_date, assessment_max_date)
## # A tibble: 4 × 5
## id analysis_min_date analysis_max_date assessment_min_date assessment_ma…¹
## <chr> <date> <date> <date> <date>
## 1 Slice1 2014-01-01 2016-12-01 2017-01-01 2017-12-01
## 2 Slice2 2014-01-01 2016-09-01 2016-10-01 2017-09-01
## 3 Slice3 2014-01-01 2016-06-01 2016-07-01 2017-06-01
## 4 Slice4 2014-01-01 2016-03-01 2016-04-01 2017-03-01
## # … with abbreviated variable name ¹assessment_max_date
最初に行った特徴量作成に加え、その他のカレンダー系特徴量を追加する。また、目的変数は対数変換を行なう。
<- recipe(value ~ ., data = training(splits)) %>%
recipe_prophet_boost step_timeseries_signature(date) %>%
step_rm(matches("(.iso$)|(.xts$)|(week)|(day)|(hour)|(minute)|(second)|(am.pm)|(.num$)|(.lbl.*$)")) %>%
step_mutate(date_month = factor(date_month, ordered = TRUE)) %>%
step_dummy(all_nominal(), one_hot = TRUE) %>%
step_log(value)
%>% prep() %>% bake(training(splits)) %>% DT::datatable() recipe_prophet_boost
次はモデルの設定。今回はBoost
Prophetを利用する。mtry
とsample_size
は上限を決める必要があるので、ここはグリッド作成の際に対処する。おそらく、ここでの設定方法が誤っていたため、これまでのノートではBoost
Prophetのフィッティングの際に、ワーニングが発生していたと思われる。
<- prophet_boost(
model_prophet_boost # prophet
mode = "regression",
growth = "linear",
changepoint_num = tune(),
changepoint_range = tune(),
seasonality_yearly = TRUE,
seasonality_weekly = FALSE, # Monthlyデータなので機能しない
seasonality_daily = FALSE, # Monthlyデータなので機能しない
season = "additive",
prior_scale_changepoints = tune(),
prior_scale_seasonality = tune(),
# prior_scale_holidays = NULL,
# xgboost
mtry = tune(),
trees = 1000,
tree_depth = tune(),
learn_rate = tune(),
loss_reduction = tune(),
sample_size = tune(),
stop_iter = tune(),
min_n = tune()
%>%
) set_engine("prophet_xgboost")
model_prophet_boost
## PROPHET Regression Model Specification (regression)
##
## Main Arguments:
## growth = linear
## changepoint_num = tune()
## changepoint_range = tune()
## seasonality_yearly = TRUE
## seasonality_weekly = FALSE
## seasonality_daily = FALSE
## season = additive
## prior_scale_changepoints = tune()
## prior_scale_seasonality = tune()
## mtry = tune()
## trees = 1000
## min_n = tune()
## tree_depth = tune()
## learn_rate = tune()
## loss_reduction = tune()
## sample_size = tune()
## stop_iter = tune()
##
## Computational engine: prophet_xgboost
作成したモデルとレシピをワークフローに登録し、これを利用してハイパーパラメータのチューニングを行なう。
<- workflow() %>%
workflow_prophet_boost add_model(model_prophet_boost) %>%
add_recipe(recipe_prophet_boost)
workflow_prophet_boost
## ══ Workflow ════════════════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: prophet_boost()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 5 Recipe Steps
##
## • step_timeseries_signature()
## • step_rm()
## • step_mutate()
## • step_dummy()
## • step_log()
##
## ── Model ───────────────────────────────────────────────────────────────────────
## PROPHET Regression Model Specification (regression)
##
## Main Arguments:
## growth = linear
## changepoint_num = tune()
## changepoint_range = tune()
## seasonality_yearly = TRUE
## seasonality_weekly = FALSE
## seasonality_daily = FALSE
## season = additive
## prior_scale_changepoints = tune()
## prior_scale_seasonality = tune()
## mtry = tune()
## trees = 1000
## min_n = tune()
## tree_depth = tune()
## learn_rate = tune()
## loss_reduction = tune()
## sample_size = tune()
## stop_iter = tune()
##
## Computational engine: prophet_xgboost
ここではラテンハイパーキューブを使って、グリッドを20個作成する。
set.seed(1989)
<- grid_latin_hypercube(
grid extract_parameter_set_dials(model_prophet_boost) %>%
update(mtry = mtry(range = c(1, 20)),
sample_size = sample_prop(c(0.4, 0.8))),
size = 20
)
%>% DT::datatable() grid
作成したグリッドを利用してチューニングを行なう。本当はここで並列化の設定をしたほうが良いかもしれないが、そこらへんは雰囲気でしか理解してないので、また後日、学習した際にノートにまとめる。
<- workflow_prophet_boost %>%
tune_workflow_prophet_boost tune_grid(
resamples = resamples_kfold,
grid = grid,
control = control_grid(verbose = TRUE,
save_pred = TRUE),
metrics = metric_set(rmse, mae)
)
tune_workflow_prophet_boost
## # Tuning results
## # NA
## # A tibble: 4 × 5
## splits id .metrics .notes .predictions
## <list> <chr> <list> <list> <list>
## 1 <split [72/24]> Slice1 <tibble [40 × 15]> <tibble [0 × 3]> <tibble [480 × 15]>
## 2 <split [66/24]> Slice2 <tibble [40 × 15]> <tibble [0 × 3]> <tibble [480 × 15]>
## 3 <split [60/24]> Slice3 <tibble [40 × 15]> <tibble [0 × 3]> <tibble [480 × 15]>
## 4 <split [54/24]> Slice4 <tibble [40 × 15]> <tibble [0 × 3]> <tibble [480 × 15]>
mae
基準で最小のパラメタの組み合わせを10個取り出す。
%>%
tune_workflow_prophet_boost show_best("mae", n = 10) %>%
::datatable() DT
パラメタと評価指標の関係を可視化する。この際に、評価指標が下がる傾向があれば、パラメタグリッドを再作成するなどして再度チューニングする。
%>%
tune_workflow_prophet_boost autoplot() +
geom_smooth(se = FALSE)
mae
基準で最小のパラメタ組み合わせは下記の通り。
<- tune_workflow_prophet_boost %>%
tuned_best_params select_best("mae")
%>% t() tuned_best_params
## [,1]
## changepoint_num "24"
## changepoint_range "0.6598805"
## prior_scale_changepoints "0.03299381"
## prior_scale_seasonality "17.86857"
## mtry "17"
## min_n "8"
## tree_depth "11"
## learn_rate "0.02808174"
## loss_reduction "0.0002741032"
## sample_size "0.5998895"
## stop_iter "10"
## .config "Preprocessor1_Model09"
一旦、これをベストなパラメタとして、ワークフローのパラメタをファイナライズする。
<- workflow_prophet_boost %>%
finalize_workflow_prophet_boost finalize_workflow(parameters = tuned_best_params)
$fit$actions$model$spec finalize_workflow_prophet_boost
## PROPHET Regression Model Specification (regression)
##
## Main Arguments:
## growth = linear
## changepoint_num = 24
## changepoint_range = 0.659880460824352
## seasonality_yearly = TRUE
## seasonality_weekly = FALSE
## seasonality_daily = FALSE
## season = additive
## prior_scale_changepoints = 0.0329938065497755
## prior_scale_seasonality = 17.8685654750194
## mtry = 17
## trees = 1000
## min_n = 8
## tree_depth = 11
## learn_rate = 0.0280817387862989
## loss_reduction = 0.000274103153914346
## sample_size = 0.599889543387108
## stop_iter = 10
##
## Computational engine: prophet_xgboost
モデルテーブルに登録するために、とりあえずフィッティングしておく。
# ここのデータはなんでもよい
<- finalize_workflow_prophet_boost %>%
fit_finalize_workflow_prophet_boost fit(training(resamples_kfold$splits[[1]]))
fit_finalize_workflow_prophet_boost
## ══ Workflow [trained] ══════════════════════════════════════════════════════════
## Preprocessor: Recipe
## Model: prophet_boost()
##
## ── Preprocessor ────────────────────────────────────────────────────────────────
## 5 Recipe Steps
##
## • step_timeseries_signature()
## • step_rm()
## • step_mutate()
## • step_dummy()
## • step_log()
##
## ── Model ───────────────────────────────────────────────────────────────────────
## PROPHET w/ XGBoost Errors
## ---
## Model 1: PROPHET
## - growth: 'linear'
## - n.changepoints: 24
## - changepoint.range: 0.659880460824352
## - yearly.seasonality: 'TRUE'
## - weekly.seasonality: 'FALSE'
## - daily.seasonality: 'FALSE'
## - seasonality.mode: 'additive'
## - changepoint.prior.scale: 0.0329938065497755
## - seasonality.prior.scale: 17.8685654750194
## - holidays.prior.scale: 10
## - logistic_cap: NULL
## - logistic_floor: NULL
##
## ---
## Model 2: XGBoost Errors
##
## xgboost::xgb.train(params = list(eta = 0.0280817387862989, max_depth = 11L,
## gamma = 0.000274103153914346, colsample_bytree = 1, colsample_bynode = 0.708333333333333,
## min_child_weight = 8L, subsample = 0.599889543387108), data = x$data,
## nrounds = 1000, watchlist = x$watchlist, verbose = 0, early_stopping_rounds = 10L,
## objective = "reg:squarederror", nthread = 1)
これをモデルテーブルに登録してキャリブレーションする。
<- modeltime_table(fit_finalize_workflow_prophet_boost) %>%
calibrattion_prophet_boost modeltime_calibrate(testing(splits), id = "id") # confを計算するために必要
%>%
calibrattion_prophet_boost pluck(".calibration_data", 1) %>%
::datatable() DT
これでテストデータの精度を確認できるので、テストデータでグローバルモデルの精度を確認する。
# Global
%>%
calibrattion_prophet_boost modeltime_accuracy(acc_by_id = FALSE) %>%
table_modeltime_accuracy()
こちらはローカルモデルの精度。
# Local
%>%
calibrattion_prophet_boost modeltime_accuracy(acc_by_id = TRUE) %>%
table_modeltime_accuracy()
テストデータでの予測値はこの通り。
%>%
calibrattion_prophet_boost modeltime_forecast(
new_data = testing(splits),
actual_data = preped_df,
conf_by_id = TRUE,
keep_data = TRUE
%>%
) ::datatable() DT
可視化すると、そこまで精度が良くないように思えるが、今回はパッケージの使い方のまとめなので、これで良いとする。
%>%
calibrattion_prophet_boost modeltime_forecast(
new_data = testing(splits),
actual_data = preped_df,
conf_by_id = TRUE,
keep_data = TRUE
%>%
) group_by(id) %>%
plot_modeltime_forecast(
.interactive = FALSE,
.legend_show = FALSE,
.facet_ncol = 3
+
) scale_x_date(limits = c(min(preped_df$date), max(preped_df$date)),
labels = date_format("%Y/%m"),
breaks = date_breaks("3 month")) +
theme(axis.text.x = element_text(angle = 45, vjust = 0.2, hjust=0.2))
あとはモデルをフルデータで再学習して、予測期間の予測値を計算する。
<- calibrattion_prophet_boost %>%
refit_prophet_boost modeltime_refit(data = preped_df)
refit_prophet_boost
## # Modeltime Table
## # A tibble: 1 × 5
## .model_id .model .model_desc .type .calibration_data
## <int> <list> <chr> <chr> <list>
## 1 1 <workflow> PROPHET W/ XGBOOST ERRORS Test <tibble [24 × 5]>
おさらいしておくと、予測期間は2019-01-01
から2019-12-01
までの1年間である。
bind_rows(
training(splits) %>% group_by(id) %>% summarise(start_date = min(date), end_date = max(date), n.obs = n()) %>% mutate(label = "train"),
testing(splits) %>% group_by(id) %>% summarise(start_date = min(date), end_date = max(date), n.obs = n()) %>% mutate(label = "test"),
%>% group_by(id) %>% summarise(start_date = min(date), end_date = max(date), n.obs = n()) %>% mutate(label = "future")
future_df %>%
) select(id, label, everything()) %>%
arrange(id, start_date)
## # A tibble: 6 × 5
## id label start_date end_date n.obs
## <fct> <chr> <date> <date> <int>
## 1 catering train 2014-01-01 2017-12-01 48
## 2 catering test 2018-01-01 2018-12-01 12
## 3 catering future 2019-01-01 2019-12-01 12
## 4 clothing train 2014-01-01 2017-12-01 48
## 5 clothing test 2018-01-01 2018-12-01 12
## 6 clothing future 2019-01-01 2019-12-01 12
再学習したモデルで予測値を計算する。
%>%
refit_prophet_boost modeltime_forecast(
new_data = future_df,
actual_data = preped_df,
conf_by_id = TRUE
%>%
) ::datatable() DT
計算した予測値を使って、データ全体を可視化する。
%>%
refit_prophet_boost modeltime_forecast(
new_data = future_df,
actual_data = preped_df,
conf_by_id = TRUE
%>%
) # recipeでlog変換したものを戻す
mutate(
.value = exp(.value),
.conf_lo = exp(.conf_lo),
.conf_hi = exp(.conf_hi)
%>%
) group_by(id) %>%
plot_modeltime_forecast(
.title = "Turnover 1-year forecast",
.interactive = FALSE,
.facet_ncol = 2
+
) scale_x_date(limits = c(min(preped_df$date), max(future_df$date)),
labels = date_format("%Y/%m"),
breaks = date_breaks("3 month")) +
theme(axis.text.x = element_text(angle = 45, vjust = 0.2, hjust=0.2))