
©[悠悠智汇笔记] 版权所有
🙏请尊重劳动成果,守护每一份劳动心血;⚖️未经授权,不得以为任何方式转载、摘编或抄袭。🔄转载合作请后台联系授权,侵权必究。
最近在Nature子刊看到一幅散点图加回归拟合图,挺好看的,分享给大家。图的横轴和纵轴分别表示两个连续变量(原文里是基因组大小和基因数量),用不同符号或颜色区分不同数据来源或类别,并叠加一条拟合趋势线来展示变量之间的整体关系。这类图在科研和数据分析中非常常见,主要用途是:(1)观察两个变量之间是否存在相关性;(2)比较不同数据集或分组在该关系中的分布差异;(3)识别异常值或偏离趋势的样本;(4)辅助判断数据的一致性、质量和潜在偏差。所以,它本质上是一种用于探索变量间关系并比较多组数据模式的组合图。

01

环境配置

导入绘图、数据处理和统计所需的全部库,并集中定义全局参数(如颜色、分组顺序、坐标轴范围等)。这样只需修改一处,就能统一控制整张图的风格和输入。
# ==========================================
# 1. 全局配置:在这里改一次,全图生效
# ==========================================
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import scipy.optimize as sciopt
import seaborn as sns
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from statannotations.Annotator import Annotator
CONFIG = {
"seed": 10, # 随机种子,保证每次运行生成的图、点的抖动一模一样
"input_file": "data.csv",
"colors": ["#3498db", "#e84393", "#546e7a"], # 蓝, 粉, 灰
"hue_order": ["SAG", "MAG", "WGS"],
"plot_limits": {
"xlim": (3 * 10 ** 5, 3 * 10 ** 7),
"ylim": (400, 15000),
"xlabel": "Genome total size [bp] (corrected)",
"ylabel": "Number of predicted CDS (corrected)"
}
}02

数据准备

读取 CSV 数据(若不存在则生成模拟数据),在对数空间中对基因组大小和基因数量做线性拟合,并计算每个样本的残差——用于后续分析“谁比预期多/少基因”。
# ==========================================
# 2. 数据处理:读取数据 + 计算残差(用于小图)
# ==========================================
def prepare_data(file_path):
"""把原始数据读进来,并算出拟合线需要的残差"""
try:
df = pd.read_csv(file_path, index_col=0)
except FileNotFoundError:
# 如果你还没准备好 csv,这里会生成假数据让你先能跑通
data = {
'length': np.random.uniform(10 ** 5, 10 ** 7, 500),
'number_of_cds': np.random.uniform(500, 10000, 500),
'type': np.random.choice(CONFIG["hue_order"], 500)
}
df = pd.DataFrame(data)
# 线性回归公式:y = ax + b (在 log 空间里跑)
def linear_func(x, a, b):
return a * x + b
# 算对数,因为生物数据通常服从幂律分布
log_x = np.log(df["length"])
log_y = np.log(df["number_of_cds"])
# 拟合:得到斜率 a 和 截距 b
popt, _ = sciopt.curve_fit(linear_func, log_x, log_y)
# 计算残差:实际值和预测值差了多少
df["log_cds"] = log_y
df["log_length"] = log_x
df["cds_fit"] = np.exp(linear_func(log_x, *popt))
df["residuals_log"] = log_y - np.log(df["cds_fit"])
return df, popt
03

主图绘制

用 seaborn 的 jointplot 画出带边缘密度图的散点图,设置对数坐标、网格、回归线,并美化图例(加入样本量 N=xxx),整体展示变量间的宏观关系。
# ==========================================
# 3. 绘图:主图(散点+边缘密度+回归线)
# ==========================================
def plot_main_joint(df, popt):
g = sns.jointplot(
data=df, x="length", y="number_of_cds", hue="type",
kind="scatter", alpha=0.7, height=6,
palette=CONFIG["colors"], hue_order=CONFIG["hue_order"],
joint_kws=dict(s=25, edgecolor='w', linewidth=0.5),
marginal_kws=dict(bw_adjust=0.2, fill=True)
)
# 消除顶部边缘图 Y 轴的科学计数法(如 1e-7)
g.ax_marg_x.ticklabel_format(axis='y', style='plain')
g.ax_joint.set(xscale="log", yscale="log", **CONFIG["plot_limits"])
g.ax_joint.grid(True, which="major", ls="--", alpha=0.5, zorder=0)
# 画回归线:在 log 空间生成点,再转回原始坐标
x_range = np.exp(np.linspace(df["log_length"].min(), df["log_length"].max(), 100))
y_fit = np.exp(popt[0] * np.log(x_range) + popt[1])
g.ax_joint.plot(x_range, y_fit, color="#34495e", ls="--", lw=1.5, zorder=5)
# 修改图例:加入样本量 N=xxx
counts = df["type"].value_counts()
handles, labels = g.ax_joint.get_legend_handles_labels()
new_labels = [f"MHQ {l} (N={counts.get(l, 0)})" for l in labels if l in counts]
g.ax_joint.legend(handles=handles, labels=new_labels, loc="upper left", fontsize=9)
return g
04

小图绘制

在主图右下角嵌入一个横向箱线图,显示三类数据的残差分布,并自动添加统计显著性星号;同时用白色圆角背景提升可读性,避免与主图元素混杂。
# ==========================================
# 4. 绘图:右下角箱线图(显示残差分布 + 显著性标注)
def add_inset_boxplot(joint_grid, df):
ax_ins = inset_axes(
joint_grid.ax_joint,
width="40%", height="22%",
loc="lower right",
bbox_to_anchor=(-0.03, 0.05, 1, 1),
bbox_transform=joint_grid.ax_joint.transAxes
)
box_colors = [CONFIG["colors"][2], CONFIG["colors"][1], CONFIG["colors"][0]]
box_args = dict(
data=df, x="residuals_log", y="type", orient="h",
order=["WGS", "MAG", "SAG"], palette=box_colors,
linewidth=1, fliersize=1.5
)
sns.boxplot(**box_args, ax=ax_ins)
pairs = [("WGS", "MAG"), ("WGS", "SAG"), ("MAG", "SAG")]
annot = Annotator(ax_ins, pairs, **box_args)
annot.configure(test="Mann-Whitney", text_format="star", loc="inside", verbose=0)
annot.apply_and_annotate()
ax_ins.set(xlabel=None, ylabel=None, yticks=[])
ax_ins.set_title("Residuals (log-scale)", fontsize=9, fontweight='bold', pad=12)
for s in ax_ins.spines.values():
s.set_visible(False)
rect = mpl.patches.FancyBboxPatch(
(0, 0), 1, 1, transform=ax_ins.transAxes,
facecolor="white", edgecolor="#bdc3c7", alpha=0.8,
boxstyle="round,pad=0.1,rounding_size=0.05", clip_on=False, zorder=0
)
ax_ins.add_patch(rect)
🌿 今日的分享就到这里啦~如果这些内容有为你带来帮助,欢迎轻点右下角的【👍赞】和【👀在看】,也欢迎分享给更多需要的人,感恩~
THE
END


数据和代码怎么获取?
点击关注后,后台回复关键词:
2025_map_005可直接获取完整的示例数据和代码
如有帮助,您的点赞、评论、转发是我持续创作的动力~

