Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Structured Data Extraction

Authors
Affiliations
Johns Hopkins Bloomberg School of Public Health
Johns Hopkins Bloomberg School of Public Health
import json

SCHEMA = {"study_id": str, "n_participants": int, "effect": float, "outcome": str}

candidates = [
    '{"study_id": "S1", "n_participants": 240, "effect": 0.31, "outcome": "mortality"}',
    '{"study_id": "S2", "n_participants": "two hundred", "effect": 0.4, "outcome": "mortality"}',
    '{"study_id": "S3", "n_participants": 88, "effect": 0.12}',
    '```json\n{"study_id": "S4", "n_participants": 51, "effect": 0.9, "outcome": "LOS"}\n```',
]

def parse(raw):
    s = raw.strip()
    if s.startswith("```"):                       # models love fencing their JSON
        s = "\n".join(l for l in s.split("\n") if not l.startswith("```"))
    try:
        obj = json.loads(s)
    except json.JSONDecodeError as e:
        return None, f"unparseable: {e.msg}"
    missing = [k for k in SCHEMA if k not in obj]
    if missing:
        return None, f"missing fields: {missing}"
    for k, typ in SCHEMA.items():
        if typ is float and isinstance(obj[k], int):
            obj[k] = float(obj[k])
        elif not isinstance(obj[k], typ):
            return None, f"field {k!r} is {type(obj[k]).__name__}, expected {typ.__name__}"
    return obj, "ok"

for raw in candidates:
    obj, msg = parse(raw)
    tag = obj["study_id"] if obj else "--"
    print(f"{tag:<4} {msg}")
S1   ok
--   field 'n_participants' is str, expected int
--   missing fields: ['outcome']
S4   ok

0.1Data Extraction

A great deal of the world’s scientific information is locked in prose: clinical notes, pathology reports, published tables, protocol documents, free-text survey responses, and the methods sections of ten thousand papers you would like to meta-analyse. Language models are startlingly good at turning that prose into a rectangular data frame, and this is arguably their highest-value application in research --- it converts a task that used to cost months of trained abstractor time into an afternoon of compute. It is also the application where a statistician’s instincts pay off fastest, because the output of an extraction pipeline is not data. It is a measurement of data, with a sensitivity, a specificity, and an error process that will propagate into every estimate computed downstream. The central message of this section is that this problem has been solved before, under different names: measurement error, misclassification, and two-phase sampling with a validation subsample. If you extract a variable with 90%90\% accuracy and then regress an outcome on it as though it were observed, you have not produced a slightly noisy estimate; you have produced a systematically attenuated one, and you can say by how much.

LLM-based extraction is a measurement-error problem in disguise; each row names the classical machinery that already handles it.

ML / AI termStatistical analogueWhat is the sameWhat is different / the catch
Information extraction / structured outputMeasurement of a latent variable with errorBoth produce a surrogate XX^\ast for an unobserved truth XX; both need the error distribution to be usableThe error process is a black box that can change without warning when the model version changes, so the measurement instrument is not stable over time
Extraction accuracySensitivity and specificity of a classifierThe 2×22\times2 table is the same tableA single “accuracy” number hides the sens/spec split, and only the split determines the direction and magnitude of downstream bias
Human-labelled gold standardValidation subsample in a two-phase designBoth let you estimate the measurement-error model and correct for itGold labels are expensive and themselves imperfect; inter-rater disagreement puts a ceiling on measurable accuracy
Non-differential extraction errorMisclassification independent of the outcomeBoth give the classical attenuation-toward-null resultIf the model reads outcome-related cues in the same note, the error becomes differential and the bias can go in either direction, including away from the null
Structured/JSON-constrained decodingRestricting the parameter space; a constrained estimatorBoth eliminate a class of impossible outputs by constructionIt guarantees the output parses, not that it is correct; a schema-valid wrong value looks exactly like a right one
Confidence score / logprob of an extracted fieldA predicted probability requiring calibrationBoth are only useful if Pr(correctp^)p^\Pr(\text{correct}\mid \hat p) \approx \hat pModel confidence is systematically overconfident and must be recalibrated on a labelled set before it can be used for triage Guo et al., 2017
Abstain / route-to-human thresholdA decision rule with an indifference regionBoth trade coverage against precision through a cut-point on a scoreThe abstention rate is not missing at random: the model abstains on the hardest, most atypical records, which are often the interesting ones
Self-consistency votingMajority rule over correlated ratersBoth aggregate votes into a single readingCorrelation among draws caps the achievable accuracy far below the independent -voting benchmark
Retrieval over a document corpusSampling frame constructionBoth determine which units are eligible to contribute dataDocuments that retrieval fails to surface are missing from the frame entirely, and that missingness correlates with document length and vocabulary
Regression on extracted variablesRegression with a mismeasured covariateBoth need a correction: regression calibration, SIMEX, or multiple imputation Carroll et al., 2006Naive analysis is not conservative in general; with several mismeasured covariates the bias in any one coefficient can be in either direction

0.1.1The pipeline, and where the statistics enter

An extraction pipeline has five stages: acquire documents, segment them into passages, prompt a model for a structured record, validate the record against a schema, and then analyse. Figure the figure draws it with the validation subsample entering where it belongs --- as a designed second phase, not as a spot check bolted on at the end. The single most consequential design decision is to reserve labelling effort for a random validation sample rather than spending all of it on prompt iteration, because the validation sample is what converts an unquantified pipeline into a measurable one. The acquisition stage is usually a document-conversion problem before it is a language problem (GROBID turns scholarly PDFs into structured TEI with the reference list and section boundaries already delimited, and general layout converters such as Docling or Marker do the same job for reports and forms), and where the corpus must itself be assembled by an agent that searches and synthesises, the verification machinery of Section that section applies to the corpus before any extraction runs on it.

Treat extraction as a two-phase design: the model reads every document, a random subsample is also read by a human, and the subsample identifies the measurement-error model that corrects the final estimate.
:width: 90%

Figure 1:Treat extraction as a two-phase design: the model reads every document, a random subsample is also read by a human, and the subsample identifies the measurement-error model that corrects the final estimate. :width: 90%

0.1.2Attenuation: the number every collaborator needs to see

Take the cleanest case. The true binary exposure X{0,1}X \in \{0,1\} has prevalence π\pi, the extracted version XX^\ast has sensitivity se=Pr(X=1X=1)\mathrm{se} = \Pr(X^\ast=1 \mid X=1) and specificity sp=Pr(X=0X=0)\mathrm{sp} = \Pr(X^\ast=0\mid X=0), and errors are non-differential, meaning XYXX^\ast \perp Y \mid X. Write π=πse+(1π)(1sp)\pi^\ast = \pi\, \mathrm{se} + (1-\pi)(1-\mathrm{sp}) for the prevalence of the extracted variable. For the linear model E[YX]=β0+β1X\EX[Y\mid X] = \beta_0 + \beta_1 X, regressing YY on XX^\ast instead estimates

β1  =  β1π(seπ)π(1π)  =  β1λ,0λ1,\beta_1^\ast \;=\; \beta_1 \,\cdot\, \frac{\pi\,(\mathrm{se} - \pi^\ast)}{\pi^\ast (1-\pi^\ast)} \;=\; \beta_1 \,\cdot\, \lambda , \qquad 0 \le \lambda \le 1 ,

so the naive slope is the true slope multiplied by an attenuation factor λ\lambda that depends only on π\pi, sensitivity and specificity. The consequences are worth stating baldly. At π=0.4\pi = 0.4 and se=sp=0.90\mathrm{se}=\mathrm{sp}=0.90 --- an extraction accuracy most people would call excellent --- the estimated effect is about 79%79\% of the truth, and no amount of additional data repairs it: the bias is in the estimand, and more documents only shrink the standard error around the wrong value.

Extraction accuracy that sounds excellent produces estimates that are badly biased: at 90\% accuracy the naive slope recovers only about 79\% of the true effect, while regression calibration using a 300-record validated subsample is approximately unbiased across the whole range at the cost of wider intervals. Points are Monte Carlo means over 300 replicates (n=2000, \pi=0.4, \beta_1=1); the blue line is the closed form the equation; the band is \pm 1 Monte Carlo standard deviation of the corrected estimator.
:width: 90%

Figure 2:Extraction accuracy that sounds excellent produces estimates that are badly biased: at 90%90\% accuracy the naive slope recovers only about 79%79\% of the true effect, while regression calibration using a 300-record validated subsample is approximately unbiased across the whole range at the cost of wider intervals. Points are Monte Carlo means over 300 replicates (n=2000n=2000, π=0.4\pi=0.4, β1=1\beta_1=1); the blue line is the closed form the equation; the band is ±1\pm 1 Monte Carlo standard deviation of the corrected estimator. :width: 90%

0.1.3Validation is a design problem, not a QA step

The temptation is to label a convenience batch of records, quote an accuracy, and proceed. Two design refinements pay for themselves. First, stratify the validation sample: oversample records the model flagged positive, since sensitivity is estimated only from true positives and those are scarce when prevalence is low, then reweight. Second, measure the human ceiling by double-labelling a subset, because an extraction pipeline cannot be shown to exceed an accuracy that your gold standard itself does not attain. (Annotation platforms such as Label Studio or doccano exist to make double-labelling, adjudication and inter-rater statistics a routine part of the design rather than something reconstructed from a spreadsheet afterwards.) A useful supplementary tool is conformal prediction, which converts any confidence score into extraction sets with finite-sample coverage under exchangeability Springer-Verlag, 2005Angelopoulos & Bates, 2021 --- for a categorical field, the model returns a set of candidate values guaranteed to contain the truth 90%90\% of the time, and the singleton sets can be accepted automatically while the rest route to a human. :::{note} Author note Note the exchangeability caveat sharply: documents from a new hospital, a new year, or a new template are not exchangeable with the calibration set, so coverage degrades exactly when you most want it. Cross-reference the uncertainty section rather than repeating the conformal machinery. :::

0.1.4Implementation: schema-constrained extraction

The practical mechanics are simple enough to show in a dozen lines. Define the target record as a typed schema, require the model to emit that schema, and let the parser --- not the prose --- decide whether an output is admissible. Constrained decoding removes parse failures entirely; it removes no factual errors at all. (The constraint is enforced in one of two mechanically different ways --- a grammar-constrained decoder such as outlines, or the GBNF grammars built into llama.cpp, masks off-schema tokens so an invalid record is unreachable, whereas a validate-and-retry wrapper such as instructor re-prompts until the parser is satisfied --- and the difference matters to a statistician, because retrying until acceptance discards the hard documents non-randomly and quietly changes the population your data frame describes.)

class ChartRecord(BaseModel): patient_age: Optional[int] = Field(None, ge=0, le=120) smoking_status: Literal[“never”, “former”, “current”, “unknown”] biopsy_performed: bool evidence_span: str # verbatim quote supporting the fields above

1Ask the model for JSON conforming to ChartRecord.model_json_schema(),

2then parse. A parse failure is a rejected record, not a silent NaN.

record = ChartRecord.model_validate_json(model_output)

2.1Tools in practice

The tooling for extraction splits cleanly along the pipeline of Figure the figure, and the split is worth internalising because each stage has its own error process. Document conversion decides what text the model ever sees; constrained decoding decides what shape the output can take; the serving layer decides whether the instrument is stable enough to re-run; and the annotation layer decides whether you can measure any of it. Two habits are worth adopting before any of these are installed. First, measure the cheap baseline: a rule-based or dictionary-based extractor built with spaCy, or its biomedical variants scispaCy and medspaCy, often recovers structured fields such as dates, dosages and identifiers at an accuracy the language model must then be shown to beat, and it is deterministic and free. Second, remember that none of these tools estimates sensitivity or specificity for you; that number comes only from the validation subsample, and it is the number the rest of this section runs on.

The claim in the callouts that constrained decoding trades a parse-failure rate for a wrong-answer rate is easy to state and easy to get wrong, so measure it. The following replays stored outputs from two runs over the same six documents, one free-form and one schema-constrained, and applies the same admission gate to both. Nothing here calls a model; the responses are fixtures so that the arithmetic is reproducible.

SMOKING = (“never”, “former”, “current”, “unknown”)

def admit(raw): “”“Schema gate: parse, check types and levels. None = rejected.”“” try: r = json.loads(raw) except json.JSONDecodeError: return None ok = (set(r) == {“age”, “smoking”, “biopsy”} and isinstance(r[“age”], int) and 0 <= r[“age”] <= 120 and r[“smoking”] in SMOKING and isinstance(r[“biopsy”], bool)) return r if ok else None

def rec(a, s, b): return json.dumps({“age”: a, “smoking”: s, “biopsy”: b})

gold = [(61, “former”, True), (47, “never”, False), (73, “current”, True), (55, “never”, True), (38, “unknown”, False), (29, “never”, False)]

3Two runs over the same six documents, replayed from stored responses.

free = [rec(61, “former”, True), rec(47, “never”, False), ‘"json\n{"age": 73, ...}\n"’, # fenced: no parse rec(55, “never”, True), rec(38, “unknown”, False), ‘The note gives no age. {“smoking”: “never”}’] # prose: no parse constrained = [rec(61, “former”, True), rec(47, “never”, False), rec(73, “former”, True), # admissible, wrong rec(55, “never”, True), rec(38, “unknown”, False), rec(29, “never”, False)]

def audit(name, outs): keep = [admit(o) for o in outs] n_ok = sum(k is not None for k in keep) n_right = sum(k is not None and tuple(k.values()) == g for k, g in zip(keep, gold)) print(f"{name:12s} admitted {n_ok}/6 correct {n_right}" f" acc|admitted {n_right / n_ok:.2f}" f" per document {n_right / 6:.2f}")

audit(“free-form”, free) audit(“constrained”, constrained)

Read the last two columns against each other. Accuracy conditional on admission is the number a dashboard will show you, and it moves the wrong way: the free-form run looks perfect because every record it failed to parse was silently dropped from the denominator. Accuracy per document is the estimand your downstream analysis actually needs, because a dropped record is a missing value, not an absence of error --- and it is missing precisely on the documents the model found hardest. The general lesson is the one this section keeps making: report the denominator you started with, not the one that survived.

3.1Exercises

  1. Derive the equation from β1=Cov(Y,X)/Var(X)\beta_1^\ast = \mathrm{Cov}(Y, X^\ast)/\mathrm{Var}(X^\ast) under non-differential misclassification. Verify λ=1\lambda = 1 when se=sp=1\mathrm{se}=\mathrm{sp}=1 and λ=0\lambda = 0 when the extracted variable is independent of the truth.

  2. With π=0.2\pi = 0.2, compute the attenuation factor for (se,sp)=(0.95,0.95)(\mathrm{se},\mathrm{sp}) = (0.95,0.95), (0.99,0.90)(0.99,0.90) and (0.90,0.99)(0.90,0.99). Explain why, at low prevalence, specificity matters far more than sensitivity for the bias in β^1\hat\beta_1.

  3. Construct an explicit differential-misclassification example in which the naive estimate is biased away from the null, and describe the feature of an LLM reading a full clinical note that makes this scenario realistic rather than contrived.

  4. You will label NvN_v records for validation. Write the expression for the standard error of se^\widehat{\mathrm{se}} and find the NvN_v needed for a ±0.05\pm 0.05 margin at se=0.9\mathrm{se} = 0.9. Then explain how stratifying on the model’s own prediction reduces that cost.

  5. (Computational) Simulate the setting of Figure the figure: generate XX, corrupt it to XX^\ast at a chosen accuracy, and compare the naive slope, the closed form the equation, and a regression-calibration estimate using a validated subsample. Report bias, Monte Carlo standard deviation, and coverage of the nominal 95%95\% interval for each.

  6. (Computational) Build a small extraction pipeline over 200 public abstracts using a schema of three fields. Hand-label a random 50, estimate per-field sensitivity and specificity with Wilson intervals Wilson, 1927, and report how the estimated effect of one extracted field on another changes before and after correction.

  7. (Computational) Take the model’s per-field confidence scores, assess their calibration with a reliability diagram, and construct a route-to-human rule that attains 98%98\% precision on the auto-accepted subset. Report the fraction of records requiring human review, and check whether the routed records differ systematically from the rest.

References
  1. Guo, C., Pleiss, G., Sun, Y., & Weinberger, K. Q. (2017). On Calibration of Modern Neural Networks. Proceedings of the 34th International Conference on Machine Learning (ICML), 1321–1330.
  2. Carroll, R. J., Ruppert, D., Stefanski, L. A., & Crainiceanu, C. M. (2006). Measurement Error in Nonlinear Models. Chapman. 10.1201/9781420010138
  3. Efron, B. (1979). Bootstrap Methods: Another Look at the Jackknife. The Annals of Statistics, 7(1). 10.1214/aos/1176344552
  4. Algorithmic Learning in a Random World. (2005). Springer-Verlag. 10.1007/b106715
  5. Angelopoulos, A. N., & Bates, S. (2021). A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification.
  6. Kaufman, S., Rosset, S., Perlich, C., & Stitelman, O. (2012). Leakage in Data Mining: Formulation, Detection, and Avoidance. ACM Transactions on Knowledge Discovery from Data, 6(4), 1–21. 10.1145/2382577.2382580
  7. Wilson, E. B. (1927). Probable Inference, the Law of Succession, and Statistical Inference. Journal of the American Statistical Association, 22(158), 209–212. 10.1080/01621459.1927.10502953