図2:propensity score のずれた分布

# 適当なPS分布の作成
s1 <- seq(0, 10, length=100)  # 0 から 10 まで長さ100 の規則的な数列を作成する
ino1 <- dnorm(s1, 6, 1.2) # 平均 6,分散 1.2 の正規分布から,s1 に対応する確率密度を計算する
ino2 <- dnorm(s1, 4, 1.4) # 平均 4,分散 1.4 の正規分布から,s1 に対応する確率密度を計算する

xl <- c(-max(ino2), max(ino1)) # x 軸のプロットする範囲を決める
yl <- c(0, 1)                  # y 軸のプロットする範囲を決める

# プロットする
# x軸みぎ側に強心剤を使用した患者の傾向スコア
# x軸ひだり側に強心剤を使用しなかった患者の傾向スコア
# y軸に確率 [0, 1]
# がくるようにプロットしたい
y <- seq(0, 1, length=length(s1)) # 0 から 1 まで長さがs1 と同じな規則的な数列を作成する
par(mar=c(5, 4.5, 2, 2))
plot(0, type="n", xlim=xl, ylim=yl, xlab="", ylab="Propensity score (PS)", cex.lab=1.6, cex.axis=1.6, las=1)
abline(v=0, lty=2)
lines(ino1, y, type="l", col="red", lwd=5)    # 強心剤が投与された患者の傾向スコア
lines(-ino2, y, type="l", col="blue", lwd=5)  # 強心剤が投与されなかった患者の傾向スコア
text(mean(c(0, par()$usr[1])), par()$usr[3], "No Inotrope", xpd=TRUE, adj=c(NA, 4), cex=1.5)
text(mean(c(0, par()$usr[2])), par()$usr[3], "Inotrope", xpd=TRUE, adj=c(NA, 4), cex=1.5)

図3:propensity score が一致した集団の分布

minPS <- apply(cbind(ino1, ino2), 1, min)
par(mar=c(5, 4.5, 2, 2))
plot(0, type="n", xlim=xl, ylim=yl, xlab="", ylab="Propensity score (PS)", cex.lab=1.6, cex.axis=1.6, las=1)
polygon(c(minPS, rev(-minPS)), c(y, rev(y)), border=NA, col=grey(0.85)) # 傾向スコアが似ている領域を塗りつぶす
abline(v=0, lty=2)
y <- seq(0, 1, length=length(s1))
lines(ino1, y, type="l", col="red", lwd=3, lty=3)
lines(-ino2, y, type="l", col="blue", lwd=3, lty=3)
text(mean(c(0, par()$usr[1])), par()$usr[3], "No Inotrope", xpd=TRUE, adj=c(NA, 4), cex=1.5)
text(mean(c(0, par()$usr[2])), par()$usr[3], "Inotrope", xpd=TRUE, adj=c(NA, 4), cex=1.5)
lines(minPS, y, type="l", col="red", lwd=5)
lines(-minPS, y, type="l", col="blue", lwd=5)

対応のある検定を対応のない検定としてしまったときの\(p\) 値の齟齬

一般的な帰無仮説検定で,帰無仮説\(H_0:\mu_1=\mu_2\) と対立仮説\(H_1:\mu_1\not=\mu_2\) として,帰無仮説が偽である(差があることが確実に分かっている状態)のみを原稿では提示していますが,帰無仮説が真(差がないことが確実に分かっている状態)もシミュレーションします.

帰無仮説が偽のときは,サンプルサイズが大きくなるほど,対応のないt検定が小さなp値を返す確率が大きく,結果として有意な結果として報告されやすくなります.

逆に,帰無仮説が真のときは,サンプルサイズが小さくなるほど,対応のないt検定が小さなp値を返す確率が大きく,サンプルサイズが大きくなるほど,対応のないt検定と対応のあるt検定のp値の差は半々になります.ただし,帰無仮説が真であるので,たとえ差がないとしても,偶然に有意な結果になる確率が\(p=0.05\) あります.

rhc データセットを使って傾向スコアマッチングを行います.

library(tableone) # tableone package itself
library(Matching) # PS matching
library(mice)     # 欠損値の補完

# データの読み込み
url <- "http://biostat.mc.vanderbilt.edu/wiki/pub/Main/DataSets/rhc.csv"
rhc <- read.csv(url)              # ネットにつながっているなら
#rhc <- read.csv("rhc.csv", row=1)  # PC に保存している場合

データ総数は

nrow(rhc)
[1] 5735

RHC の割付は

table(rhc$swang1)

No RHC    RHC 
  3551   2184 

と,論文の数値と一致する.

例えば,年齢別のRHC 分布は以下の通りである.

table(cut(rhc$age, c(0, 50, 60, 70, 80, 120), right=FALSE), rhc$swang1)
          
           No RHC RHC
  [0,50)      884 540
  [50,60)     546 371
  [60,70)     812 577
  [70,80)     809 529
  [80,120)    500 167

例えば,性別によるのRHC 分布は以下の通りである.

table(rhc$sex, rhc$swang1)
        
         No RHC  RHC
  Female   1637  906
  Male     1914 1278

主要評価項目である30日死亡(dth30)は一致する.

table(rhc$dth30, rhc$swang1) # 30日死亡
     
      No RHC  RHC
  No    2463 1354
  Yes   1088  830

180日死亡(death)は一致しない.

table(rhc$death, rhc$swang1) # 180日死亡 合わない
     
      No RHC  RHC
  No    1315  698
  Yes   2236 1486

Propensity score の項に,傾向スコアの計算に使った変数が書かれているが,primary disease category の変数と論文のTable 1 の総数が一致しないので適宜解析しやすいように編集する.

vars <- c(
  "age", "sex", "race", "edu", "income", "ninsclas", "cat1",
  "resp", "card", "neuro", "gastr", "renal", "meta", "hema", "seps", "trauma", "ortho", 
  "adld3p", "das2d3pc", "dnr1", "ca", "surv2md1", "aps1", "scoma1", "wtkilo1",
  "temp1", "meanbp1", "resp1", "hrt1", "pafi1", "paco21", "ph1", "wblc1", "hema1", "sod1", "pot1", "crea1", "bili1", "alb1", "urin1", 
  "cardiohx", "chfhx", "dementhx", "psychhx", "chrpulhx", "renalhx", "liverhx", "gibledhx", "malighx", "immunhx", "transhx", "amihx"
  )

# データの整形
rhc1 <- rhc[, c("swang1", vars)]
cat1 <- as.character(rhc1$cat1)
cat1[cat1 == "MOSF w/Malignancy"] <- "MOSF"
cat1[cat1 == "MOSF w/Sepsis"] <- "MOSF"
cat1[!is.na(match(cat1, c("COPD", "Cirrhosis", "Colon Cancer", "Coma" ,"Lung Cancer")))] <- "Other"
rhc1$cat1 <- factor(cat1)

# 傾向スコアを計算するために回帰モデルを作成する
psModel <- glm(swang1 ~ ., data=rhc1, family=binomial(link="logit"))

# urin1 やdas2d3pc に欠損値があるが論文ではまったく触れられていないので
# 適当に欠損値補完を行うことにする
imp <- complete(mice(rhc1, method="rf", printFlag=FALSE))
pRHC <- predict.glm(psModel, newdata=imp, type = "response") # 欠損値補完したうえでの,各患者の傾向スコア

# マッチングを行う
Tr <- (rhc$swang1 == "RHC")
X <- log(pRHC / (1-pRHC))
listMatch <- Match(Tr       = Tr,      # Need to be in 0,1
                   ## logit of PS,i.e., log(PS/(1-PS)) as matching scale
                   X        = X,
                   ## 1:1 matching
                   M        = 1,
                   ## caliper = 0.2 * SD(logit(PS))
                   caliper  = 0.2,
                   replace  = FALSE,
                   ties     = TRUE,
                   version  = "fast")
idx <- c(listMatch$index.treated, listMatch$index.control) # マッチングした患者ペアのindex

rhc2 <- rhc1
rhc2 <- imp
rhc2$swang1 <- c(rhc2$swang1) - 1
mb <- MatchBalance(swang1 ~ ., data=rhc2, digit=5, match.out=listMatch, nboots=100, print.level=0)

傾向スコアの分布を確認する.

d <- tapply(pRHC, rhc$swang1, density)
Y <- do.call(cbind, sapply(d, "[")["y",])
Y <- sweep(Y, 2, table(rhc$swang1)/nrow(rhc), "*")
xl <- c(-max(Y[,2]), max(Y[,1]))
xl <- c(-1, 1) * max(abs(Y))
yl <- c(0, 1)
y <- seq(0, 1, length=nrow(Y))


d1 <- tapply(pRHC[idx], rhc$swang1[idx], density)
Y1 <- do.call(cbind, sapply(d1, "[")["y",])
Y2 <- sweep(Y1, 2, table(rhc$swang1[idx])/table(rhc$swang1), "*")
Y2 <- sweep(Y1, 2, table(rhc$swang1)/nrow(rhc)*table(rhc$swang1[idx])/table(rhc$swang1), "*")

minPS <- apply(Y, 1, min)
par(mar=c(5, 4.5, 2, 2))
plot(0, type="n", xlim=xl, ylim=yl, xlab="", ylab="Propensity score (PS)", cex.lab=1.6, cex.axis=1.6, las=1)
polygon(c(Y2[,2], rev(-Y2[,1])), c(y, rev(y)), border=NA, col=grey(0.85))
abline(v=0, lty=2)
points(Y[,2], y, type="l", col="red", lwd=1.5, lty=3)
points(-Y[,1], y, type="l", col="blue", lwd=1.5, lty=3)
points(Y2[,2], y, type="l", col="red", lwd=3, lty=1)
points(-Y2[,1], y, type="l", col="blue", lwd=3, lty=1)
text(mean(c(0, par()$usr[1])), par()$usr[3], levels(rhc$swang1)[1], xpd=TRUE, adj=c(NA, 4), cex=1.5)
text(mean(c(0, par()$usr[2])), par()$usr[3], levels(rhc$swang1)[2], xpd=TRUE, adj=c(NA, 4), cex=1.5)

マッチング前後の偏りを検討する.

tableone パッケージを使うと,検定によるp値もSMD も計算できる.

SMD では一部 10% 以下にならない変数も存在する.

# 2群の偏りを調べる
# マッチング前
Tab1 <- CreateTableOne(vars = vars, strata = "swang1", data = imp, test = TRUE)
# マッチング後
Tab2 <- CreateTableOne(vars = vars, strata = "swang1", data = imp[idx,], test = TRUE)

smd <- do.call(cbind, lapply(list(Tab1, Tab2), ExtractSmd))
cols <- c("black", "red")
par(mar=c(5, 8, 2, 2))
plot(0, xlim=range(0, smd), ylim=c(nrow(smd)-1, 2), type="n", yaxt="n",
     xlab="Standardized mean difference", ylab="")
abline(v=0.1, lty=3)  # SMD 10% の線
for(i in seq(nrow(smd))){
  abline(h=i, lty=3)
  points(smd[i,], rep(i, 2), pch=16, col=cols)
  text(par()$usr[1], i, rownames(smd)[i], xpd=TRUE, pos=2)
}

生存解析を行う.

#Sadmdte
#Study Admission Date
#
#Dthdte
#Date of Death
#
#Lstctdte
#Date of Last Contact
#
#Dschdte
#Hospital Discharge Date
#
#Death
#Death at any time up to 180 Days

library(survival)
d0 <- rhc$lstctdte-rhc$sadmdte
d1 <- rhc$dthdte-rhc$sadmdte
interval <- apply(cbind(d0, d1), 1, max, na.rm=TRUE)
event <- (!is.na(d1)) + 0

# 30日死亡のKM
sdat <- cbind.data.frame(days=replace(d0, d0 > 30, 30), event=(d0 < 30) + 0, swang1=rhc$swang1)
sfit <- survfit(Surv(days, event) ~ swang1, data=sdat)

par(mar=c(4.5, 5, 2, 2), cex.lab=1.5, cex.axis=1.5, las=1)
cols <- c("red", "blue")
plot(sfit, ylim=c(0.6, 1), col=cols, lwd=5,
     xlab="Follow-up Time [day]", ylab="Proportion Surviving")
legend("topright", legend=levels(sdat$swang1), col=cols, lwd=5, cex=2)

マッチング後の生存曲線.

# マッチング後
idx <- c(listMatch$index.treated, listMatch$index.control)
mdat <- sdat[idx,]
mfit <- survfit(Surv(days, event) ~ swang1, data=mdat)

par(mar=c(4.5, 5, 2, 2), cex.lab=1.5, cex.axis=1.5, las=1)
cols <- c("red", "blue")
plot(mfit, ylim=c(0.6, 1), col=cols, lwd=5,
     xlab="Follow-up Time [day]", ylab="Proportion Surviving")
legend("topright", legend=levels(sdat$swang1), col=cols, lwd=5, cex=2)

ログランク検定は下記で行える.

# source("http://aoki2.si.gunma-u.ac.jp/R/src/logrank.R", encoding="euc-jp") 
# ログランク検定を行う
logrank <- function(    group,                  # 群を識別するベクトル(1, 2 のいずれか)
            event,                  # 死亡なら 1,生存なら 0 の値をとるベクトル
            time,                   # 生存期間ベクトル
            method=c("SAS", "Tominaga"))        # 一般的な SAS などの方法か,富永の方法か
{
    method <- match.arg(method)
    data.name <- sprintf("time: %s, event: %s, group: %s",
        deparse(substitute(time)), deparse(substitute(event)), deparse(substitute(group)))
    OK <- complete.cases(group, event, time)        # 欠損値を持つケースを除く
    group <- group[OK]
    event <- event[OK]
    time <- time[OK]
    len <- length(group)
    stopifnot(length(event) == len, length(time) == len)
    tg <- table(c(time, rep(NA, 4)),            # 後ろの4項目はダミー
            c(group, 1, 1, 2, 2)*10+c(event, 1, 0, 1, 0))
    k <- nrow(tg)
    nia <- table(group)[1]
    nib <- len-nia
    na <- c(nia, (rep(nia, k)-cumsum(tg[,1]+tg[,2]))[-k])
    nb <- c(nib, (rep(nib, k)-cumsum(tg[,3]+tg[,4]))[-k])
    da <- tg[,2]
    db <- tg[,4]
    dt <- da+db
    nt <- na+nb
    d <- dt/nt
    O <- c(sum(da), sum(db))
    ea <- na*d
    eb <- nb*d
    E <- c(sum(ea), sum(eb))

    result <- data.frame(da, db, dt, na, nb, nt, d, ea, eb)

    if (method == "Tominaga") {             # 富永による検定方式
        method <- "ログランク検定(富永)"
        chi <- sum((O-E)^2/E)
    }
    else {                          # SAS 等と同じ検定方式
        method <- "ログランク検定(一般的)"
        v <- sum(dt*(nt-dt)/(nt-1)*na/nt*(1-na/nt), na.rm=TRUE)
        chi <- (sum(da)-sum(na*d))^2/v
    }
    P <- pchisq(chi, 1, lower.tail=FALSE)
    return(structure(list(statistic=c("X-squared"=chi), parameter=c(df=1), p.value=P,
    method=method, data.name=data.name, result=result), class="htest"))
}