霞と側杖を食らう

ほしいものです。なにかいただけるとしあわせです。[https://www.amazon.jp/hz/wishlist/ls/2EIEFTV4IKSIJ?ref_=wl_share]

ガンマ分布の乱数からディリクレ分布の乱数を生成する方法に関する学習記録

【学習動機】

ディリクレ分布からの乱数を生成したいが, そんなものはないというケースがある. たとえば, Excel周りで実装しないといけない場合など. そういう場合にどうすれば良いのか調べてみると, ディリクレ乱数はガンマ分布からの乱数で生成できることを知った. その生成方法について書き, Rで実際にガンマ乱数から生成してみる, また, 既にあるパッケージの{MCMCpack}のrdirichlet関数と比較してみる.

【学習記録】

ディリクレ乱数の生成方法

以下の文献に詳しいので, 参考にした. Bela A Frigyik, Amol Kapila, and Maya R Gupta. 2010
Introduction to the Dirichlet distribution and related processes.
https://vannevar.ece.uw.edu/techsite/papers/documents/UWEETR-2010-0006.pdf [pdf注意]

ディリクレ分布の乱数生成の方法
(i) the urn-drawing method
ポリアの壺の方法. ちなみにポリアの壺は『いかにして問題をとくか』の 著者である数学者ジョージ・ポリアに因んで名付けられている.
(ii) The Stick-breaking Approach 棒折りのアプローチ. ちなみにノンパラベイズで棒折り過程の話は出てくる. 次の章にディリクレ過程の話も書かれている.
(iii) Generating the Dirichlet from Gamma RVs ガンマ確率変数による生成方法. ガンマ確率変数による生成方法は, (i)ポリアの壺の方法と(ii)棒折りのアプローチのいずれよりも計算効率が良いとのこと.

ここでは(iii)の方法のみ扱う. 数理やその他の方法などは上の文献を参照されたい.
(iii)のガンマ確率変数によるディリクレ乱数は以下の2ステップで生成される.

Step 1: ガンマ乱数生成
Generate gamma realizations: for \(i=1,...,k\), draw a number \(z_i\) from \(\Gamma\) (\(\alpha_i\), 1).

Step 2: 正規化
Normalize them to form a pmf: for \(i=1,...,k\), set


. Then q is a realization of Dir(\(\alpha\)).

Rで生成

# パッケージ読み込み
library(tidyverse)
# 乱数固定
set.seed(20230228)

乱数生成のためのサンプルサイズ設定とパラメータ設定.

# サンプルサイズ
N <- 10000
# ディリクレ分布のパラメータ設定
alpha <- c(1, 2, 3)

ガンマ分布から乱数を生成
仕組みを分かりやすくするために, 無駄にループを回す, Rとしては非効率な書き方になっているので注意(後述)

k <- length(alpha)
z <- matrix(numeric(k*N), ncol=k, byrow=TRUE)
q_gamma <- matrix(numeric(k*N), ncol=k, byrow=TRUE)
# Step 1 : ガンマ乱数生成
for(i in 1:k){
  z[,i] <- rgamma(n=N, alpha[i])
}
# Step 2 : 正規化
for(j in 1:N){
  q_gamma[j,] <- z[j,]/sum(z[j,])
}

{MCMCpack}パッケージの関数を使用して生成

q_mcmcpack <- MCMCpack::rdirichlet(n = N, alpha = alpha)

生成したディリクレ乱数を可視化したいが, 自分では作画できないので, 以下の記事のコードをほとんどそのまま使用させていただきました. 感謝. ディリクレ分布の乱数生成

www.anarchive-beta.com

自分でガンマ乱数から生成したもののヒートマップを描く.

# 軸目盛の位置を指定
axis_vals <- seq(from = 0, to = 1, by = 0.1)

# 枠線用の値を作成
ternary_axis_df <- tibble::tibble(
  y_1_start = c(0.5, 0, 1),         # 始点のx軸の値
  y_2_start = c(0.5*sqrt(3), 0, 0), # 始点のy軸の値
  y_1_end = c(0, 1, 0.5),           # 終点のx軸の値
  y_2_end = c(0, 0, 0.5*sqrt(3)),   # 終点のy軸の値
  axis = c("x_1", "x_2", "x_3")     # 元の軸
)

# グリッド線用の値を作成
ternary_grid_df <- tibble::tibble(
  y_1_start = c(
    0.5 * axis_vals, 
    axis_vals, 
    0.5 * axis_vals + 0.5
  ), # 始点のx軸の値
  y_2_start = c(
    sqrt(3) * 0.5 * axis_vals, 
    rep(0, times = length(axis_vals)), 
    sqrt(3) * 0.5 * (1 - axis_vals)
  ), # 始点のy軸の値
  y_1_end = c(
    axis_vals, 
    0.5 * axis_vals + 0.5, 
    0.5 * rev(axis_vals)
  ), # 終点のx軸の値
  y_2_end = c(
    rep(0, times = length(axis_vals)), 
    sqrt(3) * 0.5 * (1 - axis_vals), 
    sqrt(3) * 0.5 * rev(axis_vals)
  ), # 終点のy軸の値
  axis = c("x_1", "x_2", "x_3") |> 
    rep(each = length(axis_vals)) # 元の軸
)

# 軸ラベル用の値を作成
ternary_axislabel_df <- tibble::tibble(
  y_1 = c(0.25, 0.5, 0.75),               # x軸の値
  y_2 = c(0.25*sqrt(3), 0, 0.25*sqrt(3)), # y軸の値
  label = c("alpha[1]", "alpha[2]", "alpha[3]"),      # 軸ラベル
  h = c(3, 0.5, -2),  # 水平方向の調整用の値
  v = c(0.5, 3, 0.5), # 垂直方向の調整用の値
  axis = c("x_1", "x_2", "x_3") # 元の軸
)

# 軸目盛ラベル用の値を作成
ternary_ticklabel_df <- tibble::tibble(
  y_1 = c(
    0.5 * axis_vals, 
    axis_vals, 
    0.5 * axis_vals + 0.5
  ), # x軸の値
  y_2 = c(
    sqrt(3) * 0.5 * axis_vals, 
    rep(0, times = length(axis_vals)), 
    sqrt(3) * 0.5 * (1 - axis_vals)
  ), # y軸の値
  label = c(
    rev(axis_vals), 
    axis_vals, 
    rev(axis_vals)
  ), # 軸目盛ラベル
  h = c(
    rep(1.5, times = length(axis_vals)), 
    rep(1.5, times = length(axis_vals)), 
    rep(-0.5, times = length(axis_vals))
  ), # 水平方向の調整用の値
  v = c(
    rep(0.5, times = length(axis_vals)), 
    rep(0.5, times = length(axis_vals)), 
    rep(0.5, times = length(axis_vals))
  ), # 垂直方向の調整用の値
  angle = c(
    rep(-60, times = length(axis_vals)), 
    rep(60, times = length(axis_vals)), 
    rep(0, times = length(axis_vals))
  ), # ラベルの表示角度
  axis = c("x_1", "x_2", "x_3") |> 
    rep(each = length(axis_vals)) # 元の軸
)
  
# サンプルを三角座標に変換して格納(q_gamma)
data_df_q_gamma <- tibble::tibble(
  y_1 = q_gamma[, 2] + 0.5 * q_gamma[, 3], # 三角座標のx軸の値
  y_2 = sqrt(3) * 0.5 * q_gamma[, 3] # 三角座標のy軸の値
)


# 三角座標の値を作成
y_1_vals <- seq(from = 0, to = 1, length.out = 301)
y_2_vals <- seq(from = 0, to = 0.5*sqrt(3), length.out = 300)

# 格子点を作成
y_mat <- tidyr::expand_grid(
  y_1 = y_1_vals, 
  y_2 = y_2_vals
) |> # 格子点を作成
  as.matrix() # マトリクスに変換

# 3次元変数に変換
phi_mat <- tibble::tibble(
  phi_2 = y_mat[, 1] - y_mat[, 2] / sqrt(3), 
  phi_3 = 2 * y_mat[, 2] / sqrt(3)
) |> # 元の座標に変換
  dplyr::mutate(
    phi_2 = dplyr::if_else(phi_2 >= 0 & phi_2 <= 1, true = phi_2, false = as.numeric(NA)), 
    phi_3 = dplyr::if_else(phi_3 >= 0 & phi_3 <= 1 & !is.na(phi_2), true = phi_3, false = as.numeric(NA)), 
    phi_1 = 1 - phi_2 - phi_3, 
    phi_1 = dplyr::if_else(phi_1 >= 0 & phi_1 <= 1, true = phi_1, false = as.numeric(NA))
  ) |> # 範囲外の値をNAに置換
  dplyr::select(phi_1, phi_2, phi_3) |> # 順番を変更
  as.matrix() # マトリクスに変換


# パラメータラベル用の文字列を作成
param_text <- paste0(
  "list(", 
  "alpha==(list(", paste0(alpha, collapse = ", "), "))", 
  ", N==", N, 
  ")"
)

  
# サンプルの度数のヒートマップを作成
ggplot() + 
  geom_segment(data = ternary_axis_df, 
               mapping = aes(x = y_1_start, y = y_2_start, xend = y_1_end, yend = y_2_end), 
               color = "gray50") + # 三角図の枠線
  geom_segment(data = ternary_grid_df, 
               mapping = aes(x = y_1_start, y = y_2_start, xend = y_1_end, yend = y_2_end), 
               color = "gray50", linetype = "dashed") + # 三角図のグリッド線
  geom_text(data = ternary_ticklabel_df, 
            mapping = aes(x = y_1, y = y_2, label = label, hjust = h, vjust = v, angle = angle)) + # 三角図の軸目盛ラベル
  geom_text(data = ternary_axislabel_df, 
            mapping = aes(x = y_1, y = y_2, label = label, hjust = h, vjust = v), 
            parse = TRUE, size = 6) + # 三角図の軸ラベル
  geom_bin_2d(data = data_df_q_gamma, 
              mapping = aes(x = y_1, y = y_2, fill = ..count..), 
              alpha = 0.8) + # サンプル
  scale_fill_distiller(palette = "Spectral") + # 塗りつぶしの色
  scale_x_continuous(breaks = c(0, 0.5, 1), labels = NULL) + # x軸
  scale_y_continuous(breaks = c(0, 0.25*sqrt(3), 0.5*sqrt(3)), labels = NULL) + # y軸
  coord_fixed(ratio = 1, clip = "off") + # アスペクト比
  theme(axis.ticks = element_blank(), 
        panel.grid.minor = element_blank()) + # 図の体裁
  labs(title = "Dirichlet Distribution (Gamma RVs method)", 
       subtitle = parse(text = param_text), 
       fill = "frequency", 
       x = "", y = "") +
  theme_bw()



# サンプルを三角座標に変換して格納(q_mcmcpack) data_df_q_mcmcpack <- tibble::tibble( y_1 = q_mcmcpack[, 2] + 0.5 * q_mcmcpack[, 3], # 三角座標のx軸の値 y_2 = sqrt(3) * 0.5 * q_mcmcpack[, 3] # 三角座標のy軸の値 ) # サンプルの度数のヒートマップを作成 ggplot() + geom_segment(data = ternary_axis_df, mapping = aes(x = y_1_start, y = y_2_start, xend = y_1_end, yend = y_2_end), color = "gray50") + # 三角図の枠線 geom_segment(data = ternary_grid_df, mapping = aes(x = y_1_start, y = y_2_start, xend = y_1_end, yend = y_2_end), color = "gray50", linetype = "dashed") + # 三角図のグリッド線 geom_text(data = ternary_ticklabel_df, mapping = aes(x = y_1, y = y_2, label = label, hjust = h, vjust = v, angle = angle)) + # 三角図の軸目盛ラベル geom_text(data = ternary_axislabel_df, mapping = aes(x = y_1, y = y_2, label = label, hjust = h, vjust = v), parse = TRUE, size = 6) + # 三角図の軸ラベル geom_bin_2d(data = data_df_q_mcmcpack, mapping = aes(x = y_1, y = y_2, fill = ..count..), alpha = 0.8) + # サンプル scale_fill_distiller(palette = "Spectral") + # 塗りつぶしの色 scale_x_continuous(breaks = c(0, 0.5, 1), labels = NULL) + # x軸 scale_y_continuous(breaks = c(0, 0.25*sqrt(3), 0.5*sqrt(3)), labels = NULL) + # y軸 coord_fixed(ratio = 1, clip = "off") + # アスペクト比 theme(axis.ticks = element_blank(), panel.grid.minor = element_blank()) + # 図の体裁 labs(title = "Dirichlet Distribution (MCMCpack)", subtitle = parse(text = param_text), fill = "frequency", x = "", y = "") + theme_bw()


設定したパラメータから計算される平均

alpha/sum(alpha)
## [1] 0.1666667 0.3333333 0.5000000

自分でガンマ乱数から生成したものの平均値

colSums(q_gamma)/N
## [1] 0.1645176 0.3340670 0.5014154

{MCMCpack}パッケージの関数を使用して生成したものの平均値

colSums(q_mcmcpack)/N
## [1] 0.1665876 0.3357620 0.4976504

グラフ, 平均値の比較をみてみたところ, 合っていそう.

MCMCpack::rdirichletの中身

さて, MCMCpack::rdirichletの中身を確認する.

MCMCpack::rdirichlet
## function (n, alpha) 
## {
##     l <- length(alpha)
##     x <- matrix(rgamma(l * n, alpha), ncol = l, byrow = TRUE)
##     sm <- x %*% rep(1, l)
##     return(x/as.vector(sm))
## }
## <bytecode: 0x0000022e10de5e00>
## <environment: namespace:MCMCpack>

実はMCMCpack::rdirichletは, ガンマ分布の乱数から同じように生成していることが分かる.
xで一括でガンマ乱数を生成して, matrixに折りたたむ.
smは行列の積で行和を計算.
smをベクトル化して正規化して終了.
Rで実装するなら, 無駄にループするよりも当然こちらの方が効率が良い.

【学習予定】

いつかディリクレ過程周りも理解してまとめておきたい. 

関係ないけど, ディリクレ分布は単体(simplex)上の話で, ゲーム理論を少し思い出したりもした.

 

狂気の沙汰ほど狂おしい

東京ステーションギャラリーで開催されていた「佐伯祐三 自画像としての風景」へ行ってきた。この展覧会に惹かれたのは理由が2つあった。一つは、以前山種美術館で目に留まった絵にレストランの絵があって、その絵の作者が佐伯祐三だったということ。もう一つは、そのとき佐伯祐三が30歳で亡くなっていることを知ったのだが、その後、東京都美術館の「エゴン・シーレ展」でエゴン・シーレが28年で生涯を終えたということを知って、共通性が気にかかったということだ。夭逝した才能として扱われているだけあって、印象に残る絵も多くあった(ただ、来場者が多くて、少し息苦しかった)。とくに、パリの街並みに貼られたポスター広告をテーマにしている作品は面白かった。

広告業界を扱っている『左ききのエレン』を、個人的に半年ほど積んでいたのだが、つい先日、全巻読み終えた。正直に感想を言うと、周りの評価と積読期間の熟成で高まっていた期待を全く越えてこなかった。全体として疾走感があったのは良かったが、いくつか引っかかった点がある。まず第一に、全体としてキャラの軸がブレブレで安定感がないように見えた。第二に、光一にそこまでエレンを惹きつける魅力があったかという疑問だ。エレン以外のキャラクターが光一を気にするのは、関心が集まることによる相乗効果ということで理解できるが、エレンの光一へのこだわりは、行動原理同様によく分からなかった。亡くなった父親との重ね合わせもあるかもしれないが、どうも諸々が弱く感じた。第三に、作者は広告業界については詳しいかもしれないが、その他の節々が分かっていなさそうな感じを受けた。とりわけ、天才への解像度がとても低い。才能を集中力の分類で片付けて、あとは少年漫画の必殺技にして勢いで済ましている。才能に対する必死な肉薄の跡もなく、一貫したメインテーマである「天才になれなかった全ての人へ」という言葉は、imperfectな人間の少なくとも一人である私には全然響いてこなかった。むしろ、作者自身への慰めの言葉として強く聞こえてしまった。

才能の追求をテーマとして扱った映画として、『セッション』がある。デイミアン・チャゼルの新作映画『バビロン』の公開記念ということで過去作の『セッション』が新宿ピカデリーで再上映された。『セッション』は配信やテレビ放映で何度か観ていたが、劇場でぜひ楽しみたいと思ったので、即座に予約をとって観賞してきた。映画館の映像と音響で、あの才能と狂気を、あの演奏を体験できて良かった。才能の高みへの狂った追求を狂っているとしつつも、ラストシーンは極まった結果、二人の口角が上がって終わるのだから、その追求は間違っているけれど間違っていないという表現なのだろうかと思ったりもした。

後日、『バビロン』も映画館で観てきた。最初は大便小便吐瀉物まみれのドラッグ糞映画かと思ったが、最高にクソ面白い映画だった。1920年代の映画界の栄枯盛衰が描かれて、これはまさにチャゼルの描く平家物語か!!ってなった。映画史に詳しい人たちと一緒に観賞したいとも思った。『バビロン』が映画を作る側にスポットライトを当てた映画だったので、関連して、以前から気になっていた『映画大好きポンポさん』を配信で見た。『セッション』を観てから狂気について考えていたところで、物語中盤でのポンポさんのセリフに「ようこそ 夢と狂気の世界へ」とあって驚いた。原作は未読なのだが、ポンポさんの設定として好きな映画に『セッション』が挙げられているらしい。『映画大好きポンポさん』の原作者も、きっと狂気の沙汰で生まれた何かに酔いしれたことがある人なのだろう。主人公、および、映画内映画の主人公を通して、他のものを切り捨てて、一つのものに執着し、全てをそこに捧げる姿が描かれていた。

狂気の産物は、誰かを歓喜させるかもしれない。誰かの人生を180度転換させてしまうかもしれない。誰かを地獄の淵から救い出すかもしれない。狂ったほど極まったところの到達点にしか現れてこない魅力が、創作や研究、パフォーマンスなどにはある。一方で、魅力がある反面、肉体的健康や精神的健康だとか労働環境だとかコンプライアンスだとかその他諸々は度外視される。視野の外に置かれたこれらは間違いなく大切だ。社会的にも重要性がどんどん増してきているように思う。このような状況の中で、狂気とその他諸々の狭間でジレンマティックな状況にいる人は、一定以上存在するのではないだろうか。少なくとも私はその一人でありながら、狂気の側には身を置けず、その他諸々の側に立ちながら、狂気の側に憧憬を抱いている。だからこそ、『セッション』や『映画大好きポンポさん』の狂気の賛美に心震わせられたのだろうし、『左ききのエレン』に対して劇的な描写か不完全な人間への鎮魂と激励を強く求めていたのかもしれない。

こんなことを書き連ねていたら、そういえば、東大の応援歌のタイトルは『ただ一つ』だったなと思い出したが、メロディーは全く思い出せないなと、少し笑った。

2022年の後日譚々

2023年になったので、2022年を振り返る。今年はアニメの『平家物語』と大河の『鎌倉殿の13人』を見るところから始まった(『平家物語』のOP曲であった羊文学の『光るとき』は良い曲だった。)。中世の日本の歴史に関心を持って、伊藤俊一の『荘園-墾田永年私財法から応仁の乱まで』を読んだりした。背景知識が身に付いた状態でタイミングよく上映されたのが『犬王』だった。音楽もアニメーションもバディもののストーリーもすべてが自分に刺さった。

夏頃に『DR.STONE』を漫画で読破して、人類の歴史に関心を持ち、篠田謙一の『人類の起源-古代DNAが語るホモ・サピエンスの「大いなる旅」』を読んだ。ちょうどこの本で読んだ内容が、2022年のノーベル生理学・医学賞を受賞したスバンテ・ペーボのネアンデルタール人ゲノム解析の研究と関連して面白さが増幅した。この本の少し前に読んだ『サピエンス全史』や『銃・病原菌・鉄』といった人類史は最新の研究で事実関係や解釈が異なってくるということも実感したし、知識をアップデートする重要性を痛感した(c.f. 文字禍福は糾える縄の如し - 霞と側杖を食らう)。

DR.STONE』に関連して、今まであまり読んでこなかったSFに対しても興味を持った。SFは設定の分からなさを許容する力も必要なようで、今のうちに読み始めて味わう力を養っておこうと思った。『1984』や『華氏451度』といった古典的な名著から始めて、最近の柞刈湯葉の『人間たちの話』なども読んだ。とりわけ良かった作品は『星を継ぐもの』、『アルジャーノンに花束を』、『プロジェクト・ヘイル・メアリー』であった。SFに少しは慣れてきたように思う。2023年も続けてSF筋を鍛えていきたい(c.f. Yesterday is history, tomorrow is mystery, today is a gift, that's why it is called the present. - 霞と側杖を食らう)。

2022年も映画を50本見ることを目標にしていたのだが、無事50本見ることができた。映画館で見たものだと、『犬王』以外だと、『トップガン マーヴェリック』、『ペルシャン・レッスン 戦場の教室』が同じくらい良くて、その次くらいに『サバカン SABAKAN』、『四畳半タイムマシンブルース』、『THE FIRST SLAM DUNK』、『劇場版 RE:cycle of the PENGUINDRUM』あたりが良かった。とくに良かったのは『ペルシャン・レッスン 戦場の教室』で、収容所という特殊な環境における関係と、必死の言語の創作(とくに無意識の寝言にまで現出するシーン)、上映時間の枠における完成度に感動した。2023年も50本見ることを目指したい。

他に2022年に続けていたのは、瓶で購入して飲んだ日本酒を御酒飲帳に記録することだった。お酒の味も記録するために、どんな味かを言語化するようになった。言語化を継続することで、味の解像度が上がったし、どのような食事と合うかも考えるようになった。2022年に飲んで印象的で美味しかったのは、栄光冨士の闇鳴秋水、醸し人九平次のSAUVAGE、十六代九郎右衛門の雄町、山本のピュアブラックだった。2023年も解像度を上げる飲み方をしていきたい。

ポッドキャストを聞くことも継続していた。たべものRADIO、いんよう!、研エンの仲、サイエントーク、サイエンマニアあたりをよく聞いていた。とくに、いんよう!と研エンの仲の2番組は思考の言語化が良い。プレバトというTV番組の俳句のコーナーも、17音に観察と心情を落とし込む様子を楽しめて良い(俳句については、『ほしとんで』という漫画も知るきっかけとして良かった)。近年、自分の言語化能力の衰えを感じている。このままでは、世界がのっぺりとして、面白みが失われてしまう気がする。言葉を大切にするため、2023年は、辞書を読むことと漢検準1級の勉強をすることを、とりあえず手始めにやっていきたい。

アップロードしたスライドは

speakerdeck.com

のみである。2023年は勉強会で発表できる機会を見つけて、久しぶりに何かしら発表したい。

勉強面では、2021年同様、生存時間解析に入れ込んでいた。細く長くやっていることもあり、分かることも増えてきた(それと同時に、分からないということが分かることも増えた)。計量生物学会の生存時間解析のセミナーを受講したが、1年前では分からなかったであろうことが分かるようになっていて、前に進んでいることを実感した。今後も生存時間解析は勉強していくつもりだ。加えて、薬剤疫学や因果推論を勉強していくことが自分には必要だと考えている。疫学や生物統計は体系的に学ぶべきという気持ちも強くなっていて、MPHを修めに行くことも検討し始めている(それか、転職して分野を変えてしまうことも選択肢に加えている)。今年は30歳になる年で、まだまだ惑っているが、将来を見据えつつ、後悔のない人生を選んでいきたい。

キャッサバみたいな鯉がいた

猫の死因にもなりうる好奇心について今回は書く。つまりは、私の好奇心の成長記録のようなものを書く。

幼稚園にいた頃、何かを知ることはなんとなく好きだった。幼年期のことは今となっては記憶をほとんど引き出せやしないのだが、たとえば、ポケモンリザードがトカゲの英語に由来しているなどといったことが分かったときには、軽い興奮を覚えていたように思う。

だんだん物心らしきものが付いて、周りのことが見えるようになってきたのは小学四年生くらいの頃だった。そのくらいの時期に、クラスメイトが両手の親指と人差し指を広げて「ゲッツ」と言い始めたし、「ENJOY 音楽は鳴り続ける」と歌い始めた。他にもテレビ番組(おはスタエンタの神様トリビアの泉など)や音楽(ポルノグラフィティORANGE RANGEなど)が話題になっていた。最初は話についていくことができなかった。どういう経緯を経たのかは覚えていないが、バラエティー番組やお笑い番組、音楽番組を見るようになって、話題に追いつくことが少しはできるようになった。とはいえ、ドラマはほとんど全く見ていなかったので、ドラマの話(ごくせん、野ブタ。など)をされても全然分かっていなかったし、他にも分からない話はたくさんあった。分からないのは仕方ないと思いながらも、モヤモヤは心のどこかにあった。

人の話題に置いていかれたり、追いついていったりしながら、小学6年生のとき、中学入試を受け、私立の中高一貫の男子校に入学することとなった。中1のクラスではドラゴンボールが少し流行っていたので、ブックオフで頑張って買い集めて読んでいた。その後、ドラゴンボールの元ネタの西遊記を図書館で借りて全て読破したりもした。本や漫画の読書量が増え始めたのはこの時期くらいからだった。家の近くの本屋で見かけたドラゴン桜を買って読んだのが、中3の頃だったと思うが、そのころから、勉強量を増やしていった。学ぶことが増えていくと、知識の点がばらまかれ、ばらまいた点が線になり、線と線が面に、立体に、高次元の何かしらになっていくことに快感を覚えていく。学校の授業で学ぶことに限らず、あらゆる知識はつながっていることに気付き、知ることの快感を知ると、好奇心はさらに成長していった。それと同時に、友人と話すとき、その人が興味を持っていることについて知っていれば、そのことを話すことができ、会話が盛り上がる。その快感もまた、好奇心の成長を促進していった(教養とは、共有できる日常の空間を広げるものだと、個人的に定義しているのは、こういった経験からである)。これらの2つの快感はいつしか中毒的な感情になっていく。

浪人期を経て、大学に入ると、知識獲得へのリソースの配分の自由度が増えた。大学の教養の講義や専門の講義も面白いものは面白いし、高校までに蓄えた基礎知識で読めるものが増えた読書も面白いし、映画館や美術館、好きなバンドのライブに行くのも面白い。スポーツをやるのも観るのも、アニメを観るのも、漫画を読むのも、ゲームをするのも、美味しいごはんを食べて美味しいお酒を飲むのも、どれも面白い。面白さが溢れる中で、時間とエネルギー、とりわけお金が足りないという当たり前の事実に衝突する。予算制約の下で意思決定を行うことの難しさに直面する。選択肢の多さ(知らない選択肢の多さも含め)も、自分の価値観(価値関数)の分からなさも、この難題の要因となる。選択肢を取捨選択すること(とくに捨てること)、未知の選択肢を探索すること、自己の評価の仕方の検討することの重要性を実感していた。そして、これらのことは余裕の上に成り立っていることであり、余裕の有難さへ感謝をするようになった。

人差し指の愛人 - 霞と側杖を食らう

これは酒です - 霞と側杖を食らう

上の二つの記事は似たようなことを考えている。

会社に入ってからは、時間とエネルギーの制約が厳しくなっていく。余裕が少しずつ失われ、まとまった時間やまとまったエネルギーがないと消化できないものを味わいにくくなった。歯ごたえのあるものを味わえなくなっていく自分の衰えとともに、恐れを感じている。数学の本を丁寧に読むことはもう難しい。仕事や家庭の多忙さに追われていくのが人生であることは理解しているが、新しいものをディグっていくことが全くできない人々が周りにいるし、自分もそうなっていくのを感じている。ある程度はそうなることを受容しているが、反発している自分がそこにいる。少し強迫的かもしれない。このような感情を同じように抱いている人もいて、『研エンの仲』というポッドキャストのep39やep72では文化を咀嚼する顎の筋力が衰退に対するが語られていた。彼らの会話に痛く共感した。

自分の中の好奇心というモンスターが大きくなっている一方、飼いきれなくなりつつあるのかもしれない。中毒的で強迫的にまで肥大化し、怪物のように育った好奇心と、今後も上手に付き合っていかないといけない。これからも成長を見守っていくしかないのである。

ggplot2の拡張パッケージを思い出すための覚書

 

【用途】

Rのggplot2の便利な拡張パッケージの存在を覚えているが, 名前を思い出せなくて, 関連ワードで頑張ってググって探し出すために時間を使ってしまうことがある. そんなときのために簡単に思い出すためのメモ.

【内容】

たまに使うけれど, 名前を忘れがちなパッケージを記録.
ちなみに, “ggplot2 extension”などで検索すると以下のサイトがヒットし, ここではggplot2の拡張が数多く紹介されている.
https://exts.ggplot2.tidyverse.org/gallery/
このへんを参考にしつつメモしておく.

{tidyverse}パッケージ読み込み.

library(tidyverse)

ペンギンのデータ使用のためパッケージ読み込み.

library(palmerpenguins)
str(penguins)
## tibble [344 x 8] (S3: tbl_df/tbl/data.frame)
##  $ species          : Factor w/ 3 levels "Adelie","Chinstrap",..: 1 1 1 1 1 1 1 1 1 1 ...
##  $ island           : Factor w/ 3 levels "Biscoe","Dream",..: 3 3 3 3 3 3 3 3 3 3 ...
##  $ bill_length_mm   : num [1:344] 39.1 39.5 40.3 NA 36.7 39.3 38.9 39.2 34.1 42 ...
##  $ bill_depth_mm    : num [1:344] 18.7 17.4 18 NA 19.3 20.6 17.8 19.6 18.1 20.2 ...
##  $ flipper_length_mm: int [1:344] 181 186 195 NA 193 190 181 195 193 190 ...
##  $ body_mass_g      : int [1:344] 3750 3800 3250 NA 3450 3650 3625 4675 3475 4250 ...
##  $ sex              : Factor w/ 2 levels "female","male": 2 1 1 NA 1 2 1 2 NA NA ...
##  $ year             : int [1:344] 2007 2007 2007 2007 2007 2007 2007 2007 2007 2007 ...
df_penguins <-
  penguins %>% mutate(
    year = as.factor(year)
  )

データの関係性の全体的な可視化

{GGally}パッケージ

GGally::ggpairs(df_penguins)



GGally::ggpairs(df_penguins, aes(colour=species,alpha=0.8))



相関係数行列の可視化

{ggcorrplot}パッケージ

pen_corr <- df_penguins %>% 
  select(bill_length_mm, bill_depth_mm, flipper_length_mm, body_mass_g) %>% 
  drop_na(everything())
corr <- cor(pen_corr)
ggcorrplot::ggcorrplot(corr)



欠損データの可視化

{naniar}パッケージ

欠損データ, 欠測データの状況を可視化.

naniar::vis_miss(df_penguins)



naniar::gg_miss_var(df_penguins)



naniar::gg_miss_upset(df_penguins)



色覚多様性のための色使い

{viridis}パッケージ

library(viridis)
ggplot(df_penguins, aes(x=species,y=flipper_length_mm,fill=species))+
  geom_boxplot() +
  scale_fill_viridis_d() +
  scale_colour_viridis_d() +
  theme_bw()



ggplot(df_penguins, aes(x=bill_length_mm, y=bill_depth_mm, colour=species, shape=island)) +
  geom_point(size=2) +
  scale_color_viridis_d() +
  theme_bw()



Publication 向けのプロット

{ggpubr}パッケージ

ggpubr::ggboxplot(df_penguins, x = "species", y = "flipper_length_mm",
          color = "species", add = "jitter", shape = "species")



{ggthemes}パッケージ

Excel風のテーマ.

ggplot(df_penguins, aes(x=bill_length_mm, y=bill_depth_mm, colour=species, shape=island)) +
  geom_point(size=2) +
  ggthemes::theme_excel_new()

 

Stata風のテーマ.

ggplot(df_penguins, aes(x=bill_length_mm, y=bill_depth_mm, colour=species, shape=island)) +
  geom_point(size=2) +
  ggthemes::theme_stata()



{ggsci}パッケージ

df_ave_mass_penguins <- df_penguins %>%
  group_by(species, island) %>% 
  summarise(
    ave_body_mass_g = mean(body_mass_g,na.rm=TRUE)
  ) %>% 
  mutate(species_island=str_c(species,"-",island))

JAMA風の配色.

ggplot(df_ave_mass_penguins, aes(x=species_island, y=ave_body_mass_g, fill=species_island)) +
  geom_bar(stat="identity") +
  ggsci::scale_fill_jama()



Lancet風の配色.

ggplot(df_ave_mass_penguins, aes(x=species_island, y=ave_body_mass_g, fill=species_island)) +
  geom_bar(stat="identity") +
  ggsci::scale_fill_lancet()



NEJM風の配色.

ggplot(df_ave_mass_penguins, aes(x=species_island, y=ave_body_mass_g, fill=species_island)) +
  geom_bar(stat="identity") +
  ggsci::scale_fill_nejm()



simpsons風の配色.

ggplot(df_ave_mass_penguins, aes(x=species_island, y=ave_body_mass_g, fill=species_island)) +
  geom_bar(stat="identity") +
  ggsci::scale_fill_simpsons()



{ggtech}パッケージ

twitter風の配色.

# devtools::install_github("ricardo-bion/ggtech")
ggplot(df_ave_mass_penguins, aes(x=species, y=ave_body_mass_g, fill=species)) +
  geom_bar(stat="identity") +
  ggtech::scale_fill_tech(theme = "twitter")



{ggExtra}パッケージ

plotを複数並べる.

library(wooldridge)    # appleデータを使用するため
## Warning: package 'wooldridge' was built under R version 4.0.5
pp_apple <- ggplot(data = apple, aes(x = reglbs, y = ecolbs)) + 
  geom_point(color = "red") + theme_bw()
ggExtra::ggMarginal(pp_apple, margins = "x", col = "blue")
ggExtra::ggMarginal(pp_apple, margins = "both", size = 5, type = "histogram",
           col = "blue", fill = "orange") 



{survminer}パッケージ

生存曲線をggplot2の形で描く.

library(survival)
fit_surv <- survfit(Surv(exit, event) ~ sex, data = eha::child)
survminer::ggsurvplot(fit_surv, data=eha::child, 
                      risk.table = TRUE, tables.theme = survminer::theme_cleantable())



fit_surv <- survfit(Surv(time, status) ~ rx, data = survival::rats)
survminer::ggsurvplot(fit_surv, data=survival::rats, ncensor.plot = TRUE, pval = TRUE, pval.method = TRUE,
                      risk.table = TRUE, tables.theme = survminer::theme_cleantable())



{ggmap}パッケージ

ハドリー召喚.

ggmap::ggimage(ggmap::hadley)



【記憶の検索キーワード】

ggplot2, 拡張, パッケージ, GGally, ggpairs, ggcorrplot, naniar, 欠損, 欠測, 可視化, viridis, 色覚多様性, ggpubr, ggthemes, excel, stata, ggsci, theme, JAMA, Lancet, NEJM, simpsons, twitter, ggExtra, survminer, ggsurvplot, ggmap, hadley

 

文字禍福は糾える縄の如し

「文字は、まるで奇跡ですよ」、このセリフが出てきたのは、『チ。-地球の運動について-』という漫画だ。この漫画は、異端な思想として弾圧される地動説というアイデアに魅了された人々が、命をかけて地球の運動の研究を進め、思いを繋いでいく話を描いたものだ。異端審問官に弾圧されようとも、地動説への思いは、文字を通じて後の世代へ伝わっていく。文字を通してならば、空間的にも時間的にも離れた人の意思を受け取ることができるし、受け渡すこともできる。そういうわけで、文字は奇跡なのだ。漫画のコマ割りは四角四角で単純なのだけれど力強いカットの変化や、癖になる自由なルビの振り方が面白く、これらを通して作者の思いは読者の私にも伝わった。

別の科学漫画で、『DR.STONE』という漫画も読んだ。人類が突然、石になってしまって、数千年経過した世界で、たった一人で石化が解けた高校生の少年が科学の力を駆使して文明を1から取り戻していく漫画なのだが、とてつもなく良くできたサイエンスフィクションだった。この漫画のテーマは、問題解決のために未来へとただ地道に楔を打ち続けることの大切さだと思う。このテーマは最初から最後まで一貫していた。この繰り返されるテーマに加えて、ラスト数巻あたりで、科学を介して思いを託された、とあるキャラクターの孤独な奮闘に思わず涙してしまった。『DR.STONE』も『チ。』も、信念を託す場面で役立ったのは、やはり文字だったのだ。

DR.STONE』を読んでみて、人類の歴史というものが気になったので、ユヴァル・ノア・ハラリの『サピエンス全史』を読んでみた。この本では、人類の歴史を、認知革命、農業革命、科学革命の3つの革命を経た流れで説明している。ホモ・サピエンスはまず、認知能力が向上したおかげで、情報伝達能力が上昇した。さらに、言葉を使った想像上の現実、つまり虚構を作る能力を手に入れた。このおかげで、ダンバー数と言われる150人を超えた集団を形成して、見知らぬ人とも協力した行動できるようになった。その後、動植物種の分布の関係もあり、中東、中国、中央アフリカの地域で家畜化や栽培化が始まり、農耕社会を築いていった。この結果として、生まれる食糧余剰が政治・経済・宗教・文化の原動力となっていく。こうして、社会は、持ち前の虚構能力に支えられた貨幣と宗教の力で、さらに拡大していき、世界の一体化へ向かっていく。帝国として拡大して、資本を蓄積し、科学を発展させ、さらなる拡大を促す。このループを繰り返し、ヨーロッパの国々は覇権を握っていった。かなりざっくり要約すると、こんな感じだろうか。虚構を操る能力が人間の中で重要な役割を負っているというのは、自分の直観とも合っていたし、世界史の断片的な知識の復習になったものだから、面白い読み物として読んでいた。

しかし、この本を読んでいたところ、ユヴァル・ノア・ハラリへの批判記事(https://www.currentaffairs.org/2022/07/the-dangerous-populist-science-of-yuval-noah-harari)がtwitterで少し話題になった。この記事は、Darshana Narayananという神経科学者のジャーナリストが書いたものだ。『サピエンス全史』等の著作には多くの誤りが含まれているにも関わらず、巧みな語り口で数多くの読者を魅了し、ハラリが科学の権威的立場に立ってしまっていることを、"Science populists"という言葉を使って批判している。『サピエンス全史』に関する誤りの指摘は、進化生物学の観点から歯がゆい表現、動物の言語に関する表現、ヒョウとチーターの混同、エクアドルの民族の事実関係、未来の予測の誤りといったところだった。これらのファクトチェックの粗さから、徹底的に検証すれば、もっと大きな粗が出てきて瓦解するだろうと推察している。この記事の指摘が、どれくらい的を射ていて重要なものなのか、個人的にはよく分からないところもあってモヤモヤした(研究者の仲間に聞いたら、こんな違うって言ってましたわーみたいな書き方なのも、少しひっかかった)。雑さの指摘であれば、5年前に書かれていた書評(https://shorebird.hatenablog.com/entries/2016/11/01)の方が良いように感じた。

歴史学者が人類の歴史を語るために、生物学周辺の知見を使っていることや、おそらく専門ではない幅広い射程の歴史を扱っていることから、こんなことになっているのだろう。レファレンスの少なさも雑さ加減の問題な気がする。少なくとも『サピエンス全史』に関しては、ある程度割り引いて評価をするのが良いという結論で妥当なのだろう。歴史学をやってきた人には、どう見えているのだろうかは気になった。

『サピエンス全史』よりは少し以前に流行った人類の歴史を描いた本がジャレド・ダイアモンド『銃・病原菌・鉄』だが、これに対して日本の地理学の観点から批判的に書評などを見ている記事を最近、見かけた。『日本の地理学は『銃・病原菌・鉄』をいかに語るのか―英語圏と日本における受容過程の比較検討から―』(https://www.jstage.jst.go.jp/article/ejgeo/7/2/7_225/_article/-char/ja/)である。『銃・病原菌・鉄』は未読だったのだが、どういう点が批判されているのか確かめるために現在読んでいる。ジャレド・ダイアモンドは生物地理学者が本職のようで(このことは、ポッドキャストの『すごい進化ラジオ』という番組を聴いて知った。ちなみに、この番組で解説される生態と進化の語り口は分かりやすくて面白い。)、読んでいて論の切り口に本職の空気を感じた。日本の地理学者らの記事に書かれていることは、たしかになるほどと思いながら、『銃・病原菌・鉄』を読み進めている。

さて、書かれていることで、見分けがつきやすい間違いや論理的に明らかな間違いなら判断できる。しかし、記述されている学問を学んでいないと、その学者がどういう立ち位置なのか、書かれていることが妥当なことを言っているのかを判定できないことも多々ある(というかほとんどのものは確定的に判定できない)。過去に学んだ学問でさえも、時間が経過すれば学問も前に進んでいて、以前に正しいとされていたことも間違っていたと示されることもある(過去に学んでいたわけではないが、ネアンデルタール人ホモサピエンスの交雑の話やスーラの自画像の話は2010年代に科学技術で新たにひっくり返っているそうだ)。批判的に読むことが肝要なのは分かっていても、その困難さを改めて実感し、周章狼狽している。降りかかる文字共に無慙に圧死しないように、賢い人間であれるように、鍛錬を積み続けないといけない。

Immortal time biasに対する統計解析手法のシミュレーションの学習記録その2

【学習動機】

その1のつづき.

moratoriamuo.hatenablog.com

Immortal time bias(不死時間バイアス)に関して, Mi et al. 2016 “Use of the landmark method to address immortal person-time bias in comparative effectiveness research: a simulation study”のシミュレーション(naiveな方法, 未治療時間を削除する方法, ランドマーク解析(Landmark analysis), Mantel-Byarの方法を比較している.)をRでreplicateしてみている.

【学習内容】

パッケージ読み込み.

# パッケージ読み込み ########################
library(tidyverse)
library(survival)
library(survminer)

前回のコードを活用して, シミュレーション回数と各シミュレーションのサンプルサイズと真のハザード比を決定して, シミュレーションを回し, 推定値のヒストグラムとバイアスとMSEが出力される関数を作成.
(修正(2022-07-01) : twitterでnissinboさんからコメントをいただき,
treatの変数の書く位置を間違えていたことに気付けたので訂正しています. コメント, ありがとうございました. Naive MethodとExclusion Methodの結果が修正で変わっています.)

# 関数化 ######################################################

sim_Mi2016 <- function(nsim, N, HR_0){
  gamma_0 <- log(HR_0)
  
  # 基本設計 #################################

  fup_max <- 5 * 365 # 5years, max followup
  
  LHR_true <- as.numeric(nsim)
  LHR_naive <- as.numeric(nsim)
  LHR_EM <- as.numeric(nsim)
  LHR_LM_1yr <- as.numeric(nsim)
  LHR_LM_2yr <- as.numeric(nsim)
  LHR_MB <- as.numeric(nsim)
  
  # パラメータ設定 #############################
  alpha_0 <- -1.14
  alpha_L <- log(1.1)
  alpha_M <- log(1.25)
  alpha_H <- log(1.5)
  
  # treatment timeのパラメータ
  beta_T <- 1 # 0.5, 1, 1.5
  lambda_T <- 1/800 # 1/800
  # event timeのパラメータ
  beta_E <- 1 # 0.5, 1, 1.5
  lambda_E <- 1/500
  
  for(i in 1:nsim){
    # データ生成 #############################
    df0 <- data.frame(
      # 共変量 X1~X8 の生成
      X1 = sample(x = c(1,0), size = N, replace = TRUE, prob = c(0.3,0.7)),
      X2 = sample(x = c(1,0), size = N, replace = TRUE, prob = c(0.4,0.6)),
      X3 = sample(x = c(1,0), size = N, replace = TRUE, prob = c(0.5,0.5)),
      X4 = sample(x = c(1,0), size = N, replace = TRUE, prob = c(0.5,0.5)),
      X5 = rnorm(N, mean=0, sd=1),
      X6 = rnorm(N, mean=0, sd=1),
      X7 = rnorm(N, mean=0, sd=1),
      X8 = rnorm(N, mean=0, sd=1))
    
    df1 <- df0 %>% 
      mutate(
        c0 = exp(alpha_0 + alpha_L*X1 + alpha_M*X2 + alpha_H*X3 + alpha_L*X5 + alpha_M*X6 + alpha_H*X7),
        c1 = exp(alpha_0 + alpha_L*X1 + alpha_M*X2 + alpha_H*X4 + alpha_L*X5 + alpha_M*X6 + alpha_H*X8),
        # 逆変換に使用する一様乱数の生成
        u0 = runif(N, min=0, max=1),
        u1 = runif(N, min=0, max=1)
      )
    
    # treatment(exposure) timeの乱数生成 : ワイブル分布 
    ft0 <- function(u0, lambda0, beta0, c0){
      1/lambda0 * ((-log(1-u0))/c0)^(1/beta0)
    }
    
    # event timeの乱数生成(場合分け) : 時間依存性共変量の治療Zに対応
    ft1_1 <- function(u1, lambda1, beta1, c1){
      1/lambda1 * ((-log(1-u1))/c1)^(1/beta1)
    }
    ft1_2 <- function(u2, lambda2, beta2, b2, c1, c2){
      ((-log(1-u2)-c1*(lambda2*b2)^(beta2))/(c2*lambda2^beta2) + b2^beta2)^(1/beta2)
    }

    df2 <- df1 %>% 
      mutate(
        t0 = ft0(u0=u0, lambda0 = lambda_T, beta0 = beta_T, c0 = c0),    # treatment time
        Z = ifelse(t0<=fup_max, 1, 0),    # 1:treated, 0:untreated
        #treat = ifelse(Z==1, "yes", "no"), (ミス. eventの時間との兼ね合いがあるので, ここで治療はまだ定義できない)
        c2 = exp(alpha_0 + alpha_L*X1 + alpha_M*X2 + alpha_H*X4 + alpha_L*X5 + alpha_M*X6 + alpha_H*X8 + gamma_0*Z),
        t1_1 = ft1_1(u1 = u1, lambda1 = lambda_E, beta1 = beta_E, c1 = c1),
        t1_2 = ft1_2(u2 = u1, lambda2 = lambda_E, beta2 = beta_E, b2 = t0, c1 = c1, c2 = c2)
      )
    
    
    df3 <- df2 %>% 
      mutate(
        t1 = ifelse(u1<1-exp(-c1*(lambda_E*t0)^beta_E), t1_1, t1_2),    # event time
        
        time_event = ifelse(t1 < fup_max, t1, fup_max),    # event time or  max followup
        time_treat = ifelse(t0 < fup_max & t0 < time_event, t0, NA),    # treat tiem
        flg_treat = ifelse(t0 < fup_max & t0 < time_event, 1, 0),        # 1:treated, 0:untreated
        treat = ifelse(flg_treat==1, "yes", "no"),    # こっちに修正
        flg_event = ifelse(t1 < fup_max, 1, 0)
      )

    
    # C : Naive Method############################################
    df_naive <- df3
    res_cox_naive <- survival::coxph(Surv(time_event, flg_event)~treat+X1+X2+X4+X5+X6+X8, data = df_naive)
    
    # B : Exclusion Method############################################
    
    df_EM <- df3 %>% 
      mutate(
        time_event_EM = ifelse(flg_treat==1, time_event-time_treat, time_event)
      )
    res_cox_EM <- survival::coxph(Surv(time_event_EM, flg_event)~treat+X1+X2+X4+X5+X6+X8, data = df_EM)
    
    # D : Landmark Method############################################
    
    LM_year <- 1
    df_LM_1yr <- df3 %>% 
      filter(time_event >= 365*LM_year) %>% 
      mutate(
        time_event_LM_1yr = time_event - 365*LM_year,
        time_treat_LM_1yr = time_treat - 365*LM_year,
        flg_treat_LM_1yr = ifelse((time_treat >= 365*LM_year | is.na(time_treat)), 0, 1),
        treat_LM_1yr = ifelse(flg_treat_LM_1yr==1, "yes","no")
      )
    
    res_cox_LM_1yr <- survival::coxph(Surv(time_event_LM_1yr, flg_event)~treat_LM_1yr+X1+X2+X4+X5+X6+X8, data = df_LM_1yr)
    
    LM_year <- 2
    df_LM_2yr <- df3 %>% 
      filter(time_event >= 365*LM_year) %>% 
      mutate(
        time_event_LM_2yr = time_event - 365*LM_year,
        time_treat_LM_2yr = time_treat - 365*LM_year,
        flg_treat_LM_2yr = ifelse((time_treat >= 365*LM_year | is.na(time_treat)), 0, 1),
        treat_LM_2yr = ifelse(flg_treat_LM_2yr==1, "yes","no")
      )
    
    res_cox_LM_2yr <- survival::coxph(Surv(time_event_LM_2yr, flg_event)~treat_LM_2yr+X1+X2+X4+X5+X6+X8, data = df_LM_2yr)
    
    # A : Mantel-Byar Method############################################
    
    df3_MB1 <- df3 %>% mutate(id=1:nrow(df3))
    
    df3_MB2 <- 
      df3_MB1 %>% tmerge(df3_MB1, id=id, flg_event_MB=event(time_event,flg_event)) %>% 
      tmerge(df3_MB1, id=id, flg_treat_MB=tdc(time_treat))
    
    res_cox_MB <- survival::coxph(Surv(tstart,tstop,flg_event_MB)~flg_treat_MB+X1+X2+X4+X5+X6+X8, data = df3_MB2)
    
    
    # 真の値の推定値の保存(Log HR)
    
    LHR_true[i] <- gamma_0
    LHR_naive[i] <- res_cox_naive$coefficients[1]
    LHR_EM[i] <- res_cox_EM$coefficients[1]
    LHR_LM_1yr[i] <- res_cox_LM_1yr$coefficients[1]
    LHR_LM_2yr[i] <- res_cox_LM_2yr$coefficients[1]
    LHR_MB[i] <- res_cox_MB$coefficients[1]
    
  }
  
  
  # 結果の整理#######################################################
  
  df_LHR <- data.frame(
    LHR_true = LHR_true,
    LHR_naive = LHR_naive,
    LHR_EM = LHR_EM,
    LHR_LM_1yr = LHR_LM_1yr,
    LHR_LM_2yr = LHR_LM_2yr,
    LHR_MB = LHR_MB
  )
  
  df_LHR2 <- 
    df_LHR %>% mutate(
      bias_naive = (LHR_naive-LHR_true),
      bias_EM = (LHR_EM-LHR_true),
      bias_LM_1yr = (LHR_LM_1yr-LHR_true),
      bias_LM_2yr = (LHR_LM_2yr-LHR_true),
      bias_MB = (LHR_MB-LHR_true),
      
      SE_naive = (LHR_naive-LHR_true)^2,
      SE_EM = (LHR_EM-LHR_true)^2,
      SE_LM_1yr = (LHR_LM_1yr-LHR_true)^2,
      SE_LM_2yr = (LHR_LM_2yr-LHR_true)^2,
      SE_MB = (LHR_MB-LHR_true)^2
    )
  
  
  # ggplotで扱うため, long形式に
  df_LHR2_long <- 
    df_LHR2 %>% rowid_to_column() %>% 
    pivot_longer(
      cols = starts_with("LHR"),
      names_to = "Method",
      values_to = "LHR") %>% 
    select(rowid, Method, LHR) %>% 
    mutate(Method=str_remove(Method,"LHR_")) %>% 
    filter(Method != "true") %>% 
    mutate(
      Method = factor(Method, levels = c("naive","EM","LM_1yr","LM_2yr","MB"))
    )
  
  # 推定結果をヒストグラム
  p_est <-  ggplot(data=df_LHR2_long, aes(x=LHR)) +
    geom_histogram(colour="white", bins = 30) +
    facet_grid(Method~.) +
    geom_vline(aes(xintercept=gamma_0), colour="red") +
    theme_bw() + 
    ggtitle(label = str_c("Histograms of Estimates :  Log HR = ", round(gamma_0,2)))
  
  # average of Bias
  ave_bias <-   
    df_LHR2 %>% summarise(
      across(starts_with("bias"), mean)
    )
  # MSE
  MSE <-   
    df_LHR2 %>% summarise(
      across(starts_with("SE"), mean)
    )
  
  list(
    hist = p_est,
    ave_bias = ave_bias, 
    MSE = MSE)
}

シミュレーション回数を1000回, サンプルサイズを1000とし(論文では10000だが速度の都合で1000としている), ハザード比を0.5, 0.7, 1.0, 1.3, 1.5 と変更して実験.

nsim <- 1000    # number of simulations
N <- 1000 # sample size(論文では10000だが速度の問題で1000としている)

# Hazard Ratio(HR)を 0.5, 0.7, 1.0, 1.3, 1.5 変更
set.seed(2022)
res_HR_0.5 <- sim_Mi2016(nsim=nsim, N=N, HR_0=0.5)
res_HR_0.7 <- sim_Mi2016(nsim=nsim, N=N, HR_0=0.7)
res_HR_1.0 <- sim_Mi2016(nsim=nsim, N=N, HR_0=1.0)
res_HR_1.3 <- sim_Mi2016(nsim=nsim, N=N, HR_0=1.3)
res_HR_1.5 <- sim_Mi2016(nsim=nsim, N=N, HR_0=1.5)

真のハザード比が0.5のときの各手法による推定値のヒストグラム.

res_HR_0.5$hist

真のハザード比が0.7のときの各手法による推定値のヒストグラム.

res_HR_0.7$hist

真のハザード比が1.0のときの各手法による推定値のヒストグラム.

res_HR_1.0$hist

真のハザード比が1.3のときの各手法による推定値のヒストグラム.

res_HR_1.3$hist

真のハザード比が1.5のときの各手法による推定値のヒストグラム.

res_HR_1.5$hist

df_ave_bias <- 
  rbind.data.frame(
    res_HR_0.5$ave_bias,
    res_HR_0.7$ave_bias,
    res_HR_1.0$ave_bias,
    res_HR_1.3$ave_bias,
    res_HR_1.5$ave_bias
  ) %>% mutate(
    HR = c(0.5, 0.7, 1.0, 1.3, 1.5)
  )

df_ave_bias_long <-
  df_ave_bias %>% 
  pivot_longer(
    cols = starts_with("bias"),
    names_to = "Method",
    values_to = "bias"
  ) %>% mutate(Method=str_remove(Method,"bias_")) %>% 
  mutate(
    Method = factor(Method, levels = c("naive","EM","LM_1yr","LM_2yr","MB"))
  )

真のハザード比を変えていったときの,対数ハザード比の真の値と各手法の推定値とのズレの平均値の折れ線グラフ.

ggplot(data=df_ave_bias_long, aes(x=HR, y=bias, colour=Method)) +
  geom_line() +
  geom_point(aes(shape=Method)) +
  ggtitle(label = "average bias of estimated log HR ")

 

df_ave_MSE <- 
  rbind.data.frame(
    res_HR_0.5$MSE,
    res_HR_0.7$MSE,
    res_HR_1.0$MSE,
    res_HR_1.3$MSE,
    res_HR_1.5$MSE
  ) %>% mutate(
    HR = c(0.5, 0.7, 1.0, 1.3, 1.5)
  )

df_ave_MSE_long <- 
  df_ave_MSE %>% 
  pivot_longer(
    cols = starts_with("SE"),
    names_to = "Method",
    values_to = "MSE"
  ) %>% mutate(Method=str_remove(Method,"SE_")) %>% 
  mutate(
    Method = factor(Method, levels = c("naive","EM","LM_1yr","LM_2yr","MB"))
  )

真のハザード比を変えていったときの,対数ハザード比の真の値と各手法の推定値とのズレの二乗値の平均値(MSE)の折れ線グラフ.

ggplot(data=df_ave_MSE_long, aes(x=HR, y=MSE, colour=Method)) +
  geom_line() +
  geom_point(aes(shape=Method)) +
  ggtitle(label = "MSE of estimated log HR ")

 

Mi et al. 2016のβがtreatmentもeventも1のもののみ, 今回扱っている. コードの関数の引数にβを入れて変更できるようにすれば他のβの場合も扱えるが, 面倒なのとそんなに個人的に重要ではなさそうなのでここではやらない.  ランドマーク解析法とMantel-Byarの方法については, 結果は似たようになっているが, Exclusion Methodについては, 少しズレているような気がする. 今のところ, どこが間違っているのか(それとも間違っていないのか)分かっていない. HRが大きくなればなるほど, biasは減っている. HRが大きいということはtreatmentを受けたらすぐにeventが起きるということになる. Exclusion Methodがどうバイアスを引き起こしているか分からないが, treatmentの時間が短い分だけbiasが生じないようになっているのかもしれない. naiveな手法はHRが1から離れるほどbiasが大きくなっている. 正しくシミュレーションできているのか完全な自信はないので, 今後もう少し学びながら, 検討していくこととする. Naive MethodとExclusion Methodについて修正した結果, 論文のシミュレーションの結果図を全体的に良い感じに再現できたと思われる. 良かった.
ランドマーク解析も頑張っているとはいえ, やはりMantel-Byarの方法がHRに関わらず良い性能を示している. Exclusion MethodはNaive Methodよりはマシだが, バイアスが生じていることが分かる. Naive Methodは論外的にズレている. おそらく正しくシミュレーションをreplicateできていると思う.

【学習予定】

今回は以下の4つの文献を参考にした.

  • Mi et al. 2016
    “Use of the landmark method to address immortal person-time bias in comparative effectiveness research: a simulation study”
    https://pubmed.ncbi.nlm.nih.gov/27350312/
    治療の定義の都合でアウトカムが起きえないフォローアップの期間によって生じるimmortal-time biasがあるが, バイアスを回避する解析方法として, Mantel-Byarの方法とlandmarkの方法がある.
    上の2つの方法とnaiveな方法とexclusionの方法の4つをシミュレーションで評価する.
    論文ではSASで書かれていてコードもない. これをRで再現実験する.

  • Leemis et al. 1990
    “Variate generation for accelerated life and proportional hazards models with time dependent covariates” https://www.sciencedirect.com/science/article/abs/pii/0167715290900529
    時間依存性共変量付きの比例ハザードモデルと加速故障モデルの乱数生成方法を解説.
    一様乱数uを-log(1-u)に変換して, これが0~, ハザード関数の逆関数で時間tに対応させて乱数を生成.

  • Weberpals et al. 2018
    “Comparative performance of a modified landmark approach when no time of treatment data are available within oncological databases: exemplary cohort study among resected pancreatic cancer patients” https://pubmed.ncbi.nlm.nih.gov/30214315/
    ランドマーク解析やMantel-Byar methodのSASのコードが掲載されている.

  • Gleiss 2018
    “An unjustified benefit: immortal time bias in the analysis of time-dependent events”
    https://onlinelibrary.wiley.com/doi/10.1111/tri.13081
    immortal time biasについて学ぶのに参考になった.

学んでいる中で, 下の最近のレビューを発見した. Mi et al. 2016では比較されていない手法も上がっている.

  • Wang et al. 2022
    “Statistical Methods for Accommodating Immortal Time: A Selective Review and Comparison” https://arxiv.org/abs/2202.02369 2022年のpre printのレビュー. target trial emulationへのclone-censor-weightの3 stepsのアプローチも掲載されている.
    この手法は下の文献で平易に解説されている.

  • Hernán 2018
    “How to estimate the effect of treatment duration on survival outcomes using observational data”
    https://pubmed.ncbi.nlm.nih.gov/29419381/

一通り目を通してみて, この手法のやり方はなんとなく分かったけど, なぜこれが上手くいくのか全く分かっていない.
これを今回のようにシミュレーションデータ作って, 試してみたい.

 

【追記:2022/07/11】

ランドマーク解析とCox回帰の関係性について書いている論文. 理解を促しそうなので, 追加しておく.