Skip to content

PSM-DID

倾向得分匹配双重差分。

核心代码

py
    def _fit_psm_did(self, decimals: int, title: str) -> Any:
        if not self.y_var:
            raise ValueError(_('PSM 匹配需要选择因变量'))

        treat_var = self.did_options.get('treat_var')
        if not treat_var:
            raise ValueError(_('PSM 匹配需要选择处理组变量'))

        covariates = self._get_control_vars(exclude=[treat_var])
        if not covariates:
            raise ValueError(_('PSM 匹配至少需要选择一个匹配协变量'))

        regression_spec = self._get_psm_regression_spec(treat_var, covariates)
        psm_df = self._prepare_psm_frame(treat_var, covariates, regression_spec)
        psm_df[treat_var] = (pd.to_numeric(psm_df[treat_var], errors='coerce') > 0).astype(int)
        if psm_df[treat_var].nunique() != 2:
            raise ValueError(_('处理组变量必须同时包含处理组和控制组'))

        psm_df, score_result, support_note = self._estimate_propensity_scores(psm_df, treat_var, covariates)
        match_result = self._run_psm_matching(psm_df, treat_var, self.y_var)
        psm_df['__psm_support__'] = match_result['support'].astype(int)
        psm_df['__psm_weight__'] = match_result['weights']
        psm_df['__psm_matched_outcome__'] = match_result['matched_outcome']
        psm_df['__psm_regression_sample__'] = match_result['regression_sample'].astype(int)

        effect_df, effect_summary = self._build_psmatch2_effect_table(psm_df, treat_var, self.y_var, match_result)
        balance_df, balance_summary_df = self._build_pstest_tables(psm_df, treat_var, covariates, match_result)

        regression_result, regression_df, regression_x_vars = self._fit_psm_regression(psm_df, regression_spec, match_result)
        self.result = regression_result
        self._capture_panel_result(regression_result)
        self._set_model_stats(regression_result)
        self.model_stats['N'] = int(len(regression_df))
        self.display_x_vars = regression_x_vars

        method = self.did_options.get('psm_matching_method', 'neighbor')
        header = title or _('PSM 匹配与回归')
        summary_df = pd.DataFrame([
            {'项目': _('匹配方法'), '取值': self._matching_method_label(method)},
            {'项目': _('匹配效应口径'), '取值': self._psm_estimand_label()},
            {'项目': _('倾向得分模型'), '取值': self._psm_score_model_label()},
            {'项目': _('匹配尺度'), '取值': self._psm_score_scale_label()},
            {'项目': _('原始样本量'), '取值': int(len(psm_df))},
            {'项目': _('最终共同支撑域样本量'), '取值': int(match_result['support'].sum())},
            {'项目': _('匹配后原始回归样本量'), '取值': int(match_result['regression_sample'].sum())},
            {'项目': _('匹配后回归估计样本量'), '取值': int(self.model_stats.get('N', 0) or 0)},
            {'项目': _('匹配后处理组样本量'), '取值': int(((psm_df[treat_var] == 1) & match_result['regression_sample']).sum())},
            {'项目': _('匹配后控制组样本量'), '取值': int(((psm_df[treat_var] == 0) & match_result['regression_sample']).sum())},
            {'项目': _('共同支撑域区间'), '取值': support_note},
        ])

        self.diagnostic_html = (
            self._render_dataframe_table(summary_df, _('%(header)s - 匹配摘要') % {'header': header}, decimals=decimals)
            + self._render_dataframe_table(effect_df, _('%(header)s - psmatch2 效果摘要') % {'header': header}, decimals=decimals)
            + self._render_dataframe_table(balance_summary_df, _('%(header)s - pstest 总体摘要') % {'header': header}, decimals=decimals)
            + self._render_dataframe_table(balance_df, _('%(header)s - pstest 协变量平衡') % {'header': header}, decimals=decimals)
        )

        self.chart_images = []
        selected_charts = []
        if self._as_bool(self.did_options.get('psm_export_balance_chart', True)):
            selected_charts.append(self._build_pstest_bias_chart(balance_df, header))
        if self._as_bool(self.did_options.get('psm_export_support_chart', True)):
            selected_charts.append(self._build_support_chart(psm_df, treat_var, header))
        if self._as_bool(self.did_options.get('psm_export_density_chart', True)):
            selected_charts.extend([
                self._build_density_chart(psm_df, treat_var, f'{header} - ?????????', matched=False),
                self._build_density_chart(psm_df, treat_var, f'{header} - ?????????', matched=True),
            ])
        for chart in selected_charts:
            if chart:
                self.chart_images.append(chart)

        fixed_effects_label = self._format_fixed_effects_label(regression_spec['fe_vars'])
        self.table_custom_rows = [
            {'label': _('匹配方法'), 'value': self._matching_method_label(method)},
            {'label': _('匹配效应'), 'value': self._psm_estimand_label()},
            {'label': _('固定效应'), 'value': fixed_effects_label},
            {'label': _('共同支撑域'), 'value': _('是') if self._as_bool(self.did_options.get('psm_common_support', True)) else _('否')},
        ]

        score_summary_text = self._result_summary_text(score_result['result'])
        regression_summary_text = self._result_summary_text(regression_result)
        self.raw_output = (
            _('PSM 倾向得分模型\n')
            + score_summary_text
            + _('\n\npsmatch2 效果摘要\n')
            + effect_df.to_string(index=False)
            + _('\n\npstest 总体摘要\n')
            + balance_summary_df.to_string(index=False)
            + _('\n\npstest 协变量平衡\n')
            + balance_df.to_string(index=False)
            + _('\n\n匹配后回归\n')
            + regression_summary_text
        )

        self.model_stats['Matched-N'] = int(match_result['regression_sample'].sum())
        return self.result

Released under the AGPL-3.0 License.