--- title: "Stock and flow: population, admissions and length of stay" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Stock and flow: population, admissions and length of stay} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} # the vignette runs in full in GitHub CI and for the package tarball (NOT_CRAN), on r-universe # (MY_UNIVERSE), on the package website (IN_PKGDOWN) and in an interactive session; when CRAN # re-builds it in its check, none of these is set and only this chunk runs if (!interactive() && !any(nzchar(Sys.getenv(c("NOT_CRAN", "MY_UNIVERSE", "IN_PKGDOWN"))))) { knitr::knit_exit() } knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(rmoriebricklayer) ``` A custodial or clinical population can be described two ways: the **stock** (how many are held on an average day) and the **flow** (how many pass through in a period). The two are linked by the length of stay, and a published table that reports one without the others is easy to misread. These functions implement the standard measures (Lakner's formulation, as used in the Canadian corrections statistics) with exact intervals. ## Average daily population The stock. Detention days in a period divided by the days in the period: ```{r stock-and-flow-adp} # Lakner's worked example: 3,000 inmates served 13,500 detention days in a year adp(13500) # per-person days over a 30-day period give the same total adp(c(10, 20, 30), t = 30) ``` `adpcnts()` estimates person-days from periodic headcounts (a monthly census) when daily data are not published. ## Average length of stay Detention days divided by the number of people who completed a stay: ```{r stock-and-flow-alos} # Lakner: 2,700 inmates admitted and released served 12,150 detention days alos(12150, 2700) ``` ## Admissions implied by stock and stay Little's law ties the three together: in a stationary system, stock = flow × stay, so admissions = ADP × days / ALOS. Lakner (p. 20) runs it the other way: an average daily population of 25 and 1,750 admissions in a year imply a stay of 5.2 days, and back again: ```{r stock-and-flow-admissions} alosimplied <- 25 * 365 / 1750 round(alosimplied, 1) admits(25, alosimplied) ``` ## Stock and flow together `stkflow()` takes detention days and people per period, with an exposure (the population at risk) to turn counts into rates, and reports how the stock, the flow and the stay moved between periods. The pattern it is built to surface: **fewer people, held longer**, so the flow falls while the stock rises. ```{r stock-and-flow-stockflow} stkflow( days = c(115674, 126121), people = c(12647, 9608), prd = c("2023", "2025"), expo = c(15495050, 16256538) ) ``` A table that reported only admissions would show a decline; one that reported only the daily count would show growth. Both are true, and the length of stay is the reason. ## The distribution of stays The mean stay is pulled far above the median by a few long stays, so the summary that matters is the whole distribution: ```{r stock-and-flow-stays} stays <- c(rep(1, 40), rep(3, 30), rep(10, 20), 60, 90, 120) staysmry(stays) staysmry(stays)[, c("mean", "median", "max")] ``` `staysmry()` gives the mean with its interval, the quantiles and the maximum, so a report can say "half of stays were three days or less, and the longest was 120" instead of "the average stay was 9.6 days", which describes almost no one. ## Periods and fiscal years Published series use fiscal years and uneven periods: ```{r stock-and-flow-periods} perdays("2024-04-01", "2025-03-31") fylbl(2024) ``` ## From a published table 1. `adp()` for the stock from detention days, or `adpcnts()` from headcounts. 2. `alos()` for the stay; `admits()` to check the implied flow against the published one. 3. `stkflow()` across periods, with the population as exposure. 4. `staysmry()` whenever per-person stays are available, and report the median. ## Indigenous examples Stock and flow measures need counts of people and the days they spend in custody or care. The census excerpt bundled with the package counts foster children, a stock on census day: ```{r stock-and-flow-indigenous-stock} cp <- indiex("censuspop") fc <- cp[cp$GEO_NAME == "Canada" & cp$AGE_GROUP == "0 to 14 years" & cp$RESIDENCE == "Total - Residence on or off reserve", ] kids <- fc[grepl("Total - Children in census families", fc$CHARACTERISTIC_NAME), c("FIRST_NATIONS", "NON_IND")] fost <- fc[grepl("Foster children", fc$CHARACTERISTIC_NAME), c("FIRST_NATIONS", "NON_IND")] round(100 * unlist(fost) / unlist(kids), 2) ``` Turning that stock into admissions and lengths of stay needs the flow data held by child welfare agencies. ## The figures in a picture The left panel puts the counts on a common footing: children in foster care per 1,000 children, First Nations and non-Indigenous, from the census tables above. The right panel shows how the lengths of stay are spread: most are short, a few are very long, which is why the mean and the median differ. ```{r stock-and-flow-stock-flow-plots, fig.width = 8, fig.height = 4} op <- par(mfrow = c(1, 2)) per1000 <- unlist(fost) / unlist(kids) * 1000 barplot(per1000, names.arg = c("First Nations", "non-Indigenous"), col = blpal()[1:2], ylab = "per 1,000 children", main = "Children in foster care") hist(stays, breaks = 30, col = blpal()[2], xlab = "length of stay", main = "Lengths of stay") par(op) ``` ## Reference Lakner, *A Manual of Correctional Statistics* (the ADP and ALOS worked examples at pp. 15 and 17 are reproduced above).