mebench-viewer / scripts /build_data.py
timchen0618's picture
Add MEBench viewer (Corpus + Eval tabs; train/test/single_test splits)
22dd326 verified
Raw
History Blame Contribute Delete
9.79 kB
#!/usr/bin/env python3
"""Build viewer data for the MEBench viewer.
MEBench (https://github.com/tl2309/MEBench, HF dataset Tim999999/MEBench) is a
cross-document multi-entity QA benchmark. It ships **questions only** — for each
of 3 splits (train / test / single_test) a JSONL of
``{qid, topic, edge, properties, type, question, answer, ...}`` over 7 "topics"
(university associations: Ivy League, Group of Eight, ...). The actual *corpus*
in MEBench is each entity's Wikipedia intro paragraph, generated live by the
pipeline and **not shipped**.
This script therefore reconstructs the corpus from the entities that appear in
the dataset (the ``Entity`` field of the test + single_test splits, which
together cover all 7 topics), fetching each entity's Wikipedia intro. It writes:
corpus.json list[{title, topics, size, wiki_url, file}] (shared, split-independent)
corpus/<slug>.txt one Wikipedia-intro shard per unique entity (lazy-loaded)
eval_<split>.json the split's questions, projected + supporting_titles resolved
sets.json manifest of the 3 splits (counts, types, file pointers)
Wikipedia intros are cached to ``<data_dir>/wiki_cache.jsonl`` so re-runs are cheap.
Run from the viewer repo root:
python scripts/build_data.py \
--data-dir /mnt/ramdisk/blobstore/timchen0618/data/mebench
"""
import argparse
import json
import os
import re
import time
import shutil
try:
import requests
except ImportError:
requests = None
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_DATA_DIR = "/mnt/ramdisk/blobstore/timchen0618/data/mebench"
SPLITS = ["train", "test", "single_test"]
# type (normalized) -> reasoning category (paper: 3 categories, 8 types)
TYPE_CATEGORY = {
"Intercomparison": "Comparative Reasoning",
"Superlative": "Comparative Reasoning",
"Aggregation": "Statistical Reasoning",
"Distribution Compliance": "Statistical Reasoning",
"Correlation Analysis": "Statistical Reasoning",
"Variance Analysis": "Statistical Reasoning",
"Descriptive Relationship": "Relational Reasoning",
"Hypothetical Scenarios": "Relational Reasoning",
}
def norm_type(t):
t = (t or "").strip()
if t == "Hypthetical Scenarios": # dataset typo
return "Hypothetical Scenarios"
return t
def norm_entity(e):
return (e or "").strip().replace("_", " ").strip()
def slugify(idx, title):
safe = re.sub(r"[^0-9A-Za-z._-]+", "_", title).strip("_")[:80] or "doc"
return f"{idx:04d}_{safe}"
def load_jsonl(path):
with open(path, encoding="utf-8") as f:
return [json.loads(l) for l in f if l.strip()]
# ----------------------- Wikipedia corpus -----------------------
def load_cache(cache_path):
cache = {}
if os.path.exists(cache_path):
for r in load_jsonl(cache_path):
cache[r["title"]] = r["extract"]
return cache
def fetch_wikipedia_intros(titles, cache_path):
"""Return {title: intro_text}. Batched (20/req), cached, redirects resolved."""
cache = load_cache(cache_path)
todo = [t for t in titles if t not in cache]
if todo and requests is None:
raise SystemExit("`requests` is required to fetch Wikipedia intros")
session = requests.Session() if todo else None
if session:
session.headers.update({"User-Agent": "mebench-viewer/1.0 (dataset viewer; research)"})
fout = open(cache_path, "a", encoding="utf-8") if todo else None
for i in range(0, len(todo), 20):
batch = todo[i:i + 20]
params = {
"action": "query", "prop": "extracts", "exintro": 1,
"explaintext": 1, "exlimit": 20, "redirects": 1,
"format": "json", "titles": "|".join(batch),
}
got = {}
try:
data = session.get("https://en.wikipedia.org/w/api.php",
params=params, timeout=60).json()
# map redirected/normalized titles back to what we requested
alias = {}
for n in data.get("query", {}).get("normalized", []):
alias[n["to"]] = n["from"]
for rd in data.get("query", {}).get("redirects", []):
alias[rd["to"]] = alias.get(rd["from"], rd["from"])
for pg in data.get("query", {}).get("pages", {}).values():
ret = pg.get("title", "")
req = alias.get(ret, ret)
got[req] = pg.get("extract", "") or ""
except Exception as e:
print(" wiki batch error:", str(e)[:120])
for t in batch:
ex = got.get(t, "")
cache[t] = ex
fout.write(json.dumps({"title": t, "extract": ex}, ensure_ascii=False) + "\n")
fout.flush()
time.sleep(0.2)
if fout:
fout.close()
return {t: cache.get(t, "") for t in titles}
def build_corpus(data_dir):
te = load_jsonl(os.path.join(data_dir, "test.jsonl"))
si = load_jsonl(os.path.join(data_dir, "single_test.jsonl"))
ent_topics = {}
for d in te + si:
e = norm_entity(d.get("Entity", ""))
if not e:
continue
ent_topics.setdefault(e, set()).add(d["topic"].strip())
titles = sorted(ent_topics)
print(f"corpus: {len(titles)} unique entities across "
f"{len({t for ts in ent_topics.values() for t in ts})} topics")
intros = fetch_wikipedia_intros(titles, os.path.join(data_dir, "wiki_cache.jsonl"))
shard_dir = os.path.join(ROOT, "corpus")
if os.path.isdir(shard_dir):
shutil.rmtree(shard_dir)
os.makedirs(shard_dir)
index_rows = []
missing = 0
for i, title in enumerate(titles):
content = intros.get(title, "").strip()
if not content:
missing += 1
content = "(No Wikipedia intro found for this entity.)"
fname = slugify(i, title) + ".txt"
with open(os.path.join(shard_dir, fname), "w", encoding="utf-8") as f:
f.write(content)
index_rows.append({
"title": title,
"topics": sorted(ent_topics[title]),
"size": len(content),
"wiki_url": "https://en.wikipedia.org/wiki/" + title.replace(" ", "_"),
"file": f"corpus/{fname}",
})
index_rows.sort(key=lambda d: d["title"].lower())
with open(os.path.join(ROOT, "corpus.json"), "w", encoding="utf-8") as f:
json.dump(index_rows, f, ensure_ascii=False)
print(f"corpus: wrote {len(index_rows)} shards "
f"({missing} without a Wikipedia intro), "
f"{sum(r['size'] for r in index_rows)/1e6:.2f}MB text")
return {r["title"] for r in index_rows}, ent_topics
# ----------------------- eval splits -----------------------
def build_eval(data_dir, corpus_titles):
manifest = []
for split in SPLITS:
rows = load_jsonl(os.path.join(data_dir, f"{split}.jsonl"))
out = []
types = {}
for r in rows:
topic = (r.get("topic") or "").strip()
typ = norm_type(r.get("type"))
types[typ] = types.get(typ, 0) + 1
edge = r.get("edge")
if isinstance(edge, list):
edge = ", ".join(edge)
entity = norm_entity(r.get("Entity", ""))
# supporting docs: the specific entity if present & known. Otherwise
# the question ranges over the whole topic (train is topic-level) —
# we flag it and let the UI derive members from corpus.json (which
# tags every entity with its topics) rather than duplicating the
# (up to ~168-entry) member list on every question.
if entity and entity in corpus_titles:
supporting = [entity]
topic_level = False
else:
supporting = []
topic_level = True
row = {
"qid": r.get("qid"),
"topic": topic,
"type": typ,
"category": TYPE_CATEGORY.get(typ, ""),
"question": (r.get("question") or "").strip(),
"answer": r.get("answer"),
"edge": (edge or "").strip(),
"properties": (r.get("properties") or "").strip(),
"supporting_titles": supporting,
"topic_level": topic_level,
}
if r.get("sql"):
row["sql"] = r["sql"].strip()
if r.get("class"):
row["class"] = r["class"].strip()
if r.get("Hops"):
row["hops"] = r["Hops"].strip()
if entity:
row["entity"] = entity
out.append(row)
out.sort(key=lambda d: (d["topic"], d.get("qid") or 0))
path = os.path.join(ROOT, f"eval_{split}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False)
manifest.append({
"split": split,
"n_questions": len(out),
"topics": sorted({d["topic"] for d in out}),
"types": dict(sorted(types.items(), key=lambda kv: -kv[1])),
"eval_file": f"eval_{split}.json",
})
print(f"[{split}] questions={len(out)} "
f"({os.path.getsize(path)/1e6:.2f}MB)")
with open(os.path.join(ROOT, "sets.json"), "w", encoding="utf-8") as f:
json.dump(manifest, f, ensure_ascii=False, indent=2)
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--data-dir", default=DEFAULT_DATA_DIR,
help="dir with train/test/single_test.jsonl (+ wiki_cache.jsonl)")
args = ap.parse_args()
corpus_titles, ent_topics = build_corpus(args.data_dir)
build_eval(args.data_dir, corpus_titles)
if __name__ == "__main__":
main()