Skip to content

Commit bafd689

Browse files
Merge pull request #406 from jamiepratt/codex/bayesian-vocabulary-estimation-post
Add Bayes-to-Lexibench Civitas article series
2 parents 2fc911c + d33e93e commit bafd689

4 files changed

Lines changed: 1507 additions & 6 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
^{:kindly/hide-code true
2+
:kindly/options {:html/deps [:scittle :reagent]}
3+
:clay {:title "Bayes' Theorem, Revisited: Three Interactive Simulations"
4+
:quarto {:author :jamiep
5+
:description "Rebuilding three earlier Bayesian simulations with Kindly, Scittle, Reagent, and accessible SVG."
6+
:type :post
7+
:date "2026-07-13"
8+
:category :concepts
9+
:tags [:bayesian-statistics :clojure :clojurescript :scittle :simulation]
10+
:keywords [:bayes-theorem :grid-approximation :posterior-sampling :normal-distribution :data-visualisation]}}}
11+
12+
(ns language-learning.vocabulary-estimation.bayes-theorem-simulations
13+
(:require [scicloj.kindly.v4.kind :as kind]))
14+
15+
^:kindly/hide-code
16+
(kind/hiccup
17+
[:style
18+
"#title-block-header{padding-top:.75rem}#title-block-header h1{line-height:1.15;overflow-wrap:anywhere}mjx-container[display=true]{max-width:100%;overflow-x:auto;overflow-y:hidden}.bp-callout{border-left:4px solid #2780e3;background:#f2f7fc;color:#17202a;padding:1rem 1.15rem;margin:1.4rem 0;border-radius:.25rem}.bp-callout strong{display:block;margin-bottom:.3rem}.bp-simulator{margin:1.5rem 0}.bp-shell{border:1px solid #ced4da;border-radius:.65rem;padding:clamp(.8rem,3vw,1.3rem);min-width:0}.bp-shell h3{margin-top:0}.bp-shell h4{font-size:1rem}.bp-details{border:1px solid #dee2e6;border-radius:.45rem;margin:.85rem 0;background:var(--bs-body-bg,#fff)}.bp-details summary{cursor:pointer;font-weight:700;padding:.75rem 1rem}.bp-details>div{padding:0 1rem 1rem}.bp-controls{display:flex;align-items:end;gap:.65rem;flex-wrap:wrap;margin:1rem 0}.bp-controls label,.bp-field label{font-weight:700;font-size:.88rem}.bp-controls input[type=range]{width:min(15rem,100%);accent-color:#2780e3}.bp-button{border:1px solid #6c757d;border-radius:.35rem;padding:.55rem .85rem;font-weight:600;cursor:pointer;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.bp-button.bp-primary,.bp-button[aria-pressed=true]{border-color:#2780e3;background:#2780e3;color:#fff}.bp-button:disabled{opacity:.45;cursor:not-allowed}.bp-button:focus-visible,.bp-select:focus-visible,.bp-controls input:focus-visible{outline:3px solid color-mix(in srgb,#2780e3 45%,transparent);outline-offset:2px}.bp-select{border:1px solid #6c757d;border-radius:.35rem;padding:.5rem;background:var(--bs-body-bg,#fff);color:var(--bs-body-color,#212529)}.bp-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:1rem;margin:1rem 0}.bp-chart{min-width:0;border:1px solid #dee2e6;border-radius:.5rem;padding:.7rem;margin:0;background:var(--bs-body-bg,#fff)}.bp-chart h4{margin:.1rem 0 .2rem;font-size:.95rem;line-height:1.25}.bp-chart svg{display:block;width:100%;height:auto}.bp-caption,.bp-note{font-size:.86rem;color:var(--bs-secondary-color,#5c636a);margin:.4rem 0 0}.bp-stat{font-variant-numeric:tabular-nums;margin:.4rem 0}.bp-sample-sequence{font-family:var(--bs-font-monospace,monospace);font-size:.82rem;overflow-wrap:anywhere;max-height:6rem;overflow:auto;padding:.55rem;border-radius:.35rem;background:var(--bs-tertiary-bg,#f4f4f4)}.bp-axis{stroke:currentColor;stroke-opacity:.5}.bp-guide{stroke:currentColor;stroke-opacity:.11}.bp-line{fill:none;stroke:#2780e3;stroke-width:3;vector-effect:non-scaling-stroke}.bp-line-secondary{fill:none;stroke:#e69f00;stroke-width:2;stroke-dasharray:6 4;vector-effect:non-scaling-stroke}.bp-bar-water{fill:#2780e3}.bp-bar-land{fill:#c99052}.bp-points{fill:none;stroke:#2780e3;stroke-width:2;stroke-linecap:round;vector-effect:non-scaling-stroke}.bp-progress{width:100%;height:.55rem;accent-color:#2780e3}.bp-formula{font-family:var(--bs-font-monospace,monospace);overflow-wrap:anywhere}.bp-heat-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,17rem),1fr));gap:1rem}.bp-empty{display:grid;place-items:center;min-height:11rem;border:1px dashed #adb5bd;border-radius:.35rem;color:var(--bs-secondary-color,#5c636a);text-align:center;padding:1rem}.bp-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:575px){.bp-shell{padding:.75rem}.bp-controls{align-items:stretch}.bp-button{flex:1}.bp-chart{padding:.5rem}}"])
19+
20+
^:kindly/hide-code
21+
(kind/hiccup
22+
[:style
23+
".series-toc{min-width:0;border:1px solid #ced4da;border-radius:.6rem;padding:clamp(.85rem,3vw,1.2rem);margin:1.4rem 0;background:var(--bs-body-bg,#fff)}.series-toc h2{font-size:1.2rem;margin:0 0 .55rem}.series-toc p{margin:0 0 .7rem}.series-toc ol{margin:0;padding-left:1.45rem}.series-toc li{padding:.18rem 0}.series-status{display:inline-block;margin-left:.35rem;font-size:.7rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--bs-secondary-color,#5c636a)}.bp-change-details>summary{font-size:1.3rem;line-height:1.2}.bp-change-details ul{margin-bottom:0}"])
24+
25+
;; I thought I'd reproduce my previous learning about [Bayes' theorem
26+
;; simulations](https://jointprob.github.io/jointprob-shadow-cljs/#normal-distribution)
27+
;; ([source code](https://github.com/jointprob/jointprob-shadow-cljs/tree/master/src/cljs))
28+
;; as a Civitas article. I previously produced these simulations when I was
29+
;; working with [Daniel's JointProb
30+
;; group](https://scicloj.github.io/docs/community/groups/jointprob/) and used
31+
;; visualisation tools that were easily available at the time. I'm pretty
32+
;; confident that the current visualisation tools are going to do a lot better
33+
;; job with this simulation, judging by how wonderful a job they did with [my
34+
;; first Civitas article](beta_binomial_first_pass.html).
35+
36+
^:kindly/hide-code
37+
(kind/hiccup
38+
[:nav.series-toc {:aria-labelledby "series-contents-heading"}
39+
[:h2#series-contents-heading "Series contents"]
40+
[:p
41+
[:strong "Revisiting basic Bayes' theorem and applying it to a real word problem: estimating vocabulary size from "]
42+
[:a {:href "https://lexibench.com/"} "Lexibench.com"]
43+
[:strong " quiz responses."]]
44+
[:ol
45+
[:li [:a {:href "bayes_theorem_simulations.html"}
46+
"Bayes' Theorem, Revisited: Three Interactive Simulations"]
47+
[:span.series-status "published"]]
48+
[:li [:a {:href "beta_binomial_first_pass.html"}
49+
"Estimating Vocabulary Size with a Simple Bayesian Model"]
50+
[:span.series-status "published"]]
51+
[:li "Does Pair Frequency Predict Learner Responses?"
52+
[:span.series-status "planned"]]
53+
[:li "From Self-Reported CEFR to a Versioned Lemma–Form-Pair Pool"
54+
[:span.series-status "planned"]]
55+
[:li "From Correlated Form Pairs to Latent Lemma Knowledge"
56+
[:span.series-status "planned"]]
57+
[:li "Modelling Correct, Wrong, and Don't-Know Separately"
58+
[:span.series-status "planned"]]
59+
[:li "Calibrating Items Before IRT and Adaptive Selection"
60+
[:span.series-status "planned"]]
61+
[:li "When Contexts and Senses Become Identifiable"
62+
[:span.series-status "planned"]]]])
63+
64+
^:kindly/hide-code
65+
(kind/hiccup
66+
[:details.bp-details.bp-change-details
67+
[:summary "What I changed in this recreation"]
68+
[:div
69+
[:p "I kept the old application's complete three-part teaching sequence and its numerical grids, but rebuilt how it is delivered:"]
70+
[:ul
71+
[:li "The three routed Shadow-CLJS pages are now one executable Civitas article, so the globe update, posterior-sampling decision, and Gaussian update can be read as one argument."]
72+
[:li "The compiled Shadow-CLJS application is replaced by the same Kindly–Scittle–Reagent arrangement used in my first article. The mathematical core lives in this " [:code ".clj"] " article; browser state and controls live in one adjacent " [:code ".cljs"] " file."]
73+
[:li "Vega, Hanami, Semantic UI React, and MathJax React are removed. Kindly and Quarto render the prose and equations; semantic HTML supplies the controls; Reagent renders the visualisations directly as responsive SVG."]
74+
[:li "The original 201-point probability grid, four selectable priors, 200-item globe simulation, 2,000 animated posterior draws, 10,000-draw comparison, 41 × 41 Gaussian parameter grid, and complete adult-height dataset are preserved."]
75+
[:li "Previously ambient calls to " [:code "rand"] " are replaced by explicit seeds. Reset now replays the same globe data and posterior draws, making screenshots, explanations, and later regression checks reproducible."]
76+
[:li "The old long quotations from " [:em "Statistical Rethinking"] " are paraphrased while retaining their examples, section references, and attribution."]
77+
[:li "Every chart now has an SVG title and description; controls have associated labels and explicit keyboard behavior; status text is announced selectively; and the layout collapses without horizontal overflow on small screens."]
78+
[:li "The Gaussian heat maps use within-panel logarithmic opacity. This keeps low but non-zero plausibilities visible as the posterior concentrates. Their colour is therefore for comparing location and shape within a panel, not absolute density across panels."]
79+
[:li "I added direct " [:strong "Previous"] " and " [:strong "Next"] " controls to the height simulation and bounded its final update, avoiding the old end-of-data indexing edge case."]]
80+
[:p "The original project was a three-page Shadow-CLJS application using Vega, Hanami, Semantic UI React, and MathJax React. This version keeps the three lessons together in one executable Civitas article. Kindly renders the article, while Scittle and Reagent run the controls directly in the browser. The charts are responsive SVG with text alternatives, so there is no JavaScript build step and no charting wrapper between the data and the marks."]
81+
[:p "All simulated randomness below is seeded. Resetting a simulation replays the same sequence, which makes the explanations and any later checks repeatable."]]])
82+
83+
^:kindly/hide-code
84+
(kind/hiccup
85+
[:div.bp-callout
86+
[:strong "What is being recreated"]
87+
"The globe-toss grid approximation, sampling from its posterior to make a decision under absolute loss, and sequential Gaussian updating for adult heights. The long quotations in the old application are paraphrased here and still attributed to their source."])
88+
89+
;; ## 1. Updating a probability with globe tosses
90+
;;
91+
;; The first example follows the globe thought experiment in section 2.2 of
92+
;; Richard McElreath's *Statistical Rethinking*. Toss a small globe, catch it,
93+
;; and note whether the point beneath your index finger is water or land. The
94+
;; unknown parameter $p$ is the proportion of the globe covered by water.
95+
;;
96+
;; I approximate the continuous range of $p$ using 201 candidates:
97+
;;
98+
;; $$p \in \{0, 0.005, 0.010, \ldots, 0.995, 1\}.$$
99+
100+
(def probability-grid
101+
(mapv #(/ % 200.0) (range 201)))
102+
103+
(defn binomial-coefficient
104+
"Number of orderings containing k successes among n observations."
105+
[n k]
106+
{:pre [(<= 0 k n)]}
107+
(let [k (min k (- n k))]
108+
(reduce (fn [acc i]
109+
(* acc (/ (- (inc n) i) i)))
110+
1.0
111+
(range 1 (inc k)))))
112+
113+
(defn normalize-mean-one
114+
"Scale non-negative grid weights so their arithmetic mean is one."
115+
[weights]
116+
(let [mean-weight (/ (reduce + weights) (count weights))]
117+
(if (pos? mean-weight)
118+
(mapv #(/ % mean-weight) weights)
119+
(vec weights))))
120+
121+
(defn binomial-likelihood
122+
"Likelihood of water and land counts for one candidate p."
123+
[p water land]
124+
(* (binomial-coefficient (+ water land) water)
125+
(Math/pow p water)
126+
(Math/pow (- 1.0 p) land)))
127+
128+
(defn grid-posterior
129+
"Posterior grid weights for a prior and water/land observations."
130+
[prior water land]
131+
(->> probability-grid
132+
(mapv #(binomial-likelihood % water land))
133+
(mapv * prior)
134+
normalize-mean-one))
135+
136+
(def uniform-prior (vec (repeat 201 1.0)))
137+
138+
(def example-posterior
139+
(grid-posterior uniform-prior 6 3))
140+
141+
{:grid-points (count probability-grid)
142+
:example :six-water-three-land
143+
:posterior-mode (first (apply max-key second
144+
(map vector probability-grid example-posterior)))}
145+
146+
;; For $W$ water observations and $L$ land observations, the probability of one
147+
;; particular ordered sequence is
148+
;;
149+
;; $$p^W(1-p)^L.$$
150+
;;
151+
;; There are
152+
;;
153+
;; $$\binom{W+L}{W}=\frac{(W+L)!}{W!L!}$$
154+
;;
155+
;; orderings with the same counts, giving the binomial likelihood
156+
;;
157+
;; $$\Pr(W,L\mid p)=\binom{W+L}{W}p^W(1-p)^L.$$
158+
;;
159+
;; Bayes' theorem combines that likelihood with the prior:
160+
;;
161+
;; $$\Pr(p\mid W,L)=\frac{\Pr(W,L\mid p)\Pr(p)}{\Pr(W,L)},$$
162+
;;
163+
;; where the denominator is the average probability of the data across the
164+
;; prior. On a grid, normalising the products performs the same job.
165+
;;
166+
;; The simulator restores all four priors from the old application: uniform,
167+
;; step up, step down, and a symmetric ramp. Add water or land deliberately, or
168+
;; let a seeded process whose true water probability is 0.6 generate up to 200
169+
;; observations. The panels separate the previous posterior, the latest
170+
;; observation's likelihood, the full likelihood, and the new posterior.
171+
172+
^:kindly/hide-code
173+
(kind/hiccup
174+
[:div.bp-simulator
175+
[:div#globe-update-simulator
176+
[:p "Loading the globe-toss Bayesian update simulator…"]]
177+
[:noscript "This simulator needs JavaScript. The equations and executable Clojure above remain available without it."]])
178+
179+
;; The ordered-sequence likelihood and the likelihood of any ordering have the
180+
;; same shape as functions of $p$: the binomial coefficient is constant for
181+
;; fixed $W$ and $L$. Their raw vertical scales differ, but normalising either
182+
;; curve produces the same visual shape. Keeping both panels makes that fact
183+
;; visible rather than leaving it buried in the algebra.
184+
;;
185+
;; ## 2. Sampling from the posterior and choosing a decision
186+
;;
187+
;; Section 3.2 of *Statistical Rethinking* turns the globe problem into a bet.
188+
;; Choose a value $d$ for the proportion of water. A perfect answer earns $100$,
189+
;; but the payoff falls in proportion to the absolute error $|d-p|$. As in the
190+
;; previous application, I make that concrete as a loss of $1 for every 0.01 of
191+
;; error.
192+
;;
193+
;; The browser first generates a reproducible dataset of 100 globe observations
194+
;; with true $p=0.6$, then calculates its grid posterior. For every candidate
195+
;; decision $d$, the expected absolute loss is
196+
;;
197+
;; $$E[|d-p|\mid W,L] = \sum_p |d-p|\Pr(p\mid W,L).$$
198+
199+
(defn expected-absolute-loss
200+
"Expected absolute error at decision d for grid weights."
201+
[posterior d]
202+
(let [weight-total (reduce + posterior)]
203+
(/ (reduce + (map (fn [p weight]
204+
(* (Math/abs (- d p)) weight))
205+
probability-grid
206+
posterior))
207+
weight-total)))
208+
209+
(def example-losses
210+
(mapv #(expected-absolute-loss example-posterior %)
211+
probability-grid))
212+
213+
(def example-minimum-loss
214+
(apply min-key second (map vector probability-grid example-losses)))
215+
216+
{:example :six-water-three-land
217+
:minimum-loss-decision (first example-minimum-loss)
218+
:expected-absolute-loss (second example-minimum-loss)}
219+
220+
;; The direct calculation iterates over decisions from 0 to 1 in steps of
221+
;; 0.005. For each decision it multiplies the loss at every possible $p$ by that
222+
;; $p$'s posterior weight and averages over the curve. The lowest point of the
223+
;; resulting loss curve is the decision that minimises expected loss.
224+
;;
225+
;; We can reach the same answer by drawing possible values of $p$ from the
226+
;; posterior. Under absolute loss, the posterior median is the optimal
227+
;; decision. The interactive view animates up to 2,000 draws, then compares
228+
;; their trace, 200-bin counts, and standardised density with a fixed 10,000-draw
229+
;; simulation. Every visible summary uses the draws shown by that simulation.
230+
231+
^:kindly/hide-code
232+
(kind/hiccup
233+
[:div.bp-simulator
234+
[:div#posterior-sampling-simulator
235+
[:p "Loading the posterior sampling and decision simulator…"]]
236+
[:noscript "This simulator needs JavaScript. The expected-loss calculation above remains available without it."]])
237+
238+
;; ## 3. Updating a Gaussian model for adult heights
239+
;;
240+
;; The final simulation follows section 4.3 of *Statistical Rethinking*. A
241+
;; Gaussian distribution has two parameters: its mean $\mu$ and standard
242+
;; deviation $\sigma$. There are infinitely many possible pairs, so the
243+
;; original exercise considers a finite grid and ranks each candidate Gaussian
244+
;; by how compatible it is with the observed adult heights.
245+
;;
246+
;; For an observed height $h$, a candidate $(\mu,\sigma)$ has density
247+
;;
248+
;; $$f(h\mid\mu,\sigma)=
249+
;; \frac{1}{\sigma\sqrt{2\pi}}
250+
;; \exp\left(-\frac{(h-\mu)^2}{2\sigma^2}\right).$$
251+
252+
(defn normal-density
253+
"Gaussian probability density at x."
254+
[x mean sd]
255+
(/ (Math/exp (/ (* -0.5 (Math/pow (- x mean) 2.0))
256+
(Math/pow sd 2.0)))
257+
(* (Math/sqrt (* 2.0 Math/PI)) sd)))
258+
259+
(normal-density 151.765 155.0 8.0)
260+
261+
;; The grid matches the old simulation: 41 values of $\mu$ from 150 to 160 and
262+
;; 41 values of $\sigma$ from 7 to 9, or 1,681 candidate Gaussians. The prior is
263+
;; uniform over $\sigma$ and gives $\mu$ a Normal(178, 20) density. For each
264+
;; height, the display shows three heat maps:
265+
;;
266+
;; 1. the posterior before considering the next height;
267+
;; 2. the relative likelihood supplied by that height;
268+
;; 3. the posterior after multiplying and normalising.
269+
;;
270+
;; Move one height at a time or play through the complete dataset. A logarithmic
271+
;; colour scale keeps low but non-zero plausibilities visible after the
272+
;; posterior becomes concentrated.
273+
274+
^:kindly/hide-code
275+
(kind/hiccup
276+
[:div.bp-simulator
277+
[:div#gaussian-height-simulator
278+
[:p "Loading the sequential Gaussian updating simulator…"]]
279+
[:noscript "This simulator needs JavaScript. The Gaussian density and explanation above remain available without it."]
280+
[:script {:type "application/x-scittle"
281+
:src "bayes_theorem_simulations_interactive.cljs"}]])
282+
283+
;; ## What this recreation is for
284+
;;
285+
;; This is deliberately a record of my earlier learning. It does not
286+
;; silently turn these examples into the vocabulary estimator described in my
287+
;; first Civitas article; it exposes the probability tools that helped me get
288+
;; there.
289+
;;
290+
;; ## Sources
291+
;;
292+
;; - Jamie Pratt and the JointProb group, [original interactive simulations](https://jointprob.github.io/jointprob-shadow-cljs/#normal-distribution) and [ClojureScript source](https://github.com/jointprob/jointprob-shadow-cljs/tree/master/src/cljs).
293+
;; - Richard McElreath, *Statistical Rethinking*, examples from sections 2.2, 3.2, and 4.3. The prompts above are paraphrases.
294+
;; - [My first Civitas article: *Estimating Vocabulary Size with a Simple Bayesian Model*](beta_binomial_first_pass.html).
295+
296+
^:kindly/hide-code
297+
(do
298+
(assert (= 201 (count probability-grid)))
299+
(assert (< (Math/abs (- 84.0 (binomial-coefficient 9 6))) 1.0e-12))
300+
(assert (< (Math/abs (- 1.0
301+
(/ (reduce + example-posterior)
302+
(count example-posterior))))
303+
1.0e-12))
304+
(assert (= 0.645 (first example-minimum-loss)))
305+
(assert (pos? (normal-density 151.765 155.0 8.0)))
306+
:verified)

0 commit comments

Comments
 (0)