|
| 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