Skip to content

中介效应

中介效应检验,Bootstrap 置信区间。

核心代码

py
    def _fit_mediation_analysis(
        self,
        df: pd.DataFrame,
        entity_col: str,
        time_col: str,
        decimals: int,
        title: str,
    ) -> Any:
        x_var, controls = self._resolve_core_x_and_controls()
        mediator_var = str(self.mediator_var or '').strip()
        if not mediator_var:
            raise ValueError(_('中介机制必须选择 1 个中介变量。'))

        model_kind, absorb_vars, panel_dims, base_rows = self._resolve_advanced_model_spec()
        use_bootstrap = bool(self.mediation_options.get('use_bootstrap'))
        bootstrap_reps = int(self.mediation_options.get('bootstrap_reps', 500) or 500)
        bootstrap_progress_floor = max(1.0, bootstrap_reps * 0.15) if use_bootstrap else 0.0
        bootstrap_prelude_steps = [
            _('准备总效应模型'),
            _('准备中介变量模型'),
            _('准备直接效应模型'),
        ]

        def report_bootstrap_prelude(step_index: int) -> None:
            if not use_bootstrap or not callable(self.progress_callback):
                return
            step_label = bootstrap_prelude_steps[max(0, step_index - 1)]
            self.progress_callback({
                'completed': 0,
                'total': bootstrap_reps,
                'successful': 0,
                'skipped': 0,
                'current_iteration': 0,
                'current_stage_index': step_index,
                'current_stage_total': len(bootstrap_prelude_steps),
                'current_stage_label': step_label,
                'progress_value': max(1.0, bootstrap_reps * 0.05 * step_index),
                'message': _('正在准备中介机制 Bootstrap:%(step_label)s') % {'step_label': step_label},
            })

        report_bootstrap_prelude(1)
        total_fit = self._fit_linear_model(
            df,
            self.y_var,
            [x_var] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('中介机制 - 路径 c(总效应)'),
        )
        report_bootstrap_prelude(2)
        mediator_fit = self._fit_linear_model(
            df,
            mediator_var,
            [x_var] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('中介机制 - 路径 a(X→M)'),
        )
        report_bootstrap_prelude(3)
        direct_fit = self._fit_linear_model(
            df,
            self.y_var,
            [x_var, mediator_var] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('中介机制 - 路径 b / c′(Y→X,M)'),
        )

        self._result_model_snapshots = []
        self._store_result_model_snapshot(
            method='mediation_total',
            method_label=_('路径 c:总效应'),
            y_name=self.y_var,
            x_vars=[x_var] + controls,
            params=total_fit['params'],
            std_errors=total_fit['std_errors'],
            test_stats=total_fit['test_stats'],
            pvalues=total_fit['pvalues'],
            stats=total_fit['stats'],
            custom_rows=base_rows,
        )
        self._store_result_model_snapshot(
            method='mediation_a',
            method_label=_('路径 a:X→M'),
            y_name=mediator_var,
            x_vars=[x_var] + controls,
            params=mediator_fit['params'],
            std_errors=mediator_fit['std_errors'],
            test_stats=mediator_fit['test_stats'],
            pvalues=mediator_fit['pvalues'],
            stats=mediator_fit['stats'],
            custom_rows=base_rows,
        )
        self._store_result_model_snapshot(
            method='mediation_bc',
            method_label=_('路径 b / c′:Y→X,M'),
            y_name=self.y_var,
            x_vars=[x_var, mediator_var] + controls,
            params=direct_fit['params'],
            std_errors=direct_fit['std_errors'],
            test_stats=direct_fit['test_stats'],
            pvalues=direct_fit['pvalues'],
            stats=direct_fit['stats'],
            custom_rows=base_rows,
        )

        a_coef = float(mediator_fit['params'].get(x_var, np.nan))
        b_coef = float(direct_fit['params'].get(mediator_var, np.nan))
        total_effect = float(total_fit['params'].get(x_var, np.nan))
        indirect_effect = a_coef * b_coef
        direct_effect = total_effect - indirect_effect

        a_se = float(mediator_fit['std_errors'].get(x_var, np.nan))
        b_se = float(direct_fit['std_errors'].get(mediator_var, np.nan))
        direct_se = float(direct_fit['std_errors'].get(x_var, np.nan))
        total_se = float(total_fit['std_errors'].get(x_var, np.nan))

        a_var = a_se ** 2 if np.isfinite(a_se) else np.nan
        b_var = b_se ** 2 if np.isfinite(b_se) else np.nan
        sobel_se = np.sqrt((b_coef ** 2) * a_var + (a_coef ** 2) * b_var) if np.isfinite(a_var) and np.isfinite(b_var) else np.nan
        sobel_z = indirect_effect / sobel_se if np.isfinite(sobel_se) and sobel_se > 0 else np.nan
        sobel_p = 2 * (1 - norm.cdf(abs(sobel_z))) if np.isfinite(sobel_z) else np.nan

        goodman1_se = np.sqrt((b_coef ** 2) * a_var + (a_coef ** 2) * b_var + a_var * b_var) if np.isfinite(a_var) and np.isfinite(b_var) else np.nan
        goodman2_se = np.sqrt((b_coef ** 2) * a_var + (a_coef ** 2) * b_var - a_var * b_var) if np.isfinite(a_var) and np.isfinite(b_var) and (b_coef ** 2) * a_var + (a_coef ** 2) * b_var - a_var * b_var >= 0 else np.nan
        goodman1_z = indirect_effect / goodman1_se if np.isfinite(goodman1_se) and goodman1_se > 0 else np.nan
        goodman2_z = indirect_effect / goodman2_se if np.isfinite(goodman2_se) and goodman2_se > 0 else np.nan

        summary_rows = [
            {'项目': _('a 系数(X→M)'), '估计值': a_coef, '标准误': a_se, '统计量': mediator_fit['test_stats'].get(x_var, np.nan), 'P-value': mediator_fit['pvalues'].get(x_var, np.nan)},
            {'项目': _('b 系数(M→Y)'), '估计值': b_coef, '标准误': b_se, '统计量': direct_fit['test_stats'].get(mediator_var, np.nan), 'P-value': direct_fit['pvalues'].get(mediator_var, np.nan)},
            {'项目': _('间接效应(a×b)'), '估计值': indirect_effect, '标准误': sobel_se, '统计量': sobel_z, 'P-value': sobel_p},
            {'项目': _('直接效应'), '估计值': direct_effect, '标准误': direct_se, '统计量': direct_effect / direct_se if np.isfinite(direct_se) and direct_se != 0 else np.nan, 'P-value': 2 * (1 - norm.cdf(abs(direct_effect / direct_se))) if np.isfinite(direct_se) and direct_se != 0 else np.nan},
            {'项目': _('总效应'), '估计值': total_effect, '标准误': total_se, '统计量': total_fit['test_stats'].get(x_var, np.nan), 'P-value': total_fit['pvalues'].get(x_var, np.nan)},
            {'项目': 'Goodman-1', '估计值': indirect_effect, '标准误': goodman1_se, '统计量': goodman1_z, 'P-value': 2 * (1 - norm.cdf(abs(goodman1_z))) if np.isfinite(goodman1_z) else np.nan},
            {'项目': 'Goodman-2', '估计值': indirect_effect, '标准误': goodman2_se, '统计量': goodman2_z, 'P-value': 2 * (1 - norm.cdf(abs(goodman2_z))) if np.isfinite(goodman2_z) else np.nan},
            {'项目': _('中介占总效应比例'), '估计值': indirect_effect / total_effect if np.isfinite(total_effect) and not np.isclose(total_effect, 0.0) else np.nan, '标准误': np.nan, '统计量': np.nan, 'P-value': np.nan},
            {'项目': _('间接 / 直接效应比'), '估计值': indirect_effect / direct_effect if np.isfinite(direct_effect) and not np.isclose(direct_effect, 0.0) else np.nan, '标准误': np.nan, '统计量': np.nan, 'P-value': np.nan},
        ]

        html_parts = [
            '<div style="margin-top: 30px;"></div>',
            self._render_dataframe_table(pd.DataFrame(summary_rows), _('%(title)s - 效应分解') % {'title': title or _('中介机制')}, decimals=decimals),
        ]

        use_bootstrap = bool(self.mediation_options.get('use_bootstrap'))
        bootstrap_reps = int(self.mediation_options.get('bootstrap_reps', 500) or 500)
        if use_bootstrap:
            bootstrap_progress_floor = max(1.0, bootstrap_reps * 0.15)

            if callable(self.progress_callback):
                prelude_steps = [
                    _('准备总效应模型'),
                    _('准备中介变量模型'),
                    _('准备直接效应模型'),
                ]
                for step_index, step_label in enumerate(prelude_steps, start=1):
                    self.progress_callback({
                        'completed': 0,
                        'total': bootstrap_reps,
                        'successful': 0,
                        'skipped': 0,
                        'current_iteration': 0,
                        'current_stage_index': step_index,
                        'current_stage_total': len(prelude_steps),
                        'current_stage_label': step_label,
                        'progress_value': max(1.0, bootstrap_reps * 0.05 * step_index),
                        'message': _('正在准备中介机制 Bootstrap:%(step_label)s') % {'step_label': step_label},
                    })

            def estimate_effects(sample_df: pd.DataFrame) -> Dict[str, float]:
                self._notify_bootstrap_stage(1, 3, _('路径 c 回归'))
                sample_total = self._fit_linear_model(sample_df, self.y_var, [x_var] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                self._notify_bootstrap_stage(2, 3, _('路径 a 回归'))
                sample_mediator = self._fit_linear_model(sample_df, mediator_var, [x_var] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                self._notify_bootstrap_stage(3, 3, _('路径 b / c′ 回归'))
                sample_direct = self._fit_linear_model(sample_df, self.y_var, [x_var, mediator_var] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                sample_a = float(sample_mediator['params'].get(x_var, np.nan))
                sample_b = float(sample_direct['params'].get(mediator_var, np.nan))
                sample_total_effect = float(sample_total['params'].get(x_var, np.nan))
                sample_indirect = sample_a * sample_b
                sample_direct_effect = sample_total_effect - sample_indirect
                return {
                    '间接效应': sample_indirect,
                    '直接效应': sample_direct_effect,
                    '总效应': sample_total_effect,
                }

            bootstrap_df, failures = self._run_bootstrap(
                df,
                bootstrap_reps,
                estimate_effects,
                progress_label=_('中介机制 Bootstrap 重复抽样'),
                progress_floor=bootstrap_progress_floor,
            )
            if bootstrap_df.empty:
                warnings.warn("Bootstrap 重复抽样全部失败,已仅保留 Sobel/Goodman 检验结果。", UserWarning)
            else:
                bootstrap_rows = []
                for effect_name, point_estimate in [(_('间接效应'), indirect_effect), (_('直接效应'), direct_effect), (_('总效应'), total_effect)]:
                    effect_values = bootstrap_df[effect_name].dropna()
                    if effect_values.empty:
                        continue
                    bootstrap_rows.append({
                        '效应': effect_name,
                        '点估计': point_estimate,
                        'Bootstrap SE': effect_values.std(ddof=1),
                        '95% CI 下限': effect_values.quantile(0.025),
                        '95% CI 上限': effect_values.quantile(0.975),
                    })
                html_parts.append(
                    self._render_dataframe_table(
                        pd.DataFrame(bootstrap_rows),
                        _('%(title)s - Bootstrap 检验') % {'title': title or _('中介机制')},
                        decimals=decimals,
                    )
                )
                if failures:
                    warnings.warn(f"Bootstrap 过程中有 {failures} 次重复抽样失败,结果基于成功抽样重复次数生成。", UserWarning)

        self.result = direct_fit['result']
        self.model_stats = dict(direct_fit['stats'])
        self.display_x_vars = [x_var, mediator_var] + controls
        self.table_custom_rows = base_rows
        self.diagnostic_html = ''.join(html_parts)
        self.raw_output = "\n\n".join([
            total_fit['raw_output'],
            mediator_fit['raw_output'],
            direct_fit['raw_output'],
        ])
        return self.result

Released under the AGPL-3.0 License.