Mohammad Savarmand
  • Projects
  • CV
Dialyzer Ultrafiltration: A Bayesian Analysis
  • Summary
  • Inference
  • Group effects, with and without blood flow
Dialyzer ultrafiltration
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, in ml/hr) 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 ml/hr" 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)
`
Causal diagram (directed acyclic graph, DAG)
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>`
Rate vs pressure, one line per dialyzer
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 (dmHg)"},
  y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", grid: true},
  color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow (ml/min)"},
  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})
  ]
})
Rate and pressure by blood flow
html`<div style="display:flex;gap:12px;justify-content:space-around">${box("rate")}${box("pressure")}</div>`
obs_flow = obs.map(d => ({...d, flow: String(d.blood_flow)}))
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 (ml/min)", type: "band"},
  y: {labelArrow: "none", labelAnchor: "top", label: v === "rate" ? "rate (ml/hr)" : "pressure (dmHg)", 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>`
edge = (x1, y1, x2, y2) => svg`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#e4e6e8" stroke-width="2" marker-end="url(#arr)"/>`
throttle = (input) => Generators.observe(notify => {
  // sliders fire on every pixel; apply the latest value once per screen frame
  let pending = false
  const fire = () => { pending = false; notify(input.value) }
  const onInput = () => { if (!pending) { pending = true; requestAnimationFrame(fire) } }
  notify(input.value)
  input.addEventListener("input", onInput)
  return () => input.removeEventListener("input", onInput)
})
Question
html`<div class="pm-questions">${viewof question}</div>`
Settings and answer
settings_card = html`<div class="pm-settings">
${viewof who}
${q === 5 || one !== null ? "" : viewof blood_flow}
${q === 1 || q === 2 || q === 5 ? viewof pressure_in : ""}
${q === 2 || q === 4 ? viewof limit_in : ""}
${q === 3 ? viewof target_in : ""}
${q === 4 ? viewof prob_in : ""}
${q === 5 ? viewof threshold_in : ""}
${viewof ci_in}
<div class="pm-answer" id="pm-answer-2"></div>
</div>`
show_answer_2 = {
  // written into the card's slot, so the sliders are never re-inserted mid-drag
  const slot = settings_card.querySelector("#pm-answer-2")
  if (slot) slot.replaceChildren(answer)
  return answer
}
The model
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>`
Draws behind the answer, counted share shaded
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 (dmHg)" : q === 5 ? "Δ rate (ml/hr), 300 minus 200" : "rate (ml/hr)"},
  y: {labelArrow: "none", labelAnchor: "top", label: "density", grid: true, ticks: 0},
  marks: [
    Plot.areaY(hist.curve, {x: "x", y: "y", fill: "#4f8ef7", fillOpacity: 0.3}),
    Plot.areaY(hist.curve.filter(d => d.counted), {x: "x", y: "y", fill: "#f5a623", fillOpacity: 0.7}),
    Plot.line(hist.curve, {x: "x", y: "y", stroke: "#8dc0ff", strokeWidth: 1.5}),
    Plot.ruleX([hist.mean], {stroke: "#f5a623", strokeWidth: 3}),
    Plot.ruleX([hist.lo, hist.hi], {stroke: "#f5a623", strokeDasharray: "4,3"}),
    ...(hist.cut === null ? [] : [
      Plot.ruleX([hist.cut], {stroke: "#ff6b6b", strokeWidth: 2}),
      Plot.text([hist.cut], {x: d => d, y: 0, text: () => hist.cut_label, dy: -8, dx: 4, textAnchor: "start", fill: "#ff6b6b"})
    ])
  ]
})
hist = {
  // which draws the histogram shows, and which of them the answer counts
  let all, lo, hi, mean, cut = null, cut_label = "", counted
  if (q === 5) {
    all = diff.draws; mean = diff.mean; lo = diff.lo; hi = diff.hi
    cut = threshold; cut_label = `Δ rate ${threshold} ml/hr`
    counted = all.filter(v => v > threshold)
  } else if (q === 3) {
    all = x_here.filter(v => v !== null); mean = inv.mean; lo = inv.lo; hi = inv.hi
    counted = all.filter(v => v >= lo && v <= hi)
  } else if (q === 4) {
    all = x_keep === null ? y_here : y_at(x_keep)
    mean = d3.mean(all); lo = d3.quantile(all, lo_q); hi = d3.quantile(all, hi_q)
    cut = limit; cut_label = `limit ${limit} ml/hr`
    counted = all.filter(v => v < limit)
  } else if (q === 2) {
    all = y_here; mean = pred.mean; lo = pred.lo; hi = pred.hi
    cut = limit; cut_label = `limit ${limit} ml/hr`
    counted = all.filter(v => v < limit)
  } else {
    all = y_here; mean = pred.mean; lo = pred.lo; hi = pred.hi
    counted = all.filter(v => v >= lo && v <= hi)
  }
  const inside = q === 5 ? (x => x > threshold) : (q === 2 || q === 4) ? (x => x < limit) : (x => x >= lo && x <= hi)
  return {all, counted, mean, lo, hi, cut, cut_label, curve: kde(all, inside)}
}
kde = (values, inside) => {
  // gaussian kernel density, Silverman bandwidth. Draws are binned first and the
  // bins smoothed, so the cost is bins x kernel width instead of draws x grid.
  const n = values.length, sd = d3.deviation(values)
  const iqr = d3.quantile(values, 0.75) - d3.quantile(values, 0.25)
  const h = 0.9 * Math.min(sd, iqr / 1.34) * Math.pow(n, -0.2)
  const lo = d3.min(values) - 3 * h, hi = d3.max(values) + 3 * h
  const m = 240, step = (hi - lo) / m
  const counts = new Float64Array(m)
  for (const v of values) { const k = Math.floor((v - lo) / step); if (k >= 0 && k < m) counts[k] += 1 }
  const half = Math.ceil(3 * h / step)
  const kern = d3.range(-half, half + 1).map(j => Math.exp(-0.5 * (j * step / h) ** 2))
  const out = []
  for (let i = 0; i < m; i++) {
    let sum = 0
    for (let j = -half; j <= half; j++) { const k = i + j; if (k >= 0 && k < m) sum += counts[k] * kern[j + half] }
    const x = lo + (i + 0.5) * step
    out.push({x, y: sum / (n * h * Math.sqrt(2 * Math.PI)), counted: inside(x)})
  }
  return out
}
Curve with interval band, data behind it
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 (dmHg)"},
  y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", grid: true},
  color: {legend: true, type: "categorical", domain: ["200", "300"], range: ["#3ddc84", "#4f8ef7"], label: "blood flow (ml/min)"},
  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 (dmHg)"},
  y: {labelArrow: "none", labelAnchor: "top", label: "rate (ml/hr)", 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. At this pressure, what will the rate be?",
   "2. At this pressure, how likely is it for the rate to stay below the limit?",
   "3. What pressure gives the target rate?",
   "4. What is the highest pressure that keeps the rate below the limit?",
   "5. At this pressure, how likely is it for blood flow 300 to add more than Δ rate over 200?"],
  {value: "1. At this pressure, what will the rate be?", label: ""})
viewof pressure_in = Inputs.range([0.2, 3.2], {value: 1.5, step: 0.05, label: "pressure (dmHg)"})
pressure = throttle(viewof pressure_in)
viewof blood_flow = Inputs.radio([200, 300], {value: 300, label: "blood flow (ml/min)"})
viewof who = Inputs.select(["all dialyzers", ...d3.range(1, 21).map(String)], {value: "all dialyzers", label: "dialyzer"})
viewof ci_in = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "credible interval"})
ci = throttle(viewof ci_in)
viewof limit_in = Inputs.range([10, 80], {value: 50, step: 1, label: "rate limit (ml/hr)"})
limit = throttle(viewof limit_in)
viewof target_in = Inputs.range([10, 80], {value: 45, step: 1, label: "target rate (ml/hr)"})
target = throttle(viewof target_in)
viewof prob_in = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "probability"})
prob = throttle(viewof prob_in)
viewof threshold_in = Inputs.range([0, 30], {value: 10, step: 0.5, label: "Δ rate (ml/hr)"})
threshold = throttle(viewof threshold_in)
q = Number(question[0])
one = who === "all dialyzers" ? null : Number(who)
bf = one === null ? blood_flow : obs.find(d => d.dialyzer === one).blood_flow
lo_q = (1 - ci) / 2
hi_q = 1 - (1 - ci) / 2
draws = FileAttachment("explore_draws_model3.csv").csv({typed: true})
obs = FileAttachment("explore_data.csv").csv({typed: true})
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()
  }))
}
params_at = (flow) => draws.map((d, i) => ({
  a: d.a,
  b: d.b_0 + d.b_blood_flow * flow + d.sigma_b * z[i].b,
  c: Math.exp(d.c_0 + d.c_blood_flow * flow + d.sigma_c * z[i].c),
  sigma: d.sigma
}))
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)
  }
}
params = params_at(bf)
curve = (p, x) => p.a * (1 - Math.exp(-p.c * x)) + p.b
use_noise = true
y_at = (x) => params.map((p, i) => curve(p, x) + (use_noise ? p.sigma * z[i].e : 0))
y_here = y_at(pressure)
pred = {
  const ys = y_here
  return {
    mean: d3.mean(ys),
    lo: d3.quantile(ys, lo_q),
    hi: d3.quantile(ys, hi_q),
    p_below: d3.mean(ys, v => v < limit ? 1 : 0)
  }
}
band = d3.range(0.2, 3.21, 0.05).map(x => {
  const ys = y_at(x)
  return {x: x, mean: d3.mean(ys), lo: d3.quantile(ys, lo_q), hi: d3.quantile(ys, hi_q), p_below: d3.mean(ys, v => v < limit ? 1 : 0)}
})
band_both = ["200", "300"].flatMap(bf => {
  const ps = params_at(Number(bf))
  return d3.range(0.2, 3.21, 0.05).map(x => {
    const ys = ps.map(p => curve(p, x))
    return {bf, x, mean: d3.mean(ys), lo: d3.quantile(ys, lo_q), hi: d3.quantile(ys, hi_q)}
  })
})
x_keep = {
  const ok = band.filter(b => b.p_below >= prob)
  return ok.length ? d3.max(ok, b => b.x) : null
}
x_here = params.map((p, i) => {
  const f = (target - p.sigma * z[i].e - p.b) / p.a
  return f > 0 && f < 1 ? -Math.log(1 - f) / p.c : null
})
inv = {
  const ok = x_here.filter(v => v !== null)
  return {
    mean: ok.length ? d3.mean(ok) : null,
    lo: ok.length ? d3.quantile(ok, lo_q) : null,
    hi: ok.length ? d3.quantile(ok, hi_q) : null,
    reach: ok.length / x_here.length
  }
}
x_mark = q === 3 ? (inv.mean ?? 0) : q === 4 ? (x_keep ?? 0) : pressure
answer = {
  const f1 = d3.format(".1f"), f2 = d3.format(".2f")
  const whom = one === null ? `a new dialyzer at blood flow ${bf} ml/min` : `dialyzer ${one} (blood flow ${bf} ml/min)`
  const pct = d3.format(".0%")(ci)
  const kind = "measured rate"
  if (q === 1) return html`<b>${f1(pred.mean)} ml/hr</b> &nbsp; ${pct} interval ${f1(pred.lo)} to ${f1(pred.hi)}<br><small>${kind} at pressure ${pressure} dmHg, ${whom}</small>`
  if (q === 2) return html`<b>P(rate below ${limit} ml/hr) = ${f2(pred.p_below)}</b><br><small>${kind} at pressure ${pressure} dmHg, ${whom}</small>`
  if (q === 3) return inv.mean === null
    ? html`<b>rate ${target} ml/hr is not reachable</b><br><small>the measured rate never gets there for ${whom}</small>`
    : html`<b>pressure ${f2(inv.mean)} dmHg</b> &nbsp; ${pct} interval ${f2(inv.lo)} to ${f2(inv.hi)}<br><small>for a measured rate of ${target} ml/hr, ${whom}, reachable in ${d3.format(".0%")(inv.reach)} of draws</small>`
  if (q === 5) return html`<b>${f1(diff.mean)} ml/hr</b> &nbsp; ${pct} interval ${f1(diff.lo)} to ${f1(diff.hi)}<br><b>P(Δ rate above ${threshold} ml/hr) = ${f2(diff.p_above)}</b><br><small>measured rate at pressure ${pressure} dmHg at blood flow 300 minus at 200 ml/min, ${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} ml/hr) at ${prob}</b>`
    : html`<b>pressure ${f2(x_keep)} dmHg</b><br><small>highest pressure with P(rate below ${limit} ml/hr) ≥ ${prob}, ${whom}</small>`
}
Which dialyzer is unusual, and by how much
effects_card = html`<div class="pm-settings">
${viewof model}
${viewof par}
${viewof show}
${viewof sort_by}
${viewof pick}
${viewof ci2_in}
<div class="pm-answer" id="pm-answer-3"></div>
</div>`
show_answer_3 = {
  const slot = effects_card.querySelector("#pm-answer-3")
  if (slot) slot.replaceChildren(sentence)
  return sentence
}
Reading it
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 = \exp(\log\mu_c) \;\text{or}\; \exp\!\bigl(c_0 + c_{\text{bf}}\,\text{blood\_flow}_i\bigr)
\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>`
Posterior mean and interval per dialyzer
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 (ml/hr)") : par === "b" ? "b (ml/hr)" : "c (per dmHg)", 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 (ml/min)"},
  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)})`
      }})
  ]
})
Draws behind the dots: everyone pooled, the selected dialyzer in orange with its interval
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 === "b" ? "b (ml/hr)" : "c (per dmHg)"},
  y: {labelArrow: "none", labelAnchor: "top", label: "density", grid: true, ticks: 0},
  marks: [
    Plot.areaY(pooled_curve, {x: "x", y: "y", fill: "#4f8ef7", fillOpacity: 0.3}),
    Plot.line(pooled_curve, {x: "x", y: "y", stroke: "#8dc0ff", strokeWidth: 1.5}),
    ...(picked === null ? [] : [
      Plot.areaY(picked_curve, {x: "x", y: "y", fill: "#f5a623", fillOpacity: 0.6}),
      Plot.line(picked_curve, {x: "x", y: "y", stroke: "#f5a623", strokeWidth: 1.5}),
      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"})] : [])
  ]
})
pooled_curve = kde(pooled, () => false)
picked_curve = {
  // scaled to the pooled curve: one dialyzer holds 1/20 of the pooled draws
  if (picked === null) return []
  return kde(vals[picked], () => false).map(d => ({x: d.x, y: d.y / 20}))
}
viewof model = Inputs.radio(["1: no blood flow", "2: on c", "3: on b and c"], {value: "3: on b and c", label: "model"})
draws1 = FileAttachment("explore_draws_model1.csv").csv({typed: true})
draws2 = FileAttachment("explore_draws_model2.csv").csv({typed: true})
dr = model[0] === "1" ? draws1 : model[0] === "2" ? draws2 : draws
viewof par = Inputs.radio(["b", "c"], {value: "b", label: "parameter"})
viewof show = Inputs.radio(["deviation", "raw"], {value: "deviation", label: "show"})
viewof sort_by = Inputs.radio(["value", "dialyzer"], {value: "value", label: "sort by"})
viewof pick = Inputs.select(["none", ...d3.range(1, 21).map(String)], {value: "none", label: "dialyzer"})
viewof ci2_in = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "credible interval"})
ci2 = throttle(viewof ci2_in)
picked = pick === "none" ? null : Number(pick)
flow_of = Object.fromEntries(d3.range(1, 21).map(i => [i, obs.find(d => d.dialyzer === i).blood_flow]))
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}
}
per = d3.range(1, 21).map(i => ({
  dialyzer: i,
  blood_flow: flow_of[i],
  mean: d3.mean(vals[i]),
  lo: d3.quantile(vals[i], (1 - ci2) / 2),
  hi: d3.quantile(vals[i], 1 - (1 - ci2) / 2),
  p_low: ranks.low[i] / ranks.n,
  p_high: ranks.high[i] / ranks.n
}))
pooled = d3.range(1, 21).flatMap(i => vals[i])
order = (sort_by === "value" ? per.slice().sort((p, q) => p.mean - q.mean) : per.slice().sort((p, q) => p.dialyzer - q.dialyzer)).map(d => String(d.dialyzer))
fmt = show === "deviation" && par === "c" ? d3.format("+.0%") : show === "deviation" ? d3.format("+.2f") : d3.format(".3g")
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> &nbsp; P = ${f2(lo.p_low)}<br><b>Highest ${par}: dialyzer ${hi.dialyzer}</b> &nbsp; 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} ml/min)<br>${what}<br><small>${pct} interval ${fmt(d.lo)} to ${fmt(d.hi)}</small>`
}

Mohammad Savarmand