Acute Event Deduplication Window
A pre-specified gap (or "blackout") rule that collapses multiple codes or encounters for the same acute condition occurring within a defined time window into a single counted event episode, so that one clinical event is not scored as several.
In plain language
When a patient has a heart attack, their insurance records often show multiple separate charges for the same event — the hospital stay, a transfer to a second hospital, and follow-up office visits that still list the same diagnosis code. An acute event deduplication window is a pre-specified rule that says: any claim that arrives within a set number of days of the first claim (say, 30 days for a heart attack) belongs to the same event, not a new one. Without this rule, one heart attack can get counted as four or five events, inflating incidence rates and corrupting cost analyses. The window collapses those fragments into a single counted episode; only a claim that arrives more than 30 days after the first discharge is treated as a true new event.
An acute event deduplication window is the operational rule that answers a single, decisive question during outcome ascertainment: given several codes or encounters carrying the same acute diagnosis, when do they represent ONE event and when do they represent TWO? Administrative and EHR data fragment a single clinical event across many records — an acute myocardial infarction (AMI) generates an inpatient stay, often a transfer to a second facility, physician (carrier) claims, follow-up office visits still carrying the I21.x code, and rehospitalization within days. Counted naively, that one infarction becomes four or five "events," inflating incidence, double-counting in cost/utilization, and corrupting time-to-event analyses. The deduplication window collapses records whose dates fall within a pre-specified gap of an anchor record into the same event episode, and only resets to a new episode once a record falls beyond that gap.
Core conceptual distinction — and what this is NOT
The window has two parameters that must be pre-specified separately: (1) the anchor/index rule (which record opens an episode — e.g., first inpatient claim with the diagnosis in the primary position) and (2) the gap rule (the blackout/clean interval that must elapse before a subsequent qualifying record can open a new episode — e.g., 30 days from the index discharge for AMI; 14 days for COPD or asthma exacerbations and for sepsis). This concept is narrowly about within-condition episode grouping. It is distinct from three neighbors it is constantly confused with. A pre-index washout / clean look-back period (`washout-clean-lookback-period-rwe`) clears the baseline so the first observed event is plausibly incident rather than prevalent; the deduplication window operates during follow-up to merge fragments and to separate genuinely distinct recurrences. A restart / new-episode rule for treatment (`restart-rechallenge-new-episode-rwe`) builds exposure episodes from fills; this builds outcome episodes from diagnoses/encounters. Recurrent-event analysis (`recurrent-events-analysis-rwe`) is the downstream model (Andersen-Gill, PWP, frailty) that consumes the episode stream the window produces — get the window wrong and every recurrent-event estimate is wrong.
Estimand link
The window is not cosmetic data cleaning; it defines the counting unit and therefore the estimand. A short or zero gap counts each fragment, so the implied estimand is "number of records," not "number of events." A long gap can merge two true recurrences into one, biasing recurrence rates downward and shrinking the per-protocol event count. Pre-specify the gap to match the clinical natural history of the condition and the question (first-event time-to-event vs. recurrence rate vs. annualized count), and pre-register it in the protocol/SAP before touching data.
Pros, cons, and trade-offs
- vs. no deduplication (count every qualifying record): A window removes the dominant upward bias from administrative fragmentation (transfers, carrier + facility claims for the same stay, resolved-condition follow-up coding). Cost: it introduces a tuning parameter that can mask true early recurrences; the gap length is a researcher degree of freedom that must be pre-specified and sensitivity-tested. Prefer a window for essentially every acute-event count, rate, or cost analysis in fragmented data. - vs. a "one event per patient ever" (first-event-only) rule: First-event-only is the correct, simplest choice when the estimand is time to first event in a survival model and recurrences are not of interest; it needs no gap parameter. Cost: it discards all recurrence information and is wrong for incidence-rate, utilization, or cost questions where repeat events matter. Prefer the window when recurrences carry information or cost; prefer first-event-only for a clean first-event hazard. - vs. encounter/claim-line based counting with manual chart adjudication: Adjudication is the gold standard for what constitutes a distinct event but does not scale and is usually unavailable in large claims. The window is the scalable proxy; its validity should be anchored to an adjudicated or validated reference (PPV/sensitivity of the resulting event count, not just the diagnosis code). Prefer the window for scale, but validate it against a chart-reviewed or registry-adjudicated subset (`claims-outcome-algorithm-ppv-sensitivity-rwe`).
When to use
Counting acute, potentially recurrent events from fragmented claims or EHR encounters — AMI, stroke, sepsis, heart-failure or COPD/asthma exacerbations, GI bleeds, fractures, VTE, hospitalized infections — for incidence rates, recurrence analysis, utilization, or episode-based costing. Use it whenever a single clinical event is expected to generate multiple records (transfers, split facility/professional claims, post-acute follow-up coding) and whenever the same condition can genuinely recur within the observation period.
When NOT to use — and when it is actively misleading
- Chronic, continuously coded conditions (diabetes, hypertension, CKD stage). There is no discrete "event" to deduplicate; an arbitrary window manufactures pseudo-events from routine maintenance coding. Use prevalence/algorithm definitions instead (`diagnosis-phenotype-algorithm-1ip-2op-time-window-rwe`). - First-event time-to-event questions where recurrences are irrelevant. A window adds a needless parameter; use a first-event rule plus a pre-index washout. - When the gap is tuned to the result. Choosing the gap after seeing how it moves the effect estimate is p-hacking by another name; it is most dangerous when the gap length differs by exposure arm or is data-driven. The gap must be pre-specified, applied identically to all arms, and varied only in pre-planned sensitivity analyses. - When a single window is applied across heterogeneous conditions. A 30-day AMI rule misapplied to a 14-day exacerbation condition (or vice versa) systematically miscounts; gaps are condition-specific by clinical natural history.
Data-source operational depth
- Claims (FFS): The classic failure is treating each claim line as an event. One AMI hospitalization yields an inpatient facility claim, a separate carrier (Part B) professional claim, possibly an interhospital transfer to a second facility, and downstream office visits still carrying I21.x weeks later. Anchor on the inpatient facility claim with the diagnosis in the primary/principal position, set the gap from the index discharge date (not admit date), and treat transfers — overlapping or contiguous inpatient stays — as the same episode before applying the gap. Preserve raw admit/discharge dates alongside the derived `episode_id`. Claim-reversal and adjudication lag can make an event appear, vanish, and reappear; freeze the data cut and use service dates, not paid/process dates. - Medicare Advantage vs FFS, and plan switching: MA encounter data are notoriously incomplete and underreport inpatient events, so episode counts are not comparable between MA and FFS person-time (`medicare-ffs-ma-commercial-claims-differences-rwe`). A patient who switches plans mid-window can have the back half of an episode unobserved, splitting one event into two or truncating recurrence follow-up. Require continuous, FFS- observable enrollment across each episode window and censor at plan switches; do not pool MA-only person-time with FFS for event rates. - EHR: Capture is encounter-driven and site-bounded. A patient readmitted to an outside hospital for a true recurrence is missed, so a fixed gap can wrongly merge two events (the second is invisible) or, conversely, resolved- condition problem-list carry-forward can manufacture spurious follow-up "events." Prefer linkage to claims or a regional HIE to capture out-of-system events, define observation windows explicitly, and treat out-of-network care as informative missingness. - Registry / linked data: Registries often carry adjudicated event dates that should override code-derived anchors when available; linkage to claims supplies the fragmentation (transfers, professional claims) that the registry omits, and to a death index so a fatal event is not mistaken for "no recurrence." Reconcile registry, claims, and vital-records dates before assigning episode boundaries, and check transportability of the chosen gap to the analysis population. - Differential competing risks and immortal-time traps: In elderly or sicker arms, death competes with recurrence; a long gap that spans a death interval can suppress observed recurrences differentially by arm — handle death as a competing risk (`competing-risks-cause-specific-fine-gray-rwe`), not as censoring. In procedure-anchored studies, counting only patients who survived to a post-discharge code creates immortal time (`immortal-time-bias-handling`); align time zero to the index event, not to the deduplicated follow-up code.
Worked claims example (AMI, 30-day rule)
Question: incidence and 1-year recurrence of AMI in a Medicare FFS cohort. (1) Eligibility / washout: ≥365 days of continuous FFS Parts A/B enrollment with no inpatient I21.x before the first qualifying claim, so the first episode is incident, not prevalent (the deduplication window is separate from this clean period). (2) Anchor rule: an episode opens on the admit date of an inpatient facility claim with ICD-10 I21.x in the principal position. (3) Transfer handling: any inpatient claim whose admit date is on or within 1 day of a prior episode's discharge date is folded into that episode (an interhospital transfer is one event). (4) Gap rule: the next principal-position I21.x inpatient claim with an admit date ≥30 days after the index episode's discharge opens a new episode; anything within 30 days of discharge — including carrier claims and post-MI office visits — is attributed to the index episode and does not count. (5) Output: one row per `episode_id` with index admit/discharge dates, arm, and an `is_incident` flag; recurrence rate = (episodes − first episodes) per person-year of FFS-observable follow-up, censoring at disenrollment, death, and data cut. (6) Sensitivity / validation: rerun at 14- and 60-day gaps, report how incidence and recurrence move, and benchmark the event count's PPV against a chart-reviewed or registry-adjudicated subset rather than trusting the code alone.
Interpreting the output
. Applying the 30-day deduplication window to Margaret's (person 2001) four raw AMI claims collapses them into two distinct episodes. Episode 1 spans the January 3 index admit through the January 12 discharge; all claims with admit dates within 30 days of that discharge are attributed to Episode 1 and do not count as new events. Episode 2 opens on March 2, 49 days after the Episode 1 discharge — outside the 30-day gap — and ends March 9.
Formal interpretation: the 30-day window is a modeling assumption, not a clinical truth. Any two claims whose admit dates fall within 30 days of the preceding discharge are treated as one continuous episode, regardless of whether the second admission represented a genuine readmission or a billing artifact. The window choice is consequential: a 14-day window would potentially split what the 30-day rule treats as one episode; a 60-day window would collapse events the 30-day rule treats as distinct. Incidence and recurrence estimates are therefore partly a function of the window specification, and sensitivity analyses at 14 and 60 days are mandatory before presenting results to a regulator or HTA body.
Practical interpretation: always report the episode count alongside the raw claim count and the window length. Reviewers who see "218 AMI events" without knowing the deduplication rule cannot evaluate whether recurrence was over- or under-counted. The window choice should be pre-specified and motivated by the condition's natural history — for AMI, 30 days is the standard clinically meaningful readmission threshold, aligning the deduplication rule with the readmission outcome.
Worked example
Scenario
Margaret is a 72-year-old Medicare patient who has a heart attack on January 3, 2023. She is admitted to Community Hospital that day, transferred to University Medical Center on January 5 (her records show a second inpatient claim), and then sees her cardiologist in an office visit on January 20, at which the cardiologist still lists the heart-attack diagnosis code. On March 2, she is admitted again with a new heart attack. A researcher applying a 30-day deduplication window wants to count how many distinct heart-attack episodes Margaret had in 2023.
Dataset
Raw qualifying claims for Margaret (person 2001) — every claim where the principal diagnosis is I21.x (acute MI), sorted by date.
| person_id | claim_type | admit_date | discharge_date | dx_principal |
|---|---|---|---|---|
| 2001 | IP | 2023-01-03 | 2023-01-05 | I21.9 |
| 2001 | IP | 2023-01-05 | 2023-01-12 | I21.9 |
| 2001 | Office | 2023-01-20 | 2023-01-20 | I21.9 |
| 2001 | IP | 2023-03-02 | 2023-03-09 | I21.9 |
Steps
Claim 1 (Jan 3 admit, Jan 5 discharge) is an inpatient hospital claim with I21.9 in the principal position — it opens Episode 1, and Jan 5 becomes the running discharge date.
Claim 2 (Jan 5 admit) is a second inpatient claim whose admit date is 0 days after the Jan 5 discharge — it falls within the 1-day transfer window, so it is merged into Episode 1 as an interhospital transfer, not a new event. The running discharge date updates to Jan 12.
Claim 3 (Jan 20 office visit) arrives 8 days after the Jan 12 discharge. Eight days is less than the 30-day window, so this follow-up visit is attributed to Episode 1 and is NOT counted as a new event.
Claim 4 (Mar 2 admit) arrives 49 days after the Jan 12 discharge. Forty-nine days exceeds the 30-day window, so this claim opens Episode 2 — a true new event (recurrence).
Result: 4 raw claims collapse to 2 distinct episodes. Counting records without a window would have reported 4 events for this patient.
Result
4 raw claims for patient 2001 collapse to 2 distinct episodes under the 30-day deduplication window: Episode 1 (index admit Jan 3, discharge Jan 12, spanning 3 raw claims) and Episode 2 (index admit Mar 2, a true recurrence 49 days after Episode 1 discharge).
Timeline Spec
- Title
Four AMI claims collapsed to two episodes under a 30-day deduplication window
- Window
- Start
2023-01-03
- End
2023-03-09
- Label
Observation window: Jan 3 to Mar 9 (65 days shown)
- Events
- Label
Claim 1: IP admit (Episode 1 anchor)
- Start
2023-01-03
- Length Days
2
- Quantity
Jan 3-5 inpatient stay
- Label
Claim 2: IP transfer (same event)
- Start
2023-01-05
- Length Days
7
- Quantity
Jan 5-12 transfer stay
- Label
Claim 3: Office visit (follow-up, inside 30d window)
- Start
2023-01-20
- Length Days
1
- Quantity
Jan 20 office visit, 8d after Ep1 discharge
- Label
Claim 4: IP admit (Episode 2, true recurrence)
- Start
2023-03-02
- Length Days
7
- Quantity
Mar 2-9 inpatient stay, 49d after Ep1 discharge
- Spans
- Kind
covered
- Start
2023-01-03
- End
2023-01-20
- Label
Episode 1 window (anchor Jan 3 through last attributed claim Jan 20)
- Kind
gap
- Start
2023-01-21
- End
2023-03-01
- Label
39-day clean gap (all days exceed 30-day window from Jan 12 discharge)
- Kind
covered
- Start
2023-03-02
- End
2023-03-09
- Label
Episode 2 (recurrence, 49d after Ep1 discharge)
- Result
- Label
4 raw claims collapse to 2 distinct episodes
- Value
2
- Caption
Margaret has four claims carrying an acute MI diagnosis code. Claims 1 and 2 are an interhospital transfer (0-day gap, merged automatically). Claim 3 is a follow-up office visit only 8 days after discharge — inside the 30-day blackout, so it belongs to Episode 1. Claim 4 arrives 49 days after the Episode 1 discharge, beyond the 30-day window, and opens a true new episode. Naive record-counting would report 4 events; the window correctly reports 2.
- Alt Text
Timeline for patient 2001 showing four acute MI claims between January 3 and March 9, 2023. Claims 1 and 2 form an interhospital transfer merged into Episode 1. Claim 3 (office visit on January 20) falls inside the 30-day window and is absorbed into Episode 1. Claim 4 (March 2 inpatient) falls 49 days after the Episode 1 discharge and opens Episode 2 as a true recurrence. The gap between episodes is highlighted in a neutral color to show the clean separation.
Runnable example
python implementation
Acute-event episode construction with a deduplication window from claims-style inputs. Required input (already cleaned, one row per qualifying claim that carries the target diagnosis in the principal position): claims : person_id, claim_id, claim_type...
import pandas as pd
GAP_DAYS = 30 # condition-specific blackout (AMI=30; exacerbations/sepsis commonly 14)
TRANSFER_DAYS = 1 # contiguous/overlapping IP stays within this many days are the same event (transfer)
def build_event_episodes(claims: pd.DataFrame,
gap_days: int = GAP_DAYS,
transfer_days: int = TRANSFER_DAYS) -> pd.DataFrame:
c = claims.sort_values(["person_id", "admit_date", "discharge_date"]).copy()
out = []
for pid, g in c.groupby("person_id", sort=False):
episode_id = 0
ep_admit = ep_discharge = None
n_records = 0
first_seen = True
for _, row in g.iterrows():
if ep_admit is None:
# open the first episode for this person
episode_id, n_records = 1, 1
ep_admit, ep_discharge = row["admit_date"], row["discharge_date"]
continue
days_after_discharge = (row["admit_date"] - ep_discharge).days
if days_after_discharge <= transfer_days:
# transfer / fragment of the SAME event -> extend the episode, do not count anew
ep_discharge = max(ep_discharge, row["discharge_date"])
n_records += 1
elif days_after_discharge < gap_days:
# inside the blackout window -> attributed to current episode, not a new event
n_records += 1
else:
# beyond the gap -> close current episode, open a new one
out.append((pid, episode_id, ep_admit, ep_discharge, n_records, first_seen))
first_seen = False
episode_id += 1
ep_admit, ep_discharge, n_records = row["admit_date"], row["discharge_date"], 1
if ep_admit is not None:
out.append((pid, episode_id, ep_admit, ep_discharge, n_records, first_seen))
episodes = pd.DataFrame(out, columns=["person_id", "episode_id", "index_admit",
"index_discharge", "n_records", "is_incident"])
return episodesr implementation
Acute-event episode construction with a deduplication window using data.table. Input mirrors the Python version (one row per qualifying principal-position claim): claims : person_id, claim_id, claim_type ('IP'/'OP'), admit_date (Date), discharge_date...
library(data.table)
GAP_DAYS <- 30L
TRANSFER_DAYS <- 1L
build_event_episodes <- function(claims, gap_days = GAP_DAYS, transfer_days = TRANSFER_DAYS) {
setDT(claims)
setorder(claims, person_id, admit_date, discharge_date)
assign_episodes <- function(admit, discharge) {
n <- length(admit)
epi <- integer(n); ep_discharge <- discharge[1L]; epi[1L] <- 1L
for (i in seq_len(n)[-1L]) {
gap <- as.integer(admit[i] - ep_discharge)
if (gap <= transfer_days) { # transfer / same-event fragment
epi[i] <- epi[i - 1L]
ep_discharge <- max(ep_discharge, discharge[i])
} else if (gap < gap_days) { # inside blackout -> current episode
epi[i] <- epi[i - 1L]
} else { # beyond gap -> new episode
epi[i] <- epi[i - 1L] + 1L
ep_discharge <- discharge[i]
}
}
epi
}
claims[, episode_id := assign_episodes(admit_date, discharge_date), by = person_id]
episodes <- claims[, .(index_admit = min(admit_date),
index_discharge = max(discharge_date),
n_records = .N),
by = .(person_id, episode_id)]
episodes[, is_incident := episode_id == min(episode_id), by = person_id]
episodes[]
}