双峰高斯分布蒙特卡洛模拟:PDF与CDF绘图实战
发布时间:2026/9/26 4:52:35 作者:尧图编辑部 阅读量:1,286

做数据分析的人迟早会碰到这种场景观测数据明显出现两个峰却没法用单个常规高斯分布去解释。这阵子我正好在跑双峰高斯分布的蒙特卡洛模拟项目顺手把PDF和CDF的绘图流程完整捋了一遍。先说清楚这里说的PDF不是文档格式而是概率密度函数Probability Density FunctionCDF则是累积分布函数Cumulative Distribution Function。这个项目核心就三件事从已知的双峰高斯分布中用蒙特卡洛方法抽样把样本的概率密度函数画出来再把累积分布函数画出来最后和理论曲线做对比。看起来简单真动手时会遇到不少细节坑值得单独写一篇总结。1. 项目概述与整体设计思路1.1 双峰高斯分布到底是什么双峰高斯分布简单说就是两个高斯分布混合叠加后的结果。生活中这类现象很常见某地区成年人的身高分布如果按性别拆分男性和女性各自近似一个高斯分布合并之后就会呈现两个峰工厂生产一批零件如果两条产线精度不同尺寸分布也会出现双峰。数学表达式是p(x) w * N(x; μ1, σ1²) (1 - w) * N(x; μ2, σ2²)其中N(x; μ, σ²)表示均值为μ、标准差为σ的高斯分布密度函数w是第一个高斯分布的混合权重取值范围0到1。所以一个双峰高斯分布一共需要5个参数μ1、σ1、μ2、σ2、w。权重决定两个峰谁高谁低均值决定峰的位置标准差决定峰的形状和宽度。当两个均值距离足够远且标准差相对较小时概率密度曲线会出现明显的“双峰”距离太近或者标准差太大两个峰就会叠合成一个宽峰看起来像普通的单峰分布。这个特性对模拟很有价值它能模拟现实生活中“混合群体”的数据结构比如两类用户行为、两种故障模式、两类产品质量。在做聚类算法评估、异常检测、信号判别时双峰分布都是很好的测试数据集来源。1.2 为什么用蒙特卡洛模拟蒙特卡洛模拟的核心思想是“用大量随机样本近似一个概率分布的性质”。既然双峰高斯分布的PDF和CDF表达式都能写出来为什么还要抽样本模拟主要有三个原因。第一验证理论推导。公式写出来是一回事真实抽样是否按这个规律分布是另一回事。通过模拟可以确认写出的混合密度函数确实能生成符合预期的数据。第二很多实际问题中分布是未知的我们手里只有一堆观测数据没有解析表达式。这时候想估计PDF和CDF就必须靠样本去“近似”而蒙特卡洛模拟正是建立这种近似能力的基本功。第三后续的机器学习任务——比如高斯混合模型聚类、EM算法参数估计、贝叶斯推断——都需要先能从已知分布生成样本。先把蒙特卡洛模拟跑顺后面做模型验证就有底了。蒙特卡洛的误差收敛速度是O(1/√N)意思是样本量N每增加10倍统计误差差不多缩小到原来的约1/3。明白这个收敛规律之后就能科学地选择样本量而不是盲目贪多或者过少。1.3 同时绘制PDF和CDF的价值很多人只画PDF忽略CDF这是个不小的误区。PDF的优点是直观直方图一出来就能看到峰在哪、重叠区有多宽缺点是受带宽参数影响大样本量小的时候曲线锯齿严重。CDF描述的是P(X ≤ x)也就是小于等于某个值的样本占比。它是单调不减的比PDF“稳”得多不需要调带宽也不容易受离散噪声干扰。看CDF能快速判断你的模拟数据在某个区间内累积了多少比例还能用来求分位数。比如在项目里我常需要回答“哪些样本落在中间90%区间内”这种问题用CDF反推比用PDF直接看要靠谱得多。所以标准做法是把PDF和CDF放在一起看PDF看形态和峰值CDF看累积和稳健性。两者互相印证一旦模拟有偏差至少有一个图能暴露问题。2. 蒙特卡洛模拟的原理与参数设计2.1 抽样原理从混合分布中生成样本从双峰高斯分布抽样标准方法是“分量抽样法”也叫“分解法”。思路分两步先按权重w随机决定这个样本来自第一个分量还是第二个分量然后从选中的那个高斯分布中抽取一个随机数。用程序语言描述就是生成一个0到1之间的均匀随机数u如果u w就从N(μ1, σ1²)里抽样否则从N(μ2, σ2²)里抽样。重复N次就得到N个样本。这个方法的正确性在于密度函数的线性结构混合密度是分量密度的加权和那么样本的无条件分布自然也是这个加权和。这一步看起来简单却是整个项目的基石——后面所有统计量、图形都是在这批样本基础上计算的。向量化实现时很多人会写一个循环慢慢抽样其实用Numpy可以先一次性生成N个均匀随机数再根据阈值分成两组样本速度能快几十倍。2.2 参数选择与理论指标计算做模拟不能随手乱选参数得选一组能清晰体现“双峰”特征、又方便手工验证的参数。我选的是μ1 -2σ1 1.0对应左峰μ2 3σ2 1.5对应右峰w 0.4即第一个高斯分布的权重是40%第二个是60%为什么选这组首先两个峰相距5个单位容易看出双峰形态。其次左右两个峰的峰高非常接近计算一下左峰分量在μ1处的峰值 0.4 / (sqrt(2π) * 1) ≈ 0.1596 右峰分量在μ2处的峰值 0.6 / (sqrt(2π) * 1.5) ≈ 0.1596两个峰几乎一样高图形看起来对称均衡更适合观察绘图的细节变化。如果权重偏差太大弱的那个峰容易被直方图淹没达不到演示效果。混合分布的总体均值有解析解E[X] w * μ1 (1 - w) * μ2 0.4 * (-2) 0.6 * 3 1.0总体方差会更复杂一些需要同时考虑组内方差和组间差异Var[X] w * σ1² (1 - w) * σ2² w * (1 - w) * (μ1 - μ2)² 0.4 * 1 0.6 * 2.25 0.4 * 0.6 * 25 0.4 1.35 6 7.75所以总体标准差约等于2.784。模拟结束后可以计算样本均值和样本标准差和理论值对比用来检验抽样程序是否写对了。这一步是很多新手忽略的“验收环节”其实特别重要。2.3 样本量怎么定才够用样本量直接决定图形的平滑度和统计量的稳定度。1000个样本只能看出大概轮廓10000个样本曲线基本成型100000个样本才能让直方图和理论曲线的偏差小到肉眼难辨。根据收敛误差O(1/√N)如果希望标准误缩小一半样本量要扩大4倍缩小到原来的十分之一样本量要扩大100倍。实际项目里我一般先用N10000快速验证代码逻辑确认没问题后跑N100000作为最终结果。这个两阶段策略能省很多时间因为蒙特卡洛代码一旦写在绘图之前改参数就得重跑先用小样本调通画图逻辑再放大样本量拿最终图是最高效的路径。3. 实操过程Python实现与PDF/CDF绘图3.1 环境准备与依赖选择整个模拟只需要三个库Numpy负责随机抽样和数值计算Scipy负责提供高斯分布的理论PDF/CDF函数以及核密度估计Matplotlib负责绘图。如果只装一个Anaconda这些库基本都有。导入代码很简单import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm, gaussian_kde这里有一个容易踩的坑SciPy的norm.pdf和norm.cdf可以直接算单个高斯分布的理论密度和累积概率但双峰分布不能直接调用需要自己写加权求和。很多人忘了这一步直接画出来只有单峰折腾半天还以为样本生成错了。3.2 生成双峰高斯样本的两种写法第一种写法是“逐分量判断”逻辑清晰适合理解原理mu1, sigma1 -2.0, 1.0 mu2, sigma2 3.0, 1.5 w 0.4 N 100000 rng np.random.default_rng(42) u rng.random(N) component1 u w n1 np.sum(component1) samples np.empty(N) samples[component1] rng.normal(mu1, sigma1, n1) samples[~component1] rng.normal(mu2, sigma2, N - n1)注意我用了np.random.default_rng(42)而不是老的np.random.seed。在Numpy 1.17之后推荐使用Generator API它生成的随机序列质量更好而且状态管理更灵活。固定随机种子是为了让结果可复现这在写博客、做汇报、调Bug时都非常关键。第二种写法更紧凑适合熟悉向量化操作的读者choice rng.choice(2, sizeN, p[w, 1 - w]) samples np.where(choice 0, rng.normal(mu1, sigma1, N), rng.normal(mu2, sigma2, N))两种写法生成的样本分布性质完全一样区别在于第二段代码会多算一部分被浪费的随机数两个分量的样本数量是预先固定为N个但实际只用了约w*N个效率略低但代码更短。我项目里用的是第一种因为样本量大时能省一点内存和计算时间。3.3 PDF绘图直方图、KDE与理论曲线的叠加生成样本后第一步画直方图。直接用Matplotlibx np.linspace(-7, 8, 500) pdf_theory w * norm.pdf(x, mu1, sigma1) (1 - w) * norm.pdf(x, mu2, sigma2) plt.hist(samples, bins80, densityTrue, alpha0.5, label直方图) plt.plot(x, pdf_theory, r-, linewidth2, label理论PDF) plt.xlabel(x) plt.ylabel(概率密度) plt.legend() plt.title(双峰高斯分布PDF直方图与理论曲线) plt.show()有几个细节必须说清楚。densityTrue参数把直方图归一化成概率密度这样直方图的纵轴和理论PDF的纵轴是同一个量纲可以重叠比较如果不加直方图纵轴是频数曲线会被压得看不见。bins的个数也很关键bins80是我在这个数据范围下试出来的太密锯齿严重太疏看不到双峰形状。直方图默认边界是左闭右开画出来的柱子和曲线会有微小偏移耐心调整bins和alpha透明度能提升美观度。除了直方图强烈建议叠加一条核密度估计曲线。核密度估计不依赖固定bins而是用核函数对每个样本点附近做平滑叠加得到的是平滑的密度估计kde gaussian_kde(samples, bw_method0.3) plt.plot(x, kde(x), g--, linewidth2, labelKDE)bw_method是带宽系数相当于平滑窗口宽度。默认值0.3在双峰场景下效果还行调成0.5以上可能把两个峰抹成一个调到0.1则会像锯齿一样波动。经验是样本量越大带宽可以适当调小想要突出双峰就别选太大带宽。KDE的作用是给直方图一个平滑的插值近似同时它也是很多真实场景中“只有数据、没有解析分布”时的标准估计工具练一遍很有价值。3.4 CDF绘图经验累积曲线与理论曲线的叠加CDF比PDF好画但很多人不知道经验CDF怎么算。经验CDF的定义是对排序后的样本x1 ≤ x2 ≤ ... ≤ xNeCDF(x) (i) / N其中i是样本中小于等于x的样本数。用Numpy实现极其简单sorted_samples np.sort(samples) ecdf_y np.arange(1, N 1) / N cdf_theory w * norm.cdf(x, mu1, sigma1) (1 - w) * norm.cdf(x, mu2, sigma2) plt.step(sorted_samples, ecdf_y, wherepost, label经验CDF) plt.plot(x, cdf_theory, r-, linewidth2, label理论CDF) plt.xlabel(x) plt.ylabel(累积概率) plt.legend() plt.title(双峰高斯分布CDF经验曲线与理论曲线) plt.show()注意经验CDF要用plt.step来画而不是plt.plot。因为经验CDF本身是阶梯函数在每一个样本点处跳变用点线图会让人误解为连续增长。wherepost表示跳跃发生在每个数据点之后这个参数选错看起来会整体左移一格。CDF曲线在均值附近有较陡的爬升两段陡坡对应两个峰的位置看到CDF在双峰位置出现两段明显增速就说明模拟数据的分布形态对了。理论上只要样本量足够大经验CDF会收敛到理论CDF。这个收敛速度比PDF的直方图快得多所以CDF经常用于更精细的验证比如用K-S检验做量化对比。3.5 组合图的版面设计与样式参数项目做到这里只输出两张图会显得比较单薄。建议把PDF和CDF放到同一张大图里甚至加上左侧直方图、右侧CDF的双子图布局或者用2×2网格放四张图直方图、KDE曲线、理论PDF曲线、经验CDF曲线。我个人常用2×2网格排版紧凑又信息量大。排版时控制好figsize、dpi、网格线、图例位置和坐标范围。我项目里的样式参数大致如下样式项目推荐设置理由figsize(12, 8)尺寸够大子图不拥挤dpi150屏幕展示和存档都清晰gridalpha0.3, linestyle--辅助线不抢主体曲线legendlocbest自动避开数据主体x轴范围[-7, 8]覆盖理论曲线的有效范围y轴范围自动或略高于峰值防止曲线被截断还有一个常见需求是图中显示中文字体。Matplotlib默认字体不支持中文图和标题里的中文会显示成方块。解决办法是设置字体plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第二行必须加不然坐标轴上的负号会变成方块。这算是个很小的坑但在科研绘图和项目汇报里经常让人当众翻车。4. 常见问题与排查技巧实录4.1 直方图不光滑图像像锯齿直方图出现细碎锯齿第一反应是bins数量不合适。bins太密每个柱子里的样本太少统计波动大bins太疏双峰细节丢了。常用的经验规则是bins sqrt(N)但样本量到10万时sqrt(N)约等于316过于细碎。我更习惯把bins控制在60到100之间然后观察曲线形态微调。如果bins调到80还是锯齿明显检查样本量是否足够小样本量下直方图本来就是噪声。另一个办法就是上KDEKDE能提供平滑的估计和直方图互补。一句话直方图看原始数据形态KDE看平滑趋势两者都不该缺。4.2 模拟曲线和理论曲线对不上这是最让人头疼的问题明明生成了10万样本为什么直方图理论和曲线差了老远按我的排查顺序十次有九次是以下几个原因。第一权重方向搞反。公式里写了p(x) w * N(x; μ1, σ1²) (1-w) * N(x; μ2, σ2²)但代码里判断条件写成了u (1-w)或者把μ1和μ2赋值反了曲线整个翻转。解决方法是先打印样本均值和样本标准差和理论均值1.0、标准差2.784对比如果有明显偏移说明抽样逻辑有问题。第二density参数没设置。直方图纵轴是频数理论曲线纵轴是密度两者量纲不同对比当然对不上。加上densityTrue之后直方图总面积归一化成1才能和PDF重叠。第三随机种子没固定。每次运行图都长得不一样没法判断是否是代码错误。固定随机种子重复性才有保证。我建议无论调试还是正式跑数一律固定种子。第四样本量太小。1万以下样本理论曲线和样本曲线的偏差肉眼可见这是正常的统计波动不是代码问题。加大样本量到10万偏差就能缩到可以接受的范围。4.3 经验CDF看起来不自然经验CDF是阶梯函数这在样本量小时尤其明显属于正常现象。如果画出来的阶梯线像“横着的梳子”要么是样本量确实太小要么是用了plt.plot画点线而不是plt.step。建议把plt.step的where参数设为post图形就顺眼了。还有一种情况经验CDF在两侧出现水平长尾看起来幅度很大。这也是正常的因为样本最大值和最小值之外经验CDF会自然变成0或1。想图形更聚焦可以把x轴范围限制在[-7, 8]或者用百分位范围截取中间99%的数据。4.4 绘图细节翻车中文字体、坐标范围与输出精度绘图最容易被忽视的是输出精度。直接plt.show()看没问题保存图片时默认dpi只有100放大后文字和线条发虚。保存时显式指定高dpiplt.savefig(bimodal_gaussian_pdf_cdf.png, dpi300, bbox_inchestight)bbox_inchestight能自动裁剪多余白边适合插入网页或论文。还有一个坐标范围问题如果x轴范围设置得过大双峰会被压缩到中间一小团设置得过小曲线两边被截断CDF看起来像从0.2起步。我的做法是先用样本的2.5%到97.5%分位数估算范围再向两边各扩展1到2个单位。用分位数定范围的好处是极端离群值不会把坐标轴撑爆。4.5 问题排查速查表现象可能原因排查方法直方图锯齿明显bins太密或样本太少调小bins、加大N、叠加KDE样本均值明显偏离理论值权重方向写反或参数赋值错误打印样本均值/方差与理论值对比直方图纵轴非常大densityTrue未设置加归一化参数再画每次运行图形不同随机种子未固定用default_rng(固定值)图中中文变方块缺少中文字体配置设置rcParams字体经验CDF呈难看的锯齿用错了绘图类型改用plt.step(wherepost)保存图片发虚dpi过低savefig指定dpi300双峰被压成一条线x轴范围过大用分位数裁剪坐标范围5. 扩展应用与个人实操体会5.1 双峰高斯蒙特卡洛模拟还能怎么用这个项目做完之后最直接的应用就是给高斯混合模型做验证数据。你可以先用双峰高斯分布生成一批带标签的样本再丢给scikit-learn的GaussianMixture做聚类然后对比聚类结果与真实分簇的吻合程度。这样相当于在完全已知答案的数据集上测试算法比一上来就用未知数据靠谱得多。在工业场景里双峰分布常被用来模拟两类设备状态。比如传感器在正常模式和故障模式下读数分别服从两个高斯分布混合后生成的数据就能用来评估异常检测阈值。金融分析中某些资产的收益率分布会呈现“尖峰厚尾”现象双峰高斯混合也是常见的近似建模工具。我自己做信号质量评估时也用类似的蒙特卡洛流程验证过判决门限思路完全一致先构造双峰分布再模拟采样最后画PDF和CDF对比阈值误判率。更进一步这套“生成样本→估计PDF→绘制CDF→对比理论”的流程可以推广到任意混合分布。把标准差改大改小、把高斯换成t分布代码主干完全不用动只改密度函数那一行就能跑。这也是做模拟类项目最大的优势先搭好标准流程后续换分布只是替换参数和函数。5.2 我的实际调试习惯和工具建议复盘这个项目我最大的体会是模拟项目最忌讳“一把梭”。先把代码分块测试每块都有明确的验收标准。生成样本后第一件事是算样本均值和标准差和理论值对上再画图画图时先画PDF确认双峰出现后再画CDF最后再做组合排版。任何一步没验完就急着往下画后期排查问题就像大海捞针。另一个强烈的建议是全程固定随机种子。我在做参数敏感性分析时需要对比不同权重w对双峰形态的影响如果没有固定种子每次运行样本不同影响因素会混杂在一起根本没法判断是参数变了还是随机波动在干扰。固定种子之后就变成单变量对照实验结论可靠得多。最后分享一个小技巧如果觉得论文级的科研绘图要求很高可以在Matplotlib的基础上直接套用SciencePlots库一行plt.style.use(science)就能排出期刊风格坐标轴。但要注意这个库默认字体分辨率对中文支持一般用之前先确认好中文字体配置。对于像我这样既要快速验证、又要产出高颜值交付图的场景这套流程已经成了我的标准工作流希望也能帮你省下几个小时的踩坑时间。