"""F1-conf-02: balanced-panel mean life expectancy 2000-2022. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py Reads only data/; writes out/results.json, out/analytic.csv, out/exclusions.csv. No randomness is used (random_seed = null).""" import json, os import pandas as pd DATA = "data/life_expectancy_panel.csv" OUT = "out" YEARS = list(range(2000, 2023)) CANON_NAME = {"TUR": "Turkiye"} # documented choice for the duplicated Turkey/Turkiye rows raw = pd.read_csv(DATA, dtype={"country_name": str, "iso3": str}) n_raw = len(raw) ledger, issues = [], [] def step(name, rule, before, after, why): ledger.append(dict(step=name, rule=rule, n_before=before, n_after=after, n_removed=before - after, rationale=why)) # --- detect duplicates on (iso3, year) dup = raw[raw.duplicated(["iso3", "year"], keep=False)] for iso, g in dup.groupby("iso3"): conflicting = (g.groupby("year")["life_expectancy"].nunique(dropna=False) > 1).sum() if conflicting: raise SystemExit(f"Conflicting duplicate values for {iso}: decision required") names = sorted(g.country_name.unique()) issues.append({"issue": f"{iso} appears twice per year under names {names} " f"({len(g)} rows for {g.year.nunique()} years; values identical in every year) - " "assembly error from the two extractions", "handling": f"deduplicated on (iso3, year), keeping one row per year; " f"country_name standardised to '{CANON_NAME.get(iso, names[-1])}'; values unaffected"}) d = raw.copy() d["country_name"] = d.apply(lambda r: CANON_NAME.get(r.iso3, r.country_name), axis=1) d = d.sort_values(["iso3", "year"]).drop_duplicates(["iso3", "year"], keep="first") step("1_dedup", "keep one row per (iso3, year); duplicates had identical values", n_raw, len(d), "country identified by iso3; at most one observation per country-year") # --- missing values miss = d[d.life_expectancy.isna()] for _, r in miss.iterrows(): issues.append({"issue": f"{r.iso3} {int(r.year)}: empty life_expectancy cell", "handling": "treated as missing (no imputation); country excluded from balanced set"}) d_obs = d.dropna(subset=["life_expectancy"]) step("2_drop_missing", "drop country-years with empty life_expectancy", len(d), len(d_obs), "empty cell = no recorded value; no imputation allowed") # --- countries with absent years cov = d_obs.groupby("iso3")["year"].apply(lambda s: sorted(set(s))) for iso, ys in cov.items(): absent_rows = sorted(set(YEARS) - set(d.loc[d.iso3 == iso, "year"])) if absent_rows: issues.append({"issue": f"{iso}: no rows at all for years {absent_rows[0]}-{absent_rows[-1]} " f"({len(absent_rows)} years; coverage only {min(ys)}-{max(ys)})", "handling": "treated as missing (no imputation); country excluded from balanced set, " "kept in the 2022 all-available baseline"}) balanced = sorted(iso for iso, ys in cov.items() if set(YEARS) <= set(ys)) an = d_obs[d_obs.iso3.isin(balanced) & d_obs.year.isin(YEARS)] step("3_balanced", "keep countries with a recorded value in every year 2000-2022", len(d_obs), len(an), "balanced-panel estimand: fixed composition across years") an = an[["iso3", "country_name", "year", "life_expectancy"]].sort_values(["iso3", "year"]).reset_index(drop=True) assert len(an) == len(balanced) * len(YEARS) assert not an.duplicated(["iso3", "year"]).any() # --- estimates ymean = an.groupby("year")["life_expectancy"].mean() m00, m22 = float(ymean[2000]), float(ymean[2022]) base = d_obs[d_obs.year == 2022] assert base.iso3.is_unique m22_all = float(base.life_expectancy.mean()) n_tot = int(d.iso3.nunique()) res = { "task_id": "F1-conf-02", "n_countries_total": n_tot, "n_countries_balanced": len(balanced), "n_countries_incomplete": n_tot - len(balanced), "balanced_countries": balanced, "n_analysed": int(len(an)), "mean_2000_balanced": round(m00, 6), "mean_2022_balanced": round(m22, 6), "change_2000_2022_balanced": round(m22 - m00, 6), "mean_2022_all_available": round(m22_all, 6), "n_countries_2022_all_available": int(base.iso3.nunique()), "incomplete_countries": sorted(set(d.iso3) - set(balanced)), "balanced_mean_by_year": {str(y): round(float(v), 6) for y, v in ymean.items()}, "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"life_expectancy_panel.csv": int(n_raw)}, "random_seed": None, } os.makedirs(OUT, exist_ok=True) an.to_csv(f"{OUT}/analytic.csv", index=False, float_format="%.3f") pd.DataFrame(ledger).to_csv(f"{OUT}/exclusions.csv", index=False) with open(f"{OUT}/results.json", "w") as f: json.dump(res, f, indent=2, ensure_ascii=False) print(f"balanced countries: {len(balanced)} {balanced}") print(f"mean 2000 = {res['mean_2000_balanced']}, mean 2022 = {res['mean_2022_balanced']}, " f"change = {res['change_2000_2022_balanced']}; 2022 all-available = {res['mean_2022_all_available']}")