Mohammad Savarmand
  • Projects
  • CV
Dialyzer ultrafiltration: a Bayesian look
  • 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) 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)
`
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"},
  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})
  ]
})
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", 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>`
edge = (x1, y1, x2, y2) => svg`<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#e4e6e8" stroke-width="2" marker-end="url(#arr)"/>`
Question
html`<div class="pm-questions">${viewof question}</div>`
Settings and answer
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>`
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
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"})
    ] : [])
  ]
})
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"},
  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: ""})
viewof pressure = Inputs.range([0.2, 3.2], {value: 1.5, step: 0.05, label: "pressure"})
viewof blood_flow = Inputs.radio([200, 300], {value: 300, label: "blood flow"})
viewof who = Inputs.select(["all dialyzers", ...d3.range(1, 21).map(String)], {value: "all dialyzers", label: "dialyzer"})
viewof ci = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "credible interval"})
viewof limit = Inputs.range([10, 80], {value: 50, step: 1, label: "rate limit"})
viewof target = Inputs.range([10, 80], {value: 45, step: 1, label: "target rate"})
viewof prob = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "probability"})
viewof threshold = Inputs.range([0, 30], {value: 10, step: 0.5, label: "Δ rate"})
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}` : `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> &nbsp; ${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> &nbsp; ${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> &nbsp; ${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>`
}
Which dialyzer is unusual, and by how much
html`<div class="pm-settings">
${viewof model}
${viewof par}
${viewof show}
${viewof sort_by}
${viewof pick}
${viewof ci2}
<div class="pm-answer">${sentence}</div>
</div>`
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 = 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>`
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") : 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)})`
      }})
  ]
})
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},
  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"})] : [])
  ]
})
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 = Inputs.range([0.5, 0.99], {value: 0.9, step: 0.01, label: "credible interval"})
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})<br>${what}<br><small>${pct} interval ${fmt(d.lo)} to ${fmt(d.hi)}</small><br>P(lowest) = ${f2(d.p_low)} &nbsp; P(highest) = ${f2(d.p_high)}`
}

Mohammad Savarmand