傾向スコアマッチング(2値アウトカム)

この記事を読んでできること

この記事では、CursorというAIエディタを用いてRを動かし傾向スコアマッチングと2値アウトカムのリスク差やオッズ比の推定を行う方法をご紹介します。

具体的には、一切コードを書かずに論文用のTableを作成し、さらにはマッチング前後の共変量バランスの可視化までAIへの指示のみで行います。

AIエディタを使えば、AIに指示を出すだけで全ての解析が完了し、綺麗なTableやFigureが完成します。

AIエディタについてもう少し詳しく知りたい方はこちらをご覧ください。

本記事と同じ解析環境を準備する手順は下記の記事で詳細に解説しています。

Cursorで傾向スコアマッチング

本記事の解析環境

  • AIエディタ: Cursor (Version 0.45.11)
  • AIモデル: claude-3.5-sonnet

以下のステップで分析を進めていきます。

  1. データセットの準備
  2. 傾向スコアの推定
  3. 傾向スコアの分布の確認(マッチング前)
  4. 傾向スコアマッチング
  5. 傾向スコアの分布の確認(マッチング後)
  6. 共変量のバランシングを確認
  7. 患者背景表の作成
  8. マッチングコホートを用いたオッズ比の推定
  9. マッチングコホートを用いたリスク差の推定
AIへの指示内容は下記のように表記して進めていきます。
AIへの指示内容をここに記載

解析時にエラーが出て困ったときに読む記事もあるのでご安心ください。

それでは、早速分析を始めましょう。


データセットの準備

(ctrl+k) MASSパッケージのbirthwtデータをdfに格納

library(MASS)
df <- birthwt
birthwtデータに含まれる変数
変数名 説明
low 出生時体重が2.5kg未満かどうかを示す指標(0 = 2.5kg以上、1 = 2.5kg未満)
age 母親の年齢(年)
lwt 最終月経時の母親の体重(ポンド)
race 母親の人種(1 = 白人、2 = 黒人、3 = その他)
smoke 妊娠中の喫煙状況(0 = 吸わない、1 = 吸う)
ptl 以前の早産の回数
ht 高血圧の既往歴(0 = なし、1 = あり)
ui 子宮過敏性の有無(0 = なし、1 = あり)
ftv 妊娠初期の医師の診察回数
bwt 出生時体重(グラム)
  • アウトカム変数:low(出生時体重が2.5kg未満かどうかを示す)
  • 曝露変数:smoke(妊娠中の喫煙状況を示す)
  • 交絡因子:age, lwt, race, ptl, ht, ui, ftv

データの下準備として、ID列を作成します。このID列は、各患者を一意に識別するために使用されます。

(ctrl+k) 患者ID列をdfの先頭列に作成

df <- df %>%
    mutate(patient_id = row_number()) %>%
    select(patient_id, everything())

傾向スコアの推定

続いて、喫煙の有無を曝露因子として傾向スコアを推定します。

傾向スコアとは、共変量で条件付けられた時に曝露を受ける確率です。

一般的に、傾向スコアの推定には曝露を目的変数としたロジスティック回帰モデルを使用します。

今回の場合では、喫煙の有無 (smoke) を目的変数、年齢 (age)、体重 (lwt)、人種 (race)、早産歴 (ptl)、高血圧 (ht)、子宮刺激 (ui)、妊婦健診回数 (ftv)を説明変数としたロジスティック回帰モデルから得られた予測値が傾向スコアになります。

まずはロジスティック回帰モデルを構築します。

(ctrl+k) smokeを目的変数、age, lwt, race, ptl, ht, ui, ftvを説明変数としたロジスティック回帰モデルを構築

model <- glm(smoke ~ age + lwt + race + ptl + ht + ui + ftv, data=df, family=binomial())

summary(model)

続いて、データセットの各患者についてロジスティック回帰モデルを当てはめたときの予測値(傾向スコア)を推定しデータフレームに追加します。

(ctrl+k) dfの各被験者について、ロジスティック回帰モデルから得られる予測値を計算しpropensity_score列に入れる

df$propensity_score <- predict(model, type="response" )
propensity_score追加後のdfのイメージ
ID 年齢 体重 高血圧 propensity_score
1 65 75.2 1 0.824
2 42 62.5 0 0.312
3 58 80.1 1 0.756
4 35 55.8 0 0.145
5 70 68.4 1 0.678

傾向スコアの分布の確認(マッチング前)

マッチング前の曝露群(喫煙あり)と対照群(喫煙なし)における傾向スコアの分布を確認してみます。

(ctrl+k) propensity_scoreの分布を可視化

library(ggplot2)

ggplot(df, aes(x = propensity_score, fill = factor(smoke))) +
    geom_histogram(position = "identity", alpha = 0.5) +
    labs(title = "傾向スコアの分布",
         x = "傾向スコア", 
         y = "頻度",
         fill = "喫煙") +
    theme_bw()

作成したプロットがこちらです。


傾向スコアマッチング

いよいよ、傾向スコアマッチングを行います。RではMatchItパッケージのmatchit関数がよく用いられます。

関数の中には、下記のようにマッチング方法や距離の計算方法などについて指定が必要なパラメータがいくつかあります。

matchit関数の主要パラメータ

パラメータ デフォルト値 主な選択肢 説明
method nearest
  • nearest(最近傍法マッチング)
  • optimal(最適ペアマッチング)
  • full(最適フルマッチング)
  • genetic(遺伝的マッチング)
  • cem(単純化厳密マッチング)
マッチングのアルゴリズムを指定
distance glm
  • glm(一般化線形モデル)
  • mahalanobis(マハラノビス距離)
  • gam(一般化加法モデル)
マッチングの基準となる距離(類似度)の計算方法を指定
link logit
  • logit
  • probit
  • linear.{link}
距離尺度の推定に使用されるリンク関数を指定。"linear.{link}"とすると線形リンク関数が使用できる
caliper NULL(使用しない)
  • 0から1の間の数値
  • 一般的に0.2や0.25がよく使用
マッチングを許容する最大距離を指定
std.caliper TRUE
  • TRUE
  • FALSE
キャリパーを標準偏差の単位で指定するかどうかを決定
ratio 1
  • 1以上の整数
処置群1名に対して対照群を何名マッチングさせるかを指定
replace FALSE
  • TRUE(復元抽出)
  • FALSE(非復元抽出)
1度マッチング相手となった症例が再度マッチング候補になることを許容する場合はTRUE
(ctrl+k) matchit関数を用いて、マッチングを行います。 傾向スコアの推定には先ほどのmodelを使用します。
下記のパラメータを指定してください。
method = nearest,
distance = glm,
link = linear.logit,
caliper = 0.2,
std.caliper = TRUE,
ratio = 1

library(MatchIt)

matchit_model <- matchit(smoke ~ age + lwt + race + ptl + ht + ui + ftv, 
                         data = df, 
                         method = 'nearest', 
                         distance = 'glm', 
                         link = 'linear.logit', 
                         caliper = 0.2, 
                         std.caliper = TRUE, 
                         ratio = 1)

matched_data <- match.data(matchit_model)

下記2つはデフォルトの設定から変えています。

  • link = 'linear.logit'(デフォルトは'logit')
  • caliper = 0.2(デフォルトはNULL)

デフォルトのlink = `logit`の場合、傾向スコアそのものが距離の指標になります。この場合、傾向スコアがとり得る範囲 (0-1) のうち、真ん中 (0.5) 付近の0.1の差と、極端な値 (0や1) のときの0.1の差を同様に扱っています。

しかし、曝露を受ける確率が50%の症例と60%の症例と比べて、限りなく0に近い症例と10%の症例は性質が大きく異なると考えるのが自然です。

そのため、link = `linear.logit`とすることで距離の基準が0-1の範囲に強制されるのを防いでいます。

キャリパーは慣習的にlogit (傾向スコア) の標準偏差の0.2倍程度に設定されることが多いです。

キャリパーを小さくすると残余交絡を減らすことができますが、マッチングできる症例が減ってしまいマッチ後コホートの症例数が小さくなってしまいます。

キャリパーを大きくすると、マッチ後コホートの症例数が多くなりますが、残余交絡が大きくなりbiasedな結果に繋がります。

その他のパラメータはデフォルトの設定です。

今回のマッチングのアルゴリズムをまとめると

  • 曝露群を基準に、非曝露群からlogit(傾向スコア)が近い個体を1つずつ順番にマッチング(distance = 'glm', link = 'linear.logit', method = 'nearest')
  • logit(傾向スコア)が標準偏差の0.2倍(caliper = 0.2, std.caliper = TRUE)を超える相手とはマッチングしない
  • 曝露群と非曝露群とのマッチング比率は1:1(ratio = 1)で、一度マッチング相手となった非曝露群の症例はマッチング候補から除外されます(replace = FALSE)。

今回のように非復元の最近傍マッチングは多く用いられていますが、マッチング方法によるバイアスには様々議論があり、必ずしも最適とは限りません。

マッチングの方法の理論的側面を含めた実装方法については、高橋将宜著『統計的因果推論の理論と実装 ― 潜在的結果変数と欠測データ』(共立出版、2022年)が参考になりますので、ぜひご参照ください。

マッチング後のデータを確認してみましょう。

(ctrl+k) matched_dataの先頭数行を確認

head(matched_data)

   patient_id low age lwt race smoke ptl ht ui ftv  bwt propensity_score    distance weights subclass
85          1   0  19 182    2     0   0  0  1   0 2523        0.3310658 -0.70336837       1       17
87          3   0  20 105    1     1   0  0  0   1 2557        0.5827781  0.33418841       1       30
89          5   0  18 107    1     1   0  0  1   0 2600        0.6771450  0.74068213       1       39
92          7   0  22 118    1     0   0  0  0   1 2637        0.5420976  0.16878987       1        5
94          9   0  29 123    1     1   0  0  0   1 2663        0.4509575 -0.19680277       1       52
95         10   0  26 113    1     1   0  0  0   0 2665        0.5183271  0.07334119       1        1
変数名 説明 備考
smoke 処置変数(1/0) 喫煙の有無
age 年齢 マッチングに使用した共変量
lwt 体重 マッチングに使用した共変量
race 人種 マッチングに使用した共変量
ptl 既往歴 マッチングに使用した共変量
ht 高血圧の有無 マッチングに使用した共変量
ui 子宮刺激の有無 マッチングに使用した共変量
ftv 妊婦健診回数 マッチングに使用した共変量
subclass マッチングペアID 新規追加列:マッチングされたペアを識別する番号
distance プロペンシティスコア 新規追加列:ロジスティック回帰で算出された予測確率
weights マッチングの重み 新規追加列:1:1マッチングの場合はすべて1

傾向スコアの分布の確認(マッチング後)

(ctrl+k) マッチ後のpropensity scoreの分布を可視化してください。

ggplot(matched_data, aes(x = propensity_score, fill = factor(smoke))) +
    geom_histogram(position = "identity", alpha = 0.5) +
    labs(title = "マッチング後の傾向スコアの分布",
         x = "傾向スコア",
         y = "頻度",
         fill = "喫煙") +
    theme_bw()

ペアが作れなかった患者が除外されたことで、マッチング後の傾向スコアの分布はマッチング前の分布と比較して、より均一になっていることがわかります。


共変量のバランシングの確認

次に、cobaltパッケージのlove.plot関数を用いて、マッチング前後のコホートについて、age, lwt, race, ptl, ht, ui, ftvの標準化平均差を可視化してみます。

(ctrl+k) love.plot関数を用いて、マッチング前後のコホートについて、age, lwt, race, ptl, ht, ui, ftvの標準化平均差を可視化してください。

library(cobalt)

love.plot(matchit_model,
          stats = "mean.diffs",
          thresholds = c(m = 0.1),
          var.order = "unadjusted",
          binary = "std",
          abs = TRUE,
          var.names = c(age = "年齢",
                         lwt = "体重",
                         race = "人種",
                         ptl = "早産歴",
                         ht = "高血圧",
                         ui = "子宮刺激",
                         ftv = "妊婦健診回数"),
          line = TRUE,
          title = "マッチング前後の標準化平均差") +
  theme_bw() +
  theme(text = element_text(family = "HiraKakuProN-W3"))

赤色がマッチング前、青色がマッチング後のコホートにおける共変量の標準化平均差です。

この値は0に近いことが理想で、0.1を超えないことが望ましいとされています。

マッチング後は標準化平均差が小さくなっており、ほぼ0.1以内におさまっています。


患者背景表の作成

続いて、マッチング前・後それぞれの患者背景表を作成してみます。

(ctrl+k) マッチ前後のコホートについて下記の設定で患者背景表を作成する。
群分け変数: smoke
連続変数: age, lwt
カテゴリカル変数: race, ptl, ht, ui, ftv

    library(tableone)
    vars <- c("age", "lwt", "race", "ptl", "ht", "ui", "ftv")
    cat_vars <- c("race", "ptl", "ht", "ui", "ftv")
    
    tab1 <- CreateTableOne(vars = vars,
                          strata = "smoke", 
                          data = df,
                          factorVars = cat_vars)
    print(tab1, showAllLevels = TRUE)
    
    # マッチング後の患者背景表
    tab2 <- CreateTableOne(vars = vars,
                          strata = "smoke", 
                          data = matched_data,
                          factorVars = cat_vars)
    print(tab2, showAllLevels = TRUE)

見やすくなるようにkable関数で表を整えて出力します。

(ctrl+k) kableで出力して
マッチング前の患者背景表

    |                |0              |1              |p      |test |
    |:---------------|:--------------|:--------------|:------|:----|

    |n               |115            |74             |       |     |
    |age (mean (SD)) |23.43 (5.47)   |22.95 (5.05)   |0.545  |     |
    |lwt (mean (SD)) |130.90 (28.43) |128.14 (33.79) |0.546  |     |
    |race (%)        |               |               |<0.001 |     |
    |1               |44 (38.3)      |52 (70.3)      |       |     |
    |2               |16 (13.9)      |10 (13.5)      |       |     |
    |3               |55 (47.8)      |12 (16.2)      |       |     |
    |ptl (%)         |               |               |0.066  |     |
    |0               |103 (89.6)     |56 (75.7)      |       |     |
    |1               |10 ( 8.7)      |14 (18.9)      |       |     |
    |2               |2 ( 1.7)       |3 ( 4.1)       |       |     |
    |3               |0 ( 0.0)       |1 ( 1.4)       |       |     |
    |ht = 1 (%)      |7 ( 6.1)       |5 ( 6.8)       |1.000  |     |
    |ui = 1 (%)      |15 (13.0)      |13 (17.6)      |0.519  |     |
    |ftv (%)         |               |               |0.155  |     |
    |0               |55 (47.8)      |45 (60.8)      |       |     |
    |1               |35 (30.4)      |12 (16.2)      |       |     |
    |2               |19 (16.5)      |11 (14.9)      |       |     |
    |3               |3 ( 2.6)       |4 ( 5.4)       |       |     |
    |4               |3 ( 2.6)       |1 ( 1.4)       |       |     |
    |6               |0 ( 0.0)       |1 ( 1.4)       |       |     |
    
マッチング後の患者背景表

    |                |0              |1              |p     |test |
    |:---------------|:--------------|:--------------|:-----|:----|
    |n               |55             |55             |      |     |
    |age (mean (SD)) |22.85 (4.94)   |23.44 (5.30)   |0.553 |     |
    |lwt (mean (SD)) |131.36 (30.37) |129.85 (32.13) |0.801 |     |
    |race (%)        |               |               |0.887 |     |
    |1               |34 (61.8)      |35 (63.6)      |      |     |
    |2               |8 (14.5)       |9 (16.4)       |      |     |
    |3               |13 (23.6)      |11 (20.0)      |      |     |
    |ptl (%)         |               |               |0.951 |     |
    |0               |48 (87.3)      |47 (85.5)      |      |     |
    |1               |5 ( 9.1)       |6 (10.9)       |      |     |
    |2               |2 ( 3.6)       |2 ( 3.6)       |      |     |
    |ht = 1 (%)      |4 ( 7.3)       |3 ( 5.5)       |1.000 |     |
    |ui = 1 (%)      |6 (10.9)       |6 (10.9)       |1.000 |     |
    |ftv (%)         |               |               |0.156 |     |
    |0               |23 (41.8)      |32 (58.2)      |      |     |
    |1               |20 (36.4)      |9 (16.4)       |      |     |
    |2               |10 (18.2)      |10 (18.2)      |      |     |
    |3               |1 ( 1.8)       |3 ( 5.5)       |      |     |
    |4               |1 ( 1.8)       |1 ( 1.8)       |      |     |
    

ここまでで、傾向スコアマッチング、傾向スコアの分布の確認、共変量のバランシングの確認、患者背景表の作成が完了しました。

いよいよ、マッチング後のコホートを用いて解析を行います。


オッズ比の推定

傾向スコアマッチング後の解析ではロバスト分散推定を行うことが推奨されています。

2値アウトカムの解析においてロバスト分散推定を行うためには、geepackパッケージのgeeglm関数が便利です。

オッズ比を推定したいときはlink="logit"を指定します。

(ctrl+k) マッチ後のデータから、geeglm関数でリンク関数logitを用いてオッズ比を推定する。 目的変数はlow、説明変数はsmoke、クラスターはsubclassとする。 モデルを構築するところまでコードを生成してください。

library(geepack)
gee_model <- geeglm(low ~ smoke, 
                    data = matched_data,
                    id = subclass,
                    family = binomial(link = "logit"),
                    corstr = "independence")
    

geeglmの結果のサマリ作成には、broomパッケージのtidy関数が便利です。

(ctrl+k) broomパッケージのtidy関数を使って、モデルの結果からオッズ比、95%信頼区間、p値のサマリを作成してください。

library(broom)
model_summary <- tidy(gee_model, conf.int = TRUE, exp = TRUE) %>%
    select(term, estimate, conf.low, conf.high, p.value) %>%
    rename(odds_ratio = estimate)
print(model_summary)

# A tibble: 2 × 5
term        odds_ratio conf.low conf.high   p.value
                          
1 (Intercept)      0.279    0.147     0.529 0.0000926
2 smoke            1.89     0.813     4.40  0.139
    

傾向スコアマッチングにより交絡因子の調整は行われているので、解析モデルは説明変数が曝露因子(喫煙の有無)のみの単変量解析を行っています。

マッチング後も共変量のバランシングが不十分な因子がある場合は、さらに上記のモデルに共変量を加えて多変量解析にすることで、残余交絡によるバイアスを取り除くことが期待されます。

ただし、マッチングに用いた因子は全て多変量回帰モデルに含めるようにしてください。マッチングに用いた因子以外をモデルに含めたり、一部のみを使用すると、他の因子のバランスを損なう可能性があることが報告されています [1]

[1] Shinozaki T, Nojima M. Misuse of Regression Adjustment for Additional Confounders Following Insufficient Propensity Score Balancing. Epidemiology. 2019;30(4):541-548.


リスク差の推定

geeglm関数でリスク差を推定したいときはlink="identity"を指定します。

(ctrl+k) マッチ後のデータから、geeglm関数でリンク関数identityを用いてリスク差を推定する。 目的変数はlow、説明変数はsmoke、クラスターはsubclassとする。 モデルを構築するところまでコードを生成してください。

gee_model_rd <- geeglm(low ~ smoke,
                        data = matched_data,
                        id = subclass,
                        family = gaussian(link = "identity"),
                        corstr = "independence")
(ctrl+k) broomパッケージのtidy関数を使って、モデルの結果から回帰係数、95%信頼区間、p値のサマリを作成してください。

model_summary_rd <- tidy(gee_model_rd, conf.int = TRUE) %>%
    select(term, estimate, conf.low, conf.high, p.value) %>%
    rename(risk_difference = estimate)
print(model_summary_rd)

# A tibble: 2 × 5
term        risk_difference conf.low conf.high   p.value
                               
1 (Intercept)           0.218   0.109      0.327 0.0000894
2 smoke                 0.127  -0.0386     0.293 0.133

これで、傾向スコアマッチング、共変量バランシングの確認、患者背景表の作成、オッズ比・リスク差を推定することができました。

コメント