如果你认真想在Python里做统计分析SciPy的stats模块迟早会出现在你的代码里。尤其是处理“随机变量服从什么分布”“如何求累积概率”“样本峰度是高是低”这类问题时光用pandas或numpy往往差一点意思——它们给你工具但不给你完整的概率论逻辑。而scipy.stats把从概率密度、累积分布、期望方差到最大最小值、均值、偏度、峰度再到核密度估计这些统计操作都收进了同一套API。这套方法不仅能算指标更能让你在“样本”和“分布”之间自由切换。我最初入门时也绕了不少弯总习惯用numpy算一组数据的均值和标准差再把结果硬套到正态分布上。后来才意识到真正专业的做法是先明确随机变量、分布对象、概率密度函数、累积分布函数这些基础概念再借助SciPy把它们串起来。这篇文章就基于我实际使用SciPy的经验把scipy.stats在统计操作里的核心用法过一遍随机变量、概率密度、累积分布、期望、方差、描述性统计、核密度估计都会讲到代码可以直接复用。无论你是刚学Python统计的小白还是写过不少数据分析代码但没认真碰过stats模块的工程师相信都会有所收获。1. SciPy统计操作的核心入口先从随机变量说起1.1scipy.stats里藏着哪些统计能力打开SciPy的文档目录最常被用到的是scipy.stats这个子模块。它收录的内容大致可以分为三类概率分布、统计检验、描述性统计工具。概率分布又分成连续型和离散型像正态分布、均匀分布、Gamma分布、Beta分布属于连续型泊松分布、二项分布、超几何分布属于离散型。每种分布都能完成随机数生成、概率密度/质量函数求值、累积分布函数求值、百分点函数求值、矩统计等操作。这三类能力正好对应标题里提到的那些任务。比如你要生成一组服从正态分布的模拟数据可以直接用norm.rvs(size1000)你想知道某个值在正态分布下左侧累积概率是多少可以用norm.cdf(x)你想算一组样本的偏度和峰度可以用scipy.stats.describe或kurtosis、skew函数你想画一条平滑的密度曲线而不是直方图就会用到gaussian_kde。stats模块的熟练程度某种程度上决定了你工作流里“概率”和“数据”之间能不能无缝对接。我的建议是不要零散地记函数而是把所有分布都看成具有同一套接口的“随机变量对象”这样从正态分布换成Beta分布只需要换一个名字后面所有方法都通用。1.2 连续分布与离散分布千万不要混用PDF和PMF随机变量是统计学里最根上的概念。在SciPy中连续随机变量对应rv_continuous离散随机变量对应rv_discrete。两者的核心区别在于连续变量取值是实数描述概率分布用的是概率密度函数PDFPDF某一点的值不是概率需要通过积分才能得到区间概率离散变量取值是可数集合用的是概率质量函数PMF某一点的取值就是概率本身。拿代码举例from scipy import stats # 连续型标准正态分布 norm_rv stats.norm(loc0, scale1) x 0.5 print(norm_rv.pdf(x)) # 概率密度 print(norm_rv.cdf(x)) # P(X 0.5) # 离散型泊松分布lambda 3 poisson_rv stats.poisson(mu3) k 2 print(poisson_rv.pmf(k)) # P(X 2)取整数点 print(poisson_rv.cdf(k)) # P(X 2)如果你对离散分布调用pdfSciPy通常会直接报错或者给出不对的数据类型结果。这是新手最容易踩的坑尤其在判断一个数据生成过程是否适合用泊松分布时脑子里一定要先分清连续还是离散。实际操作中我会先把问题翻译成“随机变量X是什么类型”再去选API。另一个容易被忽略的点是“冻结分布”。通常我们写stats.norm得到的是分布“类工厂”需要传入loc和scale参数才能产生一个可用的连续随机变量。如果你不想每次都传参数可以用frozen stats.norm(loc2, scale3)之后frozen.cdf(x)和frozen.rvs(100)直接用就行。这种写法在写统计模拟代码时能减少大量重复参数也让程序更清晰。2. 概率密度、累积分布与分位点一个分布的关键钥匙2.1 概率密度函数PDF的几何意义与计算方法连续型随机变量在某一点的概率通常没有意义有意义的是它落在某个小区间的概率这个概率等于PDF在该区间上的积分。所以PDF是一个密度函数曲线越高说明数据落在这个位置附近的“单位长度概率”越大。用生活类比就像人口密度某个城市某块地的“人口密度”大并不意味着某个人正好站在那里而是说随机站一个人他大概率站在那块地附近。用SciPy计算PDF很简单比如标准正态分布import numpy as np from scipy import stats import matplotlib.pyplot as plt x np.linspace(-4, 4, 500) y stats.norm.pdf(x, loc0, scale1) plt.plot(x, y) plt.title(Standard Normal PDF) plt.show()实战里PDF最大的用途之一是对比理论分布与样本分布。如果你有一组样本想判断它是否近似正态可以先做直方图再叠加一个基于样本均值和标准差构造的正态PDF曲线。如果曲线和直方图轮廓不贴近就说明直接用正态假设可能有问题。不过要记住PDF值本身并不能直接和直方图的纵轴比较因为直方图纵轴是“频数”或“频率/组距”需要做归一化处理。常见做法是densityTrue让直方图面积归一化为1再和PDF叠加。2.2 累积分布函数CDF和百分点函数PPF概率与分位数的桥梁累积分布函数描述的是P(X x)。CDF单调不减值域在[0, 1]它比PDF更容易在业务里落地。比如考试百分位你想知道低于某位考生分数的比例用CDF就能直接算出来。而PPF是CDF的反函数输入一个概率q返回满足P(X x) q的x值。在金融风控、质量监控里这就是典型的“分位点”查询。继续用正态分布举例rv stats.norm(loc10, scale2) # 求P(X 12) p_under_12 rv.cdf(12) # 求0.95分位点 q95 rv.ppf(0.95) # 验证再用CDF反查一下 print(rv.cdf(q95))这段代码的输出会让你直观感受到CDF和PPF互为反演关系。实际项目中ppf常用于计算置信区间。比如95%置信区间你可以用rv.ppf(0.025)和rv.ppf(0.975)得到上下界。这里有个细节正态分布是对称的上下界关于均值对称但对偏态分布你不能直接用均值加减1.96倍标准差而应该老老实实用ppf从两端算这正是SciPy比手写公式更可靠的地方。2.3 期望、方差、偏度、峰度用stats方法一次算全统计理论里随机变量的“数字特征”通常指期望一阶原点矩、方差二阶中心矩、偏度三阶标准化矩、峰度四阶标准化矩。在SciPy中大多数连续分布对象都提供了stats(momentsmvsk)方法一次性返回均值、方差、偏度、峰度。rv stats.gamma(a2, loc0, scale3) mean, var, skew, kurt rv.stats(momentsmvsk) print(fmean{mean:.4f}) print(fvar{var:.4f}) print(fskew{skew:.4f}) print(fkurt{kurt:.4f})为什么这些矩很重要如果只给你期望和方差你只能知道数据中心在哪、离散程度如何偏度告诉你分布左右不对称的方向和程度正偏意味着右尾更长峰度告诉你尾部有多厚以及中心峰值是否尖峭。许多金融数据建模都会重点观察峰度因为厚尾分布意味着极端事件发生概率比正态假设下更大。值得注意SciPy里默认计算的峰度是“超额峰度”也就是正态分布的峰度被归一为0。有些教材或工具比如Excel使用“原始峰度”正态为3。如果你在两个工具之间切换一定要先确认口径否则结果差异会让你一头雾水。3. 描述性统计一组样本数据该怎么全面地“体检”3.1 最值、均值、方差、标准差描述统计的基础套餐描述性统计的目标是用少量指标概括样本的分布情况。最大最小值决定了数据范围均值代表集中趋势方差和标准差代表离散程度。单独看均值很容易被极端值带偏这时候中位数比均值更稳健同样地只追求低方差也可能掩盖分布的多峰特征。所以实际做数据分析时我一般先把最值、均值、标准差、偏度、峰度列一张表再从业务角度解释这些数字意味着什么。用numpy可以快速算基础指标import numpy as np data np.array([1.2, 2.3, 0.8, 3.1, 2.9, 3.5, 4.0, 5.2]) print(min:, np.min(data)) print(max:, np.max(data)) print(mean:, np.mean(data)) print(std (sample):, np.std(data, ddof1)) print(variance (sample):, np.var(data, ddof1))这里有个特别需要注意的点np.std和np.var默认ddof0计算的是总体标准差/方差。如果你手头数据是样本而非总体一般需要设置ddof1得到无偏估计。很多人在描述性统计里忽略了这个细节最后和scipy.stats.describe的结果对不上其实原因就在这里。3.2 偏度和峰度数据的“脸型”和“尾巴”标题里提到的“偏差”在中文统计教材里经常和偏度Skewness混用。偏度度量分布不对称性偏度约等于0说明分布近似对称。偏度大于0正偏/右偏说明右尾更长较多数据集中在左侧。偏度小于0左偏则左尾更长右边集中。峰度度量分布尾部厚度尤其是相对于正态分布的离群值倾向。超额峰度大于0说明比正态分布有更厚的尾巴小于0则尾部更薄、更向均值集中。SciPy提供独立的skew和kurtosis函数可以针对样本直接计算from scipy.stats import skew, kurtosis data np.random.default_rng(42).normal(loc10, scale2, size500) print(skew:, skew(data)) print(kurtosis (excess):, kurtosis(data, fisherTrue))这里fisherTrue表示返回超额峰度正态为0如果fisherFalse则返回原始峰度正态为3。我建议做汇报时先讲清楚“我这里用的是超额峰度”避免听众照着你给的图和指标做进一步计算时出现偏差。3.3 用scipy.stats.describe一次拿到核心描述指标不想一个指标一个指标地调函数可以直接用describe它会把样本量、最小最大值、均值、方差、偏度、峰度打包返回。from scipy import stats data np.array([1.2, 2.3, 0.8, 3.1, 2.9, 3.5, 4.0, 5.2]) res stats.describe(data, axis0, ddof1) print(res.nobs) print(res.minmax) print(res.mean) print(res.variance) print(res.skewness) print(res.kurtosis)在不同版本的SciPy里返回值有差异老版本返回一个元组新版本返回一个DescribeResult对象支持属性访问也支持按索引取。如果你在写通用代码建议使用角落里的新版本API同时做一次版本检查否则别人用老环境跑你的代码可能直接拆包失败。另外describe默认不允许NaN存在一旦数据里有缺失值会直接报错。此时可以用nan_policyomit参数res stats.describe(data, nan_policyomit)这个参数在stats模块的很多函数里都有和np.nanmean这类函数的中途处理方式不一样它是统一的策略值得养成习惯。4. 核密度估计让数据分布自己“说话”4.1 为什么直方图不够用KDE到底解决什么问题直方图是探索性数据分析里最常见的一步但它有两个软肋一是条带宽度binwidth选择很主观改一个数值图像形状就变化很大二是得到的阶梯形曲线不够平滑无法还原连续分布形态。核密度估计Kernel Density EstimationKDE则是一种非参数的概率密度估计方式它不假设数据来自某个已知分布而是通过在每个样本点上叠加一个“核函数”来估计整体密度。用生活化类比KDE有点像在地图上用光晕表示人群密度每个观测点都向外扩散一点亮度点密集的地方自然更亮最后形成一个连续的光照地图而不是用正方形格子统计人头。SciPy的stats.gaussian_kde提供的就是基于高斯核的KDE实现。4.2gaussian_kde使用与密度曲线绘制用gaussian_kde估计一组数据的概率密度非常简单import numpy as np import matplotlib.pyplot as plt from scipy import stats rng np.random.default_rng(2024) data np.concatenate([ rng.normal(loc0, scale1, size300), rng.normal(loc4, scale1.5, size200) ]) kde stats.gaussian_kde(data) x np.linspace(-5, 9, 500) density kde(x) plt.hist(data, bins30, densityTrue, alpha0.3) plt.plot(x, density, labelKDE) plt.legend() plt.show()这里生成的数据是两个正态分布混合在一起形成双峰结构。直方图虽然也能看出两个峰但KDE曲线更平滑也更容易做后续数值计算比如计算某个区间内的概率密度积分。gaussian_kde还有一个很方便的能力kde.integrate_box_1d(a, b)可以直接计算估计分布在一维区间[a, b]上的积分也就是概率估计。例如想知道这个混合分布落在[0, 3]之间的比例可以算kde.integrate_box_1d(0, 3)。4.3 带宽参数KDE的“显微镜焦距”KDE最重要的参数是带宽bandwidth它控制核函数的平滑程度。带宽太小曲线会跟着每个样本点剧烈波动看起来毛刺很多带宽太大又会把双峰或者多峰细节抹平看起来像一个宽胖的单一峰。gaussian_kde默认使用Scott规则自动选择带宽这对单峰近似正态的数据通常表现不错但对多峰或样本量较少的数据自动值不一定是最佳选项。你可以通过bw_method参数指定带宽计算方法或固定值kde_smooth stats.gaussian_kde(data, bw_method0.5) kde_sharp stats.gaussian_kde(data, bw_method0.05)实际使用中我一般先画出默认KDE再手工调一两个带宽值观察曲线的平滑度。带宽是一个相对值并没有绝对的“正确”关键是看你要用KDE做什么如果只是可视化可以选让曲线好看又不过度失真的值如果要做概率密度估计用于后续模拟则建议通过交叉验证方式选带宽不要凭感觉。5. 实战完整样本分析流程5.1 构造一组“不太规矩”的实验数据为了把前面所有知识点串起来我们构造一个偏态明显的混合样本。混合分布在实际业务里非常常见比如不同群体的收入合并在一起往往不是单峰。import numpy as np from scipy import stats rng np.random.default_rng(123) # 两个不同均值、方差的组合成一个偏态样本 data np.concatenate([ rng.normal(loc8, scale1.5, size600), rng.normal(loc15, scale3.0, size400) ])这里第一组人数更多均值8第二组人数少但均值15合并后整体会呈现右偏、双峰或“长尾”的形态。5.2 描述性统计、KDE与正态拟合对比先看常规描述性统计res stats.describe(data, ddof1) print(样本量:, res.nobs) print(最小/最大:, res.minmax) print(均值:, res.mean) print(方差:, res.variance) print(偏度:, res.skewness) print(峰度:, res.kurtosis)再用正态分布拟合这组数据看它有多“勉强”mu, sigma stats.norm.fit(data) print(f拟合正态: loc{mu:.3f}, scale{sigma:.3f})拟合正态得到的位置参数接近数据混合后的总体均值标准差则被两组数据拉大。如果只看均值和方差你会觉得数据还算常规但只要画出KDE和拟合正态曲线对比差距立刻暴露出来KDE曲线有两个峰而正态曲线只有一个单峰右侧尾巴明显说不好。这说明任何仅用均值和方差概括分布的做法在复杂数据面前都有风险。5.3 用CDF与KDE做分位数推断对业务分析而言分位数往往比均值更重要。比如“90%的人落在多少以下”。用样本直接计算pct_90 np.percentile(data, 90) print(样本第90百分位:, pct_90)如果用拟合出的正态分布来推断norm_pct_90 stats.norm(locmu, scalesigma).ppf(0.90) print(正态拟合第90百分位:, norm_pct_90)再用KDE积分来找第90百分位则需要用数值方法求x使得kde.integrate_box_1d(-np.inf, x) 0.9from scipy.optimize import brentq def kde_cdf_diff(x, kde, q): return kde.integrate_box_1d(-np.inf, x) - q kde stats.gaussian_kde(data) kde_pct_90 brentq(kde_cdf_diff, data.min(), data.max(), args(kde, 0.90)) print(KDE第90百分位:, kde_pct_90)这个例子提醒我们如果数据分布复杂正态拟合的分位数可能有偏而KDE作为非参数方法能更贴近样本本身的表现。当然KDE也有样本量依赖和边界效应但当你想放弃参数假设时它是个非常灵活的工具。6. 踩过的坑关于SciPy统计操作的几点经验6.1 分清pdf、pmf、cdf的适用范围这是我和身边朋友踩得最多、也最容易忽视的坑。对连续分布pdf没问题对离散分布必须改成pmf。同时ppf对离散分布也有细微问题它返回的是使CDF(x) q的最小整数不同实现的定义可能不同使用时最好先做一次小规模抽样比对。尤其在进行业务分位数计算时要明确自己需要的是“不低于某个概率的最小值”还是“不超过某个概率的最大值”否则可能差出整数值。6.2 参数顺序和形状参数不要凭直觉乱传SciPy里每个分布都有自己特定的参数签名。比如stats.norm.fit(data)返回的两个值是(loc, scale)分别对应均值和标准差但stats.gamma.fit(data)返回的(a, loc, scale)里第一个是形状参数。如果你用stats.norm.fit的习惯去理解其它分布很容易把形状参数当成均值。每次使用不熟悉的分布前建议打印一下dist.shapes或者直接查看官方文档里的参数定义。6.3 老版本和新版本API差异scipy.stats.describe在新版本返回DescribeResult对象这是好事但如果你依赖老代码里result[2]这种方式获取均值在新版本里可能拿到的是AttributeError。同理很多分布对象的方法返回类型也可能差别很大。建议在你的环境里固定SciPy版本或者在代码里写一个小的兼容层避免项目上线时行为突然变化。6.4 KDE边界效应和归一化问题KDE在数据边界处会出现“概率泄漏”现象即密度曲线在数据范围外侧仍然有值尤其当带宽较大时。比如一个取值范围在0到100之间的样本估计出的密度在-20处可能依然有微小的非零值。如果你用integrate_box_1d计算概率务必把积分区间限制在合理范围内或者对KDE结果做截断处理。同时gaussian_kde在输入高维数据时会对协方差矩阵求逆如果变量数量较多而你用默认带宽可能遇到矩阵奇异问题这时要调用bw_methodscott之外的自定义方法或考虑降维后再做估计。6.5 随机数种子让结果可复现统计模拟最怕结果“每次跑都不太一样”。scipy.stats的分布对象底层依赖numpy.random如果你不设置种子哪怕代码逻辑完全一样输出也可能不同。建议在项目开头就用np.random.seed(123)或者rng np.random.default_rng(123)然后把rng传给rvs(random_staterng)这样可以保证实验过程可复现别人也能在相同种子下核对你的结果。我自己在多次实践后最大的感受是SciPy的统计接口并不难记住难的是把它背后的统计概念和业务场景对齐。每当你准备调用某个函数先问自己一句“我现在面对的是离散随机变量还是连续随机变量我拿到的是原始样本还是概率分布我需要的输出是密度、概率还是分位点”这三个问题想清楚代码本身几乎就是抄文档的事。希望这篇经验总结能帮你少走一些自己当年绕过的弯路。