5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI for Science #31:Claude Code × Matplotlib で論文品質の科学図表 15 種を一気に作成してみた

5
Posted at

はじめに

本記事は「AI for Science」シリーズ第 31 回です。Claude Code (claude-opus-4-6) を活用し、科学論文で頻出する 15 種類の図表を 1 つの Python スクリプト で一括生成する方法を解説します。Nature / Science / Cell などのトップジャーナルが求める品質基準を満たすスタイリングを、すべてコードで再現しています。

本記事で作成する図表の全体像

動機

論文投稿時にレビュアーから「Figure の品質を上げてほしい」と指摘された経験はないでしょうか。多くの場合、以下の点が問題になります。

  • 解像度不足 (72 DPI の画面キャプチャをそのまま投稿)
  • フォントサイズが小さすぎる (印刷時に読めない)
  • カラムサイズを無視 (1 段分の幅に 2 段分の図を詰め込む)
  • 色覚多様性への配慮不足 (赤/緑の組み合わせ)
  • 不要な装飾 (3D 効果、グラデーション背景)

本スクリプトはこれらの問題を体系的に解決します。

環境

項目 バージョン
OS Linux (WSL2)
Python 3.12
matplotlib 3.10.8
seaborn 0.13.2
scipy 1.16.3
scikit-learn 1.8.0
networkx 3.6.1
AI Claude Code (claude-opus-4-6)
pip install --break-system-packages matplotlib seaborn scipy scikit-learn networkx adjustText

グローバル設定: 論文品質のスタイル定義

まず、スクリプト全体で共有するスタイル設定を定義します。これが一貫性のある図表群を作るための鍵です。

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt

RANDOM_STATE = 42
DPI = 300

# Nature-inspired color palette (ggsci パッケージの NPG カラーに近い)
NATURE_PALETTE = [
    '#E64B35', '#4DBBD5', '#00A087', '#3C5488', '#F39B7F',
    '#8491B4', '#91D1C2', '#DC0000', '#7E6148', '#B09C85',
]

# カラムサイズ (Nature 標準)
SINGLE_COL_MM = 89    # 1 段: 89 mm
DOUBLE_COL_MM = 183   # 2 段: 183 mm
MM_TO_INCH = 1 / 25.4
SINGLE_COL = SINGLE_COL_MM * MM_TO_INCH  # 3.50 in
DOUBLE_COL = DOUBLE_COL_MM * MM_TO_INCH  # 7.20 in

plt.rcParams.update({
    'font.family': 'DejaVu Sans',
    'font.size': 7,
    'axes.labelsize': 8,
    'axes.titlesize': 9,
    'xtick.labelsize': 6.5,
    'ytick.labelsize': 6.5,
    'legend.fontsize': 6.5,
    'figure.dpi': DPI,
    'savefig.dpi': DPI,
    'savefig.bbox': 'tight',
    'axes.linewidth': 0.6,
    'axes.spines.top': False,     # 上辺を消す
    'axes.spines.right': False,   # 右辺を消す
    'pdf.fonttype': 42,           # TrueType 埋め込み
    'ps.fonttype': 42,
})

なぜこの設定が重要か

設定 理由
font.size: 7 印刷時に 6pt 以上を確保 (多くのジャーナルの最小要件)
axes.spines.top/right: False Tufte の「データインク比」最大化
pdf.fonttype: 42 PDF にフォントをアウトライン化せず TrueType で埋め込む (編集可能)
DPI: 300 ほぼ全てのジャーナルで要求される最小解像度
SINGLE_COL = 89mm Nature / Science 共通の 1 カラム幅

Step 1: 基本統計プロット

Fig01: Violin Plot + Strip Plot (遺伝子発現)

image.png

バイオリンプロットは分布の形状を、ストリッププロットは個々のデータポイントを同時に表現できます。

def fig01_violin_stripplot():
    rng = np.random.default_rng(RANDOM_STATE)
    cell_types = ["T-cell", "B-cell", "Macrophage", "NK-cell", "Dendritic"]
    n_per_group = 60

    data = []
    for i, ct in enumerate(cell_types):
        vals = rng.normal(loc=2 + i * 0.8, scale=0.6 + i * 0.1, size=n_per_group)
        for v in vals:
            data.append({"Cell Type": ct, "Expression (log2 TPM)": v})
    df = pd.DataFrame(data)

    fig, ax = plt.subplots(figsize=(SINGLE_COL, SINGLE_COL * 0.75))

    # バイオリンパート
    parts = ax.violinplot(
        [df[df["Cell Type"] == ct]["Expression (log2 TPM)"].values for ct in cell_types],
        positions=range(len(cell_types)),
        showmeans=False, showmedians=False, showextrema=False,
    )
    for i, pc in enumerate(parts["bodies"]):
        pc.set_facecolor(NATURE_PALETTE[i])
        pc.set_alpha(0.3)

    # ストリッププロット (ジッター付き)
    for i, ct in enumerate(cell_types):
        vals = df[df["Cell Type"] == ct]["Expression (log2 TPM)"].values
        jitter = rng.uniform(-0.15, 0.15, size=len(vals))
        ax.scatter(i + jitter, vals, s=6, color=NATURE_PALETTE[i], alpha=0.6)
        ax.hlines(np.median(vals), i - 0.2, i + 0.2, color="black", linewidth=1.2)

    ax.set_ylabel("Expression (log$_2$ TPM)")
    ax.set_title("Gene Expression by Cell Type", fontweight="bold")

ポイント: showextrema=False で不要な線を消し、中央値は手動で水平線として描画しています。

Fig02: Grouped Bar Chart with Error Bars (薬剤 IC50)

image.png

drugs = ["Cisplatin", "Doxorubicin", "Paclitaxel", "5-FU"]
cell_lines = ["MCF-7", "HeLa", "A549"]

for j, cl in enumerate(cell_lines):
    means = rng.uniform(0.5, 15.0, size=len(drugs))
    sems = means * rng.uniform(0.08, 0.2, size=len(drugs))
    ax.bar(x + j * width - width, means, width, yerr=sems,
           label=cl, color=NATURE_PALETTE[j], capsize=2,
           error_kw={"linewidth": 0.6})

ポイント: capsize=2 でエラーバーのキャップ幅を控えめにし、error_kw で線幅を揃えます。

Fig03: Box-and-Whisker with Significance Brackets

image.png

有意差ブラケットの描画は論文図で頻出します。手動で描画する関数を用意しました。

def add_bracket(ax, x1, x2, y, p_text):
    """有意差ブラケットを描画"""
    ax.plot([x1, x1, x2, x2], [y, y + 0.2, y + 0.2, y], lw=0.7, color="black")
    ax.text((x1 + x2) / 2, y + 0.25, p_text, ha="center", va="bottom", fontsize=6)

# 使用例
add_bracket(ax, 1, 2, ymax, "ns")        # not significant
add_bracket(ax, 1, 3, ymax + 1.0, "*")   # p < 0.05
add_bracket(ax, 1, 4, ymax + 2.0, "***") # p < 0.001

Step 2: ゲノミクス図表

Fig04: Manhattan Plot (GWAS シミュレーション)

image.png

22 本の染色体にまたがるゲノムワイド関連解析の結果を可視化します。染色体 2, 8, 17 にシグナルピークを配置しました。

# 各染色体のデータを累積座標で配置
for c in range(22):
    n = chr_sizes[c]
    positions = np.sort(rng.integers(0, chr_sizes[c] * 1000, size=n))
    p = rng.uniform(0, 1, size=n)
    # ピーク染色体にシグナルを追加
    if c + 1 in [2, 8, 17]:
        peak_idx = rng.choice(n, size=min(30, n), replace=False)
        p[peak_idx] = 10 ** (-rng.uniform(5, 15, size=len(peak_idx)))

# ゲノムワイド有意水準線
ax.axhline(-np.log10(5e-8), color='#E64B35', ls='--', lw=0.7,
           label='$p = 5 \\times 10^{-8}$')

ポイント: rasterized=True で散布図をラスタライズすると、PDF 出力時のファイルサイズを大幅に削減できます。

Fig05: Oncoprint-style Heatmap

image.png

20 遺伝子 x 50 サンプルの変異マトリクスを Oncoprint スタイルで描画します。

mut_types = {0: "None", 1: "Missense", 2: "Nonsense", 3: "Frameshift", 4: "Amplification"}

for i in range(len(genes)):
    for j in range(n_samples):
        val = matrix[i, j]
        if val > 0:
            # 背景グレー → 中央に変異色の小さい四角
            ax.add_patch(plt.Rectangle((j, i), 0.95, 0.95, color='#f0f0f0'))
            ax.add_patch(plt.Rectangle((j+0.1, i+0.15), 0.75, 0.65,
                                        color=colors_mut[val]))

ポイント: サンプルは変異バーデンでソートし、遺伝子は頻度順に並べるのが標準的です。

Step 3: クラスタリング & ネットワーク図表

Fig06: Heatmap with Hierarchical Clustering

image.png

seaborn の clustermap は行列ともにデンドログラムを自動生成してくれます。

g = sns.clustermap(
    df, cmap="RdBu_r", center=0,
    figsize=(SINGLE_COL * 1.2, SINGLE_COL * 1.4),
    method="ward", metric="euclidean",
    dendrogram_ratio=(0.12, 0.12),
    cbar_pos=(0.02, 0.82, 0.03, 0.12),
    linewidths=0.1, linecolor="white",
)

ポイント: center=0 で発現の上昇/低下を赤/青に対称配置。linewidths=0.1 でセル間に微細な白線を入れると見やすくなります。

Fig14: Sankey / Alluvial Diagram (患者フロー)

image.png

Matplotlib の Path オブジェクトでベジエ曲線を手動描画し、治療ステージ間の患者の流れを表現しています。

from matplotlib.path import Path

verts = [
    (x_from, y_from_start),
    ((x_from + x_to) / 2, y_from_start),
    ((x_from + x_to) / 2, y_to_start),
    (x_to, y_to_start),
    # ... (下辺のベジエ曲線)
]
codes = [Path.MOVETO] + [Path.CURVE4] * 3 + [Path.LINETO] + [Path.CURVE4] * 3 + [Path.CLOSEPOLY]
patch = mpatches.PathPatch(Path(verts, codes), facecolor=color, alpha=0.2)
ax.add_patch(patch)

Step 4: 臨床 & 生存解析

Fig07: Forest Plot (メタアナリシス)

image.png

10 件の研究のオッズ比と 95% 信頼区間を描画し、サマリーダイヤモンドで統合効果量を示します。

# サマリーダイヤモンド
diamond = plt.Polygon([
    (summary_ci_low, diamond_y),
    (summary_or, diamond_y + diamond_h),
    (summary_ci_high, diamond_y),
    (summary_or, diamond_y - diamond_h),
], closed=True, facecolor='#E64B35', edgecolor='black', linewidth=0.5)
ax.add_patch(diamond)

# 帰無仮説線 (OR=1)
ax.axvline(1.0, color="gray", linestyle="--", linewidth=0.5)
ax.set_xscale("log")  # 対数スケール

ポイント: Forest plot では必ず対数スケールを使い、OR=1 の帰無仮説線を破線で描画します。各研究のマーカーサイズは重み (分散の逆数) に比例させています。

Fig08: Kaplan-Meier with Risk Table

image.png

生存曲線の下にリスクテーブル (Number at Risk) を配置する、臨床論文で最も標準的な形式です。

fig = plt.figure(figsize=(SINGLE_COL * 1.2, SINGLE_COL * 1.0))
gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.08)
ax_km = fig.add_subplot(gs[0])    # 生存曲線
ax_risk = fig.add_subplot(gs[1])  # リスクテーブル

# ステップ関数で生存曲線を描画
ax_km.step(t_a, s_a, where="post", color=NATURE_PALETTE[0], linewidth=1.2)

# 打ち切りマーク
ax_km.scatter(censor_times, censor_surv, marker="|", s=15)

# Log-rank p 値をテキストボックスで表示
ax_km.text(0.5, 0.15, f"Log-rank $p$ = {p_val:.3f}",
           transform=ax_km.transAxes, fontsize=6.5, ha="center",
           bbox=dict(facecolor="white", edgecolor="gray", linewidth=0.4))

ポイント: gridspec.GridSpec で上下パネルの比率を [3, 1] に設定し、hspace=0.08 で密着させています。

Fig09: Waterfall Plot (RECIST 基準)

image.png

腫瘍縮小率を降順に並べた棒グラフで、RECIST 1.1 の基準線を追加します。

# RECIST 基準
ax.axhline(-30, color="black", linestyle="--", linewidth=0.5)  # PR 閾値
ax.axhline(20, color="black", linestyle="--", linewidth=0.5)   # PD 閾値

# 色分け: PR (緑), SD (青), PD (赤)
colors = []
for c in changes:
    if c <= -30:
        colors.append('#00A087')  # PR
    elif c >= 20:
        colors.append('#E64B35')  # PD
    else:
        colors.append('#3C5488')  # SD

Step 5: 複合パネル図 (Nature / Cell スタイル)

Fig10: 4-Panel Composite Figure

image.png

Nature / Cell に掲載される Figure 1 のような複合図を作成します。4 パネル構成で、パネルラベル (A, B, C, D) を太字で配置します。

fig = plt.figure(figsize=(DOUBLE_COL, DOUBLE_COL * 0.75))
gs = gridspec.GridSpec(2, 2, hspace=0.35, wspace=0.3)

# パネル A: ワークフロー (テキストボックス + 矢印)
ax_a = fig.add_subplot(gs[0, 0])
ax_a.axis("off")
box = FancyBboxPatch((x-0.9, y-0.6), 1.8, 1.2,
                     boxstyle="round,pad=0.1",
                     facecolor=NATURE_PALETTE[1],
                     edgecolor=NATURE_PALETTE[3])
ax_a.add_patch(box)

# パネル B: UMAP / t-SNE 散布図
# パネル C: 発現ヒートマップ
# パネル D: 棒グラフ + 統計

# パネルラベル
ax_a.text(-0.05, 1.05, "A", transform=ax_a.transAxes,
          fontsize=14, fontweight="bold", va="top")

ポイント: パネルラベルは transform=ax.transAxes を使い、各 Axes の左上に配置します。フォントサイズは 12-14pt で太字にするのが慣例です。

Step 6: 先端的可視化

Fig11: UpSet Plot (ライブラリ不要、フルスクラッチ実装)

image.png

UpSet plot はベン図の代替として、5 集合以上の交差を明確に示せます。本実装では外部ライブラリを使わず、gridspec で 4 分割したレイアウトをゼロから構築しました。

fig = plt.figure(figsize=(DOUBLE_COL, SINGLE_COL * 1.0))
gs = gridspec.GridSpec(2, 2,
    height_ratios=[3, 1.5], width_ratios=[1.2, 4],
    hspace=0.05, wspace=0.05)

ax_bar = fig.add_subplot(gs[0, 1])     # 右上: 交差サイズ棒グラフ
ax_dot = fig.add_subplot(gs[1, 1])     # 右下: ドットマトリクス
ax_setsize = fig.add_subplot(gs[1, 0]) # 左下: 集合サイズ水平棒グラフ

ポイント: ドットマトリクスでは、アクティブなドット同士を縦線で結び、非アクティブなドットはグレーで表示します。

Fig12: Radar / Spider Chart

image.png

薬剤の多次元特性を比較する際に有用です。

fig, ax = plt.subplots(figsize=(SINGLE_COL, SINGLE_COL),
                       subplot_kw=dict(polar=True))
angles = np.linspace(0, 2 * np.pi, n_cats, endpoint=False).tolist()
angles += angles[:1]  # 閉じるために最初の角度を追加

ax.plot(angles, values, linewidth=1.2, label=name, color=color)
ax.fill(angles, values, alpha=0.1, color=color)

Fig13: Bubble Enrichment Plot

image.png

パスウェイエンリッチメント解析の結果を、バブルの大きさ (遺伝子数)、x 軸 (fold enrichment)、色 (-log10 p) で三次元的に表現します。

Fig15: Gallery Overview

image.png

全 14 枚の図を plt.imread でサムネイルとして 4x4 グリッドに一覧表示します。

知見: ジャーナル投稿のための実践的 Tips

1. カラーに関する注意

ジャーナル カラー料金 推奨
Nature 無料 (オンライン) オンラインはカラー、印刷版はグレースケールでも読めるように
Science 無料 (オンライン) 同上
PNAS $1,350/ページ (カラー) できるだけ少ないカラーページに
Lancet / NEJM 無料 ジャーナル指定のテンプレート遵守

2. 解像度要件

  • ラインアート (グラフ、図表): 最低 300 DPI、推奨 600 DPI
  • 写真・画像: 最低 300 DPI
  • 混合 (写真+テキスト): 最低 500 DPI
  • ファイル形式: TIFF (無圧縮) or EPS > PDF > PNG (JPEG は不可の場合あり)

3. フォントの落とし穴

# NG: Helvetica は macOS 以外で利用不可の場合あり
plt.rcParams['font.family'] = 'Helvetica'

# OK: DejaVu Sans はどの OS でも利用可能
plt.rcParams['font.family'] = 'DejaVu Sans'

# 必須: PDF のフォント埋め込み設定
plt.rcParams['pdf.fonttype'] = 42  # TrueType
plt.rcParams['ps.fonttype'] = 42

4. 色覚多様性 (CVD) への対応

本スクリプトで使用した NATURE_PALETTE は、赤 (#E64B35) と青 (#4DBBD5) を基調としており、最も一般的な赤緑色覚異常 (deuteranopia) でも区別可能です。さらに安全を期す場合は:

  • Wong パレット: ['#000000','#E69F00','#56B4E9','#009E73','#F0E442','#0072B2','#D55E00','#CC79A7']
  • viridis 系 colormap: matplotlib 標準で CVD 対応

5. random_state=42 の意義

再現性の確保はオープンサイエンスの基本です。全てのランダム処理に random_state=42 を使用することで、同じスクリプトを実行すれば常に同一の図が生成されます。

生成結果サマリー

# ファイル名 サイズ 内容
01 Fig01_violin_stripplot.png 129 KB 細胞タイプ別遺伝子発現
02 Fig02_grouped_barplot.png 41 KB 薬剤 IC50 比較
03 Fig03_box_significance.png 83 KB 有意差ブラケット付き箱ひげ図
04 Fig04_manhattan_plot.png 150 KB GWAS Manhattan plot (22 染色体)
05 Fig05_oncoprint.png 77 KB 変異マトリクス (20 遺伝子 x 50 サンプル)
06 Fig06_clustered_heatmap.png 109 KB 階層的クラスタリング付きヒートマップ
07 Fig07_forest_plot.png 119 KB メタアナリシス Forest plot (10 研究)
08 Fig08_kaplan_meier_risktable.png 71 KB Kaplan-Meier + リスクテーブル
09 Fig09_waterfall_plot.png 54 KB RECIST Waterfall plot
10 Fig10_composite_figure.png 178 KB 4 パネル複合図 (Nature スタイル)
11 Fig11_upset_plot.png 56 KB UpSet plot (遺伝子セット交差)
12 Fig12_radar_chart.png 201 KB レーダーチャート (薬剤特性比較)
13 Fig13_bubble_enrichment.png 136 KB バブルプロット (エンリッチメント解析)
14 Fig14_sankey_diagram.png 219 KB Sankey ダイアグラム (患者フロー)
15 Fig15_figure_gallery.png 740 KB 全図サムネイル一覧

まとめ

  1. 1 スクリプトで 15 種類の論文品質図表を生成 -- plt.rcParams によるグローバルスタイルで一貫性を担保
  2. Nature / Science 基準を遵守 -- 89mm / 183mm カラム幅、300 DPI、6pt 以上フォント、TrueType 埋め込み
  3. 色覚多様性に配慮 -- NPG カラーパレットは deuteranopia でも区別可能
  4. 再現性を確保 -- random_state=42 を全ランダム処理に適用
  5. 外部ライブラリ最小限 -- UpSet plot, Sankey diagram はフルスクラッチで実装
  6. Tufte のデータインク比原則 -- 上辺・右辺の spine を削除、グリッドを控えめに

Claude Code を活用することで、各図のレイアウト・スタイル調整に費やす時間を大幅に短縮でき、研究そのものに集中できます。

参考資料

ソースコード

publication_figures.py (クリックで展開)
#!/usr/bin/env python3
"""
Experiment 10: Publication-Ready Scientific Figure Compendium
=============================================================
Generates 15+ publication-quality figures commonly used in scientific papers.
Demonstrates matplotlib, seaborn, and advanced customization techniques.

Author: Generated with Claude Code (claude-opus-4-6)
Date: 2026-02-11
"""

import os
import warnings
import numpy as np
import pandas as pd
import matplotlib

matplotlib.use("Agg")

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.gridspec as gridspec
import matplotlib.colors as mcolors
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Wedge
from matplotlib.collections import PatchCollection, LineCollection
from matplotlib.lines import Line2D
import seaborn as sns
from scipy import stats
from scipy.cluster.hierarchy import linkage, dendrogram, leaves_list
from scipy.spatial.distance import pdist
from sklearn.datasets import make_blobs
from sklearn.manifold import TSNE

warnings.filterwarnings("ignore")

# ---------------------------------------------------------------------------
# Global settings
# ---------------------------------------------------------------------------
RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)

FIGURE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "figures")
RESULTS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "results")
os.makedirs(FIGURE_DIR, exist_ok=True)
os.makedirs(RESULTS_DIR, exist_ok=True)

DPI = 300

# Nature-inspired color palette
NATURE_PALETTE = [
    "#E64B35", "#4DBBD5", "#00A087", "#3C5488", "#F39B7F",
    "#8491B4", "#91D1C2", "#DC0000", "#7E6148", "#B09C85",
]

# Column widths (Nature standard)
SINGLE_COL_MM = 89
DOUBLE_COL_MM = 183
MM_TO_INCH = 1 / 25.4
SINGLE_COL = SINGLE_COL_MM * MM_TO_INCH
DOUBLE_COL = DOUBLE_COL_MM * MM_TO_INCH

# Font settings
FONT_FAMILY = "DejaVu Sans"

plt.rcParams.update({
    "font.family": FONT_FAMILY,
    "font.size": 7,
    "axes.labelsize": 8,
    "axes.titlesize": 9,
    "xtick.labelsize": 6.5,
    "ytick.labelsize": 6.5,
    "legend.fontsize": 6.5,
    "figure.dpi": DPI,
    "savefig.dpi": DPI,
    "savefig.bbox": "tight",
    "savefig.pad_inches": 0.05,
    "axes.linewidth": 0.6,
    "xtick.major.width": 0.5,
    "ytick.major.width": 0.5,
    "xtick.major.size": 3,
    "ytick.major.size": 3,
    "axes.spines.top": False,
    "axes.spines.right": False,
    "pdf.fonttype": 42,
    "ps.fonttype": 42,
})


def save_fig(fig, name, **kwargs):
    path = os.path.join(FIGURE_DIR, name)
    fig.savefig(path, dpi=DPI, facecolor="white", **kwargs)
    plt.close(fig)
    print(f"  Saved: {name}")
    return path


# =========================================================================
# Figure 01: Violin + Strip Plot
# =========================================================================
def fig01_violin_stripplot():
    rng = np.random.default_rng(RANDOM_STATE)
    cell_types = ["T-cell", "B-cell", "Macrophage", "NK-cell", "Dendritic"]
    n_per_group = 60
    data = []
    for i, ct in enumerate(cell_types):
        vals = rng.normal(loc=2 + i * 0.8, scale=0.6 + i * 0.1, size=n_per_group)
        for v in vals:
            data.append({"Cell Type": ct, "Expression (log2 TPM)": v})
    df = pd.DataFrame(data)
    fig, ax = plt.subplots(figsize=(SINGLE_COL, SINGLE_COL * 0.75))
    parts = ax.violinplot(
        [df[df["Cell Type"] == ct]["Expression (log2 TPM)"].values for ct in cell_types],
        positions=range(len(cell_types)),
        showmeans=False, showmedians=False, showextrema=False,
    )
    for i, pc in enumerate(parts["bodies"]):
        pc.set_facecolor(NATURE_PALETTE[i])
        pc.set_alpha(0.3)
        pc.set_edgecolor(NATURE_PALETTE[i])
        pc.set_linewidth(0.8)
    for i, ct in enumerate(cell_types):
        vals = df[df["Cell Type"] == ct]["Expression (log2 TPM)"].values
        jitter = rng.uniform(-0.15, 0.15, size=len(vals))
        ax.scatter(i + jitter, vals, s=6, color=NATURE_PALETTE[i], alpha=0.6, edgecolors="none", zorder=3)
        med = np.median(vals)
        ax.hlines(med, i - 0.2, i + 0.2, color="black", linewidth=1.2, zorder=4)
    ax.set_xticks(range(len(cell_types)))
    ax.set_xticklabels(cell_types, rotation=30, ha="right")
    ax.set_ylabel("Expression (log$_2$ TPM)")
    ax.set_title("Gene Expression by Cell Type", fontweight="bold", pad=8)
    save_fig(fig, "Fig01_violin_stripplot.png")


# =========================================================================
# Figure 02: Grouped Bar Chart
# =========================================================================
def fig02_grouped_barplot():
    rng = np.random.default_rng(RANDOM_STATE)
    drugs = ["Cisplatin", "Doxorubicin", "Paclitaxel", "5-FU"]
    cell_lines = ["MCF-7", "HeLa", "A549"]
    x = np.arange(len(drugs))
    width = 0.22
    fig, ax = plt.subplots(figsize=(SINGLE_COL, SINGLE_COL * 0.7))
    for j, cl in enumerate(cell_lines):
        means = rng.uniform(0.5, 15.0, size=len(drugs))
        sems = means * rng.uniform(0.08, 0.2, size=len(drugs))
        ax.bar(x + j * width - width, means, width, yerr=sems, label=cl,
               color=NATURE_PALETTE[j], edgecolor="white", linewidth=0.4,
               capsize=2, error_kw={"linewidth": 0.6})
    ax.set_xticks(x)
    ax.set_xticklabels(drugs)
    ax.set_ylabel("IC$_{50}$ ($\\mu$M)")
    ax.set_title("Drug Sensitivity Across Cell Lines", fontweight="bold", pad=8)
    ax.legend(frameon=False, loc="upper right")
    ax.set_ylim(bottom=0)
    save_fig(fig, "Fig02_grouped_barplot.png")


# =========================================================================
# Figure 03: Box-and-Whisker with Significance Brackets
# =========================================================================
def fig03_box_significance():
    rng = np.random.default_rng(RANDOM_STATE)
    groups = ["Control", "Low Dose", "Mid Dose", "High Dose"]
    data_arrays = [
        rng.normal(5.0, 1.2, 40), rng.normal(5.8, 1.0, 40),
        rng.normal(7.2, 1.1, 40), rng.normal(9.0, 1.3, 40),
    ]
    fig, ax = plt.subplots(figsize=(SINGLE_COL, SINGLE_COL * 0.75))
    bp = ax.boxplot(data_arrays, patch_artist=True, widths=0.5,
                    medianprops=dict(color="black", linewidth=1),
                    whiskerprops=dict(linewidth=0.6),
                    capprops=dict(linewidth=0.6),
                    flierprops=dict(marker="o", markersize=2, alpha=0.5))
    for i, patch in enumerate(bp["boxes"]):
        patch.set_facecolor(NATURE_PALETTE[i])
        patch.set_alpha(0.7)
        patch.set_edgecolor("black")
        patch.set_linewidth(0.6)
    for i, arr in enumerate(data_arrays):
        jitter = rng.uniform(-0.1, 0.1, size=len(arr))
        ax.scatter(i + 1 + jitter, arr, s=4, color=NATURE_PALETTE[i], alpha=0.4, zorder=3)
    ax.set_xticklabels(groups, rotation=20, ha="right")
    ax.set_ylabel("Tumor Volume (cm$^3$)")
    ax.set_title("Treatment Response", fontweight="bold", pad=14)

    def add_bracket(ax, x1, x2, y, p_text):
        ax.plot([x1, x1, x2, x2], [y, y + 0.2, y + 0.2, y], lw=0.7, color="black")
        ax.text((x1 + x2) / 2, y + 0.25, p_text, ha="center", va="bottom", fontsize=6)

    ymax = max(np.max(d) for d in data_arrays) + 0.5
    add_bracket(ax, 1, 2, ymax, "ns")
    add_bracket(ax, 1, 3, ymax + 1.0, "*")
    add_bracket(ax, 1, 4, ymax + 2.0, "***")
    ax.set_ylim(top=ymax + 3.5)
    save_fig(fig, "Fig03_box_significance.png")


# =========================================================================
# Figure 04: Manhattan Plot
# =========================================================================
def fig04_manhattan_plot():
    rng = np.random.default_rng(RANDOM_STATE)
    chr_sizes = rng.integers(1000, 3000, size=22)
    chrom, pos, pvals = [], [], []
    cumul = 0
    chr_centers = []
    for c in range(22):
        n = chr_sizes[c]
        positions = np.sort(rng.integers(0, chr_sizes[c] * 1000, size=n))
        p = rng.uniform(0, 1, size=n)
        if c + 1 in [2, 8, 17]:
            peak_idx = rng.choice(n, size=min(30, n), replace=False)
            p[peak_idx] = 10 ** (-rng.uniform(5, 15, size=len(peak_idx)))
        chrom.extend([c + 1] * n)
        pos.extend(positions + cumul)
        pvals.extend(p)
        chr_centers.append(cumul + chr_sizes[c] * 500)
        cumul += chr_sizes[c] * 1000 + 5000
    neg_log_p = -np.log10(np.array(pvals))
    pos_arr = np.array(pos)
    chrom_arr = np.array(chrom)
    fig, ax = plt.subplots(figsize=(DOUBLE_COL, SINGLE_COL * 0.55))
    colors_cycle = [NATURE_PALETTE[3], NATURE_PALETTE[5]]
    for c in range(1, 23):
        mask = chrom_arr == c
        ax.scatter(pos_arr[mask], neg_log_p[mask], s=1.5, color=colors_cycle[c % 2],
                   alpha=0.6, rasterized=True)
    ax.axhline(-np.log10(5e-8), color=NATURE_PALETTE[0], ls="--", lw=0.7,
               label="$p = 5 \\times 10^{-8}$")
    ax.axhline(-np.log10(1e-5), color=NATURE_PALETTE[4], ls=":", lw=0.6,
               label="$p = 10^{-5}$")
    ax.set_xticks(chr_centers)
    ax.set_xticklabels([str(i) for i in range(1, 23)], fontsize=5.5)
    ax.set_xlabel("Chromosome")
    ax.set_ylabel("$-\\log_{10}(p)$")
    ax.set_title("Genome-Wide Association Study", fontweight="bold", pad=8)
    ax.legend(frameon=False, fontsize=5.5, loc="upper right")
    ax.set_ylim(bottom=0)
    save_fig(fig, "Fig04_manhattan_plot.png")


# =========================================================================
# Figure 05: Oncoprint Heatmap
# =========================================================================
def fig05_oncoprint():
    rng = np.random.default_rng(RANDOM_STATE)
    genes = ["TP53","KRAS","PIK3CA","PTEN","APC","BRAF","EGFR","CDKN2A",
             "RB1","ARID1A","ATM","BRCA1","BRCA2","IDH1","NOTCH1",
             "MYC","ERBB2","FGFR3","NFE2L2","STK11"]
    n_samples = 50
    colors_mut = {0:"#f0f0f0", 1:NATURE_PALETTE[0], 2:NATURE_PALETTE[3],
                  3:NATURE_PALETTE[2], 4:NATURE_PALETTE[1]}
    matrix = np.zeros((len(genes), n_samples), dtype=int)
    for i in range(len(genes)):
        freq = rng.uniform(0.05, 0.5)
        mutated = rng.choice(n_samples, size=int(freq * n_samples), replace=False)
        matrix[i, mutated] = rng.choice([1,2,3,4], size=len(mutated), p=[0.5,0.2,0.15,0.15])
    burden = (matrix > 0).sum(axis=0)
    matrix = matrix[:, np.argsort(-burden)]
    fig, ax = plt.subplots(figsize=(DOUBLE_COL, SINGLE_COL * 0.9))
    for i in range(len(genes)):
        for j in range(n_samples):
            val = matrix[i, j]
            ax.add_patch(plt.Rectangle((j, i), 0.95, 0.95, color=colors_mut[0], linewidth=0))
            if val > 0:
                ax.add_patch(plt.Rectangle((j+0.1, i+0.15), 0.75, 0.65,
                                            color=colors_mut[val], linewidth=0))
    ax.set_xlim(0, n_samples)
    ax.set_ylim(0, len(genes))
    ax.set_yticks(np.arange(len(genes)) + 0.5)
    ax.set_yticklabels(genes, fontsize=5.5)
    ax.set_xlabel("Samples (sorted by mutation burden)")
    ax.set_title("Mutation Landscape (Oncoprint)", fontweight="bold", pad=8)
    ax.invert_yaxis()
    ax.set_xticks([])
    for spine in ax.spines.values():
        spine.set_visible(False)
    legend_elements = [
        mpatches.Patch(facecolor=colors_mut[1], label="Missense"),
        mpatches.Patch(facecolor=colors_mut[2], label="Nonsense"),
        mpatches.Patch(facecolor=colors_mut[3], label="Frameshift"),
        mpatches.Patch(facecolor=colors_mut[4], label="Amplification"),
    ]
    ax.legend(handles=legend_elements, loc="upper right", frameon=False,
              fontsize=5.5, ncol=2, bbox_to_anchor=(1.0, -0.02))
    save_fig(fig, "Fig05_oncoprint.png")


# =========================================================================
# Figure 06: Clustered Heatmap
# =========================================================================
def fig06_clustered_heatmap():
    rng = np.random.default_rng(RANDOM_STATE)
    n_genes, n_samples = 40, 20
    gene_names = [f"Gene_{i+1:02d}" for i in range(n_genes)]
    sample_names = [f"S{i+1:02d}" for i in range(n_samples)]
    data = np.zeros((n_genes, n_samples))
    for ci in range(3):
        gs = slice(ci * 13, min((ci + 1) * 13, n_genes))
        ss = slice(ci * 7, min((ci + 1) * 7, n_samples))
        data[gs, ss] = rng.normal(2.0, 0.5, size=data[gs, ss].shape)
    data += rng.normal(0, 0.3, size=data.shape)
    df = pd.DataFrame(data, index=gene_names, columns=sample_names)
    g = sns.clustermap(df, cmap="RdBu_r", center=0,
                       figsize=(SINGLE_COL * 1.2, SINGLE_COL * 1.4),
                       row_cluster=True, col_cluster=True,
                       linewidths=0.1, linecolor="white",
                       dendrogram_ratio=(0.12, 0.12),
                       cbar_pos=(0.02, 0.82, 0.03, 0.12),
                       method="ward", metric="euclidean")
    g.ax_heatmap.set_xticklabels(g.ax_heatmap.get_xticklabels(), fontsize=4.5, rotation=90)
    g.ax_heatmap.set_yticklabels(g.ax_heatmap.get_yticklabels(), fontsize=4)
    g.fig.suptitle("Clustered Gene Expression", fontweight="bold", fontsize=9, y=1.01)
    save_fig(g.fig, "Fig06_clustered_heatmap.png")


# =========================================================================
# Figure 07: Forest Plot
# =========================================================================
def fig07_forest_plot():
    rng = np.random.default_rng(RANDOM_STATE)
    studies = ["Smith et al., 2018","Johnson et al., 2019","Wang et al., 2019",
               "Garcia et al., 2020","Kim et al., 2020","Patel et al., 2021",
               "Brown et al., 2021","Chen et al., 2022","Muller et al., 2023",
               "Davis et al., 2024"]
    n_studies = len(studies)
    log_ors = rng.normal(0.3, 0.4, size=n_studies)
    ses = rng.uniform(0.1, 0.5, size=n_studies)
    weights = 1.0 / ses**2
    weights_norm = weights / weights.sum() * 100
    ors = np.exp(log_ors)
    ci_low = np.exp(log_ors - 1.96 * ses)
    ci_high = np.exp(log_ors + 1.96 * ses)
    summary_log_or = np.average(log_ors, weights=weights)
    summary_se = np.sqrt(1.0 / weights.sum())
    summary_or = np.exp(summary_log_or)
    summary_ci_low = np.exp(summary_log_or - 1.96 * summary_se)
    summary_ci_high = np.exp(summary_log_or + 1.96 * summary_se)
    fig, ax = plt.subplots(figsize=(SINGLE_COL * 1.3, SINGLE_COL * 1.1))
    y_positions = np.arange(n_studies, 0, -1)
    for i in range(n_studies):
        ax.plot([ci_low[i], ci_high[i]], [y_positions[i]]*2,
                color=NATURE_PALETTE[3], linewidth=0.8)
        ax.scatter(ors[i], y_positions[i], s=weights_norm[i]*3,
                   color=NATURE_PALETTE[3], edgecolors="black", linewidths=0.3, zorder=5)
    diamond_y = -0.5
    diamond_h = 0.35
    diamond = plt.Polygon([(summary_ci_low, diamond_y),(summary_or, diamond_y+diamond_h),
                            (summary_ci_high, diamond_y),(summary_or, diamond_y-diamond_h)],
                           closed=True, facecolor=NATURE_PALETTE[0], edgecolor="black", linewidth=0.5)
    ax.add_patch(diamond)
    ax.axvline(1.0, color="gray", linestyle="--", linewidth=0.5, zorder=0)
    ax.set_yticks(list(y_positions) + [diamond_y])
    ax.set_yticklabels(studies + ["Summary (FE)"], fontsize=5.5)
    ax.set_xlabel("Odds Ratio (95% CI)")
    ax.set_title("Meta-Analysis: Treatment Efficacy", fontweight="bold", pad=8)
    ax.set_xscale("log")
    ax.set_xlim(0.2, 8)
    for i in range(n_studies):
        ax.text(8.5, y_positions[i], f"{ors[i]:.2f} [{ci_low[i]:.2f}, {ci_high[i]:.2f}]",
                va="center", fontsize=4.5, family="monospace")
    ax.text(8.5, diamond_y, f"{summary_or:.2f} [{summary_ci_low:.2f}, {summary_ci_high:.2f}]",
            va="center", fontsize=4.5, fontweight="bold", family="monospace")
    save_fig(fig, "Fig07_forest_plot.png")


# =========================================================================
# Figure 08: Kaplan-Meier with Risk Table
# =========================================================================
def fig08_kaplan_meier():
    rng = np.random.default_rng(RANDOM_STATE)

    def simulate_km(n, hazard, max_time=60):
        times = rng.exponential(1.0 / hazard, size=n)
        censor_times = rng.uniform(0, max_time, size=n)
        observed_times = np.minimum(times, censor_times)
        events = (times <= censor_times).astype(int)
        return observed_times, events

    def km_curve(times, events):
        order = np.argsort(times)
        times, events = times[order], events[order]
        unique_times = np.unique(times[events == 1])
        surv = 1.0
        t_plot, s_plot = [0], [1.0]
        for t in unique_times:
            n_risk = np.sum(times >= t)
            n_event = np.sum((times == t) & (events == 1))
            surv *= 1 - n_event / n_risk
            t_plot.extend([t, t])
            s_plot.extend([s_plot[-1], surv])
        t_plot.append(max(times))
        s_plot.append(s_plot[-1])
        return np.array(t_plot), np.array(s_plot), times, events

    n = 80
    time_a, event_a = simulate_km(n, 0.02)
    time_b, event_b = simulate_km(n, 0.05)
    t_a, s_a, raw_t_a, raw_e_a = km_curve(time_a, event_a)
    t_b, s_b, raw_t_b, raw_e_b = km_curve(time_b, event_b)

    fig = plt.figure(figsize=(SINGLE_COL * 1.2, SINGLE_COL * 1.0))
    gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1], hspace=0.08)
    ax_km = fig.add_subplot(gs[0])
    ax_risk = fig.add_subplot(gs[1])
    ax_km.step(t_a, s_a, where="post", color=NATURE_PALETTE[0], linewidth=1.2, label="Treatment A")
    ax_km.step(t_b, s_b, where="post", color=NATURE_PALETTE[3], linewidth=1.2, label="Treatment B")
    censor_mask_a = raw_e_a == 0
    ax_km.scatter(raw_t_a[censor_mask_a], np.interp(raw_t_a[censor_mask_a], t_a, s_a),
                  marker="|", s=15, color=NATURE_PALETTE[0], linewidths=0.5)
    censor_mask_b = raw_e_b == 0
    ax_km.scatter(raw_t_b[censor_mask_b], np.interp(raw_t_b[censor_mask_b], t_b, s_b),
                  marker="|", s=15, color=NATURE_PALETTE[3], linewidths=0.5)
    ax_km.set_ylabel("Survival Probability")
    ax_km.set_ylim(-0.05, 1.05)
    ax_km.set_xlim(-1, 65)
    ax_km.legend(frameon=False, loc="upper right")
    ax_km.set_title("Kaplan-Meier Survival Analysis", fontweight="bold", pad=8)
    ax_km.set_xticklabels([])

    from scipy.stats import chi2
    all_event_times = np.sort(np.unique(np.concatenate([raw_t_a[raw_e_a==1], raw_t_b[raw_e_b==1]])))
    O_a = np.sum(raw_e_a)
    E_a = 0
    for t in all_event_times:
        n_a_t = np.sum(raw_t_a >= t)
        n_b_t = np.sum(raw_t_b >= t)
        n_total = n_a_t + n_b_t
        d_total = np.sum((np.concatenate([raw_t_a, raw_t_b]) == t) &
                         (np.concatenate([raw_e_a, raw_e_b]) == 1))
        if n_total > 0:
            E_a += d_total * n_a_t / n_total
    p_val = 1 - chi2.cdf((O_a - E_a)**2 / E_a, df=1) if E_a > 0 else 1.0
    ax_km.text(0.5, 0.15, f"Log-rank $p$ = {p_val:.3f}", transform=ax_km.transAxes,
               fontsize=6.5, ha="center",
               bbox=dict(facecolor="white", edgecolor="gray", linewidth=0.4, pad=2))

    risk_times = np.arange(0, 65, 12)
    for idx, (raw_t, label, color) in enumerate([
        (raw_t_a, "Treatment A", NATURE_PALETTE[0]),
        (raw_t_b, "Treatment B", NATURE_PALETTE[3])
    ]):
        at_risk = [np.sum(raw_t >= t) for t in risk_times]
        for j, (t, n) in enumerate(zip(risk_times, at_risk)):
            ax_risk.text(t, 1 - idx, str(n), ha="center", va="center", fontsize=5.5, color=color)
    ax_risk.set_xlim(-1, 65)
    ax_risk.set_ylim(-0.5, 2)
    ax_risk.set_yticks([0, 1])
    ax_risk.set_yticklabels(["Trt B", "Trt A"], fontsize=5.5)
    ax_risk.set_xlabel("Time (months)")
    ax_risk.set_xticks(risk_times)
    ax_risk.spines["left"].set_visible(False)
    ax_risk.tick_params(left=False)
    ax_risk.set_title("Number at Risk", fontsize=6.5, fontweight="bold", pad=2)
    save_fig(fig, "Fig08_kaplan_meier_risktable.png")


# =========================================================================
# Figure 09: Waterfall Plot
# =========================================================================
def fig09_waterfall():
    rng = np.random.default_rng(RANDOM_STATE)
    n_patients = 40
    changes = np.sort(np.clip(rng.normal(-15, 30, size=n_patients), -100, 100))[::-1]
    fig, ax = plt.subplots(figsize=(SINGLE_COL * 1.2, SINGLE_COL * 0.7))
    colors_wf = [NATURE_PALETTE[2] if c <= -30 else NATURE_PALETTE[0] if c >= 20
                 else NATURE_PALETTE[3] for c in changes]
    ax.bar(range(n_patients), changes, color=colors_wf, edgecolor="white", linewidth=0.3, width=0.85)
    ax.axhline(-30, color="black", linestyle="--", linewidth=0.5, alpha=0.7)
    ax.axhline(20, color="black", linestyle="--", linewidth=0.5, alpha=0.7)
    ax.axhline(0, color="black", linewidth=0.5)
    ax.set_xlabel("Patient")
    ax.set_ylabel("Best Change from Baseline (%)")
    ax.set_title("Waterfall Plot (RECIST 1.1)", fontweight="bold", pad=8)
    ax.set_xticks([])
    legend_elements = [
        mpatches.Patch(color=NATURE_PALETTE[2], label="PR"),
        mpatches.Patch(color=NATURE_PALETTE[3], label="SD"),
        mpatches.Patch(color=NATURE_PALETTE[0], label="PD"),
    ]
    ax.legend(handles=legend_elements, frameon=False, loc="upper right", fontsize=5.5)
    save_fig(fig, "Fig09_waterfall_plot.png")


# =========================================================================
# Figure 10: Composite 4-Panel
# =========================================================================
def fig10_composite():
    rng = np.random.default_rng(RANDOM_STATE)
    fig = plt.figure(figsize=(DOUBLE_COL, DOUBLE_COL * 0.75))
    gs = gridspec.GridSpec(2, 2, hspace=0.35, wspace=0.3)

    # Panel A: Workflow
    ax_a = fig.add_subplot(gs[0, 0])
    ax_a.set_xlim(0, 10); ax_a.set_ylim(0, 6); ax_a.axis("off")
    boxes = [(1,4,"Sample\\nCollection"),(4,4,"RNA-seq\\nLibrary"),(7,4,"Sequencing"),
             (2.5,1.5,"Quality\\nControl"),(5.5,1.5,"Differential\\nAnalysis"),
             (8.5,1.5,"Pathway\\nEnrichment")]
    for x, y, txt in boxes:
        box = FancyBboxPatch((x-0.9, y-0.6), 1.8, 1.2, boxstyle="round,pad=0.1",
                             facecolor=NATURE_PALETTE[1], edgecolor=NATURE_PALETTE[3], linewidth=0.7)
        ax_a.add_patch(box)
        ax_a.text(x, y, txt, ha="center", va="center", fontsize=5, fontweight="bold")
    ax_a.text(-0.05, 1.05, "A", transform=ax_a.transAxes, fontsize=14, fontweight="bold", va="top")

    # Panel B: t-SNE
    ax_b = fig.add_subplot(gs[0, 1])
    X, y_labels = make_blobs(n_samples=300, centers=5, n_features=10, random_state=RANDOM_STATE)
    X_2d = TSNE(n_components=2, random_state=RANDOM_STATE, perplexity=30).fit_transform(X)
    for i in range(5):
        mask = y_labels == i
        ax_b.scatter(X_2d[mask, 0], X_2d[mask, 1], s=8, c=NATURE_PALETTE[i],
                     label=f"Cluster {i+1}", alpha=0.7)
    ax_b.set_xlabel("UMAP 1"); ax_b.set_ylabel("UMAP 2")
    ax_b.legend(frameon=False, fontsize=4.5)
    ax_b.text(-0.05, 1.05, "B", transform=ax_b.transAxes, fontsize=14, fontweight="bold", va="top")

    # Panel C: Heatmap
    ax_c = fig.add_subplot(gs[1, 0])
    hm_data = rng.normal(0, 1, size=(15, 10))
    hm_data[:5, :4] += 2; hm_data[10:, 6:] += 2.5
    im = ax_c.imshow(hm_data, cmap="RdBu_r", aspect="auto", vmin=-3, vmax=3)
    ax_c.set_xlabel("Samples"); ax_c.set_ylabel("Genes")
    plt.colorbar(im, ax=ax_c, fraction=0.046, pad=0.04)
    ax_c.text(-0.05, 1.05, "C", transform=ax_c.transAxes, fontsize=14, fontweight="bold", va="top")

    # Panel D: Bar + stats
    ax_d = fig.add_subplot(gs[1, 1])
    cats = ["Control", "Treatment A", "Treatment B"]
    means = [5.2, 8.1, 10.5]; sems = [0.5, 0.6, 0.7]
    ax_d.bar(cats, means, yerr=sems, capsize=3,
             color=[NATURE_PALETTE[5], NATURE_PALETTE[0], NATURE_PALETTE[2]],
             edgecolor="black", linewidth=0.4)
    ax_d.plot([0,0,2,2], [12,12.5,12.5,12], lw=0.7, color="black")
    ax_d.text(1, 12.6, "***", ha="center", fontsize=7)
    ax_d.set_ylabel("Fold Change"); ax_d.set_ylim(0, 14)
    ax_d.text(-0.05, 1.05, "D", transform=ax_d.transAxes, fontsize=14, fontweight="bold", va="top")
    save_fig(fig, "Fig10_composite_figure.png")


# =========================================================================
# Figure 11-15: (省略 -- 記事本文参照)
# =========================================================================

def main():
    fig01_violin_stripplot()
    fig02_grouped_barplot()
    fig03_box_significance()
    fig04_manhattan_plot()
    fig05_oncoprint()
    fig06_clustered_heatmap()
    fig07_forest_plot()
    fig08_kaplan_meier()
    fig09_waterfall()
    fig10_composite()
    # fig11_upset(), fig12_radar(), fig13_bubble_enrichment(),
    # fig14_sankey(), fig15_gallery() も同様に実行
    print("All figures generated successfully!")

if __name__ == "__main__":
    main()
5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?