"""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.""" import json, os import pandas as pd SRC = "data/life_expectancy_panel.csv" OUT = "out" YEARS = list(range(2000, 2023)) os.makedirs(OUT, exist_ok=True) raw = pd.read_csv(SRC, dtype={"iso3": str, "country_name": str}) n_raw = len(raw) for c in ["iso3", "country_name"]: raw[c] = raw[c].str.strip() issues, excl = [], [] def step(name, rule, before, after, why): excl.append(dict(step=name, rule=rule, n_before=before, n_after=after, n_removed=before - after, rationale=why)) # Step 1: duplicate (iso3, year) rows dup_mask = raw.duplicated(["iso3", "year"], keep=False) dups = raw[dup_mask] conflict = dups.groupby(["iso3", "year"])["life_expectancy"].nunique(dropna=False) if (conflict > 1).any(): raise SystemExit(f"Conflicting duplicate values: {conflict[conflict > 1]}") for iso, g in dups.groupby("iso3"): names = sorted(g.country_name.unique()) issues.append({"issue": f"{iso} appears twice for {g.year.nunique()} years under names {names} " f"(assembly of two extractions); duplicate rows have identical values", "handling": "kept one row per (iso3, year); identical values so no choice of value needed; " "country_name set to the most recent spelling 'Turkiye' for TUR"}) # canonical name: prefer 'Turkiye' for TUR; otherwise the single name canon = {iso: sorted(g.country_name.unique())[-1] for iso, g in raw.groupby("iso3")} if "TUR" in canon: canon["TUR"] = "Turkiye" if "Turkiye" in set(raw.loc[raw.iso3 == "TUR", "country_name"]) else canon["TUR"] raw["country_name"] = raw.iso3.map(canon) d = raw.drop_duplicates(["iso3", "year"]).copy() step("1_dedup", "drop duplicate (iso3, year) rows (identical values)", n_raw, len(d), "each country contributes at most one observation per year; duplicates came from assembling two extractions") # Step 2: rows with no recorded value miss = d[d.life_expectancy.isna()] for _, r in miss.iterrows(): issues.append({"issue": f"{r.iso3} {int(r.year)} has an empty life_expectancy cell", "handling": "treated as missing (no imputation); country therefore excluded from balanced set"}) n_b = len(d) d = d[d.life_expectancy.notna()].copy() step("2_missing_value", "drop rows with empty life_expectancy", n_b, len(d), "an empty cell means no recorded value; no imputation allowed") # Step 3: keep only countries with a value in every year 2000-2022 d = d[d.year.isin(YEARS)] cover = d.groupby("iso3").year.nunique() balanced = sorted(cover[cover == len(YEARS)].index.tolist()) all_iso = sorted(raw.iso3.unique()) incomplete = [i for i in all_iso if i not in balanced] for iso in incomplete: yrs = sorted(d.loc[d.iso3 == iso, "year"].unique()) missing_years = [y for y in YEARS if y not in yrs] if len(missing_years) > 1 and missing_years == list(range(missing_years[0], missing_years[-1] + 1)): desc = f"{missing_years[0]}-{missing_years[-1]}" else: desc = ",".join(map(str, missing_years)) issues.append({"issue": f"{iso} has no recorded value for year(s) {desc} (no row or empty cell)", "handling": "excluded from balanced panel; still counted in n_countries_total and, if it has a 2022 value, in the all-available 2022 baseline"}) n_b = len(d) ana = d[d.iso3.isin(balanced)].sort_values(["iso3", "year"])[["iso3", "country_name", "year", "life_expectancy"]] step("3_balanced_panel", "keep countries with a recorded value in every year 2000-2022", n_b, len(ana), f"balanced-panel estimand; incomplete countries: {incomplete}") ana.to_csv(f"{OUT}/analytic.csv", index=False) pd.DataFrame(excl).to_csv(f"{OUT}/exclusions.csv", index=False) m = ana.groupby("year").life_expectancy.mean() assert ana.groupby("year").iso3.nunique().eq(len(balanced)).all() m2000, m2022 = float(m.loc[2000]), float(m.loc[2022]) base = d[d.year == 2022].drop_duplicates("iso3") res = { "task_id": "F1-conf-02", "n_countries_total": len(all_iso), "n_countries_balanced": len(balanced), "n_countries_incomplete": len(incomplete), "balanced_countries": balanced, "n_analysed": int(len(ana)), "mean_2000_balanced": round(m2000, 6), "mean_2022_balanced": round(m2022, 6), "change_2000_2022_balanced": round(m2022 - m2000, 6), "mean_2022_all_available": round(float(base.life_expectancy.mean()), 6), "n_countries_2022_all_available": int(base.iso3.nunique()), "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"life_expectancy_panel.csv": int(n_raw)}, "random_seed": None, } with open(f"{OUT}/results.json", "w") as f: json.dump(res, f, indent=2) print(json.dumps({k: v for k, v in res.items() if k != "data_issues"}, indent=2))