← Methods repository
concept

NCPDP Pharmacy Claim Fields

Pharmacy dispensing records transmitted under the NCPDP Telecommunication Standard (D.0) and adjudicated in real time at the point of sale by pharmacy benefit managers and commercial payers; each transaction carries an 11-digit NDC, quantity dispensed, days_supply, fill date, refill number, prescriber and pharmacy NPIs, a DAW (Dispense-as-Written) code, and cost-sharing amounts — the fields from which every adherence metric, generic-substitution analysis, and drug-utilization study in pharmacoepidemiology is constructed.

Data_Standardcoding-systemdata-standardprimitiveclaimspharmacyncpdpdays-supplyndc
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

When a patient picks up a prescription at a pharmacy, the pharmacy instantly sends a digital transaction to the insurance company and receives approval in seconds — that real-time record is an NCPDP pharmacy claim, and it contains three fields that researchers rely on above all others: the drug's unique 11-digit code (NDC), the fill date, and the number of days the supply is meant to last (days_supply). Unlike hospital or doctor-visit claims that take weeks to process, pharmacy data land in the database the same day the prescription is filled, making them unusually clean and timely — but the days_supply field is entered by the pharmacist and can be wrong, especially for insulin and inhalers, so every adherence calculation built on it should be validated. One important gap: pharmacy claims cannot see drugs given in hospitals, free samples, or prescriptions paid with a discount card like GoodRx.

What NCPDP pharmacy claims are and why they differ from institutional and professional claims

Pharmacy claims are fundamentally different from the two claim types that anchor the rest of US administrative data — the UB-04/837I institutional claim and the CMS-1500/837P professional claim. The institutional and professional claims are submitted in batch after services are rendered and adjudicated days to weeks later. A pharmacy claim is adjudicated in real time at the point of sale: the pharmacist's dispensing software sends the NCPDP transaction to the pharmacy benefit manager (PBM) or payer in seconds, receives an approval or rejection response before the patient leaves the counter, and the resulting paid claim lands in the insurer's data warehouse within hours. That real-time loop has a direct consequence for research: pharmacy claims data are substantially cleaner, more complete, and closer to the time of the service event than any other US administrative data source. The clean data, however, carry their own failure modes — chiefly in the `days_supply` field — that are unique to the pharmacy transaction model and can silently corrupt every adherence measure downstream.

The NCPDP Telecommunication Standard (D.0) and the NCPDP SCRIPT Standard

The NCPDP D.0 (version D, implemented 2009 under HIPAA) is the transaction set that governs real-time pharmacy claim submission, adjudication, and response. Every retail, mail-order, and specialty pharmacy in the United States uses D.0 transactions for benefit claims. The NCPDP SCRIPT standard governs electronic prescribing (e-prescribing), routing new prescriptions and refill requests between prescribers and pharmacies; it is the upstream source of the prescriber NPI that appears on the dispensing claim. Researchers encounter these transaction records as "pharmacy claims" in research databases (Medicare Part D Prescription Drug Events, commercial PBM extracts, Medicaid drug claims), but they originate from NCPDP D.0 transactions. Understanding this origin explains why pharmacy claims lack the institutional-claim complexity of interim bills, replacement claims, and TOB frequency codes — a dispensing event either happened or it did not, and reversals are their own transaction type.

Research-critical fields

NDC (11-digit, 5-4-2 HIPAA format): The National Drug Code identifying the exact drug product dispensed — labeler, product, and package. NDC is the primary key for all exposure code lists. A single drug and strength may span dozens or hundreds of NDCs across generic manufacturers and repackagers; a code list must be exhaustive or it silently undercounts exposure. NDC is covered in depth in its own catalog entry (see `ndc-national-drug-code`).

Quantity dispensed: The number of dosage units (tablets, capsules, milliliters) dispensed on this fill. Used with the NDC to confirm the supply is plausible: 30 tablets of a once-daily medication at a `days_supply` of 30 is consistent; 30 tablets at a `days_supply` of 90 signals a data-entry error or a three-tablet-per-day regimen. Quantity alone does not determine coverage duration; `days_supply` does.

days_supply: The number of days the dispensed quantity is intended to last. This is the single most consequential field in pharmacoepidemiology research — every adherence metric (PDC, MPR), every drug episode construction, and every index-date new-user window depends on it. It is also the field most prone to systematic measurement error, discussed in depth below.

Fill date (date_of_service / prescription_service_date): The date the prescription was dispensed. Fill date defines when a coverage interval begins and is the anchor for all longitudinal analyses. Because pharmacy claims are real-time, the fill date is generally trustworthy; the main exception is mail-order fills, where the dispensing date may precede actual patient receipt by several days.

Refill number / new-versus-refill flag: The fill number within an authorization sequence (0 = new prescription, 1 = first refill, etc.) or a binary new/refill indicator. This field is used in new-user design implementation to confirm the index fill is truly new (refill number = 0) rather than a continuation. In databases where refill number is unavailable, an incident new-user definition requires a washout period with no fills of the drug class to approximate the first fill.

Prescriber NPI: The National Provider Identifier of the clinician who wrote the prescription. Used to attribute dispensing events to a prescriber, link to specialty and practice characteristics, and study prescribing patterns. Completeness is generally high for Part D and commercial data, but data-entry of NPI at the pharmacy counter is imperfect, and some older records carry outdated or zeroed-out NPIs.

Pharmacy NPI / pharmacy identifier: The NPI of the dispensing pharmacy, supplemented by NCPDP provider number and chain store codes. Used in mail-order vs retail channel analyses, specialty pharmacy attribution, and geographic access studies. Mail-order pharmacies (high `days_supply`, NCPDP chain = mail) vs retail pharmacies (typical 30-day `days_supply`) behave differently for adherence and refill pattern studies, and pooling them without stratification distorts PDC estimates.

DAW code (Dispense-as-Written): A one-digit code transmitted by the pharmacy indicating the brand/generic substitution status of the fill: 0 = no product selection indicated (substitution permitted, generic dispensed); 1 = substitution not allowed by prescriber (brand required per prescriber's written DAW); 2 = substitution allowed — patient requested brand; 3–9 = other scenarios (substitution allowed — pharmacist selected product, substitution not allowed — regulatory required brand, etc.). DAW code is the primary field for generic-substitution studies: an exposure algorithm that counts all fills regardless of DAW cannot distinguish prescriber-mandated brand use (DAW = 1) from voluntary brand use (DAW = 2) from generic filling (DAW = 0). In comparative effectiveness research where the treatment contrast is brand vs generic, misclassifying DAW codes is a direct form of exposure misclassification.

Copay / plan-paid amounts: The patient copay (cost-sharing), total ingredient cost, dispensing fee, and plan-paid amount are all present in adjudicated pharmacy claim records. These fields support cost-sharing and access analyses, out-of-pocket burden studies, and cost-effectiveness inputs. A critical limitation: the ingredient cost on the claim is the PBM's adjudicated allowed amount, not the manufacturer's list price and not the actual rebate-adjusted net price paid by the payer; studies of drug cost should be clear about which cost concept they are measuring.

Transaction type / service type code: Identifies whether the record is a paid claim, a reversal (B2 or transaction type 21), or a credit — see Reversal Transactions below.

days_supply: the most consequential field and its measurement error

`days_supply` is entered by the pharmacist or dispensing system at the time of fill and is subject to systematic data-entry patterns that produce bias in adherence metrics:

28 vs 30 day conventions: Some pharmacies dispense 28-day supplies rather than 30-day supplies, particularly for medications with weekly dosing (e.g., weekly bisphosphonates dispensed as 4-week blister packs = `days_supply` 28). In a 90-day window, a patient with three 28-day fills covers only 84 days, yielding PDC = 84/90 = 0.933 vs PDC = 90/90 = 1.0 for three 30-day fills. Stratifying by `days_supply` or using the actual pharmacy channel (mail, retail) and dosing schedule before pooling avoids conflating these conventions.

Insulin dosing and variable supply estimation: Insulin is dispensed in units (a vial of 10 mL of U-100 insulin contains 1,000 units), but the `days_supply` depends on the patient's individual daily dose, which varies by body weight, insulin resistance, and titration and is not directly recorded in the claim. Pharmacies estimate `days_supply` from the prescribed dose, the dispensed quantity, and standard dosing assumptions. These estimates are notoriously unreliable: a patient who uses 50 units/day will deplete a 10 mL vial in 20 days, but if the pharmacy defaults to a standard 30-day supply estimation, the claim records `days_supply` = 30, overstating actual coverage. Studies of insulin adherence using PDC should validate `days_supply` distributions against clinical expectations and consider sensitivity analyses with alternative assumptions.

Inhaler supply and dose-counting: Similarly, inhaler `days_supply` depends on the number of puffs per day and the canister's rated dose count, neither of which is standardized in the claim. A rescue (short-acting) inhaler used PRN cannot have a meaningful `days_supply` — the claim field is meaningless for adherence purposes. Controller (maintenance) inhalers have a more predictable dosing pattern, but `days_supply` still requires validation against clinical dosing conventions for the specific product.

Reversal transactions (transaction type B2)

When a previously paid pharmacy claim is voided — because the patient returned the medication, the claim was submitted in error, or the adjudication is being corrected — the pharmacy submits a reversal transaction (identified in NCPDP D.0 by transaction type B2, or in claim files by a transaction type / claim status code indicating void or reversal). A reversal does not physically cancel the original record in most research databases; instead, both the original paid claim and the reversal record appear as separate rows. An analyst who does not remove paired reversal records will count a drug fill that was ultimately never dispensed and retained by the patient. The clean approach is to identify claim pairs (same patient, same NDC, same fill date, same pharmacy) where a reversal matches the original, and drop both. In some data products, reversals are pre-processed and only net paid claims are delivered; always verify the vendor's data dictionary to determine whether reversals are present in the extract and require analyst-level removal.

Compound drug flags

Compounded prescriptions — custom drug preparations mixed by the pharmacy rather than manufactured by a labeler — appear in pharmacy claims with NDC codes that are not registered in the FDA NDC Directory. Some data systems flag these with a compound drug indicator or a specific service type code. Compounded drugs are common in some specialty areas (pediatrics, pain management, dermatology) and should be identified and handled separately from manufactured drugs in exposure code lists, as their NDC codes carry no standard product attributes.

What pharmacy claims do NOT capture — and the exposure-misclassification consequences

  • Inpatient hospital drugs: Medications administered during a hospital admission are part of the
  • Cash-paid fills and discount-card fills (GoodRx, other discount programs): Prescriptions paid
  • Physician samples: Drug samples distributed by pharmaceutical representatives at the prescriber
  • Over-the-counter (OTC) drugs: Medications sold without a prescription — including many antihistamines,

Medical benefit vs pharmacy benefit drugs

A research-critical distinction: not all drugs in clinical use flow through pharmacy claims. Infused biologics (e.g., infliximab for rheumatoid arthritis, bevacizumab for cancer, vedolizumab for IBD) are typically administered in an infusion center or physician's office and billed as a physician-administered drug on the medical benefit — appearing as a HCPCS Level II J-code on the UB-04 institutional claim or CMS-1500 professional claim, not as an NDC on a pharmacy claim. A study of biologic adherence that uses only pharmacy claims will miss every infusion visit for infused biologics, severely underestimating exposure. Researchers must query both the pharmacy benefit (NDC-based fills from NCPDP claims) and the medical benefit (J-code lines from institutional and professional claims) to capture the full utilization of drugs with both oral and infused formulations. Some specialty drugs are available in both a self-administered subcutaneous form (pharmacy benefit, NDC) and an infused form (medical benefit, J-code), requiring separate capture of each channel.

Pros, cons, and trade-offs — specific and comparative

  • vs UB-04 / 837I institutional claims: Pharmacy claims capture every retail and mail-order
  • vs CMS-1500 / 837P professional claims: Professional claims carry J-codes for
  • vs EHR prescribing records: EHR prescription orders capture the prescribing intent but not
  • Real-time adjudication as a data quality advantage: Because pharmacy claims are adjudicated in

When to use

Use NCPDP pharmacy claim fields as the primary data source whenever the research question requires: (1) drug exposure ascertainment for self-administered, outpatient-dispensed medications; (2) any adherence measure requiring `days_supply` (PDC, MPR, persistence); (3) new-user cohort definitions based on first fill of a drug or drug class; (4) generic substitution analyses using DAW codes; (5) drug-level cost-sharing and out-of-pocket burden analyses using copay and plan-paid amounts; (6) refill sequencing and line-of-therapy analyses; (7) pharmacy channel stratification (retail vs mail-order vs specialty). Pharmacy claims from Medicare Part D, commercial PBM data, and Medicaid are the definitive data sources for all outpatient pharmacy utilization and adherence research in the United States.

When NOT to use — and when pharmacy claims are actively misleading or dangerous

  • For inpatient drug exposure: Drugs administered during hospital stays appear on UB-04 revenue
  • For infused biologics and J-coded drugs as the sole data source: A pharmacy-claims-only
  • For adherence measurement in the absence of continuous pharmacy benefit enrollment: A pharmacy
  • For drugs with unreliable `days_supply` estimation (insulin, inhalers, PRN medications):
  • As evidence of drug use during periods outside the claims database's pharmacy benefit capture:

Interpreting the output

The characteristic artifact of an NCPDP pharmacy claims analysis is a patient-level PDC score computed from fill dates and `days_supply` values. Taking the worked example below, the output is PDC = 85/90 ≈ 0.944 over a 90-day window.

Formal interpretation: A PDC of 85/90 = 0.944 means that in 85 of the 90 calendar days in the observation window, this patient had metformin supply on hand based on dispensing dates and pharmacy-reported `days_supply` values. PDC is a descriptive measure of observed fill-pattern coverage, not a causal estimand; it is bounded in [0, 1] and summarizes cumulative day-level coverage without reference to the patient's actual pill-taking behavior. The 5-day uncovered gap (March 2–6) is observable from the raw fill records; whether it represents delayed refill, travel, or a coverage decision is not recoverable from the claim alone.

Practical interpretation: This patient is adherent by the conventional PDC ≥ 0.80 threshold. The 5-day gap early in March is a small deviation from perfect coverage. For a clinical decision or quality-measure context, this patient would be counted in the adherent numerator. For a comparative effectiveness analysis, caution is warranted: PDC 0.944 is a single scalar that erases the timing and pattern of the gap — a patient with the same PDC from six scattered 1-day gaps has the same score but a different exposure trajectory. When the timing of non-adherence matters (e.g., adherence in the first 90 days vs second 90 days post-discharge), report coverage windows, not just the summary PDC.

Worked example

Scenario

An outcomes analyst is measuring metformin adherence for a newly diagnosed type 2 diabetes patient (person_id 5501) over the first 90 days after their prescription start date of 2023-01-01. The analyst pulls all paid NCPDP pharmacy claims for metformin (NDC 00093-5080-05, metformin HCl 500 mg tablets) for this patient, after first removing any reversal transactions. Three fills appear. The analyst needs to compute the proportion of days covered (PDC) using the union rule — counting each calendar day once regardless of overlap — and identify the gap in coverage.

Dataset

Three paid NCPDP pharmacy claims for patient 5501 (reversals pre-removed). NDC normalized to 11-digit HIPAA format. The 90-day observation window is 2023-01-01 through 2023-03-31.

person_idfill_datendc11drug_namedays_supplyquantity_dispenseddaw_codeplan_paid_amtrefill_number
55012023-01-0100093508005metformin HCl 500 mg306012.4
55012023-01-3100093508005metformin HCl 500 mg306012.41
55012023-03-0700093508005metformin HCl 500 mg306012.42

Steps

  • Confirm index fill: refill_number = 0 on the 2023-01-01 fill, confirming this is a new prescription. The 90-day observation window runs from 2023-01-01 through 2023-03-31 (January has 31 days, February has 28 days, March has 31 days; 31 + 28 + 31 = 90 days).

  • Fill A (2023-01-01, days_supply 30): coverage spans 2023-01-01 through 2023-01-30 (last covered day = fill_date + days_supply - 1 = Jan 1 + 29 days = Jan 30). Days covered within window = 30.

  • Fill B (2023-01-31, days_supply 30): immediately follows Fill A with no gap. Coverage spans 2023-01-31 through 2023-03-01. Days covered within window = 30. Fills A and B together form one contiguous block of 30 + 30 = 60 covered days (Jan 1 through Mar 1).

  • Gap: 2023-03-02 through 2023-03-06 — five days with no supply on hand. Fill B's last covered day is Mar 1; Fill C does not begin until Mar 7. Gap length = 5 days.

  • Fill C (2023-03-07, days_supply 30): coverage would extend to 2023-04-05, but the observation window closes 2023-03-31. Days covered within window = 31 - 7 + 1 = 25 (Mar 7 through Mar 31, inclusive).

  • Total unique covered days in the 90-day window = 30 + 30 + 25 = 85. Gap days not covered = 5.

  • DAW code = 0 on all three fills, confirming generic metformin was dispensed without any prescriber or patient brand mandate. Total plan-paid across three fills = 12.40 + 12.40 + 12.40 = 37.20 dollars.

Result

PDC = 85 / 90 ≈ 0.944. This patient is classified as adherent (PDC exceeds the 0.80 conventional threshold). The 5-day gap in early March (Mar 2–6) is observable from the fill record and represents a late refill rather than treatment discontinuation, since Fill C arrives 6 days after Fill B's supply runs out. The plan paid 12.40 + 12.40 + 12.40 = 37.20 dollars for three fills; zero patient copay (typical for a Tier 1 generic).

Timeline Spec

Title

NCPDP pharmacy fills for patient 5501: metformin days_supply coverage in 90-day window

Window
Start

2023-01-01

End

2023-03-31

Label

90-day observation window (denominator)

Events
  • Label

    Fill A (new Rx)

    Start

    2023-01-01

    Length Days

    30

    Quantity

    30 days_supply, DAW 0

  • Label

    Fill B (refill 1)

    Start

    2023-01-31

    Length Days

    30

    Quantity

    30 days_supply, DAW 0

  • Label

    Fill C (refill 2, extends past window)

    Start

    2023-03-07

    Length Days

    30

    Quantity

    30 days_supply; 25 days within window

Spans
  • Kind

    covered

    Start

    2023-01-01

    End

    2023-03-01

    Label

    Fills A+B contiguous: 60 covered days

  • Kind

    gap

    Start

    2023-03-02

    End

    2023-03-06

    Label

    5-day gap (late refill)

  • Kind

    covered

    Start

    2023-03-07

    End

    2023-03-31

    Label

    Fill C within window: 25 covered days

Result
Label

85 covered days / 90 window days = PDC 0.944

Value

0.944

Runnable example

python implementation

Core pharmacy claims operations: (1) load NCPDP claim records, remove reversal transactions, and validate days_supply; (2) compute PDC using the union rule for a fixed observation window with inpatient exclusion; (3) extract DAW code summary for generic...

import pandas as pd
import numpy as np
from datetime import timedelta

# ---------------------------------------------------------------------------
# 1. REVERSAL REMOVAL
#    Remove paid-claim / B2-reversal pairs from the raw pharmacy claims extract.
#    Reversal records are identified by transaction_type == 'B2' or by negative
#    plan_paid_amt, depending on the data vendor's encoding.
# ---------------------------------------------------------------------------

def remove_reversals(df: pd.DataFrame) -> pd.DataFrame:
    """
    Remove reversal transactions and their corresponding original paid claims.

    Strategy:
      - If transaction_type column is present: mark B2 records and their pair
      - If absent: flag rows with negative plan_paid_amt (vendor-specific encoding)
    Returns only net paid claims.
    """
    df = df.copy()
    if "transaction_type" in df.columns:
        # Drop reversal records and their originals
        reversal_keys = df.loc[df["transaction_type"] == "B2",
                               ["person_id", "fill_date", "ndc11"]].drop_duplicates()
        df = df.merge(reversal_keys, on=["person_id", "fill_date", "ndc11"],
                      how="left", indicator=True)
        # Remove both the reversal AND any original with same person/date/ndc
        df = df[df["_merge"] == "left_only"].drop(columns=["_merge"])
        df = df[df["transaction_type"] != "B2"]
    else:
        # Fallback: negative plan_paid_amt indicates a reversal in some encodings
        df = df[df.get("plan_paid_amt", pd.Series([0] * len(df))) >= 0]
    return df.reset_index(drop=True)


# ---------------------------------------------------------------------------
# 2. PDC COMPUTATION (union rule, fixed window, with optional inpatient exclusion)
# ---------------------------------------------------------------------------

def compute_pdc(
    claims: pd.DataFrame,
    window_start: str,
    window_end: str,
    inpatient_spans: list[tuple[str, str]] | None = None,
) -> dict:
    """
    Compute PDC for one patient's pharmacy claims over a fixed observation window.

    Parameters
    ----------
    claims : DataFrame with fill_date (str/date) and days_supply (int) columns.
    window_start, window_end : ISO date strings defining the denominator window.
    inpatient_spans : list of (admission_date, discharge_date) str tuples to
        exclude from both numerator and denominator.

    Returns dict with covered_days, window_days, pdc, gap_days.
    """
    w_start = pd.Timestamp(window_start)
    w_end   = pd.Timestamp(window_end)
    window_days = (w_end - w_start).days + 1   # inclusive

    # Build a boolean array: one entry per calendar day in the window
    coverage = np.zeros(window_days, dtype=bool)
    inpatient_mask = np.zeros(window_days, dtype=bool)

    claims = claims.copy()
    claims["fill_date"] = pd.to_datetime(claims["fill_date"])

    for _, row in claims.iterrows():
        start_idx = max(0, (row["fill_date"] - w_start).days)
        end_idx   = min(window_days, start_idx + int(row["days_supply"]))
        if start_idx < window_days:
            coverage[start_idx:end_idx] = True

    # Mark inpatient days (exclude from numerator + denominator)
    if inpatient_spans:
        for adm, dis in inpatient_spans:
            adm_ts = pd.Timestamp(adm); dis_ts = pd.Timestamp(dis)
            s = max(0, (adm_ts - w_start).days)
            e = min(window_days, (dis_ts - w_start).days + 1)
            if s < window_days:
                inpatient_mask[s:e] = True

    active = ~inpatient_mask   # days eligible for PDC counting
    covered_days = int(coverage[active].sum())
    eligible_days = int(active.sum())
    pdc = covered_days / eligible_days if eligible_days > 0 else None
    gap_days = eligible_days - covered_days

    return {
        "covered_days":  covered_days,
        "window_days":   eligible_days,
        "pdc":           round(pdc, 4) if pdc is not None else None,
        "gap_days":      gap_days,
    }


# ---------------------------------------------------------------------------
# 3. DAW CODE SUMMARY (generic substitution analysis)
# ---------------------------------------------------------------------------

def daw_summary(df: pd.DataFrame) -> pd.DataFrame:
    """
    Summarize DAW code distribution across pharmacy claims.
    DAW 0 = generic dispensed as allowed
    DAW 1 = brand required by prescriber
    DAW 2 = brand requested by patient
    Returns a DataFrame with fill counts and percentage by DAW code.
    """
    daw_labels = {
        0: "Generic dispensed (substitution allowed)",
        1: "Brand required by prescriber",
        2: "Brand requested by patient",
        3: "Substitution allowed - pharmacist selected product",
        4: "Substitution not allowed - generic not in stock",
        5: "Substitution allowed - brand dispensed as generic",
        7: "Substitution not allowed - regulatory required",
        9: "Other",
    }
    counts = df["daw_code"].value_counts().rename("fill_count").reset_index()
    counts.columns = ["daw_code", "fill_count"]
    counts["description"] = counts["daw_code"].map(daw_labels).fillna("Unknown")
    counts["pct"] = (counts["fill_count"] / counts["fill_count"].sum() * 100).round(1)
    return counts.sort_values("daw_code")


# ---------------------------------------------------------------------------
# WORKED EXAMPLE (reproduces patient 5501 from the catalog entry)
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    raw_claims = pd.DataFrame({
        "person_id":    [5501, 5501, 5501],
        "fill_date":    ["2023-01-01", "2023-01-31", "2023-03-07"],
        "ndc11":        ["00093508005"] * 3,
        "drug_name":    ["metformin HCl 500 mg"] * 3,
        "days_supply":  [30, 30, 30],
        "quantity_dispensed": [60, 60, 60],
        "daw_code":     [0, 0, 0],
        "plan_paid_amt":[12.40, 12.40, 12.40],
        "refill_number":[0, 1, 2],
        # No transaction_type column -> reversal fallback to plan_paid_amt check
    })

    net_claims = remove_reversals(raw_claims)
    print(f"Net paid claims after reversal check: {len(net_claims)}")

    result = compute_pdc(net_claims, "2023-01-01", "2023-03-31")
    print(f"\nPDC result for patient 5501:")
    print(f"  Covered days : {result['covered_days']}")   # expected: 85
    print(f"  Window days  : {result['window_days']}")    # expected: 90
    print(f"  PDC          : {result['pdc']}")            # expected: 0.9444
    print(f"  Gap days     : {result['gap_days']}")       # expected: 5

    daw = daw_summary(net_claims)
    print(f"\nDAW code distribution:")
    print(daw.to_string(index=False))
r implementation

Core pharmacy claims operations in R: reversal removal, PDC computation using the union rule, and DAW code summary. Uses base R date arithmetic and dplyr for the summary step. The worked example patient (5501) reproduces the 30 + 30 + 25 = 85 covered days...

library(dplyr)
library(lubridate)

# ---------------------------------------------------------------------------
# 1. REVERSAL REMOVAL
# ---------------------------------------------------------------------------

remove_reversals <- function(df) {
  # If transaction_type column is present, drop B2 records and their originals
  if ("transaction_type" %in% names(df)) {
    reversal_keys <- df |>
      filter(transaction_type == "B2") |>
      select(person_id, fill_date, ndc11) |>
      distinct() |>
      mutate(.reversal = TRUE)
    df <- df |>
      left_join(reversal_keys, by = c("person_id", "fill_date", "ndc11")) |>
      filter(is.na(.reversal), transaction_type != "B2") |>
      select(-.reversal)
  } else if ("plan_paid_amt" %in% names(df)) {
    # Fallback: negative plan_paid_amt signals reversal
    df <- df |> filter(plan_paid_amt >= 0)
  }
  df
}


# ---------------------------------------------------------------------------
# 2. PDC COMPUTATION (union rule, fixed window, optional inpatient exclusion)
# ---------------------------------------------------------------------------

compute_pdc <- function(claims, window_start, window_end,
                        inpatient_spans = NULL) {
  # window_start, window_end: character "YYYY-MM-DD"
  w_start <- as.Date(window_start)
  w_end   <- as.Date(window_end)
  all_days <- seq(w_start, w_end, by = "day")
  n_window <- length(all_days)

  # Build coverage vector (0/1 per calendar day in window)
  covered <- logical(n_window)
  for (i in seq_len(nrow(claims))) {
    fill_d <- as.Date(claims$fill_date[i])
    ds     <- as.integer(claims$days_supply[i])
    last_d <- fill_d + ds - 1L
    # Clip to window
    eff_start <- max(fill_d, w_start)
    eff_end   <- min(last_d, w_end)
    if (eff_start <= eff_end) {
      idx <- as.integer(eff_start - w_start) + 1L
      idx_e <- as.integer(eff_end - w_start) + 1L
      covered[idx:idx_e] <- TRUE
    }
  }

  # Mark inpatient days
  inpatient <- logical(n_window)
  if (!is.null(inpatient_spans)) {
    for (span in inpatient_spans) {
      adm <- as.Date(span[1]); dis <- as.Date(span[2])
      eff_s <- max(adm, w_start); eff_e <- min(dis, w_end)
      if (eff_s <= eff_e) {
        idx_s <- as.integer(eff_s - w_start) + 1L
        idx_e <- as.integer(eff_e - w_start) + 1L
        inpatient[idx_s:idx_e] <- TRUE
      }
    }
  }

  active       <- !inpatient
  covered_days <- sum(covered[active])
  window_days  <- sum(active)
  pdc          <- if (window_days > 0) covered_days / window_days else NA_real_
  gap_days     <- window_days - covered_days

  list(
    covered_days = covered_days,
    window_days  = window_days,
    pdc          = round(pdc, 4),
    gap_days     = gap_days
  )
}


# ---------------------------------------------------------------------------
# 3. DAW CODE SUMMARY
# ---------------------------------------------------------------------------

daw_summary <- function(df) {
  daw_labels <- c(
    "0" = "Generic dispensed (substitution allowed)",
    "1" = "Brand required by prescriber",
    "2" = "Brand requested by patient",
    "3" = "Substitution allowed - pharmacist selected product",
    "4" = "Substitution not allowed - generic not in stock",
    "5" = "Substitution allowed - brand dispensed as generic",
    "7" = "Substitution not allowed - regulatory required",
    "9" = "Other"
  )
  df |>
    count(daw_code, name = "fill_count") |>
    mutate(
      description = daw_labels[as.character(daw_code)],
      pct         = round(fill_count / sum(fill_count) * 100, 1)
    ) |>
    arrange(daw_code)
}


# ---------------------------------------------------------------------------
# WORKED EXAMPLE — patient 5501, reproduces 85/90 = 0.9444 PDC
# ---------------------------------------------------------------------------

raw_claims <- data.frame(
  person_id    = rep(5501L, 3),
  fill_date    = c("2023-01-01", "2023-01-31", "2023-03-07"),
  ndc11        = rep("00093508005", 3),
  drug_name    = rep("metformin HCl 500 mg", 3),
  days_supply  = c(30L, 30L, 30L),
  quantity_dispensed = c(60L, 60L, 60L),
  daw_code     = c(0L, 0L, 0L),
  plan_paid_amt = c(12.40, 12.40, 12.40),
  refill_number = c(0L, 1L, 2L),
  stringsAsFactors = FALSE
)

net_claims <- remove_reversals(raw_claims)
cat("Net paid claims after reversal check:", nrow(net_claims), "\n")

res <- compute_pdc(net_claims, "2023-01-01", "2023-03-31")
cat("\nPDC result for patient 5501:\n")
cat("  Covered days :", res$covered_days, "\n")  # expected: 85
cat("  Window days  :", res$window_days,  "\n")  # expected: 90
cat("  PDC          :", res$pdc,          "\n")  # expected: 0.9444
cat("  Gap days     :", res$gap_days,     "\n")  # expected: 5

daw <- daw_summary(net_claims)
cat("\nDAW code distribution:\n")
print(daw)