Python & MATLAB 绘制不同版本的火山图
·

导读
火山图是一种常见的数据可视化方式,以散点图为基础,通过横轴与纵轴组合展示两类信息:
-
横轴:某种差异或效应大小(Effect Size)
-
纵轴:显著性或置信度(P-value、FDR、-log10(p))
因为图形形状类似火山口两侧向上翻起的样子,因此被形象地称为“火山图(Volcano Plot)”。它常用于表达:
哪些数据点既“差异很大”,又“非常显著”。
简单说,火山图能帮你一眼看到:
- 哪些点“跳得最高”(非常显著)
- 哪些点“偏得最远”(差异很大)
- 哪些点同时满足“高 + 远”(极其重要)
一、火山图的含义是什么?
在火山图里,每一个点代表一个实体,例如:
-
基因
-
蛋白
-
指标
-
实验样本
-
用户特征
-
模型参数等
换句话说:找到最值得关注的关键点,对数据进行自动分层分类,从海量数据中快速抓住洞察点
二、火山图可以用于哪些场景?
虽然火山图最早流行于生物信息学,但实际应用场景已经非常广泛,例如如下场景:
(1)生物信息学 / 医学科研
-
DEG 差异基因分析
-
蛋白质组学显著性筛选
-
药物反应分析
-
临床指标对照分析
这是火山图的“主场”,几乎所有组学论文都有火山图。
(2)A/B 测试分析(运营 / 增长)
火山图可以快速找出:
-
哪些指标在 A/B 两组中差异很大
-
哪些表现具有统计显著性
尤其适合运营策略优化、功能上线效果评估。
(3)机器学习特征分析
-
特征重要性 vs 显著性
-
模型参数变化对结果的影响
-
超参数敏感度分析
让你快速知道哪些特征/参数最关键。
(4)金融风控 / 信贷模型
-
不同用户群特征变化分析
-
风险评分显著性筛选
-
欺诈指标异常检测
让图形化判断风险来源更直观。
(5)科研、数据可视化展示
火山图非常直观、易懂、视觉冲击力强,适合:
-
项目汇报
-
学术会议
-
数据报告
-
竞赛作品展示
总之 —— 只要你需要表达“差异 + 显著性”,就能用火山图。
三、Matlab绘制不同版本的火山图
%% 生成模拟数据
rng(42); % 设置随机种子
n_genes = 5000;
% 模拟 log2 Fold Change 和 p-value
log2FC = randn(n_genes, 1) * 1.5;
pvalue = 10.^(-abs(randn(n_genes, 1) * 2));
% 添加一些显著差异的基因
sig_idx = randperm(n_genes, 200);
log2FC(sig_idx) = log2FC(sig_idx) + sign(randn(200,1)) .* (2 + rand(200,1)*2);
pvalue(sig_idx) = 10.^(-4 - rand(200,1)*6);
neg_log10_pval = -log10(pvalue);
%% 设置阈值
fc_threshold = 1; % |log2FC| > 1
pval_threshold = 0.05; % p-value < 0.05
neg_log10_pval_threshold = -log10(pval_threshold);
%% 分类基因
up_regulated = (log2FC > fc_threshold) & (pvalue < pval_threshold);
down_regulated = (log2FC < -fc_threshold) & (pvalue < pval_threshold);
not_significant = ~up_regulated & ~down_regulated;
%% 绑制火山图
figure('Position', [100, 100, 800, 600], 'Color', 'w');
% 绑制不显著的点
scatter(log2FC(not_significant), neg_log10_pval(not_significant), ...
25, [0.7 0.7 0.7], 'filled', 'MarkerFaceAlpha', 0.4);
hold on;
% 绑制下调基因
scatter(log2FC(down_regulated), neg_log10_pval(down_regulated), ...
40, [0.2 0.4 0.8], 'filled', 'MarkerFaceAlpha', 0.7);
% 绑制上调基因
scatter(log2FC(up_regulated), neg_log10_pval(up_regulated), ...
40, [0.9 0.2 0.2], 'filled', 'MarkerFaceAlpha', 0.7);
% 添加阈值线
xline(-fc_threshold, '--', 'Color', [0.3 0.3 0.3], 'LineWidth', 1.2);
xline(fc_threshold, '--', 'Color', [0.3 0.3 0.3], 'LineWidth', 1.2);
yline(neg_log10_pval_threshold, '--', 'Color', [0.3 0.3 0.3], 'LineWidth', 1.2);
xlabel('log_2(Fold Change)', 'FontSize', 14, 'FontWeight', 'bold');
ylabel('-log_{10}(p-value)', 'FontSize', 14, 'FontWeight', 'bold');
title('Volcano Plot', 'FontSize', 16, 'FontWeight', 'bold');
legend({'Not Significant', 'Down-regulated', 'Up-regulated'}, ...
'Location', 'northeast', 'FontSize', 11);
% 添加统计信息
text_str = sprintf('Up: %d\nDown: %d\nNS: %d', ...
sum(up_regulated), sum(down_regulated), sum(not_significant));
text(max(log2FC)*0.7, max(neg_log10_pval)*0.9, text_str, ...
'FontSize', 11, 'BackgroundColor', 'w', 'EdgeColor', 'k');
set(gca, 'FontSize', 12, 'LineWidth', 1.2, 'Box', 'on');
xlim([-6 6]);
grid on;
grid minor;
hold off;

%% 生成模拟数据
rng(42);
n_genes = 5000;
log2FC = randn(n_genes, 1) * 1.5;
pvalue = 10.^(-abs(randn(n_genes, 1) * 2));
sig_idx = randperm(n_genes, 200);
log2FC(sig_idx) = log2FC(sig_idx) + sign(randn(200,1)) .* (2 + rand(200,1)*2);
pvalue(sig_idx) = 10.^(-4 - rand(200,1)*6);
neg_log10_pval = -log10(pvalue);
fc_threshold = 1;
pval_threshold = 0.05;
%% 创建渐变色火山图
figure('Position', [100, 100, 900, 700], 'Color', 'w');
% 计算颜色值(基于 log2FC)
color_values = log2FC;
% 绑制所有点,颜色根据 log2FC 渐变
scatter(log2FC, neg_log10_pval, 30, color_values, 'filled', ...
'MarkerFaceAlpha', 0.6, 'MarkerEdgeColor', 'none');
hold on;
% 高亮显著基因
significant = (abs(log2FC) > fc_threshold) & (pvalue < pval_threshold);
scatter(log2FC(significant), neg_log10_pval(significant), 50, ...
color_values(significant), 'filled', 'MarkerEdgeColor', 'k', ...
'LineWidth', 0.5, 'MarkerFaceAlpha', 0.9);
% 颜色映射
colormap(coolwarm_colormap());
cb = colorbar;
cb.Label.String = 'log_2(Fold Change)';
cb.Label.FontSize = 12;
clim([-4 4]);
% 添加阈值线
xline(-fc_threshold, '--', 'Color', 'k', 'LineWidth', 1.5, 'Alpha', 0.5);
xline(fc_threshold, '--', 'Color', 'k', 'LineWidth', 1.5, 'Alpha', 0.5);
yline(-log10(pval_threshold), '--', 'Color', 'k', 'LineWidth', 1.5, 'Alpha', 0.5);
% 标注 Top 基因
[~, top_idx] = maxk(neg_log10_pval .* abs(log2FC), 10);
for i = 1:length(top_idx)
text(log2FC(top_idx(i))+0.15, neg_log10_pval(top_idx(i)), ...
sprintf('Gene%d', top_idx(i)), 'FontSize', 9, 'Color', [0.2 0.2 0.2]);
end
xlabel('log_2(Fold Change)', 'FontSize', 14, 'FontWeight', 'bold');
ylabel('-log_{10}(p-value)', 'FontSize', 14, 'FontWeight', 'bold');
title('Gradient Volcano Plot', 'FontSize', 18, 'FontWeight', 'bold');
set(gca, 'FontSize', 12, 'LineWidth', 1.2);
xlim([-6 6]);
grid on;
alpha(0.7);
hold off;
%% 自定义 coolwarm 颜色映射函数
function cmap = coolwarm_colormap()
n = 256;
r = [linspace(0.2, 1, n/2), linspace(1, 0.8, n/2)]';
g = [linspace(0.4, 1, n/2), linspace(1, 0.2, n/2)]';
b = [linspace(0.8, 1, n/2), linspace(1, 0.2, n/2)]';
cmap = [r, g, b];
end

四、Python绘制不同版本的火山图
import numpy as np
import matplotlib.pyplot as plt
# 设置随机种子
np.random.seed(42)
# 生成模拟数据
n_genes = 5000
# 模拟 log2 Fold Change 和 p-value
log2FC = np.random.randn(n_genes) * 1.5
pvalue = 10 ** (-np.abs(np.random.randn(n_genes) * 2))
# 添加一些显著差异的基因
sig_idx = np.random.choice(n_genes, 200, replace=False)
log2FC[sig_idx] = log2FC[sig_idx] + np.sign(np.random.randn(200)) * (2 + np.random.rand(200) * 2)
pvalue[sig_idx] = 10 ** (-4 - np.random.rand(200) * 6)
neg_log10_pval = -np.log10(pvalue)
# 设置阈值
fc_threshold = 1 # |log2FC| > 1
pval_threshold = 0.05 # p-value < 0.05
neg_log10_pval_threshold = -np.log10(pval_threshold)
# 分类基因
up_regulated = (log2FC > fc_threshold) & (pvalue < pval_threshold)
down_regulated = (log2FC < -fc_threshold) & (pvalue < pval_threshold)
not_significant = ~up_regulated & ~down_regulated
# 绑制火山图
fig, ax = plt.subplots(figsize=(10, 8), facecolor='white')
# 绑制不显著的点
ax.scatter(log2FC[not_significant], neg_log10_pval[not_significant],
s=25, c='#B0B0B0', alpha=0.4, label='Not Significant', edgecolors='none')
# 绑制下调基因
ax.scatter(log2FC[down_regulated], neg_log10_pval[down_regulated],
s=40, c='#3366CC', alpha=0.7, label='Down-regulated', edgecolors='none')
# 绑制上调基因
ax.scatter(log2FC[up_regulated], neg_log10_pval[up_regulated],
s=40, c='#E63333', alpha=0.7, label='Up-regulated', edgecolors='none')
# 添加阈值线
ax.axvline(x=-fc_threshold, color='#4D4D4D', linestyle='--', linewidth=1.2)
ax.axvline(x=fc_threshold, color='#4D4D4D', linestyle='--', linewidth=1.2)
ax.axhline(y=neg_log10_pval_threshold, color='#4D4D4D', linestyle='--', linewidth=1.2)
# 美化
ax.set_xlabel(r'$\log_2$(Fold Change)', fontsize=14, fontweight='bold')
ax.set_ylabel(r'$-\log_{10}$(p-value)', fontsize=14, fontweight='bold')
ax.set_title('Volcano Plot', fontsize=16, fontweight='bold')
ax.legend(loc='upper right', fontsize=11, framealpha=0.9)
# 添加统计信息
text_str = f'Up: {np.sum(up_regulated)}\nDown: {np.sum(down_regulated)}\nNS: {np.sum(not_significant)}'
ax.text(0.95, 0.95, text_str, transform=ax.transAxes, fontsize=11,
verticalalignment='top', horizontalalignment='right',
bbox=dict(boxstyle='round', facecolor='white', edgecolor='black'))
ax.set_xlim(-6, 6)
ax.tick_params(axis='both', labelsize=12)
ax.spines['top'].set_linewidth(1.2)
ax.spines['right'].set_linewidth(1.2)
ax.spines['bottom'].set_linewidth(1.2)
ax.spines['left'].set_linewidth(1.2)
ax.grid(True, alpha=0.3)
ax.grid(True, which='minor', alpha=0.15)
ax.minorticks_on()
plt.tight_layout()
plt.savefig('volcano_plot_v1.png', dpi=300, bbox_inches='tight')
plt.show()

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap
# 设置随机种子
np.random.seed(42)
# 生成模拟数据
n_genes = 5000
log2FC = np.random.randn(n_genes) * 1.5
pvalue = 10 ** (-np.abs(np.random.randn(n_genes) * 2))
sig_idx = np.random.choice(n_genes, 200, replace=False)
log2FC[sig_idx] = log2FC[sig_idx] + np.sign(np.random.randn(200)) * (2 + np.random.rand(200) * 2)
pvalue[sig_idx] = 10 ** (-4 - np.random.rand(200) * 6)
neg_log10_pval = -np.log10(pvalue)
fc_threshold = 1
pval_threshold = 0.05
# 自定义 coolwarm 颜色映射
def create_coolwarm_cmap():
"""创建蓝-白-红渐变色"""
colors = ['#2166AC', '#4393C3', '#92C5DE', '#D1E5F0', '#F7F7F7',
'#FDDBC7', '#F4A582', '#D6604D', '#B2182B']
return LinearSegmentedColormap.from_list('coolwarm_custom', colors, N=256)
# 创建渐变色火山图
fig, ax = plt.subplots(figsize=(11, 9), facecolor='white')
# 计算颜色值(基于 log2FC)
color_values = log2FC
# 绑制所有点,颜色根据 log2FC 渐变
scatter = ax.scatter(log2FC, neg_log10_pval, s=30, c=color_values,
cmap=create_coolwarm_cmap(), alpha=0.6, edgecolors='none')
# 高亮显著基因
significant = (np.abs(log2FC) > fc_threshold) & (pvalue < pval_threshold)
ax.scatter(log2FC[significant], neg_log10_pval[significant], s=50,
c=color_values[significant], cmap=create_coolwarm_cmap(),
edgecolors='black', linewidths=0.5, alpha=0.9)
# 颜色条
cbar = plt.colorbar(scatter, ax=ax, shrink=0.8)
cbar.set_label(r'$\log_2$(Fold Change)', fontsize=12)
scatter.set_clim(-4, 4)
# 添加阈值线
ax.axvline(x=-fc_threshold, color='black', linestyle='--', linewidth=1.5, alpha=0.5)
ax.axvline(x=fc_threshold, color='black', linestyle='--', linewidth=1.5, alpha=0.5)
ax.axhline(y=-np.log10(pval_threshold), color='black', linestyle='--', linewidth=1.5, alpha=0.5)
# 标注 Top 基因
score = neg_log10_pval * np.abs(log2FC)
top_idx = np.argsort(score)[-10:]
for i in top_idx:
ax.annotate(f'Gene{i}', (log2FC[i], neg_log10_pval[i]),
xytext=(log2FC[i] + 0.3, neg_log10_pval[i]),
fontsize=9, color='#333333',
arrowprops=dict(arrowstyle='-', color='gray', lw=0.5))
ax.set_xlabel(r'$\log_2$(Fold Change)', fontsize=14, fontweight='bold')
ax.set_ylabel(r'$-\log_{10}$(p-value)', fontsize=14, fontweight='bold')
ax.set_title('Gradient Volcano Plot', fontsize=18, fontweight='bold')
ax.set_xlim(-6, 6)
ax.tick_params(axis='both', labelsize=12)
ax.spines['top'].set_linewidth(1.2)
ax.spines['right'].set_linewidth(1.2)
ax.spines['bottom'].set_linewidth(1.2)
ax.spines['left'].set_linewidth(1.2)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('volcano_plot_v2.png', dpi=300, bbox_inches='tight')
plt.show()



总结
火山图能帮你:
-
甄别差异显著的关键点
-
清晰表达复杂数据关系
-
让你的数据报告更专业、更有冲击力
更多推荐


所有评论(0)