RでCOX比例ハザードモデル

Rで COX比例ハザードモデル を試みます。

1. COX比例ハザードモデルとは

COX比例ハザードモデル(Cox Proportional Hazards Model)は、生存時間解析で利用される統計モデルの一つです。あるイベント(例:死亡、病気の再発、機械の故障など)が発生するまでの時間に、どのような要因(共変量、説明変数)が影響を与えるかを分析します。

主要な概念

  • ハザード関数 h(t): 時刻 t までイベントを経験しなかった個体が、次の瞬間にイベントを経験する「瞬間的なリスク」や「発生率」を表します。ハザードが高いほど、その瞬間にイベントが起こりやすいことを意味します。

  • 比例ハザードの仮定: 共変量の影響は、時間を通じて「比例的」であると仮定します。つまり、ある共変量を持つ個人のハザードは、持たない個人のハザードの常に「何倍」かになる、という関係性が時間によって変化しないと考えます。

モデルの数式

COXモデルは以下の数式で表されます。

h(t | X) = h₀(t) * exp(β₁X₁ + β₂X₂ + ... + βₚXₚ)

各項の意味は以下の通りです。

  • h(t | X): 共変量 X = (X₁, X₂, ..., Xₚ) を持つ個人の、時刻 t におけるハザード。
  • h₀(t): ベースラインハザード関数 (Baseline Hazard Function)。全ての共変量が0の場合のハザード関数です。これは時間の経過とともに変化する可能性のある、基本的なリスクを表します。
  • exp(...): 指数関数。共変量の効果がハザードを何倍にするか(ハザード比)を表す部分です。
  • β₁, ..., βₚ: 回帰係数。各共変量がハザードに与える影響の大きさを示します。

    • β > 0 なら、その共変量の値が大きいほどハザードは高くなります(リスク増)。
    • β < 0 なら、その共変量の値が大きいほどハザードは低くなります(リスク減)。
    • β = 0 なら、その共変量はハザードに影響を与えません。

特徴と利点

  • セミパラメトリックモデル: COXモデルは h₀(t) の具体的な形を仮定しません。これにより、時間の経過に伴うリスクの変動パターンを柔軟に捉えることができます。これが「セミパラメトリック」と呼ばれる所以です。
  • ハザード比 (Hazard Ratio, HR): exp(β) はハザード比と呼ばれ、結果の解釈が直感的です。例えば、共変量 X₁ が治療の有無(1: 新薬, 0: プラセボ)で、β₁ = -0.7 であった場合、ハザード比は exp(-0.7) ≈ 0.5 となります。これは「新薬を投与された群は、プラセボ群に比べて、各時点でのイベント発生リスクが約半分になる」と解釈できます。
  • 打ち切りデータの扱い: 研究期間の終了や追跡不能など、イベント発生を確認できなかった「打ち切り」データを扱うことができます。

2. 具体的なシナリオ

ストーリー

ある製薬会社が、進行性のがんに対する新しい治療薬「ガンキエール」を開発しました。この薬が患者の生存期間を延長させる効果があるかを確認するため、臨床試験を実施することにしました。

  • 対象: 200人の進行性がん患者
  • 介入:

    • 100人の患者には新薬「ガンキエール」を投与(治療群)。
    • 100人の患者には従来の標準治療(プラセボと見なす)を実施(対照群)。
  • 追跡期間: 5年間(60ヶ月)
  • 評価項目(イベント): 患者の死亡
  • 考慮する要因(共変量):

    1. 治療法 (treatment): 新薬か標準治療か。
    2. 診断時の年齢 (age): 年齢が生存に影響を与える可能性がある。
  • 有意水準: 5%

COX比例ハザードモデルが適している理由

このシナリオにおいて、私たちは「いつ死亡イベントが発生するか」という時間情報に関心があります。単に5年後に生存していたか否か(ロジスティック回帰)ではなく、治療法や年齢が「死亡リスクを時間的にどう変化させるか」を評価したいのです。

  1. 時間対イベントデータ: データは「登録から死亡または追跡終了までの時間」という形式になります。これは生存時間解析の典型的なデータ構造です。
  2. 打ち切りの存在: 5年間の追跡期間内にすべての患者が死亡するとは限りません。期間終了時に生存している患者や、途中で連絡が取れなくなった患者は「打ち切り」データとなります。COXモデルはこれらの情報を無駄にせず、尤度計算に組み込むことができます。
  3. ハザード比の解釈: 「新薬は標準治療に比べて、死亡リスクを何%低減させるか?」という問いに直接答えることができます。この「リスクの低減率(ハザード比)」が、治療の効果を示す重要な指標となります。
  4. 比例ハザードの仮定: このシナリオでは、「新薬の効果(死亡リスクを低減させる割合)は、治療開始直後でも数年後でも一定である」と仮定することが、第一近似として妥当と考えられます。もし効果が時間で変化するなら、その仮定を検証する別の手法もありますが、基本的なモデルとしてCOXモデルは最適な出発点です。

3. R言語によるシミュレーション

シナリオに沿って、データを生成し、COXモデルを実装します。

3.1. データの生成

まず、シミュレーション用のデータを生成します。真のパラメータを事前に設定し、そのパラメータに従うデータを生成します。

seed <- 20250625
set.seed(seed)

n <- 200 # 患者数

# 1. 共変量の生成
# treatment: 0 = 標準治療, 1 = 新薬
treatment <- rbinom(n, 1, 0.5)
# age: 40-70歳の一様分布から生成し、平均を0に標準化
age_raw <- runif(n, 40, 70)
age <- scale(age_raw, center = TRUE, scale = FALSE)

# 2. 真のパラメータ(β)を設定
# 新薬はリスクを約40%に下げる (exp(-0.9) ≈ 0.40)
# 年齢が1歳上がるとリスクが約3%上がる (exp(0.03) ≈ 1.03)
beta_true <- c(treatment = -0.9, age = 0.03)

# 3. 生存時間の生成 (Weibull分布を利用した逆関数法)
# ベースラインハザード h0(t) をWeibull分布のハザードと仮定してデータを生成する
# h(t) = shape * (lambda^shape) * t^(shape-1)
# このモデルは推定時には使わない。データ生成のためだけ。
weibull_shape <- 1.5
weibull_lambda <- 0.01

# 各個人のハザード比
hazard_ratio <- exp(treatment * beta_true["treatment"] + age * beta_true["age"])
# 逆関数法を用いて生存時間を生成
u <- runif(n)
T_event <- (-log(u) / (weibull_lambda * hazard_ratio))^(1 / weibull_shape)

# 4. 打ち切り時間の生成
# 追跡期間は最大60ヶ月とし、一部はそれ以前にランダムに打ち切られる
T_censor <- runif(n, 30, 60)

# 5. 観測データを作成
time <- pmin(T_event, T_censor) # 観測時間
status <- as.numeric(T_event <= T_censor) # 1: イベント発生, 0: 打ち切り

# データフレームにまとめる
sim_data <- data.frame(time, status, treatment, age)

# 生成されたデータの確認
print(head(sim_data))
cat("\nイベント発生率:", mean(status))
       time status treatment       age
1 11.624799      1         1 14.332969
2  3.665899      1         0 12.762012
3 59.191890      0         1 -3.893440
4  9.310014      1         1 -7.997177
5 30.340854      0         0 -8.329024
6  3.918434      1         0 -6.210677

イベント発生率: 0.79

3.2. COXモデルのパラメータ推定(実装)

COXモデルのパラメータ β は、部分尤度関数 (Partial Likelihood) を最大化することで推定します。これは数値最適化問題であり、ここではニュートン・ラフソン法を用います。

部分対数尤度関数:

logL(β) = Σ_{i: statusᵢ=1} [ β'Xᵢ - log(Σ_{j∈R(tᵢ)} exp(β'Xⱼ)) ]

ここで R(tᵢ) は、時刻 tᵢ でイベント発生リスクに晒されている個人の集合(リスクセット)です。

ニュートン・ラフソン法の更新式:

β_new = β_old + I(β_old)⁻¹ * U(β_old)

  • U(β): スコアベクトル(対数尤度の一階微分)
  • I(β): 観測情報行列(対数尤度の二階微分のマイナス)
# COXモデル実装
fun_coxph <- function(time, status, X) {
  # 行列形式に変換
  X <- as.matrix(X)
  p <- ncol(X)

  # データを時間でソート(リスクセットの計算を効率化するため)
  ord <- order(time)
  time <- time[ord]
  status <- status[ord]
  X <- X[ord, , drop = FALSE]

  # ニュートン・ラフソン法でβを推定
  beta <- rep(0, p) # 初期値
  max_iter <- 20

  for (iter in 1:max_iter) {
    # 現在のbetaでのリスクスコア
    risk_score <- exp(X %*% beta)

    # リスクセットの合計を効率的に計算 (時間でソート済みなので逆順に累積和を取る)
    S0 <- rev(cumsum(rev(risk_score)))

    # スコアベクトル U(beta) と情報行列 I(beta) を計算
    U <- numeric(p)
    I <- matrix(0, p, p)

    # イベントが発生した時点のみでループ
    event_indices <- which(status == 1)
    for (i in event_indices) {
      # リスクセット内の個体を取得
      risk_set_indices <- i:length(time)

      # S1, S2の計算
      S1_i <- colSums(X[risk_set_indices, , drop = FALSE] * risk_score[risk_set_indices])
      E_i <- S1_i / S0[i]

      # スコアベクトルの更新
      U <- U + (X[i, ] - E_i)

      # 情報行列の更新
      S2_i <- t(X[risk_set_indices, , drop = FALSE] * risk_score[risk_set_indices]) %*% X[risk_set_indices, , drop = FALSE]
      I <- I + (S2_i / S0[i] - E_i %*% t(E_i))
    }

    # betaを更新
    delta_beta <- solve(I, U)
    beta <- beta + delta_beta

    # 収束判定
    if (all(abs(delta_beta) < 1e-6)) {
      cat("収束しました (iteration =", iter, ")\n")
      break
    }
  }

  # 結果の計算
  coef <- as.vector(beta)
  names(coef) <- colnames(X)
  se <- sqrt(diag(solve(I)))
  z_val <- coef / se
  p_val <- 2 * pnorm(-abs(z_val))
  hr <- exp(coef)

  return(list(
    coef = coef,
    hazard_ratio = hr,
    se = se,
    z = z_val,
    p_value = p_val,
    var = solve(I)
  ))
}

# 実装を実行
X_vars <- sim_data[, c("treatment", "age")]
fitresult <- fun_coxph(sim_data$time, sim_data$status, X_vars)

# 実装の結果を表示
cat("\n--- 結果 ---\n")
print(data.frame(
  coef = fitresult$coef,
  exp_coef = fitresult$hazard_ratio,
  se_coef = fitresult$se,
  z = fitresult$z,
  p_value = fitresult$p_value
))
収束しました (iteration = 4 )

--- 結果 ---
                 coef  exp_coef     se_coef         z      p_value
treatment -1.01322144 0.3630476 0.169354386 -5.982847 2.192702e-09
age        0.03459087 1.0351961 0.009806044  3.527505 4.194951e-04

4. Rパッケージ (survival) との結果比較

次に、survival パッケージの coxph 関数を使って同じ分析を行い、結果を比較します。

library(survival)

# パッケージでCOXモデルを実行
package_fit <- coxph(Surv(time, status) ~ treatment + age, data = sim_data)

# パッケージの結果を表示
cat("\n--- survivalパッケージの結果 ---\n")
print(summary(package_fit))

--- survivalパッケージの結果 ---
Call:
coxph(formula = Surv(time, status) ~ treatment + age, data = sim_data)

  n= 200, number of events= 158 

               coef exp(coef)  se(coef)      z Pr(>|z|)    
treatment -1.013221  0.363048  0.169354 -5.983 2.19e-09 ***
age        0.034591  1.035196  0.009806  3.528 0.000419 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

          exp(coef) exp(-coef) lower .95 upper .95
treatment     0.363      2.754    0.2605     0.506
age           1.035      0.966    1.0155     1.055

Concordance= 0.657  (se = 0.022 )
Likelihood ratio test= 47.4  on 2 df,   p=5e-11
Wald test            = 45.6  on 2 df,   p=1e-10
Score (logrank) test = 48.31  on 2 df,   p=3e-11

結果の比較

ご覧の通り、実装の結果と、survival パッケージの結果は一致しています。

これにより、部分尤度法とニュートン・ラフソン法を用いた推定アルゴリズムが正しく機能していることが確認できます。

結論の解釈

  • 治療法 (treatment):係数 (coef) は-1.013で、p値は設定した有意水準を下回っています。さらに、ハザード比 (exp(coef)) は0.363であり、「新薬ガンキエールは、標準治療に比べて、死亡リスクを約63%低減させる (1 – 0.363 = 0.637)」と示唆されます。
  • 年齢 (age): 係数は0.034で、こちらも有意です。ハザード比は1.035であり、「年齢が1歳上がると、死亡リスクは約3.5%増加する」と解釈できます。

以上です。