← Methods repository
concept

NPI (National Provider Identifier)

A mandatory, permanent, 10-digit "intelligence-free" numeric identifier assigned to every covered health-care provider in the United States under HIPAA, serving as the universal provider key that links claims, NPPES enrollment records, prescribing data, and open-payments files in real-world evidence and health-economics research.

Data_Standardcoding-systemdata-standardprimitiveproviderlinkageclaimsnppeshipaa
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

An NPI is a permanent 10-digit number that every U.S. health-care provider — a doctor, a hospital, a pharmacy, a clinic — must have to bill insurance for care. Think of it as a universal employee ID for the health system: the same number appears on every claim a provider submits to every insurer. Researchers use it as the key to link provider information across databases — for example, joining a drug prescription to the doctor who wrote it, or comparing surgical outcomes across surgeons. The catch is that hospitals can have many NPIs and the number on a claim does not always identify the individual who actually delivered the care.

The National Provider Identifier (NPI) is the single, nationwide standard identifier for covered health-care providers mandated by the Health Insurance Portability and Accountability Act (HIPAA) Administrative Simplification provisions. CMS began enumerating NPIs through the National Plan and Provider Enumeration System (NPPES) in 2004, and the compliance deadline for most covered entities was May 2007. Every covered provider — individual clinicians, group practices, hospitals, laboratories, pharmacies, and hundreds of other entity types — must obtain an NPI to submit claims or conduct electronic administrative transactions covered by HIPAA. The system currently holds more than five million active NPI records.

Structure and design philosophy

The NPI is deliberately intelligence-free: no embedded meaning encodes state, specialty, organization type, or practice location. Digits 1–9 are assigned sequentially by NPPES; digit 10 is a Luhn check digit computed by prepending the issuer prefix 80840 (designating US health-care applications under the international ISO/IEC 7812 card-numbering standard) to the nine assigned digits, then applying the standard Luhn algorithm. The intelligence-free design was intentional: legacy identifiers such as the Unique Physician Identification Number (UPIN), Drug Enforcement Administration (DEA) number, and payer-assigned provider numbers encoded specialty or location in their structure, creating identification fragmentation and requiring provider-specific crosswalks for each payer. NPI eliminates those silos — the same 10 digits identify a provider to any covered entity.

Type 1 vs Type 2 NPI

NPPES distinguishes two enumeration types. A Type 1 NPI is issued to an individual health-care provider — a physician, nurse practitioner, physical therapist, pharmacist, or any other natural person who renders or furnishes health-care services. A Type 2 NPI is issued to a health-care organization — a hospital, clinic, group practice, home health agency, or other entity that furnishes health care through individuals. A single physician who also owns a solo practice can hold both a Type 1 NPI (as the individual clinician) and a Type 2 NPI (for the practice entity). Large health systems routinely hold dozens or hundreds of Type 2 NPIs — one per organizational subpart (hospital, outpatient clinic, ambulatory surgery center, specialty division) — making facility-level aggregation the single most common NPI trap in RWE studies.

NPPES and the public data dissemination file

NPPES is the federal registry that enumerates NPIs. The NPI Registry web portal (https://npiregistry.cms.hhs.gov) supports individual lookups. More valuable for researchers is the NPPES Data Dissemination monthly file: a complete public-domain snapshot of every active NPI record, downloadable from CMS, containing provider name, practice and mailing addresses, phone numbers, reported taxonomy codes, enumeration date, and deactivation status (in a separate deactivation file). The dissemination file is public domain and free to use without restriction. It is the standard reference file for enriching claims or EHR data with provider specialty (taxonomy) and for constructing provider-level crosswalk tables.

Taxonomy codes

Each NPI record carries one or more provider taxonomy codes — 10- character alphanumeric codes maintained by the National Uniform Claim Committee (NUCC) that classify provider type and specialty (e.g., 207Q00000X = Family Medicine, 207R00000X = Internal Medicine, 282N00000X = General Acute Care Hospital). A provider may self-report multiple taxonomies; one is flagged as the primary. Taxonomy codes are critical for specialty-based sub-grouping in pharmacoepidemiology (prescriber specialty studies, specialist vs primary-care attribution) and in workforce research. Critically, taxonomy codes are self-reported and are not equivalent to board certification: a provider may report a taxonomy that differs from their actual clinical specialty, and no systematic validation mechanism compares NPPES taxonomy against credentialing records. Specialty misclassification arising from taxonomy code reliance is a recognized limitation in the provider attribution literature.

Where NPIs appear on claims

Claims carry NPIs in multiple fields serving distinct provider roles, and which NPI you select determines who gets credited for a service — a choice that materially changes results in prescriber-attribution, surgical-outcome, and volume–outcome studies: - Professional (CMS-1500 / 837P): Box 24J = rendering provider (who performed the service); Box 33 = billing provider (who billed — often a group practice). The rendering NPI is the right choice for most provider-attribution studies; using the billing NPI attributes all services to the group practice entity rather than the individual clinician. - Institutional (UB-04 / 837I): Attending provider (FL 76), operating provider (FL 77), other operating provider (FL 78), and service facility NPI. For surgical-outcome studies, the operating-provider NPI is the correct attribution field; for readmission studies, the attending-provider NPI is usually preferred. - Pharmacy (NCPDP): Prescriber NPI and dispensing pharmacy NPI appear in the pharmacy claim; these are critical for prescriber-level drug-utilization studies and for identifying pharmacy provider type.

RWE and HEOR applications

The NPI serves as the provider-level key for a wide range of real-world studies: - Prescriber attribution: linking drug fills to the prescribing clinician to study prescribing patterns, guideline adherence, or off-label use. - Surgeon and proceduralist attribution: linking procedures to operating-provider NPI for volume–outcome or learning-curve analyses. - Provider-level clustering: accounting for within-provider correlation when the same provider treats many patients in the same cohort (shared frailty or provider fixed effects). - Cross-database provider linkage: joining claims to NPPES enrichment (specialty, address), to CMS Open Payments (industry payments by provider NPI), to the CMS Medicare Provider Utilization and Payment Data, and to state licensure or medical-board files. - Practice-level aggregation: grouping individual provider NPIs under a shared billing NPI or organizational NPI to define the "practice" as the unit of analysis.

Pros, cons, and trade-offs

- vs legacy identifiers (UPIN, DEA, payer-assigned IDs): UPIN was retired at the NPI transition; crosswalks (e.g., Parsons et al. 2017, Medical Care) exist for legacy Medicare claims that predate NPI compliance. DEA numbers persist in controlled-substance prescription records but are not present on claims. NPI is the only identifier that spans all covered entities, all payers, and all claim types since the 2007 compliance deadline. Prefer NPI as the primary provider key for any study using post-2007 data; supplement with a UPIN crosswalk for pre-2007 Medicare data. - vs provider name matching: Name-based linkage across databases is noisy (misspellings, name changes, middle-initial variation, common surnames). NPI is exact-match — 10 digits, no ambiguity. Always prefer NPI for cross-database provider linkage when both sources carry NPI; fall back to probabilistic name/address matching only when NPI is absent from one source. - vs Tax Identification Number (TIN) for practice grouping: TIN groups providers under a billing entity and is widely used for practice-level analysis. However, TINs are not public (they are protected PII in claims extracts), whereas the billing NPI (Type 2 or group practice NPI) is observable and can proxy practice affiliation. Neither perfectly maps to the clinical concept of a "practice." TIN-based grouping typically produces tighter practice clusters; NPI-based grouping is more portable but may over-split or under-aggregate. - Limitation — registry staleness: NPPES is self-maintained. Providers who retire, relocate, or change specialty are not required to proactively update their record, and CMS deactivation is reactive (based on claims inactivity or reports of death/license lapse). Stale addresses and taxonomy codes are common; do not use NPPES address data as a proxy for current practice location without checking activity recency in claims. - Limitation — organizational NPI granularity: There is no standardized rule governing how deeply a health system must sub-enumerate Type 2 NPIs. One system may enumerate at the enterprise level (one Type 2 NPI covering fifty sites); another at the facility level (one per hospital building). This inconsistency makes facility-level volume aggregation unreliable without supplementary data (CMS Certification Number for hospitals, facility address crosswalk). - Limitation — incident-to and locum billing: Under Medicare incident-to billing rules, a physician assistant's or nurse practitioner's service may be billed under the supervising physician's NPI. Locum tenens physicians may bill under the absent physician's NPI. In both cases, the rendering NPI does not identify the clinician who actually delivered the care — a critical limitation for prescriber-attribution and provider-exposure studies.

When to use

- As the universal key for any provider-linkage join across claims, NPPES, Open Payments, Medicare utilization files, or state licensure data. - As the rendering-provider identifier for prescriber- or proceduralist-attribution in pharmacoepidemiology, comparative effectiveness, and volume–outcome studies. - As a data-quality filter: an NPI failing the Luhn check digit is an invalid record and should be flagged before any linkage step. - As the grouping key for provider-level clustering or fixed-effects analysis to account for within-provider patient clustering. - In conjunction with the NPPES monthly dissemination file to attach specialty (taxonomy), enumeration date, and entity type to any provider in a claims or EHR dataset.

When NOT to use — and when it is actively misleading or dangerous

- Do not use the billing NPI as the provider of service in individual-level attribution studies. The billing NPI identifies who billed (often a group practice or health system), not who rendered the service. Using billing NPI for prescriber or surgeon attribution will collapse all services in the practice to a single entity, eliminating provider-level variation and rendering volume–outcome or learning-curve analyses nonsensical. - Do not use NPI alone to define the "practice" without understanding organizational sub-enumeration. Two hospitals from the same system that share a single Type 2 NPI cannot be distinguished from each other; two that hold separate Type 2 NPIs look like different practices. Aggregating by Type 2 NPI produces incomparable granularity across systems and may substantially misclassify the practice unit. - Do not treat NPPES taxonomy code as a validated specialty for exposure or confounder classification without sensitivity analysis. Taxonomy codes are self-reported and not cross-validated against board certification or credentialing records. For studies where specialty misclassification could bias the result materially (e.g., a study restricted to cardiologists), supplement taxonomy-based classification with procedure-code or referral- pattern validation. - Do not use NPI-based provider identity when incident-to billing or locum arrangements are common in the population studied. In studies of mid-level practitioners (NP, PA), incident-to billing significantly under-identifies the actual clinician. Design rules that detect and exclude or flag incident-to claims (presence of a supervising provider NPI, claim modifier codes) before making inferences about NP/PA practice. - Do not extrapolate pre-2007 provider attribution using NPI without a validated UPIN/NPI crosswalk. NPI records for pre-compliance claims are sparse; studies spanning the 2005–2008 transition period require explicit handling of the identifier change to avoid apparent provider turnover that is actually a field-switching artifact.

Data-source operational depth

- Claims (professional/CMS-1500): The rendering-provider NPI in Box 24J is the correct attribution field for individual clinicians. Always extract both the rendering NPI (Box 24J) and the billing NPI (Box 33) and join each to NPPES separately: rendering NPI to the individual-provider record; billing NPI to the organization record. Verify that the rendering NPI is Type 1 and the billing NPI is Type 1 or Type 2, depending on whether the practice bills under an individual or group enrollment. Stale taxonomy codes in NPPES are most problematic for specialty classification in older cohort windows; use the most recent NPPES dissemination snapshot that post-dates the study period as the reference file. - Claims (institutional/UB-04): Use the attending-provider NPI (FL 76) for admission- and discharge-level analyses (readmission, LOS, mortality) and the operating-provider NPI (FL 77) for surgical-procedure attribution. The service facility NPI (FL 82) identifies the billing facility, not the treating clinician. For volume–outcome analyses, the operating-provider NPI is the unit of analysis; cluster standard errors at the operating-provider level. - EHR: EHR systems typically store the NPI of the ordering/attending provider in structured fields alongside the encounter. Verify that the EHR NPI matches the claims rendering NPI for the same encounter using a validation subset before relying on EHR-derived provider identity for linkage. EHR-based NPI is usually cleaner for ambulatory visits than claims-based NPI because it is recorded at the point of care rather than inferred from billing. - Registry: Disease registries (SEER, cancer registries, trauma registries) increasingly include the treating-provider NPI as a linkage key. SEER-Medicare NPI linkages permit surgeon volume–outcome and oncologist specialty studies. Verify linkage rates and assess whether unlinked records differ systematically by provider type or volume. - Linked (Open Payments / CMS utilization): CMS publishes Open Payments (industry transfers by NPI), the Medicare Physician and Other Practitioners utilization file (service counts by NPI and HCPCS), and the Medicare Part D Prescriber file (drug prescribing by NPI). All three join on NPI directly. These linkages support prescriber-industry relationship studies, off-label prescribing analyses, and conflict-of-interest confounding adjustment.

Worked example

Scenario

A pharmacoepidemiology analyst is building a prescriber-attribution dataset for a study of statin prescribing in Medicare claims. Before joining any NPPES specialty data, the analyst writes a quality-check routine to confirm that every NPI in the pharmacy claims is structurally valid using the Luhn algorithm. The standard documentation example NPI is 1234567893. The analyst wants to verify that this NPI passes the check-digit test step by step.

Dataset

The NPI being validated and the intermediate values produced at each Luhn algorithm step. The algorithm prepends the 5-digit issuer prefix 80840 to the 9-digit NPI prefix, producing a 14-digit working string, then computes the check digit.

stepvalueexplanation
Input NPI123456789310-digit NPI to validate
9-digit prefix123456789Digits 1-9; digit 10 (= 3) is the check digit to verify
Issuer prefix80840Fixed prefix for US health-care applications (ISO/IEC 7812)
15-digit working string80840123456789380840 + 123456789 + check_digit_3
Luhn sum70Sum of all 15 digit contributions after doubling; 70 mod 10 = 0 -> valid

Steps

  • Build the 15-digit working string by appending all 10 NPI digits to the issuer prefix: "80840" followed by "1234567893" gives the working string 808401234567893 (15 digits total).

  • Apply the Luhn rule: starting from the rightmost digit and moving left, keep every odd-position digit (positions 1, 3, 5, ... from the right) unchanged, and double every even-position digit (positions 2, 4, 6, ... from the right). If doubling produces a result greater than 9, subtract 9 from that result to get the contribution.

  • Work through 808401234567893 right to left, computing each contribution. Odd positions (kept): pos 1 digit 3 contributes 3; pos 3 digit 8 contributes 8; pos 5 digit 6 contributes 6; pos 7 digit 4 contributes 4; pos 9 digit 2 contributes 2; pos 11 digit 0 contributes 0; pos 13 digit 8 contributes 8; pos 15 digit 8 contributes 8. Even positions (doubled, subtract 9 if over 9): pos 2 digit 9 doubles to 18, subtract 9, contributes 9; pos 4 digit 7 doubles to 14, subtract 9, contributes 5; pos 6 digit 5 doubles to 10, subtract 9, contributes 1; pos 8 digit 3 doubles to 6, contributes 6; pos 10 digit 1 doubles to 2, contributes 2; pos 12 digit 4 doubles to 8, contributes 8; pos 14 digit 0 doubles to 0, contributes 0.

  • Sum all 15 contributions: 3+9+8+5+6+1+4+6+2+2+0+8+8+0+8 = 70.

  • Apply the validity rule: 70 divided by 10 leaves remainder 0. A Luhn sum divisible by 10 confirms the check digit is correct and the NPI is structurally valid. Any NPI whose Luhn sum is not divisible by 10 is a transcription error or fabricated value and must be flagged and excluded before any NPPES join.

  • To compute the check digit from scratch (rather than verify it), use only the 14-digit prefix 80840123456789. Double even-from-right positions within that 14-digit string. The contributions from those 14 digits sum to 67. The check digit equals (10 minus 7) which is 3, matching the 10th digit of NPI 1234567893 exactly.

Result

NPI 1234567893 passes the Luhn check. Contributions 3+9+8+5+6+1+4+6+2+2+0+8+8+0+8 = 70, and 70 is divisible by 10 (valid). The check digit was independently derived as 10 minus 7 = 3, confirming digit 10. Any NPI that does not produce a Luhn sum divisible by 10 is invalid and must be excluded before any NPPES join.

Runnable example

python implementation

Two tools: (1) a vectorized Luhn check-digit validator that screens every NPI in a claims or NPPES file before any join — a structurally invalid NPI cannot link correctly regardless of other data quality; (2) an NPPES monthly-file join that attaches the...

"""
NPI data-quality tools for RWE claims and NPPES linkage.

Two utilities:
  1. luhn_valid(npi) / validate_npis(df) — Luhn check-digit screen.
  2. join_nppes(claims_df, nppes_path, deactivated_path=None) — attach taxonomy + type.

The Luhn algorithm for NPI:
  Step 1: Prepend the 5-digit issuer prefix 80840 to the 9 NPI prefix digits.
          Working string = "80840" + npi[:9]  (14 digits)
  Step 2: In the 15-digit string (prefix + all 10 NPI digits), double every digit at
          an even position counting from the right (positions 2, 4, 6, ...).
          Equivalently, in the 14-digit working string, double every digit at an ODD
          position counting from the right (positions 1, 3, 5, ...).
          If the doubled value > 9, subtract 9.
  Step 3: Sum all contributions from the 14-digit string.
          check_digit = (10 - (total % 10)) % 10
  Step 4: Compare computed check_digit to npi[9] (the 10th digit).
"""
import re
import pandas as pd
from pathlib import Path


_NPI_DIGITS = re.compile(r"^\d{10}$")
_ISSUER_PREFIX = "80840"


def luhn_valid(npi: str) -> bool:
    """Return True if *npi* is a structurally valid 10-digit NPI.

    Steps:
      1. Must be exactly 10 ASCII digits.
      2. Build the 14-digit working string: '80840' + npi[:9].
      3. Double every digit at an odd position from the right (1-indexed)
         in the working string; subtract 9 if > 9.
      4. Sum all 14 contributions.
      5. check_digit = (10 - total % 10) % 10
      6. Valid iff check_digit == int(npi[9]).
    """
    if not isinstance(npi, str) or not _NPI_DIGITS.match(npi):
        return False
    working = _ISSUER_PREFIX + npi[:9]   # 14 digits
    total = 0
    for i, ch in enumerate(reversed(working)):
        d = int(ch)
        # Positions from right are 1-indexed in the 15-digit final string.
        # In the 14-digit working string, reversed index 0 corresponds to
        # position 2 from right in the 15-digit string -> double.
        if i % 2 == 0:   # positions 2, 4, 6, ... from right -> double
            d *= 2
            if d > 9:
                d -= 9
        total += d
    check = (10 - total % 10) % 10
    return check == int(npi[9])


def validate_npis(df: pd.DataFrame, npi_col: str = "rendering_npi") -> pd.DataFrame:
    """Add a boolean column '<npi_col>_valid' to *df*.

    Any row where the NPI fails the Luhn check is a data-quality error
    and should be excluded from NPPES joins or provider-attribution analyses.
    """
    df = df.copy()
    df[f"{npi_col}_valid"] = df[npi_col].astype(str).apply(luhn_valid)
    n_invalid = (~df[f"{npi_col}_valid"]).sum()
    if n_invalid:
        print(f"[NPI QC] {n_invalid:,} invalid NPIs in '{npi_col}' ({n_invalid/len(df)*100:.1f}%)")
    return df


def join_nppes(
    claims_df: pd.DataFrame,
    nppes_path: str | Path,
    deactivated_path: str | Path | None = None,
    npi_col: str = "rendering_npi",
) -> pd.DataFrame:
    """Join NPPES dissemination file to claims on rendering NPI.

    Attaches:
      - provider_type:     '1' (individual) or '2' (organization)
      - primary_taxonomy:  primary taxonomy code (10-char NUCC code)
      - provider_name:     last + first name (Type 1) or org name (Type 2)
      - npi_deactivated:   True if NPI appears in the deactivation file

    NPPES dissemination file column names use the official CMS header names.
    Download the monthly full-replacement file from:
    https://download.cms.gov/nppes/NPI_Files.html

    Parameters
    ----------
    claims_df : DataFrame with at least one NPI column.
    nppes_path : Path to the NPPES full-replacement CSV (NPI_full_*.csv).
    deactivated_path : Optional path to the deactivation CSV (NPPES_Deactivated_*.csv).
    npi_col : Name of the NPI column in claims_df to join on.
    """
    # Load only needed NPPES columns to limit memory usage
    nppes_cols = [
        "NPI",
        "Entity Type Code",              # 1 = individual, 2 = organization
        "Healthcare Provider Taxonomy Code_1",
        "Is Primary Taxonomy Switch_1",
        "Provider Last Name (Legal Name)",
        "Provider First Name",
        "Provider Organization Name (Legal Business Name)",
    ]
    nppes = pd.read_csv(
        nppes_path,
        usecols=lambda c: c in nppes_cols,
        dtype=str,
        low_memory=False,
    )
    nppes = nppes.rename(columns={
        "NPI": "npi",
        "Entity Type Code": "provider_type",
        "Healthcare Provider Taxonomy Code_1": "primary_taxonomy_raw",
        "Is Primary Taxonomy Switch_1": "is_primary_flag",
        "Provider Last Name (Legal Name)": "last_name",
        "Provider First Name": "first_name",
        "Provider Organization Name (Legal Business Name)": "org_name",
    })

    # Use the primary taxonomy when flagged 'Y'; otherwise use Taxonomy_1 as fallback
    nppes["primary_taxonomy"] = nppes["primary_taxonomy_raw"]

    nppes["provider_name"] = nppes.apply(
        lambda r: (
            f"{r['last_name']}, {r['first_name']}".strip(", ")
            if r["provider_type"] == "1"
            else r["org_name"]
        ),
        axis=1,
    )

    nppes_slim = nppes[["npi", "provider_type", "primary_taxonomy", "provider_name"]]

    # Optional: flag deactivated NPIs
    if deactivated_path is not None:
        deact = pd.read_csv(deactivated_path, usecols=["NPI"], dtype=str)
        deact["npi_deactivated"] = True
        deact = deact.rename(columns={"NPI": "npi"})
        nppes_slim = nppes_slim.merge(deact, on="npi", how="left")
        nppes_slim["npi_deactivated"] = nppes_slim["npi_deactivated"].fillna(False)

    # Join to claims
    result = claims_df.merge(
        nppes_slim,
        left_on=npi_col,
        right_on="npi",
        how="left",
    ).drop(columns=["npi"])

    # Report match rate
    matched = result["provider_type"].notna().sum()
    print(f"[NPPES join] {matched:,}/{len(result):,} claims matched "
          f"({matched/len(result)*100:.1f}%)")

    return result


# ------------------------------------------------------------------ quick demo
if __name__ == "__main__":
    # Verify the standard documentation example NPI: 1234567893
    test_cases = [
        ("1234567893", True,  "CMS documentation example"),
        ("1234567890", False, "bad check digit"),
        ("123456789",  False, "too short"),
        ("12345678901",False, "too long"),
        ("1234567X93", False, "non-digit character"),
    ]
    print("NPI Luhn validator unit tests:")
    for npi, expected, label in test_cases:
        result = luhn_valid(npi)
        status = "PASS" if result == expected else "FAIL"
        print(f"  [{status}] luhn_valid({npi!r}) = {result}  ({label})")
r implementation

R equivalents of the Luhn validator and the NPPES taxonomy join, using base R and data.table for performance on the full NPPES dissemination file (approximately 8 million rows). The Luhn function is vectorized over a character vector of NPIs. The join...

# NPI data-quality tools for R: Luhn validator + NPPES taxonomy join
# Designed for data.table; base R only otherwise.
library(data.table)

# ------------------------------------------------------------------
# 1. Luhn check-digit validator (vectorized)
# ------------------------------------------------------------------
# Algorithm:
#   1. Pad NPI to 10 characters; reject non-10-digit strings.
#   2. Build 14-digit working string: paste0("80840", substr(npi, 1, 9))
#   3. Reverse; double digits at odd reversed positions (1, 3, 5, ...);
#      subtract 9 if doubled value > 9.
#   4. check_digit = (10 - sum %% 10) %% 10
#   5. Valid iff check_digit == as.integer(substr(npi, 10, 10))
luhn_valid_npi <- function(npi) {
  # Coerce to character, remove whitespace
  npi <- trimws(as.character(npi))
  valid <- grepl("^[0-9]{10}$", npi)

  result <- rep(FALSE, length(npi))
  if (!any(valid)) return(result)

  working <- paste0("80840", substr(npi[valid], 1, 9))  # 14 chars

  check_digit <- vapply(working, function(ws) {
    digits <- as.integer(strsplit(ws, "")[[1]])
    rev_d  <- rev(digits)
    # Odd reversed positions (1-indexed): positions 1, 3, 5, ... -> double
    for (i in seq_along(rev_d)) {
      if (i %% 2 == 1) {                 # odd reversed position -> double
        rev_d[i] <- rev_d[i] * 2L
        if (rev_d[i] > 9L) rev_d[i] <- rev_d[i] - 9L
      }
    }
    (10L - (sum(rev_d) %% 10L)) %% 10L
  }, integer(1L))

  declared_check <- as.integer(substr(npi[valid], 10, 10))
  result[valid] <- (check_digit == declared_check)
  result
}

# Quick unit tests
test_npis <- c("1234567893", "1234567890", "123456789", "1234567X93")
expected  <- c(TRUE, FALSE, FALSE, FALSE)
stopifnot(all(luhn_valid_npi(test_npis) == expected))
message("Luhn validator: all unit tests passed")


# ------------------------------------------------------------------
# 2. NPPES monthly dissemination file join
# ------------------------------------------------------------------
# Download the full replacement file from CMS:
# https://download.cms.gov/nppes/NPI_Files.html
# The CSV has ~8M rows; data.table select= loads only needed columns.
#
# Returns claims_dt with columns added:
#   provider_type    : "1" (individual) or "2" (organization)
#   primary_taxonomy : 10-char NUCC taxonomy code
#   provider_name    : last,first (Type 1) or org name (Type 2)
#   npi_deactivated  : logical (TRUE = in deactivation file)
join_nppes <- function(claims_dt,
                       nppes_path,
                       deactivated_path = NULL,
                       npi_col          = "rendering_npi") {

  stopifnot(is.data.table(claims_dt), npi_col %in% names(claims_dt))

  # Read NPPES — select only required columns by position via fread skip header trick
  nppes_all <- fread(
    nppes_path,
    select = c(
      "NPI",
      "Entity Type Code",
      "Healthcare Provider Taxonomy Code_1",
      "Is Primary Taxonomy Switch_1",
      "Provider Last Name (Legal Name)",
      "Provider First Name",
      "Provider Organization Name (Legal Business Name)"
    ),
    colClasses = "character",
    showProgress = TRUE
  )

  setnames(nppes_all,
    old = c("NPI",
            "Entity Type Code",
            "Healthcare Provider Taxonomy Code_1",
            "Is Primary Taxonomy Switch_1",
            "Provider Last Name (Legal Name)",
            "Provider First Name",
            "Provider Organization Name (Legal Business Name)"),
    new = c("npi", "provider_type", "primary_taxonomy",
            "is_primary_flag", "last_name", "first_name", "org_name"))

  nppes_all[, provider_name := ifelse(
    provider_type == "1",
    trimws(paste(last_name, first_name, sep = ", ")),
    org_name
  )]
  nppes_slim <- nppes_all[, .(npi, provider_type, primary_taxonomy, provider_name)]

  # Optional deactivation flag
  if (!is.null(deactivated_path)) {
    deact <- fread(deactivated_path, select = "NPI", colClasses = "character")
    setnames(deact, "NPI", "npi")
    deact[, npi_deactivated := TRUE]
    nppes_slim <- merge(nppes_slim, deact, by = "npi", all.x = TRUE)
    nppes_slim[is.na(npi_deactivated), npi_deactivated := FALSE]
  } else {
    nppes_slim[, npi_deactivated := FALSE]
  }

  setkey(nppes_slim, npi)

  # Join to claims
  result <- merge(
    claims_dt,
    nppes_slim,
    by.x = npi_col,
    by.y = "npi",
    all.x = TRUE
  )

  matched <- sum(!is.na(result$provider_type))
  message(sprintf("[NPPES join] %s/%s claims matched (%.1f%%)",
                  format(matched, big.mark = ","),
                  format(nrow(result), big.mark = ","),
                  matched / nrow(result) * 100))
  result
}