Skip to content
>GLB_
Go back

Tuva's encounter grouper only groups claims. Here's what happens to your ADT encounters

The Tuva encounter grouper runs on claims only. Clinical encounters (ADT feeds, EHR extracts) are never grouped: they get standardized and then stacked into the same table, core.encounter, next to the claims-derived encounters. The two sources are not matched and not deduplicated. One hospital stay present in both feeds means two rows.

The Scenario

If you load both claims and clinical data into Tuva, the obvious assumption is that core.encounter gives you one row per visit, regardless of where the visit came from. The Core Data Model is explicitly designed to unify claims and clinical data into a single longitudinal record, so the assumption is reasonable.

It’s also wrong for encounters. I wanted a definitive answer to two questions:

  1. Does the encounter grouper use only claims?
  2. If I feed clinical encounters from ADT, are they grouped too?

The docs describe the grouper in detail but don’t spell out what happens on the clinical side. So I read the code on main (commit 9cf2d1f7).

What the Code Does

Tuva core encounter: claims lane vs clinical lane

There are two independent lanes that only meet at the very end.

1. Claims lane: the grouper

The code lives in models/claims_preprocessing/encounters/.

Inputs:

Each encounter starts from an anchor service: for example, an institutional acute inpatient claim, or a professional office E&M visit. Related claim lines then attach to that anchor using rules specific to each care setting. Inpatient stays use date/NPI continuity (overlapping claims with the same facility NPI, person_id and data_source); most outpatient and office encounters are formed from person_id + data_source + date. The Encounters chapter documents these rules in detail.

There are 26 encounter types, one model each: acute inpatient, ED, office visit, ASC, outpatient radiology, lab, DME, SNF, home health, dialysis, and so on. Lines that can’t attach to any anchor land in orphaned_claim.

Outputs:

These rows carry fields that only make sense for claims: paid_amount, allowed_amount, claim_count, inst_claim_count, prof_claim_count, plus service flags like ed_flag and lab_flag.

2. Clinical lane: standardization only

The connector maps your source data into input_layer__encounter (the Input Layer contract). Then normalized__encounter:

That’s it. Nothing is merged, split or re-grouped. One source encounter produces one row. If your source emits several records for the same stay, collapsing them is the connector’s job, not Tuva’s.

Extension columns (custom columns you pass through the Input Layer) only flow on clinical rows. On claims rows they’re null.

3. Where the lanes meet

core__encounter is controlled by two feature flags:

claims_enabledclinical_enabledcore__encounter
truetruesmart_union of both lanes
truefalseclaims lane only
falsetrueclinical lane only

A union. No join, no match, no dedup.

What This Means in Practice

Check what you actually have:

select
    encounter_source_type,
    data_source,
    count(*) as encounters
from core.encounter
group by 1, 2
order by 1, 2;

Filter by encounter_source_type (or source_model / data_source) any time you compute utilization. Counting acute inpatient admits across both lanes double-counts every stay you see in ADT and claims.

If you need one row per real-world visit, build it downstream. A starting point:

-- sketch: candidate matches between clinical and claims inpatient encounters
-- adapt column names to your Tuva version and warehouse
select
    cl.encounter_id  as clinical_encounter_id,
    cm.encounter_id  as claim_encounter_id,
    cl.person_id
from core.encounter cl
join core.encounter cm
  on  cl.person_id   = cm.person_id
  and cl.facility_id = cm.facility_id
  and cl.encounter_start_date <= cm.encounter_end_date
  and cm.encounter_start_date <= cl.encounter_end_date
where cl.encounter_source_type = 'clinical'
  and cm.encounter_source_type = 'claim'
  and cl.encounter_type = 'acute inpatient'
  and cm.encounter_type = 'acute inpatient';

Things that will bite you here:

Conclusion

The encounter grouper is a claims-only algorithm; clinical encounters bypass it entirely. core.encounter is a union of two lanes, not a reconciled visit table, so the same stay in ADT and claims shows up as two rows. Filter on encounter_source_type every time, and treat ADT-to-claims reconciliation as a downstream job where dates are the hard part.

References


Share this post:

Related Posts


Previous Post
Why Athena Returns No Rows on Partitioned Tables (and How to Fix It)
Next Post
Building an AI oracle slot machine with nothing but canvas