周一上午十点,你坐在一家中型私募的研究台。3.2.2 收尾那张 tear-sheet 昨晚跑完了,落到磁盘的中间产物里有一行 returns = (closes['510300.SH'].pct_change().dropna()).to_numpy()——一根长度 252 的 np.ndarray,是沪深300 ETF(510300.SH)在 2024 年的日简单收益。PM 把咖啡放下,砸出四个问题:(1) 这只 ETF 的 5% 一边在险价值(value at risk, VaR)是多少?(2) 拿正态分布(Gaussian distribution)当近似还过得去吗,还是必须切到学生 t 分布?(3) 偏度(skewness)和超额峰度(excess kurtosis)落在什么区间?(4) 想给隔夜压力测试造一条同分布的合成路径,怎么造?四个问题在 scipy.stats 里都是一行。一行能成立,是因为 SciPy 把每一种分布都收进了同一套对象接口;这一课讲这套接口,以及它在「拟合 + 描述性统计」上的两块直接落地。
一、分布对象的统一接口
scipy.stats 里的每一个分布——norm、t、lognorm、chi2、f——既能「不冻结」直接调函数,也能「冻结」成对象反复用。冻结的写法把参数绑在对象上,避免每次调用都重传一遍 loc、scale:
import scipy.stats as st
d = st.norm(loc=0.0, scale=1.0)
d.pdf(0.0)
d.cdf(1.96)
d.ppf(0.975)
d.sf(1.96)
四个方法是每一个连续分布都支持的:.pdf(x) 是 概率密度函数(probability density function, PDF)在 处的取值(离散分布的对应物叫 .pmf,本课不展开);.cdf(x) 是 累积分布函数(cumulative distribution function, CDF),即 ;.ppf(q) 是 百分位函数(percent point function, PPF),即 CDF 的反函数,返回使 成立的那个 ——这是后面 VaR 一行的承重件;.sf(x) 是 生存函数(survival function),在尾部远离零的位置比 1 - d.cdf(x) 在数值上更稳。再加两个会用到的:.rvs(size, random_state=...) 抽样(务必显式传一个 NumPy Generator 对象,对齐 3.2.1 L3 的可复现规则),.fit(data) 对样本做 极大似然估计(maximum likelihood estimation, MLE),返回该分布参数的点估计元组。
二、五个量化最常用的分布
把 norm 换成其它分布,上述六个方法签名整体不变——这就是「统一接口」的现金价值。五个分布在量化场景里出场最频繁:
- 正态分布(Gaussian distribution,又称高斯分布)
norm:渐近正态性论证与 t 检验零分布的默认参照;参数loc=mean, scale=std。 - 学生 t 分布(Student's t distribution)
t:正态分布的厚尾表亲;参数df是 自由度(degrees of freedom,量化文献常写作 ),可选loc、scale。 时退化成正态; 尾部已明显加厚; 时拿正态当近似就不安全。 - 对数正态分布(log-normal distribution)
lognorm:当 时 的分布,也就是 GBM 模拟出来的价格在任一固定时刻的边际分布。**SciPy 的参数化是s=sigma、scale=exp(mu)**——这是一处出名的记号陷阱,第一次写一定要回去翻 docstring。 - 卡方分布(chi-squared distribution)
chi2:方差比、似然比类检验统计量的零分布,参数df。 - F 分布(F-distribution)
f:两个独立方差估计比值的零分布,参数dfn, dfd;L3 的 F 检验脚注会引用它。
五个分布的「目录学」到此打住——本课承重在统一接口,不在分布大全。
三、把分布拟合到日收益样本
回到那根 252 长的 returns。对正态与 t 各做一次 MLE:
mu_n, sigma_n = st.norm.fit(returns)
nu_t, mu_t, sigma_t = st.t.fit(returns)
assert nu_t > 0
st.norm.fit 返回 (loc, scale);st.t.fit 返回 (df, loc, scale)——返回元组的顺序逐分布不同,写一处就对照一次 docstring 的习惯比记忆可靠。在 510300.SH 这条 252 天的样本上,落地数值大致是 、、——这里只取一组示意值用于教学,并不主张匹配任一具体年份。承重诊断一句话:如果 t 拟合得到 nu_t < 6,那么正态假设在尾部就不安全,下游计算应当用 L2 的 bootstrap 置信区间或一个 t 尾模型再交叉验证一遍。nu_t ≈ 4.5 显然过线,提示 PM 别在尾部信任正态。
.fit() 在干什么?它在数值上求解 ——这就是 MLE,是 Subject 2.2.1(estimator-properties-bias-variance-and-information)整门课的对象。本课把 .fit() 当成黑盒工具:一次方法调用拿一组点估计;MLE 的相合性、渐近正态性、Fisher 信息矩阵、Cramér-Rao 下界这些理论证明都在 2.2.1 那一节,本课不动手。
四、参数化 VaR 一行落地
拿到 ,5% 一边参数化 VaR 就是一行:
var_5 = -st.norm.ppf(0.05, loc=mu_n, scale=sigma_n)
负号是约定:VaR 习惯写成正数损失,所以把 5% 分位(一个负收益)取反。把 st.norm.ppf 换成 st.t(df=nu_t, loc=mu_t, scale=sigma_t).ppf(0.05) 再取反,就是 t 分布参数化 VaR;同等波动下 t 的左尾更胖,t-VaR 在数值上比正态 VaR 更大。VaR 还有两条做法——历史 VaR(直接 np.percentile(returns, 5),不做分布拟合)和 Monte-Carlo VaR(用 .rvs() 模拟一条长路径再取分位)——这里点名,不展开;本课只跑参数化这条。
五、描述性统计四件套
回收第三个问题。scipy.stats.describe 一次把四个中心矩拿齐:
desc = st.describe(returns)
nobs, (rmin, rmax), mean, variance, skewness, kurtosis = desc
返回的是一个 DescribeResult named tuple,按顺序解包即可。两条约定开关每次都要看清楚:在 fisher=True 下 SciPy 报的是超额峰度(kurtosis − 3,正态分布取 0);在 bias=False 下方差 / 偏度 / 峰度的估计量都做无偏修正(除以 N−1);两者都是 SciPy 的默认值,也是本课统一采用的口径。两个默认刚好都对,但生产代码里建议把这两个关键字显式写出来,避免下游同事踩坑。
经验区间口径:A 股日收益的偏度落在 [-0.3, +0.3] 大致可视为近似对称;超额峰度在 [3, 8] 是日级权益收益的典型区间(厚于正态但还不算极端);超额峰度过 10 通常意味着样本里混了少量极端跳跃——先一根一根查清楚再做后续建模。
把整批收益 z 化是同一行:zs = st.zscore(returns) 返回 。应用到 (T, N) 矩阵时要写 axis=0, ddof=1:axis 决定按时间还是按截面去中心化,ddof 决定与 describe 的 bias=False 是否口径一致。
六、肉眼校验与本课不做的事
可视化不是本课承重,但留两个指针:plt.hist(returns, bins=30, density=True) 叠一条 plt.plot(grid, st.norm.pdf(grid, loc=mu_n, scale=sigma_n)) 是日常的「眼测」;正式的图形化检查是 st.probplot(returns, dist='norm', plot=plt) 出的 QQ 图。两件事都在 matplotlib 那条线上,本课只点名,不动手。多元情形下,单变量的方差拟合换成 协方差矩阵(covariance matrix)拟合,那是 L4 把 df.cov() 喂给 scipy.linalg.eigh 的入口;本课只到一元。
显式拒绝:假设检验(ttest_1samp / kstest / shapiro / jarque_bera)、p 值、自助法置信区间是下一课 L2 的承重;回归(linregress / curve_fit)是 L3;约束优化、对称特征分解、数值积分是 L4。本课交付的是「拿到一根 returns,能在五分钟内给出一组点估计、一个 VaR 数字、一份描述性统计、外加一句尾部诊断」——拿这套点估计去跑 L2 的检验,是下一步要做的事。
练习
Exercise
给定一根长度为 252 的一维 NumPy 数组 returns,代表某资产的日简单收益:(1) 用 st.norm.fit(returns) 拟合一个正态分布、st.t.fit(returns) 拟合一个学生 t 分布;(2) 用 st.norm.ppf(0.05, loc=mu_n, scale=sigma_n) 从拟合正态算出 5% 一边参数化 VaR;(3) 用 st.describe(returns) 算出描述性统计元组并取出 excess_kurtosis;(4) 返回元组 (mu_n, sigma_n, nu_t, var_5, excess_kurtosis)。沿用 SciPy 默认的 fisher=True 与 bias=False。
提示
st.norm.fit 返回 (loc, scale);st.t.fit 返回 (df, loc, scale)。解包顺序别搞反。提示
st.describe(...) 返回 named tuple;.kurtosis 字段在 SciPy 默认 fisher=True 下就是 excess kurtosis。