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 )