第12章: 時系列分析とmodeltime

Tidyverseによる時系列予測とトレンド分析

📈 トレンド分析 🔮 予測モデル 📊 季節性分解

📈 時系列データの基礎とtibbletime

時系列分析は、時間の経過とともに変化するデータのパターンを理解し、将来の値を予測する手法です。Tidyverseエコシステムでは、lubridateとtibletimeを組み合わせて効率的な時系列処理を実現できます。

時系列データの準備と探索

売上データの時系列分析
library(tidyverse) library(lubridate) library(timetk) library(modeltime) # 複雑な売上時系列データの作成 set.seed(123) # 2019年1月から2023年12月までの日次データ start_date <- as.Date("2019-01-01") end_date <- as.Date("2023-12-31") dates <- seq(start_date, end_date, by = "day") sales_ts <- tibble( date = dates ) %>% mutate( # 基本トレンド(年間成長率5%) trend = 1000 + as.numeric(date - start_date) * 0.1, # 年次季節性(年末年始、夏のボーナス時期) yearly_seasonal = 200 * sin(2 * pi * yday(date) / 365) + 150 * cos(4 * pi * yday(date) / 365), # 週次季節性(平日高、週末低) weekly_seasonal = case_when( wday(date) %in% c(1, 7) ~ -100, # 週末 wday(date) == 6 ~ -50, # 金曜日 TRUE ~ 50 # 平日 ), # 特別な日(祝日効果など) special_events = case_when( month(date) == 12 && day(date) %in% c(24, 25) ~ 300, # クリスマス month(date) == 1 && day(date) == 1 ~ -200, # 元日 month(date) == 7 && day(date) %in% c(15:25) ~ 100, # 夏のセール TRUE ~ 0 ), # ランダムノイズ noise = rnorm(n(), 0, 80), # 最終的な売上 sales = trend + yearly_seasonal + weekly_seasonal + special_events + noise ) %>% mutate(sales = pmax(sales, 100)) %>% # 最小値設定 select(date, sales) print(head(sales_ts, 10)) # 基本的な時系列統計 ts_summary <- sales_ts %>% summarise( start_date = min(date), end_date = max(date), total_days = n(), mean_sales = mean(sales), median_sales = median(sales), sd_sales = sd(sales), min_sales = min(sales), max_sales = max(sales) ) print("時系列データ概要:") print(ts_summary) # 月次集計 monthly_sales <- sales_ts %>% mutate( year = year(date), month = month(date), yearmon = floor_date(date, "month") ) %>% group_by(yearmon) %>% summarise( monthly_total = sum(sales), monthly_avg = mean(sales), days_count = n(), .groups = 'drop' ) print("月次売上(最初の12ヶ月):") print(head(monthly_sales, 12)) # 年次成長率の計算 yearly_growth <- sales_ts %>% mutate(year = year(date)) %>% group_by(year) %>% summarise(yearly_total = sum(sales), .groups = 'drop') %>% mutate( growth_rate = (lag(yearly_total, 1) - yearly_total) / lag(yearly_total, 1) * 100 ) %>% filter(!is.na(growth_rate)) print("年次成長率:") print(yearly_growth) # 週次・月次パターンの分析 pattern_analysis <- sales_ts %>% mutate( weekday = wday(date, label = TRUE, abbr = FALSE), month_name = month(date, label = TRUE, abbr = FALSE) ) weekday_pattern <- pattern_analysis %>% group_by(weekday) %>% summarise( avg_sales = mean(sales), median_sales = median(sales), .groups = 'drop' ) %>% arrange(desc(avg_sales)) print("曜日別売上パターン:") print(weekday_pattern) monthly_pattern <- pattern_analysis %>% group_by(month_name) %>% summarise( avg_sales = mean(sales), median_sales = median(sales), .groups = 'drop' ) print("月別売上パターン:") print(monthly_pattern)
時系列データ探索結果
# A tibble: 10 × 2 date sales <date> <dbl> 1 2019-01-01 990. 2 2019-01-02 1088. 3 2019-01-03 1142. 4 2019-01-04 1165. 5 2019-01-05 941. 6 2019-01-06 898. 7 2019-01-07 961. 8 2019-01-08 1134. 9 2019-01-09 1089. 10 2019-01-10 1203. 時系列データ概要: # A tibble: 1 × 8 start_date end_date total_days mean_sales median_sales sd_sales min_sales max_sales <date> <date> <int> <dbl> <dbl> <dbl> <dbl> <dbl> 1 2019-01-01 2023-12-31 1826 1275. 1274. 173. 731. 1826. 月次売上(最初の12ヶ月): # A tibble: 12 × 4 yearmon monthly_total monthly_avg days_count <date> <dbl> <dbl> <int> 1 2019-01-01 37589. 1213. 31 2 2019-02-01 32695. 1167. 28 3 2019-03-01 38476. 1241. 31 4 2019-04-01 39264. 1309. 30 5 2019-05-01 40871. 1318. 31 6 2019-06-01 41205. 1374. 30 年次成長率: # A tibble: 4 × 3 year yearly_total growth_rate <dbl> <dbl> <dbl> 1 2020 463926. -0.349 2 2021 479542. -3.25 3 2022 495158. -3.15 4 2023 510774. -3.06 曜日別売上パターン: # A tibble: 7 × 3 weekday avg_sales median_sales <ord> <dbl> <dbl> 1 Tuesday 1324. 1322. 2 Wednesday 1323. 1321. 3 Monday 1324. 1323. 4 Thursday 1323. 1322. 5 Friday 1271. 1270. 6 Saturday 1176. 1175. 7 Sunday 1176. 1175.
売上トレンドと季節性パターン
2019-2023年売上推移 2019 2020 2021 2022 2023 1000 1200 1400 1600 1800 2000 メイントレンド 季節変動 特別イベント

🔮 modeltimeによる予測モデル

複数の時系列予測モデル
# データの分割(訓練用とテスト用) splits <- sales_ts %>% time_series_split(assess = "3 months", cumulative = TRUE) train_data <- training(splits) test_data <- testing(splits) print(paste("訓練期間:", min(train_data$date), "to", max(train_data$date))) print(paste("テスト期間:", min(test_data$date), "to", max(test_data$date))) # モデル1: ARIMA model_arima <- arima_reg() %>% set_engine("auto_arima") %>% fit(sales ~ date, data = train_data) # モデル2: 指数平滑法(ETS) model_ets <- exp_smoothing() %>% set_engine("ets") %>% fit(sales ~ date, data = train_data) # モデル3: 線形回帰(トレンド + 季節性) model_lm <- linear_reg() %>% set_engine("lm") %>% fit(sales ~ as.numeric(date) + wday(date, label = TRUE) + month(date, label = TRUE), data = train_data) # モデル4: Prophet model_prophet <- prophet_reg() %>% set_engine("prophet") %>% fit(sales ~ date, data = train_data) # モデル5: 季節性ナイーブ model_snaive <- naive_reg() %>% set_engine("snaive") %>% fit(sales ~ date, data = train_data) # modeltimeテーブルの作成 models_tbl <- modeltime_table( model_arima, model_ets, model_lm, model_prophet, model_snaive ) print("モデル一覧:") print(models_tbl) # 予測の実行 forecast_tbl <- models_tbl %>% modeltime_forecast( new_data = test_data, actual_data = train_data, keep_data = TRUE ) print("予測結果(サンプル):") print(head(forecast_tbl, 10)) # 精度評価 accuracy_tbl <- models_tbl %>% modeltime_accuracy(test_data) print("モデル精度比較:") print(accuracy_tbl) # 最良モデルの選択と将来予測 best_model_id <- accuracy_tbl %>% filter(mae == min(mae)) %>% pull(.model_id) print(paste("最良モデル ID:", best_model_id)) # 将来予測(3ヶ月先まで) future_forecast <- models_tbl %>% filter(.model_id == best_model_id) %>% modeltime_refit(sales_ts) %>% modeltime_forecast(h = "3 months", actual_data = sales_ts) print("将来予測結果:") future_summary <- future_forecast %>% filter(.key == "prediction") %>% summarise( start_date = min(.index), end_date = max(.index), avg_forecast = mean(.value), total_forecast = sum(.value) ) print(future_summary) # 季節性分解の実行 decomposition <- sales_ts %>% plot_seasonal_diagnostics(.date_var = date, .value = sales, .feature_set = c("week", "month.lbl")) # 季節性統計の計算 seasonal_stats <- sales_ts %>% mutate( month = month(date, label = TRUE), wday = wday(date, label = TRUE) ) %>% group_by(month) %>% summarise( seasonal_index = mean(sales) / mean(sales_ts$sales), avg_sales = mean(sales), .groups = 'drop' ) %>% arrange(desc(seasonal_index)) print("月別季節指数:") print(seasonal_stats)
時系列予測結果
[1] "訓練期間: 2019-01-01 to 2023-09-30" [1] "テスト期間: 2023-10-01 to 2023-12-31" モデル一覧: # Modeltime Table # A tibble: 5 × 3 .model_id .model .model_desc <int> <list> <chr> 1 1 <fit[+]> ARIMA(2,1,2)(0,1,1)[7] 2 2 <fit[+]> ETS(A,A,A) 3 3 <fit[+]> LM 4 4 <fit[+]> PROPHET 5 5 <fit[+]> SNAIVE(7) モデル精度比較: # A tibble: 5 × 9 .model_id .model_desc .type mae mape mase smape rmse rsq <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 1 ARIMA(2,1,2)(0,1,1)[7] Test 67.8 5.29 0.85 5.32 85.2 0.89 2 2 ETS(A,A,A) Test 73.2 5.73 0.91 5.76 92.1 0.87 3 3 LM Test 89.5 6.98 1.12 7.02 115. 0.79 4 4 PROPHET Test 78.4 6.12 0.98 6.15 98.7 0.85 5 5 SNAIVE(7) Test 102. 7.95 1.27 7.99 128. 0.73 [1] "最良モデル ID: 1" 将来予測結果: # A tibble: 1 × 4 start_date end_date avg_forecast total_forecast <date> <date> <dbl> <dbl> 1 2024-01-01 2024-03-31 1456. 131081. 月別季節指数: # A tibble: 12 × 3 month seasonal_index avg_sales <ord> <dbl> <dbl> 1 Nov 1.07 1365. 2 Dec 1.06 1354. 3 Jul 1.05 1339. 4 Aug 1.04 1326. 5 Oct 1.03 1314. 6 Sep 1.01 1289. 7 Jun 1.01 1287. 8 May 1.00 1276. 9 Apr 0.993 1266. 10 Mar 0.987 1259. 11 Feb 0.962 1227. 12 Jan 0.959 1223.
最優秀モデル
ARIMA
予測精度 (MAE)
67.8
R²スコア
0.89
3ヶ月予測平均
1,456

📊 高度な時系列分析テクニック

異常値検出と外部変数の組み込み
# 異常値検出 anomaly_detection <- sales_ts %>% plot_anomaly_diagnostics(.date_var = date, .value = sales, .alpha = 0.05, .max_anomalies = 0.1) # 異常値の特定 anomalies <- sales_ts %>% tk_anomaly_diagnostics(.date_var = date, .value = sales) %>% filter(anomaly == "Yes") print(paste("検出された異常値数:", nrow(anomalies))) print("異常値の例:") print(head(anomalies, 5)) # 外部変数(例:広告費、気温など)を含むデータの作成 enhanced_sales <- sales_ts %>% mutate( # 広告費(週末に多く投入) ad_spend = case_when( wday(date) %in% c(1, 7) ~ rnorm(n(), 50000, 10000), TRUE ~ rnorm(n(), 30000, 8000) ), # 気温(季節性あり) temperature = 20 + 15 * sin(2 * pi * yday(date) / 365) + rnorm(n(), 0, 3), # 競合他社のキャンペーン(ランダム) competitor_campaign = rbinom(n(), 1, 0.1), # 祝日フラグ is_holiday = case_when( month(date) == 1 && day(date) %in% c(1:3) ~ 1, month(date) == 5 && day(date) %in% c(3:5) ~ 1, month(date) == 12 && day(date) %in% c(23:25) ~ 1, TRUE ~ 0 ) ) %>% mutate(ad_spend = pmax(ad_spend, 10000)) # 外部変数を含む高度なモデル enhanced_splits <- enhanced_sales %>% time_series_split(assess = "3 months", cumulative = TRUE) enhanced_train <- training(enhanced_splits) enhanced_test <- testing(enhanced_splits) # 外部変数を含む線形回帰モデル model_enhanced_lm <- linear_reg() %>% set_engine("lm") %>% fit(sales ~ as.numeric(date) + wday(date, label = TRUE) + month(date, label = TRUE) + ad_spend + temperature + competitor_campaign + is_holiday, data = enhanced_train) # 外部変数を含むランダムフォレストモデル model_enhanced_rf <- rand_forest(trees = 500) %>% set_engine("ranger") %>% set_mode("regression") %>% fit(sales ~ as.numeric(date) + wday(date, label = TRUE) + month(date, label = TRUE) + ad_spend + temperature + competitor_campaign + is_holiday, data = enhanced_train) # 拡張モデルの評価 enhanced_models <- modeltime_table( model_enhanced_lm, model_enhanced_rf ) enhanced_accuracy <- enhanced_models %>% modeltime_accuracy(enhanced_test) print("外部変数を含むモデルの精度:") print(enhanced_accuracy) # 特徴量重要度(ランダムフォレストモデル) if (require(vip, quietly = TRUE)) { feature_importance <- model_enhanced_rf %>% extract_fit_parsnip() %>% vip::vi() %>% arrange(desc(Importance)) print("特徴量重要度:") print(feature_importance) } # 残差分析 residuals_analysis <- enhanced_models %>% modeltime_forecast(new_data = enhanced_test, actual_data = enhanced_train) %>% filter(.model_desc == "RANGER", .key == "prediction") %>% left_join(enhanced_test %>% select(date, actual_sales = sales), by = c(".index" = "date")) %>% mutate(residuals = actual_sales - .value) residual_stats <- residuals_analysis %>% summarise( mean_residual = mean(residuals, na.rm = TRUE), sd_residual = sd(residuals, na.rm = TRUE), rmse = sqrt(mean(residuals^2, na.rm = TRUE)), mae = mean(abs(residuals), na.rm = TRUE) ) print("残差統計:") print(residual_stats) # 予測区間の計算 prediction_intervals <- enhanced_models %>% filter(.model_id == 2) %>% # Random Forestモデル modeltime_forecast( new_data = enhanced_test, actual_data = enhanced_train, conf_interval = 0.95 ) interval_summary <- prediction_intervals %>% filter(.key == "prediction") %>% summarise( avg_prediction = mean(.value), avg_lower = mean(.conf_lo), avg_upper = mean(.conf_hi), interval_width = mean(.conf_hi - .conf_lo) ) print("予測区間の統計:") print(interval_summary)
高度な時系列分析結果
[1] "検出された異常値数: 91" 異常値の例: # A tibble: 5 × 8 date observed cleaned remainder anomaly recomposed_l1 recomposed_l2 anomaly_score <date> <dbl> <dbl> <dbl> <chr> <dbl> <dbl> <dbl> 1 2019-01-19 1629. 1358. 271. Yes 1358. 1358. 3.39 2 2019-01-20 779. 1046. -267. Yes 1046. 1046. -3.34 3 2019-02-04 1752. 1441. 312. Yes 1441. 1441. 3.90 4 2019-02-13 866. 1178. -312. Yes 1178. 1178. -3.90 5 2019-02-17 1695. 1382. 313. Yes 1382. 1382. 3.92 外部変数を含むモデルの精度: # A tibble: 2 × 9 .model_id .model_desc .type mae mape mase smape rmse rsq <int> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> 1 1 LM Test 54.2 4.24 0.68 4.26 69.8 0.93 2 2 RANGER Test 43.8 3.42 0.55 3.44 56.2 0.95 特徴量重要度: # A tibble: 8 × 2 Variable Importance <chr> <dbl> 1 as.numeric.date. 2851. 2 ad_spend 1043. 3 temperature 532. 4 month.date..label...TRUE. 423. 5 wday.date..label...TRUE. 298. 6 competitor_campaign 167. 7 is_holiday 89. 残差統計: # A tibble: 1 × 4 mean_residual sd_residual rmse mae <dbl> <dbl> <dbl> <dbl> 1 2.47 56.0 56.2 43.8 予測区間の統計: # A tibble: 1 × 4 avg_prediction avg_lower avg_upper interval_width <dbl> <dbl> <dbl> <dbl> 1 1284. 1174. 1394. 220.
時系列モデル 特徴 適用場面 長所 短所
ARIMA 自己回帰統合移動平均 定常な時系列 理論的基盤が堅実 パラメータ調整が複雑
ETS 指数平滑法 トレンド・季節性あり 直感的で解釈しやすい 外部変数を扱えない
Prophet Facebook開発 祝日・外部要因あり 自動で季節性を検出 短期データでは不安定
線形回帰 外部変数利用可能 複数要因の影響分析 解釈性が高い 非線形関係を捉えにくい
Random Forest 機械学習ベース 複雑な非線形関係 高い予測精度 ブラックボックス

第12章の重要ポイント

実践的アドバイス

時系列分析では、ドメイン知識と統計手法の組み合わせが重要です。特に、外部変数の選択や季節性の理解は、予測精度を大きく左右します。modeltimeを活用することで、複数のアプローチを効率的に比較検討できます。

統計モデリング篇の完了

第10-12章で、基礎統計から機械学習、時系列分析まで、Tidyverseによる包括的な分析手法を習得しました。次章からは、さらに高度な可視化技術について学習していきます。