【Forest Plot】Rでフォレストプロットの書き方:臨床研究のサブグループ解析

  • URLをコピーしました!

こんにちは。ほしのはやしです。
フォレストプロットはビジュアルでわかりやすく大人気ですが、いまいち書き方について説明したサイトないことに今更ながらに気づきました。

特に臨床研究などで見られるサブグループ解析でのフォレストプロットをRで書くには、ひと工夫必要ですので具体的な方法について説明していきます!

星柴くん

メタアナリシスのページばっかり出て調べるのが大変だったのだ!

黒星柴くん

メタアナリシスするような教室やとSPSSとか持ってるからニーズなかったりするんやで

目次

フォレストプロットとは?

元々フォレストプロットは、複数の研究結果を視覚的に比較するためのグラフです。
特に、メタアナリシス(メタ分析)と呼ばれる、複数の研究結果を統合して解析する際に頻繁に使われます。

昨今では、サブグループ解析・感度分析にも用いられるため、フォレストプロットの書き方を学ぶことは重要です。

しかしながら、データテーブルから一気にフォレストプロットを出力するパッケージはRにはなく、多くの研究者はSPSSなど超高額な有料ソフトを使っている現状があり、フリーソフトのRでフォレストプロットを書くことは資金を節約する上でも大切です。

サブグループ解析とは?

サブグループ解析とは、臨床試験の結果を、特定の特性を持つグループ(サブグループ)ごとに分けて解析することです。

例えば、年齢、性別、疾患の重症度など、患者に特徴的な要素を基にグループ分けし、それぞれのグループにおいて治療効果が異なるかどうかを調べます。

フォレストプロットを用いることで、全体の傾向と違う因子を視覚的に表すことができるのが大きなメリットです。

R studioでのフォレストプロットの具体的な書き方

練習用データの作成

やりたいこと:新薬vsプラセボで心筋梗塞発生率に差があるかという研究で、サブグループ解析をしたい

自前のデータがある場合はスキップしてください

#練習用データ
PatientID: 患者の識別子
Age: 年齢(連続変数)
Sex: 性別(”Male” または “Female”)
Hypertension: 高血圧の有無(1: あり, 0: なし)
Diabetes: 糖尿病の有無(1: あり, 0: なし)
Group: 割り付け群(”NewDrug” または “Placebo”)
Time: フォローアップ期間(週数)
Event: 心筋梗塞の発生有無(1: 発生, 0: 発生なし)

# 練習用データの作成
set.seed(123) # 再現性のためのシード
n <- 200 # 患者数

# 各変数を生成
PatientID <- 1:n
Age <- round(runif(n, 50, 80)) # 年齢: 50歳から80歳
Sex <- sample(c("Male", "Female"), n, replace = TRUE)
Hypertension <- sample(c(0, 1), n, replace = TRUE, prob = c(0.6, 0.4))
Diabetes <- sample(c(0, 1), n, replace = TRUE, prob = c(0.7, 0.3))
Group <- sample(c("NewDrug", "Placebo"), n, replace = TRUE)
Time <- round(runif(n, 1, 52)) # フォローアップ期間
Event <- rbinom(n, 1, ifelse(Group == "NewDrug", 0.15, 0.25)) # イベント発生率の差を反映

# データフレームを作成
datasheet1 <- data.frame(PatientID, Age, Sex, Hypertension, Diabetes, Group, Time, Event)

このスクリプトで以下のような表を作成しました!

テーブル名:datasheet1

PatientIDAgeSexHypertensionDiabetesGroupTimeEvent
159Female10NewDrug151
274Female00NewDrug311
362Female10Placebo90
476Male00Placebo450
578Female00NewDrug440
651Female01NewDrug251
766Male10NewDrug401

多くの場合、このような形でエクセルなどにデータを集積していると思います。

サブグループ解析してフォレストプロットで図示するためには連続変数をカテゴリー変数(群分け)することが必要ですので、年齢を65歳未満・65歳以上の2群に分けます。

#AgeGroup列を追加
datasheet1$AgeGroup <- ifelse(datasheet1$Age < 65, "<65", ">=65")

ifelse(数式, A, B)
数式の結果が真ならA、偽ならBとする、という意味

全体およびサブグループでのハザード比の計算

作業量を減らすために関数を作成していきますが、ここが難しく感じるかもしれません。

汎用関数の作成

全体およびサブグループで生存分析をまとめて行うために関数を作成します。
※この部分はデータシート名や列名が異なっていてもこのまま貼り付けでOKです!

library(tidyverse)
library(survival)

perform_analysis <- function(data, group_var = NULL) {
  if (!is.null(group_var)) {
    # サブグループ解析
    results <- by(data, data[[group_var]], function(subgroup_data) {
      analyze_cox_model(subgroup_data, subgroup_data[[group_var]][1])
    })
    do.call(rbind, results)
  } else {
    # 全患者解析
    analyze_cox_model(data, "All patients")
  }
}

【引数
data: サバイバル解析を行いたいデータシート名。
group_var: サブグループ(年齢、性別、疾患など)を指定するための変数名。NULLの場合、全患者の解析を実行します。

動作
group_varが指定されていれば、サブグループごとにanalyze_cox_model関数を呼び出して解析。
group_varNULLの場合、全体解析を行います。

これにより以下の関数が完成しました!

perform_analysis(データシート名, サブグループ解析したい列名)

Coxモデル解析と結果をまとめる関数作成

Cox比例ハザードモデルで解析する関数を作成します

analyze_cox_model <- function(data, subgroup_name) {
  
  # サバイバルオブジェクトを作成(ここのTime, Eventは自分のデータの列名に応じて変更してください)
  surv_obj <- Surv(time = data$Time, event = data$Event)
  
  # Cox比例ハザードモデルを適用(ここのGroupは自分のデータの列名に応じて変更してください)
  cox_model <- coxph(surv_obj ~ Group, data = data)
  
  # HR, 95%CI, p値を取得
  hr <- exp(coef(cox_model))
  ci <- exp(confint(cox_model))
  pval <- summary(cox_model)$coefficients[5]
  
  # 各群のN数を計算(ここのGroup, "NewDrug", "Placebo"は自分のデータの列名や内容に応じて変更してください)
  counts <- table(data$Group)
  new_drug_count <- counts["NewDrug"]
  placebo_count <- counts["Placebo"]
  
  # 結果をデータフレームとして返す
  data.frame(
    Subgroup = subgroup_name,
    NewDrug = ifelse(is.na(new_drug_count), 0, new_drug_count),
    Placebo = ifelse(is.na(placebo_count), 0, placebo_count),
    HR = hr,
    CI_Lower = ci[1],
    CI_Upper = ci[2],
    p_value = pval
  )
}

【引数
data: 解析対象のサブグループまたは全患者のデータ。
subgroup_name: 解析対象のサブグループ名(例: “AgeGroup”, “All patients”など)。

動作
Surv()を使って生存分析用のオブジェクトを作成。
coxph()関数でCox比例ハザードモデルを適用し、ハザード比(HR)、95%信頼区間(CI)、p値を計算。
サブグループの中で「NewDrug」と「Placebo」のそれぞれの患者数(new_drug_count, placebo_count)を取得し、結果をデータフレーム形式で返す。

サブグループ解析の実行

調べたいサブグループ(今回の場合は、年齢、性別、高血圧、糖尿病)に対して解析を行います。

# list()の中身は自分の調べたいサブグループの列名に応じて変更してください
subgroups <- list(
  AgeGroup = "AgeGroup",
  Sex = "Sex",
  Hypertension = "Hypertension",
  Diabetes = "Diabetes"
)

# datasheet1はデータシート名に応じて変更してください
subgroup_results <- lapply(names(subgroups), function(var) {
  cbind(Variable = var, perform_analysis(datasheet1, subgroups[[var]]))
})

【動作
subgroupsというリストを定義し、それぞれのサブグループに対してperform_analysis関数を呼び出して解析を実行。
lapply()を使ってリスト内の各サブグループに対して解析を適用し、Variable列にサブグループ名を付加します。

全体解析の実行

続いて全患者に対する解析を行います。

all_patients_result <- cbind(Variable = "All patients", perform_analysis(datasheet1))

【動作
perform_analysis関数を呼び出して、全患者の解析結果を得ます。
結果にVariable = "All patients"という列を追加します。

結果の統合

全体解析結果とサブグループ解析結果を統合します。

all_results <- do.call(rbind, c(list(all_patients_result), subgroup_results))

【動作
do.call(rbind, ...)を使って、全患者解析結果(all_patients_result)と各サブグループ解析結果(subgroup_results)を行列として縦に結合します。

view(all_results)で以下のような表でまとまったことがわかります!

フォレストプロット作成用にデータを変形

フォレストプロットを美しく見えるようにデータを変形します。

# 空白行を作成(適宜列名に応じて以下の内容は変更してください)
empty_rows <- data.frame(
  Variable = NA, Subgroup = NA, NewDrug = NA, Placebo = NA,
  HR = NA, CI_Lower = NA, CI_Upper = NA, p_value = NA,
  stringsAsFactors = FALSE
)

# 空白行を4つ複製(今回4因子でサブグループ解析したので4つ複製します)
empty_rows <- empty_rows[rep(1, 4), ]

# Subgroup列にリストの値を挿入(このsubgroupはサブグループ解析の実行で作成したリストに対応しています)
empty_rows$Subgroup <- unlist(subgroups)

# all_results に新しい行を追加
all_results <- rbind(all_results, empty_rows)

# 指定した行番号順(行をマニュアルで指定して並び替えます)
row_order <- c(1, 10, 2, 3, 11, 4, 5, 12, 6, 7, 13, 8, 9)

# 並び替え
all_results <- all_results[row_order, ]

# 最初の列と最後の列を同時に削除
all_results <- all_results[, -c(1, ncol(all_results))]

# 新しい列 se(ハザード比の標準誤差) を作成
all_results$se <- (log(all_results$CI_Upper) - log(all_results$HR)) / 1.96

# 結果を確認
view(all_results)

フォレストプロットの棒線を描画するために空白行を、HR(95% CI)を表す行を作成します。
またSubgroupの名前やインデントを調整します。

# 空白行を20個作成もしフォレストプロットの横線が足りなければ30,40などに増やす
all_results$` ` <- paste(rep(" ", 20), collapse = " ")

# 描画用のHR(95% CI)を作成する
all_results$`HR (95% CI)` <- ifelse(is.na(all_results$se), "",
                           sprintf("%.2f (%.2f to %.2f)",
                                   all_results$HR, all_results$CI_Lower, all_results$CI_Upper))

# Subgroup列の名前を変更する(0, 1をNo, Yesに変更する)
all_results$Subgroup <- ifelse(all_results$Subgroup == 0, "No", 
                                ifelse(all_results$Subgroup == 1, "Yes", all_results$Subgroup))

# 見やすい用にSubgroupのインデント調整(Placeboに数値があればインデント"   "を入れるようにする)
all_results$Subgroup <- ifelse(is.na(all_results$Placebo), 
                      all_results$Subgroup,
                      paste0("   ", all_results$Subgroup))
                      
# NAが表示されないように消すNewDrug, Placeboの列にあるNAを空白にする
all_results[c("NewDrug", "Placebo")] <- lapply(all_results[c("NewDrug", "Placebo")], function(x) {
  x[is.na(x)] <- ""
  x
})

# 結果を確認
view(all_results)

Forestploterでサブグループ解析のグラフ作成

パッケージ Forestploterをインストールしてください。
一般的なパッケージのインストール方法はこちら!

以下がコードになります。

p <- forest(all_results[,c(1:3, 8:9)],
            est = all_results$HR,
            lower = all_results$CI_Lower, 
            upper = all_results$CI_Upper,
            sizes = all_results$se,
            ci_column = 4,
            ref_line = 1,
            arrow_lab = c("NewDrug Better", "Placebo Better"),
            xlim = c(-1, 4),
            ticks_at = c(0, 1, 2, 3),
            footnote = "フォレストプロットが完成したよ!")

# Print plot
plot(p)

【基本的な項目】
est: ハザード比の中央値
lower: 95%信頼区間下限
upper: 95%信頼区間上限
sizes: 点サイズ(今回は大きさが標準誤差に比例するようにしています)
ci_column: フォレストプロットの横線を何列目に表示するかの列名
ref_line: 基準線(帰無仮説となる線)
arrow_lab: 矢印線の注釈
xlim: x軸範囲
ticks_at: 補助目盛りをどこに入れるか
footnote: 注釈

星柴くん

四角の大きさが標準誤差を表してるのだ!

黒星柴くん

習うより慣れろで、まずはコードをコピペして遊んでみるんやでな

まとめ

R studioを使って無料でサブグループ解析のフォレストプロットを作成する方法についてご説明しました!
皆様のお役に立てれば幸いです!!

  • URLをコピーしました!

この記事を書いた人

柴犬をこよなく愛する読書家。
街歩きとお菓子作りを趣味にしています。
研究や論文に役立つ情報をわかりやすくお伝えします。

コメント

コメントする

目次