"""F2-conf-01: school-level programme effect with cluster-robust uncertainty. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py""" import json import numpy as np import pandas as pd import statsmodels.formula.api as smf from scipy import stats d = pd.read_csv("data/schools.csv") n_input = len(d) issues = [] # Data checks if d.isna().any().any(): issues.append({"issue": "missing values", "handling": "rows with missing values dropped"}) d = d.dropna() if d["student_id"].duplicated().any(): issues.append({"issue": "duplicate student_id", "handling": "duplicates dropped (first kept)"}) d = d.drop_duplicates("student_id") tconst = d.groupby("school_id")["treated"].nunique() if (tconst > 1).any(): issues.append({"issue": "treated not constant within school", "handling": "reported; not altered"}) sizes = d.groupby("school_id").size() G = d["school_id"].nunique() G1 = int(d.groupby("school_id")["treated"].first().sum()) 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) by school, # inference with t distribution on G-1 degrees of freedom. 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"]) 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 # Cross-check: two-sample t-test on school means (equivalent analysis at school level) sm = d.groupby("school_id").agg(y=("score", "mean"), t=("treated", "first")) tt = stats.ttest_ind(sm.y[sm.t == 1], sm.y[sm.t == 0], equal_var=True) se_means = float((sm.y[sm.t == 1].mean() - sm.y[sm.t == 0].mean()) / tt.statistic) # ICC of residual: one-way ANOVA estimator on OLS residuals by school d = d.assign(r=ols.resid) m = sizes.iloc[0] if sizes.nunique() == 1 else (len(d) - (sizes**2).sum() / len(d)) / (G - 1) gm = d.groupby("school_id")["r"].mean() msb = (sizes * (gm - d["r"].mean())**2).sum() / (G - 1) msw = ((d["r"] - d["school_id"].map(gm))**2).sum() / (len(d) - G) s2b = max((msb - msw) / m, 0.0) icc = float(s2b / (s2b + msw)) 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": G1, "n_analysed": int(len(d)), "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"schools.csv": int(n_input)}, "random_seed": None, "_diagnostics": { "t_stat_cluster": float(tstat), "df_cluster": int(df), "p_value_iid": float(ols.pvalues["treated"]), "se_school_means_ttest": se_means, "p_school_means_ttest": float(tt.pvalue), "design_effect_implied": float((se_cl / se_iid)**2), "design_effect_icc": float(1 + (m - 1) * icc), "students_per_school": float(m), "msb": float(msb), "msw": float(msw), }, } with open("out/results.json", "w") as f: json.dump(res, f, indent=2) print(json.dumps(res, indent=2))