robust_permanent_income#
Series: lecture-python.myst
File:
lectures/robust_permanent_income.mdAudit date: 2026-08-26
Corpus snapshot:
e25fdf2345Categories audited: writing, math, code, figures, references, links, admonitions (JAX out of scope)
Overall score: 7.9 / 10
Priority: HIGH
Score breakdown#
Category |
Score |
One-line note |
|---|---|---|
Writing |
3.5/10 |
|
Math |
9/10 |
|
Code |
6.5/10 |
|
JAX |
out of scope |
JAX rules target |
Figures |
6/10 |
|
References |
10/10 |
no mechanical violations detected. |
Links |
10/10 |
no mechanical violations detected. |
Admonitions |
10/10 |
no mechanical violations detected. |
Issues#
Critical#
None found.
High severity#
[qe-code-002] — Use Unicode symbols for Greek letters in code. Count: 5. Lines: 674, 675, 676. Example: spelled-out
mu.[qe-fig-003] — No matplotlib embedded titles. Count: 5. Lines: 179, 193, 516, 832, 838. Example: .set_title.
[qe-math-009] (reviewer) — Choose simplicity in mathematical notation. Count: 6. Lines: 273, 300, 282, 598, 601, 696. Example: the calligraphic alphabet is stretched past the point where it helps, and four plain letters carry two meanings each. Decoration: \(\mathcal{D}(\Omega)\) (696, used 699, 704, 708, 719) is an ordinary matrix-valued function that the code writes as plain
D(741); \(\mathcal{M}^{c}_t\) (601, 605, 616) is a scalar marginal utility, not an operator; and \(\mathcal{S}_{t,\tau}\) is defined at 598 and never used again anywhere in the file. (\(\mathcal{R}_t\) and \(\mathcal{T}_t\) are the paper’s own operator names and are not free choices.) Collisions: (1) \(R\) is the state cost matrix in \(u = -i^\top Q i - x^\top R x\) (91, 94, 704, 708) and the gross physical return \(R \equiv \delta_k + \gamma\) (273, 276, 289, 468) - the code has to inventRf(367) alongsideR = np.eye(2)(766) to keep them apart, so the implementation disambiguates what the algebra does not. (2) \(v\) is the loading vector of the marginal-utility martingale, \(\theta^2 \equiv v^\top v\) (300, 303, 312), and the malevolent player’s distortion \(\{v_t\}\), \(\hat v_t^\top \hat v_t\) (209, 214, 222, 637, 650, 713) - both appear as squared norms, 20 lines apart in the same argument at 644-650. (3) \(U\) is continuation utility \(U_t\), \(U^e_t\) (99, 552) and the selector matrices \(U_d\), \(U_b\) in \(d_t = U_d z_t\) (282). (4) \(\mu\) is the conditional mean of continuation utility (126-141), the marginal utilities \(\mu_{c,t}\), \(\mu_{s,t}\) (294, 300) and the shock means \(\mu_b = 32\), \(\mu_d\) (426, 438); \(N\) is the LQ cross-product matrix (704, 708, 722) and the normal distribution \(N(\mu, s^2)\) (126, 853); and \(s\) is both the conditional standard deviation of \(U_{t+1}\) (126) and the service stream \(s_t\) (239, 250, 727).[qe-writing-001] — Use one sentence per paragraph. Count: 5. Lines: 126, 332, 339, 598, 693. Example: 2 sentences in one paragraph.
[qe-writing-002] (reviewer) — Keep writing clear, concise, and valuable. Count: 5. Lines: 141, 409, 868, 42, 994. Example: the prose is disciplined - one sentence per paragraph almost everywhere, and only five two-sentence blocks in 1000 lines - so the cost here is repetition, not verbosity. The \(\sigma/2\)-versus-\(\sigma/4\) point is made three times in seventy lines: 139-141 (“lies halfway between the original mean \(\mu\) and this worst-case mean”), the
{note}at 143-148 (“The worst-case mean … shifts by \(\frac{\sigma}{2}s^2\), while the operator value … shifts by \(\frac{\sigma}{4}s^2\)”) and again at 199-201 (“sits halfway between it and the reference mean \(\mu\)”). Worse, the exercise-1 solution (868-891) reproduces line for line the derivation the main text already gives in full at 126-133 - same moment generating function, same \(a = \sigma/2\), same three-step chain - so the note at 150 promising “A self-contained derivation … is requested in{ref}`hst_ex1`” asks the reader to redo what they have just read. §“Related lectures” (994-1002) restates 62 and 64. On sentence length, 409 is 51 words with a role reference and a display-math reference inside it, and 42 is a 40-word question.
Medium severity#
[qe-code-001] (reviewer) — Follow PEP8 unless closer to mathematical notation. Count: 4. Lines: 384, 745, 675, 929. Example: flake8 at PEP8’s own 79-character limit finds nothing but two items in the whole file, which is a genuinely good result: 384 assigns a lambda (
f = lambda b: b * Rf * ζ_hat(b, σ, θ2) - 1, E731 - adef f(b):is the PEP8 form, and here it would also let the root-finder’s bracket be documented) and 745 over-indents a continuation line (E127:+ β * Acl.T @ D @ Acl)sits four columns past the visual indent opened at 744). The E226 hits (183, 491, 501, 502) are tight multiplication inside lower-priority expressions, which PEP8 endorses and which keepsRf**(-np.arange(H))close to the algebra, so they are not findings. Two naming items a linter cannot see: 674-676 call the worst-case likelihood ratio \(m^u\)log_mu/muin a file that uses Unicodeμfor the conditional mean twelve lines earlier (162-168), so one file hasμmeaning \(\mu\) andmumeaning \(m^u\) - rename tom_u(this is also what makes the three qe-code-002 hits false positives, see the actions); and 929’sfor θ2 in [0.01, 0.02]rebinds the module-levelθ2 = 0.01set at 368, so any later cell that relies on the global sees 0.02.[qe-fig-001] — Do not set figure size unless necessary. Count: 3. Lines: 173, 509, 821. Example: figsize=.
[qe-writing-003] (reviewer) — Maintain logical flow. Count: 4. Lines: 426, 588, 552, 755. Example: (1) 426 says “recall from the discussion of
{eq}`eq:hst_budget`that the level of \(b_t\) does not affect the decision rules, only prices” - but that discussion (269-276) is three lines about \(R \equiv \delta_k + \gamma\) and says nothing whatever about \(b_t\); the fact being recalled was never stated. (2) 588 closes the distorted transition law with “with \(\hat A\) given by{eq}`eq:hst_D`-style risk corrections”, which is both a forward reference (eq:hst_Dis introduced at 697, a section later) and a non-definition: \(\hat A\) is used in{eq}`eq:hst_pricing_lom`and never given. (3) The asset-pricing section (536-687) leans on \(\Omega\) at 552, 585 and 590 - “\(\Omega\) is negative semidefinite”, \((I - \sigma C^\top \Omega C)^{-1}\) - when all the reader has been told about \(\Omega\) is the half-sentence at 233; the Riccati iteration that produces it arrives 150 lines later at 693-711, so the pricing argument runs on an object whose construction has not been given. (4) The lecture estimates the HST economy (405-459: a parameter table, \(\phi_1 = 0.998\), \(\lambda = 2.443\)) and then never solves it: the regulator section switches at 755-767 to a made-up \(2\times2\) system (A = [[0.9, 0], [0, 0.8]],Q = [[1.0]],R = np.eye(2),β = 0.95) with no bridge back, so the worst-case distortions plotted at 821-841 belong to a different economy from the one whose \(\sigma\)-\(\hat\beta\) locus was computed at 359-396.[qe-writing-005] (reviewer) — Use bold for definitions, italic for emphasis. Count: 4. Lines: 417, 418, 428, 1000. Example: bold-for-definitions is followed unusually well here - robust (44), observationally equivalent (51), market price of risk (52), Knightian uncertainty (58), risk-sensitivity operator (102), exponential tilting (139), two-player zero-sum game (207), habit persistence (259), risk-free rate (276), martingale (291), annuity value (465), shadow prices (544), twisting operator (562), distorted transition law (580), stochastic discount factor (605), likelihood ratio (631), risk adjustment (693) - and italic is reserved for emphasis throughout. The exceptions are four terms defined in italic: the data definitions at 417-418 (“* Consumption is measured as nondurables plus services.” / “* Investment is measured as durables plus gross private investment.”) are definitions, not emphasis; 428 introduces the persistent and transitory components in italic and the file then uses those two words as the component names eleven times (440, 459, 473, 497-498, 511-512, 526); and 1000 introduces the price of model uncertainty in italic. Also worth a look: 590 sets three consecutive spans in italic including the connective,
*pessimistically shifted conditional mean* *and* an *inflated conditional variance*, which reads as italicising a whole clause.[qe-writing-007] (reviewer) — Use visual elements to enhance understanding. Count: 3. Lines: 536, 663, 594. Example: the title is “Robust Permanent Income and Pricing” and the pricing half has no figure at all. §“Asset pricing and the market price of risk” (536-687) is 150 lines of algebra whose only output is a hand-formatted print table (678-682,
f"{'|v_hat|':>10}{'E[m^u]':>12}...") - and the picture that belongs there, the exact market price of risk \([\exp(\hat v^\top \hat v) - 1]^{1/2}\) against its linear approximation \(|\hat v|\), exists in the file but is hidden in the exercise-3 dropdown at 962-982. Second, the punchline at 657-659 (“a conditional-mean misspecification of \(x\%\) … raises the market price of risk by roughly \(x/100\)”) is exactly a one-line plot and is instead left as three printed rows forscalein[0.05, 0.10, 0.20](679). Third, the multi-period pricing operator \(\mathcal{S}_{t,\tau} = \mathcal{T}_t \cdots \mathcal{T}_{t+\tau-1}\) (594-627) has neither figure nor code, where a term structure of prices under \(\sigma = 0\) against \(\sigma < 0\) would show the two distortions of 590 (“a pessimistically shifted conditional mean and an inflated conditional variance”) accumulating with horizon.
Low severity#
[qe-fig-005] — Descriptive figure names for cross-referencing. Count: 1. Lines: 962. Example: code-cell figure without mystnb figure metadata.
Strengths#
The risk-sensitive solver is validated against an independent implementation rather than trusted: 770-771 calls
qe.LQ(Q, R, A, B, C=C, beta=β).stationary_values(), 774 solves the same problem withsolve_rslqat \(\sigma = 0\), and 776-778 prints both feedback rules andmax |difference|- the \(\sigma = 0\) limit predicted at 719 (\(\mathcal{D}(\Omega) = \Omega\), \(G = 0\)) is therefore checked, not asserted.The paper’s punchline identity is verified by Monte Carlo: 663-683 draws two million standard normals, forms the likelihood ratio \(m^u\) from its log (
log_mu = w @ v_hat - 0.5 * v_hat @ v_hat, 674) and prints \(\mathbb{E}[m^u]\), \(\mathrm{std}(m^u)\) and \(|\hat v|\) side by side for three distortion sizes, so 685-687 (“A 10% distortion delivers a market price of risk near 0.10”) is a measured statement.The Gaussian certainty equivalent is decomposed rather than just stated: 137-141 separates the worst-case mean shift \(\frac{\sigma}{2}s^2\) from the operator value \(\mu + \frac{\sigma}{4}s^2\) and identifies the difference as the relative-entropy penalty, and the right panel of the first figure (182-194) draws both - the tilted density dashed and the operator value as a dotted vertical line - so the halfway property is visible.
The
{note}at 143-151 pre-empts the exact confusion the two coefficients invite (“Both are correct; the smaller shift of \(\mathcal{R}_t\) reflects the entropy cost the malevolent player pays for the distortion”), which is the right admonition in the right place.The economics of observational equivalence is given in one sentence that names both offsetting forces (337): “Increasing the preference for robustness stimulates a precautionary motive for saving; lowering \(\beta\) makes saving less attractive; along a particular locus the two effects exactly cancel” - and the locus is then computed by root-finding \(\hat\beta R \hat\zeta(\hat\beta) = 1\) (380-388) rather than asserted.
The impulse-response cell does the permanent-income arithmetic in the open:
ar2_irf(484-492) builds the AR(2) responses from the estimated roots,dc_p = (1 - 1/Rf) * np.sum(disc * ψ_p)(501) is{eq}`eq:hst_pi_mpc`transcribed, and 504-507 prints consumption’s response as a percentage of impact for the persistent and transitory shocks - which is the contrast 524-526 claims.The equations that carry the argument are labelled and genuinely reused:
{eq}`eq:hst_R`at 130, 150, 693 and 859;{eq}`eq:hst_mpr`at 661, 685, 846 and 946;{eq}`eq:hst_Omega_scalar`at 355, 903 and 936;{eq}`eq:hst_R_gauss`at 150 and 853;{eq}`eq:hst_mu_rw`at 315 and 934.All five parameter estimates that the code later uses are traceable to the table at 448-457: the AR roots 0.998, 0.704, 0.813, 0.189 and the shock scales 0.108, 0.155 appear as literals at 497-498 with
# persistent endowment d*/# transitory endowment d_hatcomments naming which row each came from.The three exercises each attach to a specific labelled equation and add something the text does not: hst_ex1 to
{eq}`eq:hst_R_gauss`(853), hst_ex2 to the discriminant of{eq}`eq:hst_Omega_scalar`and the endpoint \(\underline\sigma\) (899-903), and hst_ex3 to{eq}`eq:hst_mpr`, where 985-989 draws the correct conclusion about where the linear approximation fails.All four main-text figure cells carry
mystnbcaption and name metadata (156-160, 360-364, 478-482, 803-807), which is why qe-fig-005 reports only the exercise-3 cell at 962.
Recommended actions#
Do not “fix” the three qe-code-002 hits at 674-676.
log_muandmuare the worst-case likelihood ratio \(m^{u}_{t+1,t}\) of{eq}`eq:hst_mu`(634), not the Greek letter \(\mu\) - the file already writes \(\mu\) as Unicodeμat 162-168 - so renaming them toμwould make the cell say the opposite of what it computes. Rename tom_u/log_m_uinstead, which also clears the checker.Fix the two broken pointers: 426 recalls a fact about the level of \(b_t\) from “the discussion of
{eq}`eq:hst_budget`” (269-276), which never mentions \(b_t\); and 588 defers \(\hat A\) to “{eq}`eq:hst_D`-style risk corrections” whereeq:hst_Dis defined at 697 and \(\hat A\) is never given at all.Move the risk-sensitive regulator (689-846) ahead of the asset-pricing section, or state \(\Omega\)’s Riccati fixed point where the pricing algebra first uses it (552, 585, 590). As written the pricing measure is built out of an object the reader has only seen named.
Say why the regulator uses a toy \(2\times2\) system (759-767) rather than the estimated economy of 448-457, or feed the estimates in. A reader who has just been given \(\phi_1 = 0.998\), \(\lambda = 2.443\) and \(\delta_h = 0.682\) will expect the worst-case distortion plotted at 821-841 to belong to that economy.
Move the exercise-3 figure (962-982) into §“Asset pricing”, where 657-659 makes the claim it verifies; the print table at 678-682 can stay as the numerical check. The asset-pricing half of “Robust Permanent Income and Pricing” currently has no figure.
Cut two of the three statements of the \(\sigma/2\)-versus-\(\sigma/4\) point (139-141, 143-148, 199-201), and either drop the exercise-1 solution (868-891) or remove the same derivation from the main text at 126-133 - at present the note at 150 asks for a derivation the reader has already been given in full.
Rename to break the four collisions, in this order: \(R\) (LQ cost matrix at 91 versus gross return at 273 - the code already splits them into
RandRf), \(v\) (martingale loading at 300 versus the distortion at 209, which sit together at 644-650), \(U\) (utility at 99 versus the selector matrices \(U_d\), \(U_b\) at 282), and \(\mu\) (conditional mean at 126, marginal utilities at 294-300, shock means at 426-438).Bold the four definitions currently in italic - Consumption and Investment (417-418), persistent / transitory (428), price of model uncertainty (1000) - and un-italicise the connective in the three-span clause at 590.
Add
{cite}references for the two author names cited only in prose: “as emphasized by Zeldes” (320) and “using the methods of Hansen and Sargent” (422). qe-ref-001 does not see either (the known author-name-in-prose gap), so the report alone will not prompt this.Figures: the five qe-fig-003 hits (179, 193, 516, 832, 838) are all panel labels on 1x2 grids, so they need moving into the caption or into
ax.annotate, not deleting -caption: Risk-sensitive operator and worst-case density(159) already names both panels, so 179 and 193 are pure duplication and can just go. The threefigsize=(11, 4)settings (173, 509, 821) are genuine 1x2 grids and defensible under “unless necessary”. Add caption/name metadata to the exercise-3 cell (962).Code: replace the assigned lambda at 384 with a
def, pull the continuation at 745 back to the visual indent, rename the exercise-2 loop variable at 929 so it stops clobbering the module-levelθ2(368), and givesolve_rslq(739-749) a non-convergence path - as written it exhaustsmax_iter=100_000silently and returns the last iterate.Drop or use \(\mathcal{S}_{t,\tau}\): it is defined at 598 and never appears again. While there, split 598 into two paragraphs (it is one of the five genuine qe-writing-001 hits, along with 126, 339 and 693).