-
Notifications
You must be signed in to change notification settings - Fork 1
Add documentation for Bed Availability Prediction query #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sonzsara
wants to merge
5
commits into
main
Choose a base branch
from
ENG-516
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c9aaa2f
Add documentation for Bed Availability Prediction query
sonzsara 94269a4
Refactor bed availability prediction query to improve admissions and …
sonzsara 3bb1e16
Enhance bed availability prediction query by adding day-of-week occur…
sonzsara 46f2bdb
Update bed_availability_prediction.md
sonzsara 638e782
Update bed_availability_prediction.md
sonzsara File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
|
|
||
| # 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. | ||
|
sonzsara marked this conversation as resolved.
|
||
|
|
||
| --- | ||
|
|
||
| ## 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 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 | ||
| 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 | ||
| ), | ||
|
|
||
| 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 ( | ||
|
|
||
| SELECT | ||
| 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 ( | ||
|
|
||
| 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 >= date_trunc('day', NOW()) - INTERVAL '90 days' | ||
| AND fle_a.start_datetime < date_trunc('day', NOW()) | ||
| GROUP BY 1, 2, 3 | ||
|
|
||
| UNION ALL | ||
|
|
||
|
|
||
| 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 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 | ||
| JOIN dow_occurrences docc ON docc.dow = combined.dow | ||
| GROUP BY 1, 2, 3, docc.occurrences | ||
| ), | ||
| 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)::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 | ||
| ``` | ||
|
|
||
| ## 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* | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.