NLP for Clinical Text in RWE
A family of computational methods — from rule-based pattern matching through transformer-based models — that convert free-text clinical notes into structured variables for RWE analysis; NLP-derived variables are algorithm-derived estimates that carry systematic measurement error and must be validated against a chart-review gold standard (PPV, sensitivity, kappa) with error rates propagated into every downstream analytic result.
In plain language
Natural language processing (NLP) is a set of computer methods that read clinical notes — the free-text written by doctors, nurses, and radiologists — and convert what they say into structured data fields that can be used in a study. Because essential facts like cancer stage, how well a patient is functioning (ECOG score), and whether a tumor has grown are often only written in notes and never entered into a code field, NLP can recover information that structured records miss entirely. A key caveat is that the output of any NLP system is an educated guess based on patterns in text, not a guaranteed truth — researchers must always check a sample of records by hand to measure how often the computer is right (called validation), and they must account for those errors when interpreting study results.
Why clinical text is the dark matter of EHR data
Structured fields in electronic health records — diagnosis codes, procedure codes, laboratory result values, medication orders — capture a narrow slice of clinical reality. Cancer staging, ECOG performance status, biomarker results narrated in the clinic note, smoking and alcohol history, real-world progression assessments, and social determinants of health are all routinely documented in free-text and are absent or unreliably encoded in structured fields for a large share of patients. Studies of oncology EHR databases consistently find that performance status and staging are structurally missing from coded fields for a majority of patients yet appear in clinical notes; the same is true for progression language in radiology and oncologist reports, for BRCA and biomarker status in pathology narratives, and for substance use in the social history section. Any RWE study that relies exclusively on structured fields for these variables operates on a systematically truncated population — the patients for whom the variable is structurally unobservable are not missing at random; they may be the patients whose care is less protocol-driven and whose true variable values differ most from the imputed-as-zero default.
NLP is the method layer that bridges this gap: it reads the text, identifies clinical entities, resolves ambiguity, and outputs a structured variable that can be merged with coded data for analysis. The output is not ground truth — it is an algorithm-derived estimate with its own operating characteristics that must be measured and reported exactly as for any EHR phenotyping algorithm.
The NLP task taxonomy for RWE
Clinical NLP in RWE spans several distinct tasks, each with its own failure mode:
Named-entity recognition (NER): identifying mentions of a drug, dose, biomarker value, or clinical concept in text — for example, extracting "pembrolizumab 200 mg" as a drug-dose entity, or "ECOG 2" as a performance-status entity. The dominant failure mode is boundary error: partial extraction, missed entities in non-standard phrasing, and false positives from entities appearing in a different semantic context.
Negation and uncertainty handling: the most consequential and most frequently mishandled layer. A phrase like "no evidence of progression" expresses absence of progression, yet a naive NER system that detects "progression" will flag it as present. NegEx-style algorithms (Chapman 2001) define a left and right scope window around each assertion trigger ("no evidence of," "denied," "without") and negate any entity within that window. Uncertainty markers — "possible progression," "cannot rule out recurrence" — produce a third category that must be handled explicitly: treating uncertain mentions as non-events or as a sensitivity stratum. Ignoring negation is the single most common implementation error in clinical NLP deployments; it produces systematic false positives that inflate the apparent prevalence of any condition.
Section detection: a clinical note is not a uniform stream of text. It has a family history section, a past medical history section, a review of systems, an assessment, and a plan. A biomarker or diagnosis in the family history section pertains to a relative, not the patient. A symptom in the review of systems may be a reported complaint, not a confirmed finding. Section detection partitions each document into labeled zones so entity extraction applies the correct scope — for example, extracting "BRCA1 pathogenic variant" from a Family History section as a family-level finding, not a patient-level finding.
Relation extraction: pairing entities into structured tuples — (biomarker, value, date), (drug, dose, frequency), (tumor size, measurement date) — to capture not just entity presence but the relationship between them. Relation extraction is harder than NER alone because entities can participate in different relations in the same sentence and relations may span sentence boundaries.
Document classification: assigning a note-level or report-level binary or ordinal label — progression versus no-progression, positive versus negative biomarker status. This is the coarser task most NLP-for-RWE pipelines implement first because it can be validated with a binary chart-review label. Document classification is often the direct input to real-world progression endpoints (see `real-world-progression-rwpfs-rwe`).
The rule-based-to-transformer arc
Clinical NLP tools have evolved across three generations. Rule-based systems such as cTAKES and MedSpaCy (early-to-mid 2010s) use manually curated lexicons, regular expressions, and NegEx-style scope rules to annotate clinical concepts. They are fast, transparent, reproducible, and require no labeled training data, but they fail on novel phrasing and require expert clinical informaticists to maintain the rule set. BERT-variant models — BioBERT, ClinicalBERT, and domain-fine-tuned transformers (roughly 2019 onward) — learn contextual representations from large corpora and substantially outperform rule-based systems on benchmark NER and assertion tasks, at the cost of requiring labeled training data, GPU compute, and a less transparent decision process. Large language models can extract clinical entities from text with minimal task-specific fine-tuning using prompting alone, but introduce hallucination, run-to-run inconsistency, and absence of calibrated uncertainty as failure modes — route LLM-specific extraction approaches to `llm-assisted-abstraction-rwe`. Regardless of which generation is used, all approaches produce algorithm-derived outputs that require empirical validation. The transformer arc does not eliminate the validation imperative.
The validation imperative
NLP-derived variables are algorithm-derived variables. Staging from a progress note, ECOG from a clinic note, biomarker status from a pathology report — each is a measurement, and every measurement has error. The validation protocol mirrors that required for EHR phenotyping algorithms: define a chart-review gold standard, sample a representative set of NLP-positive and NLP-negative records (aiming for at least 150–200 reviewed records), have trained reviewers assign the reference label blind to the NLP output, and compute PPV (precision), sensitivity (recall), specificity, and inter-reviewer agreement (Cohen's kappa or weighted kappa for ordinal variables — see `agreement-statistics-kappa-icc-bland-altman`).
The measured error rates must then propagate downstream (see `algorithm-validation`). A sensitivity analysis that re-runs the primary analysis excluding patients with ambiguous NLP classifications tests robustness. A quantitative bias analysis that corrects effect estimates for the measured misclassification rate gives bounds on distortion. Reporting only the NLP pipeline's F1 score on a validation set, without propagating uncertainty into the main analytic result, is methodologically incomplete. Nondifferential NLP misclassification of a binary variable biases effect estimates toward the null; differential misclassification — if documentation density, note length, or phrasing conventions differ by treatment arm — can bias in either direction.
Portability failure and the silver-standard trap
Two structural threats dominate NLP deployment in multi-site RWE:
Portability failure: clinical NLP models trained at one institution degrade substantially at another. The root cause is template heterogeneity — the same clinical fact is documented in systematically different language depending on EHR vendor, specialty, clinical culture, and geographic region. A model calibrated on one documentation style can miss or misclassify the same concept written differently. Every new deployment site requires at minimum a targeted validation study; published F1 scores from benchmark evaluations at a single institution do not transfer. This is particularly acute for RWE networks that aggregate data across multiple health systems.
Silver-standard trap: training an NLP model using the structured codes it is meant to supplement as its weak label set creates a model that can only recover what was already coded. A "progression" classifier trained on ICD codes for secondary malignancy as positive labels will achieve excellent validation performance against those same codes but provides no additional information beyond the codes themselves — and will miss exactly the cases that motivated the NLP approach (patients with documented progression who were never coded). The correct reference standard is independent chart review by a clinician who does not have access to the structured codes during review.
Pros, cons, and trade-offs
Pros: NLP recovers clinically meaningful variables — staging, performance status, biomarkers, social history — that are absent from structured fields for a substantial share of patients. It reduces the need for full-cohort manual abstraction. When validated with reported operating characteristics, NLP-derived variables are acceptable to FDA and regulatory reviewers for supplementary endpoints and sensitivity analyses. They enable composite endpoints like real-world progression that require reading radiology or oncologist text.
Cons: NLP adds algorithm-derived measurement error that is systematic, not random, and that propagates into every downstream effect estimate. Models transport poorly across sites. Validation requires clinical reviewers and is expensive. Negation errors, section errors, and uncertainty failures can be invisible in aggregate F1 metrics — a pipeline with 90% accuracy can have near-zero recall on negated findings. The silver-standard trap produces models that appear accurate but do not improve ascertainment over structured codes alone. LLM-based pipelines add hallucination risk and run-to-run inconsistency.
Against manual abstraction: manual abstraction by trained reviewers is the gold standard for accuracy but does not scale to tens of thousands of patients. NLP scales to the full cohort but introduces systematic error. The practical design for most large RWE studies is a hybrid: NLP for the full cohort (primary analysis), a stratified random validation sample reviewed by clinicians (for operating characteristics), and a misclassification bias analysis in the statistical analysis plan.
When to use
Use NLP-derived variables when a clinically essential covariate or outcome — staging, ECOG, biomarker status, social history, progression — is absent from or unreliably captured in structured fields for a substantial share of the study population; when the cohort is large enough that full-cohort manual abstraction is infeasible; when a validation sample of at least 150–200 independently reviewed records can be obtained; and when the text corpus shares documentation conventions with the NLP model's training environment, or when a site-specific validation and fine-tuning step is planned.
When NOT to use — and when it is actively misleading
Do not use NLP-derived variables as primary study endpoints without validation: an NLP output with unknown operating characteristics carries unknown error direction and cannot be bounded. This is the dangerous case — unlike missing-at-random dropout, NLP error is systematic and may differ by exposure arm (differential misclassification), biasing in either direction with no structural guarantee of conservatism.
Do not deploy a model validated at one site at a new site without a local validation study. Do not train on structured codes as the reference label and then use the resulting model as if it supplements those codes — it does not. Do not present transformer or LLM benchmark F1 scores as applicable to your corpus without local validation. Do not present NLP-flagged counts as ground-truth ascertainment — always label outputs as algorithm-derived, report the validation operating characteristics, and include a misclassification sensitivity analysis in the statistical analysis plan.
Interpreting the output
In the worked example, an NLP pipeline classifies ECOG performance status from oncology progress notes. The pipeline flags 100 patients as ECOG ≥ 2. Chart review of those 100 confirms 85 as true ECOG ≥ 2 (true positives) and 15 as false positives (notes where the pipeline mistook a negated or conditional ECOG mention for an affirmed finding). Separately, from a hold-out set of 50 chart-review-confirmed ECOG ≥ 2 patients, the pipeline detected 40 and missed 10 (notes using non-standard phrasing outside the model's training vocabulary).
(1) Formal interpretation. PPV = 85/100 = 0.85: for every 100 patients flagged as ECOG ≥ 2 by this NLP algorithm in this validation sample, 85 truly met that criterion on chart review — 15 were false positives. Sensitivity = 40/50 = 0.80: of 50 patients known from chart review to be ECOG ≥ 2, the pipeline detected 40 and missed 10. Both are estimated in the validation sample and generalize to the full cohort only if the sample is representative of the full cohort's documentation styles, specialties, and disease severity distribution. PPV and sensitivity are independent quantities estimable only from separate sampling designs — a PPV from chart review of NLP-positive records tells you nothing directly about sensitivity.
(2) Practical interpretation. Before including NLP-derived ECOG in any comparative analysis, two corrections are needed: (a) apply PPV to correct the NLP-positive count for false positives — of 100 flagged patients, approximately 85 are true ECOG ≥ 2 cases; (b) acknowledge that the 20% miss rate attenuates any effect estimate toward the null if the miss rate is nondifferential by exposure. If the treated arm has longer or more detailed notes (a common pattern in trials of active drugs under intensive monitoring), the miss rate may be lower in that arm — differential misclassification that biases in an unpredictable direction. Report PPV and sensitivity in the methods section alongside the model name or version, the validation sample design, and a misclassification sensitivity analysis.
Worked example
Scenario
An oncology RWE team needs ECOG performance status for a study of 8,000 patients; this variable appears in structured fields for only 2,100 patients (26%). The team deploys an NLP pipeline to classify ECOG status from progress notes for the remaining 5,900 patients. To measure how well the pipeline works, they randomly sample 100 patients flagged as ECOG ≥ 2 by the NLP and have a trained oncology nurse review each chart. They also obtain a hold-out set of 50 patients whose ECOG ≥ 2 status is known from structured fields (used as a sensitivity check) and run those through the NLP pipeline to measure what fraction it detects. The five-row table shows representative examples from the validation sample, including two failure modes — a false positive from a negated mention and a false negative from non-standard phrasing.
Dataset
Five representative records from the validation sample. NLP_flag is the pipeline output; chart_ecog_ge2 is the chart-review reference standard. Classification labels the result.
| patient_id | NLP_flag | chart_ecog_ge2 | note_snippet | classification |
|---|---|---|---|---|
| 3001 | yes | yes | ECOG PS 2 noted, patient unable to work | TP |
| 3002 | yes | yes | functional status ECOG grade 3 at this visit | TP |
| 3003 | yes | no | no evidence of ECOG decline beyond grade 1 | FP |
| 3004 | no | yes | too debilitated to perform any work-related activities | FN |
| 3005 | no | no | performing all activities of daily living without restriction | TN |
Steps
The NLP pipeline flags patients as ECOG >= 2 based on mention detection plus NegEx-style negation handling. Patient 3001 and 3002 are true positives: the notes contain explicit affirmed ECOG scores of 2 and 3 respectively.
Patient 3003 is a false positive: the note says 'no evidence of ECOG decline beyond grade 1' — the NegEx scope window captured 'decline' but the pipeline erroneously propagated the ECOG entity as affirmed, missing that the surrounding context negates ECOG >= 2.
Patient 3004 is a false negative: the note describes severe functional impairment using narrative language ('too debilitated') without an explicit ECOG grade, so the pipeline produced no ECOG entity and made no prediction — a vocabulary gap failure.
Patient 3005 is a true negative: the note affirms good functional status and the pipeline correctly produces no ECOG >= 2 flag.
In the full 100-patient NLP-positive validation sample, chart review confirms 85 as true ECOG >= 2 and 15 as false positives (mostly negation failures like patient 3003). PPV = 85/100 = 0.85.
In the 50-patient known-positive hold-out, the NLP pipeline detects 40 and misses 10 (mostly vocabulary-gap failures like patient 3004). sensitivity = 40/50 = 0.8.
The 15 false positives will inflate the apparent prevalence of ECOG >= 2 if uncorrected. The 10 missed true positives (20% miss rate) will attenuate effect estimates toward the null if the miss rate is nondifferential by treatment arm — or bias in either direction if it differs across arms.
Result
PPV = 85/100 = 0.85 (85 of 100 NLP-flagged patients were true ECOG >= 2 on chart review; 15 were false positives from negation or conditional phrasing). sensitivity = 40/50 = 0.8 (the pipeline detected 40 of 50 known-positive patients; 10 were missed due to non-standard phrasing). Both metrics must be reported alongside any analysis using NLP-derived ECOG status; a quantitative bias analysis should propagate the 15% false positive rate and 20% miss rate into the primary effect estimate.
Runnable example
python implementation
End-to-end NLP validation pipeline in Python using MedSpaCy for rule-based NER and NegEx negation detection, followed by a validation metrics computation (PPV, sensitivity, specificity, Cohen's kappa) from a chart-review reference set. Covers the three core...
"""
NLP for RWE — ECOG performance status extraction from oncology progress notes.
Pipeline: medspacy NER + NegEx negation → structured flag → validation vs chart review.
"""
import re
import pandas as pd
from sklearn.metrics import cohen_kappa_score, confusion_matrix
# ── Minimal medspacy pipeline (install: pip install medspacy) ──────────────────────────
try:
import medspacy
from medspacy.ner import TargetRuleMatcher
def build_nlp_pipeline():
nlp = medspacy.load() # loads tokenizer + sentencizer + NegEx by default
# Add ECOG target rules — patterns that match ECOG grade mentions
target_matcher = nlp.get_pipe("medspacy_target_matcher")
from medspacy.ner import TargetRule
target_matcher.add([
TargetRule("ECOG_ge2", "ECOG_STATUS",
pattern=[{"LOWER": "ecog"},
{"IS_SPACE": True, "OP": "?"},
{"LOWER": {"IN": ["2", "3", "4", "grade", "ps"]}},
{"LOWER": {"IN": ["2", "3", "4"]}, "OP": "?"}]),
TargetRule("ECOG_functional_limit", "ECOG_STATUS",
pattern=[{"LOWER": {"IN": ["debilitated", "bedridden",
"ambulatory", "self-care"]}}]),
])
return nlp
nlp = build_nlp_pipeline()
def extract_ecog_ge2(note_text: str) -> bool:
"""Return True if note contains an AFFIRMED ECOG >= 2 mention (NegEx applied)."""
doc = nlp(note_text)
for ent in doc.ents:
if ent.label_ == "ECOG_STATUS" and not ent._.is_negated:
return True
return False
except ImportError:
# Fallback: simple regex + negation scope (illustrative; use medspacy in production)
_ECOG_PAT = re.compile(r"\becog\s*(?:grade\s*|ps\s*)?[2-4]\b", re.I)
_NEG_TRIGGERS = re.compile(
r"\b(no|without|denies|denied|no evidence of|ruled out|not)\b", re.I)
def extract_ecog_ge2(note_text: str) -> bool:
"""Regex NER with simple left-window negation detection (25 chars)."""
for m in _ECOG_PAT.finditer(note_text):
window = note_text[max(0, m.start() - 25): m.start()]
if not _NEG_TRIGGERS.search(window):
return True
return False
# ── Apply NLP to the full cohort ─────────────────────────────────────────────────────
# notes_df: person_id, note_text, note_date, note_type
def run_nlp_pipeline(notes_df: pd.DataFrame) -> pd.DataFrame:
"""Return one row per person: most recent NLP flag across all their notes."""
notes_df = notes_df.sort_values("note_date")
flags = []
for pid, grp in notes_df.groupby("person_id"):
# Flag the patient if ANY progress note affirms ECOG >= 2
nlp_positive = any(extract_ecog_ge2(txt) for txt in grp["note_text"])
flags.append({"person_id": pid, "nlp_ecog_ge2": int(nlp_positive)})
return pd.DataFrame(flags)
# ── Validation: compute PPV, sensitivity, specificity, kappa ─────────────────────────
# validation_df: person_id, nlp_ecog_ge2 (0/1), chart_ecog_ge2 (0/1)
def validate_nlp(validation_df: pd.DataFrame) -> dict:
"""Compute operating characteristics vs chart-review gold standard."""
y_pred = validation_df["nlp_ecog_ge2"].values
y_true = validation_df["chart_ecog_ge2"].values
tn, fp, fn, tp = confusion_matrix(y_true, y_pred, labels=[0, 1]).ravel()
ppv = tp / (tp + fp) if (tp + fp) > 0 else float("nan")
sensitivity = tp / (tp + fn) if (tp + fn) > 0 else float("nan")
specificity = tn / (tn + fp) if (tn + fp) > 0 else float("nan")
kappa = cohen_kappa_score(y_true, y_pred)
return {
"tp": int(tp), "fp": int(fp), "fn": int(fn), "tn": int(tn),
"ppv": round(ppv, 3),
"sensitivity": round(sensitivity, 3),
"specificity": round(specificity, 3),
"kappa": round(kappa, 3),
"n_reviewed": int(tp + fp + fn + tn),
}
# ── Misclassification sensitivity analysis ────────────────────────────────────────────
def misclassification_bias_bounds(ppv: float, sensitivity: float,
n_nlp_positive: int) -> dict:
"""
Approximate the true-positive count and the expected false-negative count
given measured PPV and sensitivity.
True positives in NLP-flagged cohort ≈ n_nlp_positive * PPV.
Estimated false negatives ≈ n_true_positives * (1 - sensitivity) / sensitivity.
"""
n_true_pos = n_nlp_positive * ppv
n_false_neg = n_true_pos * (1.0 - sensitivity) / sensitivity if sensitivity > 0 else float("nan")
return {
"n_nlp_positive": n_nlp_positive,
"estimated_true_positives": round(n_true_pos, 1),
"estimated_false_negatives": round(n_false_neg, 1),
"ppv": ppv,
"sensitivity": sensitivity,
"note": (
"Report these estimates in the SAP bias analysis section. "
"Nondifferential miss rate attenuates effect estimates toward the null; "
"differential miss rate by treatment arm can bias in either direction."
),
}
# ── Example usage with the worked-example numbers ─────────────────────────────────────
# 100-patient NLP-positive validation sample: 85 TP, 15 FP
# 50-patient known-positive hold-out: 40 TP, 10 FN
demo = pd.DataFrame({
"person_id": list(range(1, 151)),
"nlp_ecog_ge2": [1] * 100 + [0] * 50,
"chart_ecog_ge2": [1] * 85 + [0] * 15 + [1] * 40 + [0] * 10,
})
metrics = validate_nlp(demo)
print("Validation metrics:")
for k, v in metrics.items():
print(f" {k}: {v}")
# Expected: ppv=0.85, sensitivity=0.8 (per the worked example arithmetic)
bounds = misclassification_bias_bounds(
ppv=metrics["ppv"], sensitivity=metrics["sensitivity"], n_nlp_positive=5900)
print("\nBias bounds for full cohort:")
for k, v in bounds.items():
print(f" {k}: {v}")r implementation
NLP validation metrics and misclassification bias analysis in R. Computes PPV, sensitivity, specificity, and Cohen's kappa from a chart-review validation data frame, then estimates the expected false-negative burden in the unreviewed cohort given the...
library(stringr)
suppressMessages(library(irr))
# ── Rule-based ECOG >= 2 extraction (regex + left-window negation; illustrative) ──────
extract_ecog_ge2 <- function(note_text) {
ecog_pattern <- "(?i)\\becog\\s*(?:grade\\s*|ps\\s*)?[2-4]\\b"
neg_triggers <- "(?i)\\b(no|without|denies|denied|no evidence of|ruled out|not)\\b"
matches <- gregexpr(ecog_pattern, note_text, perl = TRUE)[[1]]
if (matches[1] == -1L) return(FALSE) # no ECOG >= 2 mention at all
for (m in as.integer(matches)) {
window <- substr(note_text, max(1L, m - 25L), m - 1L)
if (!grepl(neg_triggers, window, perl = TRUE)) return(TRUE) # affirmed mention
}
FALSE # all mentions negated
}
# Apply to a data frame of notes (person_id, note_text, note_date)
apply_nlp <- function(notes_df) {
agg <- tapply(
seq_len(nrow(notes_df)),
notes_df$person_id,
function(idx) as.integer(any(sapply(notes_df$note_text[idx], extract_ecog_ge2)))
)
data.frame(person_id = names(agg),
nlp_ecog_ge2 = as.integer(agg),
stringsAsFactors = FALSE)
}
# ── Validation: PPV, sensitivity, specificity, kappa ──────────────────────────────────
# validation_df: person_id, nlp_ecog_ge2 (0/1), chart_ecog_ge2 (0/1)
validate_nlp <- function(validation_df) {
pred <- validation_df$nlp_ecog_ge2
ref <- validation_df$chart_ecog_ge2
tp <- sum(pred == 1L & ref == 1L)
fp <- sum(pred == 1L & ref == 0L)
fn <- sum(pred == 0L & ref == 1L)
tn <- sum(pred == 0L & ref == 0L)
ppv <- if ((tp + fp) > 0) tp / (tp + fp) else NA_real_
sensitivity <- if ((tp + fn) > 0) tp / (tp + fn) else NA_real_
specificity <- if ((tn + fp) > 0) tn / (tn + fp) else NA_real_
# Cohen's kappa via irr::kappa2
kappa_res <- irr::kappa2(
data.frame(rater1 = pred, rater2 = ref), weight = "unweighted")
list(
tp = tp, fp = fp,
fn = fn, tn = tn,
ppv = round(ppv, 3),
sensitivity = round(sensitivity, 3),
specificity = round(specificity, 3),
kappa = round(kappa_res$value, 3),
n_reviewed = tp + fp + fn + tn
)
}
# ── Misclassification bias bounds ───────────────────────────────────────────────────
bias_bounds <- function(ppv, sensitivity, n_nlp_positive) {
n_true_pos <- n_nlp_positive * ppv
n_false_neg <- if (sensitivity > 0) n_true_pos * (1 - sensitivity) / sensitivity
else NA_real_
message(sprintf(
"NLP flagged: %d\n Est. true positives: %.1f (= %d x PPV %.2f)\n Est. false negatives: %.1f (missed cases; attenuate effect if nondifferential)\n Note: differential miss rate by exposure arm biases in either direction.",
n_nlp_positive, n_true_pos, n_nlp_positive, ppv, n_false_neg))
invisible(list(n_true_pos = n_true_pos, n_false_neg = n_false_neg))
}
# ── Reproduce worked-example arithmetic ─────────────────────────────────────────────
# 100-patient NLP-positive validation: 85 TP + 15 FP
# 50-patient known-positive hold-out: 40 TP + 10 FN
demo <- data.frame(
person_id = 1:150,
nlp_ecog_ge2 = c(rep(1L, 100), rep(0L, 50)),
chart_ecog_ge2 = c(rep(1L, 85), rep(0L, 15), rep(1L, 40), rep(0L, 10))
)
m <- validate_nlp(demo)
cat("PPV =", m$ppv, " sensitivity =", m$sensitivity,
" specificity =", m$specificity, " kappa =", m$kappa, "\n")
# PPV = 0.85 sensitivity = 0.8 (matches worked-example: 85/100 and 40/50)
bias_bounds(ppv = m$ppv, sensitivity = m$sensitivity, n_nlp_positive = 5900L)