当前位置:首页>python>Python绘制散点图 + KDE边缘密度 + 点密度着色(含代码)

Python绘制散点图 + KDE边缘密度 + 点密度着色(含代码)

  • 2026-09-08 10:29:49
Python绘制散点图 + KDE边缘密度 + 点密度着色(含代码)
案例代码见文末,感谢您关注PFC小姐姐,麻烦您多多对推文点赞、收藏及转发,并衷心希望您多多指教🙏,帮助PFC小姐姐进步提升。

引言

很多时候我们手里有两列连续变量的数据,第一反应就是画个散点图看看相关性,但散点图一旦点数变多就会立刻暴露两个问题:第一,点会堆成一团,密集区域到底有多密、是否存在多簇结构、是否有长尾离群点,肉眼很难分辨;第二,只看散点往往忽略了边缘分布,导致你对“这两列数据各自是什么分布、是否偏态、是否双峰”完全没有直觉。更麻烦的是,当你想把图放进论文或报告里,单纯的散点图信息密度不够,读者看完也很难快速形成判断。本文绘制的第一张图把散点、边缘KDE密度分布、点密度着色放到同一张图里,让你一眼看清“相关结构 + 分布形态”;第二张图在第一张的基础上继续加上二维密度等高线、线性趋势线,把“结构描述”升级成更接近论文表达的“解释型图形”。

图1、Scatter + 边缘KDE + 点密度着色

这张图的核心目标是:让散点图在点很多时依然可读,并且同时补上一维边缘分布信息。

  • 主图散点(中间大图)每个点代表一条观测 (x, y)。为了避免点云挤在一起看不清,代码用二维 KDE 估计每个点所在位置的“局部密度”,然后把这个密度映射成颜色(colorbar 对应 “Point density”)。这样你不用猜哪里是高密区:颜色最亮的地方就是样本最集中、最典型的区域;颜色逐渐变暗的区域对应稀疏样本与离群点。同时图里还画了 x=0、y=0 的虚线作为参考基准,便于读者判断象限分布和整体偏移。

  • 上方边缘 KDE(横向密度)上方曲线是 x 的核密度估计(KDE),它告诉你 x 的总体分布形状:是单峰还是双峰、是否偏态、是否有长尾。对读者来说,这比只看散点更直观,因为散点里 x 的密度信息会被 y 的变化“分散注意力”。

  • 右侧边缘 KDE(纵向密度)右侧曲线对应 y 的 KDE,同样用来快速判断 y 的分布形态。比如 y 是否有厚尾、是否存在两个聚集层次,都会在边缘密度里更明显。

图2、增加二维等高线 + 趋势线 + 95%区间

图2并没有改变图1的主体框架,而是在相同信息底座上叠加了“解释层”,让图从“描述”走向“表达结论”。

  • 二维密度等高线(2D KDE contours)在散点上叠加白色等高线,等高线对应二维密度的高分位水平。它的作用是把“颜色上的密度渐变”进一步变成轮廓结构:读者可以非常快速地看出主簇的形状、椭圆倾斜方向、多簇是否存在、以及分布是否呈现明显非线性弯曲。对于论文图来说,等高线比单纯颜色更“结构化”,更像统计图表达。

  • 趋势线(线性拟合)趋势线给出一个总体关系的“摘要”。这里用最小二乘做线性拟合,是为了让读者一眼获得“整体斜率与方向感”,而不是在点云里自己估计。大家也可以替换成 LOWESS、样条或分段回归,但线性版本足够通用。

具体Python如下:

import numpy as npimport matplotlib.pyplot as pltfrom matplotlib.gridspec import GridSpecfrom scipy.stats import gaussian_kdedef make_synthetic_data(seed=2026, n1=1600,n2=900, outliers=35):rng = np.random.default_rng(seed)    x= np.concatenate([rng.normal(0.0, 1.0, n1), rng.normal(2.2, 0.7, n2)])sigma = 0.35 + 0.20 * (1 / (1 + np.exp(-(x - 1.0)))) + 0.08 * np.abs(x)    y= 0.9 * x + 0.25 * np.sin(1.6 * x) + rng.normal(0, sigma, size=x.size)idx = rng.choice(x.size, size=outliers, replace=False)y[idx] += rng.normal(0, 3.0, size=outliers)return x, ydef kde_1d(samples, grid,bw="scott"):return gaussian_kde(samples, bw_method=bw)(grid)def scatter_marginal_kde(x, y,out_png,title,cmap_scatter="cividis",kde_color="0.22",add_contours=False,add_trend=False,dpi=240,):plt.rcParams.update({"font.size": 11,"axes.titlesize": 14,        "axes.labelsize": 12,"xtick.labelsize": 10.5,"ytick.labelsize": 10.5,})    #2D KDE:用于点密度着色 & 等高线k2 = gaussian_kde(np.vstack([x, y]), bw_method="scott")dens = k2(np.vstack([x, y]))order = np.argsort(dens)  # 低密度先画,高密度后画xs, ys, ds = x[order], y[order], dens[order]    #边缘 KDE 网格x_grid = np.linspace(x.min() - 0.8, x.max() + 0.8, 450)y_grid = np.linspace(y.min() - 0.8, y.max() + 0.8, 450)kx = kde_1d(x, x_grid)ky = kde_1d(y, y_grid)    #布局:main | gap | colorbar(cax) | gap | rightKDEfig = plt.figure(figsize=(10.6, 7.4), dpi=dpi)gs = GridSpec(4, 5, figure=fig,width_ratios=[5.2, 0.26, 0.22, 0.28, 1.55],height_ratios=[1.25, 0.18, 5.05, 0.06], wspace=0.0, hspace=0.0    )ax_top   = fig.add_subplot(gs[0,0])ax_main  = fig.add_subplot(gs[2,0], sharex=ax_top)cax      = fig.add_subplot(gs[2,2])ax_right = fig.add_subplot(gs[2, 4], sharey=ax_main)    #主散点:点密度着色sc = ax_main.scatter(xs, ys, c=ds, s=10, cmap=cmap_scatter, alpha=0.95,linewidths=0, zorder=2)    #参考线ax_main.axhline(0, lw=1.1, ls="--", color="0.55",alpha=0.7, zorder=1)ax_main.axvline(0, lw=1.1, ls="--", color="0.55",alpha=0.35, zorder=1)    #上边缘 KDE(x)ax_top.fill_between(x_grid, 0, kx, color=kde_color, alpha=0.12,zorder=1)ax_top.plot(x_grid, kx, color=kde_color, lw=2.1, zorder=2)    #右边缘 KDE(y)ax_right.fill_betweenx(y_grid, 0, ky, color=kde_color, alpha=0.12,zorder=1)ax_right.plot(ky, y_grid, color=kde_color, lw=2.1, zorder=2)    #2D KDE 等高线(可选)if add_contours:X, Y = np.meshgrid(x_grid, y_grid)Z = k2(np.vstack([X.ravel(), Y.ravel()])).reshape(X.shape)levels = np.quantile(Z, [0.80, 0.90, 0.96, 0.985])ax_main.contour(X, Y, Z, levels=levels, colors="white",linewidths=1.0, alpha=0.9, zorder=4)    #趋势线(可选):线性拟合 y = a + b xif add_trend:A = np.c_[np.ones_like(x), x]a, b = np.linalg.lstsq(A, y, rcond=None)[0]x_line = np.linspace(x_grid.min(), x_grid.max(), 260)y_line = a + b * x_line# 趋势线用深灰,不用纯黑ax_main.plot(x_line, y_line, color="0.15", lw=2.4, zorder=6)    #主图网格ax_main.grid(True, which="major", alpha=0.16, lw=0.9)ax_main.minorticks_on()ax_main.grid(True, which="minor", alpha=0.07, lw=0.6)ax_main.set_xlabel("Synthetic feature X")ax_main.set_ylabel("Synthetic response Y")    #top / right 轴简化plt.setp(ax_top.get_xticklabels(), visible=False)ax_top.tick_params(axis="x", length=0)ax_top.set_ylabel("KDE", labelpad=8)ax_top.grid(False)plt.setp(ax_right.get_yticklabels(), visible=False)ax_right.tick_params(axis="y", length=0)ax_right.set_xlabel("KDE", labelpad=8)ax_right.grid(False)for a in (ax_top, ax_main, ax_right):a.spines["top"].set_visible(False)a.spines["right"].set_visible(False)    #colorbar:独立轴,避免遮挡右 KDEcb = fig.colorbar(sc, cax=cax)cb.set_label("Point density (KDE)", fontsize=11, labelpad=10)cb.ax.tick_params(pad=6)    #标题:suptitle + 留白,避免压住 top KDEfig.suptitle(title, y=0.985, fontsize=15)fig.tight_layout(rect=[0, 0, 1, 0.96])fig.savefig(out_png, dpi=dpi, bbox_inches="tight",pad_inches=0.04)plt.close(fig)if __name__ == "__main__":x, y = make_synthetic_data()    #图1:cividis(更克制)scatter_marginal_kde(x, y,out_png="fig1_scatter_marginalKDE.png",title="Figure 1 — Scatter with marginal KDE (synthetic data)",cmap_scatter="cividis",kde_color="0.22",add_contours=False,add_trend=False    )    #图2:magma(与图1区分)+ 等高线 + 趋势线scatter_marginal_kde(x, y,out_png="fig2_scatter_marginalKDE_plus.png",title="Figure 2 — + 2D density contours + trend line (syntheticdata)",cmap_scatter="magma",kde_color="0.22",add_contours=True,add_trend=True    )

特别声明:

以上代码与文案均为网上资料整合而成,仅供广大同行们参考学习,如有侵权请联系删除。

如有其他需要,欢迎关注我的咸鱼号:pfc小姐姐

最新文章

随机文章