mccall_q#
Series: lecture-python.myst
File:
lectures/mccall_q.mdAudit date: 2026-08-26
Corpus snapshot:
e25fdf2345Categories audited: writing, math, code, figures, references, links (JAX out of scope)
Overall score: 7.8 / 10
Priority: HIGH
Score breakdown#
Category |
Score |
One-line note |
|---|---|---|
Writing |
4/10 |
|
Math |
9.5/10 |
|
Code |
7/10 |
|
JAX |
out of scope |
JAX rules target |
Figures |
7/10 |
|
References |
9/10 |
|
Links |
10/10 |
no mechanical violations detected. |
Admonitions |
N/A |
no admonitions, exercises or solutions. |
Issues#
Critical#
None found.
High severity#
[qe-code-001] (reviewer) — Follow PEP8 unless closer to mathematical notation. Count: 17. Lines: 133, 136, 186, 192, 193, 214, 494, 532, 549, 560, …. Example: binary operators and comparisons are written without surrounding spaces throughout, against the same file’s own practice elsewhere (
v_next - vat 190,c + β * (v @ q)at 174). flake8 confirms: E225 atflag=0(186),))<=eps(192),flag=1(193),params=[(494),accept==0(532),random()<=eps(560) and))<=δ(574); E231 (no space after a comma) at the fivefigsize=(10,6)calls (136, 225, 627, 648, 669) and atnp.zeros((len(w_new),2))(666); and unspaced additive operators atn+1(133, 644),qtable_new-qtable(574) andvalfunc-valfunc_VFI(602) - the tightβ*np.max(...)(534, 538, 540) andlr*TD(572) are not findings, since PEP8 endorses binding the higher-priority operator tighter inside a mixed expression. Three substantive items a spacing sweep would miss: 214’sfor i in range(n)is nested insidefor i in range(num_plots)(211) and clobbers the outer index - it works today only because the labelf"iterate {i}"is consumed at 212 before the inner loop runs; 549 unpacksc, β, w = self.c, self.β, self.winrun_one_epochand never uses any of the three (flake8 F841 x3); and the convergenceflagthatVFIcomputes and returns (186, 193, 197) is unpacked at 223 and 658 and never tested, so a run that hitsmax_iter=500without converging is indistinguishable from one that converged. Line 538 is also 92 characters.[qe-fig-001] — Do not set figure size unless necessary. Count: 5. Lines: 136, 225, 627, 648, 669. Example: figsize=.
[qe-fig-008] — Use lw=2 for line charts. Count: 7. Lines: 137, 212, 628, 629, 649, 670, 681. Example: plot() without lw=.
[qe-writing-002] (reviewer) — Keep writing clear, concise, and valuable. Count: 5. Lines: 42, 245, 264, 298, 699. Example: lines 42-51 spend five separate paragraphs establishing that what Q-learning learns is a Q-table, including “But it is something that is closely affiliated with it” (47) and “Sometimes a Q-function or Q-table is called a quality-function or quality-table” (51), before the definition arrives at 245. Four sentences are broken as written: “A quality function \(Q\) maps state-action pairs into optimal values. / They are tightly linked to optimal value functions” (245-247, singular subject, plural pronoun); “equals the maximum value of that a previously unemployed worker” (264); “This fact provides us with an / an alternative” (298-299, a doubled word across the line break); and “The above graphs indicates that” (699, subject-verb disagreement, and there is one graph, not several). Line 58 also ends without a full stop.
[qe-writing-008] — Remove excessive whitespace between words. Count: 79. Lines: 33, 36, 38, 44, 51, 56, 58, 64, 66, 89, …. Example: 2 spaces.
Medium severity#
[qe-fig-005] — Descriptive figure names for cross-referencing. Count: 4. Lines: 128, 221, 625, 639. Example: code-cell figure without mystnb figure metadata.
[qe-math-009] (reviewer) — Choose simplicity in mathematical notation. Count: 4. Lines: 258, 349, 385, 461. Example: the same Q estimate wears three different decorations:
\hat Q_tin the informal derivation (349, 350, 357, 358, 365, 368),\widetilde{Q}in the algorithm (385, 399, 406, 407, 421-440, 457, 725, 726) and\tilde{Q}in step 6 of the pseudo-code (461) - the last differing from the second only in macro, so the same symbol is typeset two widths apart on the page.\widetilde{TD}puts an accent over a two-letter operator name (406, 411, 457, 483, 725-726), and\textrm{argmax}/\textrm{argmin}(421, 427, 434, 440) hand-set what the series already provides as\argmax(lectures/_config.yml:110,"argmax" : ["\\operatorname*{argmax}", 0]). The state and action spaces are calligraphic, \(\mathcal{W}\) and \(\mathcal{A}\) (258, 260), where plain \(W\) and \(A\) are unused in the file and would do; the sibling lectureinventory_qwrites the same two sets as \(\mathsf X\) and \(\mathsf A\).[qe-writing-001] — Use one sentence per paragraph. Count: 1. Lines: 483. Example: 2 sentences in one paragraph.
[qe-writing-003] (reviewer) — Maintain logical flow. Count: 3. Lines: 537, 487, 483. Example: (1) the
quit_allowedflag means the opposite of its name, and as a result the lecture’s two headline figures are swapped. Intemp_diff, the branchif self.quit_allowed == 0takesβ*np.max(qtable[state_next, :])(538) - a max over both actions at the same state, i.e. the employed worker may switch to reject, which is quitting - whileelsetakesβ*qtable[state_next, 1](540), locking the worker into the accept column (489: “the second represents accepting the wage”), i.e. quitting forbidden. Match that against the math:{eq}`eq:old4`(405-409) carries the\max_{a'}and is the version the text says allows quitting;{eq}`eq:temp-diff`(723-728) replaces it with \(\widetilde{Q}^{old}(w,\text{accept})\) and is the version that forbids it. Soplot_epochs(...)at 696, which sits under ‘## Q-learning’ where quitting is allowed, takesplot_epochs’s defaultquit_allowed=1(663) and actually forbids it, whileplot_epochs(..., quit_allowed=0)at 742, under ‘## Employed worker can’t quit’, actually allows it. Each figure illustrates the other section. The two defaults disagree with each other as well: the class defaults toquit_allowed=0(509), which is why the earlier run at 607 is the right way round. (2) 487 tells the reader “we letsrepresent the state index between \(0\) and \(n=50\)”, but \(n\) is 10 at 129 and 30 at 640, and 50 appears nowhere in the lecture. (3) The Remark at 483 says \(\widetilde{TD}\) is “defined in{eq}`eq:old3`automatically above” - \(\widetilde{TD}\) is defined in{eq}`eq:old4`(409),eq:old3(400) is the update rule, and “automatically above” is a stray word.[qe-writing-004] — Avoid unnecessary capitalization in narrative text. Count: 1. Lines: 344. Example: mid-sentence ‘Law’.
[qe-writing-005] (reviewer) — Use bold for definitions, italic for emphasis. Count: 3. Lines: 62, 245, 345. Example: “on average” (345) is bold used for emphasis, which the rule reverses - italic is what that sentence wants. And the same term is bolded three times as if new each time: Q-function at 49, again at 62 alongside quality function, and quality function again at 245 opening its own section. There is no italic anywhere in the file - a grep for single-asterisk spans returns nothing - so bold is carrying both jobs, definitions and emphasis alike (statistical learning 42, value function 44, Q-table 49, temporal difference learning 64, gain 368, temporal difference errors 411, experimentation 417, deep Q-learning 751 are the definitional uses).
[qe-writing-007] (reviewer) — Use visual elements to enhance understanding. Count: 2. Lines: 615, 696. Example: the Q-table is the object the whole lecture is about - it is a \((n+1) \times 2\) matrix, described at 53-54 and 489 - and the only time the reader sees one it is
print(qtable)at 615, a wall of floats. A heatmap over (state, action) would show at a glance the thing the lecture claims at 701, that “the Q-learning algorithm has trouble learning the Q-table well for wages that are rarely drawn”. That claim also needs the wage distribution next to the value functions: the distribution is plotted at 648 and the value functions at 696, fifty lines apart and never overlaid, so the reader has to hold one figure in mind while reading the other.
Low severity#
[qe-ref-001] — Use correct citation style. Count: 1. Lines: 20. Example:
{cite}in narrative flow: ‘{cite}’.
Strengths#
The lecture earns its Q-learning derivation instead of asserting it: the exact Q-factor system
{eq}`eq:impliedq`(270-275), then the scalar reduction \(Q_r\) (295) exploiting IID wages, then the deliberately “illegitimate” step of erasing the integral (329-339), then the temporal-difference errors (355-360) and the adaptive rule (364-366) - and 329 tells the reader in advance that the step is not legitimate, which is more honest than most treatments.Every experiment is measured against a benchmark computed in the same file:
valfunc_VFIfrom value function iteration (223, 658) is carried through and plotted against the learned value function at 628-629 and again at 670, andcompute_error(601-602) puts the mean absolute deviation in the legend of every snapshot (682) - so “the quality of approximation … improves for longer epochs” (703) is read off the figure rather than argued.The Remark at 483 states the exact condition under which the algorithm can be expected to work - \(\widetilde{TD} = 0\) at an optimal Q-table, and convergence depends on visiting all state-action pairs often enough - rather than leaving convergence implicit.
The two-regime comparison is a genuinely good design: the same
plot_epochsfunction is run twice, once where an employed worker may quit and once where she may not (696, 742), against the same VFI benchmark - so the cost of losing the option to explore is visible rather than argued (see the judgment findings for the flag that inverts which is which).The state-space experiment at 637-660 re-runs the whole comparison with \(n = 30\) instead of \(n = 10\), which is what makes the “rarely drawn wages” point at 701 observable at all.
Randomness is threaded explicitly rather than left to global state: one
rng = np.random.default_rng(123)at 82 is passed down throughrun_epochs(585),run_one_epoch(544),temp_diff(525) anddraw_offer_index(517), so every draw in the lecture is reproducible from a single seed - and this is a fix relative to thenp.random.seed(123)still carried by thelecture-dpcopy of the same lecture.
Recommended actions#
Fix
quit_allowed(509, 537-540): the flag is inverted relative to its name, so the figure at 696 demonstrates the no-quitting regime under the quitting-allowed narrative and the figure at 742 does the reverse. Either invert the branch test or rename the parameter to something likeno_quit, then reconcile the two defaults -quit_allowed=0in the class (509) againstquit_allowed=1inplot_epochs(663) - and re-check both call sites.Fix the two factual slips: 487 says the state index runs to
$n=50$where the lecture uses \(n = 10\) (129) and \(n = 30\) (640), and 483 attributes the definition of \(\widetilde{TD}\) to{eq}`eq:old3`when it is{eq}`eq:old4`.Re-sync the
lecture-dpcopy of this lecture from this file. The two have diverged and this one is ahead: it sentence-cases the five H2 headings that still trip qe-writing-006 there (‘Review of McCall Model’, ‘Implied Quality Function \(Q\)’, ‘From Probabilities to Samples’, ‘Employed Worker Can’t Quit’, ‘Possible Extensions’), fixes four misspellings (‘illegitmate’, ‘previos’, ‘prematurally’, ‘algorthm’), removes four double spaces, repairs the ‘map/maps’ agreement at 245 and threadsrngexplicitly. Per the repo’s own note thatlecture-dpsyncs fromlecture-python.myst, one sync clears all of those in the other series.Clear the 79 double spaces (qe-writing-008 x79 - by far the largest single item in the file: ‘tells consequences’ at 36, ‘An equation that resembles a Bellman equation plays’ at 56, ‘let’s import’ at 66) and lowercase ‘Law of Large numbers’ at 344 (qe-writing-004), which is capitalised inconsistently even within itself.
Settle the notation on one symbol for the Q estimate:
\hat Q_t(349-368),\widetilde{Q}(385-726) and\tilde{Q}(461) are the same object; use the series\argmaxmacro (_config.yml:110) in place of\textrm{argmax}(421, 434) and\textrm{argmin}(427, 440); and consider plain \(W\), \(A\) for the state and action spaces (258, 260).Repair the delimiters at 287:
\left\{ Q(w, \text{accept} \right)pairs an opening brace with a closing parenthesis and the brace is then closed by a bare\}. It happens to render acceptably, which is why it has survived, but the auto-sizing is wrong and the source is one edit away from breaking.Figures: add
mystnbcaption/name metadata to the four figure cells (128, 221, 625, 639) - qe-fig-005’s four hits are all genuine - drop the five hand-setfigsize=(10,6)(136, 225, 627, 648, 669) and addlw=2to the seven line plots (137, 212, 628, 629, 649, 670, 681); then add a Q-table heatmap after 615 and overlay the wage distribution on the value-function comparison so the ‘rarely drawn wages’ claim at 701 is visible.Code: put spaces around the seven flake8 E225 operators (186, 192, 193, 494, 532, 560, 574) and after the six commas (136, 225, 627, 648, 666, 669); rename the inner loop index at 214 so it stops shadowing 211’s
i; delete the unused unpacking at 549; and either test theflagreturned byVFI(223, 658) or stop returning it.Clean up the loose ends: the two orphan labels
eq_mccallbellman(112) andeq:probtosample2(339) are never cited - and 339 is written$$(eq:probtosample2)with no space, unlike every other label in the file, which is likely why it never attached. Also settle the label naming (eq_mccallbellmanis the only one using an underscore rather than a colon), drop the stray+++cell marker at 314, convert line 20 to{cite:t}`Sutton_2018`since the author is the subject of the sentence (the single qe-ref-001 hit, genuine), split the two-sentence Remark at 483, and add the missing full stop at 58.