Skip to content

方差膨胀因子(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 基于含截距的辅助回归。'))

Released under the AGPL-3.0 License.