Rの関数loessの出力 one.delta、two.delta、trace.hatおよびenpをスクラッチで算出

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

AICを利用してloess{stats}の最適spanを選択
const typesetMath = (el) => { if (window.MathJax) { // MathJax Typeset window.MathJax.typeset(); } else if (window.katex...

LOESS における Smoothing Matrix(L) および、Rの関数 stats::loessを利用して得られる4つの統計量 trace.hatone.deltatwo.deltaenp をスクラッチで求めます。

5つの統計量の関係

  • \(L\)

    • \(\hat{\mathbf{y}} = L\mathbf{y}\)
    • hat matrix:観測値から予測値への線形写像
    • Smoothing Matrix / 平滑化行列
  • \(\mathrm{tr}(L)\)

    • \(\sum_j L_{jj}\)
    • 有効パラメータ数(当てはめ側の自由度)
    • trace.hat
  • \(\delta_1\)

    • \(\mathrm{tr}\!\left[(I-L)(I-L)^\top\right]\)
    • 残差の有効自由度(分散推定の分母)
  • \(\delta_2\)

    • \(\mathrm{tr}\!\left[((I-L)(I-L)^\top)^2\right]\)
    • 自由度の補正係数(信頼区間の精度向上)
  • \(\text{enp}\)

    • \(\delta_1 + 2\,\mathrm{tr}(L) - N\)
    • 等価パラメータ数(モデルの複雑さの総合指標)
    • Equivalent Number of Parameters

δ1、δ2、trace hat、enp および L の算出アルゴリズム

コードは build_hat_matrix()lowesc_R()、そして enpの計算 の 3 段階で構成されています。

アルゴリズム全体の流れは以下の通りです。

入力: x(説明変数), y(応答変数), weights, span, degree
  │
  ├─ [build_hat_matrix]  ─────────────────────────────────
  │   j = 1, 2, ..., N のループ
  │     1. 全点との二乗距離 dist2 を計算
  │     2. 近傍 nf 点のインデックス psi を選択
  │     3. バンド幅 rho = dist2[psi[nf]]
  │     4. tricube 重み w_kern を計算(sqrt 付き)
  │     5. 重み付きデザイン行列 B を構築
  │     6. WLS: L[j, psi] = xj (B'B)^{-1} B' * w_kern
  │   → N×N の hat matrix L
  │
  ├─ [lowesc_R]  ─────────────────────────────────────────
  │     7. LL = (L - I)(L - I)'
  │     8. trace.hat = tr(L)
  │        δ1        = tr(LL)
  │        δ2        = tr(LL²)
  │   → trace.hat, δ1, δ2
  │
  └─ [enp の計算]  ───────────────────────────────────────
        9. enp = δ1 + 2 × trace.hat - N
      → enp

Rコード

対応するソースコード

hat matrix の構築 (loessf.f の ehg127 / ehg136 に対応)

  • 引数:
    • x : 説明変数ベクトルまたは行列 (N × D)
    • weights : case weights (長さ N)。NULL なら全て 1
    • span : バンド幅パラメータ (0 < span <= 1)
    • degree : 多項式次数 (1L または 2L)
  • 戻り値:
    • N×N の hat matrix L
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
}

lowesc に対応する計算

  • 引数:
    • L : hat matrix (N×N)
  • 戻り値:
    • list(trl, delta1, delta2)
lowesc_R <- function(L) {
  N <- nrow(L)

  # M = L - I = -(I - L)
  M <- L - diag(N)

  # LL = M M^T = (I-L)(I-L)^T
  LL <- M %*% t(M)

  # trace(L) → trace.hat
  trl <- sum(diag(L))

  # trace(LL) → one.delta = Σ_i ||(I-L)[i,:]||²
  delta1 <- sum(diag(LL))

  # trace(LL^2) → two.delta
  delta2 <- sum(diag(LL %*% LL))

  list(trl = trl, delta1 = delta1, delta2 = delta2)
}

メイン関数

compute_loess_stats_scratch <- function(y, x, weights = NULL,
                                        span = 0.75, degree = 2L) {
  N   <- length(y)
  L   <- build_hat_matrix(x, weights, span, degree)
  res <- lowesc_R(L)

  # enp の計算
  enp <- res$delta1 + 2 * res$trl - N

  list(
    one_delta = res$delta1,
    two_delta = res$delta2,
    trace_hat = res$trl,
    enp       = enp,
    L         = L
  )
}

Rの関数loessと結果が一致するか検証

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

res <- compute_loess_stats_scratch(y, x, w, span = 0.75, degree = 2L)
cat("スクラッチ one.delta =", res$one_delta, "\n")
cat("スクラッチ two.delta =", res$two_delta, "\n")
cat("スクラッチ trace.hat =", res$trace_hat, "\n")
cat("スクラッチ enp       =", res$enp,       "\n")
cat("-----------------\n")

fit <- loess(y ~ x, span = 0.75, degree = 2, weights = w,
             control = loess.control(surface = "direct",
                                     statistics = "exact",
                                     trace.hat = "exact"))
cat("公式   one.delta =", fit$one.delta, "\n")
cat("公式   two.delta =", fit$two.delta, "\n")
cat("公式   trace.hat =", fit$trace.hat, "\n")
cat("公式   enp       =", fit$enp,       "\n")
スクラッチ one.delta = 244.916 
スクラッチ two.delta = 244.6779 
スクラッチ trace.hat = 4.743286 
スクラッチ enp       = 4.402545 
-----------------
公式   one.delta = 244.916 
公式   two.delta = 244.6779 
公式   trace.hat = 4.743286 
公式   enp       = 4.402545 

4つの統計量は全て一致しています。

以上です。