← Methods repository
concept

Administrative Claims Analysis

A family of study designs that constructs cohorts and defines exposures, outcomes, and covariates from administrative billing and enrollment records (diagnosis, procedure, drug, and eligibility files) generated for reimbursement rather than research, whose validity rests on transparent, pre-specified operationalization of enrollment, time zero, and code-based phenotypes.

Study_Designclaimsadministrative-datapharmacoepidemiologyphenotype-algorithmcontinuous-enrollmentpayer-heterogeneitysecondary-datareal-world-data
Methods reference only. Use primary source citations and local policy before applying this in a study protocol, regulatory submission, payer dossier, or clinical decision.

In plain language

Administrative claims analysis is research built from the billing and enrollment records an insurer keeps to pay for care, not to do science. Every diagnosis, drug fill, and procedure shows up as a dated line that says "someone billed for this," so you reuse those lines to figure out who was treated, when, and what happened next. The catch is that a claim only proves a bill was paid, never that a patient was truly sick or truly took the medicine, and care the plan never paid for (cash-paid drugs, free samples, out-of-network visits) is simply invisible.

Administrative claims analysis

is not a single design but the substrate on which most US real-world evidence is built. The raw material is billing and enrollment data — inpatient (facility/MedPAR), outpatient/professional (carrier), pharmacy (NDC + `days_supply`), DME, and eligibility files — created to adjudicate payment, then repurposed to estimate treatment effects, incidence, costs, and utilization in populations far larger than any trial. Because the data are a byproduct of reimbursement, every research variable is a proxy: a diagnosis code means "someone billed for evaluating this," a fill means "a pharmacy was reimbursed," and the absence of a code can mean the patient is healthy, was seen out-of-network, or was enrolled in a plan that does not submit that claim type. The discipline of claims analysis is making those proxies explicit, validated, and reproducible.

Core conceptual distinction — what claims observe versus what you want to measure

Claims capture adjudicated billing events with service dates and enrollment spans, not clinical truth. The central operating decisions are therefore (1) observability: a variable can only be measured during continuous, claim-submitting enrollment, so person-time must be bounded by an explicit enrollment definition; (2) time zero: index must be a real claim event (a `fill_date`, procedure date, or first qualifying diagnosis) with a pre-specified lookback for washout and covariates; and (3) phenotype: each exposure/outcome/covariate is an algorithm over codes (e.g., 1 inpatient OR 2 outpatient diagnoses ≥30 days apart) with a known positive predictive value (PPV) and sensitivity in this specific source and population. Get these three right and claims rival registries for many questions; get them wrong and large N simply produces a precisely estimated bias.

Pros, cons, and trade-offs

- vs EHR-only studies: Claims give near-complete cross-provider capture within a plan (every reimbursed encounter, pharmacy, and hospital, not just one health system) and clean enrollment denominators, which makes person-time and "loss" well defined. EHR offers labs, vitals, notes, severity, and physiologic outcomes that claims lack. The classic failure of claims-only: no HbA1c, ejection fraction, stage, or smoking status, so confounding by severity is controlled only through proxies (a high-dimensional propensity score) rather than measurement. Prefer linked claims–EHR when severity or a lab-defined outcome drives the question; prefer claims alone when the question is utilization, cost, adherence, or a well-coded hospitalization outcome across a defined population. - vs disease/product registries: Registries win on adjudicated outcomes, disease severity, and indication; claims win on completeness of treatment/utilization and unbiased denominators. Registries often miss off-protocol care and lack a true denominator. Prefer linkage (registry for severity + claims for full exposure and a death index for censoring). - vs primary data collection / pragmatic trials: Claims are orders of magnitude cheaper, faster, and larger, and capture routine practice — but cannot randomize, cannot capture PROs, and inherit all measurement and channeling bias. - Trade-off that defines the field: scale and external validity bought with measurement error and unmeasured confounding. The methodological response is not "more N" but validated phenotypes, active-comparator new-user design, hdPS, negative controls, and quantitative bias analysis.

When to use

Comparative safety/effectiveness of marketed drugs across a defined insured population; drug utilization, adherence (PDC/MPR), and persistence; healthcare resource use and cost (PPPM/PPPY); incidence/prevalence with a clean enrollment denominator; post-authorization safety surveillance (the Sentinel model). Claims are the default when the question is about what was billed across all providers a patient touched and a population denominator is required.

When NOT to use — and when claims are actively misleading

- The key variable is clinical, not billed. Disease severity, lab values, tumor stage, performance status, physiologic outcomes (BP, eGFR), symptoms, and PROs are absent. Studying a severity-driven outcome on claims alone invites uncontrolled confounding by indication — large N makes the bias look certain, not absent. - Outcome ascertainment differs by exposure. If one arm is seen more often (more visits → more incidental codes), surveillance/detection bias inflates the more-monitored arm's apparent event rate. Equalize ascertainment opportunity or use an outcome with mandatory encounters (hospitalization, death). - Person-time is unobservable for one group. In Medicare Advantage, fee-for-service claims are often not submitted (capitated/encounter data may be incomplete for non-risk variables), so "no prior fill" or "no event" can be missingness, not truth. Mixing MA-only and FFS person-time without restriction produces differential outcome capture. - Immortal time from procedure/qualifying-event designs. Defining exposure by a downstream procedure or a second fill, then starting follow-up earlier, guarantees the exposed group survives to be exposed — a textbook claims pitfall in surgery and oncology line-of-therapy studies. Align time zero to the first qualifying event. - Cash-pay, samples, $4 generics, and specialty-pharmacy carve-outs are invisible — adherence and exposure are undercounted, often differentially by drug cost.

Data-source operational depth (each with real failure modes and workarounds)

- Medicare FFS: Payment-driven and relatively complete for covered services; MedPAR for inpatient, carrier for professional, Part D for pharmacy. Failure modes: Part D started in 2006 (no earlier drug data); Part B drugs are captured as J-codes on medical claims, not Part D, so oral-vs-infused capture differs; differential competing risk of death in this elderly population means cause-specific vs subdistribution estimands genuinely diverge — pre-specify which you report. Workaround: require enrollment in all needed parts (A/B/D); link to a death index (NDI/Vital Statistics) because Part A only reliably captures inpatient deaths. - Medicare Advantage: HCC risk-adjustment incentives drive higher coding intensity (chart reviews, in-home health risk assessments add diagnoses), so MA patients can look sicker than identical FFS patients — a confounder for any code-count covariate. Failure mode: encounter submission has historically been less complete/standardized for non-risk variables, and FFS-style line items may be absent under capitation, so MA-only person-time can lack the FFS claims that define exposure or outcome. Workaround: either restrict to FFS person-time or treat MA explicitly with source-specific phenotypes and sensitivity analyses; never pool naively. - Commercial (e.g., MarketScan, Optum): Younger, employed/dependent populations with high churn (job change ends enrollment); benefit/formulary differences shape exposure; out-of-network and carved-out behavioral/specialty claims may be missing. Failure mode: short median enrollment truncates lookback and follow-up, biasing toward short-term effects and toward healthier persistent enrollees. Workaround: report the attrition funnel, test lookback length, and model loss to follow-up as potentially informative. - EHR (when used in place of or with claims): Adds labs/vitals/notes/severity but capture is visit-driven — a patient who leaves the system is differentially lost, and exposure is the order/administration, not a guaranteed fill. Workaround: link to pharmacy claims to confirm initiation and to a death index for mortality. - Registry and linked claims–EHR–vital records: Registry adds adjudicated outcomes and severity but weak exposure; linkage is the ideal substrate but introduces selection (only the linkable subset) and order/fill/service date discrepancies that must be reconciled before time-zero assignment.

Worked claims example (FFS + commercial, oral anticoagulant safety)

Question: incidence of hospitalized GI bleed in new users of oral anticoagulant A vs B among adults with atrial fibrillation. (1) Enrollment/observability: require ≥365 days of continuous medical + pharmacy enrollment before index, and exclude MA-only person-time so absence of prior fills and outcomes is observed, not missing. (2) Indication: ≥1 inpatient OR ≥2 outpatient AF diagnoses (ICD-10 I48) ≥7 days apart in the baseline window. (3) Time zero: the first pharmacy `fill_date` for A or B (NDC list), with arm assigned from that day's NDC; washout = no fill of any oral anticoagulant in the prior 365 days (this makes both arms incident users and removes prevalent-user/depletion-of-susceptibles bias). (4) Covariates: measured only in `[index_date − 365, index_date]` (HAS-BLED component diagnoses, prior bleed, NSAID/antiplatelet fills, utilization counts) feeding an hdPS to proxy unmeasured severity. (5) Outcome: first hospitalization with a primary GI-bleed diagnosis — a high-PPV phenotype because inpatient coding requires a billed admission, reducing detection bias relative to outpatient codes. (6) Exposure window/censoring: stitch consecutive fills with a 30-day grace period and stockpiling cap; follow from time zero to first event, censoring at disenrollment, death, switch to the other arm, end of on-treatment supply + grace, or end of data. (7) Competing risk: because death competes with GI bleed in an AF/elderly population, report the cause-specific hazard (etiology) and the cumulative incidence function via Fine–Gray (absolute risk) rather than naive Kaplan–Meier. (8) Sensitivity: vary washout (180/365/730 days) and grace period, add a negative-control outcome, and check standardized differences <0.1 after PS adjustment.

The estimand must be pre-specified

In claims, the choice among cause-specific rate, cumulative incidence (Fine–Gray subdistribution), and an ITT-like (initiation) vs as-treated (on-treatment, censoring at switch/discontinuation with inverse-probability-of-censoring weights) contrast changes both the model and the interpretation — especially where death is a frequent competing event. Decide it in the protocol, not after seeing the data.

Worked example

Scenario

We pull every 2023 claim line for one insured member (member_id 5001) and want to read those raw rows the way an analyst would: tell apart a diagnosis from a drug fill from a procedure, see when the member started a statin and for how long it was meant to last, and spot whether a heart attack was billed during the year.

Dataset

A handful of raw claim lines for one member, mixing pharmacy and medical claims exactly as they sit in the source tables.

member_idclaim_typeservice_datecode_systemcodedays_supplyamount
5001medical2023-01-10ICD-10I48.91142.0
5001pharmacy2023-02-01NDC00071-0155-233011.45
5001pharmacy2023-03-03NDC00071-0155-233011.45
5001medical2023-05-12CPT9300038.0
5001medical2023-09-15ICD-10I21.99820.0

Steps

  • Read claim_type and code_system together to see what each row is: the rows with claim_type 'medical' carry an ICD-10 diagnosis code (I48.91, I21.9) or a CPT procedure code (93000), while the 'pharmacy' rows carry an NDC drug code.

  • Row 1 is a diagnosis: ICD-10 I48.91 is atrial fibrillation, billed on 2023-01-10. A diagnosis row has no days_supply (it is null) because nothing is being dispensed.

  • Rows 2 and 3 are drug fills: the same NDC (00071-0155-23, atorvastatin) filled on 2023-02-01 and again on 2023-03-03. The exposure starts at the first fill's service_date, and service_date + days_supply tells you how long it covers: 2023-02-01 plus 30 days covers through 2023-03-02, then the 2023-03-03 refill continues coverage.

  • Row 4 is a procedure, not a diagnosis: CPT 93000 (an electrocardiogram) billed on 2023-05-12; CPT codes describe a service performed, so this row says a test was done, not that a disease was present.

  • Row 5 is the outcome we were watching for: ICD-10 I21.9 is an acute myocardial infarction (heart attack). Because its code starts with I21, the outcome phenotype flags it, dated 2023-09-15.

Result

Reading member 5001's rows: 1 statin (atorvastatin) start on 2023-02-01 with 30 days_supply (coverage through 2023-03-02), continued by a 30-day refill on 2023-03-03; a baseline atrial-fibrillation diagnosis on 2023-01-10; an ECG procedure on 2023-05-12; and 1 MI diagnosis (ICD-10 I21.9) on 2023-09-15 flagged as the outcome.

Runnable example

python implementation

Claims cohort construction (a drug-exposure new-user cohort) from standard claims tables. Required inputs, already cleaned and de-duplicated (reversals/voids removed): rx : pharmacy fills -> person_id, fill_date (datetime), ndc, days_supply (int),...

import pandas as pd

WASHOUT_DAYS = 365        # drug-free + continuous-enrollment lookback that defines a "new user"
INDICATION_CODES = {"I48"}  # e.g., atrial fibrillation (ICD-10, truncated to 3 char for matching)

def build_claims_cohort(rx: pd.DataFrame, dx: pd.DataFrame, enroll: pd.DataFrame) -> pd.DataFrame:
    rx = rx.sort_values(["person_id", "fill_date"])

    # 1. Candidate time zero = first fill of the study drug class (NDC -> drug_class upstream).
    study = rx[rx["drug_class"].isin(["STUDY", "COMPARATOR"])]
    idx = (study.groupby("person_id", as_index=False).first()
                .rename(columns={"fill_date": "index_date", "drug_class": "arm"}))

    # 2. New-user washout: no study/comparator fill in the WASHOUT_DAYS before index.
    prior = study.merge(idx[["person_id", "index_date"]], on="person_id")
    in_washout = prior[(prior["fill_date"] < prior["index_date"]) &
                       (prior["fill_date"] >= prior["index_date"] - pd.Timedelta(days=WASHOUT_DAYS))]
    idx = idx[~idx["person_id"].isin(in_washout["person_id"])].copy()
    idx["baseline_start"] = idx["index_date"] - pd.Timedelta(days=WASHOUT_DAYS)

    # 3. Continuous, claim-submitting enrollment across the whole washout through index (no MA-only person-time).
    e = enroll.merge(idx[["person_id", "index_date", "baseline_start"]], on="person_id")
    covered = e[(e["enroll_start"] <= e["baseline_start"]) & (e["enroll_end"] >= e["index_date"]) &
                e["med_benefit"] & e["rx_benefit"] & (~e["ma_only"])]
    idx = idx[idx["person_id"].isin(covered["person_id"])].copy()

    # 4. Validate indication with a claims phenotype: >=1 IP OR >=2 OP qualifying dx in the baseline window.
    d = dx.merge(idx[["person_id", "index_date", "baseline_start"]], on="person_id")
    d = d[d["dx_code"].str[:3].isin(INDICATION_CODES) &
          (d["dx_date"] >= d["baseline_start"]) & (d["dx_date"] <= d["index_date"])]
    ip = d[d["claim_setting"] == "IP"].groupby("person_id").size()
    op = d[d["claim_setting"] == "OP"].groupby("person_id").size()
    has_indication = set(ip[ip >= 1].index) | set(op[op >= 2].index)
    idx = idx[idx["person_id"].isin(has_indication)]

    return idx[["person_id", "arm", "index_date", "baseline_start"]].reset_index(drop=True)
r implementation

Claims cohort construction with data.table. Inputs mirror the Python version: rx : person_id, fill_date (Date), ndc, days_supply (int), drug_class dx : person_id, dx_date (Date), dx_code, claim_setting in {'IP','OP'} enroll : person_id, enroll_start,...

library(data.table)
WASHOUT_DAYS <- 365L
INDICATION_CODES <- c("I48")  # atrial fibrillation, 3-char ICD-10 match

build_claims_cohort <- function(rx, dx, enroll) {
  setDT(rx); setDT(dx); setDT(enroll)
  setorder(rx, person_id, fill_date)

  # 1. Candidate time zero = first study/comparator fill.
  study <- rx[drug_class %chin% c("STUDY", "COMPARATOR")]
  idx <- study[, .(index_date = fill_date[1L], arm = drug_class[1L]), by = person_id]
  idx[, baseline_start := index_date - WASHOUT_DAYS]

  # 2. New-user washout: drop anyone with a prior study/comparator fill inside the window.
  study <- merge(study, idx[, .(person_id, index_date)], by = "person_id")
  prior_ids <- unique(study[fill_date < index_date &
                            fill_date >= index_date - WASHOUT_DAYS, person_id])
  idx <- idx[!person_id %chin% prior_ids]

  # 3. Continuous claim-submitting enrollment across washout -> index (no MA-only spans).
  e <- merge(enroll, idx[, .(person_id, index_date, baseline_start)], by = "person_id")
  ok_enroll <- e[enroll_start <= baseline_start & enroll_end >= index_date &
                 med_benefit & rx_benefit & !ma_only, unique(person_id)]
  idx <- idx[person_id %chin% ok_enroll]

  # 4. Indication phenotype: >=1 IP OR >=2 OP qualifying dx in the baseline window.
  d <- merge(dx, idx[, .(person_id, index_date, baseline_start)], by = "person_id")
  d <- d[substr(dx_code, 1L, 3L) %chin% INDICATION_CODES &
         dx_date >= baseline_start & dx_date <= index_date]
  cnt <- d[, .(ip = sum(claim_setting == "IP"), op = sum(claim_setting == "OP")), by = person_id]
  keep <- cnt[ip >= 1L | op >= 2L, person_id]
  idx <- idx[person_id %chin% keep]

  idx[, .(person_id, arm, index_date, baseline_start)]
}