本ポストはこちらの続きです。よって、seed値は同じく 20250517 とします。

Rによる時系列データの Box-Cox変換 を試みます。
改めてサンプルデータを作成します。
<- 20250517
seed set.seed(seed)
<- 200
n <- 1:n
t_idx <- 50 + 0.5 * t_idx
trend <- 0.15
noise_sd_ratio <- rnorm(n, mean = 0, sd = trend * noise_sd_ratio)
noise <- trend + noise
x_original_values # Box-Cox変換のため、値が確実に正であることを保証させる
<= 0] <- 1e-6
x_original_values[x_original_values <- ts(x_original_values, frequency = 1) # 季節性なしのデータとして扱う x_ts
関数 forecast::BoxCox.lambda を利用して最適なラムダ(λ)を求めます。
<- forecast::BoxCox.lambda(x_ts, method = "loglik")
optimal_lambda_loglik cat(paste("Optimal lambda (loglik method):", round(optimal_lambda_loglik, 4), "\n"))
Optimal lambda (loglik method): 0.45
Box-Cox変換 のための関数を作成します。
<- function(y, lambda) {
boxcox_transform <- attributes(y) # tsオブジェクトの属性を保持
original_attributes <- as.numeric(y) # 計算のために数値ベクトルに変換
y_numeric
# Box-Cox変換の計算
if (abs(lambda) < 1e-9) { # lambdaがほぼ0の場合、対数変換
<- log(y_numeric)
transformed_y else { # lambdaが0でない場合
} <- (y_numeric^lambda - 1) / lambda
transformed_y
}
# 元のオブジェクトがtsなら、tsオブジェクトとして返す
if (is.ts(y)) {
# tsオブジェクトの属性(開始時期、周期など)を復元
<- ts(transformed_y,
transformed_y start = original_attributes$tsp[1],
frequency = original_attributes$tsp[3]
)
}return(transformed_y)
}
作成した関数を利用してサンプル時系列データを Box-Cox変換 します。
<- boxcox_transform(x_ts, optimal_lambda_loglik)
x_ts_transformed x_ts_transformed
Time Series:
Start = 1
End = 200
Frequency = 1
[1] 12.303560 10.926115 9.895295 11.212919 11.181870 12.216017 11.197002
[8] 11.522548 9.791767 8.765698 10.754899 10.582257 12.581710 11.013034
[15] 11.306413 12.897183 11.662039 10.142130 13.192650 13.823372 13.262558
[22] 13.627527 10.941004 13.988421 12.158101 12.917781 11.529698 13.099224
[29] 11.761910 13.067547 11.758352 12.437319 12.191684 9.995317 12.566340
[36] 12.973881 13.131552 12.522335 12.217904 12.682857 11.244514 12.346608
[43] 11.410475 12.336399 12.397760 14.005780 14.667806 14.221040 13.581120
[50] 12.323514 12.880274 11.658165 14.131319 13.198142 14.695949 12.607398
[57] 14.387814 14.643078 12.350418 14.021409 14.288380 13.001766 13.529733
[64] 14.977056 12.662699 14.296725 14.844133 13.416750 13.966334 14.419613
[71] 11.526020 12.713276 15.015883 15.381004 14.076720 15.059598 13.078936
[78] 13.742992 14.425897 14.894895 15.390410 13.566456 13.822698 15.662622
[85] 15.610601 13.247973 15.741638 15.145323 14.714575 13.559551 17.387243
[92] 11.967950 15.912261 15.268174 12.837342 15.705460 15.179007 11.597971
[99] 16.316404 15.614148 14.474377 15.414680 14.488281 16.835206 13.973556
[106] 15.465861 14.414259 17.050159 16.314383 15.462131 17.304628 15.402270
[113] 16.805953 16.112487 15.655637 17.283437 17.370022 16.310301 13.999779
[120] 14.778950 17.823199 15.346389 16.536468 15.947433 16.327611 15.625809
[127] 15.420474 15.170425 14.084622 15.534046 14.515386 17.176438 16.272183
[134] 16.005614 16.895622 14.541354 17.782362 17.948177 17.694553 19.000831
[141] 17.864323 16.295407 18.889447 14.169382 13.178991 15.869686 16.833604
[148] 17.835879 15.245122 15.529272 16.752181 17.959172 17.244867 19.088067
[155] 16.209960 16.481083 18.736243 20.842727 15.416968 16.806621 15.372214
[162] 18.262131 17.449263 17.989684 17.418979 17.370260 18.943023 14.190743
[169] 15.506217 18.062476 17.928363 17.942794 18.644096 18.570261 17.939075
[176] 16.247411 17.495822 18.345460 19.854506 19.164663 17.903038 19.684803
[183] 20.035654 18.004475 17.612414 18.847308 19.072240 18.648185 16.207550
[190] 19.855366 16.770800 19.244883 18.992126 18.892618 18.911704 18.619563
[197] 19.970070 19.078027 18.625075 19.767949
なお、Rでは forecast::BoxCox により Box-Cox変換 が可能です。
unique(x_ts_transformed - forecast::BoxCox(x_ts, lambda = optimal_lambda_loglik))
[1] 0
変換前と変換後のサンプル時系列データを比較します。
library(ggplot2)
library(zoo)
library(patchwork)
<- data.frame(
df_comparison time = time(x_ts),
original = coredata(x_ts),
transformed = coredata(x_ts_transformed)
)
<- ggplot(df_comparison, aes(x = time, y = original)) +
p_original_ts geom_line(color = "blue") +
ggtitle("Original Time Series") +
ylab("Value") +
xlab("Time") +
theme_bw()
<- ggplot(df_comparison, aes(x = time, y = transformed)) +
p_transformed_ts geom_line(color = "red") +
ggtitle(paste("Box-Cox Transformed Time Series (lambda =", round(optimal_lambda_loglik, 3), ")")) +
ylab("Transformed Value") +
xlab("Time") +
theme_bw()
+ p_transformed_ts + plot_layout(ncol = 1) p_original_ts
変換の前後で分散が変化していること(安定していること)を確認します。
library(dplyr)
<- function(series, window_size) {
calculate_rolling_sd <- length(series)
n_series <- rep(NA, n_series)
rolling_sd if (n_series < window_size) {
return(rolling_sd)
}for (i in window_size:n_series) {
<- sd(series[(i - window_size + 1):i], na.rm = TRUE)
rolling_sd[i]
}return(rolling_sd)
}
<- 20 # ローリングウィンドウのサイズ (データ長の10%程度)
window_size $rolling_sd_original <- calculate_rolling_sd(df_comparison$original, window_size)
df_comparison$rolling_sd_transformed <- calculate_rolling_sd(df_comparison$transformed, window_size)
df_comparison
<- ggplot(
p_rolling_sd %>% filter(!is.na(rolling_sd_original)),
df_comparison aes(x = time)
+
) geom_line(aes(y = rolling_sd_original, color = "Original"), linewidth = 1) +
geom_line(aes(y = rolling_sd_transformed, color = "Transformed"), linewidth = 1) +
scale_color_manual(values = c("Original" = "blue", "Transformed" = "red")) +
ggtitle(paste("Rolling Standard Deviation (Window =", window_size, ")")) +
ylab("Rolling SD") +
xlab("Time") +
labs(color = "Series") +
theme_bw() +
theme(legend.position = "top")
print(p_rolling_sd)
一定期間(ウィンドウ)ごとのデータの標準偏差(ばらつき具合)の推移を確認しますと、青線(Original)は経時に伴い分散が変化していますが、赤線(Transformed)は 青線(Original)と比較しますと低い水準で安定しており、変換によって分散が安定化したことを確認できます。
続いて、ヒストグラムによる分布の変化を確認します。
<- ggplot(df_comparison, aes(x = original)) +
p_hist_original geom_histogram(aes(y = ..density..), bins = 20, fill = "blue", alpha = 0.7, color = "white") +
geom_density(color = "darkblue", linewidth = 1) +
ggtitle("Histogram of Original Data") +
xlab("Value") +
ylab("Density") +
theme_bw()
<- ggplot(df_comparison, aes(x = transformed)) +
p_hist_transformed geom_histogram(aes(y = ..density..), bins = 20, fill = "red", alpha = 0.7, color = "white") +
geom_density(color = "darkred", linewidth = 1) +
ggtitle(paste("Histogram of Transformed Data (lambda =", round(optimal_lambda_loglik, 3), ")")) +
xlab("Transformed Value") +
ylab("Density") +
theme_bw()
<- p_hist_original | p_hist_transformed
plot_comparison_hist print(plot_comparison_hist)
左のヒストグラム(Original)は、右に裾の長い(歪んだ)分布を示していますが、右のヒストグラム(Transformed)では、変換により分布がより対称的になっていることを確認できます。
最後に両者の基本統計量を確認します。
<- summary(df_comparison$original)
summary_original <- summary(df_comparison$transformed)
summary_transformed
cat("--- 要約統計量の比較 ---\n")
cat("Original Data Summary:\n")
print(summary_original)
cat("\nTransformed Data Summary:\n")
print(summary_transformed)
cat(paste("SD Original:", round(sd(df_comparison$original), 2), " SD Transformed:", round(sd(df_comparison$transformed), 2), "\n"))
cat(paste("IQR Original:", round(IQR(df_comparison$original), 2), " IQR Transformed:", round(IQR(df_comparison$transformed), 2), "\n"))
--- 要約統計量の比較 ---
Original Data Summary:
Min. 1st Qu. Median Mean 3rd Qu. Max.
34.87 72.76 96.61 98.63 124.44 181.19
Transformed Data Summary:
Min. 1st Qu. Median Mean 3rd Qu. Max.
8.766 13.076 15.158 15.089 17.255 20.843
SD Original: 31.98 SD Transformed: 2.58
IQR Original: 51.68 IQR Transformed: 4.18
特に標準偏差 (sd) や四分位範囲 (IQR = Q3 – Q1) に注目すると、変換後のデータの方が値のスケールが小さくなり、ばらつきが 抑えられていることが確認できます。
ただし、スケールが異なっているのですから(意図的に変換しているのですから)、単純なSDの比較は注意が必要 です。
時系列データの Box-Cox変換 の主目的は「時間を通じたばらつきの均一化」です。
以上です。