Matplotlib三元图KDE密度缩放异常问题排查
三元KDE密度缩放异常问题排查与解决
问题概述
使用Matplotlib + SciPy的gaussian_kde绘制三元核密度估计(KDE)图时,出现密度缩放不符合预期的情况:稀疏的X组数据对应的KDE未呈现暗淡效果,反而和数据量更多的Y、Z组密度强度相当,但散点图已正确展示三组数据的分布差异。
疑问解答与问题修复
1. 为什么X的KDE密度呈现结构化且未如预期般暗淡?
核心原因是每组KDE的密度是独立归一化的,且你手动设置的密度截断逻辑进一步抹平了组间的密度差异:
gaussian_kde输出的是概率密度,会自动归一化到整个分布的积分=1,数据量少的组单个核的贡献会被放大,最终和数据多的组的密度数值范围接近- 你对每组都用自身的
max_density做截断,相当于把每组的密度都拉伸到了相同的相对范围,导致三组的颜色刻度没有统一基准,视觉上看起来密度强度一致
修复方案:
- 先计算三组KDE的全局最大密度,统一设置颜色刻度的
vmin和vmax参数,固定所有contourf的颜色范围 - 移除或调整手动截断逻辑,保留原始密度的相对差异
2. 带宽选择(bw_method=0.25;'scott')是否导致了不必要的平滑效果?
是的,固定值bw_method=0.25对于稀疏的X组来说带宽过大,会过度平滑数据,导致原本稀疏的点被扩散成结构化的分布:
- 带宽控制KDE的平滑程度,带宽越大,平滑效果越强,稀疏数据会被拉成更连续的分布
- 对于小样本数据,固定带宽会比自适应带宽(如'scott'或'silverman')产生更严重的过度平滑
修复方案:
- 改用自适应带宽方法,比如
bw_method='scott'或bw_method='silverman',这类方法会根据样本量自动调整带宽,小样本对应更小的带宽,减少过度平滑 - 如果需要手动调整,可针对不同组设置不同带宽,比如给X组设置更小的
bw_method值(如0.1)
3. 我是否在数据归一化或转换过程中出现错误?
数据归一化和笛卡尔坐标转换逻辑是正确的,细节上可以做一点优化:
- 归一化时
data = data[valid_rows] / sum_values[valid_rows]确保了三元组的和为1,符合 barycentric 坐标要求 - 笛卡尔转换使用的等边三角形顶点是标准设置,没有问题
- 噪声添加逻辑(
cartesian_points += np.random.normal(0, 1e-6, ...))合理,避免了协方差矩阵秩不足的问题
优化点:
- 可以在归一化后添加校验:
assert np.allclose(np.sum(data, axis=1), 1),确保归一化正确
修复后的核心代码示例
以下是修改后的关键代码片段,解决上述问题:
# 先预计算所有组的KDE密度,获取全局最大密度 all_kde_values = [] ternary_data = [] for data_columns, norm_filter in ternary_combinations: filtered_df = df[df['NN'] == norm_filter] data = filtered_df[data_columns].dropna().to_numpy() if data.shape[0] == 0: ternary_data.append(None) continue sum_values = np.sum(data, axis=1, keepdims=True) valid_rows = sum_values[:, 0] > 0 data = data[valid_rows] / sum_values[valid_rows] cartesian_points = barycentric_to_cartesian(data) if np.linalg.matrix_rank(np.cov(cartesian_points.T)) < 2: cartesian_points += np.random.normal(0, 1e-6, cartesian_points.shape) kde = gaussian_kde(cartesian_points.T, bw_method='scott') # 改用自适应带宽 # 计算网格点密度 x = np.linspace(0, 1, 200) y = np.linspace(0, np.sqrt(3)/2, 200) X, Y = np.meshgrid(x, y) grid_points = np.vstack([X.ravel(), Y.ravel()]).T triangle_vertices = np.array([[0,0],[1,0],[0.5, np.sqrt(3)/2]]) mask = is_inside_triangle(grid_points, triangle_vertices) Z = np.full(grid_points.shape[0], np.nan) Z[mask] = kde(grid_points[mask].T) Z = Z.reshape(X.shape) all_kde_values.append(Z) ternary_data.append((cartesian_points, X, Y, Z)) # 获取全局最大密度,用于统一颜色刻度 global_max = np.nanmax([np.nanmax(z) for z in all_kde_values if z is not None]) # 绘图函数修改为使用全局颜色刻度 def plot_ternary(ax, data_columns, normative_filter, cartesian_points, X, Y, Z, global_max): # 散点图绘制 ax.scatter(cartesian_points[:, 0], cartesian_points[:, 1], s=20, alpha=0.5, facecolors='none', edgecolors='black', linewidth=0.6, zorder=1) # 使用全局统一的颜色范围绘制KDE contour = ax.contourf(X, Y, Z, levels=12, cmap='viridis', alpha=0.85, zorder=2, vmin=0, vmax=global_max) # 绘制三角形边界、调整轴样式等逻辑不变... ax.add_patch(Polygon(np.array([[0,0],[1,0],[0.5, np.sqrt(3)/2]]), closed=True, fill=False, edgecolor='black', linewidth=2.5, linestyle='dashed', zorder=3)) ax.set_xticks([]) ax.set_yticks([]) ax.set_frame_on(False) ax.set_aspect('equal') cbar = plt.colorbar(contour, ax=ax, orientation='horizontal', fraction=0.05, pad=0.08) cbar.set_label('Density', fontsize=12) # 遍历绘图 for ax, (ternary_set, norm_filter), data in zip(axes, ternary_combinations, ternary_data): if data is None: continue cartesian_points, X, Y, Z = data plot_ternary(ax, ternary_set, norm_filter, cartesian_points, X, Y, Z, global_max)
内容的提问来源于stack exchange,提问作者Shubhadeep Roy
相关产品推荐
相关产品推荐

