The Kim & Lu replication: CARS move-steps in article introductions
What it is. Sentences from 50 BioRxiv article introductions, each labeled with a Swales CARS Move (1–3) and Step (a–d) — the same scheme Kim & Lu (2024) used.
Difficulty of the labeling judgment: ★★★ — hard. The dataset’s own expert agreement is only κ ≈ 0.43.
License: CC BY 4.0 Cite: Lam & Nnamoko (2025), Mendeley Data, doi:10.17632/kwr9s5c4nk.1
Every dataset in this course is reshaped into the same canonical schema so one notebook works for all of them:
[{"id":1,"text":"...","label":"..."}]
The raw data, though, looks different every time. That difference is the lesson — half of building a gold standard is getting messy real data into a clean, consistent shape.
Step 1 — Download the raw data
This dataset is on Mendeley Data as 50 separate XML files. There is no single zip, so we ask Mendeley’s public API for the list of files and download each one. (You don’t need to understand every line — read it as: get the file list, then loop and download.)
Show code
import urllib.request, json, os, time# Mendeley needs a normal browser User-Agent, so we add one to each request.def fetch(url): req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})return urllib.request.urlopen(req, timeout=60)os.makedirs("cars50", exist_ok=True)meta = json.load(fetch("https://data.mendeley.com/public-api/datasets/kwr9s5c4nk"))for f in meta["files"]: url = f["content_details"]["download_url"] dest = os.path.join("cars50", f["filename"])for attempt inrange(3): # retry on the occasional dropped connectiontry:with fetch(url) as resp, open(dest, "wb") as out: out.write(resp.read())breakexceptException: time.sleep(2)print("downloaded", len(os.listdir("cars50")), "XML files")
Step 2 — Look at the raw format
Each file is XML. Inside, every sentence is a <sentence> element holding a <text> and a <step> code like 1b (Move 1, Step b). Let’s print the start of one file.
Show code
withopen("cars50/text001.xml", encoding="utf-8") as f:print(f.read()[:900])
Step 3 — Reshape into the canonical schema
We parse the XML and pull out each sentence’s text and code. Decision: for a first pass we use just the Move (the leading digit of the code) as the label — 3 classes instead of 11. (The full Move+Step is a stretch goal.)
Show code
import globimport xml.etree.ElementTree as ETrows = []for path in glob.glob("cars50/*.xml"): tree = ET.parse(path)for sentence in tree.iter("sentence"): text_el = sentence.find("text") step_el = sentence.find("step")if text_el isNoneor step_el isNone:continue text = (text_el.text or"").strip() code = (step_el.text or"").strip()if text and code and code[0].isdigit(): rows.append({"text": text, "label": f"Move {code[0]}"})print("sentences:", len(rows))
Step 4 — Inspect the labels
Show code
from collections import Countercounts = Counter(item["label"] for item in rows)print("total items:", len(rows))print("label counts:", dict(counts))rows[:3] # peek at the first three reshaped items
Step 5 — Build a balanced gold set
Three moves × 20 = 60 items.
Show code
# Build a small BALANCED gold set: an equal number of items per label.# Balance matters so precision/recall/F1 and the confusion matrix are meaningful.import randomfrom collections import defaultdictPER_LABEL =20# how many items per labelrandom.seed(42) # fixed seed = same sample every run (reproducible)by_label = defaultdict(list)for item in rows: by_label[item["label"]].append(item)gold = []for label insorted(by_label): bucket = by_label[label] random.shuffle(bucket) gold.extend(bucket[:PER_LABEL])random.shuffle(gold)gold = [{"id": i +1, "text": x["text"], "label": x["label"]} for i, x inenumerate(gold)]from collections import Counterprint("items:", len(gold), "| per label:", dict(Counter(x["label"] for x in gold)))
Step 6 — Save it
Show code
OUT_FILE ="cars50_moves.json"
Show code
import jsonwithopen(OUT_FILE, "w", encoding="utf-8") as f: json.dump(gold, f, ensure_ascii=False, indent=2)print(f"Saved {len(gold)} items to {OUT_FILE}")gold[:3] # preview the first three items
You built cars50_moves.json — the open stand-in for Kim & Lu’s data. Use it in the Day 3 tutorial.
Stretch: change the label to the full code (code instead of f"Move {code[0]}") for the 11-class Move+Step task and watch accuracy drop.