Skip to content

Commit 0ad6339

Browse files
author
Alexandra Pavlyshina
committed
measure-evaluate: subject push-down for all 12 measures
1 parent a5c370b commit 0ad6339

14 files changed

Lines changed: 280 additions & 42 deletions

File tree

aidbox-custom-operations/measure-evaluate/app/evaluate_measure.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,18 @@ def build_evidence_sql(measure_sql: str, evidence_sql_fragment: str,
144144
def build_patient_sql(measure_sql: str, patient_id: str | None) -> str:
145145
"""Convert aggregate measure SQL to patient-level query.
146146
147-
Replaces the final SELECT COUNT(*) with a patient-level SELECT,
148-
optionally filtered to a single patient.
147+
Replaces the final SELECT COUNT(*) with a patient-level SELECT.
148+
149+
Push-down behavior: when patient_id is provided, this function does two things:
150+
1. Substitutes `-- $SUBJ$ <col-expr>` markers in CTEs with
151+
`AND <col-expr> = '<pid>'`. The col-expr can be a simple
152+
`e.patient_id` or a raw JSONB path (e.g. `o.resource->'subject'->>'id'`).
153+
2. Pushes the patient filter into the outer FROM clause as
154+
`WHERE id = '<pid>'` instead of a top-level WHERE on the joined result.
155+
3. Replaces `/*$SUBJ_PARAM$*/` with `, '<pid>'` to pass the subject
156+
to shared exclusion functions.
157+
158+
Measures without these markers keep old behavior (outer WHERE only).
149159
"""
150160
# Find where the final SELECT starts
151161
idx = measure_sql.rfind("\nSELECT\n COUNT(*)")
@@ -157,10 +167,23 @@ def build_patient_sql(measure_sql: str, patient_id: str | None) -> str:
157167

158168
ctes = measure_sql[:idx]
159169

160-
patient_filter = ""
161170
if patient_id:
162171
pid = _sanitize_patient_id(patient_id)
163-
patient_filter = f"\n WHERE ap.patient_id = '{pid}'"
172+
# Marker-based push-down (no-op when markers absent in measure SQL)
173+
ctes = re.sub(
174+
r"--\s*\$SUBJ\$\s+(.+?)\s*$",
175+
lambda m: f"AND {m.group(1)} = '{pid}'",
176+
ctes,
177+
flags=re.MULTILINE,
178+
)
179+
ctes = ctes.replace("/*$SUBJ_PARAM$*/", f", '{pid}'")
180+
outer_from = (
181+
f"FROM (SELECT id AS patient_id FROM patient_flat WHERE id = '{pid}') ap"
182+
)
183+
else:
184+
# Population mode — remove SUBJ_PARAM markers entirely (no-op comment)
185+
ctes = ctes.replace("/*$SUBJ_PARAM$*/", "")
186+
outer_from = "FROM (SELECT id AS patient_id FROM patient_flat) ap"
164187

165188
# Detect multi-group numerators (numerator_2, numerator_3, ...)
166189
extra_nums = []
@@ -182,11 +205,10 @@ def build_patient_sql(measure_sql: str, patient_id: str | None) -> str:
182205
CASE WHEN ip.patient_id IS NOT NULL THEN 1 ELSE 0 END AS den,
183206
CASE WHEN ip.patient_id IS NOT NULL AND de.patient_id IS NOT NULL THEN 1 ELSE 0 END AS exc,
184207
CASE WHEN ip.patient_id IS NOT NULL AND de.patient_id IS NULL AND n.patient_id IS NOT NULL THEN 1 ELSE 0 END AS num{extra_select}
185-
FROM (SELECT id AS patient_id FROM patient_flat) ap
208+
{outer_from}
186209
LEFT JOIN initial_population ip ON ip.patient_id = ap.patient_id
187210
LEFT JOIN denominator_exclusion de ON de.patient_id = ap.patient_id
188211
LEFT JOIN numerator n ON n.patient_id = ap.patient_id{extra_join}"""
189-
+ patient_filter
190212
+ "\nORDER BY ap.patient_id;"
191213
)
192214
return patient_sql

aidbox-custom-operations/measure-evaluate/sql/02-shared-exclusions.sql

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,43 @@
22
-- Reusable SQL functions for exclusion blocks shared across multiple measures.
33
-- Eliminates ~100 lines of copy-paste per measure.
44
--
5+
-- Each function accepts an optional p_subject parameter (DEFAULT NULL).
6+
-- p_subject IS NULL → population mode (scan whole cohort, used by population reports)
7+
-- p_subject = '<id>' → subject mode (filter early via ix_*_subject indexes; ~100× faster)
8+
--
9+
-- Existing 2-arg callers (shared_hospice(mp.mp_start, mp.mp_end)) continue to work — the
10+
-- subject parameter defaults to NULL and the filter condition becomes a no-op.
11+
--
512
-- Usage in measure SQL (LATERAL pattern):
613
-- hospice AS (SELECT h.* FROM mp, LATERAL shared_hospice(mp.mp_start, mp.mp_end) h),
714
-- palliative AS (SELECT h.* FROM mp, LATERAL shared_palliative(mp.mp_start, mp.mp_end) h),
8-
-- ...
15+
--
16+
-- With push-down for a specific patient:
17+
-- hospice AS (SELECT h.* FROM mp, LATERAL shared_hospice(mp.mp_start, mp.mp_end, $subject) h),
918
--
1019
-- Prerequisites: shared/sql/01-views.sql (flat views must exist)
1120

21+
-- ============================================================
22+
-- Drop existing functions in reverse-dependency order.
23+
-- shared_advanced_illness_frailty depends on the other three (frailty/illness/dementia),
24+
-- so it must be dropped first.
25+
-- ============================================================
26+
DROP FUNCTION IF EXISTS shared_advanced_illness_frailty(timestamptz, timestamptz);
27+
DROP FUNCTION IF EXISTS shared_hospice(timestamptz, timestamptz);
28+
DROP FUNCTION IF EXISTS shared_palliative(timestamptz, timestamptz);
29+
DROP FUNCTION IF EXISTS shared_has_frailty(timestamptz, timestamptz);
30+
DROP FUNCTION IF EXISTS shared_advanced_illness(timestamptz, timestamptz);
31+
DROP FUNCTION IF EXISTS shared_dementia_meds(timestamptz, timestamptz);
32+
DROP FUNCTION IF EXISTS shared_nursing_home(timestamptz, timestamptz);
33+
1234
-- ============================================================
1335
-- HOSPICE (6 sub-checks) — used by 9 measures
1436
-- ============================================================
15-
CREATE OR REPLACE FUNCTION shared_hospice(p_mp_start timestamptz, p_mp_end timestamptz)
37+
CREATE OR REPLACE FUNCTION shared_hospice(
38+
p_mp_start timestamptz,
39+
p_mp_end timestamptz,
40+
p_subject text DEFAULT NULL
41+
)
1642
RETURNS TABLE(patient_id text) AS $$
1743
SELECT DISTINCT patient_id FROM (
1844
-- Inpatient discharge to hospice
@@ -25,6 +51,7 @@ RETURNS TABLE(patient_id text) AS $$
2551
AND e.period_end <= p_mp_end AND e.period_end >= p_mp_start
2652
AND (r.resource->'hospitalization'->'dischargeDisposition'->'coding'->0->>'code'
2753
IN ('428361000124107', '428371000124100'))
54+
AND (p_subject IS NULL OR e.patient_id = p_subject)
2855
UNION ALL
2956
-- Hospice encounter
3057
SELECT e.patient_id
@@ -34,6 +61,7 @@ RETURNS TABLE(patient_id text) AS $$
3461
WHERE e.status = 'finished'
3562
AND e.period_start <= p_mp_end
3663
AND (e.period_end IS NULL OR e.period_end >= p_mp_start)
64+
AND (p_subject IS NULL OR e.patient_id = p_subject)
3765
UNION ALL
3866
-- Hospice observation (LOINC 45755-6 = yes)
3967
SELECT o.patient_id
@@ -43,6 +71,7 @@ RETURNS TABLE(patient_id text) AS $$
4371
AND o.status IN ('final', 'amended', 'corrected')
4472
AND o.effective_start <= p_mp_end
4573
AND (o.effective_end IS NULL OR o.effective_end >= p_mp_start)
74+
AND (p_subject IS NULL OR o.patient_id = p_subject)
4675
UNION ALL
4776
-- Hospice order (ServiceRequest)
4877
SELECT sr.patient_id
@@ -52,6 +81,7 @@ RETURNS TABLE(patient_id text) AS $$
5281
WHERE sr.status IN ('active', 'completed')
5382
AND sr.intent IN ('order', 'original-order', 'reflex-order', 'filler-order', 'instance-order')
5483
AND sr.authored_on >= p_mp_start AND sr.authored_on <= p_mp_end
84+
AND (p_subject IS NULL OR sr.patient_id = p_subject)
5585
UNION ALL
5686
-- Hospice procedure
5787
SELECT pr.patient_id
@@ -61,6 +91,7 @@ RETURNS TABLE(patient_id text) AS $$
6191
WHERE pr.status = 'completed'
6292
AND pr.performed_start <= p_mp_end
6393
AND (pr.performed_end IS NULL OR pr.performed_end >= p_mp_start)
94+
AND (p_subject IS NULL OR pr.patient_id = p_subject)
6495
UNION ALL
6596
-- Hospice diagnosis
6697
SELECT c.patient_id
@@ -71,13 +102,18 @@ RETURNS TABLE(patient_id text) AS $$
71102
OR c.verification_status IN ('confirmed', 'unconfirmed', 'provisional', 'differential'))
72103
AND c.onset_date <= p_mp_end
73104
AND (c.abatement_date IS NULL OR c.abatement_date >= p_mp_start)
105+
AND (p_subject IS NULL OR c.patient_id = p_subject)
74106
) sub
75107
$$ LANGUAGE sql STABLE;
76108

77109
-- ============================================================
78110
-- PALLIATIVE CARE (4 sub-checks) — used by 5 measures
79111
-- ============================================================
80-
CREATE OR REPLACE FUNCTION shared_palliative(p_mp_start timestamptz, p_mp_end timestamptz)
112+
CREATE OR REPLACE FUNCTION shared_palliative(
113+
p_mp_start timestamptz,
114+
p_mp_end timestamptz,
115+
p_subject text DEFAULT NULL
116+
)
81117
RETURNS TABLE(patient_id text) AS $$
82118
SELECT DISTINCT patient_id FROM (
83119
-- Palliative observation (LOINC 71007-9)
@@ -87,6 +123,7 @@ RETURNS TABLE(patient_id text) AS $$
87123
AND o.status IN ('final', 'amended', 'corrected')
88124
AND o.effective_start <= p_mp_end
89125
AND (o.effective_end IS NULL OR o.effective_end >= p_mp_start)
126+
AND (p_subject IS NULL OR o.patient_id = p_subject)
90127
UNION ALL
91128
-- Palliative diagnosis
92129
SELECT c.patient_id
@@ -97,6 +134,7 @@ RETURNS TABLE(patient_id text) AS $$
97134
OR c.verification_status IN ('confirmed', 'unconfirmed', 'provisional', 'differential'))
98135
AND c.onset_date <= p_mp_end
99136
AND (c.abatement_date IS NULL OR c.abatement_date >= p_mp_start)
137+
AND (p_subject IS NULL OR c.patient_id = p_subject)
100138
UNION ALL
101139
-- Palliative encounter
102140
SELECT e.patient_id
@@ -106,6 +144,7 @@ RETURNS TABLE(patient_id text) AS $$
106144
WHERE e.status = 'finished'
107145
AND e.period_start <= p_mp_end
108146
AND (e.period_end IS NULL OR e.period_end >= p_mp_start)
147+
AND (p_subject IS NULL OR e.patient_id = p_subject)
109148
UNION ALL
110149
-- Palliative procedure
111150
SELECT pr.patient_id
@@ -115,13 +154,18 @@ RETURNS TABLE(patient_id text) AS $$
115154
WHERE pr.status = 'completed'
116155
AND pr.performed_start <= p_mp_end
117156
AND (pr.performed_end IS NULL OR pr.performed_end >= p_mp_start)
157+
AND (p_subject IS NULL OR pr.patient_id = p_subject)
118158
) sub
119159
$$ LANGUAGE sql STABLE;
120160

121161
-- ============================================================
122162
-- FRAILTY INDICATORS (5 sub-checks) — used by 4 measures
123163
-- ============================================================
124-
CREATE OR REPLACE FUNCTION shared_has_frailty(p_mp_start timestamptz, p_mp_end timestamptz)
164+
CREATE OR REPLACE FUNCTION shared_has_frailty(
165+
p_mp_start timestamptz,
166+
p_mp_end timestamptz,
167+
p_subject text DEFAULT NULL
168+
)
125169
RETURNS TABLE(patient_id text) AS $$
126170
SELECT DISTINCT patient_id FROM (
127171
-- Frailty Device (DeviceRequest)
@@ -131,6 +175,7 @@ RETURNS TABLE(patient_id text) AS $$
131175
AND vs.valueset_url = 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113883.3.464.1003.118.12.1300' -- FrailtyDevice
132176
WHERE dr.status IN ('active', 'completed')
133177
AND dr.authored_on >= p_mp_start AND dr.authored_on <= p_mp_end
178+
AND (p_subject IS NULL OR dr.patient_id = p_subject)
134179
UNION ALL
135180
-- Frailty Diagnosis
136181
SELECT c.patient_id
@@ -141,6 +186,7 @@ RETURNS TABLE(patient_id text) AS $$
141186
OR c.verification_status IN ('confirmed', 'unconfirmed', 'provisional', 'differential'))
142187
AND c.onset_date <= p_mp_end
143188
AND (c.abatement_date IS NULL OR c.abatement_date >= p_mp_start)
189+
AND (p_subject IS NULL OR c.patient_id = p_subject)
144190
UNION ALL
145191
-- Frailty Encounter
146192
SELECT e.patient_id
@@ -150,6 +196,7 @@ RETURNS TABLE(patient_id text) AS $$
150196
WHERE e.status = 'finished'
151197
AND e.period_start <= p_mp_end
152198
AND (e.period_end IS NULL OR e.period_end >= p_mp_start)
199+
AND (p_subject IS NULL OR e.patient_id = p_subject)
153200
UNION ALL
154201
-- Frailty Symptom
155202
SELECT o.patient_id
@@ -159,6 +206,7 @@ RETURNS TABLE(patient_id text) AS $$
159206
WHERE o.status IN ('preliminary', 'final', 'amended', 'corrected')
160207
AND o.effective_start <= p_mp_end
161208
AND (o.effective_end IS NULL OR o.effective_end >= p_mp_start)
209+
AND (p_subject IS NULL OR o.patient_id = p_subject)
162210
UNION ALL
163211
-- Frailty via Medical Equipment (ObservationScreeningAssessment)
164212
SELECT o.patient_id
@@ -168,13 +216,18 @@ RETURNS TABLE(patient_id text) AS $$
168216
WHERE o.code = '98181-1' AND o.code_system = 'http://loinc.org'
169217
AND o.status IN ('final', 'amended', 'corrected')
170218
AND o.effective_end >= p_mp_start AND o.effective_end <= p_mp_end
219+
AND (p_subject IS NULL OR o.patient_id = p_subject)
171220
) sub
172221
$$ LANGUAGE sql STABLE;
173222

174223
-- ============================================================
175224
-- ADVANCED ILLNESS (condition within MP-1yr to MP end) — used by 4 measures
176225
-- ============================================================
177-
CREATE OR REPLACE FUNCTION shared_advanced_illness(p_mp_start timestamptz, p_mp_end timestamptz)
226+
CREATE OR REPLACE FUNCTION shared_advanced_illness(
227+
p_mp_start timestamptz,
228+
p_mp_end timestamptz,
229+
p_subject text DEFAULT NULL
230+
)
178231
RETURNS TABLE(patient_id text) AS $$
179232
SELECT c.patient_id
180233
FROM condition_flat c
@@ -184,12 +237,17 @@ RETURNS TABLE(patient_id text) AS $$
184237
OR c.verification_status IN ('confirmed', 'unconfirmed', 'provisional', 'differential'))
185238
AND c.onset_date >= (p_mp_start - INTERVAL '1 year')
186239
AND c.onset_date <= p_mp_end
240+
AND (p_subject IS NULL OR c.patient_id = p_subject)
187241
$$ LANGUAGE sql STABLE;
188242

189243
-- ============================================================
190244
-- DEMENTIA MEDICATIONS (active, overlapping MP-1yr to MP end) — used by 4 measures
191245
-- ============================================================
192-
CREATE OR REPLACE FUNCTION shared_dementia_meds(p_mp_start timestamptz, p_mp_end timestamptz)
246+
CREATE OR REPLACE FUNCTION shared_dementia_meds(
247+
p_mp_start timestamptz,
248+
p_mp_end timestamptz,
249+
p_subject text DEFAULT NULL
250+
)
193251
RETURNS TABLE(patient_id text) AS $$
194252
SELECT mr.patient_id
195253
FROM medicationrequest_flat mr
@@ -199,27 +257,37 @@ RETURNS TABLE(patient_id text) AS $$
199257
AND mr.intent IN ('order', 'original-order', 'reflex-order', 'filler-order', 'instance-order')
200258
AND COALESCE(mr.validity_start, mr.authored_on) <= p_mp_end
201259
AND COALESCE(mr.validity_end, mr.authored_on) >= (p_mp_start - INTERVAL '1 year')
260+
AND (p_subject IS NULL OR mr.patient_id = p_subject)
202261
$$ LANGUAGE sql STABLE;
203262

204263
-- ============================================================
205264
-- ADVANCED ILLNESS + FRAILTY combined (standard: age >= 66) — used by CMS125, 130, 131
206265
-- CMS165 uses its own variant with age 66-80 / >=81 split
207266
-- ============================================================
208-
CREATE OR REPLACE FUNCTION shared_advanced_illness_frailty(p_mp_start timestamptz, p_mp_end timestamptz)
267+
CREATE OR REPLACE FUNCTION shared_advanced_illness_frailty(
268+
p_mp_start timestamptz,
269+
p_mp_end timestamptz,
270+
p_subject text DEFAULT NULL
271+
)
209272
RETURNS TABLE(patient_id text) AS $$
210273
SELECT p.id AS patient_id
211274
FROM patient_flat p
212-
JOIN shared_has_frailty(p_mp_start, p_mp_end) f ON f.patient_id = p.id
213-
LEFT JOIN shared_advanced_illness(p_mp_start, p_mp_end) ai ON ai.patient_id = p.id
214-
LEFT JOIN shared_dementia_meds(p_mp_start, p_mp_end) dm ON dm.patient_id = p.id
275+
JOIN shared_has_frailty(p_mp_start, p_mp_end, p_subject) f ON f.patient_id = p.id
276+
LEFT JOIN shared_advanced_illness(p_mp_start, p_mp_end, p_subject) ai ON ai.patient_id = p.id
277+
LEFT JOIN shared_dementia_meds(p_mp_start, p_mp_end, p_subject) dm ON dm.patient_id = p.id
215278
WHERE EXTRACT(YEAR FROM AGE(p_mp_end, p.birth_date::date)) >= 66
216279
AND (ai.patient_id IS NOT NULL OR dm.patient_id IS NOT NULL)
280+
AND (p_subject IS NULL OR p.id = p_subject)
217281
$$ LANGUAGE sql STABLE;
218282

219283
-- ============================================================
220284
-- NURSING HOME (age >= 66, housing status = lives in nursing home) — used by 4 measures
221285
-- ============================================================
222-
CREATE OR REPLACE FUNCTION shared_nursing_home(p_mp_start timestamptz, p_mp_end timestamptz)
286+
CREATE OR REPLACE FUNCTION shared_nursing_home(
287+
p_mp_start timestamptz,
288+
p_mp_end timestamptz,
289+
p_subject text DEFAULT NULL
290+
)
223291
RETURNS TABLE(patient_id text) AS $$
224292
SELECT o.patient_id
225293
FROM observation_flat o
@@ -229,4 +297,5 @@ RETURNS TABLE(patient_id text) AS $$
229297
AND o.value_code = '160734000'
230298
AND o.status IN ('final', 'amended', 'corrected')
231299
AND o.effective_end <= p_mp_end
300+
AND (p_subject IS NULL OR o.patient_id = p_subject)
232301
$$ LANGUAGE sql STABLE;

0 commit comments

Comments
 (0)