Skip to content
Scenario5 min read

Unconfoundedness

We call 'Uncofoundedness' a scenario where a treatment is not randomly assigned to participants, so confounders effect on treatment assignment and outcome. We h...

Unconfoundedness

We call 'Uncofoundedness' a scenario where a treatment is not randomly assigned to participants, so confounders effect on treatment assignment and outcome. We have client - level data. Confounders were measured before treatment and outcome after

Let's look at the example:

In our ecosystem we have a product, which effect on LTV we want to estimate

Treatment - first purchase in product.

Outcome - LTV after first purchase.

We will test hypothesis:

HoH_o - There is no difference in LTV between treatment and control groups.

HaH_a - There is a difference in LTV between treatment and control groups.

Causal Assumptions

Unconfoundedness: We control all confounders

Overlap: propensity distributions overlap

SUTVA: no interference and consistent treatment definitions. By design

DML works correctly: nuisance functions are correctly specified

No leakage: confounders are measured before treatment

Data

For the analysis you need data in pandas dataframe:

  • treatment column in binary format (1/0)
  • outcome column numeric format, measured after treatment time
  • user_id column (Optional, but useful)
  • confounders columns (Optional, measured before treatment time, numeric format, used for causal assumption check)

We will take data from Causalis DGP.. Read more at https://causalis.causalcraft.com/articles/generate_obs_hte_26_rich

Result
user_idydtenure_monthsavg_sessions_weekspend_last_monthage_yearsincome_monthlyprior_purchases_12msupport_tickets_90dpremium_usermobile_userurban_residentreferred_usermm_obstau_linkg0g1cate
010.0000000.028.8146541.077.93676750.2341011926.6983011.02.01.01.01.00.00.0454530.0454530.0890958.1379819.1423951.004414
1280.0996111.025.9133453.053.77774028.1158595104.2715093.00.01.01.00.01.00.0415140.0415140.24667960.45925778.81730718.358049
236.4004821.024.96992910.0134.76432222.9070625267.9382558.03.00.01.01.00.00.0525930.0525930.1629687.7128559.1385771.425723
342.7882380.040.6550895.059.51707431.9704906597.3270183.02.01.01.01.00.00.0362210.0362210.18875525.38651031.1599325.773422
450.0000000.018.5608993.074.37093039.2372484930.0096285.01.01.01.00.00.00.0363430.0363430.17475715.35925018.6002273.240977
Result

CausalData(df=(100000, 14), treatment='d', outcome='y', confounders=['tenure_months', 'avg_sessions_week', 'spend_last_month', 'age_years', 'income_monthly', 'prior_purchases_12m', 'support_tickets_90d', 'premium_user', 'mobile_user', 'urban_resident', 'referred_user'], user_id='user_id')

EDA

Result
treatmentcountmeanstdminp10p25medianp75p90max
009505176.087138240.8007130.00.00.08.3954464.859278190.22790021396.007575
11494958.506172199.4856250.00.00.00.0000036.958280148.8371935143.642132

Our data has strong treatment class disbalance. Only 5% of sample activated in treatment.

Treatment group has lower mean LTV. It's too early to draw conclusions.

Result

png

We see large right tale

Result

png

Result
treatmentnoutlier_countoutlier_ratelower_boundupper_boundhas_outliersmethodtail
0095051113000.118884-97.288916162.148194Trueiqrboth
1149497210.145686-55.43742092.395699Trueiqrboth

We see many outliers. It's common situation for LTV metric. Dropping them will lead to a biased conclusion

Result
confoundersmean_d_0mean_d_1abs_diffsmdks_pvalue
0premium_user0.7518070.5918370.159970-0.3457210.00000
1income_monthly4549.3851903918.058798631.326392-0.2776110.00000
2spend_last_month89.09180167.37538921.716412-0.2683600.00000
3support_tickets_90d0.9845451.2592440.2746990.2539740.00000
4avg_sessions_week5.0477534.2301480.817606-0.2017350.00000
5prior_purchases_12m3.9042203.5136390.390581-0.1893720.00000
6tenure_months28.74010025.5591613.180939-0.1841560.00000
7age_years36.43598434.8090831.626901-0.1441420.00000
8referred_user0.2714860.3071330.0356470.0786710.00001
9urban_resident0.6007930.5688020.031991-0.0649540.00013
10mobile_user0.8745730.8700750.004498-0.0134770.99998

As we see clients are differ on this confounders. We need to controll them to make causal inference

Inference

ATTE is right estimand here. We will estimate effect on clients that were treated, had first purchase in our product

Read more about DML at https://causalis.causalcraft.com/articles/unconfoundedness-model

Result
value
field
outcomey
estimandATTE
modelIRM
value9.9026 (ci_abs: 4.4731, 15.3320)
value_relative20.3741 (ci_rel: 8.3518, 32.3964)
alpha0.0500
p_value0.0004
is_significantTrue
n_treated4949
n_control95051
treatment_mean58.5062
control_mean76.0871
time2026-07-19

Our estimate is 12.1542 dollars (ci_abs: 7.7933, 16.5152). Mean in treatment group is 58.5062 dollars, so without our product it would be 46.3520 dollars.

Refutation

Unconfoundedness

Result

png

balance_max_smd is 0.011635 so dml specification dealt with controlling confounders

Sensitivity

Result

PASS PASS: the practically meaningful effect is robust to hidden confounding comparable to every primary benchmark. The reported stress-test margin is limited. DoubleML boundary calibration was used for ['rank_2_cluster_0', 'rank_3_cluster_4'].

SUTVA

Result

1.) Are your clients independent (i). Outcome of ones do not depend on others? 2.) Are all clients have full window to measure metrics? 3.) Do you measure confounders before treatment and outcome after? 4.) Do you have a consistent label of treatment, such as if a person does not receive a treatment, he has a label 0?

SUTVA is untestable from data alone, so we call it true by design

Score

Result
metricvalueflag
0psi_p99_over_med63.832306RED
1psi_kurtosis12446.186007RED
2max_|t|_g00.669255GREEN
3max_|t|_m1.781017GREEN
4oos_max_abs_t0.000015GREEN
Result

png

Result

png

DML is specified correctly. There are many outliers in data that effect the score

Overlap

Result

png

Result

png

Customers are not inclined to activate the our product

Result
metricvalueflag
0edge_0.01_below0.018790GREEN
1edge_0.01_above0.000000GREEN
4KS0.196517GREEN
6ESS_treated_ratio0.465148GREEN
7ESS_control_ratio0.996909GREEN
10ATT_identity_relerr0.005642GREEN
12calib_ECE0.005459GREEN

if calib_ECE is YELLOW/RED look at plot_propensity_reliability(dml_result)

Result

png

Logistic recalibration trend is a fitted correction curve for your predicted propensities.

The plot takes your original propensity p and fits this model:

corrected_p = sigmoid(alpha + beta * logit(p)) So the orange line answers:

If I recalibrate these predicted probabilities to better match observed treatment frequencies, what corrected probability would I use?

How to read it:

  • If orange line matches the diagonal: predictions are already well calibrated
  • If orange line is below the diagonal: model tends to overpredict treatment
  • If orange line is above the diagonal: model tends to underpredict treatment

Rule of thumb:

  • ECE near 0 and slope near 1: great
  • ECE near 0 but slope far from 1: average calibration is fine, but probabilities are distorted in shape
  • Many tiny bad bins + one huge good bin: visually noisy, but often not a major practical issue

Let's compare with oracle effect

Result

Ground truth ATE is 19.40958652966079 Ground truth ATTE is 10.914991423363862

Conclusion

Estimated ATTE is 9.9026 (ci_abs: 4.4731, 15.3320) dollars. The DoubleML-compatible sensitivity protocol returns PASS for all three primary benchmarks, but with a limited robustness margin: tenure_months and income_monthly use boundary calibration (cf_d_raw < 0 clipped to cf_d=0, rho=1 fallback), while the rho=1 adversarial scenario for avg_sessions_week fails.