RWD 分析を R で

西川 寛来

2026-04-08

スライドはこちら

自己紹介

こんな話をします 🙆

  • RWD 分析におけるデータハンドリングの tips
  • データベース研究で便利なパッケージ

RWD 分析の大変なところ 🤔

製薬企業でよく利用される RWD

  • 医療情報データベース
    • レセプトデータ
    • 電子カルテデータ

解析上の難しい点・勘所

  • 大規模データのハンドリング
  • 治療の連続性 (処方継続、中止、切り替え) の追跡
  • ICD-10 等のコード体系

結論: これで学ぼう

これが最速だと思います。が、本発表ではもっとマニアックな tips をお伝えします

基本的な操作

非等価結合 (Non-equi joins) 🔗

  • RWD 研究で頻出のシチュエーションで便利
    • 処方日より前の直近の検査値を取得したい
    • 診断日から n 日以内の入院を探したい
# 処方テーブル
rx <- tibble(patient_id = c(1, 1, 2), rx_date = as.Date(c("2023-01-15", "2023-06-01", "2023-03-10")))

# 検査値テーブル
lab <- tibble(
  patient_id = c(1, 1, 1, 2),
  lab_date   = as.Date(c("2022-12-01", "2023-01-10", "2023-05-20", "2023-02-15")),
  hba1c      = c(7.2, 7.5, 6.8, 8.1)
)

# 処方日以前の直近の検査値を非等価結合で取得
result <- rx |>
  left_join(lab, join_by(patient_id, closest(rx_date >= lab_date)))
1
join_by() で非等価結合、closest() で直近レコードを取得
  • left_join()filter()arrange()slice_head() と同様の処理が、join_by() 1 行で完結!

治療継続性 (Persistency) の定義 💉

e.g., 「治療が 15 日以上途切れたら中断とみなす」みたいなルールを定義したい

rx <- tribble(
  ~patient_id, ~start_date,   ~end_date,
  1,           "2024-01-01",  "2024-04-01",
  1,           "2024-01-15",  "2024-02-01",
  1,           "2024-02-15",  "2024-03-01"
) |>
  mutate(start_date = as.Date(start_date), end_date = as.Date(end_date))

# 各処方を lubridate の interval 形式に変換
rx_intervals <- rx |>
  arrange(patient_id, start_date) |>
  group_by(patient_id) |>
  mutate(
    rx_interval     = interval(start_date, end_date),
    running_max_end = accumulate(end_date, max),
    prev_coverage   = lag(interval(first(start_date),
                                   running_max_end))
  )

# int_overlaps() で「前の処方カバレッジと重なるか」を判定
rx_intervals |>
  mutate(
    is_continuation = !is.na(prev_coverage) &
                       int_overlaps(rx_interval, prev_coverage)
  )
1
interval() で処方日と処方日数をもとに「治療期間」を定義
2
accumulate() で「これまでの”最遅終了日”」を更新
3
int_overlaps() で重なれば継続と判定

単純な lead/lag の比較だと…?

❌ lead/lag の比較

処方1  |===========1/1〜4/1===========|

処方2    |=1/15〜2/1=|
処方3                    |=2/15〜3/1=|
                       ↑ 処方2と処方3の間の gap で
                         誤って「中断」と判定

✅ “最遅終了日”を更新するやり方

処方1  |===========1/1〜4/1===========|
           ↓ running_max_end = 4/1
処方2    |=1/15〜2/1=|  → 4/1以前: 継続✅
処方3                    |=2/15〜3/1=|  → 4/1以前: 継続✅

治療の連続性の判定には、lead()/lag() だとダメなことがある!
accumulate(end_date, max) とすると確実

大規模データへの対応

あなたのデータ環境は?

  • DB が用意されている → SQL で抽出すれば OK
    • SQL または dbplyr
  • DB がない / R 上で大きなデータを扱う → DuckDB
    • duckplyr

DB があるなら: SQL, dbplyr どちらもOK

library(DBI)
library(dplyr)
library(dbplyr)

con <- dbConnect(RPostgres::Postgres(), ...)  # DB接続

SQL

sql <- "
SELECT patient_id, drug, COUNT(*) AS n_rx
FROM rx
WHERE start_date >= DATE '2023-01-01'
  AND start_date <  DATE '2024-01-01'
GROUP BY patient_id, drug
"

n_rx <- dbGetQuery(con, sql)

dbplyr

n_rx <- con |> 
  tbl("rx") |>
  filter(
    between(
      start_date, 
      as.Date("2023-01-01"), 
      as.Date("2024-01-01")
    )
  ) |>
  count(patient_id, drug) |> 
  collect()  # ここで初めて R に持ってくる

それでも「R 上で大きなデータ」を扱いたい場面がある

  • ウチには DB なんてないよ! CSV ファイルでしかデータもらえないよ!
  • DB はあるけど、権限の都合で DB 上に一時テーブルが作れない

そんな時には duckplyr

duckplyr のうれしさ

  • DB を準備しなくても、R 上で大規模データを扱える
  • 速い
  • dplyr 記法で書ける
library(duckplyr)

penguins_duck <- penguins |> 
  as_duckdb_tibble() |>  # duckplyr data frame に変換
  group_by(species) |>  # 普通に dplyr を書くだけ
  summarise(mean_bill_length = mean(bill_len, na.rm = TRUE))

dplyrduckplyr の速度比較

Code
.gen_data <- \(n_group, n_row, n_col_value, .seed = 1) {
  groups <- seq_len(n_group) |>
    rep_len(n_row) |>
    as.character()
  
  set.seed(.seed)
  
  runif(n_row * n_col_value, min = 0, max = 100) |>
    round() |>
    matrix(ncol = n_col_value) |>
    tibble::as_tibble(
      .name_repair = \(x) paste0("col_value_", seq_len(n_col_value))
    ) |>
    dplyr::mutate(col_group = groups, .before = 1)
}

.use_dplyr <-
  function(.data) {
    .data |>
      dplyr::summarise(
        value = sum(col_value_1, na.rm = TRUE),
        .by = col_group
      ) |>
      dplyr::arrange(col_group)
  }

.use_duckplyr <-
  function(.data) {
    .data |>
      duckplyr::as_duckdb_tibble() |>
      dplyr::summarise(
        value = sum(col_value_1, na.rm = TRUE),
        .by = col_group
      ) |>
      dplyr::arrange(col_group)
  }

n_col_value <- 1

res_sum <- bench::press(
  n_row = c(1e5, 1e6),
  n_group = c(1e4, 1e5),
  {
    dat <- .gen_data(n_group, n_row, n_col_value)
    bench::mark(
      check = FALSE,
      min_iterations = 5,
      dplyr = .use_dplyr(dat),
      duckplyr = .use_duckplyr(dat)
    )
  }
)

res_sum |>
  ggplot2::autoplot("violin")

  • 速いです

フローチャート作成

CONSORT diagram とは

  • 臨床研究において患者選択や割り当ての流れを示したフローチャート
  • 集団選択が複雑な RWD 研究ではとても重要!

よくある CONSORT diagram の作り方

🤔

  • コピペ、体裁を整える作業で消耗するほど人生は長くない
  • データと紐付けて、患者数を自動で入れたい。どうせ明日には除外基準が 1 つ追加されて、患者数も変わるんだし…

CONSORT diagram を作るための R パッケージ

  • flowchart
    • “Tidy Flowchart Generator”
    • dplyr っぽく piping してフローチャートを作成

flowchart の使い方

safo |> 
  as_fc(label = "Patients assessed for eligibility") |>
  fc_filter(!is.na(group), label = "Randomized", show_exc = TRUE) |>
  fc_split(group) |>
  fc_filter(itt == "Yes", label = "Included in ITT") |>
  fc_draw()
1
as_fc(): fc オブジェクトを定義
2
fc_filter(): 条件を満たす行を残す
3
fc_split(): 割付群ごとに分岐
4
fc_draw(): 図を描画

  • 患者集団やコードセットに変更があっても、患者数のコピペは不要!

まとめ

RWD 分析を R でやっていきましょう

📦 基本的な操作

  • join_by() を使った非等価結合は便利
  • 治療継続性は lubridate の interval と accumulate() を組み合わせると確実

🚀 大規模データ

  • DB があるなら SQL / dbplyr
  • ローカル の R でも duckplyr を使うだけで高速化!

🗂 フローチャート作成

  • 全部 R でやって、コピペ作業から解放

Enjoy 🙋