化合物の構造式を描きたい

という相談を受けた。
化合物なんてまったく扱ってないし構造式も大学入試以来描いたことがないが、こういうのはRで出来るだろうと思って調べたらあった。
ChemmineR: a compound mining framework for R | Bioinformatics | Oxford Academic
ChemmineR: Cheminformatics Toolkit for R
ChemmineRを使ってみよう【1】 - アメリエフの技術ブログ

SDFでファイルをもらったのでSDFでやってみる。
化合物の情報はpubchemというデータベースからいろいろ取ってこれるので、ここからフェンタニルを取ってきてプロットしてみる。
f:id:MikuHatsune:20210329122939p:plain

sdfset <- read.SDFset("https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/3345/record/SDF/?record_type=3d&response_type=display")
plot(sdfset, print_cid=sdfset@SDF[[1]]@header["Molecule_Name"])

メタアナリシスで、サンプルサイズが最も大きいわけではないのに、weightが最大になるのはおかしくないですか?

という質問を受けた。
結論から言うとおかしくない。メタアナリシスのweight w_iは各研究内の分散v_i と研究間の分散\tauにより
w_i=\frac{1}{v_i}(fixed model の場合)もしくはw_i=\frac{1}{v_i+\tau}(random effect model の場合)
で決まるから、分散が小さい、すなわち推定精度の高い研究はweight が大きくなる。サンプルサイズが大きいと推定精度が増すので、サンプルサイズが大きいことがweight を大きくする要因だが、分散がそもそも小さいことも重要である。

とあるメタアナリシスで、サンプルサイズが114+115のKienast という研究(29%)より、サンプルサイズが33+23のBaudoという研究(57%)のほうがweight が大きい。
f:id:MikuHatsune:20210318132522p:plain

Baudo の研究だけ取り出してみると、分割表は
\begin{matrix}23&10\\20&3\end{matrix}
であるが、周辺度数を固定したときに、この分割表の取りうる運命は、\Delta を変動させて
\begin{matrix}23+\Delta&10-\Delta\\20-\Delta&3+\Delta\end{matrix}
となる。
\Deltaを変動させて、そのときの分割表のMH検定を行い、信頼区間の幅を見てみると、\Delta=-2のときに信頼区間の幅が最小になる。すなわち、推定の精度がよいことになる。
f:id:MikuHatsune:20210318132535p:plain

すべての\Delta の取りうる範囲が、各々メタアナリシスに組み入れられた研究だと仮定して、それら全体をメタアナリシスしてみる。確かに、信頼区間の幅の狭い\Delta=2などの研究が、weight が大きい、ということになっている。
f:id:MikuHatsune:20210318132730p:plain

library(meta)
library(epiR)
library(latex2exp)
# 元のデータ
dat <- cbind.data.frame(
   author=c("Baudo", "Fourrier", "Gando", "Inthom", "Kienast"),
   year=c(1998, 1993, 2013, 1997, 2006),
   ev.exp=c(23, 4, 3, 3, 29),
   n.exp=c(33, 17, 30, 7, 114),
   ev.cont=c(20, 9, 4, 7, 46),
   n.cont=c(23, 18, 30 ,7, 115)
   )

cols <- replace(rep("black", nrow(dat)), dat$author=="Baudo", "blue")

m <- metabin(ev.exp, n.exp, ev.cont, n.cont,
             method="Inverse",
             data=dat, studlab = paste(author, year),
             label.e="Antithrombin", label.c="Control",
             label.right="Favours Control", col.label.right="red",
             label.left = "Favours Antithrombin", col.label.left="blue")

forest(m, col.study=cols, layout="RevMan5", comb.fixed=FALSE)

# Gando に着目する
i <- 1
b <- rbind(c(dat[i, "ev.exp"], dat[i, "n.exp"]-dat[i, "ev.exp"]),
           c(dat[i, "ev.cont"], dat[i, "n.cont"]-dat[i, "ev.cont"]))

# MH test
A <- matrix(c(1, -1, -1, 1), 2)
r <- epi.2by2(b, method="cohort.count")
r$massoc$RR.strata.taylor

# Delta をすべて動かして検定する
x <- (-sort(b)[1]):(sort(b)[2])
bs <- mapply(function(z) b + A*z, x, SIMPLIFY=F)
r <- mapply(function(z) try(unlist(epi.2by2(z)$massoc$RR.strata.taylor)), bs, SIMPLIFY=FALSE)
g <- t(mapply(function(z) switch(any((is(z)%in%"try-error"))+1, z, rep(NA, 3)), r))
g <- matrix(unlist(g), nrow(g))
colnames(g) <- c("RR", "CI lower", "CI upper")

cols <- c("black", "blue", "red")
txt <- mapply(function(z, w) substitute(x~y~Delta, list(x=z, y=w)), c(b), c("+", "-", "-", "+"))
matplot(x, g, type="o", pch=15, lty=1, xlab=expression(Delta), ylab="RR", col=cols, lwd=3, log="y")
legend("topleft", legend=colnames(g), col=cols, lty=1, lwd=3)
legend("bottomright", legend=sapply(txt, as.expression), ncol=2)

# すべてのDelta に対応する分割表が各研究だとみなしてメタアナリシスする
bs1 <- sapply(bs, c)
bs0 <- cbind(bs1[1,], bs1[1,]+bs1[3,], bs1[2,], bs1[2,]+bs1[4,])
colnames(bs0) <- tail(colnames(dat), 4)
bs0 <- cbind.data.frame(year=x, bs0)
m <- metabin(ev.exp, n.exp, ev.cont, n.cont,
             method="Inverse",
             data=bs0, studlab=year,
             label.e="Antithrombin", label.c="Control",
             label.right="Favours Control", col.label.right="red",
             label.left = "Favours Antithrombin", col.label.left="blue",
             )

forest(m, layout="RevMan5", comb.fixed=FALSE)

エビデンスはあるのか、というとほとんどないに等しい

読んだ。

COI:とあるポイントが期限が迫っていたので新品で買うはめになった

3000円くらいでB5版200ページもないので2時間もあれば読める。
エビデンスに基づくと自信をもって言えますか?と帯で煽っておきながら、エビデンスに基づいたことはあまり記載されておらず、自分が想定していたエビデンスとは違った。
positive list とnegative list に分けて記載しているが、自分が想定していたのは、「○○という薬剤or手技は予後に良いor悪いことがエビデンスとしてあるので、やる(やらない)べき」みたいなことを想定していたが、実際に書かれていたのは(例:2章の血管収縮薬)
positive list
血管収縮薬は臓器灌流を増加させる (そんな一般的な概念を語られても…)
血管収縮薬の違いによる死亡率に差はない (こういう話を読みたかった)
血管収縮薬は心拍出量を増加させる(前負荷増加) (と書いておきながら)

negative list
血管収縮薬は心拍出量を減少させる(後負荷増加) (機序の違いがあるのは分かるけど前項と言ってることが矛盾する。結局臨床的にどっちかになるのでは?)
vasoplegic shock には臨床背景の異なった検討が多く、まだ明確な定義はない (ほとんどの項でこうなる)
人工心肺後のvasoplegic shock時の血管収縮薬選択のエビデンスはほぼない (ほとんどの項でこうなる)

と、当たり障りのないことに終始してて読み応えはなかった。
面白かった章は後半のエキスパートオピニオンの項で、胸部大動脈瘤の手術でひだり上肢に取った静脈路が、無名静脈損傷されたためまったく役に立たなかった、とか、エホバの無輸血手術が予後がどうなったか、とかが興味深かった。
CPBで血圧50維持が好きな外科医がいるがINVOS 40台で(これは脳みそイッたな…)と思ってても意外と脳機能障害がなかったり、レジデントにカテコラミンルートフラッシュされてMVPなのに血圧200になって(これは心臓イッたな…)と思っても心臓破裂しなかったりと、よくわからない。

ACLS

読んだ。というかやってきた。

特に言うことはない。
迅速挿管、という訳があったがこんな語は存在しないのでは?
原文にあたってないがどう書いてあるのだろう。ちなみに原文は半額くらいだし楽天kobo電子書籍なら無料の場合もある。ただし中身が同一なのかは確認していない。

ACLSの資格は麻酔科学会および循環器内科学会の専門医認定要件である。循環器内科は期限内(2年間)であるのに対し、麻酔科学会は取得から5年間有効である。ゆるい。

ACLSを受けるにはBLSを1日潰して受けて、さらに2日間のACLS講習を受けなければならない、と思っていたら違ったようである。
そもそもACLSの受講要件にBLS認定は必要ない。
ACLS受講にBLS資格は不要です【アメリカ心臓協会】 | BLS横浜
ACLSコースにBLS資格は不要 - BLS大分で救急スキルを|受講申込

さらに上記団体が1日コースなるものを提供している。
AHA-ACLSvoC_[y1úzR[XóuÄàyBLS¡lz
<AHA公認)ACLS1日コース - BLS大分で救急スキルを|受講申込

横浜は都市部で人気があるのですぐ埋まってしまうので、専門医受験申請までに時間がない場合は注意が必要である。
BLSやってACLS2日コースをやる場合、全体で3日間も浪費するのはどう考えても無駄なので遠方だとしても1日コースでがんばったほうがコスパがよい。
テキストを通読しておくのは当然であるが、普通に業務をしていたら知っている内容やガイドラインの範囲であるし、通常2日コースで学ぶ心電図や生理学は、AHAのサイトから事前学習としてプレテストがあるが、これも普通に麻酔科もしくは循環器内科をやっていたら分かる内容(不整脈読影とシナリオに基づいた投薬など)なので、専門医の要件を満たすだけなら1日コースで十分である。

受講に4万かかったわけだが、これを専門医要件にしたやつ、キックバックもらってるに違いない(個人の感想です

かかった費用はこれに比べたらまだマシ。これは専門医要件ですらない。
mikuhatsune.hatenadiary.com

判別分析の判別線がおかしい

という相談を受けた。
解析したいデータは2変数なので、例として
判別分析モデルの応用
がついてきたが、3変数の場合はirisを用いて
データ解析・マイニングとR言語
もついてきて、こんな感じでやりたい、と言われた。

0/1のtype は以下の線形モデルで表現されて
Type = \alpha + \beta_xX+\beta_yY
判別線は
Y=-(\alpha + \beta_xX)/\beta_y
となる。

しかし、リンクの通りに\alphaを計算すると、判別線より上か下かを決めるスコア(正負で上下が決まる)と、判別線との関係がなにかおかしい、というのが問題のようである。
図で言うと、青の判別線の下にある8個の☓がおかしい点である。
f:id:MikuHatsune:20210212164704p:plain

結論から言うと

# 以上の手順は1回で済ますこともできる
apply(disc.model$means %*% disc.model$scaling, 2, mean)

の部分が平均を取っており、リンクの例題なら2カテゴリーが1:1 で作成されたので偶然結果が一致するが、今回のデータは0/1 が偏っているので、その偏りに応じた重み付けが必要になる。
これはiris を使った2番目のリンクの例題でも、1:1:1 になって平均を取って偶然一致するので、わかりにくい。

alpha <- c(disc.model$prior %*% disc.model$means %*% disc.model$scaling)

とすればデータの事前確率に応じた重み付けが取れる(行列演算なので返り値がベクトルになるようにcをかませている)。

緑の判別線ならうまくいったようである。
f:id:MikuHatsune:20210212165254p:plain

txt <- "
type X Y
0 0 107
0 0 109
0 0 97
0 0 75
0 0 101
0 0 92
0 0 88
0 0 99
0 0 89
0 22 96
0 37 110
0 0 90
0 0 101
0 92 68
0 51 99
0 26 105
0 0 92
0 0 81
0 0 95
0 0 96
0 18 88
0 0 89
0 0 94
0 0 92
0 0 90
0 0 109
0 0 88
0 0 84
0 0 108
0 0 92
1 0 75
1 47 61
1 29 73
1 96 76
1 46 85
1 103 72
1 68 60
1 0 78
1 0 52
1 0 84
1 0 64
1 64 85
1 72 89
1 50 65
1 50 75
1 116 87
1 79 80
1 130 77
1 48 82
1 40 86
1 140 71
1 34 77
1 109 95
1 208 67
1 85 81
1 0 62
1 91 64
1 106 95
1 27 73
1 36 61
1 28 77
1 53 94
1 113 94
1 59 77
1 38 63
1 0 75
1 78 60
1 321 68
1 0 55
1 81 54
1 77 87
1 49 57
1 131 69
1 100 54
1 149 51
1 59 65
1 83 43
1 39 56
1 176 47
1 122 60
1 172 78
1 123 46
1 164 54
1 42 64
1 86 60
1 31 75
1 27 73
1 64 57
1 124 72
1 59 62
1 0 84
1 18 66
1 103 92
1 0 76
1 22 44
1 125 54
1 197 71
"

dat <- read.table(text=txt, header=TRUE)

library(MASS)
disc.model <- lda(type ~ Y + X, data=dat)
p <- predict(disc.model)
pp <- p$posterior
p2 <- function(x) ifelse(x < 1, 1/x, x)

TF <- p$class == dat$type

group.mean <- disc.model$mean
# 線形係数のデータ行列をcoefに代入
coef <- disc.model$scaling
# 行列のかけ算は%*%で行える
# その結果をaに代入
a <- group.mean %*% coef
# 行列aの列平均を計算する
# 引数に2ではなく1を指定すれば行に対して計算される
# 仮にmeanをsumにすると、平均ではなく合計が計算される
apply(a, 2, mean)
# 以上の手順は1回で済ますこともできる
# web のやつ
alpha1 <- apply(disc.model$means %*% disc.model$scaling, 2, mean)
# データの事前確率を反映させた正しいもの
alpha2 <- c(disc.model$prior %*% disc.model$means %*% disc.model$scaling)

# 判別線
f2 <- function(x, alpha) -(-alpha + disc.model$scaling["X", "LD1"]*x)/disc.model$scaling["Y", "LD1"]
x <- seq(0, 500, length=1000)

plot(Y ~ X, data=dat, col=dat$type+1, pch=ifelse(p$x < 0, 4, 16))
legend("topright", legend=sprintf("type %d", 0:1), pch=16, col=1:2)
lines(x, f2(x, alpha1), lty=3, col=4, lwd=3)


plot(Y ~ X, data=dat, col=dat$type+1, pch=ifelse(p$x < 0, 4, 16))
lines(x, f2(x, alpha1), lty=3, col=4, lwd=3)
lines(x, f2(x, alpha2), lty=3, col=3, lwd=3)
legend("topright", legend=sprintf("type %d", 0:1), pch=16, col=1:2)
legend("bottomright", legend=c("正", "負"), pch=c(4, 16), col=1:2)


plot(Y ~ X, data=dat, col=dat$type+1, pch=c(4, 16)[TF+1], cex=log10(p2(pp[,1]/pp[,2]))+1)
legend("topright", legend=sprintf("type %d", 0:1), pch=16, col=1:2)
lines(x, f2(x, alpha2), lty=3, col=4, lwd=3)

Basics of Anesthesia

輪読回で後半から参加して読んだ。

Basics of Anesthesia

Basics of Anesthesia

アメリカの制度の話にはなるが、救急やICU、オペ室の運営の仕方や労働裁量の話などがあって興味深かった。

新型肺炎の年齢階級別死亡率

新型コロナウイルス 国内感染の状況
こちらの2021年1月13日時点の各年代別感染者数と死亡者数のデータがあったので単純に推定してみる。

その1
drcパッケージにあるLL.4関数は、4パラメータモデルのロジスティック曲線に当てはめる。
これで簡単にやる

その2
rstan を使う。年齢を経るごとに死亡率が上がるとして、orderedで昇順の値のサンプリングをして、inv_logitにより0~1 の値になるようにする。すなわちこれが死亡率になり、やっていることはロジスティック曲線への変換である。

死亡率は以下のようになる(%)

      age             
 [1,]   5  0.007447185
 [2,]  15  0.007450361
 [3,]  25  0.007735825
 [4,]  35  0.013074665
 [5,]  45  0.059155769
 [6,]  55  0.310493872
 [7,]  65  1.314663597
 [8,]  75  4.443249524
 [9,]  85 12.025304864

f:id:MikuHatsune:20210118232728p:plain

rstan でやるよりかは、推定曲線へのフィッティングのほうが、predict関数でinterval="confidence"を指定すると区間推定値が出るので有用だと思う。
例えば100歳での死亡率は95%信頼区間

     Lower Prediction      Upper 
  30.62692   33.85670   37.08648 

となる。

# 1/13
death <- cbind(c(0, 0, 2, 11, 35, 113, 328, 944, 2401),
               c(7160, 18377, 69311, 46455, 42939, 39148, 24823, 21243, 19967))
rownames(death) <- c("10代以下", sprintf("%d代", seq(10, 70, by=10)), "80代以上")

age <- seq(5, 85, by=10)
p <- death[,1]/death[,2]



library(vioplot)
library(drc)
library(rstan)
rstan_options(auto_write = TRUE)
options(mc.cores = parallel::detectCores())

code <- "
data{
  int N;
  int death[N];
  int positive[N];
}
parameters{
  ordered[N] theta;
}
model{
  for(i in 1:N){
    death[i] ~ binomial(positive[i], inv_logit(theta[i]));
  }
}
"

m0 <- stan_model(model_code=code)
standata <- list(N=nrow(death), death=death[,1], positive=death[,2])
fit <- sampling(m0, data=standata, iter=3000, warmup=1500, seed=1234)

ex <- extract(fit)
r <- 1/(1+exp(-ex$theta))

d <- drm(p ~ age, fct=LL.4())
x <- seq(0, 100, by=1)
y <- predict(d, as.data.frame(x), interval="confidence")

xl <- range(x)
yl <- range(0, r, y)

plot(d, log="", xlim=xl, ylim=yl, xlab="年齢", ylab="陽性者死亡率", bty="n", axes=FALSE)
lines(x, y[,2], col=2)
lines(x, y[,3], col=2)
vioplot(r, add=TRUE, at=age, colMed="yellow", wex=5)
axis(1, lwd=0, lwd.ticks=1, at=age, labels=age)
axis(2, lwd=0, lwd.ticks=1)