mcpField
P
plotly-interactive-plots

plotly-interactive-plots

github.com/jaechang-hitsデータ・データベース38471 回インストール6 日前に更新
リポジトリ

plotly-interactive-plots について

Plotly Interactive Plots は、Plotly Express と graph_objects でインタラクティブなウェブ対応グラフを作成する Python 向けスキルです。データ分析者や研究者が、データ探索、独自レイアウト、HTML および静的画像の出力に利用できます。

主な機能

  1. 01px による DataFrame 向けグラフ作成
  2. 02go によるトレースとレイアウトの詳細制御
  3. 03ホバー、ズーム、パン、クリック操作
  4. 043D、アニメーション、特殊チャート
  5. 05サブプロットと HTML・画像出力

活用シーン

  1. 01ホバー情報を使った科学データの探索
  2. 02複数パネルの用量反応ダッシュボード作成
  3. 03インタラクティブな図の HTML 共有
  4. 04論文掲載用グラフの書き出し

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jaechang-hits/sciagent-skills --skill plotly-interactive-plots

インストール後、Claude Code で/plotly-interactive-plotsを入力して読み込まれたか確認します。

詳細

Plotly インタラクティブプロット

概要

Plotly は、HTML と JavaScript によって支えられた、インタラクティブで Web 対応の図を作成するための Python ライブラリです。相補的な 2 つの API を提供します。plotly.express(px)は、一般的な種類のグラフを 1 行で生成できる、高水準の DataFrame 指向インターフェースです。一方、plotly.graph_objects(go)は、すべてのトレース、軸、レイアウトのプロパティを細かく制御できます。図はデフォルトで完全にインタラクティブで、ホバーツールチップ、ズーム、パン、クリックイベントに対応しています。Web ページや Jupyter ノートブックに埋め込んだり、Dash フレームワークを使って Web アプリケーションに組み込んだりできます。

使用する場面

  • 静的な図を煩雑にせずに、遺伝子名、p 値、サンプルのメタデータを表示するホバーツールチップが必要な場合。
  • 用量反応曲線、患者コホート、複数条件の比較用に、複数パネルのインタラクティブなダッシュボードを構築する場合。
  • プログラミングの知識がない人もブラウザーで操作できる、自己完結型の HTML ファイルとして図を共有したい場合。
  • 構造生物学、コンフォメーションランドスケープ、高次元データの PCA 用に、3D 散布図またはサーフェスプロットが必要な場合。
  • ユーザーが特定の遺伝子クラスターにズームできる、遺伝子発現または相関行列のヒートマップを作成する場合。
  • 時系列または処置反応の推移を示すアニメーションフレームが必要な場合。
  • 最小限のコードで統計的な集約(信頼区間、回帰フィット)を自動で行いたい場合は、代わりに seaborn を使用してください。
  • ジャーナルの正確な仕様に合わせた出版用の図を作成するため、すべての軸要素を細かく制御したい場合は、matplotlib を使用してください。

前提条件

  • Python パッケージ:plotly、kaleido(静的画像のエクスポート)、pandas、numpy
  • データ要件:pandas DataFrame または NumPy 配列。px ではロング形式(tidy)のデータが最適です。
  • 環境:Jupyter Lab/Notebook(インライン表示)、またはブラウザー表示用に HTML として保存
pip install plotly kaleido pandas numpy

Jupyter Lab でインライン表示する場合(自動で表示されない場合):

pip install "jupyterlab>=3" ipywidgets

クイックスタート

import plotly.express as px
import pandas as pd

# Gene expression scatter with hover info
df = pd.DataFrame({
    "log2FC": [-3.1, 0.2, 1.8, 2.5, -0.5, 4.1],
    "neg_log10_padj": [8.2, 0.4, 2.1, 6.8, 0.1, 9.3],
    "gene": ["BRCA1", "MYC", "TP53", "EGFR", "CDKN1A", "KRAS"],
    "significance": ["sig", "ns", "ns", "sig", "ns", "sig"],
})

fig = px.scatter(
    df, x="log2FC", y="neg_log10_padj",
    color="significance", hover_name="gene",
    title="Volcano Plot — Treatment vs Control",
)
fig.show()

コア API

モジュール 1:px 散布図と折れ線グラフ — 関係性プロット

px.scatter() と px.line() は、DataFrame の列を視覚的なエンコーディング(色、記号、サイズ)に対応付け、hover_data からホバーツールチップを自動的に設定します。

import plotly.express as px
import pandas as pd
import numpy as np

# Dose-response scatter: color by drug, symbol by cell line
np.random.seed(42)
df = pd.DataFrame({
    "dose_uM": np.tile([0.01, 0.1, 1, 10, 100], 4),
    "viability": np.clip(np.random.normal(
        [100, 90, 70, 40, 10] * 4, 5), 0, 110),
    "drug": ["DrugA"] * 5 + ["DrugA"] * 5 + ["DrugB"] * 5 + ["DrugB"] * 5,
    "cell_line": ["HCT116"] * 10 + ["MCF7"] * 10,
    "replicate": np.tile([1, 2, 3, 4, 5], 4),
})

fig = px.scatter(
    df, x="dose_uM", y="viability",
    color="drug", symbol="cell_line",
    log_x=True,
    hover_data={"replicate": True, "dose_uM": ":.2f"},
    labels={"viability": "Cell Viability (%)", "dose_uM": "Dose (µM)"},
    title="Dose-Response by Drug and Cell Line",
)
fig.show()
print(f"Figure has {len(fig.data)} traces")
# Time-course gene expression line plot
time_df = pd.DataFrame({
    "hour": list(range(0, 25, 4)) * 3,
    "expression": [1.0, 1.8, 3.2, 4.5, 3.8, 2.1, 1.2,
                   1.0, 2.5, 5.1, 6.8, 5.5, 3.2, 1.8,
                   1.0, 1.1, 1.0, 1.2, 1.1, 1.0, 0.9],
    "gene": ["MYC"] * 7 + ["EGFR"] * 7 + ["GAPDH"] * 7,
})

fig = px.line(
    time_df, x="hour", y="expression",
    color="gene", markers=True,
    labels={"expression": "Relative Expression (log2)", "hour": "Time (h)"},
    title="Time-Course Gene Expression",
)
fig.update_traces(line=dict(width=2.5), marker=dict(size=8))
fig.show()
モジュール 2:px 統計プロット — 分布とカテゴリ

px.box()、px.violin()、px.histogram()、px.strip() は、グループ化機能を備えた、出版に適した分布の要約を生成します。

import plotly.express as px
import pandas as pd
import numpy as np

# Violin + strip overlay: expression by cell type
np.random.seed(7)
n = 60
cell_data = pd.DataFrame({
    "expression": np.concatenate([
        np.random.normal(4.2, 0.8, n),
        np.random.normal(6.5, 1.2, n),
        np.random.normal(2.8, 0.6, n),
    ]),
    "cell_type": ["T cell"] * n + ["B cell"] * n + ["NK cell"] * n,
    "patient_id": np.tile([f"P{i:02d}" for i in range(1, 11)], 18),
})

fig = px.violin(
    cell_data, x="cell_type", y="expression",
    color="cell_type", box=True, points="all",
    hover_data=["patient_id"],
    labels={"expression": "CD3E Expression (log2 CPM)"},
    title="CD3E Expression Across Cell Types",
)
fig.update_traces(jitter=0.3, pointpos=-1.5)
fig.show()
print(f"Cells per type: {cell_data.groupby('cell_type').size().to_dict()}")
# Histogram with rug: distribution of fold changes
fc_df = pd.DataFrame({
    "log2FC": np.concatenate([
        np.random.normal(0.1, 0.8, 500),   # not DE genes
        np.random.normal(2.5, 0.4, 50),    # upregulated
        np.random.normal(-2.3, 0.4, 40),   # downregulated
    ]),
    "category": ["background"] * 500 + ["up"] * 50 + ["down"] * 40,
})

fig = px.histogram(
    fc_df, x="log2FC", color="category",
    nbins=60, barmode="overlay", opacity=0.7,
    marginal="rug",
    labels={"log2FC": "log2 Fold Change", "count": "Gene Count"},
    title="Distribution of Fold Changes (DESeq2 Results)",
    color_discrete_map={"background": "gray", "up": "crimson", "down": "steelblue"},
)
fig.show()
モジュール 3:px ヒートマップと行列 — 遺伝子発現と相関

px.imshow() は、2D 配列または DataFrame を色で値を表した行列として描画します。発現ヒートマップや相関行列に最適です。

import plotly.express as px
import pandas as pd
import numpy as np

# Gene expression heatmap (genes × samples)
np.random.seed(12)
genes = [f"Gene_{g}" for g in ["BRCA1", "TP53", "EGFR", "MYC", "KRAS",
                                 "CDKN1A", "RB1", "PTEN", "VHL", "APC"]]
samples = [f"S{i:02d}" for i in range(1, 9)]

expr_matrix = pd.DataFrame(
    np.random.normal(0, 1.5, (10, 8)) +
    np.array([2, -1, 3, -2, 1, -3, 0, 2, -1, 3]).reshape(-1, 1),
    index=genes, columns=samples,
)

fig = px.imshow(
    expr_matrix,
    color_continuous_scale="RdBu_r",
    color_continuous_midpoint=0,
    aspect="auto",
    labels={"color": "log2 Expression (z-score)"},
    title="Gene Expression Heatmap",
)
fig.update_xaxes(side="top")
fig.update_layout(width=600, height=500)
fig.show()
print(f"Heatmap shape: {expr_matrix.shape} (genes × samples)")
# Correlation matrix heatmap
from itertools import combinations

markers = ["IL6", "TNF", "CXCL10", "IFNg", "IL10", "IL1B", "CCL2", "IL17A"]
np.random.seed(3)
raw = np.random.multivariate_normal(
    mean=np.zeros(8),
    cov=np.eye(8) * 0.3 + 0.7,
    size=80,
)
corr_df = pd.DataFrame(raw, columns=markers).corr()

fig = px.imshow(
    corr_df,
    color_continuous_scale="RdBu_r",
    color_continuous_midpoint=0,
    zmin=-1, zmax=1,
    text_auto=".2f",
    title="Cytokine Correlation Matrix (n=80 patients)",
)
fig.update_traces(textfont_size=10)
fig.show()
モジュール 4:go グラフオブジェクト — トレースの完全な制御

plotly.graph_objects は、マーカー記号、誤差範囲、塗りつぶし領域、複数トレースのレイアウトなど、すべてのトレースプロパティを細かく制御できます。px では必要な柔軟性が得られない場合に不可欠です。

import plotly.graph_objects as go
import numpy as np

# Volcano plot built from scratch with go.Scatter
np.random.seed(99)
n_genes = 5000
log2fc = np.random.normal(0, 1.2, n_genes)
pval = np.random.uniform(0, 1, n_genes) ** 2  # skew toward low p-values
neg_log10_p = -np.log10(pval + 1e-300)
gene_names = [f"Gene_{i:04d}" for i in range(n_genes)]

# Classify genes
sig_mask = (np.abs(log2fc) > 1.5) & (neg_log10_p > 3)
up_mask = sig_mask & (log2fc > 0)
down_mask = sig_mask & (log2fc < 0)
ns_mask = ~sig_mask

fig = go.Figure()

# Non-significant background
fig.add_trace(go.Scatter(
    x=log2fc[ns_mask], y=neg_log10_p[ns_mask],
    mode="markers",
    name="Not significant",
    marker=dict(color="lightgray", size=4, opacity=0.5),
    text=[gene_names[i] for i in np.where(ns_mask)[0]],
    hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))

# Upregulated
fig.add_trace(go.Scatter(
    x=log2fc[up_mask], y=neg_log10_p[up_mask],
    mode="markers",
    name=f"Up ({up_mask.sum()} genes)",
    marker=dict(color="crimson", size=7, opacity=0.8),
    text=[gene_names[i] for i in np.where(up_mask)[0]],
    hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))

# Downregulated
fig.add_trace(go.Scatter(
    x=log2fc[down_mask], y=neg_log10_p[down_mask],
    mode="markers",
    name=f"Down ({down_mask.sum()} genes)",
    marker=dict(color="steelblue", size=7, opacity=0.8),
    text=[gene_names[i] for i in np.where(down_mask)[0]],
    hovertemplate="<b>%{text}</b><br>log2FC: %{x:.2f}<br>-log10(p): %{y:.2f}<extra></extra>",
))

# Threshold lines
fig.add_hline(y=3, line_dash="dash", line_color="black", line_width=1)
fig.add_vline(x=1.5, line_dash="dash", line_color="black", line_width=1)
fig.add_vline(x=-1.5, line_dash="dash", line_color="black", line_width=1)

fig.update_layout(
    title="Volcano Plot (Treatment vs Control, n=5000 genes)",
    xaxis_title="log2 Fold Change",
    yaxis_title="-log10(adjusted p-value)",
    legend=dict(x=0.01, y=0.99),
    width=750, height=550,
)
fig.show()
print(f"Up: {up_mask.sum()}, Down: {down_mask.sum()}, NS: {ns_mask.sum()}")
# Bar chart with error bars: mean ± SEM per treatment group
groups = ["Vehicle", "DrugA 1µM", "DrugA 10µM", "DrugB 1µM", "DrugB 10µM"]
means = [100.0, 82.3, 54.7, 91.2, 68.5]
sems = [3.2, 4.1, 3.8, 3.5, 4.7]

fig = go.Figure(go.Bar(
    x=groups, y=means,
    error_y=dict(type="data", array=sems, visible=True),
    marker_color=["gray", "lightsalmon", "crimson", "lightblue", "steelblue"],
    hovertemplate="%{x}<br>Mean: %{y:.1f}%<br>SEM: ±%{error_y.array:.1f}%<extra></extra>",
))

fig.update_layout(
    title="Cell Viability by Treatment (Mean ± SEM, n=6)",
    yaxis_title="Viability (%)", yaxis_range=[0, 120],
    xaxis_title="Treatment Group",
    showlegend=False,
)
fig.show()
モジュール 5:3D グラフと特殊なグラフ

Plotly は、3D 散布図、サーフェスプロット、平行座標、ツリーマップに対応しています。これらのグラフは seaborn や標準の matplotlib では利用できません。

import plotly.express as px
import numpy as np
import pandas as pd

# 3D PCA scatter: cell clusters in embedding space
np.random.seed(42)
n_per_cluster = 80
cluster_centers = {"T cell": [3, 2, 1], "B cell": [-3, 1, 2], "Monocyte": [0, -3, -1]}

records = []
for ctype, center in cluster_centers.items():
    coords = np.random.normal(center, 0.8, (n_per_cluster, 3))
    for row in coords:
        records.append({
            "PC1": row[0], "PC2": row[1], "PC3": row[2],
            "cell_type": ctype,
            "score": np.random.uniform(0.5, 1.0),
        })

pca_df = pd.DataFrame(records)

fig = px.scatter_3d(
    pca_df, x="PC1", y="PC2", z="PC3",
    color="cell_type", size="score", opacity=0.7,
    hover_data={"score": ":.3f"},
    title="3D PCA — Single-Cell Transcriptomics",
)
fig.update_traces(marker=dict(sizeref=0.04))
fig.show()
print(f"Total cells: {len(pca_df)}, clusters: {pca_df['cell_type'].nunique()}")
import plotly.graph_objects as go
import numpy as np
import pandas as pd

# Parallel coordinates: multi-parameter drug screen
np.random.seed(5)
n_compounds = 200
drug_df = pd.DataFrame({
    "MW": np.random.normal(380, 60, n_compounds),
    "logP": np.random.uniform(-1, 6, n_compounds),
    "HBA": np.random.randint(2, 10, n_compounds),
    "HBD": np.random.randint(0, 6, n_compounds),
    "IC50_nM": np.random.lognormal(4, 1.5, n_compounds),
    "selectivity": np.random.uniform(1, 100, n_compounds),
})

fig = px.parallel_coordinates(
    drug_df,
    color="IC50_nM",
    color_continuous_scale="RdYlGn_r",
    dimensions=["MW", "logP", "HBA", "HBD", "IC50_nM", "selectivity"],
    labels={
        "MW": "MW (Da)", "logP": "logP",
        "HBA": "H-Bond Acceptors", "HBD": "H-Bond Donors",
        "IC50_nM": "IC50 (nM)", "selectivity": "Selectivity Index",
    },
    title="Drug Candidate Properties — Parallel Coordinates",
)
fig.show()
モジュール 6:サブプロットとエクスポート

make_subplots() は、軸を共有するレイアウト、異なる種類のグラフの組み合わせ、パネルごとに独立したトレースを備えた複数パネルのレイアウトを作成します。fig.write_html() はインタラクティブな図をエクスポートし、fig.write_image() は kaleido を使って静的ファイルをエクスポートします。

from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np

# Two-panel: raw data + summary statistics
np.random.seed(77)
doses = [0.01, 0.1, 1, 10, 100]
drugs = {"DrugA": {"EC50": 1.0, "hill": 1.5}, "DrugB": {"EC50": 8.0, "hill": 0.9}}

def hill_curve(dose, ec50, hill, top=100, bottom=0):
    return bottom + (top - bottom) / (1 + (ec50 / dose) ** hill)

fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=["Dose-Response Curves", "IC50 Comparison"],
    shared_yaxes=False,
)

colors = {"DrugA": "crimson", "DrugB": "steelblue"}
ic50_values = []

for drug, params in drugs.items():
    # Smooth fit curve
    x_fit = np.logspace(-2, 2, 200)
    y_fit = hill_curve(x_fit, params["EC50"], params["hill"])
    fig.add_trace(go.Scatter(
        x=x_fit, y=y_fit, mode="lines",
        name=f"{drug} fit", line=dict(color=colors[drug], width=2.5),
    ), row=1, col=1)

    # Noisy data points
    y_data = [hill_curve(d, params["EC50"], params["hill"]) +
              np.random.normal(0, 4) for d in doses]
    fig.add_trace(go.Scatter(
        x=doses, y=y_data, mode="markers",
        name=f"{drug} data", marker=dict(color=colors[drug], size=9),
        showlegend=False,
    ), row=1, col=1)
    ic50_values.append(params["EC50"])

# Bar chart of IC50 values
fig.add_trace(go.Bar(
    x=list(drugs.keys()), y=ic50_values,
    marker_color=list(colors.values()),
    showlegend=False,
    hovertemplate="%{x}<br>IC50: %{y:.2f} µM<extra></extra>",
), row=1, col=2)

fig.update_xaxes(type="log", title_text="Dose (µM)", row=1, col=1)
fig.update_yaxes(title_text="Viability (%)", row=1, col=1)
fig.update_xaxes(title_text="Drug", row=1, col=2)
fig.update_yaxes(title_text="IC50 (µM)", row=1, col=2)
fig.update_layout(title="Dose-Response Dashboard", height=450, width=850)
fig.show()
print(f"Subplots: {len(fig.data)} traces across 2 panels")
# Export to HTML (interactive) and PNG (static)
# Requires: pip install kaleido

fig.write_html("dose_response_dashboard.html")
print("Saved: dose_response_dashboard.html (interactive, shareable)")

fig.write_image("dose_response_dashboard.png", width=1200, height=600, scale=2)
print("Saved: dose_response_dashboard.png (300 DPI equivalent with scale=2)")

fig.write_image("dose_response_dashboard.svg")
print("Saved: dose_response_dashboard.svg (vector, editable in Inkscape/Illustrator)")

よくあるワークフロー

ワークフロー 1:遺伝子注釈付きのインタラクティブなボルケーノプロット

目的:DESeq2 の結果から、遺伝子名のホバーツールチップ、閾値線、強調表示されたヒットラベルを備え、HTML で共有できる、注釈付きの完全なボルケーノプロットを作成します。

import plotly.graph_objects as go
import pandas as pd
import numpy as np

# Simulate DESeq2 output (replace with pd.read_csv("deseq2_results.csv"))
np.random.seed(42)
n = 3000
df = pd.DataFrame({
    "gene": [f"GENE_{i:04d}" for i in range(n)],
    "log2FC": np.random.normal(0, 1.0, n),
    "padj": np.clip(np.random.exponential(0.1, n), 1e-20, 1.0),
    "baseMean": np.random.lognormal(5, 1.5, n),
})
# Inject some hits
df.loc[:20, "log2FC"] = np.random.uniform(2.5, 5, 21)
df.loc[:20, "padj"] = np.random.uniform(1e-15, 1e-5, 21)
df.loc[21:35, "log2FC"] = np.random.uniform(-4, -2, 15)
df.loc[21:35, "padj"] = np.random.uniform(1e-12, 1e-4, 15)

df["neg_log10_padj"] = -np.log10(df["padj"].clip(1e-300))

# Classify
FC_THRESH, P_THRESH = 1.5, 2.0   # |log2FC| > 1.5, -log10(padj) > 2
df["category"] = "NS"
df.loc[(df["log2FC"] > FC_THRESH) & (df["neg_log10_padj"] > P_THRESH), "category"] = "Up"
df.loc[(df["log2FC"] < -FC_THRESH) & (df["neg_log10_padj"] > P_THRESH), "category"] = "Down"

color_map = {"NS": "lightgray", "Up": "crimson", "Down": "steelblue"}
size_map = {"NS": 4, "Up": 7, "Down": 7}
opacity_map = {"NS": 0.4, "Up": 0.85, "Down": 0.85}

fig = go.Figure()

for cat in ["NS", "Up", "Down"]:
    sub = df[df["category"] == cat]
    fig.add_trace(go.Scatter(
        x=sub["log2FC"], y=sub["neg_log10_padj"],
        mode="markers",
        name=f"{cat} (n={len(sub)})",
        marker=dict(
            color=color_map[cat],
            size=size_map[cat],
            opacity=opacity_map[cat],
        ),
        customdata=sub[["gene", "padj", "baseMean"]].values,
        hovertemplate=(
            "<b>%{customdata[0]}</b><br>"
            "log2FC: %{x:.3f}<br>"
            "padj: %{customdata[1]:.2e}<br>"
            "baseMean: %{customdata[2]:.1f}<extra></extra>"
        ),
    ))

# Threshold lines
fig.add_hline(y=P_THRESH, line_dash="dot", line_color="black", line_width=1.2,
              annotation_text=f"padj=0.01", annotation_position="right")
fig.add_vline(x=FC_THRESH, line_dash="dot", line_color="black", line_width=1.2)
fig.add_vline(x=-FC_THRESH, line_dash="dot", line_color="black", line_width=1.2)

# Label top 5 upregulated hits by significance
top_up = df[df["category"] == "Up"].nlargest(5, "neg_log10_padj")
for _, row in top_up.iterrows():
    fig.add_annotation(
        x=row["log2FC"], y=row["neg_log10_padj"],
        text=row["gene"], showarrow=True,
        arrowhead=2, arrowsize=1, arrowcolor="crimson",
        font=dict(size=9, color="crimson"),
        xshift=8, yshift=5,
    )

fig.update_layout(
    title="Volcano Plot — Treatment vs Control (DESeq2)",
    xaxis_title="log2 Fold Change",
    yaxis_title="-log10(adjusted p-value)",
    legend=dict(x=0.01, y=0.99, bordercolor="lightgray", borderwidth=1),
    width=800, height=560,
    plot_bgcolor="white",
)
fig.update_xaxes(showgrid=True, gridcolor="lightgray", zeroline=True, zerolinecolor="darkgray")
fig.update_yaxes(showgrid=True, gridcolor="lightgray")

fig.write_html("volcano_interactive.html")
print(f"Up: {(df.category=='Up').sum()}, Down: {(df.category=='Down').sum()}")
print("Saved: volcano_interactive.html")
ワークフロー 2:make_subplots を使った複数パネルの用量反応ダッシュボード

目的:複数の薬剤について、複数の細胞株にわたる用量反応曲線を、共通のカラースケールと一貫した書式を備えたグリッドレイアウトで表示します。

from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
import pandas as pd

# Simulated IC50 data for 3 drugs × 3 cell lines
np.random.seed(10)
drugs = ["DrugA", "DrugB", "DrugC"]
cell_lines = ["HCT116", "MCF7", "A549"]
doses = np.logspace(-2, 2, 7)  # 0.01 to 100 µM

def hill(x, ec50, hill_n, top=100, bottom=0):
    return bottom + (top - bottom) / (1 + (ec50 / x) ** hill_n)

ec50_table = {
    ("DrugA", "HCT116"): 0.5, ("DrugA", "MCF7"): 2.0, ("DrugA", "A549"): 8.0,
    ("DrugB", "HCT116"): 5.0, ("DrugB", "MCF7"): 0.8, ("DrugB", "A549"): 15.0,
    ("DrugC", "HCT116"): 12.0, ("DrugC", "MCF7"): 6.0, ("DrugC", "A549"): 1.2,
}

palette = px_colors = ["#EF553B", "#636EFA", "#00CC96", "#AB63FA", "#FFA15A",
                        "#19D3F3", "#FF6692", "#B6E880", "#FF97FF"]

fig = make_subplots(
    rows=len(drugs), cols=len(cell_lines),
    subplot_titles=[f"{d} / {c}" for d in drugs for c in cell_lines],
    shared_xaxes=True, shared_yaxes=True,
    vertical_spacing=0.08, horizontal_spacing=0.04,
)

for r, drug in enumerate(drugs, start=1):
    for c, cell_line in enumerate(cell_lines, start=1):
        ec50 = ec50_table[(drug, cell_line)]
        x_fit = np.logspace(-2, 2, 200)
        y_fit = hill(x_fit, ec50, hill_n=1.5)

        # Noisy replicate data
        y_data = np.array([hill(d, ec50, 1.5) + np.random.normal(0, 5) for d in doses])

        color = palette[(r - 1) * len(cell_lines) + (c - 1)]
        show_legend = (c == 1 and r == 1)

        fig.add_trace(go.Scatter(
            x=x_fit, y=y_fit, mode="lines",
            line=dict(color=color, width=2),
            name=f"{drug}/{cell_line}",
            showlegend=False,
            hovertemplate=f"{drug} in {cell_line}<br>Dose: %{{x:.2f}} µM<br>Viability: %{{y:.1f}}%<extra></extra>",
        ), row=r, col=c)

        fig.add_trace(go.Scatter(
            x=doses, y=np.clip(y_data, 0, 110), mode="markers",
            marker=dict(color=color, size=7, opacity=0.8),
            showlegend=False,
            hovertemplate=f"Measured<br>Dose: %{{x:.2f}} µM<br>Viability: %{{y:.1f}}%<extra></extra>",
        ), row=r, col=c)

        # IC50 annotation
        fig.add_annotation(
            x=np.log10(ec50), y=50,
            text=f"IC50={ec50:.1f}µM",
            font=dict(size=8), showarrow=False,
            xref=f"x{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}",
            yref=f"y{(r-1)*len(cell_lines)+c if (r-1)*len(cell_lines)+c > 1 else ''}",
        )

# Apply log scale to all x-axes
for i in range(1, len(drugs) * len(cell_lines) + 1):
    axis_key = f"xaxis{i if i > 1 else ''}"
    fig.layout[axis_key].update(type="log", title_text="Dose (µM)" if i > 6 else "")

for i in range(1, len(drugs) * len(cell_lines) + 1):
    axis_key = f"yaxis{i if i > 1 else ''}"
    fig.layout[axis_key].update(range=[-5, 115],
                                 title_text="Viability (%)" if i in [1, 4, 7] else "")

fig.update_layout(
    title="Dose-Response Dashboard — 3 Drugs × 3 Cell Lines",
    height=700, width=900,
)
fig.write_html("dose_response_dashboard.html")
print("Saved: dose_response_dashboard.html")
print(f"Grid: {len(drugs)} drugs × {len(cell_lines)} cell lines = {len(drugs)*len(cell_lines)} panels")

主なパラメーター

パラメーターモジュール/関数デフォルト範囲/選択肢効果
colorpx.*None列名DataFrame の列をトレースの色に割り当てます。パレットは自動で設定されます
hover_datapx.*{}列名の辞書またはリストホバーツールチップに表示する追加の列
log_x / log_ypx.*FalseTrue、Falsex 軸または y 軸に log10 スケールを適用します
facet_col / facet_rowpx.*None列名カテゴリ変数に基づいてサブプロットのグリッドに分割します
color_continuous_scalepx.imshow、px.scatter"plasma""RdBu_r"、"Viridis"、"Hot" など連続値の色マッピングに使うカラーマップ
color_continuous_midpointpx.imshowNone任意の数値この値を発散カラーマップの中心に設定します(z スコアには 0 を使用します)
barmodepx.histogram、px.bar"relative""relative"、"overlay"、"group"複数の棒トレースの表示方法
opacitygo.Scatter、px.*1.00.0~1.0点/棒の透明度
size / sizerefgo.Scatter6 / 自動正の数値マーカーのサイズ。sizeref はトレース間でサイズを正規化します
line_dashfig.add_hline、go.Scatter"solid""solid"、"dash"、"dot"、"dashdot"参照線とトレースの線種
shared_xaxes / shared_yaxesmake_subplotsFalseTrue、False、"rows"、"cols"サブプロットのパネル間で軸を連動させます
scalefig.write_image11~4PNG 出力時の解像度倍率(約 150 DPI にするには 2 を使用します)

ベストプラクティス

  1. DataFrame のデータには px を優先し、複数トレースの合成には go を使用します。 プロットの 80% では px.scatter() とその関連関数を使用します。同じ図に異なる種類のトレース(例:散布図+塗りつぶし領域)が必要な場合や、トレースごとに細かく制御する必要がある場合は go に切り替えます。

    # Correct: px for simple grouped plots
    fig = px.box(df, x="treatment", y="expression", color="genotype")
    
    # Correct: go when px cannot express the structure
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=x, y=y_upper, fill="tonexty", ...))
    fig.add_trace(go.Scatter(x=x, y=y_lower, ...))
    
  2. 科学図では必ず hovertemplate を指定します。 デフォルトのツールチップには単位や遺伝子名がなく、生の座標が表示されます。customdata を使ったカスタムテンプレートを設定すると、生物学的な背景情報を十分に提示できます。

    fig.add_trace(go.Scatter(
        customdata=df[["gene", "padj"]].values,
        hovertemplate="<b>%{customdata[0]}</b><br>padj: %{customdata[1]:.2e}<extra></extra>",
    ))
    
  3. 共有には HTML、学術誌への投稿には PNG/SVG を出力します。 fig.write_html() は外部依存関係のない自己完結型ファイルを生成します。印刷に十分な解像度を得るには、write_image() で scale=2 以上を使用します。

  4. バッチスクリプトでは fig.show() を使用しないでください。 CI、HPC、cron ジョブなどの非対話型環境では、fig.show() によってブラウザーウィンドウが開いたり、失敗したりする場合があります。write_html() または write_image() のみを使用してください。

  5. z スコアのデータに発散パレットを使う場合は color_continuous_midpoint=0 を設定します。 設定しない場合、中心色はゼロではなくデータの中点がデフォルトになるため、対称的なフォールド変化や相関が誤って表現されます。

    fig = px.imshow(corr_matrix, color_continuous_scale="RdBu_r",
                    color_continuous_midpoint=0, zmin=-1, zmax=1)
    
  6. 論文用の図では plot_bgcolor="white" を設定します。 Plotly のデフォルトでは、背景に薄いグレーのグリッドが表示されます。白い背景に控えめなグリッド線を組み合わせると、ほとんどの科学的用途で見やすくなります。

    fig.update_layout(plot_bgcolor="white")
    fig.update_xaxes(showgrid=True, gridcolor="lightgray")
    fig.update_yaxes(showgrid=True, gridcolor="lightgray")
    

よく使うレシピ

レシピ:条件を切り替えるドロップダウンメニュー

使用する場面:1 つの図に複数の条件を重ね、ドロップダウンボタンで各トレースをすっきりと表示/非表示にします。

import plotly.graph_objects as go
import numpy as np

conditions = ["Untreated", "DrugA", "DrugB"]
colors = ["gray", "crimson", "steelblue"]
np.random.seed(1)
x = np.linspace(0, 24, 49)

fig = go.Figure()
for i, (cond, color) in enumerate(zip(conditions, colors)):
    y = np.sin(x / 4 + i * 0.5) * (1 - i * 0.2) + np.random.normal(0, 0.05, len(x))
    fig.add_trace(go.Scatter(
        x=x, y=y, mode="lines+markers",
        name=cond, line=dict(color=color, width=2),
        visible=(i == 0),  # only first trace visible initially
    ))

# One button per condition (shows only that trace)
buttons = []
for i, cond in enumerate(conditions):
    visibility = [j == i for j in range(len(conditions))]
    buttons.append(dict(label=cond, method="update",
                        args=[{"visible": visibility}, {"title": f"Gene Expression — {cond}"}]))

# "Show All" button
buttons.append(dict(label="Show All", method="update",
                    args=[{"visible": [True] * len(conditions)}, {"title": "Gene Expression — All Conditions"}]))

fig.update_layout(
    updatemenus=[dict(type="dropdown", x=0.01, y=1.15, showactive=True, buttons=buttons)],
    title="Gene Expression — Untreated",
    xaxis_title="Time (h)", yaxis_title="Relative Expression",
)
fig.show()
レシピ:矢印で特定のヒットを注釈する

使用する場面:注釈のない点を見づらくすることなく、外れ値、薬剤ヒット、または有意な遺伝子に図上で直接ラベルを付けます。

import plotly.graph_objects as go
import numpy as np
import pandas as pd

np.random.seed(33)
df = pd.DataFrame({
    "x": np.random.normal(0, 1.5, 300),
    "y": np.random.normal(0, 1.5, 300),
    "gene": [f"G{i:03d}" for i in range(300)],
})
# Inject top hits
hits = pd.DataFrame({
    "x": [3.2, -2.8, 2.5, -3.5],
    "y": [4.1, 3.8, -3.2, -2.9],
    "gene": ["BRCA1", "TP53", "EGFR", "KRAS"],
})

fig = go.Figure()
fig.add_trace(go.Scatter(
    x=df["x"], y=df["y"], mode="markers",
    marker=dict(color="lightgray", size=5, opacity=0.6),
    text=df["gene"],
    hovertemplate="<b>%{text}</b><br>x: %{x:.2f}, y: %{y:.2f}<extra></extra>",
    name="Background",
))
fig.add_trace(go.Scatter(
    x=hits["x"], y=hits["y"], mode="markers",
    marker=dict(color="crimson", size=10, symbol="diamond"),
    text=hits["gene"],
    hovertemplate="<b>%{text}</b> [HIT]<br>x: %{x:.2f}, y: %{y:.2f}<extra></extra>",
    name="Hits",
))

for _, row in hits.iterrows():
    fig.add_annotation(
        x=row["x"], y=row["y"],
        text=f"<b>{row['gene']}</b>",
        showarrow=True, arrowhead=2, arrowwidth=1.5,
        arrowcolor="crimson", font=dict(size=11, color="crimson"),
        ax=25, ay=-30,  # arrow offset in pixels
        bgcolor="rgba(255,255,255,0.7)", bordercolor="crimson", borderwidth=1,
    )

fig.update_layout(
    title="Hit Identification with Arrow Annotations",
    xaxis_title="Score A", yaxis_title="Score B",
    plot_bgcolor="white",
)
fig.show()

トラブルシューティング

問題原因解決策
ValueError: kaleido is required for static image exportkaleido がインストールされていないpip install kaleido を実行します。import kaleido で確認します
Jupyter Notebook で図が空白になるレンダラーが設定されていないimport plotly.io as pio; pio.renderers.default = "notebook" を実行するか、JupyterLab を ≥3 にアップグレードします
fig.show() で空白のブラウザータブが開く図にデータがないか、オフラインレンダラーに問題があるlen(fig.data) > 0 を確認し、pio.renderers.default = "browser" を使用します
ホバーツールチップに誤った値が表示されるhovertemplate 内の customdata インデックスが一致していないcustomdata の列順が、テンプレート内の %{customdata[N]} インデックスと一致することを確認します
トレース間で色が一貫して割り当てられないカテゴリー数が異なると px がパレットの順序を変更するcolor_discrete_map={"Cat1": "#color1", ...} を使用して、色を明示的に固定します
write_image でぼやけた PNG が生成されるデフォルトの scale=1 では印刷時の解像度が低すぎる150 DPI には fig.write_image("fig.png", scale=2)、300 DPI には scale=4 を使用します
shared_xaxes=True の後もサブプロットのすべての x 軸が対数スケールにならない共有軸で同期されるのは範囲のみで、型は同期されないfig.layout 内のすべての xaxis キーを反復処理し、type="log" を明示的に設定します
大規模なデータセットのブラウザーでの描画が遅い1 つの散布図トレースに個々の点が多すぎる背景のノイズ点をダウンサンプリングし、ラベル付きのヒットは別の小さなトレースにします
fig.update_layout がサブプロットの軸に適用されない複数パネルの図では、インデックス付きの軸(xaxis2、xaxis3)が使用されるfig.update_xaxes()(すべての軸に適用)を使用するか、fig.layout["xaxis2"] を明示的に指定します

関連スキル

  • seaborn-statistical-plots — 統計的集計(信頼区間、回帰)、少ないコードでの出版品質の静的な図、matplotlib レベルの出力が必要な場合に使用します
  • matplotlib-scientific-plotting — 図のすべての要素を完全に制御する場合、カスタムレイアウト、テキストの埋め込み表示、ジャーナルの仕様に沿った図の準備に使用します
  • pydeseq2-differential-expression — DESeq2 の結果から生成するボルケーノプロットは、上記のインタラクティブなボルケーノプロットワークフローの主な入力です
  • scanpy-scrna-seq — Scanpy の UMAP 埋め込みは、px.scatter_3d で 3D のインタラクティブな表示ができます

参考資料

コメント