Skip to content

Heckman 两阶段

Heckman 样本选择模型两阶段估计。

核心代码

py
    def _fit_heckman_two_step(self, decimals: int, title: str) -> Any:
        if not self.y_var:
            raise ValueError(_('请先选择因变量'))
        if not self.selection_var:
            raise ValueError(_('Heckman 两阶段需要设置样本是否被观察到的选择变量'))

        outcome_vars = self._unique_preserve_order(self.x_vars)
        exclusion_vars = self._unique_preserve_order(self.selection_exclusion_vars)
        cluster_var = self.se_options.get('cluster_var')

        if not outcome_vars:
            raise ValueError(_('Heckman 两阶段至少需要一个结果方程解释变量'))

        selection_vars = self._unique_preserve_order(outcome_vars + exclusion_vars)
        if not exclusion_vars:
            warnings.warn(
                _('当前 Heckman 选择方程没有额外排除限制变量,模型主要依赖 Probit 非线性识别。实证论文中通常建议加入至少一个只进入选择方程的变量。'),
                UserWarning
            )

        stage1_cols = [self.selection_var] + selection_vars
        if cluster_var:
            stage1_cols.append(cluster_var)
        stage1_cols = self._unique_preserve_order([col for col in stage1_cols if col])

        stage1_df = self.data[stage1_cols].dropna().copy()
        if stage1_df.empty:
            raise ValueError(_('选择方程可用样本为空,请检查选择变量和协变量缺失情况'))

        selection_values = set(pd.Series(stage1_df[self.selection_var]).dropna().unique().tolist())
        if not selection_values.issubset({0, 1, False, True}):
            raise ValueError(_('Heckman 的选择变量必须是 0/1 二元变量,其中 1 表示结果变量可被观察'))
        stage1_df[self.selection_var] = stage1_df[self.selection_var].astype(int)

        stage1_y = stage1_df[self.selection_var]
        stage1_x = sm.add_constant(stage1_df[selection_vars], has_constant='add')

        try:
            stage1_model = Probit(stage1_y, stage1_x)
            self.selection_result = stage1_model.fit(disp=0)
        except Exception as exc:
            raise ValueError(_('Heckman 第一阶段 Probit 估计失败: %(error)s') % {'error': str(exc)})

        stage1_index = pd.Index(stage1_df.index)
        stage1_linear = pd.Series(
            np.asarray(stage1_x @ self.selection_result.params),
            index=stage1_index,
            name='selection_index'
        )
        cdf_values = np.clip(norm.cdf(stage1_linear), 1e-8, 1 - 1e-8)
        imr_selected = pd.Series(norm.pdf(stage1_linear) / cdf_values, index=stage1_index, name='IMR')

        stage2_cols = [self.y_var, self.selection_var] + outcome_vars
        if cluster_var:
            stage2_cols.append(cluster_var)
        stage2_cols = self._unique_preserve_order([col for col in stage2_cols if col])

        stage2_df = self.data[stage2_cols].copy()
        stage2_df = stage2_df.join(imr_selected, how='inner')
        stage2_df[self.selection_var] = stage2_df[self.selection_var].astype(float)
        stage2_df = stage2_df[stage2_df[self.selection_var] == 1]
        stage2_df = stage2_df.dropna(subset=[self.y_var] + outcome_vars + ['IMR'])

        if stage2_df.empty:
            raise ValueError(_('Heckman 第二阶段没有可用样本,请确认选择变量=1 的观测中因变量和解释变量不缺失'))
        if len(stage2_df) <= len(outcome_vars) + 2:
            raise ValueError(_('Heckman 第二阶段有效样本量过少,无法稳定估计结果方程'))

        stage2_y = stage2_df[self.y_var]
        stage2_x = sm.add_constant(stage2_df[outcome_vars + ['IMR']], has_constant='add')
        stage2_model = sm.OLS(stage2_y, stage2_x)
        self.result = self._fit_statsmodels_model(stage2_model, stage2_df)

        if self.se_options.get('type', 'iid') == 'iid':
            self._apply_heckman_twostep_covariance(
                stage2_x=stage2_x,
                stage2_y=stage2_y,
                stage1_x=stage1_x,
                stage1_index=stage1_index
            )
        else:
            self._params = self.result.params.copy()
            self._std_errors = self.result.bse.copy()
            self._test_stats = self.result.tvalues.copy()
            self._pvalues = self.result.pvalues.copy()

        imr_p = self._pvalues.get('IMR', np.nan)
        f_value = self._safe_stat(getattr(self.result, 'fvalue', np.nan), max_abs=1e12)
        self.model_stats = {
            'N': int(len(stage1_df)),
            'Selected-N': int(len(stage2_df)),
            'R2': self._safe_stat(getattr(self.result, 'rsquared', np.nan), max_abs=10),
            'Adj-R2': self._safe_stat(getattr(self.result, 'rsquared_adj', np.nan), max_abs=10),
            'F': f_value,
            'IMR-p': imr_p,
            'Selection-Pseudo-R2': getattr(self.selection_result, 'prsquared', np.nan),
        }

        self.display_x_vars = outcome_vars + ['Inverse Mills Ratio']
        self.table_custom_rows = [
            {'label': 'Selection correction', 'value': 'Yes'}
        ]

        self.raw_output = (
            "Heckman Two-Step - Outcome Equation\n"
            + self.result.summary().as_text()
            + "\n\n"
            + "Heckman Two-Step - Selection Equation (Probit)\n"
            + self.selection_result.summary().as_text()
        )
        self.diagnostic_html = self._build_heckman_diagnostic_html(
            stage1_sample=len(stage1_df),
            stage2_sample=len(stage2_df),
            selection_vars=selection_vars,
            exclusion_vars=exclusion_vars,
            decimals=decimals,
            title=title
        )

        return self.result

Released under the AGPL-3.0 License.