Imports¶
%load_ext autoreload
%autoreload 2
import logging
import os
import pandas as pdThe autoreload extension is already loaded. To reload it, use:
%reload_ext autoreload
# import helpers.hmodule as hmodule
# hmodule.install_module_if_not_present(
# "networkx",
# use_activate=True,
# )
# hmodule.install_module_if_not_present(
# "pgmpy",
# use_activate=True,
# )import helpers.hnotebook as hnotebo
import msml610_utils as ut
import L08_04_05_causal_inference_utils as mtl0cire05
ut.config_notebook()
# Initialize logger.
logging.basicConfig(level=logging.INFO)
_LOG = logging.getLogger(__name__)
hnotebo.set_logger_to_print(_LOG)
hnotebo.set_all_loggers_to_print()pymc is not installed
arviz is not installed
preliz is not installed
sns is not installed
Python 3.12.13
Linux 3f3aef6a9c0f 6.12.67-linuxkit #1 SMP Sun Jan 25 02:26:28 UTC 2026 aarch64 GNU/Linux
dir_name = "L08_data"
#!ls $dir_name
out_dir_name = "figures/"df = pd.read_csv(os.path.join(dir_name, "management_training.csv"))
import helpers.hpandas_display as hpandisp
hpandisp.display_df(df)Loading...
The dataset contains information on managers with the following variables:
- intervention: Binary treatment indicator (1 = received training, 0 = control)
- engagement_score: Primary outcome—average standardized engagement score of manager’s employees
- department_id: Unique department identifier
- tenure: Years the manager has been with the company
- n_of_reports: Number of direct reports the manager has
- gender: Manager’s identified gender (categorical)
- role: Job category within the company (categorical)
- department_size: Number of employees in the department
- department_score: Average engagement score in the department
- last_engagement_score: Previous period’s engagement score for the manager
import helpers.hpandas_stats as hpanstat
show_distributions = True
show_correlations = True
hpanstat.explore_dataframe(
df,
show_distributions=show_distributions,
show_correlations=show_correlations,
)INFO:helpers.hpandas_stats:stats_df=
WARNING: Can't find pytz: continuing
Loading...
INFO:helpers.hpandas_stats:Univariate distributions:

INFO:helpers.hpandas_stats:Correlation matrix:
Loading...
import statsmodels.formula.api as smfmodel = smf.ols("engagement_score ~ intervention", data=df).fit()
print("ATE:", model.params["intervention"])
print("95% CI:", model.conf_int().loc["intervention", :].values.T)
smf.ols("engagement_score ~ intervention", data=df).fit().summary().tables[1]ATE: 0.43459886274141496
95% CI: [0.39693035 0.47226738]
Loading...
mtl0cire05.plot_engagement_vs_intervention(df)
# Density curves comparing distributions
mtl0cire05.plot_engagement_vs_intervention(df)
mtl0cire05.plot_engagement_vs_intervention_by_department(df)
# mtl0cire05.plot_all_variables_vs_intervention(df)mtl0cire05.plot_all_variables_density_by_intervention(df)
# To reduce this bias, you can adjust for the covariates you have in your data.
model = smf.ols(
"""
engagement_score ~ intervention
+ tenure + last_engagement_score + department_score
+ n_of_reports + C(gender) + C(role)""",
data=df,
).fit()
print("ATE:", model.params["intervention"])
print("95% CI:", model.conf_int().loc["intervention", :].values.T)ATE: 0.26779085766768607
95% CI: [0.23357751 0.30200421]
model = smf.ols("engagement_score ~ intervention", data=df).fit()
print("ATE:", model.params["intervention"])
print("95% CI:", model.conf_int().loc["intervention", :].values.T)ATE: 0.43459886274141496
95% CI: [0.39693035 0.47226738]
- The effect estimate here is considerably smaller than the one you got earlier.
- This is some indication of positive bias, which means that managers whose employees were already more engaged are more likely to have participated in the manager training program
Propensity score¶
ps_model = smf.logit(
"""
intervention ~
tenure + last_engagement_score + department_score
+ C(n_of_reports) + C(gender) + C(role)""",
data=df,
).fit(disp=0)
data_ps = df.copy()
data_ps["propensity_score"] = ps_model.predict(df)
data_ps[["intervention", "engagement_score", "propensity_score"]].head()Loading...
# Estimate using propensity score as confounder / covariate.
model = smf.ols(
"""
engagement_score ~ intervention + propensity_score
""",
data=data_ps,
).fit()
print(model.params["intervention"])0.26331267490277155
Propensity score matching¶
# Perform 1-nearest neighbor propensity score matching.
predicted = mtl0cire05.propensity_score_matching(data_ps)
predicted.head()Loading...
# Calculate average treatment effect from propensity score matching.
hat_ATE = mtl0cire05.calculate_psm_ate(predicted)
print(f"ATE (Propensity Score Matching): {hat_ATE:.4f}")ATE (Propensity Score Matching): 0.2878
# Plot inverse probability of treatment weighting results.
mtl0cire05.plot_iptw(data_ps)
# Estimate ATE using IPTW.
weighted_e_y1, weighted_e_y0, hat_ATE = mtl0cire05.estimate_ate_iptw(data_ps)
print("E[Y1]:", weighted_e_y1)
print("E[Y0]:", weighted_e_y0)
print("ATE:", hat_ATE)Variance¶
# Prepare formula and variables for IPW estimation.
formula = """
tenure + last_engagement_score + department_score
+ C(n_of_reports) + C(gender) + C(role)
"""
T = "intervention"
Y = "engagement_score"# Estimate ATE using IPW estimator.
ate_ipw = mtl0cire05.estimate_ate_with_ps(
df, formula, treatment_col=T, outcome_col=Y
)
print(f"ATE (IPW): {ate_ipw:.4f}")# Compute bootstrap 95% confidence interval for ATE using IPW.
print(f"ATE: {ate_ipw:.4f}")
# Define bootstrap function that resamples data and computes ATE.
est_fn = lambda data: mtl0cire05.estimate_ate_with_ps(
data, ps_formula=formula, treatment_col=T, outcome_col=Y
)
# Estimate confidence interval using bootstrap resampling.
ci = mtl0cire05.estimate_confidence_interval_bootstrap(
df, est_fn, rounds=200, seed=123, n_jobs=4, pcts=[2.5, 97.5]
)
print(f"95% Confidence interval: {ci}")Stabilized Propensity Weights¶
# Show sample sizes for original and pseudo-population.
print("Original Sample Size:", data_ps.shape[0])
# Compute sample sizes after IPTW weighting.
treated = data_ps.query("intervention==1")
control = data_ps.query("intervention==0")
weight_t = 1 / treated["propensity_score"]
weight_nt = 1 / (1 - control["propensity_score"])
print("Treated Pseudo-Population Sample Size:", sum(weight_t))
print("Untreated Pseudo-Population Sample Size:", sum(weight_nt))Original Sample Size: 10391
Treated Pseudo-Population Sample Size: 10435.089079197925
Untreated Pseudo-Population Sample Size: 10354.298899788268
# Estimate ATE using stabilized propensity weights.
ate_stabilized = mtl0cire05.estimate_ate_stabilized_weights(data_ps)
print(f"ATE (Stabilized Weights): {ate_stabilized:.4f}")ATE (Stabilized Weights): 0.2660
# Plot propensity score distributions before and after weighting.
mtl0cire05.plot_propensity_distributions(data_ps)