Robust-Regression Synthetic Control with Interference (RRSC)#
Overview#
Most synthetic-control methods assume the no-interference condition: the treatment given to the treated unit does not affect any control unit. When that fails – a policy announced in one place changes behaviour elsewhere, a pollution-control order in a city pushes pollution to its suburbs – the donor pool is contaminated, and a synthetic control built from it is biased.
RRSC (He, Li, Shi and Miao 2026) estimates causal effects when interference is present and its structure is unknown. Under a factor model for the untreated outcome it recovers two things at once: the direct effect on the treated unit, and the interference effect on every control unit. The key idea is a change of viewpoint – the average direct and interference effects are treated as a sparse outlier component of a robust regression against the estimated factor loadings, so a control unit that is interfered with looks like a contaminated observation. Nothing has to be prespecified about which controls are affected.
When to use this estimator#
You suspect the intervention spills over onto some control units, and you do not know in advance which ones. RRSC estimates the interference effect for every unit and lets the data flag the affected ones.
You want the interference itself as an estimand, not just a nuisance to be purged. RRSC returns an interference map – an effect, a confidence interval and a p-value for each unit – which is often the substantive question (how far, and in what direction, did the policy spill?).
Your panel is either short and wide (many units, few post-periods) or narrow and long (few units, many periods). RRSC has a regime for each; see below.
Reach for a different tool when interference is absent (any donor-pooling SCM
is simpler) or when you can name the interfered units up front and want to model
the spillover parametrically (Spillover-Aware Synthetic Control (SPILLSYNTH) offers cd, sar, iscm
and grossi for that case).
Notation#
There are \(N\) units indexed \(i\) over \(T\) periods indexed \(t\), with the intervention at \(T_0\) (so \(T_0\) pre-periods and \(T - T_0\) post-periods). Unit \(i = 1\) is treated. Let \(Z_t = \mathbf{1}(t > T_0)\). Write \(Y_{it}(1)\) and \(Y_{it}(0)\) for the potential outcomes with and without the realised intervention. The observed outcome is \(Y_{it} = Z_t Y_{it}(1) + (1 - Z_t) Y_{it}(0)\).
The estimands are the post-period averages
where \(\bar\beta_1\) is the average direct effect on the treated unit and \(\bar\beta_i\), \(i \ge 2\), is the average interference effect on control unit \(i\).
Mathematical formulation#
Factor model with interference#
The untreated outcome follows a linear factor model, and the effect enters additively:
with \(f_t \in \mathbb{R}^r\) a vector of \(r\) common factors, \(\lambda_i \in \mathbb{R}^r\) unit-specific loadings that are constant over the study window, and \(\varepsilon_{it}\) a mean-zero error. The interactive term \(\lambda_i^\top f_t\) generalises the two-way fixed effects of difference-in-differences and plays the role of the synthetic-control weights: it absorbs the unobserved confounding common to all units. Because the loadings are shared between treated and control units, they are what makes the comparison valid, not a pre-period trajectory match.
Averaging over the post-period gives the regression that identifies the effects,
a linear model in the loadings \(\lambda_i\) with coefficient \(\bar f_{\text{post}}\), in which the effects \(\bar\beta_i\) appear as an outlier term: units with no interference lie on the hyperplane \(\bar Y = \Lambda \bar f_{\text{post}}\), and interfered units deviate from it. Estimation therefore proceeds in two stages – estimate \(\Lambda\) from the pre-period, then recover \(\bar f_{\text{post}}\) (and hence the residual effects) by a robust regression that is not dragged off the hyperplane by the outliers.
Two regimes#
RRSC provides a regime for each asymptotic setting; regime="auto" chooses
fixed_n when \(T_0 \ge 5N\) (the rule of thumb for reliable pre-period
factor analysis) and large_n otherwise.
Fixed-N (Section 3 of the paper). Few units, long pre- and post-periods. With a
long post-period the idiosyncratic errors average out, so the non-interfered
units lie (asymptotically) on a common hyperplane. Under the majority-valid-
controls condition – more than half of the controls are unaffected, without
knowing which – a strict majority of the points sit on that hyperplane, and a
high-breakdown regression (Least Trimmed Squares) locks onto it while ignoring
the interfered minority. mlsynth estimates the loadings with an MLE factor
analysis on the pre-period correlation matrix (matching R’s factanal),
rescales them by the covariance square root, fits LTS
(\(\alpha = 0.5\), matching robustbase::ltsReg including its
finite-sample scale correction), selects the valid controls by a
Donoho-Johnstone threshold, and refits by ordinary least squares on them.
Large-N (Section 4). Many units, a short post-period allowed. Here the errors do not vanish and outliers cannot be separated exactly; instead a robust loss dilutes their influence through cross-sectional averaging as \(N \to \infty\) – a blessing of dimensionality. mlsynth estimates the loadings with an EM quasi-maximum-likelihood factor analysis (Bai and Li 2012) on the centred pre-period panel, then recovers \(\bar f_{\text{post}}\) by a Huber-IRLS regression of the post-period mean on the loadings; the per-unit effect is the residual \(\bar\beta_i = \bar Y_{i,\text{post}} - \lambda_i^\top \hat{\bar f}_{\text{post}}\).
Assumptions#
Assumption 1 (Consistency). For each \(i, t\), \(Y_{it} = Z_t Y_{it}(1) + (1 - Z_t) Y_{it}(0)\).
Remark. Standard. Only two intervention regimes are ever realised in a comparative case study, so the potential outcomes are indexed by the treated unit’s status alone.
Assumption 2 (Linear factor model). The untreated outcome satisfies \(Y_{it}(0) = \lambda_i^\top f_t + \varepsilon_{it}\) with time-invariant loadings and \(\mathbb{E}(\varepsilon_{it}) = 0\).
Remark. The number of factors \(r\) is treated as known; set it with
n_factorsor let the Bai and Ng (2002) criterion pick it. On small panels the criterion tends to over-select (a near-saturated factor model absorbs the effect itself), so for a handful of units prefer to setn_factorsby hand, as the paper does in its Middle East application. There is no separate intercept: the unit level must be carried by the factors. In the large-N regime the loadings come from a mean-centred panel, so a level that is a pure constant is removed; a level that varies over time (a trend, a cycle) survives and is reconstructed. When the treated unit is nearly orthogonal to the common factors, its counterfactual collapses to its own pre-period mean and the effect approaches a raw before-after comparison – watch thecommunalitydiagnostic (below).
Assumption 3 (Majority valid controls, fixed-N). More than half of the control units are free of interference, with the valid set unknown.
Remark. This is what the high-breakdown regression exploits: LTS at \(\alpha = 0.5\) tolerates up to half the points being arbitrary outliers. It is the fixed-N analogue of a well-identified donor pool.
Assumption 4 (Sparse-or-weak interference, large-N). A vanishing fraction of units may exhibit large interference, while the remainder may be interfered only weakly (\(\|\bar\beta_{S^c}\|_1 = o(N)\)).
Remark. This is more permissive than global sparsity: dense but weak spillover across many units is allowed, provided the strong effects are rare. The robust loss makes the strong outliers asymptotically negligible.
Inference and diagnostics#
Circular block bootstrap (default, inference="cbb"). The pre- and
post-periods are resampled in circular blocks (separately by default), the
estimator is recomputed on each resample, and the per-unit bootstrap variance
drives a normal (Wald) confidence interval and p-value – exactly what the
authors’ analyses report. The point estimates are deterministic; the intervals
and p-values are Monte-Carlo objects, so they reproduce a reference run only up
to the bootstrap tolerance.
Conformal permutation (inference="conformal"). The moving-block permutation
test of Chernozhukov, Wuthrich and Zhu (2021), applied to the per-period
counterfactual residuals. It is valid with a short post-period, where the
bootstrap variance is unreliable; it returns p-values but no interval.
The interference map. The fit exposes, for every unit, an effect, a confidence
interval and a p-value (RRSCResults.interference_effects,
interference_ci, interference_pvalue);
RRSCResults.significant_units() returns the controls whose interference
is significant. The counterfactual proxy \(\hat\mu_i + \lambda_i^\top \hat
f_t\) for every unit and period is on counterfactual_panel.
The communality diagnostic. communality reports, per unit, the fraction of
its pre-period variance the common factors explain. A value near zero for the
treated unit is a warning: the factor adjustment is doing almost nothing, so the
direct effect is close to a raw pre/post difference rather than a factor-model
counterfactual.
Example#
import numpy as np, pandas as pd
from mlsynth import RRSC
# a small panel drawn from the untreated factor model, with a direct effect
# on the treated unit and interference planted on two controls
rng = np.random.default_rng(0)
N, T, T0, r = 12, 40, 30, 2
F = rng.normal(size=(T, r)); F[:, 0] = np.cumsum(rng.normal(size=T)) / 3 # trend factor
L = rng.normal(size=(N, r))
Y = (F @ L.T).T + 0.3 * rng.normal(size=(N, T))
Y[0, T0:] += 8.0 # direct effect on the treated unit
Y[[1, 2], T0:] += 5.0 # interference on unit1, unit2
df = pd.DataFrame(
[dict(u=f"unit{i}", t=t, y=Y[i, t], d=int(i == 0 and t >= T0))
for i in range(N) for t in range(T)]
)
res = RRSC({
"df": df, "outcome": "y", "treat": "d", "unitid": "u", "time": "t",
"regime": "auto", "n_factors": 2, "inference": "cbb", "cbb_reps": 200,
}).fit()
print(res.regime, res.att) # direct effect ~ 8
print(res.significant_units(0.05)) # flags unit1, unit2 (interference ~ 5)
print(res.communality["unit0"]) # treated-unit factor communality
The att accessor is the treated unit’s direct effect; the interference map
(interference_effects / interference_ci / interference_pvalue)
carries every unit’s direct or interference effect with uncertainty.
Verification#
RRSC is validated value-for-value against the authors’ reference R code (both regimes) on their two empirical applications – the U.S. embassy relocation to Jerusalem (fixed-N) and Beijing’s orange-alert pollution policy (large-N). The durable cross-validation lives in benchmarks/cases/rrsc_reference.py and the full replication story is on RRSC — Robust-Regression Synthetic Control with Interference (He, Li, Shi & Miao 2026).
Core API#
- class mlsynth.RRSC(config: RRSCConfig | dict)#
Robust-regression synthetic control with interference (He et al. 2026).
- Parameters:
config (RRSCConfig or dict) – Configuration. See
mlsynth.config_models.RRSCConfig.- Returns:
RRSCResults – An
EffectResultwhose flat accessors report the treated unit’s direct effect, plus the per-unit interference map.
- fit() RRSCResults#
Run the RRSC pipeline end to end.
Configuration#
- class mlsynth.config_models.RRSCConfig(*, df: ~pandas.DataFrame, outcome: str, treat: str, unitid: str, time: str, display_graphs: bool = True, save: bool | str = False, counterfactual_color: ~typing.List[str] = <factory>, treated_color: str = 'black', plot: ~mlsynth.config_models.PlotConfig = <factory>, regime: ~typing.Literal['auto', 'fixed_n', 'large_n'] = 'auto', inference: ~typing.Literal['cbb', 'conformal', 'none'] = 'cbb', n_factors: ~typing.Annotated[int | None, ~annotated_types.Ge(ge=1)] = None, max_factors: ~typing.Annotated[int, ~annotated_types.Ge(ge=1)] = 15, loss: ~typing.Literal['huber', 'logcosh'] = 'huber', huber_delta: ~typing.Annotated[float, ~annotated_types.Gt(gt=0)] = 1.345, lts_alpha: ~typing.Annotated[float, ~annotated_types.Ge(ge=0.5), ~annotated_types.Le(le=1.0)] = 0.5, update_dif: bool = True, cbb_block_length: ~typing.Annotated[int | None, ~annotated_types.Ge(ge=1)] = None, cbb_reps: ~typing.Annotated[int, ~annotated_types.Ge(ge=50)] = 500, cbb_separation: bool = True, alpha: ~typing.Annotated[float, ~annotated_types.Gt(gt=0.0), ~annotated_types.Lt(lt=1.0)] = 0.05, random_state: int = 2025)#
Configuration for RRSC – robust-regression synthetic control with interference (He, Li, Shi & Miao 2026, A robust regression approach to synthetic control with interference).
RRSC recovers the direct effect on the treated unit and the interference effects on every control unit under an untreated linear factor model, without prespecifying which controls are interfered. Stage one estimates time-invariant factor loadings from the pre-period panel; stage two treats the average direct and interference effects as a sparse-outlier component in a robust regression against those loadings.
- Parameters:
regime ({“auto”, “fixed_n”, “large_n”}) – Asymptotic regime.
"fixed_n"(Section 3): few units, long pre- and post-periods, high-breakdown LTS under the majority-valid-controls condition."large_n"(Section 4): many units, short post-period allowed, robust M-estimation."auto"(default) picks"fixed_n"whenT0 >= 5 N(the paper’s rule of thumb for reliable pre-period factor analysis) and"large_n"otherwise.inference ({“cbb”, “conformal”, “none”}) – Uncertainty procedure.
"cbb"(default) is the circular block bootstrap: the per-unit bootstrap variance drives a normal (Wald) confidence interval and p-value, exactly as the authors’ analyses report."conformal"is the moving-block permutation test of Chernozhukov et al. (2021), valid with a short post-period."none"returns point estimates only.n_factors (int or None) – Number of common factors
r.None(default) selectsrby the Bai & Ng (2002) IC on the pre-period panel.max_factors (int) – Upper bound for the Bai-Ng factor search.
loss ({“huber”, “logcosh”}) – Robust loss for the large-N factor regression (Assumption 7).
huber_delta (float) – Huber tuning constant (large-N).
lts_alpha (float) – LTS trimming proportion for the fixed-N regime (0.5 = maximal breakdown, robustbase default).
update_dif (bool) – Fixed-N only: regress the post-minus-pre difference (
True, the authors’ real-data setting) rather than the raw post mean.cbb_block_length (int or None) – Circular-block-bootstrap block length.
Noneusesmax(2, T0//10).cbb_reps (int) – Number of bootstrap replicates.
cbb_separation (bool) – Resample the pre- and post-periods in separate blocks (the authors’ default).
alpha (float) – Two-sided level for confidence intervals and the significance map.
random_state (int) – Seed for the bootstrap / permutation RNG.
Results#
- class mlsynth.utils.rrsc_helpers.structures.RRSCResults(*, effects: ~mlsynth.config_models.EffectsResults | None = None, fit_diagnostics: ~mlsynth.config_models.FitDiagnosticsResults | None = None, time_series: ~mlsynth.config_models.TimeSeriesResults | None = None, weights: ~mlsynth.config_models.WeightsResults | None = None, inference: ~mlsynth.config_models.InferenceResults | None = None, method_details: ~mlsynth.config_models.MethodDetailsResults | None = None, sub_method_results: ~typing.Dict[str, ~typing.Any] | None = None, additional_outputs: ~typing.Dict[str, ~typing.Any] | None = None, raw_results: ~typing.Dict[str, ~typing.Any] | None = None, execution_summary: ~typing.Dict[str, ~typing.Any] | None = None, plot_config: ~mlsynth.config_models.PlotConfig | None = None, inputs: ~mlsynth.utils.rrsc_helpers.structures.RRSCInputs, regime: str, n_factors: int, interference_effects: ~typing.Dict[str, float], interference_ci: ~typing.Dict[str, ~typing.Tuple[float | None, float | None]], interference_pvalue: ~typing.Dict[str, float], counterfactual_panel: ~numpy.ndarray, communality: ~typing.Dict[str, float], metadata: ~typing.Dict[str, ~typing.Any] = <factory>)#
Top-level container returned by
mlsynth.RRSC.fit().An
EffectResult: the standardized sub-models are populated for the treated unit (soatt/att_ci/counterfactual/gap/pre_rmseresolve through the base contract, the treatedattbeing the direct effect). The interference map – an average effect, CI and p-value for every unit – lives on the typed fields below.- Parameters:
inputs (RRSCInputs) – Preprocessed panel.
regime (str) – Regime used (
"fixed_n"/"large_n").n_factors (int) – Number of common factors.
interference_effects (dict) –
{unit label: average effect}for every unit (treated = direct).interference_ci (dict) –
{unit label: (lower, upper)}confidence intervals.interference_pvalue (dict) –
{unit label: p-value}.counterfactual_panel (np.ndarray) – Per-period counterfactual proxy
mu_i + lambda_i' f_t, shape(N, T).communality (dict) –
{unit label: factor communality}– the fraction of the unit’s pre-period variance the common factors explain. A near-zero value for the treated unit warns that its counterfactual is close to a raw before-after comparison.metadata (dict) – Free-form pipeline diagnostics (block length, loss, selection set, …).
- property interference_map#
A tidy
(unit, effect, ci_lower, ci_upper, pvalue)table as a list of dicts – the per-unit direct/interference summary, treated first.
Helper Modules#
RRSC estimation pipeline (He, Li, Shi & Miao 2026).
Both regimes share the two-stage skeleton: estimate untreated factor loadings from the pre-period, then recover the average direct/interference effects as the residual of a robust regression against those loadings.
large-N (Section 4): EM factor analysis + Huber-IRLS factor regression on the post-period mean.
fixed-N (Section 3):
factanalloadings (rescaled by the covariance square root) + high-breakdown LTS, then a Donoho-Johnstone selection of valid controls and an OLS refit on them.
point_effects() recomputes the per-unit effects from an arbitrary
(Y, X) so the circular block bootstrap can call it on resampled panels.
- mlsynth.utils.rrsc_helpers.pipeline.counterfactual_panel(Y, T0, Lambda, sigma, mu, delta=1.345)#
Per-period counterfactual proxy
mu_i + lambda_i' f_t(eq. 7): GLS factor scores pre-intervention, Huber-robust regression post-intervention.
- mlsynth.utils.rrsc_helpers.pipeline.point_effects(Y, X, r, regime, update_dif=True, lts_alpha=0.5, delta=1.345)#
Per-unit average direct/interference effects for a panel
(Y, X).Recomputed from scratch (loadings included) so the circular block bootstrap can call it on resampled panels. Returns
(effects, aux)whereauxcarries regime artifacts used by the point fit.
- mlsynth.utils.rrsc_helpers.pipeline.run_rrsc(inputs, config) RRSCFit#
Point estimate: select the number of factors, fit the loadings, recover the per-unit effects, and build the counterfactual panel.
Inference for RRSC (He, Li, Shi & Miao 2026).
run_cbb()– circular block bootstrap (referenceCBB.ci). The per-unit bootstrap variance drives a normal (Wald) confidence interval and p-value, as the authors’ analyses report.run_conformal()– the moving-block (cyclic-shift) permutation test of Chernozhukov, Wuthrich & Zhu (2021) on the per-period counterfactual residuals; valid with a short post-period.