Skip to content

Commit c9aaa2f

Browse files
committed
Add documentation for Bed Availability Prediction query
1 parent b8bf63b commit c9aaa2f

1 file changed

Lines changed: 116 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
2+
# Bed Availability Prediction
3+
4+
> 7-day forward forecast of bed occupancy and vacancies per floor/ward, with bottleneck flagging
5+
6+
## Purpose
7+
8+
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`.
9+
10+
## Parameters
11+
12+
| Variable | Default | Meaning | When to change it |
13+
|----------|---------|---------|--------------------|
14+
| `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` |
15+
| `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` |
16+
| `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` |
17+
18+
> **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.
19+
20+
---
21+
22+
## Query
23+
24+
```sql
25+
WITH capacity AS (
26+
SELECT
27+
COALESCE(gp.name, p.name) AS floor,
28+
p.name AS ward,
29+
COUNT(*) AS total_beds
30+
FROM emr_facilitylocation fl
31+
LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id
32+
LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id
33+
WHERE fl.deleted = FALSE AND fl.status = 'active'
34+
AND fl.form = 'bd' AND fl.root_location_id != 300
35+
GROUP BY 1, 2
36+
),
37+
current_occ AS (
38+
SELECT
39+
COALESCE(gp.name, p.name) AS floor,
40+
p.name AS ward,
41+
COUNT(DISTINCT fle.id) AS occupied_beds
42+
FROM emr_facilitylocationencounter fle
43+
INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id
44+
LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id
45+
LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id
46+
WHERE fl.deleted = FALSE AND fl.status = 'active'
47+
AND fl.form = 'bd' AND fle.deleted = FALSE
48+
AND fl.root_location_id != 300
49+
AND fle.start_datetime <= NOW()
50+
AND (fle.end_datetime IS NULL OR fle.end_datetime > NOW())
51+
GROUP BY 1, 2
52+
),
53+
patterns AS (
54+
55+
SELECT
56+
COALESCE(gp.name, p.name) AS floor,
57+
p.name AS ward,
58+
EXTRACT(DOW FROM fle.start_datetime) AS dow,
59+
COUNT(*) FILTER (WHERE fle.start_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_admits,
60+
COUNT(*) FILTER (WHERE fle.end_datetime > NOW() - INTERVAL '90 days') / 13.0 AS avg_discharges
61+
FROM emr_facilitylocationencounter fle
62+
INNER JOIN emr_facilitylocation fl ON fle.location_id = fl.id
63+
LEFT JOIN emr_facilitylocation p ON fl.parent_id = p.id
64+
LEFT JOIN emr_facilitylocation gp ON p.parent_id = gp.id
65+
WHERE fl.deleted = FALSE AND fl.status = 'active'
66+
AND fl.form = 'bd' AND fle.deleted = FALSE
67+
AND fl.root_location_id != 300
68+
GROUP BY 1, 2, 3
69+
),
70+
days AS (
71+
SELECT generate_series(
72+
date_trunc('day', NOW()) + INTERVAL '1 day',
73+
date_trunc('day', NOW()) + INTERVAL '7 days',
74+
INTERVAL '1 day'
75+
) AS forecast_day
76+
),
77+
forecast AS (
78+
SELECT
79+
c.floor,
80+
c.ward,
81+
c.total_beds,
82+
d.forecast_day,
83+
COALESCE(co.occupied_beds, 0) AS occupied_beds,
84+
SUM(
85+
COALESCE(pt.avg_admits, 0) * {{admission_multiplier}}
86+
- COALESCE(pt.avg_discharges, 0) * {{turnover_multiplier}}
87+
) OVER (PARTITION BY c.floor, c.ward ORDER BY d.forecast_day) AS cum_net_flow
88+
FROM capacity c
89+
CROSS JOIN days d
90+
LEFT JOIN current_occ co ON co.floor = c.floor AND co.ward = c.ward
91+
LEFT JOIN patterns pt
92+
ON pt.floor = c.floor AND pt.ward = c.ward
93+
AND pt.dow = EXTRACT(DOW FROM d.forecast_day)
94+
)
95+
SELECT
96+
floor,
97+
ward,
98+
forecast_day,
99+
total_beds,
100+
ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_occupied,
101+
total_beds - ROUND(LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds)) AS predicted_vacancies,
102+
ROUND(100.0 * LEAST(GREATEST(occupied_beds + cum_net_flow, 0), total_beds) / total_beds, 1) AS predicted_occupancy_pct,
103+
CASE WHEN (occupied_beds + cum_net_flow) / total_beds::float >= {{bottleneck_threshold}}
104+
THEN 'BOTTLENECK RISK' ELSE 'OK' END AS status
105+
FROM forecast
106+
ORDER BY floor, ward, forecast_day;
107+
```
108+
109+
## Notes
110+
111+
- **Tunable variables:** See the Parameters table above.
112+
- `admission_multiplier` and `turnover_multiplier` scale the historical daily averages to reflect expected surges/slowdowns.
113+
- `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.
114+
- Results are ordered by floor, then ward, then forecast day — giving a 7-day trend per ward.
115+
116+
*Last updated: 2026-07-24*

0 commit comments

Comments
 (0)