unemployment_shocks#

  • Series: lecture-python.myst

  • File: lectures/unemployment_shocks.md

  • Audit date: 2026-08-26

  • Corpus snapshot: e25fdf2345

  • Categories audited: writing, math, code, figures, links, admonitions (JAX out of scope)

  • Overall score: 8.9 / 10

  • Priority: NONE

Score breakdown#

Category

Score

One-line note

Writing

6.5/10

qe-writing-005 ×3; qe-writing-003 ×3; qe-writing-007 ×3.

Math

9.5/10

qe-math-009 ×2.

Code

7.5/10

qe-code-001 ×4; qe-code-003 ×1.

JAX

out of scope

JAX rules target lecture-jax.

Figures

10/10

no mechanical violations detected.

References

N/A

no citations in this lecture.

Links

10/10

no mechanical violations detected.

Admonitions

10/10

no mechanical violations detected.

Issues#

Critical#

None found.

High severity#

None found.

Medium severity#

  • [qe-code-001] (reviewer) — Follow PEP8 unless closer to mathematical notation. Count: 4. Lines: 180, 148, 449, 201. Example: column alignment is the file’s dominant code-style problem and it accounts for almost every flake8 item. Over the code cells with --select=E1,E2,E5,E7,W2,W3,W6,F,C4 --max-line-length=79 there are 16 E221 (multiple spaces before an operator) and 8 E241 (multiple spaces after a comma) hits, all from lining = signs and dict values into columns: 180-185 and 188-192 in jump_model, 154 and 244 in the two density cells, 432-434 in linear_model, 460 in the elpd pair, and 500-501 in the az.compare dict. Lines 180, 181 and 182 additionally carry trailing whitespace (W291) hidden behind that padding. Second, normal_pdf is defined at 148-149 inside a figure cell and then reused a hundred lines later inside a different figure cell (243-244), so the second figure fails with a NameError for anyone who runs it alone - and the same cell pair rebinds η, quiet and jump (151-154 against 242-244) to different objects, the first from illustrative constants and the second from posterior medians. Third, three imports arrive mid-lecture: logsumexp at 449 and arviz/xarray at 486-487, four hundred lines after the import cell at 52-63. Fourth, 201 (83 characters) and 435 (82) exceed 79. Two smaller items worth folding in: 152’s trailing-underscore names p_, μJ_, σs_, σJ_ guard against a collision that does not exist (the model’s p is local to jump_model), and pointwise_loo reads the global u_annual at 454 while to_arviz takes u as a parameter at 489, for the same job.

  • [qe-code-003] — Package installation at lecture top. Count: 1. Lines: 44. Example: non-Anaconda import with no install cell: [‘xarray’].

  • [qe-math-009] (reviewer) — Choose simplicity in mathematical notation. Count: 2. Lines: 290, 102. Example: \(p\) carries two unrelated meanings in the same lecture, both inside maths. The parameter table at 123 defines “\(p\) | jump probability | how often a large shock arrives” and 111-112 uses it that way in the mixture; then 287-423 uses \(p\) as the generic density symbol about thirty times - \(p(u_i \mid u_{-i})\), \(p(u_i \mid \theta)\), \(p(\theta \mid u)\), \(p(u \mid \theta)\) - so a reader meeting \(p(u_i \mid \theta)\) at 290 can legitimately parse it as the jump probability multiplying something. The proof at 357-393 is the worst affected, being wall-to-wall \(p(\cdot\mid\cdot)\) in a model one of whose six parameters is \(p\). The code keeps them apart (p is local to jump_model at 182; the density never appears as a name) and the maths does not; \(f\) or \(\pi\) for the density, or renaming the jump probability \(\lambda\), costs one substitution. Second, {eq}`eq:shocks` at 102 renames the parameters of the equation it says it is keeping: 99 states “We keep the linear structure from {doc}`unemployment_linear`” and then writes \(u_{t+1} = \bar u + \rho(u_t - \bar u) + \eta_{t+1}\), where unemployment_linear.md:148 writes the identical equation as \(u_{t+1} = \bar u + \phi(u_t - \bar u) + \varepsilon_{t+1}\). Both the persistence parameter and the innovation change letter, and the code follows the maths - numpyro.sample("rho", ...) here (181, 433) against numpyro.sample("phi", ...) there (unemployment_linear:177) - so the two lectures’ print_summary() tables label the same quantity differently, in a pair explicitly presented as a lecture and its sequel.

  • [qe-writing-003] (reviewer) — Maintain logical flow. Count: 3. Lines: 339, 505, 538. Example: the lecture adopts an assumption it knows is false, promises to discuss the consequences, and then discusses something else. 331-332 says of {prf:ref}`assm-cond-ind` that “For our autoregressive data this assumption is false, since each \(u_i\) is dependent on earlier observations through the transition dynamics”, and 339 says “We discuss the consequences in the section on time-series structure below”. That section (510-539) raises a different objection - that leave-one-out does not respect the order of time, so the scoring model saw both neighbours of the held-out step - which is about the design of the cross-validation, not about the factorisation \(p(u\mid\theta) = \prod_j p(u_j\mid\theta)\) that {prf:ref}`prop-loo-identity` needs. The consequence the reader was promised - what the false factorisation does to the identity and hence to the numbers at 462-463 - is never given. Second, 505-506 says “Here az.compare confirms the hand calculation and ranks the jump model first (its table rounds the scores for display)”. az.compare defaults to PSIS-LOO, which is a deliberately different estimator from the raw harmonic mean derived at 397-409: the naive importance-sampling estimator is stabilised in ArviZ precisely because it is unreliable, and 411-413’s convergence argument covers only its mean (“The integrand has finite mean … so the law of large numbers applies”) and says nothing about its variance, which is the whole reason PSIS exists. So any gap between the two is not rounding, and the lecture’s own careful derivation deserves the caveat. Third, 538-539 asserts a result with no code, no number and no way to check it - “(We ran it without including it here and the conclusion holds: under leave-future-out the jump model still beats the linear one, by an even larger margin.)” - in a lecture that is otherwise scrupulous about printing what it claims.

  • [qe-writing-005] (reviewer) — Use bold for definitions, italic for emphasis. Count: 3. Lines: 32, 106, 137. Example: introduced terms are split between italic and bold with no discernible rule. Italic: model comparison (32) and Bayesian model comparison (263) - the same technique named twice, both italic - mixture of two normals (106), cross-validation (278). Bold: expected log predictive density (299), pointwise log-likelihood (415), leave-future-out (521), skew-normal (571). All eight are first uses of a term the lecture goes on to rely on, and four are set one way and four the other. Third weight: 137 names the lecture’s organising idea in double quotes and neither font - ‘Milton Friedman’s “plucking model”’ - which is the closest thing the lecture has to a definition of its own mechanism, and the term the paragraph at 127-135 has just spent five sentences describing. The two genuine emphases in the file are correctly italic (log at 295, both at 516), so the distinction is understood.

  • [qe-writing-007] (reviewer) — Use visual elements to enhance understanding. Count: 3. Lines: 127, 459, 510. Example: the sawtooth the model exists to produce is described in five paragraphs and never simulated. 127-137 is the mechanism - “a spike arrives in one step, while the recovery takes time”, Friedman’s plucking image, “a floor near full employment, from which bad shocks pluck the series upward” - and the obvious check, a path simulated from the fitted parameters shown beside {numref}`fig-annual-data`, is absent although m at 238-239 already holds every number it needs and the sibling lecture does exactly this kind of simulate-and-compare at unemployment_linear.md:255-278. Second, pointwise_loo returns a per-observation array (457) and the lecture immediately sums it away: 462-463 prints two scalars and 471-474 a difference and standard error. Plotting \(\ell_i^{\rm jump} - \ell_i^{\rm lin}\) against the year would show which years the jump model wins on - presumably the recessions - and would tie the model comparison back to the first figure. The array is computed; only the .sum() is kept. Third, 510-539 is the one section whose claim is not shown at all: leave-future-out is defined, its cost is discussed, its result is asserted in a parenthesis (538) and nothing is plotted, where a one-step-ahead score against year for both models is the natural close.

Low severity#

None found.

Strengths#

  • The lecture is built as a repair of a specific defect diagnosed in its predecessor and says so precisely: 24 recalls that “the model’s residuals were both heavy-tailed and right-skewed”, 26-28 says why a symmetric Gaussian cannot produce that, and 36-40 lists the three things this lecture will do about it - and it then does exactly those three, in that order.

  • The mixture is presented in three registers that agree with one another: the case-split display at 108-115, the six-row parameter table at 119-125 giving each symbol a name and a role, and the NumPyro model at 178-195 whose six numpyro.sample calls are those six parameters in the same order.

  • {prf:ref}`prop-loo-identity` is proved rather than quoted, and proved cleanly: 357-393 divides the two posteriors, substitutes the factorisation, integrates out the leave-one-out posterior and lands on \(p(u_{-i})/p(u)\) in five short steps - and 395 then says in one sentence why the result matters (“The leave-\(i\)-out posterior has vanished; only the full posterior remains”).

  • The derivation is carried all the way to the line of code that implements it: 399-409 gives the harmonic-mean approximation, 417-424 rewrites it on the log scale, and 426 names the expression the reader will meet - “which is the jnp.log(S) - logsumexp(-ll) we use below” - which is exactly what 457 contains.

  • The false assumption is flagged as false at the moment it is introduced (331-332) with two explicit reasons for adopting it anyway (336-337), rather than being smuggled in - and {prf:ref}`assm-cond-ind` is given a label so the proposition that depends on it can cite it.

  • The comparison is not left at ‘the jump model wins’: 468-477 computes the standard error of the elpd difference and reads it as an effect size (“a bit over two standard errors, so a real improvement”), and 479-506 then reproduces the same comparison through ArviZ, so the hand calculation has an independent check.

  • 268-278 states the methodological principle before the machinery, in four short paragraphs, and gets the reason right: “more complex models can always be tuned to better match the data they are fitting”, so the criterion has to be out-of-sample - which is the sentence that justifies the next sixty lines.

  • All three figures carry a caption and a name and all three are referenced by {numref} in the prose that follows (95, 165, and the fit figure discussed at 255), so qe-fig-001 through qe-fig-008 all measure zero - the only file in this batch with a clean Figures score.

  • 560-562 names the model that would beat this one (Markov switching) rather than implying the lecture’s own is the end of the line, and the exercise at 566-573 asks whether a single skewed shock would do the mixture’s job - a question whose answer is not obvious and which reuses az.compare from the body.