Skip to content

链式中介

多中介变量的链式中介效应分析。

核心代码

py
    def _fit_chain_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_vars = self._unique_preserve_order(self.mediator_vars or self.chain_mediation_options.get('mediator_vars', []))
        if len(mediator_vars) != 2:
            raise ValueError(_('链式中介必须选择 2 个按顺序进入模型的中介变量。'))
        mediator1, mediator2 = mediator_vars

        model_kind, absorb_vars, panel_dims, base_rows = self._resolve_advanced_model_spec()
        use_bootstrap = bool(self.chain_mediation_options.get('use_bootstrap'))
        bootstrap_reps = int(self.chain_mediation_options.get('bootstrap_reps', 500) or 500)
        bootstrap_progress_floor = max(1.0, bootstrap_reps * 0.20) if use_bootstrap else 0.0
        bootstrap_prelude_steps = [
            _('准备总效应模型'),
            _('准备 %(mediator1)s 模型') % {'mediator1': mediator1},
            _('准备 %(mediator2)s 模型') % {'mediator2': mediator2},
            _('准备联合结果模型'),
        ]

        def report_chain_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_chain_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_chain_bootstrap_prelude(2)
        mediator1_fit = self._fit_linear_model(
            df,
            mediator1,
            [x_var] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('链式中介 - 方程 1(X→M1)'),
        )
        report_chain_bootstrap_prelude(3)
        mediator2_fit = self._fit_linear_model(
            df,
            mediator2,
            [x_var, mediator1] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('链式中介 - 方程 2(X,M1→M2)'),
        )
        report_chain_bootstrap_prelude(4)
        outcome_fit = self._fit_linear_model(
            df,
            self.y_var,
            [x_var, mediator1, mediator2] + controls,
            model_kind=model_kind,
            absorb_vars=absorb_vars,
            panel_dims=panel_dims,
            header=_('链式中介 - 方程 3(X,M1,M2→Y)'),
        )

        self._result_model_snapshots = []
        self._store_result_model_snapshot(
            method='chain_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='chain_m1',
            method_label=_('方程 1:X→M1'),
            y_name=mediator1,
            x_vars=[x_var] + controls,
            params=mediator1_fit['params'],
            std_errors=mediator1_fit['std_errors'],
            test_stats=mediator1_fit['test_stats'],
            pvalues=mediator1_fit['pvalues'],
            stats=mediator1_fit['stats'],
            custom_rows=base_rows,
        )
        self._store_result_model_snapshot(
            method='chain_m2',
            method_label=_('方程 2:X,M1→M2'),
            y_name=mediator2,
            x_vars=[x_var, mediator1] + controls,
            params=mediator2_fit['params'],
            std_errors=mediator2_fit['std_errors'],
            test_stats=mediator2_fit['test_stats'],
            pvalues=mediator2_fit['pvalues'],
            stats=mediator2_fit['stats'],
            custom_rows=base_rows,
        )
        self._store_result_model_snapshot(
            method='chain_outcome',
            method_label=_('方程 3:X,M1,M2→Y'),
            y_name=self.y_var,
            x_vars=[x_var, mediator1, mediator2] + controls,
            params=outcome_fit['params'],
            std_errors=outcome_fit['std_errors'],
            test_stats=outcome_fit['test_stats'],
            pvalues=outcome_fit['pvalues'],
            stats=outcome_fit['stats'],
            custom_rows=base_rows,
        )

        a1 = float(mediator1_fit['params'].get(x_var, np.nan))
        a2 = float(mediator2_fit['params'].get(x_var, np.nan))
        d21 = float(mediator2_fit['params'].get(mediator1, np.nan))
        b1 = float(outcome_fit['params'].get(mediator1, np.nan))
        b2 = float(outcome_fit['params'].get(mediator2, np.nan))
        total_effect = float(total_fit['params'].get(x_var, np.nan))
        direct_effect = float(outcome_fit['params'].get(x_var, np.nan))

        indirect_m1 = a1 * b1
        indirect_m2 = a2 * b2
        indirect_chain = a1 * d21 * b2
        total_indirect = indirect_m1 + indirect_m2 + indirect_chain

        summary_rows = [
            {'项目': f'{x_var}{mediator1}{self.y_var}', '估计值': indirect_m1},
            {'项目': f'{x_var}{mediator2}{self.y_var}', '估计值': indirect_m2},
            {'项目': f'{x_var}{mediator1}{mediator2}{self.y_var}', '估计值': indirect_chain},
            {'项目': _('总间接效应'), '估计值': total_indirect},
            {'项目': _('直接效应'), '估计值': direct_effect},
            {'项目': _('总效应'), '估计值': total_effect},
            {'项目': _('中介占总效应比例'), '估计值': total_indirect / total_effect if np.isfinite(total_effect) and not np.isclose(total_effect, 0.0) else 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.chain_mediation_options.get('use_bootstrap'))
        bootstrap_reps = int(self.chain_mediation_options.get('bootstrap_reps', 500) or 500)
        if use_bootstrap:
            bootstrap_progress_floor = max(1.0, bootstrap_reps * 0.20)

            if callable(self.progress_callback):
                prelude_steps = [
                    _('准备总效应模型'),
                    _('准备 %(mediator1)s 模型') % {'mediator1': mediator1},
                    _('准备 %(mediator2)s 模型') % {'mediator2': mediator2},
                    _('准备联合结果模型'),
                ]
                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_chain_effects(sample_df: pd.DataFrame) -> Dict[str, float]:
                self._notify_bootstrap_stage(1, 4, _('总效应回归'))
                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, 4, _('%(mediator1)s 回归') % {'mediator1': mediator1})
                sample_m1 = self._fit_linear_model(sample_df, mediator1, [x_var] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                self._notify_bootstrap_stage(3, 4, _('%(mediator2)s 回归') % {'mediator2': mediator2})
                sample_m2 = self._fit_linear_model(sample_df, mediator2, [x_var, mediator1] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                self._notify_bootstrap_stage(4, 4, _('联合结果回归'))
                sample_y = self._fit_linear_model(sample_df, self.y_var, [x_var, mediator1, mediator2] + controls, model_kind=model_kind, absorb_vars=absorb_vars, panel_dims=panel_dims)
                s_a1 = float(sample_m1['params'].get(x_var, np.nan))
                s_a2 = float(sample_m2['params'].get(x_var, np.nan))
                s_d21 = float(sample_m2['params'].get(mediator1, np.nan))
                s_b1 = float(sample_y['params'].get(mediator1, np.nan))
                s_b2 = float(sample_y['params'].get(mediator2, np.nan))
                s_total = float(sample_total['params'].get(x_var, np.nan))
                s_direct = float(sample_y['params'].get(x_var, np.nan))
                s_indirect_m1 = s_a1 * s_b1
                s_indirect_m2 = s_a2 * s_b2
                s_indirect_chain = s_a1 * s_d21 * s_b2
                return {
                    '经 M1 的间接效应': s_indirect_m1,
                    '经 M2 的间接效应': s_indirect_m2,
                    '链式间接效应': s_indirect_chain,
                    '总间接效应': s_indirect_m1 + s_indirect_m2 + s_indirect_chain,
                    '直接效应': s_direct,
                    '总效应': s_total,
                }

            bootstrap_df, failures = self._run_bootstrap(
                df,
                bootstrap_reps,
                estimate_chain_effects,
                progress_label=_('链式中介 Bootstrap 重复抽样'),
                progress_floor=bootstrap_progress_floor,
            )
            if bootstrap_df.empty:
                warnings.warn("链式中介的 Bootstrap 重复抽样全部失败,已仅保留点估计结果。", UserWarning)
            else:
                bootstrap_rows = []
                effect_map = {
                    '经 M1 的间接效应': indirect_m1,
                    '经 M2 的间接效应': indirect_m2,
                    '链式间接效应': indirect_chain,
                    '总间接效应': total_indirect,
                    '直接效应': direct_effect,
                    '总效应': total_effect,
                }
                for effect_name, point_estimate in effect_map.items():
                    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 = outcome_fit['result']
        self.model_stats = dict(outcome_fit['stats'])
        self.display_x_vars = [x_var, mediator1, mediator2] + controls
        self.table_custom_rows = base_rows
        self.diagnostic_html = ''.join(html_parts)
        self.raw_output = "\n\n".join([
            total_fit['raw_output'],
            mediator1_fit['raw_output'],
            mediator2_fit['raw_output'],
            outcome_fit['raw_output'],
        ])
        return self.result

Released under the AGPL-3.0 License.