md`A dialyzer is the filter in a dialysis machine that pulls excess fluid out of a patient's blood. Twenty of them were tested, each run at seven pressure settings and two blood-flow settings, and the amount of fluid removed per hour (the rate) was recorded. The rate rises with pressure, then levels off, and sits higher at the higher blood flow.
I modelled all twenty devices together so the model learns one shared curve and how much each device strays from it. Because it's Bayesian, the result is a range of plausible answers rather than a single number, which is what lets the next pages say things like "there is a 90% chance the rate stays under 50" or "device 10 is almost certainly the lowest."
Thumbnail icon from [Magnific from Flaticon](https://www.flaticon.com/free-icon/filtration_12041804?term=mesh+filter&related_id=12041804)
`svg`<svg viewBox="0 0 720 160" style="width:100%;height:auto;max-height:100%;display:block;font-family:inherit">
<defs>
<marker id="arr" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="12" markerHeight="12" markerUnits="userSpaceOnUse" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#e4e6e8"/>
</marker>
</defs>
${edge(185, 66, 286, 103)}
${edge(360, 66, 360, 101)}
${edge(535, 66, 434, 103)}
${node(120, 40, "pressure", "#434a52", "continuous, set")}
${node(360, 40, "blood_flow", "#434a52", "categorical, 200 or 300")}
${node(600, 40, "dialyzer", "#434a52", "categorical, 20 devices")}
${node(360, 128, "rate", "#8a6a2a", "continuous, measured")}
</svg>`Plot.plot({
width: width * 0.46,
height: 300,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate", grid: true},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow"},
marks: [
Plot.line(obs, {x: "pressure", y: "rate", z: "dialyzer", stroke: d => String(d.blood_flow), strokeOpacity: 0.8}),
Plot.dot(obs, {x: "pressure", y: "rate", fill: "#dfe3e6", r: 2, fillOpacity: 0.8})
]
})box = (v) => Plot.plot({
width: width * 0.21,
height: 240,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "blood flow", type: "band"},
y: {labelArrow: "none", labelAnchor: "top", label: v, grid: true},
marks: [Plot.boxY(obs_flow, {x: "flow", y: v, fill: "#434a52", stroke: "#e4e6e8"})]
})node = (x, y, label, fill, sub) => svg`<g>
<rect x="${x - 76}" y="${y - 26}" width="152" height="52" rx="10" fill="${fill}" stroke="#e4e6e8" stroke-width="1.5"/>
<text x="${x}" y="${y + (sub ? -3 : 6)}" text-anchor="middle" font-size="17" font-weight="600" fill="#e4e6e8">${label}</text>
${sub ? svg`<text x="${x}" y="${y + 16}" text-anchor="middle" font-size="11" fill="#b4b9bf">${sub}</text>` : ""}
</g>`html`<div class="pm-settings">
${viewof who}
${q === 5 || one !== null ? "" : viewof blood_flow}
${q === 1 || q === 2 || q === 5 ? viewof pressure : ""}
${q === 2 || q === 4 ? viewof limit : ""}
${q === 3 ? viewof target : ""}
${q === 4 ? viewof prob : ""}
${q === 5 ? viewof threshold : ""}
${viewof ci}
<div class="pm-answer">${answer}</div>
</div>`html`${tex.block`
\begin{aligned}
\text{rate} &= a\,\bigl(1 - e^{-c\,\text{pressure}}\bigr) + b \\[4pt]
b &= b_0 + b_{\text{bf}}\,\text{blood\_flow} + \sigma_b\, z_b \\
c &= \exp\!\bigl(c_0 + c_{\text{bf}}\,\text{blood\_flow} + \sigma_c\, z_c\bigr) \\[4pt]
\text{measured rate} &= \text{rate} + \sigma\,\varepsilon
\end{aligned}
`}
<p style="margin-top:0.5rem">${tex`z_b, z_c, \varepsilon \sim \mathrm{N}(0,1)`}. For "all dialyzers" the ${tex`z`} values are random, a different hypothetical device in every sample, so the spread between devices is included. For a chosen dialyzer its own estimated ${tex`z_b, z_c`} are used. Every answer is about the measured rate, so the noise ${tex`\sigma\varepsilon`} is always included.</p>`Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: q === 3 ? "pressure" : q === 5 ? "Δ rate, 300 minus 200" : "rate"},
y: {labelArrow: "none", labelAnchor: "top", label: "draws", grid: true},
marks: q === 5 ? [
Plot.rectY(diff.draws, Plot.binX({y: "count"}, {x: d => d, fill: "#4f8ef7", fillOpacity: 0.55, thresholds: 40})),
Plot.ruleX([diff.mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([diff.lo, diff.hi], {stroke: "#f5a623", strokeDasharray: "4,3"}),
Plot.ruleX([threshold], {stroke: "#ff6b6b", strokeWidth: 2}),
Plot.text([threshold], {x: d => d, y: 0, text: () => `Δ rate ${threshold}`, dy: -8, dx: 4, textAnchor: "start", fill: "#ff6b6b"})
] : q === 3 ? [
Plot.rectY(x_here.filter(v => v !== null), Plot.binX({y: "count"}, {x: d => d, fill: "#4f8ef7", fillOpacity: 0.55, thresholds: 40})),
Plot.ruleX([inv.mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([inv.lo, inv.hi], {stroke: "#f5a623", strokeDasharray: "4,3"})
] : [
Plot.rectY(y_here, Plot.binX({y: "count"}, {x: d => d, fill: "#4f8ef7", fillOpacity: 0.55, thresholds: 40})),
Plot.ruleX([pred.mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([pred.lo, pred.hi], {stroke: "#f5a623", strokeDasharray: "4,3"}),
...(q === 2 || q === 4 ? [
Plot.ruleX([limit], {stroke: "#ff6b6b", strokeWidth: 2}),
Plot.text([limit], {x: d => d, y: 0, text: () => `limit ${limit}`, dy: -8, dx: 4, textAnchor: "start", fill: "#ff6b6b"})
] : [])
]
})q === 5 ? Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate", grid: true},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow"},
marks: [
Plot.areaY(band_both, {x: "x", y1: "lo", y2: "hi", fill: "bf", fillOpacity: 0.25}),
Plot.line(band_both, {x: "x", y: "mean", stroke: "bf", strokeWidth: 2.5}),
Plot.dot(obs, {x: "pressure", y: "rate", fill: d => String(d.blood_flow), r: 2.5, fillOpacity: 0.6}),
Plot.ruleX([pressure], {stroke: "#f5a623", strokeWidth: 2})
]
}) : Plot.plot({
width: width * 0.62,
height: 310,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: "pressure"},
y: {labelArrow: "none", labelAnchor: "top", label: "rate", grid: true},
marks: [
Plot.areaY(band, {x: "x", y1: "lo", y2: "hi", fill: "#8dc0ff", fillOpacity: 0.35}),
Plot.line(band, {x: "x", y: "mean", stroke: "#4f8ef7", strokeWidth: 2.5}),
Plot.dot(obs.filter(d => d.blood_flow === bf), {x: "pressure", y: "rate", fill: "#dfe3e6", r: 2.5, fillOpacity: one === null ? 0.6 : 0.2}),
...(one === null ? [] : [Plot.dot(obs.filter(d => d.dialyzer === one), {x: "pressure", y: "rate", fill: "#dfe3e6", r: 4})]),
Plot.ruleX([x_mark], {stroke: "#f5a623", strokeWidth: 2}),
...(q === 2 || q === 4 ? [Plot.ruleY([limit], {stroke: "#ff6b6b", strokeDasharray: "4,3"})] : []),
...(q === 3 ? [Plot.ruleY([target], {stroke: "#ff6b6b", strokeDasharray: "4,3"})] : [])
]
})viewof question = Inputs.radio(
["1. What will the rate be at a pressure?",
"2. How likely is the rate to stay below a limit?",
"3. What pressure gives a target rate?",
"4. What is the highest pressure that keeps the rate below a limit?",
"5. How likely is the rate at blood flow 300 to exceed the rate at 200 by more than Δ rate?"],
{value: "1. What will the rate be at a pressure?", label: ""})z = {
const r = d3.randomNormal.source(d3.randomLcg(42))(0, 1)
if (one === null) return draws.map(() => ({b: r(), c: r(), e: r(), e2: r()}))
const bf1 = obs.find(d => d.dialyzer === one).blood_flow
return draws.map(d => ({
b: (d[`b_${one}`] - (d.b_0 + d.b_blood_flow * bf1)) / d.sigma_b,
c: (Math.log(d[`c_${one}`]) - (d.c_0 + d.c_blood_flow * bf1)) / d.sigma_c,
e: r(),
e2: r()
}))
}diff = {
const hi = params_at(300), lo = params_at(200)
const d = hi.map((p, i) => (curve(p, pressure) + p.sigma * z[i].e) - (curve(lo[i], pressure) + lo[i].sigma * z[i].e2))
return {
draws: d,
mean: d3.mean(d),
lo: d3.quantile(d, lo_q),
hi: d3.quantile(d, hi_q),
p_above: d3.mean(d, v => v > threshold ? 1 : 0)
}
}answer = {
const f1 = d3.format(".1f"), f2 = d3.format(".2f")
const whom = one === null ? `a new dialyzer at blood flow ${bf}` : `dialyzer ${one} (blood flow ${bf})`
const pct = d3.format(".0%")(ci)
const kind = "measured rate"
if (q === 1) return html`<b>${f1(pred.mean)}</b> ${pct} interval ${f1(pred.lo)} to ${f1(pred.hi)}<br><small>${kind} at pressure ${pressure}, ${whom}</small>`
if (q === 2) return html`<b>P(rate below ${limit}) = ${f2(pred.p_below)}</b><br><small>${kind} at pressure ${pressure}, ${whom}</small>`
if (q === 3) return inv.mean === null
? html`<b>rate ${target} is not reachable</b><br><small>the measured rate never gets there for ${whom}</small>`
: html`<b>pressure ${f2(inv.mean)}</b> ${pct} interval ${f2(inv.lo)} to ${f2(inv.hi)}<br><small>for a measured rate of ${target}, ${whom}, reachable in ${d3.format(".0%")(inv.reach)} of draws</small>`
if (q === 5) return html`<b>${f1(diff.mean)}</b> ${pct} interval ${f1(diff.lo)} to ${f1(diff.hi)}<br><b>P(Δ rate above ${threshold}) = ${f2(diff.p_above)}</b><br><small>measured rate at pressure ${pressure} at blood flow 300 minus at 200, ${one === null ? "a new dialyzer, same z at both flows" : `dialyzer ${one}'s own effects at both flows`}</small>`
return x_keep === null
? html`<b>no pressure in range keeps P(rate below ${limit}) at ${prob}</b>`
: html`<b>pressure ${f2(x_keep)}</b><br><small>highest pressure with P(rate below ${limit}) ≥ ${prob}, ${whom}</small>`
}html`${tex.block`
\begin{aligned}
\text{deviation of } b &= b_i - \hat b_i, \qquad \hat b_i = \mu_b \;\text{or}\; b_0 + b_{\text{bf}}\,\text{blood\_flow}_i \\[4pt]
\text{deviation of } c &= c_i / \hat c_i - 1, \qquad \hat c_i = e^{\log\mu_c} \;\text{or}\; e^{\,c_0 + c_{\text{bf}}\,\text{blood\_flow}_i}
\end{aligned}
`}
<p style="margin-top:0.5rem">Each dialyzer's own value minus what the population part predicts for it: a single mean when the model has no blood flow on that parameter, its blood-flow line when it does. Zero means the device is functioning as the average of all devices would. Model 1 shows the deviations splitting into two clumps by blood flow. Putting blood flow on the parameter (model 2 for c, model 3 for b) turns them into one scatter around zero.</p>`Plot.plot({
width: width * 0.62,
height: 330,
marginLeft: 70,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: show === "deviation" ? (par === "c" ? "deviation of c (relative)" : "deviation of b") : par, grid: true},
y: {labelArrow: "none", labelAnchor: "top", label: "dialyzer", domain: order},
color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow"},
marks: [
...(show === "deviation" ? [Plot.ruleX([0], {stroke: "#b4b9bf", strokeDasharray: "4,3"})] : []),
Plot.ruleY(per, {y: d => String(d.dialyzer), x1: "lo", x2: "hi", stroke: d => String(d.blood_flow), strokeWidth: d => d.dialyzer === picked ? 4 : 2, strokeOpacity: d => picked === null || d.dialyzer === picked ? 1 : 0.35}),
Plot.dot(per, {y: d => String(d.dialyzer), x: "mean", fill: d => String(d.blood_flow), r: d => d.dialyzer === picked ? 7 : 5,
stroke: d => d.dialyzer === picked ? "#f5a623" : "none", strokeWidth: 2,
tip: {fontSize: 18, lineHeight: 1.5, format: {x: false, y: false, fill: false, r: false, stroke: false}},
channels: {
"Dialyzer": d => `${d.dialyzer}, blood flow ${d.blood_flow}`,
"Mean effect": d => fmt(d.mean),
"Credible interval": d => `${fmt(d.lo)} to ${fmt(d.hi)} (${d3.format(".0%")(ci2)})`
}})
]
})Plot.plot({
width: width * 0.62,
height: 300,
marginLeft: 50,
marginBottom: 55,
style: {background: "transparent", fontSize: 12, color: "#e4e6e8"},
x: {labelArrow: "none", labelAnchor: "center", labelOffset: 42, label: show === "deviation" ? "deviation" : par},
y: {labelArrow: "none", labelAnchor: "top", label: "draws", grid: true},
marks: [
Plot.rectY(pooled, Plot.binX({y: "count"}, {x: d => d, fill: "#4f8ef7", fillOpacity: 0.35, thresholds: 50})),
...(picked === null ? [] : [
Plot.rectY(vals[picked], Plot.binX({y: "count"}, {x: d => d, fill: "#f5a623", fillOpacity: 0.75, thresholds: 50})),
Plot.ruleX([per.find(d => d.dialyzer === picked).mean], {stroke: "#f5a623", strokeWidth: 3}),
Plot.ruleX([per.find(d => d.dialyzer === picked).lo, per.find(d => d.dialyzer === picked).hi], {stroke: "#f5a623", strokeDasharray: "4,3"})
]),
...(show === "deviation" ? [Plot.ruleX([0], {stroke: "#b4b9bf", strokeDasharray: "4,3"})] : [])
]
})vals = Object.fromEntries(d3.range(1, 21).map(i => {
const bf_i = flow_of[i]
const v = dr.map(d => {
const raw = d[`${par}_${i}`]
if (show === "raw") return raw
if (par === "b") {
const hat = "b_blood_flow" in d ? d.b_0 + d.b_blood_flow * bf_i : d.mu_b
return raw - hat
}
const hat = "c_blood_flow" in d ? Math.exp(d.c_0 + d.c_blood_flow * bf_i) : Math.exp(d.log_mu_c)
return raw / hat - 1
})
return [i, v]
}))ranks = {
const n = dr.length, ids = d3.range(1, 21)
const low = Object.fromEntries(ids.map(i => [i, 0])), high = Object.fromEntries(ids.map(i => [i, 0]))
for (let k = 0; k < n; k++) {
let lo = ids[0], hi = ids[0]
for (const i of ids) {
if (vals[i][k] < vals[lo][k]) lo = i
if (vals[i][k] > vals[hi][k]) hi = i
}
low[lo] += 1; high[hi] += 1
}
return {low, high, n}
}sentence = {
const pct = d3.format(".0%")(ci2), f2 = d3.format(".2f")
if (picked === null) {
const lo = per.slice().sort((p, q) => q.p_low - p.p_low)[0], hi = per.slice().sort((p, q) => q.p_high - p.p_high)[0]
return html`<b>Lowest ${par}: dialyzer ${lo.dialyzer}</b> P = ${f2(lo.p_low)}<br><b>Highest ${par}: dialyzer ${hi.dialyzer}</b> P = ${f2(hi.p_high)}<br><small>pick a dialyzer above for its own numbers</small>`
}
const d = per.find(p => p.dialyzer === picked)
const what = show === "raw" ? `${par} = ${fmt(d.mean)}` :
par === "b" ? `${par} is ${d3.format(".2f")(Math.abs(d.mean))} ${d.mean < 0 ? "below" : "above"} its blood-flow line`
: `${par} is ${d3.format(".0%")(Math.abs(d.mean))} ${d.mean < 0 ? "slower" : "faster"} than its blood-flow line`
return html`<b>Dialyzer ${picked}</b> (blood flow ${d.blood_flow})<br>${what}<br><small>${pct} interval ${fmt(d.lo)} to ${fmt(d.hi)}</small><br>P(lowest) = ${f2(d.p_low)} P(highest) = ${f2(d.p_high)}`
}