跳转至

数据可视化进阶:Plotly + Streamlit 交互式可视化教程

一句话说明

Plotly 让你的图表能缩放、悬停、筛选,Streamlit 让你一行命令就能把分析脚本变成网页 App——两者结合是生信数据展示的最强组合。


为什么要学交互式可视化?

静态图 vs 交互式图

维度 静态图(matplotlib/seaborn) 交互式图(Plotly/Bokeh)
缩放 不行,导出多大就多大 鼠标滚轮随意缩放
悬停信息 看不到具体数值 鼠标悬停显示每个点的详细数据
筛选 重新写代码重新跑 点击图例即可隐藏/显示某类
分享 发 PNG/PDF 发一个 HTML 文件,对方浏览器打开就能交互
适合场景 论文投稿(期刊要求静态图) 汇报、探索数据、Web 展示

白话类比

  • matplotlib = 拍照片(拍完就定死了)
  • Plotly = 拍视频+加了触摸屏(能暂停、快进、放大看细节)
  • Streamlit = 给视频加了遥控器界面(别人不用装任何东西就能操作)

生信中数据展示的重要性

面试时老板问"你做了什么",如果你能甩出一个交互式的 PCA 图让他自己拖拽探索,比贴一张静态图的印象分高 10 倍。


环境安装

# 建议在 bioinfo 环境或新建环境
conda activate bioinfo

# 安装 Plotly(交互式图表库)
pip install plotly

# 安装 Streamlit(Web App 框架)
pip install streamlit

# 可选:Jupyter 中显示 Plotly
pip install nbformat

# 验证安装
python -c "import plotly; print(f'Plotly 版本: {plotly.__version__}')"
python -c "import streamlit; print(f'Streamlit 版本: {streamlit.__version__}')"

Plotly 教程

1. 基础图表

1.1 折线图(Line Chart)

import plotly.express as px  # px 是 Plotly Express,高级接口,一行出图
import pandas as pd  # 数据处理

# 模拟时间序列数据:某菌属在不同时间点的相对丰度
data = pd.DataFrame({
    "天数": [0, 7, 14, 21, 28, 35, 42],  # 采样时间点
    "Bacteroides": [0.25, 0.28, 0.31, 0.35, 0.33, 0.30, 0.29],  # 拟杆菌属丰度
    "Firmicutes": [0.40, 0.38, 0.35, 0.32, 0.34, 0.36, 0.38],  # 厚壁菌门丰度
})

# 宽表转长表(Plotly 更喜欢长表格式)
data_long = data.melt(
    id_vars="天数",  # 保持不变的列
    var_name="菌属",  # 原来的列名变成这一列的值
    value_name="相对丰度"  # 原来的值放到这一列
)

# 画折线图
fig = px.line(
    data_long,  # 数据源
    x="天数",  # X 轴
    y="相对丰度",  # Y 轴
    color="菌属",  # 按菌属分颜色
    markers=True,  # 显示数据点标记
    title="肠道菌群丰度随时间变化",  # 标题
    labels={"相对丰度": "Relative Abundance"}  # 自定义轴标签
)

# 更新布局:设置字体、背景等
fig.update_layout(
    font=dict(size=14),  # 全局字体大小
    template="plotly_white",  # 白色背景模板
    hovermode="x unified"  # 悬停时显示同一 X 值的所有数据
)

fig.show()  # 在浏览器/Jupyter 中显示

1.2 柱状图(Bar Chart)

import plotly.express as px  # 高级画图接口
import pandas as pd

# 模拟数据:不同分组的菌属丰度
data = pd.DataFrame({
    "菌属": ["Bacteroides", "Prevotella", "Ruminococcus", "Faecalibacterium", "Akkermansia"],
    "健康组": [0.25, 0.15, 0.12, 0.18, 0.08],  # 健康人的丰度
    "T2D组": [0.18, 0.08, 0.20, 0.10, 0.03],  # 2型糖尿病患者的丰度
})

# 宽转长
data_long = data.melt(id_vars="菌属", var_name="分组", value_name="相对丰度")

# 分组柱状图
fig = px.bar(
    data_long,
    x="菌属",  # X 轴:菌属名称
    y="相对丰度",  # Y 轴:丰度值
    color="分组",  # 按分组上色
    barmode="group",  # "group" 并排显示,"stack" 堆叠显示
    title="健康组 vs T2D组 菌属丰度对比",
    color_discrete_map={"健康组": "#2ecc71", "T2D组": "#e74c3c"}  # 自定义颜色
)

fig.update_layout(template="plotly_white")
fig.show()

1.3 散点图(Scatter Plot)

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

np.random.seed(42)  # 固定随机种子,保证可复现

# 模拟 50 个样本的 alpha 多样性数据
n = 50
data = pd.DataFrame({
    "Shannon指数": np.random.normal(3.5, 0.8, n),  # Shannon 多样性指数
    "BMI": np.random.normal(25, 5, n),  # 体质指数
    "分组": np.random.choice(["健康", "T2D"], n),  # 随机分组
    "样本ID": [f"S{i:03d}" for i in range(n)]  # 样本编号
})

# 散点图 + 趋势线
fig = px.scatter(
    data,
    x="BMI",  # X 轴
    y="Shannon指数",  # Y 轴
    color="分组",  # 按分组着色
    hover_name="样本ID",  # 悬停时显示的主标题
    trendline="ols",  # 添加 OLS 线性回归趋势线(需安装 statsmodels)
    title="BMI vs Shannon 多样性指数",
    opacity=0.7  # 透明度,防止点重叠看不清
)

fig.update_traces(marker=dict(size=10))  # 调大散点尺寸
fig.show()

1.4 热图(Heatmap)

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

# 模拟物种-样本丰度矩阵(生信最常见的数据格式之一)
species = ["Bacteroides", "Prevotella", "Roseburia", "Akkermansia", "Blautia",
           "Faecalibacterium", "Lactobacillus", "Bifidobacterium"]
samples = [f"Sample_{i}" for i in range(1, 11)]  # 10 个样本

np.random.seed(42)
# 生成 0-1 之间的随机丰度值
abundance_matrix = np.random.rand(len(species), len(samples))

# 转成 DataFrame
df = pd.DataFrame(abundance_matrix, index=species, columns=samples)

# 画热图
fig = px.imshow(
    df,  # 直接传 DataFrame
    labels=dict(x="样本", y="物种", color="相对丰度"),  # 轴标签
    title="物种丰度热图",
    color_continuous_scale="RdYlBu_r",  # 配色方案(红黄蓝反转)
    aspect="auto"  # 自动调整宽高比
)

fig.update_layout(
    width=800,  # 图宽
    height=500  # 图高
)
fig.show()

1.5 箱线图(Box Plot)

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

np.random.seed(42)

# 模拟三组样本的 Shannon 多样性
data = pd.DataFrame({
    "Shannon指数": np.concatenate([
        np.random.normal(4.0, 0.5, 30),  # 健康组:均值高
        np.random.normal(3.0, 0.7, 30),  # T2D组:均值低
        np.random.normal(3.5, 0.6, 30),  # 前驱糖尿病组:居中
    ]),
    "分组": ["健康"] * 30 + ["T2D"] * 30 + ["前驱糖尿病"] * 30
})

# 箱线图 + 显示所有数据点
fig = px.box(
    data,
    x="分组",  # 分组变量
    y="Shannon指数",  # 数值变量
    color="分组",  # 按组上色
    points="all",  # 显示所有数据点("outliers" 只显示离群点)
    title="各组 Alpha 多样性(Shannon 指数)对比",
    notched=True  # 显示缺口(缺口不重叠 = 中位数有显著差异)
)

fig.update_layout(template="plotly_white", showlegend=False)
fig.show()

1.6 小提琴图(Violin Plot)

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

np.random.seed(42)

# 同样的数据,换小提琴图展示分布形态
data = pd.DataFrame({
    "Firmicutes/Bacteroidetes比值": np.concatenate([
        np.random.lognormal(0.5, 0.3, 40),  # 健康组
        np.random.lognormal(1.0, 0.5, 40),  # T2D组
    ]),
    "分组": ["健康"] * 40 + ["T2D"] * 40
})

# 小提琴图:比箱线图多了分布密度信息
fig = px.violin(
    data,
    x="分组",
    y="Firmicutes/Bacteroidetes比值",
    color="分组",
    box=True,  # 小提琴内部加箱线图
    points="all",  # 同时显示散点
    title="F/B 比值分布(小提琴图)"
)

fig.update_layout(template="plotly_white")
fig.show()

2. 生信应用场景

2.1 PCA 3D 散点图

import plotly.express as px
import numpy as np
import pandas as pd
from sklearn.decomposition import PCA  # 主成分分析

np.random.seed(42)

# 模拟 60 个样本 × 20 个物种的丰度矩阵
n_samples = 60
n_features = 20
X = np.random.rand(n_samples, n_features)  # 原始丰度数据

# 人为让前 30 个样本(健康)和后 30 个(T2D)有差异
X[:30, :5] += 1.5  # 健康组前 5 个物种丰度偏高
X[30:, 5:10] += 1.5  # T2D 组另外 5 个物种丰度偏高

# 做 PCA 降维到 3 维
pca = PCA(n_components=3)  # 保留 3 个主成分
components = pca.fit_transform(X)  # 拟合并转换

# 解释方差比例(每个 PC 能解释多少信息)
var_explained = pca.explained_variance_ratio_

# 整理成 DataFrame
df_pca = pd.DataFrame({
    "PC1": components[:, 0],
    "PC2": components[:, 1],
    "PC3": components[:, 2],
    "分组": ["健康"] * 30 + ["T2D"] * 30,
    "样本ID": [f"S{i:03d}" for i in range(n_samples)]
})

# 3D 散点图
fig = px.scatter_3d(
    df_pca,
    x="PC1", y="PC2", z="PC3",  # 三个轴分别对应三个主成分
    color="分组",  # 按组着色
    hover_name="样本ID",  # 悬停显示样本 ID
    title=f"PCA 3D 散点图(PC1:{var_explained[0]:.1%} PC2:{var_explained[1]:.1%} PC3:{var_explained[2]:.1%})",
    opacity=0.8,
    symbol="分组"  # 不同组用不同形状
)

# 调整 3D 场景
fig.update_layout(
    scene=dict(
        xaxis_title=f"PC1 ({var_explained[0]:.1%})",  # 轴标签带解释方差
        yaxis_title=f"PC2 ({var_explained[1]:.1%})",
        zaxis_title=f"PC3 ({var_explained[2]:.1%})",
    ),
    width=800, height=600
)

fig.show()  # 可以鼠标拖拽旋转 3D 图!

2.2 物种丰度堆叠图

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

np.random.seed(42)

# 模拟 10 个样本的 Top 8 物种丰度(堆叠图常用于展示群落组成)
samples = [f"S{i:02d}" for i in range(1, 11)]
species = ["Bacteroides", "Prevotella", "Faecalibacterium",
           "Roseburia", "Akkermansia", "Blautia", "Ruminococcus", "Others"]

# 生成随机丰度并归一化到 100%
raw = np.random.rand(10, 8)
normalized = raw / raw.sum(axis=1, keepdims=True)  # 每个样本归一化到和为 1

# 构建长格式 DataFrame
rows = []
for i, sample in enumerate(samples):
    for j, sp in enumerate(species):
        rows.append({"样本": sample, "物种": sp, "相对丰度": normalized[i, j]})

df = pd.DataFrame(rows)

# 堆叠柱状图
fig = px.bar(
    df,
    x="样本",  # X 轴为样本
    y="相对丰度",  # Y 轴为丰度
    color="物种",  # 每种颜色代表一个物种
    title="物种相对丰度堆叠图(Top 8)",
    color_discrete_sequence=px.colors.qualitative.Set3  # 使用 Set3 配色板
)

fig.update_layout(
    barmode="stack",  # 堆叠模式
    yaxis_title="Relative Abundance",
    template="plotly_white",
    legend_title="物种"
)

fig.show()

2.3 火山图交互版(Volcano Plot)

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

np.random.seed(42)

# 模拟差异分析结果:1000 个基因/物种
n_genes = 1000
data = pd.DataFrame({
    "gene": [f"Gene_{i}" for i in range(n_genes)],  # 基因名
    "log2FC": np.random.normal(0, 1.5, n_genes),  # log2 Fold Change
    "pvalue": np.random.uniform(0, 1, n_genes),  # 原始 p 值
})

# 计算 -log10(p-value)
data["-log10(p)"] = -np.log10(data["pvalue"])

# 标记显著性:|log2FC| > 1 且 p < 0.05
data["显著性"] = "不显著"  # 默认不显著
data.loc[(data["log2FC"] > 1) & (data["pvalue"] < 0.05), "显著性"] = "上调"
data.loc[(data["log2FC"] < -1) & (data["pvalue"] < 0.05), "显著性"] = "下调"

# 火山图
fig = px.scatter(
    data,
    x="log2FC",  # X 轴:倍数变化
    y="-log10(p)",  # Y 轴:显著性
    color="显著性",  # 按显著性着色
    hover_name="gene",  # 悬停显示基因名(这就是交互的核心价值!)
    title="差异表达火山图(悬停查看基因名)",
    color_discrete_map={
        "上调": "#e74c3c",  # 红色
        "下调": "#3498db",  # 蓝色
        "不显著": "#95a5a6"  # 灰色
    },
    opacity=0.6
)

# 添加阈值线
fig.add_hline(y=-np.log10(0.05), line_dash="dash", line_color="gray",
              annotation_text="p=0.05")  # 水平线:p 值阈值
fig.add_vline(x=1, line_dash="dash", line_color="gray")  # 垂直线:FC 阈值
fig.add_vline(x=-1, line_dash="dash", line_color="gray")

fig.update_layout(template="plotly_white", width=800, height=600)
fig.show()

2.4 系统发育树可视化(简化版环形图)

import plotly.graph_objects as go  # go 是 Plotly 底层接口,更灵活
import numpy as np

# 系统发育树本质是层级结构,这里用 Sunburst(旭日图)展示分类层级
# 实际项目中可用 ETE3 + Plotly 结合

# 模拟分类学层级数据
labels = [
    "Bacteria",  # 根
    "Firmicutes", "Bacteroidetes", "Proteobacteria",  # 门
    "Clostridia", "Bacilli",  # Firmicutes 下的纲
    "Bacteroidia",  # Bacteroidetes 下的纲
    "Gammaproteobacteria",  # Proteobacteria 下的纲
    "Lachnospiraceae", "Ruminococcaceae",  # Clostridia 下的科
    "Lactobacillaceae",  # Bacilli 下的科
    "Bacteroidaceae", "Prevotellaceae",  # Bacteroidia 下的科
]

parents = [
    "",  # Bacteria 没有父节点
    "Bacteria", "Bacteria", "Bacteria",  # 门的父节点是 Bacteria
    "Firmicutes", "Firmicutes",  # 纲的父节点是门
    "Bacteroidetes",
    "Proteobacteria",
    "Clostridia", "Clostridia",  # 科的父节点是纲
    "Bacilli",
    "Bacteroidia", "Bacteroidia",
]

values = [100, 45, 35, 20, 25, 20, 35, 20, 15, 10, 20, 20, 15]  # 丰度值

# 旭日图(Sunburst)展示层级分类
fig = go.Figure(go.Sunburst(
    labels=labels,  # 每个节点的标签
    parents=parents,  # 每个节点的父节点
    values=values,  # 每个节点的值(决定扇区大小)
    branchvalues="total",  # 父节点值 = 子节点值之和
    hovertemplate="<b>%{label}</b><br>丰度: %{value}<extra></extra>",  # 悬停模板
    maxdepth=3  # 最多显示 3 层
))

fig.update_layout(
    title="肠道菌群分类层级(旭日图)",
    width=700, height=700
)

fig.show()  # 点击某个扇区可以"钻入"查看下级

3. 子图布局(Subplots)

from plotly.subplots import make_subplots  # 子图工具
import plotly.graph_objects as go
import numpy as np
import pandas as pd

np.random.seed(42)

# 创建 2×2 子图布局
fig = make_subplots(
    rows=2, cols=2,  # 2 行 2 列
    subplot_titles=("Shannon 指数", "物种数", "Chao1 指数", "Simpson 指数"),  # 每个子图标题
    vertical_spacing=0.12,  # 垂直间距
    horizontal_spacing=0.1  # 水平间距
)

# 生成模拟数据
groups = ["健康", "T2D"]
colors = ["#2ecc71", "#e74c3c"]

for i, (group, color) in enumerate(zip(groups, colors)):
    shannon = np.random.normal(4 - i * 0.8, 0.5, 25)  # Shannon
    richness = np.random.normal(200 - i * 40, 30, 25)  # 物种数
    chao1 = np.random.normal(250 - i * 50, 40, 25)  # Chao1
    simpson = np.random.normal(0.9 - i * 0.1, 0.05, 25)  # Simpson

    # 左上:Shannon
    fig.add_trace(go.Box(y=shannon, name=group, marker_color=color,
                         showlegend=(True if i == 0 else True)),
                  row=1, col=1)
    # 右上:物种数
    fig.add_trace(go.Box(y=richness, name=group, marker_color=color,
                         showlegend=False),
                  row=1, col=2)
    # 左下:Chao1
    fig.add_trace(go.Box(y=chao1, name=group, marker_color=color,
                         showlegend=False),
                  row=2, col=1)
    # 右下:Simpson
    fig.add_trace(go.Box(y=simpson, name=group, marker_color=color,
                         showlegend=False),
                  row=2, col=2)

fig.update_layout(
    title_text="Alpha 多样性指标全景对比",  # 总标题
    height=700, width=900,
    template="plotly_white"
)

fig.show()

4. 保存为 HTML

import plotly.express as px
import pandas as pd

# 任意一个 fig 对象都可以保存
fig = px.scatter(x=[1, 2, 3], y=[4, 5, 6], title="示例")

# 保存为独立 HTML 文件(包含所有 JS,无需联网即可打开)
fig.write_html(
    "my_interactive_plot.html",  # 输出文件名
    include_plotlyjs=True,  # 把 Plotly.js 嵌入文件中(文件约 3MB)
    full_html=True  # 生成完整 HTML(含 <html><head> 等标签)
)

# 轻量版:引用 CDN(文件小但需要联网)
fig.write_html("my_plot_cdn.html", include_plotlyjs="cdn")

# 保存为静态图片(需要安装 kaleido)
# pip install kaleido
fig.write_image("my_plot.png", width=800, height=600, scale=2)  # scale=2 为高清
fig.write_image("my_plot.pdf")  # PDF 格式,投稿用
fig.write_image("my_plot.svg")  # SVG 矢量图

print("保存完成!HTML 文件双击即可在浏览器打开")

Streamlit 教程

什么是 Streamlit?

白话:Streamlit = 用纯 Python 写网页 App 的神器。你不需要学 HTML/CSS/JavaScript,写完 Python 脚本直接 streamlit run app.py,浏览器自动弹出一个可交互的网页应用。

适合场景: - 给导师/面试官展示分析结果 - 团队内部共享数据分析工具 - 快速搭建原型验证想法

安装与启动

pip install streamlit

# 创建一个 app.py 后运行
streamlit run app.py

# 浏览器自动打开 http://localhost:8501

基础组件速览

# app_basic.py - Streamlit 基础组件演示
import streamlit as st  # st 是 Streamlit 的标准缩写
import pandas as pd
import numpy as np

# ========== 文本组件 ==========
st.title("我的生信分析平台")  # 大标题(H1)
st.header("数据概览")  # 二级标题(H2)
st.subheader("样本信息")  # 三级标题(H3)
st.write("这是一段普通文本,支持 **Markdown** 格式")  # 万能输出函数
st.markdown("- 支持列表\n- 支持公式 $E=mc^2$")  # Markdown 渲染

# ========== 输入组件 ==========
name = st.text_input("输入样本名称", value="Sample_001")  # 文本输入框
threshold = st.slider("设置丰度阈值", 0.0, 1.0, 0.05, 0.01)  # 滑块
group = st.selectbox("选择分组", ["健康", "T2D", "前驱糖尿病"])  # 下拉选择
show_all = st.checkbox("显示所有数据点")  # 复选框

# ========== 数据展示 ==========
df = pd.DataFrame(np.random.rand(10, 3), columns=["Bacteroides", "Prevotella", "Roseburia"])
st.dataframe(df)  # 交互式表格(可排序、搜索)
st.table(df.head())  # 静态表格

# ========== 指标卡片 ==========
col1, col2, col3 = st.columns(3)  # 三列布局
col1.metric("样本数", "120", "+5")  # 指标:标题、值、变化量
col2.metric("物种数", "856", "-12")
col3.metric("Shannon 均值", "3.45", "+0.2")

# ========== 侧边栏 ==========
with st.sidebar:  # 侧边栏(放参数调节)
    st.header("参数设置")
    n_samples = st.number_input("样本数量", 10, 1000, 100)
    method = st.radio("分析方法", ["PCA", "t-SNE", "UMAP"])

搭建生信数据分析 Web App(完整示例)

# bioinfo_app.py - 生信数据分析 Web App
# 运行方式: streamlit run bioinfo_app.py

import streamlit as st  # Web App 框架
import pandas as pd  # 数据处理
import numpy as np  # 数值计算
import plotly.express as px  # 交互式图表
from io import BytesIO  # 内存中的字节流,用于文件下载

# ========== 页面配置(必须放在最前面) ==========
st.set_page_config(
    page_title="生信数据分析平台",  # 浏览器标签页标题
    page_icon="🧬",  # 标签页图标
    layout="wide"  # 宽屏布局
)

# ========== 标题区域 ==========
st.title("微生物组数据分析平台")
st.markdown("上传你的丰度表格,一键生成交互式可视化")
st.divider()  # 分隔线

# ========== Step 1: 上传数据 ==========
st.header("Step 1: 上传数据")

uploaded_file = st.file_uploader(
    "上传 CSV 丰度表(行=样本,列=物种)",  # 提示文字
    type=["csv", "tsv", "txt"],  # 允许的文件类型
    help="格式要求:第一列为样本ID,其余列为物种丰度"  # 帮助提示
)

# 如果没上传文件,使用演示数据
if uploaded_file is None:
    st.info("未上传文件,使用演示数据")
    # 生成演示数据
    np.random.seed(42)
    n_samples = 30
    species = ["Bacteroides", "Prevotella", "Faecalibacterium",
               "Roseburia", "Akkermansia", "Blautia", "Ruminococcus", "Lactobacillus"]

    demo_data = np.random.rand(n_samples, len(species))
    demo_data = demo_data / demo_data.sum(axis=1, keepdims=True)  # 归一化

    df = pd.DataFrame(demo_data, columns=species)
    df.insert(0, "Sample_ID", [f"S{i:03d}" for i in range(n_samples)])
    df["Group"] = ["Healthy"] * 15 + ["T2D"] * 15  # 添加分组列
else:
    # 读取上传的文件
    df = pd.read_csv(uploaded_file)
    st.success(f"成功读取文件!共 {df.shape[0]} 行 × {df.shape[1]} 列")

# ========== Step 2: 展示表格 ==========
st.header("Step 2: 数据预览")

# 用 expander 折叠长内容
with st.expander("点击展开完整数据表", expanded=False):
    st.dataframe(df, use_container_width=True)  # 自适应宽度

# 基本统计
col1, col2, col3, col4 = st.columns(4)
col1.metric("样本数", df.shape[0])
col2.metric("特征数", df.shape[1] - 2)  # 减去 Sample_ID 和 Group
col3.metric("分组数", df["Group"].nunique() if "Group" in df.columns else "N/A")
col4.metric("缺失值", df.isnull().sum().sum())

# ========== Step 3: 交互式图表 ==========
st.header("Step 3: 交互式可视化")

# 侧边栏:图表参数
with st.sidebar:
    st.header("可视化参数")
    chart_type = st.selectbox(
        "选择图表类型",
        ["物种丰度柱状图", "箱线图对比", "PCA 散点图", "热图", "相关性网络"]
    )

    if "Group" in df.columns:
        selected_group = st.multiselect(
            "选择展示的分组",
            df["Group"].unique().tolist(),
            default=df["Group"].unique().tolist()
        )
    else:
        selected_group = None

    top_n = st.slider("展示 Top N 物种", 3, 20, 8)

# 筛选数据
if selected_group and "Group" in df.columns:
    df_filtered = df[df["Group"].isin(selected_group)]
else:
    df_filtered = df

# 获取数值列(物种丰度列)
numeric_cols = df_filtered.select_dtypes(include=[np.number]).columns.tolist()

# 根据选择渲染不同图表
if chart_type == "物种丰度柱状图":
    # 计算各物种平均丰度,取 Top N
    mean_abundance = df_filtered[numeric_cols].mean().sort_values(ascending=False).head(top_n)

    fig = px.bar(
        x=mean_abundance.index,
        y=mean_abundance.values,
        title=f"Top {top_n} 物种平均丰度",
        labels={"x": "物种", "y": "平均相对丰度"},
        color=mean_abundance.values,
        color_continuous_scale="Viridis"
    )
    st.plotly_chart(fig, use_container_width=True)  # 在 Streamlit 中展示 Plotly 图

elif chart_type == "箱线图对比":
    if "Group" in df_filtered.columns:
        selected_species = st.selectbox("选择物种", numeric_cols)
        fig = px.box(
            df_filtered, x="Group", y=selected_species,
            color="Group", points="all",
            title=f"{selected_species} 在各组中的分布"
        )
        st.plotly_chart(fig, use_container_width=True)
    else:
        st.warning("数据中没有 Group 列,无法做分组对比")

elif chart_type == "PCA 散点图":
    from sklearn.decomposition import PCA
    # 对数值列做 PCA
    pca = PCA(n_components=2)
    pca_result = pca.fit_transform(df_filtered[numeric_cols])

    df_pca = pd.DataFrame({
        "PC1": pca_result[:, 0],
        "PC2": pca_result[:, 1],
    })
    if "Group" in df_filtered.columns:
        df_pca["Group"] = df_filtered["Group"].values

    fig = px.scatter(
        df_pca, x="PC1", y="PC2",
        color="Group" if "Group" in df_pca.columns else None,
        title=f"PCA(PC1: {pca.explained_variance_ratio_[0]:.1%}, PC2: {pca.explained_variance_ratio_[1]:.1%})"
    )
    st.plotly_chart(fig, use_container_width=True)

elif chart_type == "热图":
    top_species = df_filtered[numeric_cols].mean().sort_values(ascending=False).head(top_n).index
    fig = px.imshow(
        df_filtered[top_species].T,
        labels=dict(x="样本", y="物种", color="丰度"),
        title=f"Top {top_n} 物种丰度热图",
        color_continuous_scale="RdYlBu_r",
        aspect="auto"
    )
    st.plotly_chart(fig, use_container_width=True)

elif chart_type == "相关性网络":
    # 物种间相关性矩阵
    top_species = df_filtered[numeric_cols].mean().sort_values(ascending=False).head(top_n).index
    corr_matrix = df_filtered[top_species].corr()
    fig = px.imshow(
        corr_matrix,
        title="物种间相关性矩阵",
        color_continuous_scale="RdBu_r",
        zmin=-1, zmax=1
    )
    st.plotly_chart(fig, use_container_width=True)

# ========== Step 4: 下载结果 ==========
st.header("Step 4: 下载结果")

col1, col2 = st.columns(2)

with col1:
    # 下载处理后的 CSV
    csv_buffer = BytesIO()
    df_filtered.to_csv(csv_buffer, index=False)
    st.download_button(
        label="下载筛选后的数据 (CSV)",  # 按钮文字
        data=csv_buffer.getvalue(),  # 文件内容
        file_name="filtered_data.csv",  # 下载的文件名
        mime="text/csv"  # MIME 类型
    )

with col2:
    # 下载统计摘要
    summary = df_filtered[numeric_cols].describe()
    summary_buffer = BytesIO()
    summary.to_csv(summary_buffer)
    st.download_button(
        label="下载统计摘要 (CSV)",
        data=summary_buffer.getvalue(),
        file_name="statistics_summary.csv",
        mime="text/csv"
    )

# ========== 页脚 ==========
st.divider()
st.caption("Built with Streamlit + Plotly | 面试展示用")

运行命令:

streamlit run bioinfo_app.py

工具对比:Plotly vs matplotlib vs seaborn vs Bokeh

维度 matplotlib seaborn Plotly Bokeh
交互性 无(静态图) 无(基于 matplotlib) 强(缩放/悬停/筛选) 强(类似 Plotly)
学习曲线 中等(API 繁琐) 低(高级封装) 低(Express 一行出图) 中等
出图美观度 需要大量调参 默认就好看 默认好看+交互 好看
3D 支持 有但难用 原生支持,流畅 有限
Web 集成 差(需 mpld3) 原生 HTML 原生 HTML
Streamlit 兼容 st.pyplot() st.pyplot() st.plotly_chart() 需要额外适配
论文投稿 首选(期刊认可) 首选 可导出静态图 较少用于论文
动画 复杂 不支持 原生 animation 支持
生态/社区 最大 大且增长快 中等
适合场景 论文图、精细控制 统计图快速出图 数据探索、Web 展示 大数据流式

结论: - 投论文 → matplotlib/seaborn - 数据探索 + 汇报展示 → Plotly - 做 Web App → Plotly + Streamlit - 大规模实时数据 → Bokeh


常见报错与解决

1. Plotly 图在 Jupyter 中不显示

# 症状:fig.show() 执行了但什么都看不到
# 原因:Jupyter 渲染器没配置

# 解决方案 1:安装扩展
pip install nbformat
jupyter labextension install jupyterlab-plotly  # JupyterLab

# 解决方案 2:改用离线模式
import plotly.io as pio
pio.renderers.default = "notebook"  # Jupyter Notebook
# 或
pio.renderers.default = "browser"  # 直接弹浏览器窗口

2. Streamlit 报错 st.set_page_config() can only be called once

# 症状:SetPageConfigMustBeFirstCommandError
# 原因:set_page_config 必须是脚本中第一个 Streamlit 命令

# 错误写法:
import streamlit as st
st.title("Hello")  # 这行在 set_page_config 之前!
st.set_page_config(page_title="My App")

# 正确写法:
import streamlit as st
st.set_page_config(page_title="My App")  # 必须第一个调用
st.title("Hello")

3. ValueError: Mime type rendering requires nbformat>=4.2.0

# 原因:缺少 nbformat 包
pip install nbformat>=4.2.0

# 如果还不行,重启 Jupyter kernel

4. Streamlit 文件上传后数据丢失(页面刷新)

# 症状:每次交互操作后,上传的文件"消失"了
# 原因:Streamlit 每次交互都会重新执行整个脚本

# 解决方案:使用 session_state 缓存数据
import streamlit as st
import pandas as pd

uploaded_file = st.file_uploader("上传文件")

if uploaded_file is not None:
    # 把数据存到 session_state 中,不会因为刷新丢失
    st.session_state["df"] = pd.read_csv(uploaded_file)

# 后续使用时从 session_state 取
if "df" in st.session_state:
    df = st.session_state["df"]
    st.dataframe(df)

5. Plotly 导出 PDF/PNG 报错 ValueError: kaleido

# 症状:fig.write_image() 报错找不到 kaleido
# 原因:Plotly 导出静态图需要 kaleido 引擎

pip install -U kaleido

# 如果 conda 环境有冲突:
conda install -c conda-forge python-kaleido

6. Streamlit 端口被占用

# 症状:Address already in use / Port 8501 is already in use
# 原因:之前的 Streamlit 进程没关掉

# 解决方案 1:指定其他端口
streamlit run app.py --server.port 8502

# 解决方案 2:杀掉占用的进程
# Linux/Mac:
lsof -i :8501
kill -9 <PID>

# Windows:
netstat -ano | findstr :8501
taskkill /PID <PID> /F

速查表

Plotly 图表类型速查

图表 函数 适用场景
折线图 px.line() 时间序列、趋势变化
柱状图 px.bar() 分类对比、丰度排名
散点图 px.scatter() 两变量关系、聚类
3D散点 px.scatter_3d() PCA/t-SNE 3D 展示
箱线图 px.box() 分组分布对比
小提琴图 px.violin() 分布形态 + 密度
热图 px.imshow() 丰度矩阵、相关性
直方图 px.histogram() 单变量分布
饼图 px.pie() 组成比例(样本少时)
堆叠柱状 px.bar(barmode="stack") 群落组成
旭日图 px.sunburst() 分类层级
树图 px.treemap() 层级 + 面积比例
气泡图 px.scatter(size=...) 三变量关系
平行坐标 px.parallel_coordinates() 多维数据模式
动画图 px.scatter(animation_frame=...) 时间演变

Streamlit 组件速查

组件 函数 用途
标题 st.title() / st.header() 页面结构
文本 st.write() / st.markdown() 输出内容
表格 st.dataframe() / st.table() 展示数据
指标 st.metric() KPI 展示
图表 st.plotly_chart() Plotly 图
图表 st.pyplot() matplotlib 图
文本输入 st.text_input() 用户输入
数字输入 st.number_input() 数值参数
滑块 st.slider() 范围选择
下拉选择 st.selectbox() 单选
多选 st.multiselect() 多选
复选框 st.checkbox() 开关
单选按钮 st.radio() 互斥选择
文件上传 st.file_uploader() 上传数据
下载按钮 st.download_button() 导出结果
侧边栏 st.sidebar 参数面板
列布局 st.columns() 多列排列
折叠区 st.expander() 折叠内容
进度条 st.progress() 任务进度
状态消息 st.success() / st.error() / st.warning() 反馈
缓存 @st.cache_data 避免重复计算
会话状态 st.session_state 跨交互保持数据

延伸资源

官方文档

  • Plotly Python: https://plotly.com/python/
  • Streamlit 官方: https://docs.streamlit.io/
  • Plotly Express API: https://plotly.com/python/plotly-express/

生信相关

  • Plotly 生物学图表示例: https://plotly.com/python/v3/ipython-notebooks/bioinformatics/
  • microbiome-dashboard(微生物组 Streamlit 参考): 在 GitHub 搜索 "microbiome streamlit"

进阶学习

  • Dash(Plotly 的完整 Web 框架): https://dash.plotly.com/
  • Streamlit 组件库: https://streamlit.io/components
  • Plotly 配色方案大全: https://plotly.com/python/builtin-colorscales/

部署分享

  • Streamlit Cloud(免费部署): https://streamlit.io/cloud
  • 把你的 app 部署到云端,面试时直接发链接给面试官

面试加分建议

  1. 准备一个 Streamlit App 部署在线上:面试时说"我做了个交互式分析平台,您可以直接访问",立刻和其他候选人拉开差距。

  2. 火山图一定要用交互版:面试官问"哪些基因差异最显著",你让他自己鼠标悬停去看,而不是背列表。

  3. PCA 用 3D 版:2D 的 PCA 图大家都会,3D 可旋转的那种立刻显得技术含量高。

  4. 学会 @st.cache_data:大数据集加载慢时用缓存装饰器,面试中提到这个说明你考虑了性能。