Rの関数loessの平滑化曲線をスクラッチで求める

本ポストは、こちらの続きです。

Rの関数loessの出力 one.delta、two.delta、trace.hatおよびenpをスクラッチで算出
const typesetMath = (el) => { if (window.MathJax) { // MathJax Typeset window.MathJax.typeset(); } else if (window.katex...

Rの関数 stats::loess により算出される 平滑化曲線(LOESS曲線) をスクラッチで求めます。

処理の流れ

  1. 自作関数 build_hat_matrix を利用して 平滑化行列 / Smoothing Matrix(\(L\)) を求める。
  2. 観測値 \(y\) と求めた \(L\) から予測値 \(\hat{y}\)(平滑化曲線 / LOESS曲線) を算出。 \[\hat{\mathbf{y}} = L\mathbf{y}\]

平滑化行列 / Smoothing Matrix 算出のための自作関数

コードを上記ポストから引用します。

build_hat_matrix <- function(x, weights = NULL, span = 0.75, degree = 2L) {
  x <- as.matrix(x)
  N <- nrow(x)
  if (is.null(weights)) weights <- rep(1.0, N)
  # 近傍点数 nf の計算
  nf <- min(N, as.integer(floor(N * span + 1e-5)))
  L <- matrix(0.0, N, N)
  for (j in seq_len(N)) {
    q <- x[j, , drop = TRUE]
    # 二乗距離の計算
    dist2 <- rowSums(sweep(x, 2, q, "-")^2)
    # 近傍 nf 点の選択(partial sort)
    psi <- order(dist2)[seq_len(nf)]
    # バンド幅 rho = nf 番目二乗距離 × max(1, span)
    rho <- dist2[psi[nf]] * max(1.0, span)
    if (rho == 0) rho <- .Machine$double.eps
    # tricube カーネル重みの計算(ehg127 の 2 ループを忠実に再現)
    u <- sqrt(dist2[psi] / rho) # u_i = dist_i / sqrt(rho)
    w_kern <- sqrt(weights[psi] * pmax(0, 1 - u^3)^3) # sqrt(rw * tricube)
    # 重み付きデザイン行列 B = W^{1/2} X (nf × p) の構築
    dx <- x[psi, 1, drop = TRUE] - q[1]
    X_raw <- if (degree == 1L) cbind(1, dx) else cbind(1, dx, dx^2)
    B <- X_raw * w_kern # 各行 i に w_kern[i] を乗算
    # WLS による hat matrix 行 j の計算
    # B^T B = X^T W X  (W = diag(w_kern^2))
    BtB <- crossprod(B)
    BtB_inv <- tryCatch(
      solve(BtB),
      error = function(e) {
        # ランク落ち時は Moore-Penrose 疑似逆行列
        s <- svd(BtB)
        tol <- max(dim(BtB)) * .Machine$double.eps * max(s$d)
        s$v %*% diag(ifelse(s$d > tol, 1 / s$d, 0), length(s$d)) %*% t(s$u)
      }
    )
    # 評価点 j は psi の中で距離 0 の点(必ず含まれる)
    j_idx <- which(psi == j)
    xj <- matrix(X_raw[j_idx, ], nrow = 1) # 1 × p
    # L[j, psi] = xj %*% (X^T W X)^{-1} X^T W
    #           = xj %*% BtB_inv %*% t(B) * w_kern
    # t(B) * w_kern は各列 i に w_kern[i] を乗算 → X^T W に等しい
    L[j, psi] <- as.numeric(xj %*% BtB_inv %*% t(B) * w_kern)
  }
  L
}

サンプルデータの生成

seed <- 20260613
set.seed(seed)
n <- 50
x <- seq(0, 2 * pi, length.out = n)
y <- sin(x) + rnorm(n, 0, 0.2)
w <- rep(1, n)

平滑化行列 / Smoothing Matrix(hat matrix) L の構築と予測値の計算

L <- build_hat_matrix(x, weights = w, span = 0.75, degree = 2L)
y_hat <- as.numeric(L %*% y) # 予測値: ŷ = L y

cat("--- 平滑化行列 / Smoothing Matrix の一部を確認 ---\n")
str(L)
--- 平滑化行列 / Smoothing Matrix の一部を確認 ---
 num [1:50, 1:50] 0.284 0.242 0.203 0.168 0.135 ...

stats::loess による予測値の算出

fit <- loess(y ~ x,
  span = 0.75, degree = 2, weights = w,
  control = loess.control(
    surface = "direct",
    statistics = "exact",
    trace.hat = "exact"
  )
)
y_hat_ref <- fitted(fit)

自作関数build_hat_matrixと関数stats::loessの比較

library(ggplot2)

# ------------------------------------------------------------
# プロット用データフレーム
# ------------------------------------------------------------
df <- data.frame(
  x         = x,
  y_obs     = y, # 観測値
  y_scratch = y_hat, # 自作関数予測値 (L %*% y)
  y_loess   = y_hat_ref # 関数loess の予測値(参照)
)

# ------------------------------------------------------------
# ggplot によるプロット
# ------------------------------------------------------------
p <- ggplot(df, aes(x = x)) +

  # 観測値(散布図)
  geom_point(aes(y = y_obs, colour = "観測値"),
    size = 2, alpha = 0.7
  ) +

  # 自作関数予測値 L %*% y(実線)
  geom_line(aes(y = y_scratch, colour = "自作関数  (L %*% y)"),
    linewidth = 1.0
  ) +

  # 関数loess 予測値(破線)
  geom_line(aes(y = y_loess, colour = "関数loess fitted"),
    linewidth = 1.6, linetype = "dashed"
  ) +

  # 凡例・軸ラベル
  scale_colour_manual(
    name = NULL,
    values = c(
      "観測値" = "#888888",
      "自作関数  (L %*% y)" = "#2166AC",
      "関数loess fitted" = "#D6604D"
    ),
    breaks = c("観測値", "自作関数  (L %*% y)", "関数loess fitted")
  ) +
  labs(
    title    = expression(paste("loess  ", hat(bold(y)), " = ", bold(L), bold(y))),
    subtitle = "span = 0.75,  degree = 2,  n = 50",
    x        = "x",
    y        = "y"
  ) +
  theme_bw(base_size = 13) +
  theme(
    legend.position  = "bottom",
    plot.title       = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )

print(p)

cat(paste0("両関数の結果は一致しているか: ", all.equal(y_hat, y_hat_ref)))
両関数の結果は一致しているか: TRUE
Figure 1

以上です。