外观
方差分解
预测误差方差分解(FEVD)。
核心代码
py
def _fit_fevd(self, decimals: int, title: str) -> None:
"""
执行 VAR 模型的方差分解(FEVD),对应 Stata 的 irf table fevd 命令。
基于 Cholesky 分解,计算各变量对预测误差方差的贡献比例。
输出格式为论文可用的表格:每个响应变量一张表,行为期数,列为各冲击变量的贡献占比。
"""
from statsmodels.tsa.api import VAR
target_cols = self.x_vars
if not target_cols or len(target_cols) < 2:
raise ValueError(_('方差分解至少需要选择 2 个变量。'))
valid_cols = [c for c in target_cols if c in self.data.columns]
if len(valid_cols) < 2:
raise ValueError(_('有效的数值型变量不足 2 个。'))
df_clean = self.data[valid_cols].select_dtypes(include=[np.number]).dropna()
if df_clean.shape[0] < 20:
raise ValueError(_('有效数据行数不足(至少需要 20 行)。'))
# 获取参数
max_lags = int(self.ts_options.get('fevd_max_lags', 8))
ic = self.ts_options.get('fevd_ic', 'aic')
trend = self.ts_options.get('fevd_trend', 'c')
diff_order = int(self.ts_options.get('fevd_diff_order', 0))
periods = int(self.ts_options.get('fevd_periods', 10))
# 响应变量:用户指定的单个响应变量,默认为全部
response_var = self.ts_options.get('fevd_response_var', '')
# 差分处理
if diff_order > 0:
for _step in range(diff_order):
df_clean = df_clean.diff().dropna()
if df_clean.shape[0] < 20:
raise ValueError(_('经过 %(diff_order)s 阶差分后数据行数不足(至少需要 20 行)。') % {'diff_order': diff_order})
# 构建并拟合 VAR 模型
model = VAR(df_clean)
try:
lag_order_result = model.select_order(maxlags=max_lags)
except Exception as e:
raise ValueError(_('滞后阶数选择失败: %(error)s') % {'error': str(e)})
ic_map = {'aic': 'aic', 'bic': 'bic', 'hqic': 'hqic', 'fpe': 'fpe'}
selected_ic = ic_map.get(ic, 'aic')
optimal_lag = getattr(lag_order_result, selected_ic, None)
if optimal_lag is None or optimal_lag == 0:
optimal_lag = 1
try:
var_result = model.fit(maxlags=optimal_lag, trend=trend)
except Exception as e:
raise ValueError(_('VAR 模型拟合失败: %(error)s') % {'error': str(e)})
# 计算方差分解(多取一期,因为 statsmodels step=0 对应 Stata step=1)
fevd = var_result.fevd(periods=periods)
# decomp 维度: (n_vars, periods, n_vars) 即 decomp[response][step][impulse]
var_names = list(var_result.names)
n_vars = len(var_names)
# 差分后的显示名称
display_names = [f'D{diff_order}.{c}' for c in valid_cols] if diff_order > 0 else valid_cols
# 确定要输出的响应变量
if response_var and response_var in var_names:
# 用户指定了单个响应变量
response_indices = [var_names.index(response_var)]
else:
# 未指定则输出全部
response_indices = list(range(n_vars))
# 构建 HTML 输出
diff_note = _('(%(diff_order)s阶差分)') % {'diff_order': diff_order} if diff_order > 0 else ""
overall_title = title if title else _('方差分解(FEVD)%(diff_note)s') % {'diff_note': diff_note}
html = f'<div class="table-editable-container">'
html += render_table_title_editor(overall_title)
# 模型信息摘要
html += '<table class="academic-table" style="margin-bottom: 16px;">'
html += '<tbody>'
html += '<tr><td style="text-align:center;font-weight:bold;">' + _('内生变量') + '</td><td style="text-align:center;">' + ", ".join(display_names) + '</td></tr>'
html += '<tr><td style="text-align:center;font-weight:bold;">' + _('VAR 滞后阶数') + '</td><td style="text-align:center;">' + f'{optimal_lag}({selected_ic.upper()} 准则选择)</td></tr>'
html += _('<tr><td style="text-align:center;font-weight:bold;">样本量</td><td style="text-align:center;">%(var_result_nobs)s</td></tr>') % {'var_result_nobs': var_result.nobs}
html += _('<tr><td style="text-align:center;font-weight:bold;">预测期数</td><td style="text-align:center;">%(periods)s</td></tr>') % {'periods': periods}
html += _('<tr><td style="text-align:center;font-weight:bold;">分解方法</td><td style="text-align:center;">Cholesky 正交分解(变量排序影响结果)</td></tr>')
html += '</tbody></table>'
# 为每个响应变量生成一张方差分解表
for resp_idx in response_indices:
resp_name = display_names[resp_idx]
html += self._build_fevd_table(
fevd.decomp[resp_idx], resp_name, display_names, periods, decimals
)
html += '</div>'
self.custom_html = html