From c9aaa2fecc4c8fc698f120b9ccfb48dc7679619c Mon Sep 17 00:00:00 2001 From: sonzsara Date: Fri, 24 Jul 2026 11:48:18 +0530 Subject: [PATCH 1/5] Add documentation for Bed Availability Prediction query --- .../Prediction/bed_availability_prediction.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 Internal/Prediction/bed_availability_prediction.md diff --git a/Internal/Prediction/bed_availability_prediction.md b/Internal/Prediction/bed_availability_prediction.md new file mode 100644 index 0000000..79e9b1c --- /dev/null +++ b/Internal/Prediction/bed_availability_prediction.md @@ -0,0 +1,116 @@ + +# Bed Availability Prediction + +> 7-day forward forecast of bed occupancy and vacancies per floor/ward, with bottleneck flagging + +## Purpose + +Forecasts bed occupancy for each floor/ward over the next 7 days by combining current occupancy with historical (90-day) day-of-week admission and discharge patterns. Applies tunable multipliers to account for known surges or slowdowns, and flags any ward/day projected to cross an occupancy threshold as a `BOTTLENECK RISK`. + +## Parameters + +| Variable | Default | Meaning | When to change it | +|----------|---------|---------|--------------------| +| `admission_multiplier` | `1.0` | Scales expected admissions vs. the 90-day average. `1.0` = normal, `1.2` = expect 20% more admissions, `0.8` = expect 20% fewer | Festival/flu season surge → `1.2`–`1.5`. Holiday period with fewer elective admits → `0.7`–`0.9` | +| `turnover_multiplier` | `1.0` | Scales expected discharges. `1.0` = normal discharge pace, `1.2` = faster discharges, `0.8` = slower | Discharge drive/faster turnover initiative → `1.1`–`1.3`. Long weekend when doctors discharge fewer patients → `0.7`–`0.9` | +| `bottleneck_threshold` | `0.90` | Occupancy % (as a fraction) at which a ward gets flagged as `"BOTTLENECK RISK"` | ICU/critical wards may warrant `0.80` (flag earlier). General wards may tolerate `0.95` | + +> **Note:** `bottleneck_threshold` is a **fraction**, so enter `0.90`, not `90`. If all three variables are set to `1`, the threshold is effectively `100%`, meaning nothing will flag until a ward is completely full — set it to `0.9` (or your desired fraction) instead. + +--- + +## Query + +```sql +WITH capacity AS ( + SELECT + COALESCE(gp.name, p.name) AS floor, + p.name AS ward, + COUNT(*) AS total_beds + FROM emr_facilitylocation fl + LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id + LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id + WHERE fl.deleted = FALSE AND fl.status = 'active' + AND fl.form = 'bd' AND fl.root_location_id != 300 + GROUP BY 1, 2 +), +current_occ AS ( + SELECT + COALESCE(gp.name, p.name) AS floor, + p.name AS ward, + COUNT(DISTINCT fle.id) AS occupied_beds + FROM emr_facilitylocationencounter fle + INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id + LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id + LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id + WHERE fl.deleted = FALSE AND fl.status = 'active' + AND fl.form = 'bd' AND fle.deleted = FALSE + AND fl.root_location_id != 300 + AND fle.start_datetime <= NOW() + AND (fle.end_datetime IS NULL OR fle.end_datetime > NOW()) + GROUP BY 1, 2 +), +patterns AS ( + + SELECT + COALESCE(gp.name, p.name) AS floor, + p.name AS ward, + EXTRACT(DOW FROM fle.start_datetime) AS dow, + COUNT(*) FILTER (WHERE fle.start_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_admits, + COUNT(*) FILTER (WHERE fle.end_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_discharges + FROM emr_facilitylocationencounter fle + INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id + LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id + LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id + WHERE fl.deleted = FALSE AND fl.status = 'active' + AND fl.form = 'bd' AND fle.deleted = FALSE + AND fl.root_location_id != 300 + GROUP BY 1, 2, 3 +), +days AS ( + SELECT generate_series( + date_trunc('day', NOW()) + INTERVAL '1 day', + date_trunc('day', NOW()) + INTERVAL '7 days', + INTERVAL '1 day' + ) AS forecast_day +), +forecast AS ( + SELECT + c.floor, + c.ward, + c.total_beds, + d.forecast_day, + COALESCE(co.occupied_beds, 0) AS occupied_beds, + SUM( + COALESCE(pt.avg_admits, 0) * {{admission_multiplier}} + - COALESCE(pt.avg_discharges, 0) * {{turnover_multiplier}} + ) OVER (PARTITION BY c.floor, c.ward ORDER BY d.forecast_day) AS cum_net_flow + FROM capacity c + CROSS JOIN days d + LEFT JOIN current_occ co ON co.floor = c.floor AND co.ward = c.ward + LEFT JOIN patterns pt + ON pt.floor = c.floor AND pt.ward = c.ward + AND pt.dow = EXTRACT(DOW FROM d.forecast_day) +) +SELECT + floor, + ward, + forecast_day, + total_beds, + ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_occupied, + total_beds - ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_vacancies, + ROUND(100.0 * LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds) / total_beds, 1) AS predicted_occupancy_pct, + CASE WHEN (occupied_beds + cum_net_flow) / total_beds::float >= {{bottleneck_threshold}} + THEN 'BOTTLENECK RISK' ELSE 'OK' END AS status +FROM forecast +ORDER BY floor, ward, forecast_day; +``` + +## Notes + +- **Tunable variables:** See the Parameters table above. + - `admission_multiplier` and `turnover_multiplier` scale the historical daily averages to reflect expected surges/slowdowns. + - `bottleneck_threshold` is compared against the *fraction* `(occupied_beds + cum_net_flow) / total_beds`, so it must be entered as a decimal fraction (e.g. `0.90` for 90%), not a whole number. +- Results are ordered by floor, then ward, then forecast day — giving a 7-day trend per ward. + +*Last updated: 2026-07-24* From 94269a4c927bf8fa1512ff2a9d79d6392b0914e9 Mon Sep 17 00:00:00 2001 From: sonzsara Date: Fri, 24 Jul 2026 15:04:21 +0530 Subject: [PATCH 2/5] Refactor bed availability prediction query to improve admissions and discharges calculation --- .../Prediction/bed_availability_prediction.md | 56 ++++++++++++++----- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/Internal/Prediction/bed_availability_prediction.md b/Internal/Prediction/bed_availability_prediction.md index 79e9b1c..24d550e 100644 --- a/Internal/Prediction/bed_availability_prediction.md +++ b/Internal/Prediction/bed_availability_prediction.md @@ -51,20 +51,50 @@ current_occ AS ( GROUP BY 1, 2 ), patterns AS ( - SELECT - COALESCE(gp.name, p.name) AS floor, - p.name AS ward, - EXTRACT(DOW FROM fle.start_datetime) AS dow, - COUNT(*) FILTER (WHERE fle.start_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_admits, - COUNT(*) FILTER (WHERE fle.end_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_discharges - FROM emr_facilitylocationencounter fle - INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id - LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id - LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id - WHERE fl.deleted = FALSE AND fl.status = 'active' - AND fl.form = 'bd' AND fle.deleted = FALSE - AND fl.root_location_id != 300 + floor, + ward, + dow, + SUM(admit_count) / 13.0 AS avg_admits, + SUM(discharge_count) / 13.0 AS avg_discharges + FROM ( + -- Admissions by admission day-of-week + SELECT + COALESCE(gp_a.name, p_a.name) AS floor, + p_a.name AS ward, + EXTRACT(DOW FROM fle_a.start_datetime) AS dow, + COUNT(*) AS admit_count, + 0 AS discharge_count + FROM emr_facilitylocationencounter fle_a + INNER JOIN emr_facilitylocation fl_a ON fle_a.location_id = fl_a.id + LEFT JOIN emr_facilitylocation p_a ON fl_a.parent_id = p_a.id + LEFT JOIN emr_facilitylocation gp_a ON p_a.parent_id = gp_a.id + WHERE fl_a.deleted = FALSE AND fl_a.status = 'active' + AND fl_a.form = 'bd' AND fle_a.deleted = FALSE + AND fl_a.root_location_id != 300 + AND fle_a.start_datetime > NOW() - INTERVAL '90 days' + GROUP BY 1, 2, 3 + + UNION ALL + + -- Discharges by discharge day-of-week + SELECT + COALESCE(gp_d.name, p_d.name) AS floor, + p_d.name AS ward, + EXTRACT(DOW FROM fle_d.end_datetime) AS dow, + 0 AS admit_count, + COUNT(*) AS discharge_count + FROM emr_facilitylocationencounter fle_d + INNER JOIN emr_facilitylocation fl_d ON fle_d.location_id = fl_d.id + LEFT JOIN emr_facilitylocation p_d ON fl_d.parent_id = p_d.id + LEFT JOIN emr_facilitylocation gp_d ON p_d.parent_id = gp_d.id + WHERE fl_d.deleted = FALSE AND fl_d.status = 'active' + AND fl_d.form = 'bd' AND fle_d.deleted = FALSE + AND fl_d.root_location_id != 300 + AND fle_d.end_datetime > NOW() - INTERVAL '90 days' + AND fle_d.end_datetime IS NOT NULL + GROUP BY 1, 2, 3 + ) combined GROUP BY 1, 2, 3 ), days AS ( From 3bb1e16d4b4ff3d0df16193a274b5925f2722cd6 Mon Sep 17 00:00:00 2001 From: sonzsara Date: Fri, 24 Jul 2026 15:14:19 +0530 Subject: [PATCH 3/5] Enhance bed availability prediction query by adding day-of-week occurrences for improved admissions and discharges calculations --- .../Prediction/bed_availability_prediction.md | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/Internal/Prediction/bed_availability_prediction.md b/Internal/Prediction/bed_availability_prediction.md index 24d550e..9b2eb28 100644 --- a/Internal/Prediction/bed_availability_prediction.md +++ b/Internal/Prediction/bed_availability_prediction.md @@ -50,13 +50,26 @@ current_occ AS ( AND (fle.end_datetime IS NULL OR fle.end_datetime > NOW()) GROUP BY 1, 2 ), +-- Actual count of each day-of-week in the 90-day lookback window (12 or 13) +dow_occurrences AS ( + SELECT + EXTRACT(DOW FROM d) AS dow, + COUNT(*) AS occurrences + FROM generate_series( + date_trunc('day', NOW()) - INTERVAL '90 days', + date_trunc('day', NOW()) - INTERVAL '1 day', + INTERVAL '1 day' + ) AS d + GROUP BY 1 +), patterns AS ( + -- Admissions grouped by admission DOW; discharges grouped by discharge DOW SELECT - floor, - ward, - dow, - SUM(admit_count) / 13.0 AS avg_admits, - SUM(discharge_count) / 13.0 AS avg_discharges + combined.floor, + combined.ward, + combined.dow, + SUM(combined.admit_count) / docc.occurrences::float AS avg_admits, + SUM(combined.discharge_count) / docc.occurrences::float AS avg_discharges FROM ( -- Admissions by admission day-of-week SELECT @@ -72,11 +85,12 @@ patterns AS ( WHERE fl_a.deleted = FALSE AND fl_a.status = 'active' AND fl_a.form = 'bd' AND fle_a.deleted = FALSE AND fl_a.root_location_id != 300 - AND fle_a.start_datetime > NOW() - INTERVAL '90 days' + AND fle_a.start_datetime >= date_trunc('day', NOW()) - INTERVAL '90 days' + AND fle_a.start_datetime < date_trunc('day', NOW()) GROUP BY 1, 2, 3 - + UNION ALL - + -- Discharges by discharge day-of-week SELECT COALESCE(gp_d.name, p_d.name) AS floor, @@ -91,11 +105,13 @@ patterns AS ( WHERE fl_d.deleted = FALSE AND fl_d.status = 'active' AND fl_d.form = 'bd' AND fle_d.deleted = FALSE AND fl_d.root_location_id != 300 - AND fle_d.end_datetime > NOW() - INTERVAL '90 days' AND fle_d.end_datetime IS NOT NULL + AND fle_d.end_datetime >= date_trunc('day', NOW()) - INTERVAL '90 days' + AND fle_d.end_datetime < date_trunc('day', NOW()) GROUP BY 1, 2, 3 ) combined - GROUP BY 1, 2, 3 + JOIN dow_occurrences docc ON docc.dow = combined.dow + GROUP BY 1, 2, 3, docc.occurrences ), days AS ( SELECT generate_series( @@ -127,13 +143,13 @@ SELECT ward, forecast_day, total_beds, - ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_occupied, - total_beds - ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_vacancies, - ROUND(100.0 * LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds) / total_beds, 1) AS predicted_occupancy_pct, + ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)::numeric) AS predicted_occupied, + total_beds - ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)::numeric) AS predicted_vacancies, + ROUND((100.0 * LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds) / total_beds)::numeric, 1) AS predicted_occupancy_pct, CASE WHEN (occupied_beds + cum_net_flow) / total_beds::float >= {{bottleneck_threshold}} THEN 'BOTTLENECK RISK' ELSE 'OK' END AS status FROM forecast -ORDER BY floor, ward, forecast_day; +ORDER BY floor, ward, forecast_day ``` ## Notes From 46f2bdba198ac6f6da96f6c9b351db4bd6a9e43f Mon Sep 17 00:00:00 2001 From: Sona Sara Shibu Date: Fri, 24 Jul 2026 15:23:04 +0530 Subject: [PATCH 4/5] Update bed_availability_prediction.md --- Internal/Prediction/bed_availability_prediction.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Internal/Prediction/bed_availability_prediction.md b/Internal/Prediction/bed_availability_prediction.md index 9b2eb28..ef539f8 100644 --- a/Internal/Prediction/bed_availability_prediction.md +++ b/Internal/Prediction/bed_availability_prediction.md @@ -38,7 +38,7 @@ current_occ AS ( SELECT COALESCE(gp.name, p.name) AS floor, p.name AS ward, - COUNT(DISTINCT fle.id) AS occupied_beds + COUNT(DISTINCT fl.id) AS occupied_beds FROM emr_facilitylocationencounter fle INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id From 638e782512713dde80e9e10ce2ab01e380fa309d Mon Sep 17 00:00:00 2001 From: Sona Sara Shibu Date: Fri, 24 Jul 2026 15:27:34 +0530 Subject: [PATCH 5/5] Update bed_availability_prediction.md --- Internal/Prediction/bed_availability_prediction.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Internal/Prediction/bed_availability_prediction.md b/Internal/Prediction/bed_availability_prediction.md index ef539f8..d309572 100644 --- a/Internal/Prediction/bed_availability_prediction.md +++ b/Internal/Prediction/bed_availability_prediction.md @@ -50,7 +50,7 @@ current_occ AS ( AND (fle.end_datetime IS NULL OR fle.end_datetime > NOW()) GROUP BY 1, 2 ), --- Actual count of each day-of-week in the 90-day lookback window (12 or 13) + dow_occurrences AS ( SELECT EXTRACT(DOW FROM d) AS dow, @@ -63,7 +63,7 @@ dow_occurrences AS ( GROUP BY 1 ), patterns AS ( - -- Admissions grouped by admission DOW; discharges grouped by discharge DOW + SELECT combined.floor, combined.ward, @@ -71,7 +71,7 @@ patterns AS ( SUM(combined.admit_count) / docc.occurrences::float AS avg_admits, SUM(combined.discharge_count) / docc.occurrences::float AS avg_discharges FROM ( - -- Admissions by admission day-of-week + SELECT COALESCE(gp_a.name, p_a.name) AS floor, p_a.name AS ward, @@ -91,7 +91,7 @@ patterns AS ( UNION ALL - -- Discharges by discharge day-of-week + SELECT COALESCE(gp_d.name, p_d.name) AS floor, p_d.name AS ward,