☰
分位数Granger因果检验与QVAR:Python图形界面工具实战解析
2026/10/12 11:46:43 网站建设 项目流程

简介:一套基于Python与PyQt的分位数回归完整实现方案,适合毕业设计、课程设计或量化分析项目,代码围绕分位数Granger因果检验展开,可计算各分位区间的Sup-Wald统计量,并支持分位数VAR(QVAR)模型估计、脉冲响应函数计算及各分位点脉冲图绘制,便于从多个分位点考察变量间的非线性动态影响。界面层使用PyQt5构建,statsmodels完成分位数回归,pandas将结果导出为Excel,操作流程直观;资源共53个文件,包括Python源码、UI界面、C++辅助代码和Excel结果表格,压缩包仅190KB,结构紧凑、目录清晰,便于阅读和二次开发。作者已对源码严格测试,运行可靠,可在其基础上扩展,包内还附有运行结果与说明文档,有助于快速理解实现思路。目前已有220人学习下载,适合作为计量经济学实证分析、金融风险度量或课程项目的参考模板。

1. 别只盯着均值:这个项目把 Granger 因果检验和 VAR 全部做成了分位数版本

做金融或宏观数据的人应该都有体会:OLS 回归拟合的是条件均值,你拿到一个 0.6 的回归系数,只代表"平均来看"是这个关系。可现实里我们更想回答的是——当市场处于极端下跌时,货币供应量对 CPI 还有没有预测力?当收益率处在高分位(暴涨)时,某个因子的历史值还灵不灵?这些问题均值回归答不了,而分位数回归能答。这个 Python + PyQt5 项目就是干这件事的:它把分位数回归、分位数 Granger 因果检验、QVAR 和脉冲响应函数串成了一套带图形界面的工具,直接出 Excel 结果和分位点脉冲图。适合正在做毕业设计、课程设计,或者想快速把分位数方法落地到自有数据上的同学。

2. 原理先行:分位数回归、Granger 因果检验与 QVAR,到底在算什么

2.1 从 OLS 到分位数回归,丢失的信息在哪里

OLS 回归假设的是 (E(y|x)=x'\beta),也就是解释变量变化时,被解释变量的条件均值如何变化。但金融收益率、宏观经济指标的分布往往厚尾、非对称,均值只能告诉你"中心",中心以外的形状和范围完全看不到。分位数回归的思路是把目标函数从最小化残差平方和改成最小化加权绝对残差:

[ \min_{\beta} \sum_{i:y_i \ge x_i'\beta} \tau |y_i - x_i'\beta| + \sum_{i:y_i < x_i'\beta} (1-\tau) |y_i - x_i'\beta| ]

其中 (\tau \in (0,1)) 是分位点。(\tau=0.5) 就是最小绝对偏差(LAD),等价于中位数回归;(\tau=0.1) 则把重心放在条件分布的低尾。这么做的好处是,不同分位点可能得到完全不同的系数方向和显著性,这在金融数据里非常常见:某个因子在 0.1 分位点显著为负,在 0.9 分位点却不显著,说明它主要是"下行保护"因子而非"上行助推"因子。

这个项目里用到的 statsmodels 库封装好了 QuantReg,底层优化用的是线性规划的内点法或者单纯形法,不需要自己造轮子。但你需要注意一个关键点:分位数回归的目标函数不可导,所以它没有解析解,也没有传统意义上的 R²,模型对比用的大多是 AIC/BIC 或者不同分位点上的拟合优度 (R_1(\tau))。这在后面看输出结果时一定要有心理准备。

2.2 分位数 Granger 因果检验:Sup-Wald 统计量是怎么来的

传统 Granger 因果检验的逻辑是:如果 X 的滞后项对 Y 的当前值有联合显著的解释力,就说 X 是 Y 的 Granger 原因。做法是对 VAR 方程做 F 检验,看 X 滞后项的系数是否全为零。分位数 Granger 因果检验则把这个逻辑搬到任意分位点 (\tau) 上:检验在条件分布的第 (\tau) 分位点上,X 的滞后项系数是否联合为零。

这里最常用的统计量就是 Sup-Wald。它的计算过程是这样的:先对每个分位点 (\tau_k) 估计分位数回归,得到系数估计 (\hat{\beta}(\tau_k)) 和协方差矩阵,然后构造 Wald 统计量,检验约束条件 (R\beta=0)(R 是选择滞后项系数的约束矩阵)。最后对一组分位点取 Wald 统计量的最大值:

[ SupWald = \max_{\tau_k \in \Gamma} W(\tau_k) ]

为什么取最大值而不是平均值?因为在多个分位点同时检验时,传统检验的显著性水平会被膨胀,Sup-Wald 本质上是联合检验,它把"至少有一个分位点上存在因果关系"作为原假设的备择,这样能避免你挨个看分位点 p 值时犯多重比较的错误。这个项目里默认的分位点组一般是 [0.1, 0.25, 0.5, 0.75, 0.9],你也可以在 GUI 里改成自己需要的组合。

2.3 QVAR 与脉冲响应:自回归分布滞后模型如何逐方程估计

QVAR(分位数向量自回归)是标准的 VAR 模型在分位数框架下的推广。VAR 的核心假设是所有方程在均值意义上联合估计,而 QVAR 的想法更直接:对每一个分位点 (\tau),把每个方程单独拿出来做分位数回归。也就是说,对于 N 个变量的系统,滞后阶数为 L,在给定 (\tau) 时,你需要估计 N 个分位数回归方程,每个方程的右侧包含常数项和所有变量的 L 阶滞后值。

这种逐方程估计的做法有一个好处:你不需要做完整系统的联合估计,也就避开了 VAR 里误差项协方差矩阵的估计问题;但它的代价是,每个方程的分位数回归残差之间可能存在相关性,而且这种相关性没有被显式建模。所以在解释脉冲响应时,通常的理解是"在给定分位数状态下施加一个变量的冲击,观察系统在该状态下的动态响应",而不是传统 VAR 里经过 Cholesky 分解正交化后的结构脉冲响应。

脉冲响应函数的计算也是在这个框架下完成的:利用估计出来的每个方程的分位数回归系数,组装出分位数状态下的自回归系数矩阵,然后从初始冲击出发递归推演。由于没有残差协方差矩阵的正交化步骤,它的含义更接近"广义脉冲响应",解释时要说明这一点。

3. 跑通工程:环境配置、文件结构与 GUI 里的每个参数

3.1 环境准备:Python 版本与依赖安装

这个项目是在 Python 3.8 环境下开发的,我实际跑的时候用 3.9 和 3.10 也没问题。核心依赖是 PyQt5、statsmodels、pandas、numpy、matplotlib 和 openpyxl。其中 openpyxl 很容易被漏掉,因为它不是 pandas 的默认依赖,但不装它,pandas 的 to_excel 在写入 .xlsx 时会直接报错。

pip install pyqt5 statsmodels pandas numpy matplotlib openpyxl

如果你用的是 Anaconda 环境,建议用 conda 装 PyQt5 和 statsmodels,因为 conda 的 PyQt5 二进制包在 Windows 上对高分屏的支持更好,用 pip 装的有时会出现界面字体模糊的问题。装完可以用下面这段代码快速验证环境是否正常:

python -c "import statsmodels.api as sm; from statsmodels.regression.quantile_regression import QuantReg; import PyQt5; print('ok')"

如果出现ModuleNotFoundError: No module named 'PyQt5',多半是当前 shell 的 Python 环境跟你 IDE 里选的解释器不是同一个。我在帮别人排查时发现,这种情况十个里有八个是 IDE 选错了虚拟环境。

3.2 工程文件结构:每个脚本负责什么

解压之后你会看到这么几个核心文件,它们的职责划分得很清楚:

main.py # 程序入口,初始化 QApplication 和主窗口 beauty_UI.py # GUI 界面布局与交互逻辑 func.py # 核心计算:分位数回归、Sup-Wald、QVAR、脉冲响应 data/ # 测试数据集目录 README.md # 安装使用说明

main.py 和 beauty_UI.py 是前后端分离的典型写法——界面文件里只放控件和事件绑定,所有的统计计算全部丢给 func.py。你如果只想替换自己的数据,完全不碰 UI 文件,直接在 func.py 里改数据读取函数就行。

3.3 GUI 操作流程与关键参数

启动程序后就一条路:点按钮、填参数、跑结果。界面主要分三块:左侧是数据加载区,中间是参数设置区,下方是结果展示区。参数面板里有几个选项需要你理解后再填,不能无脑默认。

滞后阶数(Lags)是最影响结果的参数。选小了会漏掉变量间的动态关系,选大了自由度会急剧下降。常见做法是对原始序列先拟合一个 VAR,用 AIC 或 BIC 定阶,再把阶数填进去。分位点组合可以输入成0.1,0.25,0.5,0.75,0.9这样的形式,代码内部会做解析。抽样方法(Bootstrap)可以选择残差抽样的迭代次数,这个数字直接决定了检验的耗时。

注意:如果你的数据不是平稳序列,需要先做差分。界面里没有内置差分选项,我一般会在 Excel 里先把数据处理成平稳的再导入。

跑完一次完整流程,你会在输出目录得到两个东西:一个 Excel 文件,里面包含 Sup-Wald 统计量表、QVAR 系数表;另一组是各分位点的脉冲响应函数图,PNG 格式,每个分位点一张。

4. 拆核心代码:从数据预处理到脉冲响应与 Excel 导出的完整链条

4.1 数据读入与平稳性处理

func.py 里数据读入用的就是 pandas 的 read_excel。这里有一个容易被忽略的细节:分位数回归本身不要求残差正态,但它对数据里的时间趋势和单位根很敏感。如果序列不平稳,Sup-Wald 统计量会出现严重的尺寸失真——也就是明明没有因果关系,检验结果却告诉你"有"。我一般会在进入 func.py 之前用 ADF 检验过一遍:

import pandas as pd from statsmodels.tsa.stattools import adfuller def prepare_series(excel_path, sheet_name=0, cols=None): """读取数据并对非平稳序列做一阶差分""" df = pd.read_excel(excel_path, sheet_name=sheet_name, index_col=0) if cols is not None: df = df[cols] diff_cols = [] for col in df.columns: adf_stat, p_value = adfuller(df[col].dropna(), autolag='AIC')[0], adfuller(df[col].dropna(), autolag='AIC')[1] if p_value > 0.05: # 不平稳才差分 df[col] = df[col].diff() diff_cols.append(col) # 差分会在头部产生 NaN,统一透传 NaN,后续估计时整体 dropna df = df.dropna() return df, diff_cols

这段代码的逻辑是把每个序列单独做 ADF 检验,p 值大于 0.05 的自动差分。参数autolag='AIC'表示 ADF 检验的滞后阶数由 AIC 自动选择,比手动指定更稳健。需要注意,df[col].diff()产生的 NaN 不能在这时候 dropna,因为多个序列的缺失位置可能不一致,要等所有列都处理完再统一删除。

4.2 分位数 Granger 因果:自己写 Sup-Wald 统计量

statsmodels 的 QuantReg 提供了系数估计和协方差矩阵,但没有现成的 Wald 检验接口,所以 Sup-Wald 需要自己拼。核心代码逻辑如下:

import numpy as np from statsmodels.regression.quantile_regression import QuantReg def sup_wald_test(df, y_col, x_col, lags=2, quantiles=(0.1, 0.25, 0.5, 0.75, 0.9)): y = df[y_col].values X_raw = df[x_col].values T = len(y) # 构造滞后矩阵:常数项 + y 的 lags 阶滞后 + x 的 lags 阶滞后 X = [np.ones(T - lags)] for i in range(1, lags + 1): X.append(y[lags - i: T - i]) for i in range(1, lags + 1): X.append(X_raw[lags - i: T - i]) X = np.column_stack(X) Y = y[lags:] # 约束矩阵 R:只有 x 的滞后项系数被约束为 0 n_params = X.shape[1] R = np.zeros((lags, n_params)) R[:, -lags:] = np.eye(lags) # 最后 lags 列对应 x 的滞后系数 wald_scores = [] for tau in quantiles: model = QuantReg(Y, X) res = model.fit(q=tau) theta = res.params cov = res.cov_params() # Wald = (Rθ)' (RΣR')⁻¹ (Rθ) r_theta = R @ theta r_cov = R @ cov @ R.T # 用 pinv 防止极端分位点下协方差矩阵奇异 wald = float(r_theta @ np.linalg.pinv(r_cov) @ r_theta) wald_scores.append(wald) return max(wald_scores), wald_scores

这段代码的要点有三处。第一,滞后矩阵的构造用的是切片索引,y[lags-i: T-i]表示取第 i 阶滞后,要搞清楚行对齐——因为 Y 是从第 lags 行开始的,所以所有滞后项也要从第 lags 行对齐。第二,约束矩阵 R 的最后 lags 列是 x 的滞后项系数,中间 lags 列是 y 自己的滞后项系数,检验 Granger 因果只看 x 的滞后项是否为 0。第三,np.linalg.pinv用的是伪逆而不是inv,因为极端分位点(比如 0.1 或 0.9)的协方差矩阵经常接近奇异,用逆矩阵会直接抛LinAlgError。

4.3 QVAR 模型估计与脉冲响应函数

QVAR 的估计就是把上面思路推广到多变量系统。每个分位点跑 N 个分位数回归,然后组装系数矩阵:

def qvar_fit(df, lags=2, tau=0.5): """对给定分位点 tau 估计 QVAR,返回每个方程的模型""" n_cols = df.shape[1] col_names = list(df.columns) Y_all = df.values T = len(Y_all) # 通用滞后矩阵 rows = [np.ones(T - lags)] for i in range(1, lags + 1): for j in range(n_cols): rows.append(Y_all[lags - i: T - i, j]) X = np.column_stack(rows) models = {} for j in range(n_cols): y_j = Y_all[lags:, j] models[col_names[j]] = QuantReg(y_j, X).fit(q=tau) return models, X, lags, n_cols

脉冲响应函数的计算需要利用 QVAR 的系数递归:

def compute_irf(models, lags, n_cols, horizon=10, shock_idx=0): """基于分位点模型系数计算广义脉冲响应""" A = np.zeros((n_cols, lags * n_cols)) const = np.zeros(n_cols) for j, res in models.items(): const[j] = res.params[0] A[j, :] = res.params[1:] # 该方程所有滞后项系数 irf = np.zeros((horizon + 1, n_cols)) irf[0, shock_idx] = 1.0 # 初始冲击,单位冲击 hist = np.zeros((horizon + 1, n_cols)) hist[0, :] = irf[0, :] for h in range(1, horizon + 1): y_contrib = const.copy() for lag in range(1, min(h, lags) + 1): y_contrib = y_contrib + A @ hist[h - lag] hist[h, :] = y_contrib irf[h, :] = y_contrib - const # 脉冲响应是偏离基准线的增量 return irf

这里解释一下为什么脉冲响应要减掉 const。const 是无冲击状态下的基准水平,脉冲响应关心的是"施加冲击后相对基准线的偏离路径",如果不减 const,你得到的是带漂移的水平路径而不是响应函数。另外,递归过程中min(h, lags)保证了滞后阶数不超过可用的历史长度——在 h < lags 时,缺少更早的滞后值就用 0 填充,这是分位数脉冲响应通用的近似处理。

4.4 结果导出:Excel 多 Sheet 保存与分位点脉冲图

import pandas as pd from openpyxl import load_workbook def export_results(wald_df, qvar_coefs_dict, irf_dict, out_path): with pd.ExcelWriter(out_path, engine='openpyxl') as writer: wald_df.to_excel(writer, sheet_name='Sup-Wald', index=False) for tau, coef_df in qvar_coefs_dict.items(): coef_df.to_excel(writer, sheet_name=f'QVAR_tau_{tau}', index=False) # 每个分位点的 IRF 存成一个 sheet for tau, irf_df in irf_dict.items(): irf_df.to_excel(writer, sheet_name=f'IRF_tau_{tau}', index=False)

ExcelWriter配合openpyxl引擎可以把多个 DataFrame 写到同一个文件的不同 sheet,这是分析结果归档最常用也最省事的方案。如果你的结果里有 NaN,openpyxl 写入时会保留空单元格,不会报错;但如果你读到的是 inf,Excel 会显示#NUM!,最好在导出前做一次np.isfinite检查和替换。

脉冲图的部分用的是 matplotlib,每个分位点画一条响应曲线,同一变量的不同分位点可以叠加到同一张图里方便对比。如果你对默认配色不满意,把 func.py 里绘图函数的color参数换成自定义列表即可。

5. 避坑清单:这个项目跑了三遍,踩过的四个真坑

5.1 数据没做平稳性处理,Sup-Wald 统计量严重失真

现象:跑出来的 Sup-Wald 值大得离谱,随便一个分位点都是几十上百,p 值全小于 0.001,看起来"结论非常干净"。

原因:序列里带单位根时,分位数回归的系数估计和 Wald 统计量不再服从标准渐近分布,检验的显著性被系统性放大。最典型的是用国内宏观季度数据时,GDP、M2 这类序列几乎必然不平稳。

解决:进 func.py 之前先做 ADF 检验,不平稳就一阶差分。注意差分后的序列经济含义变了——比如 CPI 差分后表示通胀变化率,而不是通胀水平,解释结论时要用差分后的表述口径。从那以后我每次拿到新数据都强制先跑一遍平稳性检查再谈因果检验。

5.2 极端分位点下协方差矩阵奇异,np.linalg.inv 直接报错

现象:程序跑到 tau=0.05 或者 tau=0.95 时抛异常numpy.linalg.LinAlgError: Singular matrix,但 tau=0.5 一切正常。

原因:极端分位点的有效样本量少,加上滞后项之间的多重共线性,导致系数协方差矩阵近似奇异。特别是小样本(少于 200 个观测)时,这个问题几乎是必然出现的。

解决:把协方差求逆改成np.linalg.pinv,同时在设置分位点组合时避免低于 0.05 或高于 0.95。如果你确实需要 0.01 这种极端分位点,建议把滞后阶数降一阶,或者对变量先做主成分压缩再回归。

5.3 PyQt5 界面点击"计算"后直接白屏卡死

现象:点击按钮后界面变成"未响应"状态,转圈转很久,Windows 提示"无响应"。

原因:计算逻辑放在 GUI 主线程里,耗时操作(特别是 bootstrap 循环)阻塞了 Qt 的事件循环,系统以为程序崩溃了。

解决:把 func.py 里的计算任务放到 QThread 子线程里跑,主线程只负责刷新进度条和状态栏。具体做法是写一个继承QThread的 Worker 类,把run()方法里调func.py的计算函数,信号槽把结果传回主线程。如果你只是想自己用不想改架构,也可以把分位点组合缩小、把 bootstrap 次数从 1000 降到 100,会稍微缓解。

5.4 导出 Excel 时日期索引丢失或变成字符串

现象:index_col=0 读取后,pandas 自动把日期列当成了索引,但导出时日期列消失了,只剩数据列。

原因:to_excel默认不导出索引,除非设置index=True。如果你的索引恰好是日期,这在后续分析里会是致命的——没有时间轴的数据没法回推样本区间。

解决:导出前用df.reset_index(inplace=True)把日期列恢复成普通列,并且在to_excel里设index=False。我一般还会顺手把列名检查一遍,避免"Unnamed: 0"这种残留列混进结果表。

6. 进阶验证:用残差自举和多分位点扫描,让结论更有说服力

Sup-Wald 统计量本身只给你一个数值、一个判断,但审稿老师或者答辩评委大概率会追问一句:"你的临界值是怎么来的?" 这时残差自举(residual bootstrap)就是你最有说服力的回应。

自举的基本思路是:在原假设(x 对 y 没有分位数 Granger 因果)下,用仅含 y 滞后项的模型去拟合,得到残差;然后把残差重采样、重构出新的虚拟 y 序列,重新计算 Sup-Wald 统计量;重复 200 次或 500 次,得到统计量的经验分布,用其 95% 分位数作为临界值。这样完全不依赖渐近理论,小样本下尤其有效。实现时只需要在 func.py 的sup_wald_test外围套一层循环:

def bootstrap_critical_value(df, y_col, x_col, lags=2, quantiles=(0.1, 0.25, 0.5, 0.75, 0.9), n_boot=200): """残差自举求解 Sup-Wald 统计量临界值""" y = df[y_col].values T = len(y) # 约束模型:y 只对自身滞后项回归 X_restr = np.column_stack([np.ones(T - lags)] + [y[lags - i: T - i] for i in range(1, lags + 1)]) Y = y[lags:] resid = Y - X_restr @ QuantReg(Y, X_restr).fit(q=0.5).params boot_stats = [] for _ in range(n_boot): y_boot = Y + np.random.choice(resid, size=len(Y), replace=True) df_boot = pd.DataFrame({y_col: np.concatenate([[np.nan]*lags, y_boot]), x_col: df[x_col].values}) # 计算 Sup-Wald,注意要跳过 NaN 行 _, w_scores = sup_wald_test(df_boot.dropna(), y_col, x_col, lags, quantiles) boot_stats.append(max(w_scores)) return np.percentile(boot_stats, 95)

参数n_boot是自举重采样次数,200 次够看趋势,写论文建议至少 500 次。注意np.random.choice里必须设置replace=True,这是有放回抽样的核心,搞错成无放回结果家庭就全偏了。有了临界值,你在文章里写结论时就可以说:Sup-Wald 统计量为 28.6,超过 95% 自举临界值 17.2,因此在 5% 显著性水平下拒绝"无分位数 Granger 因果"的原假设。

多分位点扫描的做法更简单:写一个循环把 tau 从 0.05 到 0.95 按 0.05 步长遍历,把不同分位点下的系数估计和 Wald 值画成一张折线图。这么做能直观看到"因果关系在哪个区域显著、哪个区域不显著",比如 0.1 到 0.3 的分位点显著,而 0.7 到 0.9 不显著,说明这个变量主要影响的是下行风险——这种结论比单纯说"存在因果关系"要丰富得多,也是论文里最出彩的分析段落。

最后提醒一句:项目里自带的 data 目录是演示数据,跑通流程没问题,但你的论文结论一定要用自己的研究数据重新跑一遍。我把数据替换之后又重新验证了三遍才放心,从那以后我每次换数据源都会强制走一遍平稳性检查 + 自举临界值 + 多分位点对比这三步。希望这个拆解能帮你少走几个弯路,把精力留给真正重要的模型解释上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询