対数尤度 を利用して Box-Cox変換 における 最適ラムダ(λ) の算出を R で試みます。
始めにサンプルデータを作成します。なお、サンプルデータは意図的に「分散が非定常」かつ「季節性(周期性)の無い」時系列データとします。
library(ggplot2)
<- 20250517
seed set.seed(seed)
<- 200 # データ点数
n <- 1:n # 時間インデックス
t
# 線形トレンドを作成 (時間とともに増加)
<- 50 + 0.5 * t
trend
# トレンドの値に比例する標準偏差を持つ正規ノイズを生成
<- 0.15 # トレンドに対するノイズ標準偏差の比率
noise_sd_ratio # 標準偏差が trend * noise_sd_ratio となるノイズ
<- rnorm(n, mean = 0, sd = trend * noise_sd_ratio)
noise
# 時系列データを作成 (トレンド + ノイズ)
<- trend + noise
x
# Box-Cox変換の前提である「データがすべて正の値である」が満たされない場合は
# x <- pmax(x, 1e-6) によって非常に小さい正の値に置き換える
if (any(x <= 0)) {
<- pmax(x, 1e-6)
x
}
# tsオブジェクト (時系列オブジェクト) に変換
<- ts(x, frequency = 1) # 今回のサンプルデータには周期性がないため frequency = 1
x_ts
# 生成したデータの確認
<- forecast::autoplot(x_ts) +
p_original ggtitle("Original Time Series (Non-constant Variance)") +
ylab("Value") +
xlab("Time") +
theme_minimal()
print(p_original)
最適なラムダを選択する関数 boxcox_loglik_lambda_calculator を作成します。
なお、同関数は forecast::BoxCox.lambda を引用参照しています。
関数の処理の流れは以下の通りです。
- データ前処理
- 計画行列の作成
- ラムダ候補の評価
- 最適なラムダの選択
与えられたデータ x に対して、指定された範囲 lower から upper まで0.05刻みでラムダ候補を評価し、対数尤度に比例する値を最大化するラムダを探索します。
<- function(x, lower = -1, upper = 2) {
boxcox_loglik_lambda_calculator # 元のxの属性を保持
<- inherits(x, "ts")
x_orig_is_ts <- if (x_orig_is_ts) stats::frequency(x) else 1
original_freq
# --- 1. データ前処理 ---
<- stats::na.omit(c(x)) # ベクトルとして扱いNA除去
x_clean <- length(x_clean)
n <- log(x_clean)
logx_clean <- exp(mean(logx_clean)) # x_clean の幾何平均
xdot
# --- 2. 計画行列の作成 ---
# 計画行列 X_matrix は元の x の特性(時系列かどうか、周期)に依存し、
# 行数は n (x_clean の長さ) となります。
<- NULL
X_matrix
if (x_orig_is_ts) {
if (original_freq > 1) {
# トレンドと季節性を持つ時系列データ
<- 1:n
trend_vec # x_clean の長さに合わせて季節性ファクターを作成
<- stats::ts(seq_len(n), frequency = original_freq)
temp_ts_for_season <- factor(stats::cycle(temp_ts_for_season))
season_fac
# 季節性ファクターのレベルが複数あり、かつデータが十分ある場合のみ季節性項を考慮
if (length(levels(season_fac)) > 1 && n >= original_freq) {
<- data.frame(trend_vec = trend_vec, season_fac = season_fac)
df_for_matrix <- stats::model.matrix(~ trend_vec + season_fac, data = df_for_matrix)
X_matrix else {
} # 季節性を考慮できない場合はトレンドのみ
<- data.frame(trend_vec = trend_vec)
df_for_matrix <- stats::model.matrix(~trend_vec, data = df_for_matrix)
X_matrix
}else {
} # トレンドのみの時系列データ (周期 <= 1)
<- 1:n
trend_vec <- data.frame(trend_vec = trend_vec)
df_for_matrix <- stats::model.matrix(~trend_vec, data = df_for_matrix)
X_matrix
}else {
} # 非時系列データ: 切片のみのモデル
<- data.frame(dummy_for_intercept = rep(1, n))
df_for_matrix <- stats::model.matrix(~1, data = df_for_matrix)
X_matrix
}
# --- 3. ラムダ候補の評価 ---
<- seq(lower, upper, by = 0.05)
lambdas <- numeric(length(lambdas))
loglik_values
for (i in seq_along(lambdas)) {
<- lambdas[i]
la <- numeric(n)
xt_transformed
# Box-Cox変換の適用
if (abs(la) > 0.02) { # 標準的なBox-Cox変換
<- (x_clean^la - 1) / la
xt_transformed else { # laが0に近い場合 (対数変換)、テイラー展開で近似
} <- logx_clean * (1 + (la * logx_clean) / 2 *
xt_transformed 1 + (la * logx_clean) / 3 *
(1 + (la * logx_clean) / 4)))
(
}
# 変換後のデータをスケーリング
<- xdot^(la - 1)
scale_factor <- xt_transformed / scale_factor
z_lambda
# z_lambda にモデルをフィットさせ、残差平方和 (RSS) を計算
<- stats::lm.fit(X_matrix, z_lambda)
current_fit <- sum(current_fit$residuals^2)
rss <- -n / 2 * log(rss)
loglik_values[i]
}
# --- 4. 最適なラムダの選択 ---
<- which.max(loglik_values)
best_idx return(lambdas[best_idx])
}
それでは、サンプルデータに対して最適なラムダを求めます。
boxcox_loglik_lambda_calculator(x = x_ts, lower = -1, upper = 2)
[1] 0.45
関数 BoxCox.lambda {forecast} による結果と比較します。
::BoxCox.lambda(x = x_ts, method = "loglik", lower = -1, upper = 2) forecast
[1] 0.45
一致しています。
以上です。