急速導入のセリック手技はしなくてもいいのではないか

読んだ。
Effect of Cricoid Pressure Compared With a Sham Procedure in the Rapid Sequence Induction of Anesthesia: The IRIS Randomized Clinical Trial.
JAMA Surg. 2018 Oct 17. doi: 10.1001/jamasurg.2018.3577.
緊急手術時の急速導入で、セリック手技するかしない(Shamという)で、挿管時の嘔吐リスクがどうなのかを非劣性試験で試したもの。
結論から言うとSham の非劣性は示せなかったので、この記事のタイトルだけを読んで「セリック手技しません」というのはやめておいたほうがいい。
 
ASAPS2 程度までの、なんのリスクのない虫垂炎や胆嚢炎は任されるようになったが、それでも導入時はフルストマックなので嘔吐、誤嚥のリスクはつきまとう。
手術場で嘔吐されたことはないが、研修医のときにERで若い男性のイレウスに胃管をいれたらドボドボ吐かれてクソ日和ったことと、バイト先で朝起きてこない高齢女性が息も絶え絶えでSpO2 80台で搬送されてきて、挿管しようと口を見たら真っ青で、挿管時にこれまたドボドボと青い液体を吐かれて、吸引してもズルズル青い液体が引けて犯人はロヒプノールでした、ということはあった。
というわけで、この論文では挿管時の嘔吐発生事前確率を2.8% に設定しているが、根拠とした論文は手術場以外のすべての挿管状況を含んでいるので、けっこう高い印象である。セリック手技は誤嚥を防ぐためにしたほうがいいと45年前に言われたらしいが、それを支持する強固な証拠というのがあるのかというとそうでもないらしい。成書には「してもしなくても…」とは書いてあったのをみたことはある。でも書かれている以上、やっておくのは無難だと思われる。
 
試験は有意水準0.05、検出力0.8で、両群それぞれ1700例程度が必要となった。primary outcome は挿管直後の誤嚥で、セリック手技では10/1784 (0.6%), Sham では9/1736 (0.5%) 例の誤嚥が生じて、Sham でも全然いけるやん、と思ったらRR は最大で2 になるため、非劣性は示せなかった。というのも、本文でリスクが95%CI 上限で1.5 を超えないときに、非劣性を示す、と定義したので、非劣性かどうかでいうと、統計学的には非劣性を示せなかったようである。これは事前確率を2.8% と設定したものの、実際に手術室で用意万端(プロポフォールと、ほとんどがサクシニルコリンで導入)の状態で挿管するので、誤嚥の発生は高く見積もり過ぎてた、ということらしい。
誤嚥するときの事前確率が0.6% だった場合に、非劣性を示すのにどれくらいの症例数が必要だったかというと、もともとの2.8% のときは1.5 倍(4.2%)までを許容する、と考えればこんな感じになる。ずれてはいるが2100人くらいは各群に必要、つまり4200人はリクルートされないといけない。

power.prop.test(p1=0.028, p2=0.028*1.5, sig.level=0.05, power=0.8, alternative="one.sided")
     Two-sample comparison of proportions power calculation 

              n = 2129.728
             p1 = 0.028
             p2 = 0.042
      sig.level = 0.05
          power = 0.8
    alternative = one.sided

NOTE: n is number in *each* group

 
0.6% の事前確率のときは、1.5倍して0.9% までを許容するので、差が小さいときには非常に大きなサンプルサイズを必要とする。各群で10000人は必要となる。

power.prop.test(p1=0.006, p2=0.006*1.5, sig.level=0.05, power=0.8, alternative="one.sided")
     Two-sample comparison of proportions power calculation 

              n = 10225.93
             p1 = 0.006
             p2 = 0.009
      sig.level = 0.05
          power = 0.8
    alternative = one.sided

NOTE: n is number in *each* group

 
Sham は非劣性だったけれども、secondary outcome としては、セリック手技したほうが挿管までに時間がかかる(27秒 vs 23秒)とか、結局セリック手技を中断するはめになって、セリック手技をやめたらCormack が改善した(62% vs 33%)とか、挿管すること自体に影響を与えるようである。初回挿管成功率はどちらの群も92% だった。
フランスの報告だが麻酔看護師が60% 程度挿管に携わっているようである。ほへぇ。
 
まともにセリック手技というものを教えてもらったことがないのであれだが、30N の圧力でthe first 3 fingers(親指、人差し指、中指のことでいいの?)を使ってC5 に向けて押す、ということらしい。論文では50ml シリンジを使って、40ml の空気を33ml にまで圧縮するくらいの押し具合がそうなるようで、これを使って教育したらしい。

ようやくShinyを使ってインタラクティブにパラメータをいじって遊べるシミュレーターをweb上にあげることができた

MikuHatsune2018-10-10

Shinyというhtml 上でいろいろパラメータをいじったものを即座に計算して描出するインタラクティブでなんかすごいアプリがあったのだが、rmarkdown と同様に手をつけることなく放置していた。
せっかちな人はこちら → https://yfujii08.shinyapps.io/pkpdsimulator/
一番出来のいいエクセルは → https://home.hiroshima-u.ac.jp/r-nacamura/
 
話は変わるのだが、手術の最中に鎮痛(痛み止め)としてフェンタニルという麻薬を使う。この薬は適当に投与していてもなんとかなるが、本当にいい感じに使いたかったら、痛みがとれる濃度に収めつつ、かつ、副作用として呼吸抑制(息をしてくれない)を起こさない濃度にしなければならない。
そして、手術の最中は適当に投与していてもいい(手術中は人工呼吸器という機械で息をしてくれているのは保証されているから)が、手術が終わったあとも、上記のいい感じの濃度に保って鎮痛を継続して図りたいのである。
ここで、濃度のシミュレーションアプリとして、FentaSim(無料)、iTIVA anesthesia(無料といいながらアプリ内課金でぜんぜん無料で使えない)、AnestAssist PK/PD(潔く有料)というものがいくつか出ているのだが、これらのアプリがやってくれているのは、結局、
・濃度シミュレーション
・UI として可視化
というわけである。濃度シミュレーションはAnesthesiology. 1990 Dec;73(6):1091-102.あたりから必要な微分方程式とパラメータを持ってきている。要は3コンパートメントモデルと効果部位を想定して、コンパートメントi からj への移動をk_{ij} と表記する。ここで効果部位はe, メイン(というか静脈注射されて薬が一気に流入してくるコンパートメントのことで、たいてい1番目)コンパートメントから他のコンパートメント以外への排泄を0 として、使われるパラメータはk_{10}, k_{12}, k_{13}, k_{21}, k_{31}, k_{e0} となる。
というわけでフェンタニルのパラメータを使って開始時と1時間のときに適当に投与した結果がこちら。

# patient setting
weight <- 50
height <- 160
gender <- "M"
LBM <- ifelse(gender == "M", 1.1*weight-128*(weight/height)^2,1.07*weight-148*(weight/height)^2)
timestep <- 120

# parameter setting
k_1N <- c(0.0827, 0.471, 0.225)
k_N1 <- c(0.102, 0.006)
k_e0 <- 0.114

k <- c(k_1N, k_N1, k_e0)/(60/timestep)
names(k) <- c(sprintf("k1%d", c(0, 2, 3)), sprintf("k%d1", 2:3), "ke0")
k1 <- k["k10"] + k["k12"] + k["k13"]

V1 <- 0.105*weight
CLs <- k[c("k10", "k12", "k13")]*(60/timestep)*V1
V2 <- CLs[2]/(k["k21"]*(60/timestep))
V3 <- CLs[3]/(k["k31"]*(60/timestep))
# 適当な投与
X[1, "Bolus"] <- 123000*(60/timestep)
X[60, "Bolus"] <- 100000*(60/timestep)

X <- rbind(0, X)

for(i in 1:(nrow(X)-1)){
  X$dA1_1[i+1] <- k["k21"]*X$A2[i]+k["k31"]*X$A3[i]-k1*X$A1[i]+X$Cont[i+1]/(60/timestep)
  X$dA2_1[i+1] <- k["k12"]*X$A1[i] - k["k21"]*X$A2[i]
  X$dA3_1[i+1] <- k["k13"]*X$A1[i] - k["k31"]*X$A3[i]
  X$dAe_1[i+1] <- k["ke0"]*(X$A1[i] - X$Ae[i])
  X$dA1_2[i+1] <- k["k21"]*(X$A2[i]+X$dA2_1[i+1]/2)+k["k31"]*(X$A3[i]+X$dA3_1[i+1]/2)-k1*(X$A1[i]+X$dA1_1[i+1]/2)+X$Cont[i+1]/(60/timestep)
  X$dA2_2[i+1] <- k["k12"]*(X$A1[i]+X$dA1_1[i+1]/2) - k["k21"]*(X$A2[i]+X$dA2_1[i+1]/2)
  X$dA3_2[i+1] <- k["k13"]*(X$A1[i]+X$dA1_1[i+1]/2) - k["k31"]*(X$A3[i]+X$dA3_1[i+1]/2)
  X$dAe_2[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_1[i+1]/2 - (X$Ae[i]+X$dAe_1[i+1]/2))
  X$dA1_3[i+1] <- k["k21"]*(X$A2[i]+X$dA2_2[i+1]/2)+k["k31"]*(X$A3[i]+X$dA3_2[i+1]/2)-k1*(X$A1[i]+X$dA1_2[i+1]/2)+X$Cont[i+1]/(60/timestep)
  X$dA2_3[i+1] <- k["k12"]*(X$A1[i]+X$dA1_2[i+1]/2) - k["k21"]*(X$A2[i]+X$dA2_2[i+1]/2)
  X$dA3_3[i+1] <- k["k13"]*(X$A1[i]+X$dA1_2[i+1]/2) - k["k31"]*(X$A3[i]+X$dA3_2[i+1]/2)
  X$dAe_3[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_2[i+1]/2 - (X$Ae[i]+X$dAe_2[i+1]/2))
  X$dA1_4[i+1] <- k["k21"]*(X$A2[i]+X$dA2_3[i+1])+k["k31"]*(X$A3[i]+X$dA3_3[i+1])-k1*(X$A1[i]+X$dA1_3[i+1])+X$Cont[i+1]/(60/timestep)
  X$dA2_4[i+1] <- k["k12"]*(X$A1[i]+X$dA1_3[i+1]) - k["k21"]*(X$A2[i]+X$dA2_3[i+1])
  X$dA3_4[i+1] <- k["k13"]*(X$A1[i]+X$dA1_3[i+1]) - k["k31"]*(X$A3[i]+X$dA3_3[i+1])
  X$dAe_4[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_3[i+1] - (X$Ae[i]+X$dAe_3[i+1]))
  X$A1[i+1]    <- X$A1[i]+(X$dA1_1[i+1]+2*X$dA1_2[i+1]+2*X$dA1_3[i+1]+X$dA1_4[i+1])/6+X$Bolus[i+1]/(60/timestep)
  X$A2[i+1]    <- X$A2[i]+(X$dA2_1[i+1]+2*X$dA2_2[i+1]+2*X$dA2_3[i+1]+X$dA2_4[i+1])/6
  X$A3[i+1]    <- X$A3[i]+(X$dA3_1[i+1]+2*X$dA3_2[i+1]+2*X$dA3_3[i+1]+X$dA3_4[i+1])/6
  X$Ae[i+1]    <- X$Ae[i]+(X$dAe_1[i+1]+2*X$dAe_2[i+1]+2*X$dAe_3[i+1]+X$dAe_4[i+1])/6
  X$Cp[i+1]    <- X$A1[i+1]/V1/1000
  X$Ce[i+1]    <- X$Ae[i+1]/V1/1000
  X$C1[i+1]    <- X$A1[i+1]/V1/1000
  X$C2[i+1]    <- X$A2[i+1]/V2/1000
  X$C3[i+1]    <- X$A3[i+1]/V3/1000
}
X <- X[-1,]


t0 <- Sys.time()
tx <- t0 + (seq(nrow(X))-1)*60
yl <- c(0, max(X$Ce))
par(mar=c(5, 5, 1, 1), las=1)
plot(tx, X$Ce, type="l", ylim=yl)


さて、自分のPCが手元にあるときは自由にパラメータを設定して任意にシミュレーションできるが、PCがないときやRを使わない他の人に使ってもらいたいときに困る。このようなときにアプリ化しておくと便利である。これはShiny でできる。
Shiny のサンプルはRstudio の新規作成 > shiny にすれば、ヒストグラムを描いてbin をインタラクティブに操作できるものが作成される。出力はoutput$hoge に入っていて、サンプルではoutput$distPlot でヒストグラムが翔ようになっている。ここをいじる。
output にはinput$bins から必要な変数が取得できるので、ここをいじる。生値を入力するのか、スライドバーで設定するのか、選択肢を作るのかはhogehogeInput 関数で設定できる。
アプリを作るにはui.R とapp.R を作る必要があるようだが、app.R 内にui とserver をごちゃごちゃ書きこめばなんとかなるっぽい。アプリを公開するには手っ取り早いのはshinyserver を使う。rmd をRpubs に公開するのと同じように、Rstudio から run app したあとに出力されるhtml の右上に公開、というボタンがあるのでそれを押すと、「アカウントある?」と聞かれるのでとりあえずアカウントを作る。アカウントを作成したあとは、アプリ公開のためのキーを取得して紐づけする必要があるので、SECRET を直して他人に見られないようにRにコピペする。これはツイッターAPI などを使う人はやったことがあるはず。
そのあと、Rstudio から公開しようとするとなぜかできなかった。なのでRコンソールから

rsconnect::deployApp('path/to/your/app')

とするとなぜかうまくいった。このとき、app はapp.R ではなく、app.R があるディレクトリを参照する。

### app.R
#
# This is a Shiny web application. You can run the application by clicking
# the 'Run App' button above.
#
# Find out more about building applications with Shiny here:
#
#    http://shiny.rstudio.com/
#
# http://shiny.rstudio.com/gallery/
# https://shiny.qhmqk.me/shinywidgets/

library(shiny)
library(shinyTime)
# rsconnect::deployApp('path/to/your/app')

# Define UI for application that draws a histogram
ui <- fluidPage(
   # Application title
   titlePanel("Fentanyl PK/PD"),
   # Sidebar with a slider input for number of bins 
   sidebarLayout(
      sidebarPanel(
         sliderInput("hour", "Hour", min = 1, max = 12, value = 4, step=1),
         #dateInput("date", label = h3("Date input"), value = Sys.Date()),
         timeInput("starttime", label = "Simulation start time", value = strptime("09:00:00", "%T"), seconds=FALSE),
         hr(),
         radioButtons("gender", "Gender:", c("Male" = "M", "Female" = "F")),
         sliderInput("age", label="Age", value = 40, min = 0, max = 120, step=1),
         numericInput("height", label = "Height [cm]", min=1, max=250, value = 150),
         numericInput("weight", label = "Weight [kg]", min=1, max=150, value = 50),
         hr(),
         timeInput("ftime1", label = "Fentanyl time 1", value = strptime("09:00:00", "%T"), seconds=FALSE),
         numericInput("iv1", label = "Fentanyl 1 [ng]", min=0, max=1000, value = 50),
         timeInput("ftime2", label = "Fentanyl time 2", value = strptime("10:00:00", "%T"), seconds=FALSE),
         numericInput("iv2", label = "Fentanyl 2 [ng]", min=0, max=1000, value = 0),
         timeInput("ftime3", label = "Fentanyl time 3", value = strptime("11:00:00", "%T"), seconds=FALSE),
         numericInput("iv3", label = "Fentanyl 3 [ng]", min=0, max=1000, value = 0),
         timeInput("ftime4", label = "Fentanyl time 4", value = strptime("12:00:00", "%T"), seconds=FALSE),
         numericInput("iv4", label = "Fentanyl 4 [ng]", min=0, max=1000, value = 0),
         timeInput("ftime5", label = "Fentanyl time 5", value = strptime("13:00:00", "%T"), seconds=FALSE),
         numericInput("iv5", label = "Fentanyl 5 [ng]", min=0, max=1000, value = 0),
         hr(),
         #numericInput("ylim", label = h3("ylim [ng/ml]"), min=0, max=50, value=3),
         sliderInput("ylim", label="ylim [ng/ml]", value = 0, min = 0, max = 5, step=0.5),
         sliderInput("Cont_conc", label="ivPCA [ng/hr]", value = 0, min = 0, max = 50, step=5),
         timeInput("ivPCAstart", label = "ivPCA start time", value = strptime("12:00:00", "%T"), seconds=FALSE),
         hr(),
         fluidRow(column(3, verbatimTextOutput("value")))
         #submitButton("Submit")
      ),
      # Show a plot of the generated distribution
      mainPanel(
         plotOutput("distPlot", click="plot_click"),
         verbatimTextOutput("info"),
         p("やりたいこと", style = "font-family: 'times'; font-size: 16pt"),
         p("フェンタニルの投与回数が5回までなのでsidebarPanel をどうやれば任意に追加できるか"),
         p("フェンタニル以外の薬剤の並列表記、切り替え"),
         p("プロット上にカーソルを持ってくると時刻と濃度のインタラクティブな表示"),
         strong("Strong() makes bold text."),
         em("And em() makes italicized (i.e, emphasized) text."),
         br(),
         code("code displays your text like computer code"),
         div("Use span and div to create segments of text that all have a similar style. For example, this division of text is all blue because I passed the argument 'style = color:blue' to div", style = "color:blue"),
         br(),
         p("span does the same thing, but it works with",
           span("groups of words", style = "color:blue"),
           "that appear inside a paragraph."),
         br(),
         p("https://home.hiroshima-u.ac.jp/r-nacamura/")
      )
   )
)

# Define server logic required to draw a histogram
server <- function(input, output) {
   test <- 1
   output$distPlot <- renderPlot({
      # generate bins based on input$bins from ui.R
      weight <- input$weight
      comp <- c(1:3, "e")
      cname <- c("Bolus", "Cont", sprintf("A%s", comp), mapply(function(z) sprintf("dA%s_%d", comp, z), 1:4), "Cp", sprintf("C%s", comp))
      X <- matrix(0, input$hour*60, length(cname), dimnames=list(NULL, cname))
      X <- as.data.frame(X)
      timestep <- 60
      k_1N <- c(0.0827, 0.471, 0.225)
      k_N1 <- c(0.102, 0.006)
      k_e0 <- 0.114
      
      k <- c(k_1N, k_N1, k_e0)/(60/timestep)
      names(k) <- c(sprintf("k1%d", c(0, 2, 3)), sprintf("k%d1", 2:3), "ke0")
      k1 <- k["k10"] + k["k12"] + k["k13"]
      
      V1 <- 0.105*weight
      CLs <- k[c("k10", "k12", "k13")]*(60/timestep)*V1
      V2 <- CLs[2]/(k["k21"]*(60/timestep))
      V3 <- CLs[3]/(k["k31"]*(60/timestep))
      t0 <- input$starttime - 61*0
      tx <- t0 + (seq(nrow(X))-1)*60
      X[tx > input$ivPCAstart, "Cont"] <- input$Cont_conc/60*1000
      X <- na.omit(X)
      #X[1, "Bolus"] <- input$iv1*1000*(60/timestep)
      #X[100, "Bolus"] <- 150000*(60/timestep)
      X <- rbind(0, X)
      ### together
      #ivs <- unlist(mapply(function(z) eval(parse(text=z)), sprintf("input$iv%d", 1:5)))
      #ft <- unlist(mapply(function(z) eval(parse(text=z)), sprintf("input$ftime%d", 1:5)))
      #idx <- mapply(function(z) tail(which(tx < z), 1), ft)
      #X[idx, "Bolus"] <- ivs*(60/timestep)
      ### each
      X[tail(which(tx < input$ftime1+61), 1), "Bolus"] <- input$iv1*1000*(60/timestep)
      X[tail(which(tx < input$ftime2), 1), "Bolus"] <- input$iv2*1000*(60/timestep)
      X[tail(which(tx < input$ftime3), 1), "Bolus"] <- input$iv3*1000*(60/timestep)
      X[tail(which(tx < input$ftime4), 1), "Bolus"] <- input$iv4*1000*(60/timestep)
      X[tail(which(tx < input$ftime5), 1), "Bolus"] <- input$iv5*1000*(60/timestep)
      for(i in 1:(nrow(X)-1)){
        X$dA1_1[i+1] <- k["k21"]*X$A2[i]+k["k31"]*X$A3[i]-k1*X$A1[i]+X$Cont[i+1]/(60/timestep)
        X$dA2_1[i+1] <- k["k12"]*X$A1[i] - k["k21"]*X$A2[i]
        X$dA3_1[i+1] <- k["k13"]*X$A1[i] - k["k31"]*X$A3[i]
        X$dAe_1[i+1] <- k["ke0"]*(X$A1[i] - X$Ae[i])
        X$dA1_2[i+1] <- k["k21"]*(X$A2[i]+X$dA2_1[i+1]/2)+k["k31"]*(X$A3[i]+X$dA3_1[i+1]/2)-k1*(X$A1[i]+X$dA1_1[i+1]/2)+X$Cont[i+1]/(60/timestep)
        X$dA2_2[i+1] <- k["k12"]*(X$A1[i]+X$dA1_1[i+1]/2) - k["k21"]*(X$A2[i]+X$dA2_1[i+1]/2)
        X$dA3_2[i+1] <- k["k13"]*(X$A1[i]+X$dA1_1[i+1]/2) - k["k31"]*(X$A3[i]+X$dA3_1[i+1]/2)
        X$dAe_2[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_1[i+1]/2 - (X$Ae[i]+X$dAe_1[i+1]/2))
        X$dA1_3[i+1] <- k["k21"]*(X$A2[i]+X$dA2_2[i+1]/2)+k["k31"]*(X$A3[i]+X$dA3_2[i+1]/2)-k1*(X$A1[i]+X$dA1_2[i+1]/2)+X$Cont[i+1]/(60/timestep)
        X$dA2_3[i+1] <- k["k12"]*(X$A1[i]+X$dA1_2[i+1]/2) - k["k21"]*(X$A2[i]+X$dA2_2[i+1]/2)
        X$dA3_3[i+1] <- k["k13"]*(X$A1[i]+X$dA1_2[i+1]/2) - k["k31"]*(X$A3[i]+X$dA3_2[i+1]/2)
        X$dAe_3[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_2[i+1]/2 - (X$Ae[i]+X$dAe_2[i+1]/2))
        X$dA1_4[i+1] <- k["k21"]*(X$A2[i]+X$dA2_3[i+1])+k["k31"]*(X$A3[i]+X$dA3_3[i+1])-k1*(X$A1[i]+X$dA1_3[i+1])+X$Cont[i+1]/(60/timestep)
        X$dA2_4[i+1] <- k["k12"]*(X$A1[i]+X$dA1_3[i+1]) - k["k21"]*(X$A2[i]+X$dA2_3[i+1])
        X$dA3_4[i+1] <- k["k13"]*(X$A1[i]+X$dA1_3[i+1]) - k["k31"]*(X$A3[i]+X$dA3_3[i+1])
        X$dAe_4[i+1] <- k["ke0"]*(X$A1[i]+X$dA1_3[i+1] - (X$Ae[i]+X$dAe_3[i+1]))
        X$A1[i+1]    <- X$A1[i]+(X$dA1_1[i+1]+2*X$dA1_2[i+1]+2*X$dA1_3[i+1]+X$dA1_4[i+1])/6+X$Bolus[i+1]/(60/timestep)
        X$A2[i+1]    <- X$A2[i]+(X$dA2_1[i+1]+2*X$dA2_2[i+1]+2*X$dA2_3[i+1]+X$dA2_4[i+1])/6
        X$A3[i+1]    <- X$A3[i]+(X$dA3_1[i+1]+2*X$dA3_2[i+1]+2*X$dA3_3[i+1]+X$dA3_4[i+1])/6
        X$Ae[i+1]    <- X$Ae[i]+(X$dAe_1[i+1]+2*X$dAe_2[i+1]+2*X$dAe_3[i+1]+X$dAe_4[i+1])/6
        X$Cp[i+1]    <- X$A1[i+1]/V1/1000
        X$Ce[i+1]    <- X$Ae[i+1]/V1/1000
        X$C1[i+1]    <- X$A1[i+1]/V1/1000
        X$C2[i+1]    <- X$A2[i+1]/V2/1000
        X$C3[i+1]    <- X$A3[i+1]/V3/1000
      }
      X <- X[-1,]
      
      yl <- c(0, ifelse(input$ylim==0, max(X$Ce), ifelse(input$ylim<max(X$Ce), input$ylim, max(X$Ce))))
      par(mar=c(5, 5, 2, 1), las=1, cex.lab=1.5)
      plot(tx, X$Ce, type="n", ylim=yl, xlab="Time", ylab="Effect concentratoin [ng/ml]")
      lines(tx, X$Ce, lwd=7)
      abline(h=1.0, lty=3)
      abline(h=2.0, lty=3, col="red", lwd=2)
      abline(v=seq.POSIXt(tx[1], tail(tx, 1)+100, by=15*60), lty=3)
      if(input$Cont_conc > 0){
        x_ivPCA <- as.numeric(input$ivPCAstart)
        abline(v=x_ivPCA, lty=3, col="yellow")
        text(x_ivPCA, par()$usr[4], "ivPCA", pos=3, xpd=TRUE, cex=1.6)
      }
   })
   output$info <- renderText({
     xy_str <- function(e) {
       if(is.null(e)) return("Click figure\n")
       click_time <- format(as.POSIXlt(as.numeric(e$x), origin="1970-01-01"), "%H:%M")
       sprintf("Ce is %.4f at %s", e$y, click_time)
       #paste0("x=", round(e$x, 1), " y=", round(e$y, 1), "\n")
     }
     paste0(
       xy_str(input$plot_click)
     )
   })
}

# Run the application 
shinyApp(ui = ui, server = server)

gganatogram を使って人体を描く

MikuHatsune2018-09-11

こんなのを見かけた。

github
 
ggplot で人体を描いて、臓器やその臓器に与えられたパラメータに応じた色を描いてくれるらしい。

devtools::install_github("jespermaag/gganatogram")

library(ggplot2)
library(ggpolypath)
library(gganatogram)
library(dplyr)
library(gridExtra)

gganatogram(data=hgMale_key, fillOutline='#a6bddb', organism='human', sex='male', fill="colour") + theme_void()


 
hgMake_key にはデフォルトで入っている臓器とタイプ分け、タイプに応じた色が入っている。

                       organ           type    colour     value
1                bone marrow          other   #41ab5d  3.465121
2             frontal cortex nervous system    purple 16.279637
3          prefrontal cortex nervous system    purple 19.914552
4  gastroesophageal junction      digestion    orange  1.007031
5                     caecum      digestion    orange  1.805996
6                      ileum      digestion    orange  9.911434
7                     rectum      digestion    orange 19.989139
8                       nose          other   #41ab5d 12.858249
9                     tongue      digestion    orange  8.574843
10                     penis          other   #41ab5d 15.586977
11             nasal pharynx          other   #41ab5d 18.635302
12               spinal cord nervous system    purple 10.512042
13                    throat      digestion    orange 17.288282
14                 diaphragm    respiratory steelblue 10.941339
15                     liver      digestion    orange  5.335311
16                   stomach      digestion    orange  8.305132
17                    spleen      digestion    orange 16.728927
18                  duodenum      digestion    orange  4.879619
19              gall bladder      digestion    orange 14.012052
20                  pancreas      digestion    orange 10.786245
21                     colon      digestion    orange 16.293596
22           small intestine      digestion    orange  4.601459
23                  appendix          other   #41ab5d 12.857249
24           urinary bladder      digestion    orange  2.420635
25                      bone          other   #41ab5d  1.624326
26                 cartilage          other   #41ab5d 17.913499
27                 esophagus      digestion    orange 19.941666
28                      skin          other   #41ab5d  2.310034
29                     brain nervous system    purple 16.736891
30                     heart    circulation       red  8.383394
31                lymph_node    circulation       red 16.455134
32           skeletal_muscle          other   #41ab5d 19.707006
33                 leukocyte    circulation       red 10.528939
34             temporal_lobe nervous system    purple 16.984754
35          atrial_appendage          other   #41ab5d 17.094688
36           coronary_artery    circulation       red 13.996476
37               hippocampus nervous system    purple 15.832699
38              vas_deferens nervous system    purple 17.723010
39           seminal_vesicle          other   #41ab5d  1.047456
40                epididymis          other   #41ab5d 13.174310
41                    tonsil      digestion    orange  6.264270
42                      lung    respiratory steelblue 13.708475
43                   trachea      digestion    orange  1.825725
44                  bronchus    respiratory steelblue 14.416962
45                     nerve nervous system    purple 17.915368
46                    kidney      digestion    orange 15.225223

46臓器各々やろうと思ったけどfacet_grid がorgan をなぜか受け付けてくれないのでtype 別にプロットする。

gganatogram(data=hgMale_key, fillOutline=grey(0.9), organism='human', sex='male', fill="colour") +
  theme_void() +
  theme(title=element_text(size=24,face="bold")) +
  facet_grid(. ~ type)

ggsave("gganatogram.png", width=5, height=2)


 
実際には、正常と病気で比較したいだろうから、例では癌として適当な図を作っている。

compareGroups <- rbind(data.frame(organ = c("heart", "leukocyte", "nerve", "brain", "liver", "stomach", "colon"), 
  colour = c("red", "red", "purple", "purple", "orange", "orange", "orange"), 
 value = c(10, 5, 1, 8, 2, 5, 5), 
 type = rep('Normal', 7), 
 stringsAsFactors=F),
 data.frame(organ = c("heart", "leukocyte", "nerve", "brain", "liver", "stomach", "colon"), 
  colour = c("red", "red", "purple", "purple", "orange", "orange", "orange"), 
 value = c(5, 5, 10, 8, 2, 5, 5), 
 type = rep('Cancer', 7), 
 stringsAsFactors=F))

gganatogram(data=compareGroups, fillOutline='#a6bddb', organism='human', sex='male', fill="value") + 
theme_void() +
facet_wrap(~type) +
scale_fill_gradient(low = "white", high = "red") 

医薬データ解析のためのベイズ統計学

読んだ。

医薬データ解析のためのベイズ統計学

医薬データ解析のためのベイズ統計学

めっちゃ時間がかかってしかも記事にしておくのも時間かかってた。
 
古典的なp値の頻度論的な話も含んでいて、基礎の確認…にはならない。けっこう大変だった。
しかし、収束の判定法や、変数選択としての使い方とか、他のベイズ本ではあまりしっかり書いていないようなことも書かれているような気がした(読了したのが昔過ぎて記憶があやふやである)。

レプリカ交換法

読んだ。
Bayesian estimation of phase response curves. Neural Netw. 2010 Aug;23(6):752-63.
 
Phase response curve (PRC) という神経細胞の発火の記録を推定したいが、周期のズレや発火タイミングの変化などで普通にやったら推定が収束しないらしい。
レプリカ交換法は現時点でRstan では実装されていないので、コードをコンパイルしたものを使いまわして各iteration の最終サンプリング結果から取り出してきてやる方法がある。
StanとRでレプリカ交換MCMC(parallel tempering) を実行する - StatModeling Memorandum
 
曲線の推定は2階差分としてなめらかになるようにしている。prior に z_{j-1}-2z_j + z_{j+1}=(z_{j-1}-z_j)-(z_j-z_{j+1}) とする。

VARで本当にPKが多くなっているのか

MikuHatsune2018-07-03

ロシアW杯でVAR が導入されたことにより、PK の数が多いような印象である。実際、予選リーグの時点で過去最高だとか、いろいろ言われている。毎日新聞によると
https://mainichi.jp/articles/20180703/ddm/035/050/161000c

VARは、得点▽PK▽レッドカード▽警告などの選手間違い−−の4項目に関わるものに適用されるが、1次リーグではPKに関わるものが最も多かった。VARによりPKが認められたケースは7件で、PK回数は24件。逆に、VARによってPKが取り消されたケースは2件あった。

 1大会で実施されたPKの最多記録18(1990年イタリア、98年フランス、2002年日韓大会)を既に更新している。このほか、レッドカードの扱いが覆ったケースが2件あった。

ということである。
しかし、PK というのは1試合中にそうそう起こることではないので、単純にポアソン分布に従うと考えられる。とすると、ポアソン分布の平均は\lambda で、分散も\lambda であるので、いままでの平均を超えたからと言ってそんなに簡単に分布の裾にならないのである。
というわけでデータを取ってみる。RSSSF というサイトに2014から1930年大会の試合記録があって、そこにはPKでの得点だけではなく、PKの失敗(外した、セーブした)まで残っている。これを一生懸命見てみると、こんな感じのデータになる(ポアソン分布での解析をしたかったので、1試合中に複数回PKがあった場合も調べた)。
836試合で218回のPK があったようである。

year goal nogoal match pk2 pk3
2014 12 1 64 0 0
2010 9 6 64 1 0
2006 16 0 64 0 0
2002 13 5 64 1 0
1998 17 1 64 0 0
1994 15 0 52 0 0
1990 13 5 52 2 0
1986 12 4 52 1 0
1982 8 3 52 0 0
1978 12 2 38 0 0
1974 6 2 38 0 0
1970 5 0 32 0 0
1966 8 0 32 0 0
1962 8 1 32 0 0
1958 7 3 35 0 0
1954 7 1 26 0 0
1950 3 0 22 0 0
1938 3 2 18 0 0
1934 3 1 17 1 0
1930 1 3 18 0 1

 
さて、単純に、予選リーグ48試合で24回のPKがあったようなので、

poisson.test(c(24, sum(s1)), c(48, sum(s2)), alternative="greater")
	Comparison of Poisson rates

data:  c(24, sum(s1)) time base: c(48, sum(s2))
count1 = 24, expected count1 = 13.14, p-value = 0.003486
alternative hypothesis: true rate ratio is greater than 1
95 percent confidence interval:
 1.297443      Inf
sample estimates:
rate ratio 
  1.917431 

確かにPK は多いようである。
64試合すべて消化するまでに、何回PKがあれば有意に多そうだ、と言えるかというと、25回あると0.05 を下回る。
VAR によりPK が認められたのは予選リーグ48試合までで7回あるので、7回の上乗せ効果はやはりPK の回数増加に寄与してそうな感じはある。

g <- 0:40
pv <- mapply(function(z) poisson.test(c(z, sum(s1)), c(64, sum(s2)), alternative="greater")$p.value, g)

plot(g, pv, type="o", pch=15, xlab="1大会64試合中のPK回数", ylab="p value", lwd=3)
abline(h=0.05, lty=3)

W杯の試合観戦中にトイレはいついくべきか

MikuHatsune2018-06-25

こんなツイートを観測した。

 
ハーフタイムに水道使用料が増えているのがわかる。
試合中に離席すると一番盛り上がる得点シーンを見逃してしまうため、試合中はなかなかトイレやお風呂にいけない。
というわけで試合中に離席するにはどの時間帯が一番よいかを調べる。
 
高校サッカーの点差を解析したときと同様に、過去のW杯の試合結果から得点が入った時刻を取得する。ここで、1930年から2014年大会までの20大会(1942年と1946年は中止)について、836試合あり、得点シーンは2373だった(wikiFIFA W杯のページをパースしたため、本当にそうなのかはわからない)。
1970年大会が6ゴール取得できてなかったようである。

1930 1934 1938 1950 1954 1958 1962 1966 1970 1974 1978 1982 1986 1990 1994 1998 2002 2006 2010 2014 
  70   70   84   88  140  126   89   89   89   97  102  146  132  115  141  171  161  147  145  171 

 
いつ得点が入るかをベタに分布をとると、高校サッカー選手権とほぼ同様で、どの時間帯でもほぼ一様な感じである。ただし、アディショナルタイムはすべて45分もしくは90分に換算しており、後半90分はほかの時間帯に比べて突出して得点が入っているので、試合終了間際は離席せずに見届けたほうがよい。
前半では全体の44%、後半では56% の得点が分布するので、どちらかというと前半のほうに離席するのがよい。

 
ある時間帯に得点が入ってから、次の得点が入るまでの時間の分布は、ガンマ分布のようになる。このため、ガンマ分布で推定すると、パラメータ(1.266, 0.0554) をもつ以下のようなガンマ分布になる。
得点が入ってから17分間で、50% の確率で次の点がはいるようなので、得点が入った直後は油断しないほうがよい。

 
ある時間帯にN点差がついているときに、そのままリードして試合終了する確率も選手権の解析と同様にしてみたが、ほとんど高校サッカーと同じ結果になった。試合終了間際まで1点リードしているとき、劇的ゴールで追いつくのは2.4% しかない(89分時点で1点リードしてそのまま90分=試合終了までリードを保つ確率が97.6%)。
勝ったな(確信)と思って風呂にはいったり寝たりするのは、90%の確信度でいくならば1点差なら後半75分あたり、2点差なら前半30分あたりでよい。

 
今回のデータ取りはstringr を使ってR 内で完結させた。

# データとり
# wiki から
# W杯のトップページより、各グループステージ、決勝トーナメントの
# ページからデータをパースするほうが、フォーマットが整っていて効率がよい

url <- NULL
y1 <- seq(2014, 1998, -4)
lab <- c(sprintf("_Group_%s", LETTERS[1:8]), "_knockout_stage")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- seq(1994, 1986, -4)
lab <- c(sprintf("_Group_%s", LETTERS[1:6]), "_knockout_stage")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- 1982
lab <- c(sprintf("_Group_%s", c(1:6, LETTERS[1:4])), "_knockout_stage")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- c(1978, 1974)
lab <- c(sprintf("_Group_%s", c(1:4, LETTERS[1:2])), "_knockout_stage")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- c(seq(1970, 1954, -4), 1930)
lab <- c(sprintf("_Group_%s", 1:4), "_knockout_stage")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- 1950
lab <- c(sprintf("_Group_%s", 1:4), "_final_round")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))

y1 <- c(1938, 1934)
lab <- c(sprintf("_Group_%s", 1:4), "_final_tournament")
url <- c(url, mapply(function(z) sprintf("https://en.wikipedia.org/wiki/%d_FIFA_World_Cup%s", z, lab), y1))
write(url, "list.txt")

# パース
library(stringr)
fi <- list.files(pattern="FIFA")
Res <- NULL
g <- 0
for(f in fi){
  txt <- readLines(f)
  flag <- c("score"=0, "right"=1)
  res <- NULL
  for(tmp in txt){
    #if(str_detect(tmp, "<th style.*\\d+&#8211;\\d+.*</th>")){
    if(str_detect(tmp, "<th style.*&#8211;.*</th>")){ # 1986年のグループステージが半角スペースがあっておかしい
      flag["score"] <- 1
      goaltime <- vector("list", 2)
    }
    if(flag["score"] == 1){
      if(str_detect(tmp, "[\\d+]+'")){
        goaltime[[ flag["right"] ]] <- c(goaltime[[ flag["right"] ]], gsub("'", "", str_extract_all(tmp, "[\\d+]+'")[[1]]))
      }
      if(str_detect(tmp, "Report")){
        flag["right"] <- 2
      }
      if(str_detect(tmp, "</table>")){
        res <- c(res, list(goaltime))
        flag <- c("score"=0, "right"=1)
      }
    }
  }
  year <- as.numeric(str_extract(f, "\\d+"))
  for(i in seq(res)){
    g <- g + 1
    for(j in seq(res[[i]])){
      for(k in res[[i]][[j]]){
        l <- as.numeric(strsplit(k, "\\+")[[1]])
        hoge <- c(year, g, j, l, rep(0, 2-length(l)))
        Res <- rbind(Res, hoge)
      }
    }
  }
}
colnames(Res) <- c("year", "gameID", "HA", "time", "extra")
# 解析
dat <- read.table("score.txt", header=TRUE)
# 試合数の確認
Ngame <- c(18, 17, 18, 22, 26, 35, rep(32, 3), 38, 38, rep(52, 4), rep(64, 5))
mapply(function(z) length(unique(z$gameID)), split(dat, dat$year))

# 得点時間分布
sb <- subset(dat, time <= 90)
t1 <- 1:45
t2 <- 46:90
tab <- table(factor(sb$time, c(t1, t2)))/nrow(sb) * 100

cols <- c("red", "green")
par(mar=c(5, 5, 2, 2), cex.lab=1.6, cex.main=2)
b <- barplot(tab, col=c(mapply(rep, cols, each=sapply(list(t1, t2), length))), las=1, main="得点時間分布", ylab="頻度[%]", ylim=c(0, 3))
mtext("前半(分)", 1, line=3, at=mean(t1))
mtext("後半(分)", 1, line=3, at=mean(t2)+15)

# 得点間隔
sb <- dat
Tgoal <- lapply(split(sb$time, sb$gameID), sort)
difftime <- lapply(Tgoal, function(z) tail(c(0, z), -1) - head(c(0, z), -1))
difftime <- unlist(difftime)

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

# ガンマ分布による得点時間間隔
code <- "
data{
  int N;
  vector<lower=0>[N] Time;
}
parameters{
  real<lower=0, upper=50> p[2];
}
model{
  Time ~ gamma(p[1], p[2]);
}
"

m <- stan_model(model_code=code)
standata <- list(N=length(difftime), Time=difftime)
fit <- sampling(m, data=standata, iter=6000, warmup=1500, seed=1234)
ex <- extract(fit)

tab <- table(factor(difftime, 0:max(difftime)))/length(difftime) * 100
b <- barplot(tab, las=1)
ps <- apply(ex$p, 2, median)
x0 <- seq(0, max(difftime), length=1000)
y0 <- dgamma(x0, ps[1], ps[2])

alpha <- c(0.25, 0.5, 0.75, 0.9)
d0 <- c(5, 0.2)
par(mar=c(5, 5, 2, 2), cex.lab=1.6, cex.main=2)
plot(tab, xlab="前の得点からの経過時間(分)", ylab="頻度[%]", las=1)
lines(x0, y0*100, col=2, lwd=3)
for(i in seq(alpha)){
  x1 <- qgamma(alpha[i], ps[1], ps[2])
  y1 <- dgamma(x1, ps[1], ps[2])*100
  arrows(x1+d0[1], y1+d0[2], x1, y1, length=0.1, lwd=3, col=4)
  points(x1, y1, pch=16, col=4)
  text(x1+d0[1], y1+d0[2], sprintf("%2d %s (%.1f min)", alpha[i]*100, "%", x1), pos=4)
  title("次の得点の時間の分布")
}

# 得点差勝利確率
sb <- subset(dat, time <= 90)
mat <- mat0 <- mat1 <- mat2 <- matrix(0, max(dat$gameID), 90)
idx <- c(1, -1)
for(i in unique(sb$gameID)){
  tmp <- subset(sb, gameID==i)
  tmp <- tmp[order(tmp$time),]
  for(j in 1:nrow(tmp)){
    mat[i, tmp$time[j] ] <- mat[i, tmp$time[j]] + idx[tmp$HA[j]]
  }
}

mat0[,1] <- mat[,1]
for(j in 2:ncol(mat)){
  mat0[,j] <- mat0[, j-1] + mat[, j]
}

# 1点差以上
mat1[abs(mat0) > 0] <- 1
prob1 <- prob2 <- rep(0, 89)
for(j in 1:89){
  j1 <- mat1[, j] > 0
  prob1[j] <- mean(sapply(apply(mat1[j1, j:90], 1, unique), length) <= 1)
}

mat2 <- abs(mat0)
for(j in 1:89){
  j1 <- mat2[, j] > 1
  prob2[j] <- mean(apply(mat2[j1, j:90] > 0, 1, all))
}

p <- cbind(prob1, prob2)
par(mar=c(5, 5, 2, 4), cex.lab=1.6, cex.main=2, las=1)
matplot(p, xlab="時間", ylab="勝利確率", col=cols, pch=16, xaxp=c(0, 90, 6))
abline(v=45, lty=3)
legend("bottomright", legend=sprintf("%d 点差", 2:1), col=rev(cols), pch=16, cex=2)
text(par()$usr[2], tail(p[,1], 1), sprintf("%.1f %s", tail(p[,1], 1)*100, "%"), pos=4, xpd=TRUE)