SPB Git

spb/wp29_uqo

Public
12.9 KB · 226 lines python
1"""F4-conf-01: extract printed treatment effects/uncertainty from two source extracts.2Reads only data/; writes out/results.json deterministically. No randomness (random_seed = null).3Every extracted value is parsed from the text and the matched source line is printed."""4import json, re, os, hashlib5from pathlib import Path67ROOT = Path(__file__).resolve().parent.parent8DATA = ROOT / "data"9DCR = DATA / "sources" / "arxiv-2609.27732v1.txt"10RI = DATA / "sources" / "arxiv-2606.14621v1.txt"11DCR_ID, RI_ID = "arxiv-2609.27732v1", "arxiv-2606.14621v1"12Z = 1.96  # normal critical value for two-sided 95%1314def lines(p):15    return p.read_text(encoding="utf-8").split("\n")1617def num(s):18    """Parse a printed number keeping its printed decimals (e.g. '-0.431**' -> -0.431, '$1,153' -> 1153)."""19    s = s.strip().replace("$", "").replace(",", "").replace("%", "")20    s = re.sub(r"\*+$", "", s).strip()21    s = s.strip("()")22    return int(s) if re.fullmatch(r"-?\d+", s) else float(s)2324def stars(s):25    m = re.search(r"(\*+)\s*$", s.strip()); return len(m.group(1)) if m else 02627def block(ls, start_pat, end_pat):28    i0 = next(i for i, l in enumerate(ls) if re.match(start_pat, l))29    i1 = next(i for i in range(i0 + 1, len(ls)) if re.match(end_pat, ls[i]))30    return i0, ls[i0:i1]3132def parse_reg_table(ls, title_pat, end_pat):33    """Return header columns, {row: {col: (raw_coef, raw_se, lineno)}}, {row: {col: raw}} for non-coef rows."""34    i0, blk = block(ls, title_pat, end_pat)35    rows = [(i0 + k + 1, l) for k, l in enumerate(blk) if l.strip() and " | " in l or l.startswith("|")]36    rows = [(n, l) for n, l in rows if l.strip()]37    header = [c.strip() for c in rows[0][1].split("|")][1:]38    coef, other = {}, {}39    k = 140    while k < len(rows):41        n, l = rows[k]42        cells = [c.strip() for c in l.split("|")]43        label, vals = cells[0], cells[1:]44        nxt = rows[k + 1][1] if k + 1 < len(rows) else ""45        if label and nxt.strip().startswith("|") and "(" in nxt:46            ses = [c.strip() for c in nxt.split("|")][1:]47            coef[label] = {h: (v, s, n, rows[k + 1][0]) for h, v, s in zip(header, vals, ses)}48            k += 249        else:50            other[label] = {h: (v, n) for h, v in zip(header, vals)}51            k += 152    return header, coef, other5354dl = lines(DCR); rl = lines(RI)55h6, t6, o6 = parse_reg_table(dl, r"^Table 6:", r"^Note:")56h7, t7, o7 = parse_reg_table(dl, r"^Table 7:", r"^Note:")57hA, tA, oA = parse_reg_table(dl, r"^Table A19:", r"^Note:")58if h7 and h7[0] == "Outcome":  # Table 7 header line starts with 'Outcome |'59    pass60# Table 7 header has 'Outcome' as first cell rather than empty: re-parse header61_, blk7 = block(dl, r"^Table 7:", r"^Note:")62hdr7 = next(l for l in blk7 if l.startswith("Outcome |"))63h7 = [c.strip() for c in hdr7.split("|")][1:]64_, t7, o7 = (None, *parse_reg_table(dl, r"^Table 7:", r"^Note:")[1:])65# re-key Table 7 entries with the correct header66def rekey(tab, hdr):67    out = {}68    for r, d in tab.items():69        vals = list(d.values())70        out[r] = dict(zip(hdr, vals))71    return out72t7 = rekey(t7, h7); o7 = rekey(o7, h7)7374def show(src, n):75    print(f"  [{src} line {n}] {(dl if src == DCR_ID else rl)[n - 1]}")7677res, loc = {}, {}78def put_coef(key, tab, tabname, row, col):79    v, s, n1, n2 = tab[row][col]80    res[key] = num(v); res[key + "_se"] = num(s)81    loc[key] = f'{DCR_ID} | Table {tabname} | row "{row}" | column "{col}"'82    loc[key + "_se"] = f'{DCR_ID} | Table {tabname} | row "{row}" (standard-error line below, in parentheses) | column "{col}"'83    print(f"{key} = {res[key]} (stars={stars(v)}), {key}_se = {res[key + '_se']}"); show(DCR_ID, n1); show(DCR_ID, n2)84    return stars(v)8586put_coef("dcr_att_5km", t6, "6", "ATT", "5 km")87put_coef("dcr_att_3km", t6, "6", "ATT", "3 km")88v, n = o6["Observations"]["5 km"]89res["dcr_obs_5km"] = num(v)90loc["dcr_obs_5km"] = f'{DCR_ID} | Table 6 | row "Observations" | column "5 km"'91print(f"dcr_obs_5km = {res['dcr_obs_5km']}"); show(DCR_ID, n)92put_coef("dcr_narcotics_att_3km", t7, "7", "Narcotics", "3 km")93put_coef("dcr_fatal_att_1km", t7, "7", "Drug-related (fatal)", "1 km")94hiv_stars = put_coef("dcr_hiv_att_5km", t7, "7", "HIV", "5 km")9596# exact p-value: search the whole DCR extract for any printed p-value97pv_lines = [i + 1 for i, l in enumerate(dl) if re.search(r"\bp\s*=\s*0?\.\d", l)]98print(f"lines in {DCR_ID} printing an exact p-value (p = ...): {pv_lines}")99res["dcr_hiv_att_5km_p_value"] = None100loc["dcr_hiv_att_5km_p_value"] = (f"not reported in {DCR_ID} (Table 7 prints only the coefficient, the clustered SE "101                                  f"and significance stars; no exact p-value is printed anywhere in the extract)")102res["dcr_hiv_att_5km_significant_10pct"] = hiv_stars >= 1103print(f"dcr_hiv_att_5km_significant_10pct = {res['dcr_hiv_att_5km_significant_10pct']} (stars on Table 7 HIV 5 km cell: {hiv_stars})")104105# derived 95% CI from Table 6 printed SE, normal approximation106b, se = res["dcr_att_5km"], res["dcr_att_5km_se"]107res["dcr_att_5km_ci_low"] = round(b - Z * se, 4)108res["dcr_att_5km_ci_high"] = round(b + Z * se, 4)109loc["dcr_att_5km_ci_low"] = f'{DCR_ID} | Table 6 | row "ATT" | column "5 km" | derived (estimate - 1.96 x printed SE)'110loc["dcr_att_5km_ci_high"] = f'{DCR_ID} | Table 6 | row "ATT" | column "5 km" | derived (estimate + 1.96 x printed SE)'111112# ---- RESEA Table 2 ----113_, blk2 = block(rl, r"^\[Table 2", r"^Table 2:")114i2 = next(i for i, l in enumerate(rl) if l.startswith("[Table 2"))115hdr = next(l for l in blk2 if l.startswith("Outcome |"))116H2 = [c.strip() for c in hdr.split("|")]117T2 = {}118for k, l in enumerate(blk2):119    if " | " in l and not l.startswith("Outcome |") and not l.startswith("["):120        cells = [c.strip() for c in l.split("|")]121        T2[cells[0]] = (dict(zip(H2, cells)), i2 + k + 1)122123def parse_effect(cell):124    m = re.match(r"^(-?)\$?(-?[\d,\.]+)\s*(pp|weeks|wks)?;\s*p\s*([=<])\s*([\d\.]+);\s*95% CI:\s*\[([^,\]]+(?:,\d{3})*),\s*([^\]]+)\]$",125                 cell.replace("-$", "-"))126    if not m:127        raise ValueError(cell)128    est = num(m.group(1) + m.group(2))129    # split CI bounds robustly (thousands separators: "166, 2,141")130    ci = cell.split("95% CI:")[1].strip().strip("[]")131    parts = re.findall(r"-?\d{1,3}(?:,\d{3})+|-?\d+(?:\.\d+)?", ci)132    return est, num(parts[0]), num(parts[1]), m.group(4) + m.group(5)133134ATE = "Average treatment effect estimate"135for key, row in [("ri_wage_ate", "Wage"), ("ri_reemployment_ate_pp", "Reemployment"), ("ri_weeks_ui_ate", "Weeks on UI")]:136    d, n = T2[row]137    est, lo, hi, p = parse_effect(d[ATE])138    res[key] = est139    loc[key] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (point estimate)'140    print(f"{key} = {est}; printed 95% CI [{lo}, {hi}]; p{p}"); print(f"  [{RI_ID} line {n}] {rl[n - 1]}")141    if key in ("ri_wage_ate", "ri_weeks_ui_ate"):142        res[key + "_ci_low"], res[key + "_ci_high"] = lo, hi143        loc[key + "_ci_low"] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (95% CI lower bound)'144        loc[key + "_ci_high"] = f'{RI_ID} | Table 2 | row "{row}" | column "{ATE}" (95% CI upper bound)'145d, n = T2["Wage"]146cm = re.match(r"^\$([\d,]+)", d["Control"]).group(1)147res["ri_control_mean_wage"] = num(cm)148loc["ri_control_mean_wage"] = f'{RI_ID} | Table 2 | row "Wage" | column "Control" (mean)'149print(f"ri_control_mean_wage = {res['ri_control_mean_wage']}"); print(f"  [{RI_ID} line {n}] {rl[n - 1]}")150151wage_cell = T2["Wage"][0][ATE]152res["ri_wage_ate_se_reported"] = bool(re.search(r"\(\s*[\d,\.]+\s*\)|\bSE\b|s\.e\.", wage_cell, re.I))153print(f"ri_wage_ate_se_reported = {res['ri_wage_ate_se_reported']} (wage ATE cell: '{wage_cell}')")154lo, hi = res["ri_wage_ate_ci_low"], res["ri_wage_ate_ci_high"]155res["ri_wage_ate_se_derived"] = round((hi - lo) / (2 * Z), 2)156loc["ri_wage_ate_se_derived"] = (f'{RI_ID} | Table 2 | row "Wage" | column "{ATE}" | derived '157                                 f'((CI upper - CI lower) / (2 x 1.96))')158159# ---- consistency checks that feed data_issues ----160issues = []161seA = num(tA["ATT"]["CS-DR"][1])162if seA != res["dcr_att_5km_se"]:163    issues.append({"issue": (f"{DCR_ID}: the same 5 km CS-DR overall ATT ({res['dcr_att_5km']}) is printed with SE "164                             f"{res['dcr_att_5km_se']} in Table 6 but SE {seA} in Appendix Table A19 (column 'CS-DR')."),165                   "handling": "Used Table 6 as the key definitions require (dcr_att_5km_se, derived CI); A19 value not used."})166ctrl6 = num(o6["Control regions"]["5 km"][0])167mA = re.search(r"never-treated comparison regions \((\d+) regions\)", "\n".join(dl))168if mA and int(mA.group(1)) != ctrl6:169    issues.append({"issue": (f"{DCR_ID}: Tables 6/7 and A19 rows report {ctrl6} never-treated control regions, but the "170                             f"Table A19 note says {mA.group(1)} never-treated comparison regions."),171                   "handling": "Recorded only; no requested key depends on the control-region count."})172fatal5 = num(t7["Drug-related (fatal)"]["5 km"][0])173issues.append({"issue": (f"{DCR_ID}: the abstract says fatal hospitalisations 'fall by 35%', whereas Table 7 gives a 5 km "174                         f"fatal ATT of {fatal5} log points (exp({fatal5})-1 = {100 * (2.718281828459045 ** fatal5 - 1):.1f}%), "175                         f"and Section 6.1 says 'approximately 30% at 5 km'; the abstract figure appears to equate log points with percent."),176               "handling": "Recorded only; values were taken from the tables named in the key definitions."})177hiv5 = res["dcr_hiv_att_5km"]178issues.append({"issue": (f"{DCR_ID}: the text describes HIV estimates as 'insignificant and close to zero', but the printed "179                         f"5 km HIV ATT is {hiv5} log points (SE {res['dcr_hiv_att_5km_se']}); it is insignificant (no star) but not numerically close to zero."),180               "handling": "Recorded only; significance flag set from the absence of stars in Table 7."})181issues.append({"issue": (f"{RI_ID}: the Results text states the weeks-on-UI CI as magnitudes '[1.75 weeks, 2.24 weeks]' for a "182                         f"'1.99 week reduction', while Table 2 prints the ATE as -1.99 with CI [-2.24, -1.75]."),183               "handling": "Used the signed Table 2 values as required (lower bound first)."})184if any("¡" in l for l in rl):185    issues.append({"issue": f"{RI_ID}: rendering defect in the Section 3 text: 'p ¡ 0.001' (should read p < 0.001).",186                   "handling": "Not used for any key (Table 2 cell prints 'p<0.001')."})187issues.append({"issue": (f"{RI_ID}: Tables 2 and 3 were re-set by the extract editor into a one-row-per-outcome layout "188                         f"(HTML rendering split rows and detached p-values)."),189               "handling": "Relied on the editor's statement that values are verbatim; Table 3 'Difference in means' column matches Table 2 ATE column exactly (checked)."})190# check Table 3 duplicates Table 2 ATEs191_, blk3 = block(rl, r"^\[Table 3", r"^Table 3:")192t3w = next(l for l in blk3 if l.startswith("Wage |")).split("|")[1].strip()193assert parse_effect(t3w)[:3] == (res["ri_wage_ate"], lo, hi), "Table 3 vs Table 2 wage mismatch"194195def wc_l(p):196    return p.read_bytes().count(b"\n")197198out = {199    "task_id": "F4-conf-01",200    **{k: res[k] for k in ["dcr_att_5km", "dcr_att_5km_se", "dcr_att_3km", "dcr_att_3km_se", "dcr_obs_5km",201                           "dcr_narcotics_att_3km", "dcr_narcotics_att_3km_se", "dcr_fatal_att_1km", "dcr_fatal_att_1km_se",202                           "dcr_hiv_att_5km", "dcr_hiv_att_5km_se", "dcr_hiv_att_5km_p_value",203                           "dcr_hiv_att_5km_significant_10pct", "dcr_att_5km_ci_low", "dcr_att_5km_ci_high",204                           "ri_wage_ate", "ri_wage_ate_ci_low", "ri_wage_ate_ci_high", "ri_wage_ate_se_reported",205                           "ri_wage_ate_se_derived", "ri_control_mean_wage", "ri_reemployment_ate_pp",206                           "ri_weeks_ui_ate", "ri_weeks_ui_ate_ci_low", "ri_weeks_ui_ate_ci_high"]},207    "locators": loc,208    "derivation_notes": {209        "dcr_att_5km_ci": ("95% CI = ATT +/- 1.96 x SE using the Table 6 printed ATT (-0.431) and clustered SE (0.210); "210                           "normal (large-sample) critical value 1.96; rounded to 4 decimals. Assumes asymptotic normality of the "211                           "CS-DR estimator and that the printed SE (itself rounded to 3 decimals) is the relevant SE."),212        "ri_wage_ate_se": ("SE = (upper - lower) / (2 x 1.96) using the Table 2 printed 95% CI [166, 2,141]; normal critical value "213                           "1.96 (with N = 23,549 a t critical value is indistinguishable); rounded to 2 decimals. Assumes the "214                           "printed CI is a symmetric Wald interval (midpoint 1153.5 vs estimate 1,153, consistent up to rounding)."),215    },216    "feasibility": "complete",217    "feasibility_reason": None,218    "data_issues": issues,219    "n_input_rows": {"README.md": wc_l(DATA / "README.md"), DCR.name: wc_l(DCR), RI.name: wc_l(RI)},220    "n_analysed": 25,221    "random_seed": None,222}223os.makedirs(ROOT / "out", exist_ok=True)224(ROOT / "out" / "results.json").write_text(json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")225print("wrote out/results.json")226
Files · main
Menu

Theme