Queueing Theory and ICU Colonization

An intensive care unit is a queue. Patients (the customers) arrive, occupy a bed (the server) for a while, and leave; when every bed is full a new arrival is diverted elsewhere. Layered on top of that flow is an epidemiological process: while they share the unit, patients can pass a colonizing organism — MRSA, VRE, or a carbapenem-resistant Enterobacterales — to one another via the hands of staff and shared surfaces. Queueing theory describes who is in the beds and how fast they turn over; a small Markov chain on top describes how many of them are colonized, and together they tell you the endemic prevalence and how much each infection-prevention (IP) practice actually buys.

Left: the two-bed ward as a birth-death chain on the number colonized, with colonization moving the state up and discharge or clearance moving it down. Right: endemic colonized prevalence rising with the transmission rate, and how decolonization, admission screening, and a smaller ward shift the curve.
Figure 1. Left: the two-bed ward as a birth-death chain on the number colonized, with colonization moving the state up and discharge or clearance moving it down. Right: endemic colonized prevalence rising with the transmission rate, and how decolonization, admission screening, and a smaller ward shift the curve.

The ward as a queue#

Strip out colonization for a moment and just watch the beds. Admissions arrive as a Poisson process at rate λ\lambda, each patient stays for an exponentially distributed length of stay with mean 1/μ1/\mu, and there are cc beds and no waiting room — a full ICU turns patients away rather than queueing them. This is the M/M/cc/cc (Erlang loss) system, and the chance an arrival finds all cc beds full is the Erlang-B formula B(c,a)=ac/c!k=0cak/k!,a=λ/μ,B(c, a) = \frac{a^c/c!}{\sum_{k=0}^{c} a^k/k!}, \qquad a = \lambda/\mu, with aa the offered load in Erlangs. For a busy unit the occupancy sits close to cc, so from here on we take the ward to be full, and read μ\mu as the per-bed turnover rate: every bed empties and refills at rate μ\mu, mean stay 1/μ1/\mu.

A two-bed ward as a birth-death chain#

Now add the bug. Let the state XX be the number of colonized patients among the occupied beds; in a two-bed bay X{0,1,2}X \in \{0, 1, 2\}. Three rates move it.

For N=2N = 2 this is a birth-death chain with up-rates b0=2μfb_0 = 2\mu f (both beds can import) and b1=β+μfb_1 = \beta + \mu f (transmission to the one susceptible, plus its import risk), and down-rates d1=μ(1f)+γd_1 = \mu(1-f)+\gamma and d2=2(μ(1f)+γ)d_2 = 2\big(\mu(1-f)+\gamma\big) (Figure 1, left). A birth-death chain satisfies detailed balance, so the stationary distribution follows by multiplying the ratios up the ladder: π1=π0b0d1,π2=π1b1d2,π0+π1+π2=1.\pi_1 = \pi_0\,\frac{b_0}{d_1}, \qquad \pi_2 = \pi_1\,\frac{b_1}{d_2}, \qquad \pi_0 + \pi_1 + \pi_2 = 1 .

The ward reproduction number#

One summary decides whether transmission can sustain itself inside the unit. A newly colonized patient transmits at rate β\beta and stays colonized-and-present for a mean time 1/(μ+γ)1/(\mu + \gamma) before being discharged or clearing, so it generates RA=βμ+γ(1)R_A = \frac{\beta}{\mu + \gamma} \tag{1} secondary colonizations — the ward reproduction number, the hospital analog of R0R_0. When RA>1R_A > 1, colonization is self-sustaining: it persists through in-ward spread even if no one is ever admitted already carrying (f=0f = 0). When RA<1R_A < 1, in-ward chains fade out on their own and prevalence is importation-driven — kept alight only by colonized admissions, so controlling who comes in matters more than controlling spread once they are there. Notice importation ff does not appear in (1): it seeds outbreaks but does not change the threshold.

Scaling up to a full unit#

Nothing above was special to two beds. For an NN-bed unit the same three mechanisms give a birth-death chain on X{0,,N}X \in \{0, \dots, N\} with bX=βX(NX)N1transmission+μf(NX)importation,dX=(μ(1f)+γ)X,b_X = \underbrace{\beta\,\frac{X(N-X)}{N-1}}_{\text{transmission}} + \underbrace{\mu f\,(N-X)}_{\text{importation}}, \qquad d_X = \big(\mu(1-f) + \gamma\big)\,X , and the stationary prevalence E[X]/N\mathbb{E}[X]/N follows from the same detailed-balance product. This is exactly an SIS epidemic running in a demographically open population — the ward — with importation playing the role of an external reservoir.

Ward size matters more than the mean-field intuition suggests. Because transmission is frequency-dependent, RAR_A in (1) does not depend on NN, yet the endemic prevalence does: in a tiny bay, chance fade-out repeatedly extinguishes transmission, whereas a large unit sustains it. The same per-encounter transmissibility that barely simmers in a two-bed bay can hold a twelve-bed unit at high prevalence — the hospital version of a critical community size (Figure 1, right).

Infection-prevention levers#

Each IP practice is a specific parameter change, and (1) and the stationary distribution price them out.

PracticeMechanismParameter
Hand hygiene, contact precautions, better staffing ratiosfewer effective colonized→susceptible contactslowers β\beta
Decolonization (chlorhexidine bathing, mupirocin)shortens carriageraises γ\gamma
Admission screening + preemptive isolationcatches importers before they seedlowers effective ff
Cohorting / isolating known carriersseparates carriers from susceptibleslowers β\beta

Two practices can hit the same threshold by different routes: halving β\beta and raising γ\gamma enough both drive RAR_A to 11, but decolonization also shortens carriage, so it tends to lower prevalence a little more per unit of RAR_A. When RAR_A is already below 11, the residual prevalence is all importation, and screening (lowering ff) does the heavy lifting that hand hygiene no longer can.

A worked example#

Take a two-bed bay with turnover μ=0.2day1\mu = 0.2\,\text{day}^{-1} (mean stay 55 days), spontaneous clearance γ=0.05day1\gamma = 0.05\,\text{day}^{-1}, admission prevalence f=0.05f = 0.05, and transmission β=0.5day1\beta = 0.5\,\text{day}^{-1}, so RA=0.5/0.25=2R_A = 0.5/0.25 = 2. The per-colonized loss rate is μ(1f)+γ=0.2(0.95)+0.05=0.24\mu(1-f)+\gamma = 0.2(0.95) + 0.05 = 0.24, giving up-rates b0=2(0.2)(0.05)=0.02b_0 = 2(0.2)(0.05) = 0.02 and b1=0.5+0.01=0.51b_1 = 0.5 + 0.01 = 0.51, and down-rates d1=0.24d_1 = 0.24, d2=0.48d_2 = 0.48. Detailed balance gives π1/π0=0.02/0.24=0.0833\pi_1/\pi_0 = 0.02/0.24 = 0.0833 and π2/π1=0.51/0.48=1.0625\pi_2/\pi_1 = 0.51/0.48 = 1.0625, which normalize to π(0.853, 0.071, 0.076)\pi \approx (0.853,\ 0.071,\ 0.076). The colonized prevalence is E[X]/2=(0.071+20.076)/211%\mathbb{E}[X]/2 = (0.071 + 2\cdot 0.076)/2 \approx 11\%. In a twelve-bed unit the same parameters give roughly 46%46\% colonized — four times higher — precisely the ward-size effect above. Halving β\beta with hand hygiene, or raising γ\gamma with decolonization, drops both figures sharply, as the code below tabulates.

In code#

We build the generator, solve for the stationary prevalence exactly by detailed balance, and cross-check it with a Gillespie simulation.

R#

R
stationary <- function(N, beta, mu, gamma, f) {
  X <- 0:N; Sus <- N - X
  b <- ifelse(N > 1, beta * X * Sus / (N - 1), 0) + mu * f * Sus  # up
  d <- (mu * (1 - f) + gamma) * X                                 # down
  logpi <- c(0, cumsum(log(b[1:N]) - log(d[2:(N + 1)])))          # detailed balance
  pi <- exp(logpi - max(logpi)); pi <- pi / sum(pi)
  list(pi = pi, prevalence = sum(X * pi) / N)
}

mu <- 0.2; f <- 0.05
for (s in list(c(0.50, 0.05), c(0.25, 0.05), c(0.50, 0.30), c(0.25, 0.30))) {
  p2  <- stationary(2,  s[1], mu, s[2], f)$prevalence
  p12 <- stationary(12, s[1], mu, s[2], f)$prevalence
  cat(sprintf("R_A=%.2f  2-bed=%.1f%%  12-bed=%.1f%%\n",
              s[1] / (mu + s[2]), 100 * p2, 100 * p12))
}

Python#

Python
import numpy as np

def stationary(N, beta, mu, gamma, f):
    """Birth-death CTMC for the number colonized in a full N-bed ward."""
    b = np.zeros(N + 1)                        # colonization (up) rates
    d = np.zeros(N + 1)                        # loss (down) rates
    for X in range(N + 1):
        Sus = N - X
        transmission = beta * X * Sus / (N - 1) if N > 1 else 0.0
        b[X] = transmission + mu * f * Sus     # cross-transmission + importation
        d[X] = (mu * (1 - f) + gamma) * X      # discharge-to-susceptible + clearance
    pi = np.ones(N + 1)
    for X in range(1, N + 1):
        pi[X] = pi[X - 1] * b[X - 1] / d[X]    # detailed balance up the ladder
    pi /= pi.sum()
    return pi, (np.arange(N + 1) * pi).sum() / N

mu, f = 0.2, 0.05
print("practice          R_A   2-bed   12-bed")
for name, beta, gamma in [("baseline",       0.50, 0.05),
                          ("hand hygiene",   0.25, 0.05),
                          ("decolonization", 0.50, 0.30),
                          ("both",           0.25, 0.30)]:
    _, p2  = stationary(2,  beta, mu, gamma, f)
    _, p12 = stationary(12, beta, mu, gamma, f)
    print(f"{name:15s} {beta / (mu + gamma):5.2f}  {p2:5.1%}  {p12:5.1%}")
practice          R_A   2-bed   12-bed
baseline         2.00  11.1%  46.0%
hand hygiene     1.00   7.7%  14.8%
decolonization   1.00   3.9%   8.3%
both             0.50   3.0%   3.6%

A stochastic simulation of the same generator confirms the exact prevalence.

Python
def gillespie_prevalence(N, beta, mu, gamma, f, T, seed):
    rng = np.random.default_rng(seed)
    X, t, colonized_bed_days = 0, 0.0, 0.0
    while t < T:
        Sus = N - X
        up = beta * X * Sus / (N - 1) + mu * f * Sus
        down = (mu * (1 - f) + gamma) * X
        rate = up + down
        dt = rng.exponential(1 / rate)
        colonized_bed_days += X * dt           # time-integral of colonized count
        t += dt
        X += 1 if rng.random() < up / rate else -1
    return colonized_bed_days / (t * N)         # long-run colonized prevalence

sim = gillespie_prevalence(12, 0.50, mu, 0.05, f, T=100_000, seed=1)
_, exact = stationary(12, 0.50, mu, 0.05, f)
print(round(sim, 2), round(exact, 2))           # simulated vs exact prevalence
0.46 0.46

Julia#

Julia
function stationary(N, beta, mu, gamma, f)
    X = 0:N; Sus = N .- X
    b = (N > 1 ? beta .* X .* Sus ./ (N - 1) : zeros(N + 1)) .+ mu * f .* Sus
    d = (mu * (1 - f) + gamma) .* X
    logpi = cumsum(vcat(0.0, log.(b[1:N]) .- log.(d[2:N+1])))
    pi = exp.(logpi .- maximum(logpi)); pi ./= sum(pi)
    (pi, sum(X .* pi) / N)
end

mu, f = 0.2, 0.05
for (beta, gamma) in [(0.50, 0.05), (0.25, 0.05), (0.50, 0.30), (0.25, 0.30)]
    _, p2  = stationary(2,  beta, mu, gamma, f)
    _, p12 = stationary(12, beta, mu, gamma, f)
    println("R_A=", round(beta / (mu + gamma); digits = 2),
            "  2-bed=", round(100p2; digits = 1),
            "%  12-bed=", round(100p12; digits = 1), "%")
end

Why it matters#

Colonization is the reservoir from which hospital infections and resistant-organism outbreaks erupt, and control budgets are finite, so the question is always which lever to pull. Casting the ward as a queue with a colonization chain on top separates the two forces that keep a bug endemic — in-ward transmission (RAR_A) and importation (ff) — and shows that they call for different responses: hand hygiene and cohorting when RA>1R_A > 1, admission screening when the unit is running on imports. It also explains why the same organism can smoulder in a small step-down unit yet blaze in a large ICU, why understaffing (which effectively raises β\beta) shows up as outbreaks, and how to compare a bundle of interventions before spending on any of them.