外观
方差膨胀因子(VIF)
通过辅助回归计算 VIF,诊断多重共线性。
核心代码
py
def _fit_vif(self, df: pd.DataFrame, decimals: int, title: str) -> None:
"""
执行 VIF 共线性检验
参数:
df: 清洗后的数据框
decimals: 小数位数
title: 表格标题
"""
values = df.to_numpy(dtype=float)
if len(df) < 2 or not np.isfinite(values).all():
raise ValueError(_('VIF 检验至少需要 2 条有效样本,且不能包含无穷值。'))
constant = np.all(values == values[0], axis=0)
active = np.flatnonzero(~constant)
# 先缩放再中心化,辅助回归隐含截距;不新增同名列,也不覆盖用户的 const。
scaled = values[:, active] / np.max(np.abs(values[:, active]), axis=0) if len(active) else np.empty((len(df), 0))
centered = scaled - scaled.mean(axis=0)
norms = np.linalg.norm(centered, axis=0)
design = centered / norms if len(active) else centered
rows, table_rows = [], []
labels = {'finite': '', 'infinite': _('完全共线'), 'not_applicable': _('无变异,与截距重复'),
'numerical_failure': _('数值计算失败')}
for index, name in enumerate(df.columns):
item = {'variable': str(name), 'value': None, 'tolerance': None, 'status': 'not_applicable'}
if not constant[index]:
try:
position = int(np.flatnonzero(active == index)[0])
target = design[:, position]
others = np.delete(design, position, axis=1)
residual = target - others @ np.linalg.lstsq(others, target, rcond=None)[0]
residual_norm = np.linalg.norm(residual)
# 用机器精度判定完全共线;近似共线保留实际的大 VIF,不按固定阈值截顶。
exact_limit = np.finfo(float).eps * max(design.shape) * 10
if residual_norm <= exact_limit:
item.update(status='infinite', tolerance=0.0)
else:
tolerance = min(float(residual_norm ** 2), 1.0)
value = 1.0 / tolerance
if not np.isfinite(value):
raise FloatingPointError()
item.update(status='finite', value=value, tolerance=tolerance)
except (ValueError, FloatingPointError, np.linalg.LinAlgError):
item['status'] = 'numerical_failure'
rows.append(item)
table_rows.append({'Variable': str(name),
'VIF': 'Inf' if item['status'] == 'infinite' else (f"{item['value']:.{decimals}f}" if item['status'] == 'finite' else _('不适用')),
'1/VIF': f"{item['tolerance']:.{decimals}f}" if item['tolerance'] is not None else '—',
_('说明'): labels[item['status']]})
eligible = [item for item in rows if item['status'] != 'not_applicable']
mean = {'value': None, 'status': 'not_applicable'}
if any(item['status'] == 'numerical_failure' for item in eligible):
mean['status'] = 'numerical_failure'
elif any(item['status'] == 'infinite' for item in eligible):
mean['status'] = 'infinite'
elif eligible:
mean.update(status='finite', value=float(np.mean([item['value'] for item in eligible])))
table_rows.append({'Variable': 'Mean VIF',
'VIF': 'Inf' if mean['status'] == 'infinite' else (f"{mean['value']:.{decimals}f}" if mean['value'] is not None else _('不适用')),
'1/VIF': '—', _('说明'): labels[mean['status']]})
self.result_payload = {'vif_result': {'schema_version': 2, 'n': len(df), 'rows': rows, 'mean': mean}}
self.custom_html = self._generate_html_table(pd.DataFrame(table_rows), title or 'Variance Inflation Factor', index_name=None,
bottom_note=_('Mean VIF 不包含无变异变量;存在完全共线项时为 Inf。VIF 基于含截距的辅助回归。'))