数据可视化进阶: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 部署到云端,面试时直接发链接给面试官
面试加分建议¶
-
准备一个 Streamlit App 部署在线上:面试时说"我做了个交互式分析平台,您可以直接访问",立刻和其他候选人拉开差距。
-
火山图一定要用交互版:面试官问"哪些基因差异最显著",你让他自己鼠标悬停去看,而不是背列表。
-
PCA 用 3D 版:2D 的 PCA 图大家都会,3D 可旋转的那种立刻显得技术含量高。
-
学会
@st.cache_data:大数据集加载慢时用缓存装饰器,面试中提到这个说明你考虑了性能。