unemployment_shocks#
Series: lecture-python.myst
File:
lectures/unemployment_shocks.mdAudit date: 2026-08-26
Corpus snapshot:
e25fdf2345Categories 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 |
|
Math |
9.5/10 |
|
Code |
7.5/10 |
|
JAX |
out of scope |
JAX rules target |
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=79there 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 injump_model, 154 and 244 in the two density cells, 432-434 inlinear_model, 460 in the elpd pair, and 500-501 in theaz.comparedict. Lines 180, 181 and 182 additionally carry trailing whitespace (W291) hidden behind that padding. Second,normal_pdfis 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 aNameErrorfor anyone who runs it alone - and the same cell pair rebindsη,quietandjump(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:logsumexpat 449 andarviz/xarrayat 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 namesp_, μJ_, σs_, σJ_guard against a collision that does not exist (the model’spis local tojump_model), andpointwise_looreads the globalu_annualat 454 whileto_arviztakesuas 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 (
pis local tojump_modelat 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}\), whereunemployment_linear.md:148writes 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) againstnumpyro.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 “Hereaz.compareconfirms the hand calculation and ranks the jump model first (its table rounds the scores for display)”.az.comparedefaults 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 althoughmat 238-239 already holds every number it needs and the sibling lecture does exactly this kind of simulate-and-compare atunemployment_linear.md:255-278. Second,pointwise_looreturns 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.samplecalls 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
nameand all three are referenced by{numref}in the prose that follows (95, 165, and the fit figure discussed at 255), soqe-fig-001throughqe-fig-008all 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.comparefrom the body.
Recommended actions#
Deliver the discussion promised at 339, or point it somewhere it exists: the section at 510-539 addresses the ordering of time, not the failure of
{prf:ref}`assm-cond-ind`’s factorisation, and the factorisation is what{prf:ref}`prop-loo-identity`and every number at 462-477 rest on.Fix 505-506:
az.compareuses PSIS-LOO, a stabilised estimator, not the raw harmonic mean of 397-409, so it does not merely ‘confirm’ the hand calculation with rounding. Say what the difference is - and add the variance caveat to 411-413, since finite mean is not what makes the naive importance-sampling estimator usable.Simulate a path from the fitted model and put it beside
{numref}`fig-annual-data`. 127-137 spends five paragraphs describing a sawtooth and the plucking mechanism,mat 238-239 holds the medians, and nothing in the lecture shows the model producing the shape it was built to produce.Move
normal_pdf(148-149) out of the figure cell into its own cell, and move the three mid-lecture imports (449, 486-487) up to the import cell at 52-63; then stop the second density cell (242-244) from rebindingη,quietandjumpfrom the first.Add
xarrayto the install line at 47. The draftedqe-code-003hit is a true positive worth acting on rather than arguing with:xarrayis imported at 487 and is the only package in the file that the pip line does not name.Free up \(p\): it is the jump probability at 111-123 and the generic density from 287 to 423, and the proof at 357-393 is written entirely in the second meaning. Then reconcile
{eq}`eq:shocks`with its predecessor - \(\rho\) and \(\eta\) here against \(\phi\) and \(\varepsilon\) atunemployment_linear.md:148, and"rho"against"phi"in the twonumpyro.samplecalls - since 99 says the structure is being kept.Drop the column alignment in
jump_model(180-192),linear_model(432-434), the two density cells (154, 244) and theaz.comparedict (500-501): it is the sole cause of 16 E221 and 8 E241 items plus three lines of hidden trailing whitespace, and it will drift again on the next edit. Wrap 201 and 435.Plot the pointwise elpd difference by year -
pointwise_looalready returns it at 457 and 471 already formsdiff- so the reader can see which years the jump model wins, and settle the bold-versus-italic split among the eight introduced terms (32, 106, 137, 263, 278, 299, 415, 521).