"""F2-conf-01: school-level programme effect with cluster-robust uncertainty. Reads data/schools.csv only; writes out/results.json (deterministic, no randomness).""" 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_raw = len(d) issues = [] # Audit checks (no exclusions needed if all pass) assert d[["school_id", "student_id", "treated", "score"]].notna().all().all(), "missing values" assert not d.student_id.duplicated().any(), "duplicate student ids" assert (d.groupby("school_id").treated.nunique() == 1).all(), "treatment varies within school" assert set(d.treated.unique()) <= {0, 1} sizes = d.groupby("school_id").size() if sizes.nunique() != 1: issues.append({"issue": "unbalanced school sizes", "handling": "kept; CR1 handles it"}) # OLS of score on treated with intercept m_iid = smf.ols("score ~ treated", data=d).fit() G = d.school_id.nunique() m_cl = smf.ols("score ~ treated", data=d).fit(cov_type="cluster", cov_kwds={"groups": d.school_id}, use_t=True) tau = float(m_iid.params["treated"]) se_iid = float(m_iid.bse["treated"]) se_cl = float(m_cl.bse["treated"]) # CR1: G/(G-1)*(N-1)/(N-K) small-sample correction df = G - 1 tstat = tau / se_cl p = float(2 * stats.t.sf(abs(tstat), df)) q = stats.t.ppf(0.975, df) lo, hi = tau - q * se_cl, tau + q * se_cl # Sensitivity: two-sample t-test on school means (balanced design) sm = d.groupby("school_id").agg(score=("score", "mean"), treated=("treated", "first")) tt = stats.ttest_ind(sm.score[sm.treated == 1], sm.score[sm.treated == 0], equal_var=True) # ICC of residual: one-way ANOVA estimator on OLS residuals by school d["resid"] = m_iid.resid m = int(sizes.iloc[0]) if sizes.nunique() == 1 else float(sizes.mean()) gm = d.groupby("school_id").resid.mean() msb = m * ((gm - d.resid.mean()) ** 2).sum() / (G - 1) msw = ((d.resid - d.school_id.map(gm)) ** 2).sum() / (len(d) - G) s2b = max((msb - msw) / m, 0.0) icc = float(s2b / (s2b + msw)) deff = 1 + (m - 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(sm.treated.sum()), "n_analysed": int(len(d)), "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"schools.csv": int(n_raw)}, "random_seed": None, "t_df": int(df), "t_stat_cluster": float(tstat), "p_value_iid": float(m_iid.pvalues["treated"]), "se_ratio_cluster_to_iid": se_cl / se_iid, "design_effect_from_icc": float(deff), "school_mean_ttest_t": float(tt.statistic), "school_mean_ttest_p": float(tt.pvalue), "students_per_school": m, } 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))