통계를 배우자!: Group Differences Adjustment in Observational Studies

Zarathu Co., Ltd

김진섭

2025-01-18

자기소개

회사: 차라투 주식회사(Zarathu Co.,Ltd)

  • R 활용 의학연구지원
  • R 패키지 개발 및 교육

경력

  • 의학사, 성균관대학교 (~2009)
  • 예방의학전문의/박사수료,서울대보건대학원 (~2013)
  • 책임, 삼성전자 DMC연구소/무선사업부 (~2016)
  • 대표, 차라투 (2018~)

jinseob2kim@gmail.com, github.com/jinseob2kim

Executive summary

Baseline 맞춘다(X), RCT를 모방한다(O)

  • ATE(average treatment effect) vs ATT(average treatment effect on treated)

  • Matching은 ATT, IPTW는 ATE

2그룹 MatchIt, 3그룹 twang 패키지

  • Logistic regression, Nearest neighbor, caliper 이해

  • 3그룹 matching은 가장 작은 N수에 맞춰 2번 수행

  • openstat.ai 에서 2그룹 matching/IPTW 지원

분석 이슈

  • Matching 후 pair정보 이용해야 하는가?(ex: stratified cox)

  • 성별에 따라 매칭/IPTW 해도 되는가?

Causal inference

목표는 Causal inference, RCT like design

  • \(ITE = Y_{1i} - Y_{0i}\), 하늘만이..

  • \(ATE = E[Y_1 - Y_0]\), RCT

  • \(ATT = E[Y_1 - Y_0 | T=1]\), \(ATC = E[Y_1 - Y_0 | T=0]\)

PS matching

Propensity score란?

  • 치료군 vs 대조군 연구에서, age/sex/기저질환 등의 변수를 이용하여 치료군에 속할 확률을 계산한 값
  • 같은 PS 면 치료군에 속할 확률이 동일
  • 같은 PS면 age/sex/기저질환 이 비슷
  • 치료/대조군을 1/0으로 코딩하여 Logistic regression(다른 것도 가능)
  • 치료군이 더 많다면 치료군을 0, 대조군을 1로 코딩

따라서, 치료군의 PS와 비슷한 사람을 대조군에서 뽑으면 두 그룹의 Baseline이 비슷해지겠군!

  • 여기까지 알면 50점

RCT 관점

RCT: 어떤 사람이 두 군에 배정될 확률이 50:50

PS matching: PS가 0.7인 사람이 두 군에 배정될 확률? 50:50

  • PS 0.7인 사람이 치료군에 있다면, 대조군에서도 맞춰서 뽑았을 것임
  • 여기까지 알면 90점

그럼 PS matching하면 RCT만큼 인정받을 수 있다?

  • No
  • 치료군과 동일 특성을 가진 대조군이 매칭되므로, 연구집단 전체가 치료군의 특성을 가짐(예: 더 고령/ 남자가 많다 등)
  • ATE(average treatment effect) 가 아님, ATT(average treatment effect on the treated).

IPTW

Inverse probability of treatment weighting

  • 매칭 안하고 모든 샘플을 씀. 단 각 사람마다 가중치가 다름
  • A는 1명이지만 2명처럼 간주, B는 1명이지만 10명처럼 간주
  • 사람별 가중치를 조절하면 Baseline을 동일하게 맞출 수 있음
  • 치료군엔 \(1/PS\), 대조군엔 \(1/(1-PS)\) 로 가중치 부여
  • 여기까지 알면 50점

RCT 관점

PS 0.7인 사람이 각 군에 속할 확률은?

  • 치료군 0.7, 대조군 0.3

\[0.7 \times \frac{1}{0.7} = 0.3 \times \frac{1}{0.3} = 1\]

  • 따라서 PS 0.7인 사람이 치료군과 대조군에 동일하게 분포

그럼 IPTW는 ATE vs ATT?

ATE weight, ATT weight

ATE weight

\[ w_i \text{ for } ATE= \begin{cases} \frac{1}{p_i} & \text{if treated} \\ \frac{1}{1 - p_i} & \text{if control} \end{cases} \] 전체 샘플(Treated + Control) 의 2배를 랜덤하게 배정한 RCT

ATT weight

\[ w_i \text{ for } ATT = \begin{cases} 1 & \text{if treated} \\ \frac{p_i}{1 - p_i} & \text{if control} \end{cases} \] Treated + Treated 를 랜덤하게 배정

ATT& ATE

  • ATE(Average Treatment Effect): 전체 환자 집단(코호트)에서 TAVI수술 에 비해 효과적인가?
  • 대조군(Surgery)와 TAVI군의 baseline을 가중치를 주어 전체 코호트(AS 환자군)와 양군을 유사하게 만들어, 전체 코호트에서 RCT를 진행하였을 때 예상되는 결과를 모사 \[ w_i = \begin{cases} \frac{1}{p_i} & \text{if treated TAVI} \\ \frac{1}{1 - p_i} & \text{if (Surgery)} \end{cases} \]

TAVI vs Surgery: Example data

ATT& ATE

name age TAVI Survival Propensity_score ATE ATT
빈센조 35 1 1 0.1718567 5.818801 1.0000000
루카스 48 0 1 0.3163092 1.462649 0.4626495
제이슨 50 0 1 0.3435664 1.523383 0.5233834
토마스 53 1 0 0.3864109 2.587919 1.0000000
리오넬 55 0 1 0.4160330 1.712426 0.7124256
카밀라 68 1 1 0.6136449 1.629607 1.0000000
아칸지 70 1 1 0.6424478 1.556547 1.0000000
에밀리 75 0 1 0.7097903 3.445784 2.4457837
노이어 80 1 0 0.7690095 1.300374 1.0000000
호날두 85 1 0 0.8192224 1.220670 1.0000000
앨리스 40 0 1 0.2202580 1.282475 0.2824754
45 0 1 0.2777194 1.384504 0.3845035
찰리 52 0 0 0.3718948 1.592090 0.5920901
다니엘 60 1 0 0.4923210 2.031195 1.0000000
엘리자베스 62 1 1 0.5231400 1.911534 1.0000000
프랭크 67 0 1 0.5989249 2.493298 1.4932985
그레이스 73 1 1 0.6837417 1.462541 1.0000000
헨리 77 0 1 0.7345263 3.766852 2.7668518
이사벨 82 1 0 0.7901901 1.265518 1.0000000
제임스 88 1 0 0.8450253 1.183396 1.0000000
42 0 1 0.2421700 1.319557 0.3195571
마리아 49 0 1 0.3297948 1.492080 0.4920803
피터 54 1 0 0.4011317 2.492947 1.0000000
사라 59 1 0 0.4769187 2.096793 1.0000000
데이비드 61 0 1 0.5077378 2.031438 1.0314379
제니퍼 46 0 1 0.2902582 1.408963 0.4089632
케빈 51 1 1 0.3576063 2.796371 1.0000000
레베카 76 0 1 0.7223278 3.601369 2.6013690
토니 81 1 0 0.7797825 1.282409 1.0000000
엘리 83 1 0 0.8002318 1.249638 1.0000000
스티브 37 1 1 0.1901277 5.259622 1.0000000
안나 47 0 1 0.3031256 1.434979 0.4349789
마이클 51 0 1 0.3576063 1.556678 0.5566777
제시카 68 1 0 0.6136449 1.629607 1.0000000
84 1 1 0.8099085 1.234707 1.0000000
소피아 59 0 1 0.4769187 1.911749 0.9117489
브라이언 62 1 1 0.5231400 1.911534 1.0000000
나탈리 78 0 1 0.7463771 3.942861 2.9428614
대니얼 84 1 0 0.8099085 1.234707 1.0000000
엘레나 89 1 0 0.8529311 1.172428 1.0000000
로버트 39 0 1 0.2098490 1.265581 0.2655808
줄리아 46 0 1 0.2902582 1.408963 0.4089632
스콧 53 0 0 0.3864109 1.629755 0.6297551
니콜 77 1 0 0.7345263 1.361422 1.0000000
앤드류 75 1 1 0.7097903 1.408867 1.0000000
케이트 54 0 1 0.4011317 1.669816 0.6698162
라이언 59 1 1 0.4769187 2.096793 1.0000000
미셸 86 0 1 0.8281768 5.819935 4.8199354
조셉 88 1 0 0.8450253 1.183396 1.0000000
엘레나 83 1 0 0.8002318 1.249638 1.0000000

ATT& ATE

Treatment, Control 그룹의 Age 비교
그룹 Treatment Control
ATT 70 69.04
ATE 62.17 63.1
Original Cohort 70(63.72) 56.35(63.72)

IPTW win?

IPTW는 ATE니까 무조건 이걸해야겠네?

  • Weight 100 인 사람이 있다면?

Truncated weight

  • 99% quantile 값 이상은 99%로 바꿈
  • weight 10 이상은 10으로 바꿈
  • Baseline 이 덜 맞춰지게 됨, ATE 훼손

분석난이도 증가

  • GLM, Cox에 Weight를 고려 (glm, cox weights 옵션 또는 svyglm, svycox)

  • Weighted Kaplan-meier from svycox (survfit weights 옵션 또는 svykm)

s1 <- svykm(Surv(time, status > 0) ~ sex, design = dpbc)
jskm::svyjskm(s1, pval = T, table = T, design = dpbc)

log-rank test(X), survey rank test(O, survey::svyranktest)

  • 또는 IPWsurvival::adjusted.LR 이용(SAS 디폴트)

MatchIt

2그룹 PS/IPTW에서 가장 많이 쓰는 패키지

library(MatchIt)
data("lalonde", package = "MatchIt")

table(lalonde$treat)

  0   1 
429 185 
#1:1 NN matching w/ replacement on a logistic regression PS
m.out <- matchit(treat ~ age + educ + race + married + 
                   nodegree + re74 + re75, data = lalonde, distance = "glm", method = "nearest", ratio = 1, caliper = NULL)
m.out
A matchit object
 - method: 1:1 nearest neighbor matching without replacement
 - distance: Propensity score
             - estimated with logistic regression
 - number of obs.: 614 (original), 370 (matched)
 - target estimand: ATT
 - covariates: age, educ, race, married, nodegree, re74, re75
  • distance: glm(로지스틱) 이 기본, 다른 ML모델도 가능
  • method: nearest 충분, ratio: 1:N 매칭
  • caliper: 매칭이 잘 안될때(ex: Treat/Control 숫자가 비슷할 때)

Matching data

mdata <- match.data(m.out)
rmarkdown::paged_table(mdata[order(mdata$subclass), c("treat", "distance", "subclass")])

Matching 결과 확인

summary(m.out) 

Call:
matchit(formula = treat ~ age + educ + race + married + nodegree + 
    re74 + re75, data = lalonde, method = "nearest", distance = "glm", 
    caliper = NULL, ratio = 1)

Summary of Balance for All Data:
           Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance          0.5774        0.1822          1.7941     0.9211    0.3774
age              25.8162       28.0303         -0.3094     0.4400    0.0813
educ             10.3459       10.2354          0.0550     0.4959    0.0347
raceblack         0.8432        0.2028          1.7615          .    0.6404
racehispan        0.0595        0.1422         -0.3498          .    0.0827
racewhite         0.0973        0.6550         -1.8819          .    0.5577
married           0.1892        0.5128         -0.8263          .    0.3236
nodegree          0.7081        0.5967          0.2450          .    0.1114
re74           2095.5737     5619.2365         -0.7211     0.5181    0.2248
re75           1532.0553     2466.4844         -0.2903     0.9563    0.1342
           eCDF Max
distance     0.6444
age          0.1577
educ         0.1114
raceblack    0.6404
racehispan   0.0827
racewhite    0.5577
married      0.3236
nodegree     0.1114
re74         0.4470
re75         0.2876

Summary of Balance for Matched Data:
           Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean
distance          0.5774        0.3629          0.9739     0.7566    0.1321
age              25.8162       25.3027          0.0718     0.4568    0.0847
educ             10.3459       10.6054         -0.1290     0.5721    0.0239
raceblack         0.8432        0.4703          1.0259          .    0.3730
racehispan        0.0595        0.2162         -0.6629          .    0.1568
racewhite         0.0973        0.3135         -0.7296          .    0.2162
married           0.1892        0.2108         -0.0552          .    0.0216
nodegree          0.7081        0.6378          0.1546          .    0.0703
re74           2095.5737     2342.1076         -0.0505     1.3289    0.0469
re75           1532.0553     1614.7451         -0.0257     1.4956    0.0452
           eCDF Max Std. Pair Dist.
distance     0.4216          0.9740
age          0.2541          1.3938
educ         0.0757          1.2474
raceblack    0.3730          1.0259
racehispan   0.1568          1.0743
racewhite    0.2162          0.8390
married      0.0216          0.8281
nodegree     0.0703          1.0106
re74         0.2757          0.7965
re75         0.2054          0.7381

Sample Sizes:
          Control Treated
All           429     185
Matched       185     185
Unmatched     244       0
Discarded       0       0

jstable::CreateTableOneJS

jstable::CreateTableOneJS(vars = names(m.out$X), strata = "treat", data = lalonde, smd = T)$table %>% kable
level 0 1 p test SMD sig
n 429 185 NA
age 28.03 ± 10.79 25.82 ± 7.16 0.003 0.242 **
educ 10.24 ± 2.86 10.35 ± 2.01 0.585 0.045
race (%) black 87 (20.3) 156 (84.3) <0.001 1.701 **
hispan 61 (14.2) 11 ( 5.9) NA
white 281 (65.5) 18 ( 9.7) NA
married 0.51 ± 0.50 0.19 ± 0.39 <0.001 0.719 **
nodegree 0.60 ± 0.49 0.71 ± 0.46 0.007 0.235 **
re74 5619.24 ± 6788.75 2095.57 ± 4886.62 <0.001 0.596 **
re75 2466.48 ± 3292.00 1532.06 ± 3219.25 0.001 0.287 **

Mathing data

jstable::CreateTableOneJS(vars = names(m.out$X), strata = "treat", data = mdata, smd = T)$table %>% kable
level 0 1 p test SMD sig
n 185 185 NA
age 25.30 ± 10.59 25.82 ± 7.16 0.585 0.057
educ 10.61 ± 2.66 10.35 ± 2.01 0.290 0.110
race (%) black 87 (47.0) 156 (84.3) <0.001 0.855 **
hispan 40 (21.6) 11 ( 5.9) NA
white 58 (31.4) 18 ( 9.7) NA
married 0.21 ± 0.41 0.19 ± 0.39 0.604 0.054
nodegree 0.64 ± 0.48 0.71 ± 0.46 0.151 0.150
re74 2342.11 ± 4238.98 2095.57 ± 4886.62 0.605 0.054
re75 1614.75 ± 2632.35 1532.06 ± 3219.25 0.787 0.028

Caliper

m.out.caliper <- matchit(treat ~ age + educ + race + married + 
                   nodegree + re74 + re75, data = lalonde, method = "nearest", ratio = 1, caliper = 0.1)
jstable::CreateTableOneJS(vars = names(m.out$X), strata = "treat", data = match.data(m.out.caliper), smd = T)$table %>% kable
level 0 1 p test SMD sig
n 111 111 NA
age 26.14 ± 10.88 25.95 ± 6.58 0.870 0.022
educ 10.49 ± 2.66 10.49 ± 2.04 1.000 <0.001
race (%) black 80 (72.1) 82 (73.9) 0.954 0.041
hispan 12 (10.8) 11 ( 9.9) NA
white 19 (17.1) 18 (16.2) NA
married 0.24 ± 0.43 0.21 ± 0.41 0.523 0.086
nodegree 0.61 ± 0.49 0.65 ± 0.48 0.580 0.074
re74 2408.56 ± 4436.77 2667.11 ± 5732.76 0.707 0.050
re75 1800.87 ± 2917.71 1811.30 ± 3699.16 0.981 0.003

IPTW: svyCreateTableOneJS

library(survey)
lalonde$wt <- ifelse(lalonde$treat == 1, 1/m.out$distance, 1/(1 - m.out$distance))
design.lalonde <- svydesign(ids=~1, strata=NULL, weights=~wt, data = lalonde)
jstable::svyCreateTableOneJS(vars = names(m.out$X), strata = "treat", data = design.lalonde, smd = T)$table %>% kable
level 0 1 p test SMD sig
n 616.00 553.63 NA
age 27.10 ± 10.80 25.57 ± 6.53 0.088 0.172
educ 10.29 ± 2.74 10.61 ± 2.05 0.303 0.132
race (%) black 245.1 (39.8) 247.9 (44.8) 0.695 0.112
hispan 72.1 (11.7) 67.4 (12.2) NA
white 298.8 (48.5) 238.3 (43.0) NA
married 0.41 ± 0.49 0.31 ± 0.47 0.228 0.197
nodegree 0.62 ± 0.48 0.57 ± 0.50 0.458 0.112
re74 4552.74 ± 6337.09 2932.18 ± 5709.42 0.040 0.269 **
re75 2172.04 ± 3160.14 1658.07 ± 3072.89 0.136 0.165

openstat.ai

3그룹 A vs B vs C

Matching: N수 제일 작은 그룹(A) 에 맞춰 여러번 매칭

  • MatchIt 그대로 쓸 수 있는건 장점
  • \(ATT\)도 아닌 \(ATT_{A}\)?

IPTW: twang::mnps

  • 시간이 오래걸림: 공단데이터에서 24시간 걸릴수도
  • \(ATE\) 가능, 더 추천

Issue 1: Matching 후 pair정보 이용

Matching 후 stratified cox를 해야하는가? Yes or No

cox1 <- coxph(Surv(time, status) ~ treat, data = mdata)

cox2 <- coxph(Surv(time, status) ~ treat + strata(subclass), data = mdata)


Pair 마다 baseline hazard가 다르다는 가정

  • RCT 재현 관점: 필요없음(pair정보 없음)

  • 매칭이 너무 잘 되어서 pair정보가 확실하다면?

  • Pair 들끼리 뭔가 다르다면?

  • OMOP-CDM ATLAS에서는 strata 사용하는게 Default.

번외: baseline hazard 보정방법

예) 다기관 RCT에서 hospital, stratified random

cox1 <- coxph(Surv(time, status) ~ treat + strata(hospital), data = mdata)

cox2 <- coxph(Surv(time, status) ~ treat + hospital, data = mdata)

cox3 <- coxme::coxme(Surv(time, status) ~ treat + (1|hospital), data = mdata)

cox4 <- coxph(Surv(time, status) ~ treat, data = mdata, cluster = hospital)


  • + hospital 도 가능, but 각 병원의 계수를 굳이 구할필요가?

  • coxme: mixed model의 Random effect, strata와 비슷한 의미

  • cluster = hospital: 같은 hospital 간에 상관관계가 있음을 보정하므로 strata와 비슷한 의미일 것 같지만, baseline hazard 이 다르다는 가정이 아님. cluster 옵션 전후 HR은 변하지 않음, SE만 변화.

Issue 2: Sex를 그룹변수로 Matching 가능?

Sex가 RCT 가능한가?

  • 불가능 vs 하늘(?) 이 준 랜덤

Matching: \(ATT\)

  • 남자 vs 남자같은 여자, 여자 vs 여자같은 남자

IPTW: \(ATE\) 니까 OK?

  • 전체 연구대상자(남자 + 여자) 를 남/여 1:1로 랜덤배정했다는 뜻

최신 Trial emulation 방법론


- Clone-Censor-Weight method
- Clone : eligibile한 모든 대상자를 clone -> Treatment, Control arm에 assign
- Censor : assign된 arm의 strategy에서 벗어나면, Protocol violation -> artificial censoring
- Weight : 관찰시간에 따라 artificial censoring되지 않고 남아있을 확률의 역수를 가중치로 부여

  • 연구 시작시점에 치료군과 대조군이 완전히 동일

Immortal time bias

  • When Time-zero (f/u start date) ≠ Treatment start date
  • Misclassification bias, Selection bias
  • Overestimation of treatment effect
  • Clone-Censor-Weight method를 통해 Intervention/약물 연구에서 발생하는 immortal time bias를 해소

Clone-Censor-Weight

CCW Model
  • A는 치료군, A의 clone인 A’은 대조군
  • A’은 A가 치료를 받은 시점에 artificial censoring
  • A’과 B’은 artificial censoring 되는 시점이 다르다
  • real-world RCT에서는 artificial censoring이 발생하지 않음
    • artificial censoring에 의해 관찰하지 못하는 event 발생을 보정
  • 시점별로 artificial censor(0/1) 될 확률을 logistic regression으로 추정
    • censor되지 않고 남아있을 확률이 0.2라면, 가중치 5를 곱하여 5명으로 간주 (IPCW)

Executive summary

Baseline 맞춘다(X), RCT를 모방한다(O)

  • ATE(average treatment effect) vs ATT(average treatment effect on treated)

  • Matching은 ATT, IPTW는 ATE

2그룹 MatchIt, 3그룹 twang 패키지

  • Logistic regression, Nearest neighbor, caliper 이해

  • 3그룹 matching은 가장 작은 N수에 맞춰 2번 수행

  • openstat.ai 에서 2그룹 matching/IPTW 지원

분석 이슈

  • Matching 후 pair정보 이용해야 하는가?(ex: stratified cox)

  • 성별에 따라 매칭/IPTW 해도 되는가?

감사합니다.