| """Housing Statute QA (reglab/housing_qa), New York subset, with and |
| without retrieval. Metrics: yes/no accuracy and retrieval hit rate. |
| |
| Usage: python run_housingqa.py --mode rag --k 5 |
| """ |
| import argparse, os, re |
| from bench_common import (Retriever, generate, context_block, norm_ws, |
| save_outputs, SYSTEM_PREAMBLE, NORAG_PREAMBLE) |
|
|
| STATE = "NY" |
| SCRATCH = os.environ.get("SCRATCH", ".") |
|
|
|
|
| def chunk_text(text, size=1500, overlap=150): |
| out, start = [], 0 |
| while start < len(text): |
| out.append(text[start:start + size]) |
| start += size - overlap |
| return out |
|
|
|
|
| def load_data(): |
| """Download the questions and statutes files and filter both to NY. |
| (datasets>=3 can't load this old-style script dataset directly.)""" |
| import json, zipfile |
| import pandas as pd |
| from huggingface_hub import hf_hub_download |
|
|
| def is_target_state(v): |
| return str(v).strip().lower() in {"ny", "new york"} |
|
|
| qzip = hf_hub_download("reglab/housing_qa", "data/questions.json.zip", |
| repo_type="dataset") |
| with zipfile.ZipFile(qzip) as z: |
| name = [n for n in z.namelist() if n.endswith(".json")][0] |
| all_qs = json.loads(z.read(name).decode("utf-8")) |
| qs = [q for q in all_qs if is_target_state(q.get("state"))] |
| if not qs: |
| seen = sorted({str(q.get("state")) for q in all_qs})[:60] |
| raise RuntimeError(f"no questions matched state {STATE}; " |
| f"distinct state values in data: {seen}") |
|
|
| szip = hf_hub_download("reglab/housing_qa", "data/statutes.tsv.zip", |
| repo_type="dataset") |
| kept, seen_states = [], set() |
| for chunk in pd.read_csv(szip, sep="\t", chunksize=200_000, |
| dtype=str, on_bad_lines="skip"): |
| seen_states.update(chunk["state"].dropna().unique().tolist()) |
| kept.append(chunk[chunk["state"].map(is_target_state)]) |
| statutes = pd.concat(kept, ignore_index=True) |
| if not len(statutes): |
| raise RuntimeError(f"no statutes matched state {STATE}; " |
| f"distinct values: {sorted(seen_states)[:60]}") |
| print(f"{len(qs)} {STATE} questions, {len(statutes)} {STATE} statutes") |
|
|
| passages = [] |
| for _, s in statutes.iterrows(): |
| text = s.get("text") or "" |
| if not isinstance(text, str) or not text.strip(): |
| continue |
| for c in chunk_text(text): |
| passages.append({"text": c, "id": str(s["citation"]), |
| "statute_idx": str(s["idx"])}) |
| print(f"{len(passages)} statute chunks") |
| return qs, passages |
|
|
|
|
| def parse_yesno(response): |
| m = re.search(r"\b(yes|no)\b", response.lower()) |
| return m.group(1) if m else "unparsed" |
|
|
|
|
| def main(): |
| ap = argparse.ArgumentParser() |
| ap.add_argument("--mode", choices=["rag", "norag"], required=True) |
| ap.add_argument("--k", type=int, default=5) |
| ap.add_argument("--limit", type=int, default=0, |
| help="debug only; leave 0 for the full NY set") |
| args = ap.parse_args() |
|
|
| qs, passages = load_data() |
| if args.limit: |
| qs = qs[:args.limit] |
| print(f"DEBUG LIMIT: {args.limit} questions") |
|
|
| retriever = None |
| if args.mode == "rag": |
| retriever = Retriever(passages, f"{SCRATCH}/emb/housingqa_{STATE}.npy") |
|
|
| samples, n_correct, n_hit = [], 0, 0 |
| for q in qs: |
| instruction = (f"Answer with Yes or No first, then one sentence of " |
| f"support.\n\nQuestion (state: {STATE}): {q['question']}") |
| gold_cites = [st["citation"] for st in q.get("statutes", [])] |
| if args.mode == "rag": |
| hits = retriever.search(q["question"], k=args.k) |
| block = f"Context:\n{context_block(hits)}\n\n{instruction}" |
| resp = generate(block, SYSTEM_PREAMBLE) |
| retrieved = [{"id": h["id"], "score": s} for h, s in hits] |
| hit = any(norm_ws(h["id"]) in {norm_ws(c) for c in gold_cites} |
| for h, _ in hits) |
| else: |
| resp = generate(instruction, NORAG_PREAMBLE) |
| retrieved, hit = [], False |
| pred = parse_yesno(resp) |
| correct = pred == q["answer"].lower() |
| n_correct += correct |
| n_hit += hit |
| samples.append({"idx": q["idx"], "question": q["question"], |
| "gold": q["answer"], "pred": pred, "correct": correct, |
| "gold_citations": gold_cites, "retrieved": retrieved, |
| "retrieval_hit": hit, "response": resp}) |
|
|
| metrics = {"accuracy": n_correct / len(samples)} |
| if args.mode == "rag": |
| metrics["retrieval_hit_rate"] = n_hit / len(samples) |
| save_outputs(f"{SCRATCH}/results", "housingqa", args.mode, metrics, |
| samples, extra={"state": STATE, "k": args.k}) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|