Skip to content
Case Study8 min read

Synthetic Control Method

This notebook presents the in store navigation case and the core analysis workflow.

Synthetic Control Method

This notebook presents the in store navigation case and the core analysis workflow.

Data

Result

Ground-truth ATTE is 2.750000

Result
unit_idcalendar_timetreated_timeyy_cftau_realized_truemu_cfmu_treatedtau_mean_true
0donor_12000-0104.04.00.06.1506716.1506710.0
1donor_12000-0209.09.00.05.5994035.5994030.0
2donor_12000-0304.04.00.04.9781194.9781190.0
3donor_12000-0406.06.00.05.7772475.7772470.0
4donor_12000-0508.08.00.05.6857115.6857110.0

EDA

Result

PanelDataSCM(df=(3885, 4), y='y', unit_col='unit_id', time_col='calendar_time', treated_time='treated_time', time_freq='M', treated_unit='treated', treatment_start=Period('2015-02', 'M'), last_post_period=Period('2015-05', 'M'), n_pre_periods=181, n_post_periods=4, donor_units=['donor_1', 'donor_10', 'donor_11', 'donor_12', 'donor_13', 'donor_14', 'donor_15', 'donor_16', 'donor_17', 'donor_18', 'donor_19', 'donor_2', 'donor_20', 'donor_3', 'donor_4', 'donor_5', 'donor_6', 'donor_7', 'donor_8', 'donor_9'])

Result
donorpre_meanpre_stdpre_slopecorr_with_treated_prermse_to_treated_prermse_to_treated_pre_standardizedmean_diff_preslope_diff_premax_abs_gap_preis_never_treatedn_missing_precorr_rankstd_rmse_rankslope_rankcomposite_similarity_scorerank_by_similaritynotes
0donor_49.7624317.1791540.1155530.7750966.6706250.842433-4.287293-0.00792920.0True04510.8833331ok
1donor_1013.5635367.6028920.1146740.7550045.4620740.689805-0.486188-0.00880717.0True08120.8666672ok
2donor_1816.2209949.2983570.1409310.7745486.3101250.7969062.1712710.01744920.0True05330.8666673ok
3donor_913.7016576.7332310.1001520.7314605.4918320.693564-0.348066-0.02333019.0True010240.7833334ok
4donor_110.6464095.4900060.0797640.7402066.3297940.799390-3.403315-0.04371720.0True09460.7333335ok
5donor_1717.45856410.8868970.1787790.7706677.7441830.9780133.4088400.05529727.0True06990.6500006ok
6donor_1910.4530395.1533670.0737880.6913116.7656100.854429-3.596685-0.04969323.0True012670.6333337ok
7donor_138.7182325.7941820.0809500.6810397.8869780.996047-5.331492-0.04253130.0True0131050.5833338ok
8donor_89.1988955.1522880.0701840.6699637.6230240.962712-4.850829-0.05329823.0True014880.5500009ok
9donor_510.9502765.2677230.0653470.6096167.0141920.885823-3.099448-0.05813422.0True0167100.50000010ok
10donor_77.2044204.6104830.0648720.6961738.9433451.129455-6.845304-0.05861022.0True01112110.48333311high_std_rmse
11donor_1121.03867412.9445760.2245280.82882810.4498281.3197096.9889500.10104635.0True0216170.46666712high_std_rmse
12donor_1415.68508315.0734940.2500560.8324989.6879491.2234911.6353590.12657434.0True0115190.46666713high_std_rmse
13donor_68.6906084.1496020.0496490.5640908.4598501.068395-5.359116-0.07383327.0True01711130.36666714high_std_rmse
14donor_127.5690613.7640250.0464410.6185559.0550801.143566-6.480663-0.07704031.0True01513140.35000015high_std_rmse
15donor_1527.75138113.6612150.2128210.76474516.4806942.08134713.7016570.08933953.0True0719160.35000016high_std_rmse
16donor_231.86187829.3673200.4828320.82550829.2999353.70029017.8121550.359351135.0True0320200.33333317high_std_rmse
17donor_2019.9723766.5083300.0562040.4809559.5164881.2018375.922652-0.06727823.0True01914120.30000018high_std_rmse
18donor_165.6629833.6523220.0403900.56108710.6734551.347951-8.386740-0.08309131.0True01817150.21666719high_std_rmse
19donor_319.9005525.562408-0.0001380.10585610.8879271.3750375.850829-0.12361933.0True02018180.11666720high_std_rmse
Result

png

Inference

Result
value
field
estimandaverage_post_effect
modelAugmentedSyntheticControl
inferenceaverage_att_ttest
value4.9727 (ci_abs: 0.2503, 9.6951)
value_relative16.9843 (ci_rel: 0.8551, 33.1135)
alpha0.0500
p_value0.0411
is_significantTrue
post_outcome_d_mean34.0000
pointwise_post_period_average4.7216
effect_by_time[{'period': 2015-02, 'estimate': 0.60800479979...
cumulative_effect18.8864
n_significant_periods0
n_donors20
n_pre_periods181
n_post_periods4
time2026-03-09

Refutation

Result

png

Result

png

Placebo

Result
placebo_treatment_startn_pre_before_placebon_post_after_placeboaverage_att_placeboci_lowerci_upperp_valuerejects_zeropre_fit_metric
02000-03245.462732-14.40886525.3343280.177510False3.957005e-09
12000-04345.8202081.56883510.0715820.027632True1.342351e-09
22000-05446.443555-0.21788613.1049950.053169False1.285495e-02
32000-06544.912309-1.31083811.1354550.076833False9.762317e-03
42000-0764-3.505020-14.7281007.7180590.311187False6.010438e-02
..............................
1712014-0617343.033284-15.20110321.2676710.548432False3.604817e+00
1722014-0717443.855998-14.37636922.0883650.458889False3.596280e+00
1732014-0817544.315601-13.94385622.5750580.416190False3.586448e+00
1742014-0917643.386534-15.53870022.3117680.521847False3.584910e+00
1752014-1017741.954115-15.87815119.7863820.683716False3.645295e+00

176 rows × 9 columns

We pretend treatment has happened earlier.

  • placebo_treatment_start - where treatment starts
  • n_pre_before_placebo - how many periods were fitted before the treatment
  • n_post_after_placebo - how many periods were fitted after the treatment
  • average_att_placebo - average_post_effect
  • rejects_zero - p_value < alpha

So weak robustness when we have many rows have rejects_zero = false and average_att_placebo is small

Result
unit_idpre_rmsepost_rmsepost_pre_rmspe_ratioaverage_post_gapmax_abs_post_gaprank_post_pre_rmspe_ratiois_actual_treated
0donor_29.90826142.5824034.29766735.06957171.9934401False
1donor_184.16042713.9110603.343661-9.71508526.3662752False
2donor_115.22701817.0083443.253929-7.81475922.3683823False
3donor_157.26702219.2600872.650341-11.74105236.9000284False
4donor_13.1611217.7773792.460323-2.92591812.6932815False
5donor_145.04189212.2416232.4279820.20932616.8938026False
6donor_72.9223716.7084252.295542-0.1240269.6776787False
7donor_122.6389155.9736292.2636683.03293710.2842568False
8donor_43.2819846.2593851.9071951.8248188.2017559False
9donor_83.2349385.8508491.808643-4.7367579.62996010False
10donor_133.3924495.4179471.5970614.1116288.67875811False
11treated3.6312255.5913491.5397974.7966898.14998412True
12donor_62.9358534.2988231.4642502.8508208.23390013False
13donor_205.8423988.4119781.4398165.49348414.65382114False
14donor_174.7210096.4603811.3684325.1814889.61974815False
15donor_53.6786244.9503691.3457122.1843549.50280816False
16donor_36.9043539.1091561.3193359.0811979.78575917False
17donor_192.8850923.6830511.276580-3.6150344.36148918False
18donor_103.8522264.5897471.1914530.3282347.37402719False
19donor_162.6814412.0574740.767302-0.8526812.65712320False
20donor_93.8303762.3894940.623827-0.6367293.15498221False

How to read columns:

  • unit_id: unit used as treated in that placebo run.
  • pre_rmse: pre-treatment fit error (lower is better).
  • post_rmse: post-treatment fit error.
  • post_pre_rmspe_ratio: post_rmse / pre_rmse (main Abadie-style statistic).
  • average_post_gap: mean post-treatment gap (observed - synthetic).
  • max_abs_post_gap: largest absolute post gap.
  • rank_post_pre_rmspe_ratio: rank of ratio, descending (1 = largest ratio).
  • is_actual_treated: True for your real treated unit.

Interpretation: You want the real treated unit (is_actual_treated=True) to have one of the largest post_pre_rmspe_ratio values (ideally rank near 1). If many placebo units have similar/larger ratios, treatment effect is less convincing. Also check pre_rmse: if pre-fit is poor for some placebos, their high ratios are less informative.

Sensitivity

Result
dropped_donoraverage_att_reestimateddelta_vs_full_modelpre_rmse_reestimatedmax_weight_after_refiteffective_n_donors_after_refit
0donor_14.058002-0.9147253.6983830.1610138.102712
1donor_104.434911-0.5378163.6393240.2192605.749925
2donor_113.689260-1.2834673.6881310.2510065.031053
3donor_124.9980160.0252893.6551900.2232626.959361
4donor_134.487509-0.4852183.6316700.2203325.983026
5donor_144.617818-0.3549093.6975990.2133985.489285
6donor_153.880155-1.0925723.6579460.2061936.328750
7donor_164.561821-0.4109073.6314500.2228335.832072
8donor_174.260393-0.7123343.6367980.2182686.296147
9donor_184.148347-0.8243803.6329120.2212925.896571
10donor_194.623318-0.3494093.6314060.2200395.951044
11donor_24.089068-0.8836603.6328670.2185136.036224
12donor_205.0000860.0273593.6605050.2505005.511244
13donor_34.604093-0.3686343.6338200.2232625.772354
14donor_44.864541-0.1081863.6520230.2182316.019011
15donor_54.555381-0.4173463.6313240.2229675.844479
16donor_64.205424-0.7673033.6499570.2031937.324002
17donor_74.514112-0.4586163.6314520.2241135.809195
18donor_84.256794-0.7159343.6382970.2263995.976241
19donor_94.531504-0.4412233.6484590.2197205.889569
  • dropped_donor: donor unit removed in that refit.
  • average_att_reestimated: ATT after removing that donor.
  • delta_vs_full_model: reestimated_ATT - original_full_model_ATT. Near 0 estimate is stable to that donor. Large |deltal that donor is influential.
  • pre_rmse_reestimated: pre-treatment fit error after refit. Higher than baseline worse pre-fit when donor is removed.
  • max_weight_afterrefit: largest donor weight in refit.Very high (close to 1) dependence on one donor.
  • effective_n_donors_after_refit: effective donor count (1 / sum (w^2)). Lower concentration on fewer donors.

Model

Result

{'metrics': {'n_donors': 20, 'n_pre': 181, 'n_post': 4, 'pre_rmse_sc': None, 'pre_rmse_aug': 3.63162621137165, 'pre_rmse_augmented': 3.63162621137165, 'pre_mae_augmented': 2.865683953423894, 'max_abs_pre_gap': 12.319490234592525, 'max_abs_pre_gap_augmented': 12.319490234592525, 'mean_signed_pre_gap': -0.093703057672044, 'mean_signed_pre_gap_augmented': -0.093703057672044, 'att_sc': None, 'att_aug': 4.972727350333998, 'max_weight_sc': 0.1878646195458257, 'max_abs_weight_aug': 0.21641008257012256, 'max_abs_weight_augmented': 0.21641008257012256, 'l1_norm_weight_aug': 1.36881478737466, 'l1_norm_weights_augmented': 1.36881478737466, 'sum_weights_augmented': 1.0, 'effective_n_donors': None, 'effective_n_donors_sc': None, 'herfindahl_weights': None, 'herfindahl_weights_sc': None, 'effective_n_donors_abs_augmented': 11.74898939775322, 'herfindahl_abs_augmented': 0.0851137034978712, 'negative_weight_share': 0.13472048621056845, 'n_negative_weights': 5, 'cond_augmented_gram': 1051.7388011810417, 'n_placebos': 0, 'min_possible_p': None, 'p_value_att': 0.04108734188619114, 'ci_low_abs': 0.25034659065689, 'ci_high_abs': 9.695108110011105, 'placebo_ci_is_unbounded': False, 'missing_cell_fraction': None, 'completion_converged': None, 'completion_effective_rank': None, 'mean_gap_last_k_pre_sc': None, 'mean_gap_last_k_pre_aug': 1.8972574040111958, 'pre_tail_k_used': 3, 'slsqp_fallback_count': 2, 'slsqp_fallback_reasons': ['Positive directional derivative for linesearch'], 'suppressed_fit_warning_count': 1, 'suppressed_fit_warnings': ['SLSQP simplex optimization did not converge; falling back to projected-gradient solver. Occurrences: 2. Reason: Positive directional derivative for linesearch']}, 'warnings': ['SLSQP simplex optimization did not converge; falling back to projected-gradient solver. Occurrences: 2. Reason: Positive directional derivative for linesearch']}