Stratified Sampling & Calibration
Sample by segment; calibrate with labeled validation sets.
Stratified Sampling & Calibration is a free Claude Architect lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Claude Architect learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Aggregate Accuracy Lies
You ship a Claude extraction pipeline and report 97% accuracy. Leadership approves full automation. Three weeks later, every refund invoice with a foreign-currency line is wrong.
The headline number was real — but it was an average. Aggregate accuracy can hide catastrophic failure on a specific document type or a specific field, because the common cases drown out the rare ones.
This lesson is about the discipline that keeps you honest before you automate: stratified sampling to measure where you actually fail, and calibration on labeled validation sets so your confidence scores mean something.
The Core Rule
Memorize the exam-level principle from the oversight domain:
Aggregate accuracy can hide poor performance on a specific document type or field. Use stratified random sampling plus field-level confidence calibrated on labeled validation sets before automating.
Two moves, in order:
- Stratify, then sample — partition the population into segments, sample within each, so rare-but-critical slices are actually measured.
- Calibrate confidence — make the model's per-field confidence correspond to real-world correctness, proven against ground-truth labels.
Skip either step and you are guessing, not governing.
Why Random Sampling Isn't Enough
Plain random sampling oversamples whatever is common. If 95% of your invoices are simple single-currency receipts, a random sample of 200 will be ~190 easy cases and maybe a handful of the hard foreign-currency ones — too few to detect a 40% error rate on that slice.
Stratified sampling fixes this: define segments that matter (document type, language, vendor, field presence), then draw a sample from each segment. Now the rare slice is measured with enough statistical power to fail loudly if it's broken.
You measure the population you're worried about, not the population that happens to be biggest.
Choosing Your Strata
Good strata isolate the dimensions where behavior plausibly diverges. For a structured-extraction pipeline (Scenario 6), useful segments include:
- Document type — invoice vs. receipt vs. statement.
- Field — totals, dates, currency, line items. Field-level matters because
97%overall can mask60%on the currency field. - Edge conditions — multi-page docs, scanned/low-quality, multilingual, or records where an optional field is absent.
That last one ties to a schema rule: never mark a possibly-absent field as required, or the model fabricates it. Stratify by presence/absence so you catch fabrication.
Drawing a Stratified Sample
Concretely, group your validation pool by segment and pull a fixed quota from each — enough per stratum to trust the per-stratum metric, not proportional to the population.
import random
from collections import defaultdict
# Each record carries the dimensions we stratify on.
def segment_key(rec):
return (rec["doc_type"], rec["has_currency_line"], rec["is_multilingual"])
buckets = defaultdict(list)
for rec in validation_pool:
buckets[segment_key(rec)].append(rec)
# Fixed quota per stratum -> rare slices get real coverage,
# not just a couple of incidental samples.
PER_STRATUM = 40
sample = []
for key, recs in buckets.items():
random.shuffle(recs)
sample.extend(recs[:PER_STRATUM])
print({k: min(len(v), PER_STRATUM) for k, v in buckets.items()})Field-Level Confidence, Not Just a Verdict
To calibrate, the model has to emit a confidence per field, not one blanket score. Force structured output so confidence is a typed field you can audit — not prose you parse.
Use tool_choice to guarantee the model returns the schema. "any" forces it to call some tool; {"type":"tool","name":"X"} forces a specific one. A JSON Schema eliminates syntax errors and enforces required fields.
extract_tool = {
"name": "emit_extraction",
"description": "Return extracted fields, each with a per-field confidence in [0,1].",
"input_schema": {
"type": "object",
"properties": {
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string"},
"value": {"type": "string"},
"confidence": {"type": "number"}
},
"required": ["name", "value", "confidence"]
}
}
},
"required": ["fields"]
}
}
resp = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=[extract_tool],
tool_choice={"type": "tool", "name": "emit_extraction"},
messages=[{"role": "user", "content": invoice_text}],
)What Calibration Actually Means
A model is calibrated when its stated confidence matches observed accuracy: of all fields it labels 0.90 confident, about 90% should be correct against ground truth.
Raw model confidence is usually over-confident — it says 0.95 but is right 70% of the time. You can't trust an auto-accept threshold built on uncalibrated scores; you'd auto-approve wrong data.
Calibration requires labeled validation sets: human-verified ground truth for your stratified sample. There is no shortcut — the model's self-rated confidence alone is exactly the kind of signal the exam tells you NOT to trust for automated decisions.
Measuring Calibration with Labels
Bucket predictions by stated confidence, then compare each bucket's stated confidence to its actual accuracy on the labeled set. The gap is your calibration error — and you compute it per stratum so a well-calibrated 'easy' slice can't mask a broken 'hard' one.
from collections import defaultdict
# preds: list of {stratum, confidence, predicted, ground_truth}
bins = defaultdict(lambda: {"n": 0, "correct": 0, "conf_sum": 0.0})
for p in preds:
b = round(p["confidence"], 1) # 0.0..1.0 buckets
key = (p["stratum"], b)
bins[key]["n"] += 1
bins[key]["conf_sum"] += p["confidence"]
bins[key]["correct"] += int(p["predicted"] == p["ground_truth"])
for (stratum, b), s in sorted(bins.items()):
stated = s["conf_sum"] / s["n"]
actual = s["correct"] / s["n"]
print(stratum, b, f"stated={stated:.2f} actual={actual:.2f} gap={stated-actual:+.2f}")Setting Auto-Accept Thresholds Per Segment
Once calibrated, derive a confidence threshold for each stratum that meets your accuracy bar — say, auto-accept only where calibrated accuracy ≥ 99%. The threshold will differ by segment: easy single-currency invoices might auto-accept at 0.85, while foreign-currency lines need 0.98 — or stay human-reviewed entirely.
This is the bridge from measurement to oversight: low-confidence or low-accuracy strata route to a human; high-confidence calibrated strata automate. A single global threshold would either over-trust the hard slice or needlessly gate the easy one.
Self-Correction Strengthens the Signal
Confidence improves when the model can cross-check itself. For numeric extraction, have it emit both a calculated_total (sum of line items) and the stated_total printed on the document, then flag any discrepancy. A mismatch is a hard, deterministic signal — far more reliable than a self-rated confidence number.
Pair this with provenance: keep each claim's source (doc name, quote, page) so a flagged field can be traced and verified, not just re-guessed.
verify_schema = {
"name": "emit_totals",
"description": "Extract both the computed and printed total so discrepancies are detectable.",
"input_schema": {
"type": "object",
"properties": {
"line_items": {"type": "array", "items": {"type": "number"}},
"calculated_total": {"type": "number"},
"stated_total": {"type": "number"},
"source_quote": {"type": "string"}
},
"required": ["calculated_total", "stated_total", "source_quote"]
}
}
# Deterministic check beats trusting a self-rated score:
# discrepancy = abs(out['calculated_total'] - out['stated_total']) > 0.01Run the Audit Off the Critical Path
Stratified calibration audits are large, non-blocking jobs — exactly what the Message Batches API is for: 50% cheaper, up to a 24-hour window, no latency SLA. Run your overnight calibration sweep as a batch; correlate results with custom_id and re-submit only the failures.
The boundary matters for the exam: never use the Batch API for blocking or time-sensitive checks (it has no latency SLA and does not support multi-turn tool calling). A pre-merge gate or a live extraction stays on the synchronous API; the periodic audit goes to batch.
requests = [
{
"custom_id": f"val-{rec['id']}",
"params": {
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"tools": [extract_tool],
"tool_choice": {"type": "tool", "name": "emit_extraction"},
"messages": [{"role": "user", "content": rec["text"]}],
},
}
for rec in stratified_sample
]
batch = client.messages.batches.create(requests=requests)
# Overnight audit: no latency SLA, 50% cheaper. NOT for pre-merge gates.Quick Check: Approving Automation
Apply the rule to a real go/no-go decision.
Recap: Measure Before You Trust
Key takeaways for the exam and for production:
- Aggregate accuracy hides per-type and per-field failure — never automate on a headline average alone.
- Stratified random sampling partitions by the dimensions that matter (doc type, field, edge conditions) and samples each, giving rare slices real statistical power.
- Calibration aligns stated confidence with observed accuracy, proven on a labeled validation set; raw model confidence is over-confident and self-rated scores are not a trustworthy automation signal.
- Set auto-accept thresholds per segment; automate calibrated high-accuracy strata, route the rest to humans.
- Strengthen the signal with self-correction (calculated vs. stated total) and provenance; run the heavy calibration audit on the Batch API — never on a blocking, time-sensitive path.
Frequently asked questions
Is the “Stratified Sampling & Calibration” lesson free?
Yes — the full text of “Stratified Sampling & Calibration” is free to read here on the web, and the Claude Architect course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Claude Architect course, upgrade to CoddyKit PRO.
What will I learn in “Stratified Sampling & Calibration”?
Sample by segment; calibrate with labeled validation sets. You practise Claude Architect with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Claude Architect?
No prior experience is required. Claude Architect on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Stratified Sampling & Calibration” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Claude Architect lesson?
Yes. Every Claude Architect lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Claim to Source Mappings
- Conflicting Data & Dates
- Aggregate Metrics Hide Failures
- Stratified Sampling & Calibration