"""F2-conf-01: school-level programme effect with cluster-robust uncertainty.""" import json, os import numpy as np, pandas as pd import statsmodels.formula.api as smf from scipy import stats ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) d = pd.read_csv(os.path.join(ROOT, "data", "schools.csv")) n_rows = len(d) issues = [] # Data checks if d.isna().any().any(): issues.append({"issue": "missing values", "handling": "rows dropped"}); d = d.dropna() if d.student_id.duplicated().any(): issues.append({"issue": "duplicate student_id", "handling": "first kept"}); d = d.drop_duplicates("student_id") g = d.groupby("school_id") assert (g.treated.nunique() == 1).all(), "treatment varies within school" assert set(d.treated.unique()) <= {0, 1} sizes = g.size() ols = smf.ols("score ~ treated", data=d).fit() tau = float(ols.params["treated"]); se_iid = float(ols.bse["treated"]) # Cluster-robust (CR1, Stata-style small-sample correction), inference with t(G-1) cl = smf.ols("score ~ treated", data=d).fit(cov_type="cluster", cov_kwds={"groups": d.school_id, "use_correction": True, "df_correction": True}) se_cl = float(cl.bse["treated"]) G = d.school_id.nunique(); dfree = G - 1 tstat = tau / se_cl p = float(2 * stats.t.sf(abs(tstat), dfree)) q = stats.t.ppf(0.975, dfree) lo, hi = tau - q * se_cl, tau + q * se_cl # Cross-check: school-means two-sample t-test (exact analogue in balanced design) m = g.agg(score=("score", "mean"), treated=("treated", "first")) tt = stats.ttest_ind(m.score[m.treated == 1], m.score[m.treated == 0]) # ICC of residuals: one-way ANOVA estimator on residuals, balanced clusters d = d.assign(res=ols.resid) k = int(sizes.iloc[0]) grp_mean = d.groupby("school_id").res.transform("mean") msb = k * ((d.groupby("school_id").res.mean() - d.res.mean()) ** 2).sum() / (G - 2) # 2 params removed msw = ((d.res - grp_mean) ** 2).sum() / (len(d) - G) s2b = max((msb - msw) / k, 0.0) icc = float(s2b / (s2b + msw)) deff = 1 + (k - 1) * icc res = { "task_id": "F2-conf-01", "tau_hat": tau, "se_iid": se_iid, "se_cluster": se_cl, "p_value": p, "tau_ci_low": float(lo), "tau_ci_high": float(hi), "effect_significant_5pct": bool(p < 0.05), "icc_hat": icc, "n_schools": int(G), "n_treated_schools": int(m.treated.sum()), "n_analysed": int(len(d)), "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"schools.csv": int(n_rows)}, "random_seed": None, "_diagnostics": { "cluster_se_type": "CR1 (G/(G-1)*(N-1)/(N-K)), t with G-1 df", "t_stat_cluster": float(tstat), "df": dfree, "school_means_ttest_t": float(tt.statistic), "school_means_ttest_p": float(tt.pvalue), "se_ratio_cluster_over_iid": se_cl / se_iid, "design_effect_from_icc": deff, "sqrt_design_effect": float(np.sqrt(deff)), "students_per_school": k, "p_value_iid": float(ols.pvalues["treated"]), }, } os.makedirs(os.path.join(ROOT, "out"), exist_ok=True) with open(os.path.join(ROOT, "out", "results.json"), "w") as f: json.dump(res, f, indent=2) print(json.dumps(res, indent=2))