☰
分位数回归与QVAR:基于pyQt的量化时序分析系统
2026/10/3 14:01:48 网站建设 项目流程

简介:这是一套基于Python与PyQt5开发的分位数回归分析工具,涵盖分位数Granger因果检验(含各分位区间Sup-Wald统计量)、分位数VAR(QVAR)模型估计与脉冲响应函数计算,并支持各分位点脉冲图绘制,适合金融计量、宏观经济等方向的高校毕业设计或课程设计参考。项目采用statsmodels完成分位数回归(自回归分布滞后模型),pandas将结果导出至Excel,自带的PyQt5图形界面便于交互操作。压缩包共53个文件,除Python源码外,还包含UI设计文件、C++辅助模块、Excel结果样例、说明文档及运行脚本,整体仅190KB,结构紧凑、易读易改。源码已经过严格测试,留有扩展接口,可在原有回归与检验逻辑上继续加入自定义模型;内置的界面截图与项目文档能帮助快速复现结果。目前已有220人浏览学习,适合需要完整可运行示例的计量经济学或数据科学学习者。

1. 分位数回归+pyQt:这个毕设题目到底在做什么

打开金融或宏观经济数据,你问“x 对 y 的影响有多大”,默认答案是均值上的 OLS 回归。但真实世界里,尾部才是风险:收益率暴跌时 x 还管不管用?房价处在最高 10% 分位时,货币政策的传导和平时还一样吗?分位数回归(quantile regression)就是回答这类问题的工具。把 python + pyQt 做成桌面应用,把分位数 Granger 因果检验、QVAR(分位数向量自回归)和脉冲响应函数一次性集成进去,就是这个标题能撑起一份毕业设计或课程设计的原因。它适合经济学、金融工程、管理科学方向的学生,也适合想把量化分析变成可视化工具的人。

2. 分位数回归与 QVAR:从条件分位数模型到分位数 VAR

分位数回归并不神秘,它只是把“均值建模”换成“条件分位数建模”。但 QVAR 不是单纯把几个分位数回归堆在一起,它还涉及滞后结构、方程间协方差和脉冲响应的计算方式。这一章先解决基础模型,再往上搭 QVAR。

2.1 为什么均值回归看不见尾部关系

OLS 估计的是 E(y|x),它用一个斜率概括 x 的整个影响。当数据存在异方差、厚尾或非线性时,这个斜率可能没有意义。例如股市中,x 是流动性指标,y 是收益率,x 对 y 的均值影响很弱,但它对 y 的 5% 分位影响很强——这意味着流动性枯竭时市场会暴跌。分位数回归通过最小化非对称绝对损失函数 ρτ(u)=u(τ−I(u<0)),直接估计 Q_τ(y|x)=x'β(τ)。每个分位数都有一条自己的系数向量,你能看到影响在整个条件分布上如何变化。

这种做法在实际建模中非常自然。金融风控关注 VaR,本质上就是条件分位数;宏观经济学关注衰退期变量间的传导,也是典型的尾部关系。用 OLS 看平均效应,再用分位数回归看尾部分化,是数据分析里最扎实的互补组合。

2.2 用 statsmodels 跑通最小分位数回归

statsmodels 自带QuantReg,接口和 OLS 很像,几行代码就能出一个完整结果。下面这段是我项目里最常用的最小脚本:

import numpy as np import pandas as pd import statsmodels.api as sm from statsmodels.regression.quantile_regression import QuantReg # 模拟数据:x 在 [0,1] 均匀分布,误差项为厚尾 t 分布并伴随异方差 rng = np.random.default_rng(42) n = 500 x = rng.uniform(0, 1, n) eps = rng.standard_t(3, n) * 0.2 # t(3) 厚尾 y = 0.2 + 1.2 * x + eps * (1 + x) # 截距 0.2,斜率 1.2,异方差 df = pd.DataFrame({"x": x, "y": y}) X = sm.add_constant(df["x"]) # 手动加常数项 model = QuantReg(df["y"], X) res = model.fit(q=0.5) # q=0.5 即最小绝对偏差 LAD print(res.summary())

QuantReg内部用线性规划求解,默认对每个分位数单独拟合。q=0.5时它等价于最小绝对偏差估计,对厚尾和离群值比 OLS 稳健得多。fit之后的结果对象支持params、conf_int()、pvalues等常见属性,所以后续提取系数、画置信区间都很方便。注意sm.add_constant不能省,否则截距会被并进误差项,导致所有系数估计偏掉。

2.3 从单方程到 QVAR:按分位数逐方程估计 VAR

QVAR 的本质是对 VAR 的每个方程做分位数回归。假设有 K 个变量,滞后阶为 p,那么在第 τ 个分位数上,一个变量的当前值对该变量和所有其他变量的滞后 p 阶做回归。不同方程、不同分位数独立估计,得到的系数矩阵就构成 QVAR 的参数。

def build_lag_matrix(data, p): """ data: DataFrame,每一列一个时间序列变量 p: 滞后阶数 返回对齐后的因变量 Y 和自变量 X(含常数项) """ T, K = data.shape cols = [] for lag in range(1, p + 1): shifted = data.shift(lag).iloc[p:] # 丢弃前 p 行,避免 NaN shifted.columns = [f"lag{lag}_{c}" for c in data.columns] cols.append(shifted) X = pd.concat(cols, axis=1) X["const"] = 1.0 Y = data.iloc[p:].reset_index(drop=True) X = X.reset_index(drop=True) return Y, X # 假设 df_ts 里有 y 和 x 两列,已经平稳 Y, X = build_lag_matrix(df_ts[["y", "x"]], p=2) for tau in [0.1, 0.5, 0.9]: for k in range(Y.shape[1]): res = QuantReg(Y.iloc[:, k], X).fit(q=tau) print(f"tau={tau} equation={k}: {res.params.round(3)}")

这里的关键是build_lag_matrix必须把所有变量的滞后同时放进解释变量矩阵,否则跨方程传导关系会丢。.shift(lag).iloc[p:]这个切片容易写错,如果索引不连续,建议先reset_index(drop=True)再操作。滞后阶 p 的选择可以参考 AIC/BIC,但分位数场景下我会控制在 2~3 阶,过高的 p 会让分位数回归矩阵快速变稀疏,小样本下很容易奇异。

2.4 分位数脉冲响应的两种计算路径:递归模拟与局部投影

VAR 的脉冲响应一般靠 Cholesky 分解后的递归模拟得到,但 QVAR 每个分位数对应不同的系数矩阵,残差分布也是分位数相关的,直接套经典 IRF 公式会出错。常见做法有两种:一是绕开解析公式,用蒙特卡洛模拟从每个分位数的残差中重抽样后递归;二是用局部投影(local projections)直接回归不同预测期上的条件分位数。我一般用局部投影,因为它不需要识别残差的完整分布,且对设定误差更稳健。

def irf_local_projection(y, x, p, H, tau, shock_first=True): """ 局部投影法计算分位数脉冲响应 y, x: 一维 Series(平稳) p: 控制滞后阶 H: 最大响应期 tau: 分位数 shock_first: 冲击变量是否为 x(否则为 y) """ data = pd.DataFrame({"y": y, "x": x}) shock = data["x"] if shock_first else data["y"] irf = [] controls = pd.DataFrame(index=data.index) for i in range(1, p + 1): controls[f"y_lag{i}"] = data["y"].shift(i) controls[f"x_lag{i}"] = data["x"].shift(i) controls["const"] = 1.0 for h in range(1, H + 1): y_h = data["y"].shift(-h) frame = pd.concat([shock, controls, y_h], axis=1) # 列顺序:第一列是冲击变量,最后一列是因变量 frame.columns = ["shock"] + list(controls.columns) + ["y_h"] valid = frame.dropna() if len(valid) < 20: irf.append(np.nan) continue res = QuantReg(valid["y_h"], valid[["shock"] + list(controls.columns)]).fit(q=tau) irf.append(res.params["shock"]) return np.array(irf)

这个函数对每个 h 分别回归一次,系数值就是第 h 期的脉冲响应。dropna()是必须的,shift(-h)会在末尾产生缺失值,不丢的话整个回归结果全错。shock_first参数控制冲击变量来自哪一个序列,如果做两个变量互相冲击,就需要跑两次。局部投影的缺点是回归次数多、样本损失大,所以 H 不要超过样本量的四分之一,我通常设 H=10。

3. 分位数 Granger 因果检验:把均值因果推广到分位数

Granger 因果检验在均值框架下是 VAR 里的 F 检验,但分位数场景下,要检验的是“在某个分位数上,x 的滞后是否对 y 的条件分位数有增量预测能力”。这要求我们在受限和非受限模型之间做比较,而不是直接调用现成的grangercausalitytests。

3.1 检验逻辑:受限模型与非受限模型的 Wald 比较

标准做法是建立两个分位数回归模型。非受限模型包含 y 和 x 各自的所有滞后项,受限模型只包含 y 的滞后项。如果 x 的滞后项系数联合为零,说明 x 在分位数 τ 上对 y 没有 Granger 因果。用 Wald 统计量检验“这些系数全部为 0”的原假设。由于分位数回归的目标函数不是正态似然,Wald 统计量依赖参数协方差矩阵的估计,小样本下建议用自助法 p 值。

3.2 构造滞后矩阵与分位数回归检验

下面这个函数是我在项目里用的核心实现。它返回 Wald 统计量和基于卡方近似的 p 值,足够课设阶段使用。

import numpy as np import pandas as pd from statsmodels.regression.quantile_regression import QuantReg from scipy.stats import chi2 def qgranger_test(y, x, p, tau): """ 分位数 Granger 因果检验 y, x: 平稳的一维 Series p: 滞后阶 tau: 分位数 """ data = pd.DataFrame({"y": y, "x": x}).reset_index(drop=True) # 构造非受限模型:包含 y 和 x 的滞后 X_full = pd.DataFrame(index=range(len(data))) for i in range(1, p + 1): X_full[f"y_lag{i}"] = data["y"].shift(i) X_full[f"x_lag{i}"] = data["x"].shift(i) X_full["const"] = 1.0 df_all = pd.concat([data["y"], X_full], axis=1).dropna() Y = df_all.iloc[:, 0] Xf = df_all.iloc[:, 1:] x_lag_cols = [c for c in Xf.columns if c.startswith("x_lag")] # 受限模型:去掉 x 的滞后列 Xr = Xf.drop(columns=x_lag_cols) res_full = QuantReg(Y, Xf).fit(q=tau) res_res = QuantReg(Y, Xr).fit(q=tau) # Wald:只针对 x 滞后系数 beta = res_full.params[x_lag_cols].values cov = res_full.cov_params().loc[x_lag_cols, x_lag_cols].values W = beta @ np.linalg.pinv(cov) @ beta # pinv 避免奇异矩阵 pval = 1 - chi2.cdf(W, df=len(x_lag_cols)) return W, pval

np.linalg.pinv是我习惯用的替代方案,当协方差子矩阵不满秩时不会抛错,但代价是自由度会失真。如果样本量超过 200,且 p 不超过 3,协方差一般都很稳定。检验结果可以这样解读:p 值小于 0.05,就在该分位数上拒绝“x 不是 y 的 Granger 原因”的原假设。不同分位数上的结论可以不同——这正是分位数 Granger 和均值 Granger 最大的区别。

3.3 用自助法计算 p 值,避免小样本翻车

Wald 统计量渐近服从卡方分布,但分位数回归的小样本性质并不好。我见过太多小样本下 Wald p 值显著、换了样本就不显著的情况。稳妥做法是做一个固定设计的残差自助法:先拟合受限模型得到残差,再对残差重抽样构造新的 y,重复 Wald 检验,统计拒绝比例。

def qgranger_bootstrap_pval(y, x, p, tau, B=199, seed=2024): rng = np.random.default_rng(seed) data = pd.DataFrame({"y": y, "x": x}).reset_index(drop=True) # 先跑一次受限模型取残差 X_full = pd.DataFrame(index=range(len(data))) for i in range(1, p + 1): X_full[f"y_lag{i}"] = data["y"].shift(i) X_full[f"x_lag{i}"] = data["x"].shift(i) X_full["const"] = 1.0 df_all = pd.concat([data["y"], X_full], axis=1).dropna() Y0 = df_all.iloc[:, 0] Xr_cols = [c for c in df_all.columns if c.startswith("y_lag")] + ["const"] Xr = df_all[Xr_cols] res_res = QuantReg(Y0, Xr).fit(q=tau) resid = Y0 - res_res.fittedvalues W_obs, _ = qgranger_test(y, x, p, tau) W_boot = [] for _ in range(B): # 对残差重抽样后生成新 y,保持 x 不变 y_star = res_res.fittedvalues + rng.choice(resid, size=len(Y0), replace=True) W_b, _ = qgranger_test(y_star, x.iloc[df_all.index], p, tau) W_boot.append(W_b) pval_boot = (np.array(W_boot) >= W_obs).mean() return W_obs, pval_boot

这里有个细节:y_star构造后,qgranger_test内部会重新对齐索引,所以传入的x必须和Y0的位置一一对应。我把df_all.index传进去,是为了保证模拟数据和原始数据行数一致。这个代码不是工程级的黑匣子,但作为毕设里的“稳健性检验”完全够用。B 一般取 199 或 399,太多会导致 GUI 长时间无响应,所以在 pyQt 里我会把 B 设小点,默认 199。

4. pyQt 界面落地:把统计模型封装成答辩能演示的桌面应用

模型本身再漂亮,交到答辩现场还是得有个能点的东西。pyQt 这层不需要很复杂,核心就是把按钮、参数框、结果表格和 matplotlib 图嵌进一个窗口,同时保证计算不卡死界面。

4.1 界面分层:参数输入、计算线程、结果展示

我把界面分成三块:左侧是数据文件和参数设置(文件路径、分位数列表、滞后阶、预测期数),中间是结果表格,右侧是脉冲响应图。计算必须放在 QThread 里,否则点击“运行”后窗口直接失去响应,这是 pyQt 课设最常见的翻车点。

import sys import pandas as pd from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox, QTableWidget, QTableWidgetItem from PyQt5.QtCore import QThread, pyqtSignal class CalcThread(QThread): finished = pyqtSignal(dict) error = pyqtSignal(str) def __init__(self, data, taus, p, H): super().__init__() self.data = data self.taus = taus self.p = p self.H = H def run(self): try: result = run_all_analysis(self.data, self.taus, self.p, self.H) self.finished.emit(result) except Exception as e: self.error.emit(str(e))

这里run_all_analysis是一个你自己写的汇总函数,内部依次调用第 2、3 章里的回归函数,并把结果打包成字典。QThread 里不能用界面对象,只能通过信号传数据;finished信号携带计算结果字典,主线程收到后再刷新表格和图表。

4.2 数据导入与校验代码

我不会让使用者手工改路径,直接用QFileDialog.getOpenFileName选择 CSV 文件。读进来之后先检查必要列名是否存在,再做平稳性判断,最后把数据传给计算线程。

class MainWindow(QMainWindow): def load_data(self): path, _ = QFileDialog.getOpenFileName(self, "选择数据文件", "", "CSV files (*.csv)") if not path: return df = pd.read_csv(path) required = ["y", "x"] if not set(required).issubset(df.columns): QMessageBox.warning(self, "列名错误", "CSV 中必须包含 y 和 x 两列") return self.data = df[required].dropna().reset_index(drop=True) self.statusBar().showMessage(f"已加载 {len(self.data)} 行数据")

注意 dropna 必须在 reset_index 之前,否则索引错位会导致后续 shift 结果全乱。这里我只接受 y 和 x 两列,如果你的毕设变量更多,可以动态读取列名。数据校验越早做,后面回归报错的概率越小。

4.3 结果表格与脉冲响应图的嵌入

结果表格我用QTableWidget填充分位数系数矩阵;脉冲响应图用matplotlib.backends.backend_qt5agg.FigureCanvasQTAgg嵌入。这是 pyQt + matplotlib 的标准组合,网上教程很多,但有一个常见坑:图对象要在主窗口里初始化一次,后续只更新数据,不要每次绘图都 new 一个 figure,否则内存疯涨。

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class IrfCanvas(FigureCanvas): def __init__(self, parent=None): self.figure = Figure(figsize=(6, 4)) super().__init__(self.figure) self.setParent(parent) def update_irf(self, irf_df): self.figure.clear() ax = self.figure.add_subplot(111) irf_df.plot(ax=ax) ax.axhline(0, color="black", lw=0.5) ax.set_xlabel("horizon") ax.set_ylabel("response") self.draw()

irf_df是一个 DataFrame,行是 h,列是不同分位数的脉冲响应曲线。这样一次就能看到 tau=0.1、0.5、0.9 三条曲线的差异,答辩效果比一堆回归系数直观得多。绘图函数里要调用self.draw(),这个步骤经常被漏掉,导致图不刷新。

4.4 一键导出结果与图表

导出用 pandas 写 Excel 最省事,一个按钮搞定。代码不复杂,但要注意用with pd.ExcelWriter(path)替代每次新建 writer,否则多 sheet 会相互覆盖。

def export_report(self): path, _ = QFileDialog.getSaveFileName(self, "保存报告", "report.xlsx", "Excel files (*.xlsx)") if not path: return with pd.ExcelWriter(path) as writer: self.coef_frame.to_excel(writer, sheet_name="系数表") self.irf_df.to_excel(writer, sheet_name="脉冲响应") self.gc_results.to_excel(writer, sheet_name="Granger因果") QMessageBox.information(self, "完成", f"报告已保存到 {path}")

导出功能虽然在学术严谨性上没有增量价值,但对期末验收很有用。老师可以直接打开 Excel 看系数表,不需要现场跑数。建议把 Granger 检验的 Wald 统计量和 p 值也放进同一个 Excel 文件,方便对比不同分位数上的因果结论。

5. 避坑与排查:分位数回归毕设最常见的 5 个坎

这里写的是我实际做过这类题目后总结的硬伤,每一条都有人当结课作业踩过。

5.1 安装期:pyQt 与 numpy/scipy 版本冲突

现象:pip install pyqt5 一切正常,但from PyQt5.QtWidgets import QApplication直接 segfault,或者 statsmodels 的 QuantReg 报SVD did not converge。 原因:conda 环境里混用了 conda 安装的 numpy(比如 1.16)和 pip 安装的 pyqt5,两个包依赖的 Qt 库冲突。 解决:新建一个干净的虚拟环境,统一用 pip 安装:先pip install numpy scipy pandas statsmodels matplotlib pyqt5,再跑一次最小回归脚本。如果还是崩,就把 numpy 升级到最新版再重装 pyqt5。血泪教训是不要图省事在 Anaconda base 里 pip install,我因为这个浪费过半天。

5.2 数据期:不平稳序列做因果检验等于自嗨

现象:用原始价格序列跑分位数 Granger 因果,所有分位数 p 值都接近 0,结论“完美”。 原因:价格类序列通常带单位根,两个非平稳序列之间即使没有真实因果,回归也会表现出显著相关,这是伪回归。 解决:先对每个变量做 ADF 检验。不能通过平稳性检验的变量,先取对数或一阶差分,再放进模型。我通常在 pyQt 界面里加一个“自动差分”复选框,默认勾选。还要注意:脉冲响应函数也要求平稳,对差分后的变量做响应,解释时要说清楚是“增量的响应”。

5.3 估计期:奇异矩阵与系数不收敛

现象:np.linalg.inv(cov)报LinAlgError: Singular matrix,或者QuantReg.fit(q=0.99)之后参数为 nan。 原因:解释变量高度共线性;或者某个分位数附近样本点太少,比如 q=0.99 时只有 1% 的样本在支撑回归。 解决:把滞后阶 p 从 3 降到 2;把 tau 限定在 0.1~0.9 之间,0.01 这种极端分位数课设数据量撑不住;协方差矩阵求逆统一用np.linalg.pinv。如果仍然奇异,检查数据是否有重复行或完全相同的列。

5.4 模拟期:脉冲响应发散

现象:脉冲响应曲线像喇叭一样向两边张开,h 越大绝对值越大,甚至指数爆炸。 原因:最常见的是变量不平稳;其次是局部投影的控制变量滞后阶不足,冲击按格兰杰逻辑传导时没有足够的历史信息。 解决:先做 ADF 检验,确保变量平稳。控制滞后 p 不要小于 2。H 设成 10 就够,不要试图画到 20 期。每一条曲线计算完后做一个简单判断:如果响应绝对值超过初始冲击的 5 倍,就在界面里标黄提示用户检查平稳性。这点我深有体会,答辩时评委看到发散的图,第一反应就是“你是不是写错了”。

5.5 展示期:GUI 卡死和中文乱码

现象:点击计算后窗口变白,标题栏显示“未响应”;或者 matplotlib 图里的中文全变成方块。 原因:计算放主线程,阻塞了 Qt 事件循环;matplotlib 默认字体没有中文字符。 解决:把一切耗时函数放进CalcThread,主线程只负责接收信号刷新界面。绘图前设置plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"],同时plt.rcParams["axes.unicode_minus"] = False,后一行不设置的话负号会显示成方块。另外注意 pyqt 的QThread不要手动调用run(),只能启动start()。

6. 进阶验证:用蒙特卡洛模拟和稳健性检验让答辩加分

到了这一步,你已经能跑通全流程。我建议在毕设里再加上两个验证步骤:一个是蒙特卡洛功效检验,证明你的分位数 Granger 检验不是摆设;另一个是分位数网格图,直观展示系数随 τ 的变化。

蒙特卡洛的思路是模拟已知因果结构的数据,测试检验能否正确拒绝。比如构造 y_t 确实受 x_{t-1} 影响的机制,然后记录在 199 次重复里有多少次拒绝了原假设,这个比例就是检验功效。

def monte_carlo_power(p, tau, B=100): reject = 0 for i in range(B): rng = np.random.default_rng(i) n = 300 x = rng.normal(size=n) y = np.zeros(n) for t in range(p, n): y[t] = 0.1 + 0.6 * x[t-1] + rng.standard_t(3) * 0.5 W, pval = qgranger_test(pd.Series(y), pd.Series(x), p=1, tau=tau) if pval < 0.05: reject += 1 print(f"tau={tau} 功效: {reject/B:.2f}")

如果功效远低于 0.7,说明样本量或滞后阶设计不合理,你要去调整数据长度。反过来,还要做一次“无因果”情景测试,确认虚警率接近 0.05。这两个数放在论文里,比贴一堆 p 值更有说服力。

分位数网格图则更适合放在 GUI 里作为第二个标签页:横轴是 τ,纵轴是某个变量的系数,画出一条带置信带的折线。它不算复杂,只需要把多次QuantReg.fit(q=tau)的结果收集起来,但视觉效果非常直观,评委一看就懂。我会在图上标注均值回归的系数作为参考线,让“均值回归看不见尾部”这句话直接变成图表证据。

做这个项目最终教会我的一件事:统计模型能不能落地,往往不取决于模型本身,而取决于你愿不愿意把数据校验、协方差求逆、线程卡死这些脏活处理干净。分位数回归的每一行代码都值得自己敲一遍再放进 GUI,别直接抄网上的整合包。希望帮到你。

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

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

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

立即咨询