teal の話

西川 寛来

2026-04-08

スライドはこちら

自己紹介

こんな話をします 🙆

  • teal とはなにか
  • “あの日の CRAN” を使う
  • OSS contribution について

teal とはなにか

teal

活用場面

  • データモニタリング
  • 照会事項対応
  • 事後解析
  • 探索的な解析
  • (将来的には) 承認申請

💁 治験データのダッシュボードでやれることは多い

製薬企業の採用事例

これ以外にも事例は増えている印象。日本でもやっていきましょう

teal で作った Shiny はこんな感じ 🙋

teal.gallery efficacy

実装の雰囲気

app.R
# load libraries
library(teal)
library(teal.modules.general)
library(teal.widgets)
library(sparkline)

# teal_data object
data <- teal_data()
data <- within(data, {
  ADSL <- teal.data::rADSL
  ADTTE <- teal.data::rADTTE
})
join_keys(data) <- default_cdisc_join_keys[names(data)]

# initialize the app
app <- init(
  data = data,
  modules = modules(
    tm_data_table(),
    tm_variable_browser()
    # ここにモジュールを追加していく
  )
)

shinyApp(app$ui, app$server)
1
パッケージ読み込み
2
データと主キー変数を定義
3
モジュールの選択
  • 治験データでよく行われる分析手法をモジュールとして利用可能
    • 裏では tern パッケージが動いている

teal を使うメリット

インタラクティブな探索

  • ユーザー自身でフィルタの操作や、カスタムレポートを出力
  • 解析部門の負担を減らし、エンドユーザーの満足度を高める

コード管理による効率性と再現性

  • 試験間での横展開がしやすい
  • アプリ上の操作を再現する R コードを出力

モジュールによる標準化

  • 生物統計に特化しており、要求に素早く対応
  • ダッシュボードの作り方を揃えやすい

モジュールの探し方

UI で簡単にデータをフィルタリング

カスタムレポートを作る

表やグラフを組み合わせてレポートを出力可能 (html, pdf, pptx, docx)

アプリ上の操作をコードとして出力

https://rinpharma.shinyapps.io/nest_efficacy_stable/

コードをコピペして実行すると…

結果が再現された!

Code
library(dplyr)
library(random.cdisc.data)
library(nestcolor)
library(sparkline)
ADSL <- radsl(seed = 1)
.adsl_labels <- teal.data::col_labels(ADSL, fill = FALSE)
.char_vars_asl <- names(Filter(isTRUE, sapply(ADSL, is.character)))
.adsl_labels <- c(.adsl_labels, AGEGR1 = "Age Group")
ADSL <- ADSL %>% mutate(AGEGR1 = factor(case_when(AGE < 45 ~ "<45", AGE >= 45 ~ ">=45"))) %>% mutate_at(.char_vars_asl, factor)
teal.data::col_labels(ADSL) <- .adsl_labels
ADRS <- radrs(ADSL, seed = 1)
.adrs_labels <- teal.data::col_labels(ADRS, fill = FALSE)
ADRS <- filter(ADRS, PARAMCD == "BESRSPI" | AVISIT == "FOLLOW UP")
teal.data::col_labels(ADRS) <- .adrs_labels
stopifnot(rlang::hash(ADSL) == "a38e9170aea81f1199dfe480c202142e") # @linksto ADSL
stopifnot(rlang::hash(ADRS) == "b0d502767c7e9f7b2d9118c30570b61a") # @linksto ADRS
.raw_data <- list2env(list(ADSL = ADSL, ADRS = ADRS))
lockEnvironment(.raw_data) # @linksto .raw_data
ADRS <- dplyr::inner_join(x = ADRS, y = ADSL[, c("STUDYID", "USUBJID"), drop = FALSE], by = c("STUDYID", "USUBJID"))
library(magrittr)
ANL_1 <- ADSL %>% dplyr::select(STUDYID, USUBJID, ARM, STRATA1)
ANL_2 <- ADRS %>% dplyr::filter(PARAMCD == "BESRSPI") %>% dplyr::select(STUDYID, USUBJID, PARAMCD, AVISIT)
ANL_3 <- ADRS %>% dplyr::select(STUDYID, USUBJID, PARAMCD, AVISIT, AVALC)
ANL <- ANL_1
ANL <- dplyr::inner_join(ANL, ANL_2, by = c("STUDYID", "USUBJID"))
ANL <- dplyr::inner_join(ANL, ANL_3, by = c("STUDYID", "USUBJID", "PARAMCD", "AVISIT"))
ANL <- ANL %>% teal.data::col_relabel(ARM = "Description of Planned Arm", PARAMCD = "Parameter Code", STRATA1 = "Stratification Factor 1", AVALC = "Analysis Value (C)")
library(magrittr)
ANL_ADSL_1 <- ADSL %>% dplyr::select(STUDYID, USUBJID, ARM, STRATA1)
ANL_ADSL <- ANL_ADSL_1
ANL_ADSL <- ANL_ADSL %>% teal.data::col_relabel(ARM = "Description of Planned Arm", STRATA1 = "Stratification Factor 1")
anl <- ANL %>% dplyr::mutate(ARM = droplevels(ARM)) %>% dplyr::mutate(is_rsp = dplyr::if_else(!is.na(AVALC), AVALC %in% c("CR", "PR"), NA)) %>% dplyr::mutate(AVALC = factor(AVALC, levels = c("CR", "PR", "SD")))
ANL_ADSL <- ANL_ADSL %>% dplyr::mutate(ARM = droplevels(ARM)) %>% tern::df_explicit_na(na_level = "<Missing>")
lyt <- rtables::basic_table(show_colcounts = TRUE, title = "Table of BESRSPI for CR and PR Responders", subtitles = "Stratified by STRATA1") %>% rtables::split_cols_by(var = "ARM") %>% tern::estimate_proportion(vars = "is_rsp", conf_level = 0.95, method = "waldcc", table_names = "prop_est", denom = "N_col")
table <- rtables::build_table(lyt = lyt, df = anl, alt_counts_df = ANL_ADSL)
table
Table of BESRSPI for CR and PR Responders
Stratified by STRATA1

——————————————————————————————————————————————————————————————————————————————
                                   A: Drug X      B: Placebo    C: Combination
                                    (N=133)        (N=141)         (N=126)    
——————————————————————————————————————————————————————————————————————————————
Responders                        131 (98.5%)    134 (95.0%)     126 (100.0%) 
95% CI (Wald, with correction)   (96.1, 100.0)   (91.1, 99.0)   (99.6, 100.0) 

teal の歴史を歩む

v0.16.0 -> v1.0.0 にメジャーアップデートした際の Changelog を見てみる

v1.0.0 で何が変わった?

「UI を大幅刷新、bslib 使ってレイアウト改善したよ」とのこと

UI を比べてみる

  • めっちゃ変わった!!!
    • 全体的にモダンなデザイン
    • モジュール選択がタブからドロップダウンに
    • フィルタパネルの位置が変わった

“あの日の CRAN” を使う

バージョンを指定してインストールはできるけど…

パッケージのインストールには pak::pak() が便利です。使いましょう。

# バージョンを指定してインストール
pak::pak("teal@1.0.0")
  • デフォルトでは CRAN binary archive から指定バージョンをインストール
  • 指定したパッケージ以外は「本日の CRAN」を参照する
    • 依存パッケージはどうなる?
    • 複数パッケージをバージョン指定してダウンロードしたい時は?

🤔 依存パッケージなども含めて”当時の環境”をそのまま再現したい

P3M とは?

  • Posit Public Package Manager (P3M)
    • Posit 社が提供する パッケージリポジトリ
    • 指定した日付の CRAN snapshot を利用可能
      • Windows の例: https://packagemanager.posit.co/cran/2025-08-21

P3M を使って”あの日の CRAN” を使う

pak::repo_add()pak::repo_resolve() で CRAN snapshot をリポジトリとして追加する

# パッケージのバージョンを指定すると、CRAN リリースの翌日の snapshot を参照する
pak::repo_add(CRAN = pak::repo_resolve("PPM@teal-1.0.0"))

# 直接日付を指定してもOK
pak::repo_add(CRAN = "PPM@2025-08-21")

# 指定した CRAN snapshot でインストール
pak::pak("teal")

💁️ “あの日の CRAN” を使うテクニックは、製薬業界や承認申請周りで重要になりそう

OSS contribution について

teal に貢献してみよう

  • コミュニティが活発
    • プレゼンテーション・ウェビナーも頻繁に開催
    • Pharmaverse Slack のチャンネルもある
  • 機能追加の要望が多い
  • バグ報告 (= 伸びしろ) もまだまだある
  • 開発チームが親切で、初心者歓迎の雰囲気
  • 他社の統計プログラマーとの協働の機会

OSS contribution の流れ

  1. 興味のある issue を見てみる
  2. Fork して修正・動作確認
  3. Pull Request 送信
  4. レビュー対応 → マージ 🎉

PR を送ってみた

些細な貢献かもしれないが、自分が使っているツールを自分で改善する体験は楽しい

まとめ

やっていきましょう

🤝 teal

  • 治験データのダッシュボード作成に最適
  • 多くの企業で採用実績あり
  • エンドユーザーが自分でレポートを作れる
  • P3M で”あの日の CRAN” を使おう

♥️ OSS contribution

  • まずは good first issue から
  • 自分が使っているツールを自分で改善できる

Enjoy 🙋

紹介しきれなかった teal のお役立ちリンク