← Methods repository
concept

Diagnosis Phenotype Algorithm (1 IP / 2 OP, Time Window)

A claims-based case-finding rule that classifies a patient as having a condition if they have at least one inpatient claim, or two outpatient claims on different service dates within a defined time window, carrying the relevant diagnosis code, whose performance (PPV, sensitivity, specificity) is condition-, position-, and data-source-specific and must be validated.

Outcome_Measurephenotypediagnosis-algorithmclaims-phenotyping1ip-2opppv-validationoutcome-algorithmmisclassificationdata-quality
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

A diagnosis phenotype algorithm is a rule that combs through billing records to decide whether a patient actually has a given condition, rather than just having a code that was entered to rule something out. The '1 inpatient OR 2 outpatient' version says a patient counts as a case if they were admitted to the hospital with the relevant billing code at least once, OR if a doctor's office or outpatient clinic billed that code on at least two separate visits that are spread far enough apart in time. The gap between the two outpatient visits and the overall time window both matter — without them, a single 'could this be condition X?' code from one afternoon visit would create a false case. Every such rule carries error: it will flag some patients who do not truly have the condition (false positives) and miss others who do (false negatives), so analysts always report how accurate the rule was when tested against actual medical records.

The 1 inpatient or 2 outpatient (1 IP / 2 OP) diagnosis phenotype is the workhorse case-finding rule of administrative-claims research. Because claims carry billing codes (ICD-9/10-CM) rather than clinical findings, a patient is classified as a case when they accumulate enough coded evidence to make a billing artifact an unlikely explanation: a single inpatient claim with the diagnosis (one institutional encounter that was adjudicated and paid), OR two outpatient claims with the diagnosis on different service dates within a pre-specified window. The single-IP arm leans on the higher specificity of an admission diagnosis; the two-OP arm requires temporal corroboration so that a one-off "rule-out" or screening code does not by itself create a case. The window length encodes the disease's tempo: 7-30 days for acute events (MI, ischemic stroke, PE), 180-365 days for chronic conditions (atrial fibrillation, diabetes, COPD). For incident (new-onset) phenotypes the rule is paired with a lookback washout (e.g., no qualifying code in the prior 365 days of continuous enrollment); without it the algorithm captures prevalent, not incident, disease.

Core conceptual distinction

. The rule is a measurement instrument with error, not a diagnosis. Three knobs trade positive predictive value (PPV) against sensitivity. (1) Setting / arm structure — 1 IP OR 2 OP balances the specificity of admissions against the sensitivity of outpatient capture; an IP-only rule raises PPV but misses outpatient-managed disease. (2) Code position — primary (principal) position reflects the reason for the encounter and is more specific; any-position captures comorbidity coding but is noisier and more vulnerable to copy-forward and rule-out codes. (3) Window and washout — the OP-to-OP window and the incident washout jointly determine whether you measure new onset, and how much delayed-second-visit loss you accept. The two failure directions are not symmetric: false positives (rule-out/screening codes coded as confirmed disease) dilute toward the null in a comparative study, while differential misclassification by exposure (e.g., new initiators are seen more often and coded more completely) biases in an unpredictable direction. Never treat a code as a clinical diagnosis without validating the algorithm in your own data source and population, and pre-specify position, window, and washout in the protocol before looking at outcomes.

Pros, cons, and trade-offs

. - vs a broad single-code rule (1 claim, any position, no window): the 1 IP / 2 OP rule sharply cuts false positives from isolated rule-out and screening codes and is widely validated to PPV >80% for many conditions. Cost: it loses the few true cases who have only one outpatient code and die or disenroll before a second visit; it is more code to build. Prefer 1 IP / 2 OP for almost any outcome or covariate that drives an effect estimate. - vs a high-specificity rule (IP-only, primary position, plus procedure/lab/drug confirmation): the standard rule has higher sensitivity and is easier to harmonize across databases. Cost: lower PPV than a confirmation-augmented rule. Prefer the high-specificity variant when a false positive is costly (e.g., a serious safety outcome that triggers a label change) and you can tolerate missing milder, outpatient-managed cases. - vs EHR / registry phenotypes (problem lists, NLP on notes, registry adjudication): claims rules scale to millions of patients and decades of history and are consistent across payers if harmonized. Cost: lower clinical specificity than a chart-adjudicated or NLP-confirmed phenotype; they miss out-of-network and uncoded events. Prefer claims for large comparative studies, and use linked EHR/registry as the validation gold standard rather than the primary ascertainment source.

When to use

. Identifying outcomes, covariates, or cohort-entry diagnoses in administrative claims (Medicare FFS, commercial) or claims-linked data, especially when (a) a published, validated 1 IP / 2 OP algorithm exists for your condition and code era, and (b) a chart-review or registry-linked subset is available to estimate PPV (and ideally sensitivity) in your population. It is the default for outcome ascertainment in pharmacoepidemiologic safety and comparative-effectiveness studies and for covariate construction feeding propensity or high-dimensional propensity scores.

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

. - No validation is feasible and PPV is plausibly low or differential. If the condition is heavily coded as "rule-out" (chest pain coded as MI before troponin returns; "screening for malignancy" before biopsy) and you cannot estimate PPV, an unvalidated rule can manufacture or erase an effect — quantitative bias analysis or a linked validation subset is mandatory before the estimate is trusted. - The window or washout is misaligned with the disease tempo. A 30-day chronic-disease window or a too-short incident washout converts prevalent cases into "incident" ones, importing immortal time and survivor effects into the analysis. - MA-only person-time is treated as observed. In Medicare Advantage, fee-for-service institutional and carrier claims are largely absent in legacy claims products; "no prior code" in MA-only spans is missingness, not a clean washout, and silently fabricates incident cases. Restrict the washout to FFS-observable (Parts A/B) enrollment. - The algorithm differs across arms. Applying a different position/window/setting rule, or a different data source, to exposed vs comparator patients guarantees differential misclassification — the single most dangerous error here.

Data-source operational depth

. - Claims (Medicare FFS / commercial): build from institutional/facility claims (use the discharge date for the IP arm) plus carrier/professional and outpatient-facility claims (use the service date for the OP arm). Deduplicate same-day OP claims so two line items on one visit count as one. Require continuous medical enrollment across the entire washout so absence of a prior code is observed, not unobserved. Failure modes: rule-out/screening codes inflate OP false positives (mitigate with primary position or a confirmatory procedure/drug); bundled or interim claims and later adjustments perturb counts (prefer final paid claims); MA-only person-time lacks FFS claims so washout and OP counting break (exclude or flag MA-only spans); differential competing risks — in elderly claims, patients who die before a second OP visit are missed for the 2-OP arm, and if death rates differ by exposure this is differential. - EHR: the IP/OP distinction maps to encounter type; the "second OP claim" can be a second encounter or one encounter plus an active problem-list entry, and NLP on notes can confirm a coded diagnosis. Strength: clinical detail to adjudicate. Weakness: visit-driven capture — sicker, more-engaged patients are coded more completely, and care that leaks out of the system is invisible; link to claims for a complete encounter history. - Registry: disease registries (SEER for cancer, stroke/MI registries) are the validation gold standard for PPV and for the cases the claims rule misses, and registry death data anchor competing-risk and censoring logic. Weakness: incomplete pharmacy/encounter timing — link to claims to apply the 1 IP / 2 OP rule and to recover timing. - Linked claims-EHR / claims-registry: the ideal substrate (scale + adjudication), but probabilistic linkage error can be differential by severity (sicker patients link more reliably), biasing sensitivity estimates; quantify linkage error and reconcile service vs discharge vs registry diagnosis dates before assigning the index date.

Worked claims example (incident atrial fibrillation)

Question: incident AF ascertainment in a Medicare FFS + commercial database, ICD-10 I48.x (atrial fibrillation/flutter). Algorithm: 1 IP discharge diagnosis (any position) OR 2 OP claims on service dates ≥7 days apart within a 365-day window, with a 365-day incident washout (no I48.x in the prior year) and continuous Part A/B (or commercial medical) enrollment across that washout; same rule applied identically to every study arm. Suppose 5,000 patients have at least one I48.x code. Applying the rule: 1,200 qualify on a single inpatient discharge code; of the remainder, 2,800 have ≥2 OP codes ≥7 days apart within 365 days (the other outpatient-only patients have a single isolated code — many of these are "rule-out" or pre-test codes and are correctly not counted). That leaves 4,000 algorithm-positive patients. Applying the 365-day incident washout drops 1,000 with a prior-year I48.x (prevalent), yielding 3,000 incident AF cases; index date = the discharge date (IP arm) or the first of the two qualifying OP dates (OP arm). Production checks: exclude MA-only person-time before counting (otherwise "no prior code" is missingness and over-counts incident cases); flag that MA risk-adjustment HCC capture makes AF look more prevalent in MA than in FFS, so a multi-database pooled estimate must report algorithm performance by payer and sensitivity-test the window (30 vs 90 vs 365 days) and position (any vs primary). Validate PPV in a chart-reviewed or registry-linked subset and report it with a 95% CI; payer-specific heterogeneity and differential ascertainment by exposure are the dominant threats to validity, not the rule itself.

Interpreting the output

. The output of the 1 IP / 2 OP algorithm is a classified patient list: each patient either meets the rule or does not, with an assigned index date. In the Medicare AF example, Patient 2201 qualifies via the 2-OP arm — two I48.x outpatient claims 42 days apart, both in primary position — with an index date at the first qualifying OP date. Patient 2202 qualifies via the 1-IP arm — one inpatient claim with I48.x as principal diagnosis — with index date at discharge. After applying the 365-day incident washout, 3,000 patients remain as incident AF cases.

Formal interpretation: meeting the 1 IP / 2 OP rule means a patient accumulated billing evidence that makes a documentation artifact an unlikely sole explanation. It is not a clinical confirmation of atrial fibrillation. The rule's PPV — the fraction of flagged patients who truly had AF when charts were reviewed — must be reported alongside the result. Rule-out codes are the leading false-positive driver: an I48.x entered to justify a rhythm monitor that came back negative can satisfy the 1-IP arm on its own. The 2-OP arm's 30-day separation requirement suppresses isolated rule-out codes; tightening or loosening that window is a planned sensitivity analysis, not a post-hoc fix.

Practical interpretation: report the algorithm's PPV and the window/position choices prominently in the methods, because those choices — not the underlying condition — largely determine the false-positive rate. For any regulatory or HTA submission, provide a chart-review validation substudy and a Wilson 95% confidence interval on the PPV estimate.

Worked example

Scenario

We are building an incident atrial fibrillation (AF) cohort from a Medicare claims database using ICD-10 code I48.x. The rule is: qualify as an AF case if you have (a) at least one inpatient discharge claim with I48.x, OR (b) at least two outpatient claims with I48.x on different service dates that are at least 7 days apart and both fall within a 365-day case-finding window. We look at two patients. Patient 2201 has only outpatient visits; we trace her two AF codes to see whether and when she qualifies. Patient 2202 was hospitalized with AF; his single inpatient discharge code qualifies him immediately.

Dataset

Raw claim rows an analyst would see after filtering to I48.x codes. claim_type: IP = inpatient discharge, OP = outpatient service. service_date for IP rows already carries the discharge date.

person_idclaim_typeservice_datedx_codedx_position
2201OP2024-01-08I48.11primary
2201OP2024-02-19I48.11primary
2202IP2024-03-05I48.19primary

Steps

  • Patient 2201 has no inpatient claim, so we check the outpatient arm.

  • Her two outpatient claims are on 2024-01-08 and 2024-02-19 — these are different service dates, so they are not the same visit.

  • Gap between the two dates: from January 8 to February 19 is 42 days (23 remaining days in January plus 19 days into February).

  • 42 days is ≥ 7 (the minimum gap required) and ≤ 365 (the window), so the pair qualifies.

  • Patient 2201's index date is 2024-02-19 — the date of the second, confirming outpatient claim.

  • Patient 2202 has one inpatient discharge claim on 2024-03-05; a single inpatient claim always qualifies under the IP arm.

  • Patient 2202's index date is 2024-03-05 — the discharge date of that inpatient claim.

  • If patient 2201 had only the January 8 claim and never returned, she would NOT be a case — one isolated outpatient code is not enough.

Result

Patient 2201: qualifies via the outpatient arm (2 OP claims, gap = 42 days, within 365-day window); index date = 2024-02-19. Patient 2202: qualifies via the inpatient arm (1 IP discharge claim); index date = 2024-03-05.

Timeline Spec

Title

1 IP / 2 OP phenotype — patient 2201 (outpatient arm, atrial fibrillation I48.11)

Caption

Patient 2201 has two outpatient claims for atrial fibrillation. The gap between them is 42 days, which satisfies both the 7-day minimum and the 365-day window. The second claim date becomes her index date. A patient with only the first claim would not qualify.

Alt Text

Horizontal timeline for patient 2201 showing a 365-day case-finding window from 2024-01-08 to 2025-01-07. Two short bars mark outpatient claims: Claim 1 on January 8 and Claim 2 on February 19. A span labeled '42-day gap (≥7 days required)' stretches between them. Both claims fall inside a span labeled 'Case-finding window (≤365 days)'. A result marker at February 19 reads 'Index date — patient qualifies'.

Window
Start

2024-01-08

End

2025-01-07

Label

365-day case-finding window (both OP claims must fall within this span)

Events
  • Label

    Claim 1 — OP visit (I48.11)

    Start

    2024-01-08

    Length Days

    1

    Quantity

    1 outpatient claim

  • Label

    Claim 2 — OP visit (I48.11) → index date

    Start

    2024-02-19

    Length Days

    1

    Quantity

    1 outpatient claim (confirming)

Spans
  • Kind

    followup

    Start

    2024-01-08

    End

    2025-01-07

    Label

    Case-finding window (365 days)

  • Kind

    gap

    Start

    2024-01-09

    End

    2024-02-18

    Label

    42-day gap between claims (≥7 days required)

  • Kind

    covered

    Start

    2024-02-19

    End

    2024-02-19

    Label

    Index date (2nd qualifying OP date)

Result
Label

Patient 2201 qualifies: 2 OP claims, gap = 42 days (≥7), both within 365-day window. Index date = 2024-02-19.

Value

qualifies

Runnable example

python implementation

1 IP / 2 OP incident phenotype construction from claims-style inputs. Required inputs (already cleaned, de-duplicated to one row per claim line): dx : diagnosis claims -> person_id, claim_type in {'IP','OP'}, service_date (datetime; IP arm should already...

import pandas as pd
import numpy as np

TARGET_CODES = ("I48",)          # ICD-10 prefix match for atrial fibrillation/flutter (I48.x)
OP_GAP_DAYS  = 7                 # minimum days between the two qualifying outpatient claims
OP_WINDOW    = 365              # the two OP claims must fall within this many days of each other
WASHOUT_DAYS = 365              # incident clean period: no qualifying code in the prior year

def build_phenotype(dx: pd.DataFrame, enroll: pd.DataFrame,
                    any_position: bool = True) -> pd.DataFrame:
    d = dx.copy()
    d = d[d["dx_code"].str.startswith(TARGET_CODES)]
    if not any_position:
        d = d[d["dx_position"] == "primary"]
    d = d.sort_values(["person_id", "service_date"])

    # IP arm: a single inpatient claim qualifies; index = that discharge date.
    ip = d[d["claim_type"] == "IP"].groupby("person_id", as_index=False)["service_date"].min()
    ip = ip.rename(columns={"service_date": "ip_date"})

    # OP arm: ANY pair of DIFFERENT service dates OP_GAP_DAYS..OP_WINDOW apart (sliding pair, not
    # anchored to the first code) so qualifying pairs after day 365 are not missed.
    op = d[d["claim_type"] == "OP"][["person_id", "service_date"]].drop_duplicates()
    pairs = op.merge(op, on="person_id", suffixes=("_1", "_2"))
    gap = (pairs["service_date_2"] - pairs["service_date_1"]).dt.days
    qual = pairs[(gap >= OP_GAP_DAYS) & (gap <= OP_WINDOW)]  # service_date_2 is the confirming code
    op_idx = qual.groupby("person_id", as_index=False)["service_date_2"].min()  # earliest confirming date
    op_idx = op_idx.rename(columns={"service_date_2": "op_date"})

    cand = ip.merge(op_idx, on="person_id", how="outer")
    cand["index_date"] = cand[["ip_date", "op_date"]].min(axis=1)             # earliest qualifying event

    # Incident washout: drop anyone with a target code in [index - WASHOUT_DAYS, index).
    prior = cand.merge(d[["person_id", "service_date"]], on="person_id")
    in_wash = prior[(prior["service_date"] < prior["index_date"]) &
                    (prior["service_date"] >= prior["index_date"] - pd.Timedelta(days=WASHOUT_DAYS))]
    cand = cand[~cand["person_id"].isin(in_wash["person_id"])]

    # Continuous, FFS-observable enrollment across the washout through index (exclude MA-only spans).
    e = enroll.merge(cand[["person_id", "index_date"]], on="person_id")
    e["covers"] = ((e["enroll_start"] <= e["index_date"] - pd.Timedelta(days=WASHOUT_DAYS)) &
                   (e["enroll_end"]   >= e["index_date"]) & (~e["ma_only"]))
    eligible = e.loc[e["covers"], "person_id"].unique()

    out = cand[cand["person_id"].isin(eligible)].copy()
    return out[["person_id", "index_date"]].sort_values("person_id")
r implementation

1 IP / 2 OP incident phenotype construction with data.table. Inputs mirror the Python version: dx : person_id, claim_type ('IP'/'OP'), service_date (Date; IP carries discharge date), dx_code, dx_position enroll : person_id, enroll_start, enroll_end, ma_only...

library(data.table)

TARGET   <- "^I48"   # ICD-10 atrial fibrillation/flutter (I48.x)
OP_GAP   <- 7L       # min days between the two qualifying OP claims
OP_WIN   <- 365L     # the two OP claims within this many days
WASHOUT  <- 365L     # incident clean period

build_phenotype <- function(dx, enroll, any_position = TRUE) {
  setDT(dx); setDT(enroll)
  d <- dx[grepl(TARGET, dx_code)]
  if (!any_position) d <- d[dx_position == "primary"]
  setorder(d, person_id, service_date)

  # IP arm: single inpatient claim; index = earliest discharge date.
  ip <- d[claim_type == "IP", .(ip_date = min(service_date)), by = person_id]

  # OP arm: ANY pair of distinct service dates OP_GAP..OP_WIN apart (sliding pair, not anchored to the
  # first code) so qualifying pairs after day 365 are not missed.
  op <- unique(d[claim_type == "OP", .(person_id, service_date)])
  pairs <- merge(op, op, by = "person_id", allow.cartesian = TRUE,
                 suffixes = c("_1", "_2"))
  pairs[, gap := as.integer(service_date_2 - service_date_1)]
  op_idx <- pairs[gap >= OP_GAP & gap <= OP_WIN,
                  .(op_date = min(service_date_2)), by = person_id]  # earliest confirming date

  cand <- merge(ip, op_idx, by = "person_id", all = TRUE)
  cand[, index_date := pmin(ip_date, op_date, na.rm = TRUE)]

  # Incident washout: drop anyone with a target code in [index - WASHOUT, index).
  pr <- merge(cand[, .(person_id, index_date)], d[, .(person_id, service_date)], by = "person_id")
  in_wash <- unique(pr[service_date < index_date &
                       service_date >= index_date - WASHOUT, person_id])
  cand <- cand[!person_id %chin% in_wash]

  # Continuous FFS-observable enrollment across washout through index (no MA-only spans).
  e <- merge(enroll, cand[, .(person_id, index_date)], by = "person_id")
  ok <- e[enroll_start <= index_date - WASHOUT &
          enroll_end   >= index_date & !ma_only, unique(person_id)]

  cand[person_id %chin% ok, .(person_id, index_date)][order(person_id)]
}