goad-toolkit 0.2.7__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511/.claude/settings.local.json +7 -0
  2. goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511/CHANGELOG.md +152 -0
  3. goad_toolkit-0.2.8/.remember/archive.md +3 -0
  4. goad_toolkit-0.2.8/.remember/logs/memory-2026-08-12.log +28 -0
  5. goad_toolkit-0.2.8/.remember/recent.md +7 -0
  6. goad_toolkit-0.2.8/.remember/tmp/capture-alive +1 -0
  7. goad_toolkit-0.2.8/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 +0 -0
  8. goad_toolkit-0.2.8/.remember/tmp/last-save-ts +1 -0
  9. goad_toolkit-0.2.8/.remember/tmp/post-tool-ran +0 -0
  10. goad_toolkit-0.2.8/.remember/tmp/save-session.pid +1 -0
  11. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/tmp/session-slug +1 -1
  12. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/MCP_SERVER.md +7 -7
  13. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/PKG-INFO +1 -1
  14. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/README.md +30 -0
  15. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/02-pipelines.md +31 -32
  16. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/03-plot-composition.md +43 -3
  17. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/04-five-families.md +40 -11
  18. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/05-distributions.md +52 -7
  19. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/06-models-and-residuals.md +44 -0
  20. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/08-api-reference.md +115 -5
  21. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/docs/09-analysis-method.md +52 -1
  22. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/goad_mcp.py +77 -12
  23. goad_toolkit-0.2.8/img/null-distribution.png +0 -0
  24. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/pyproject.toml +2 -1
  25. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/src/goad_toolkit/analytics.py +230 -34
  26. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/src/goad_toolkit/datatransforms.py +87 -1
  27. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/src/goad_toolkit/distributions.py +16 -0
  28. goad_toolkit-0.2.8/src/goad_toolkit/visualizer.py +1439 -0
  29. goad_toolkit-0.2.8/tests/test_datatransforms.py +167 -0
  30. goad_toolkit-0.2.8/tests/test_distributions.py +195 -0
  31. goad_toolkit-0.2.8/tests/test_nulldistribution.py +156 -0
  32. goad_toolkit-0.2.8/tests/test_visualizer.py +666 -0
  33. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/uv.lock +48 -1
  34. goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/visualizer.py +0 -545
  35. goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511/tests/test_distributions.py +0 -72
  36. goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511/tests/test_visualizer.py +0 -113
  37. goad_toolkit-0.2.7/.remember/tmp/capture-alive +0 -1
  38. goad_toolkit-0.2.7/.remember/tmp/last-save-ts +0 -1
  39. goad_toolkit-0.2.7/.remember/tmp/save-session.pid +0 -1
  40. goad_toolkit-0.2.7/CHANGELOG.md +0 -37
  41. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/settings.local.json +0 -0
  42. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/.gitignore +0 -0
  43. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/.python-version +0 -0
  44. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/MCP_SERVER.md +0 -0
  45. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/README.md +0 -0
  46. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/demo/linear.py +0 -0
  47. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/docs/01-goal-oriented-analysis.md +0 -0
  48. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/02-pipelines.md +0 -0
  49. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/03-plot-composition.md +0 -0
  50. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/04-five-families.md +0 -0
  51. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/05-distributions.md +0 -0
  52. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/06-models-and-residuals.md +0 -0
  53. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/docs/07-visual-critique.md +0 -0
  54. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/08-api-reference.md +0 -0
  55. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/docs/09-analysis-method.md +0 -0
  56. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/docs/10-teaching-path.md +0 -0
  57. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/docs/README.md +0 -0
  58. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/goad_mcp.py +0 -0
  59. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/img/distribution_fit.png +0 -0
  60. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/img/goaded.png +0 -0
  61. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/img/linear_results.png +0 -0
  62. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/img/null-distribution.png +0 -0
  63. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/img/residuals.png +0 -0
  64. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/img/zscores.png +0 -0
  65. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/pyproject.toml +0 -0
  66. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/__init__.py +0 -0
  67. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/analytics.py +0 -0
  68. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/cli.py +0 -0
  69. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/config.py +0 -0
  70. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/dataprocessor.py +0 -0
  71. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/datatransforms.py +0 -0
  72. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/distributions.py +0 -0
  73. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/filehandler.py +0 -0
  74. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/src/goad_toolkit/models.py +0 -0
  75. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/src/goad_toolkit/visualizer.py +0 -0
  76. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/tests/test_cli.py +0 -0
  77. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/tests/test_datatransforms.py +0 -0
  78. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/tests/test_distributions.py +0 -0
  79. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.claude/worktrees/quizzical-solomon-b62511/tests/test_filehandler.py +0 -0
  80. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/tests/test_nulldistribution.py +0 -0
  81. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/tests/test_visualizer.py +0 -0
  82. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8/.claude/worktrees/quizzical-solomon-b62511}/uv.lock +0 -0
  83. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.gitignore +0 -0
  84. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.python-version +0 -0
  85. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/.gitignore +0 -0
  86. /goad_toolkit-0.2.7/.remember/logs/autonomous/save-195516.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-104759.log +0 -0
  87. /goad_toolkit-0.2.7/.remember/logs/autonomous/save-195720.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-195516.log +0 -0
  88. /goad_toolkit-0.2.7/.remember/logs/autonomous/save-195933.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-195720.log +0 -0
  89. /goad_toolkit-0.2.7/.remember/logs/autonomous/save-200831.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-195933.log +0 -0
  90. /goad_toolkit-0.2.7/.remember/logs/autonomous/save-201034.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-200831.log +0 -0
  91. /goad_toolkit-0.2.7/.remember/logs/hook-errors.log → /goad_toolkit-0.2.8/.remember/logs/autonomous/save-201034.log +0 -0
  92. /goad_toolkit-0.2.7/.remember/tmp/capture-alive.d/90574e63-97cd-4c11-87e5-23b656c56fd9 → /goad_toolkit-0.2.8/.remember/logs/hook-errors.log +0 -0
  93. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/logs/memory-2026-08-10.log +0 -0
  94. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/now.md +0 -0
  95. /goad_toolkit-0.2.7/.remember/tmp/post-tool-ran → /goad_toolkit-0.2.8/.remember/tmp/capture-alive.d/16b4788a-5507-4216-98ec-06bac13a9aba +0 -0
  96. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/tmp/case-divergence +0 -0
  97. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/tmp/last-ndc.ts +0 -0
  98. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/tmp/last-save.json +0 -0
  99. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/.remember/tmp/now-day +0 -0
  100. /goad_toolkit-0.2.7/.remember/today-2026-08-10.md → /goad_toolkit-0.2.8/.remember/today-2026-08-10.done.md +0 -0
  101. {goad_toolkit-0.2.7/.claude/worktrees/quizzical-solomon-b62511 → goad_toolkit-0.2.8}/CHANGELOG.md +0 -0
  102. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/demo/linear.py +0 -0
  103. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/docs/01-goal-oriented-analysis.md +0 -0
  104. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/docs/07-visual-critique.md +0 -0
  105. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/docs/10-teaching-path.md +0 -0
  106. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/docs/README.md +0 -0
  107. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/img/distribution_fit.png +0 -0
  108. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/img/goaded.png +0 -0
  109. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/img/linear_results.png +0 -0
  110. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/img/residuals.png +0 -0
  111. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/img/zscores.png +0 -0
  112. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/__init__.py +0 -0
  113. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/cli.py +0 -0
  114. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/config.py +0 -0
  115. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/dataprocessor.py +0 -0
  116. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/filehandler.py +0 -0
  117. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/src/goad_toolkit/models.py +0 -0
  118. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/tests/test_cli.py +0 -0
  119. {goad_toolkit-0.2.7 → goad_toolkit-0.2.8}/tests/test_filehandler.py +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(cd /Users/rgrouls/code/courses/goad_toolkit && cat pyproject.toml)"
5
+ ]
6
+ }
7
+ }
@@ -0,0 +1,152 @@
1
+ # Changelog
2
+
3
+ ## 0.2.7
4
+
5
+ ### Added
6
+
7
+ - `ScatterPlot`, `RegPlot` and `CorrelationHeatmap`: the relation plots for two-variable
8
+ questions, in their order of use — a scatter first, a fit once the scatter has shown which
9
+ kind, a correlation matrix when there are too many pairs to plot by hand.
10
+ - `ScatterPlot` defaults `alpha` below 1, since overplotting is the standard way a scatter
11
+ misreports its own density.
12
+ - `RegPlot` wraps `sns.regplot`, which is axis-level and so can draw onto a layer's axis.
13
+ `scatter=False` lays a fit over an existing `ScatterPlot`; `lowess` together with
14
+ `order != 1` raises, since both answer "what shape" and only one can apply.
15
+ - `CorrelationHeatmap` pins the colour scale to `[-1, 1]` centred on 0 and defaults to a
16
+ diverging colormap, so the sign of a correlation stays readable. Non-numeric columns are
17
+ dropped, and a frame with nothing numeric left raises rather than drawing an empty grid.
18
+ - docs/04 §4.4 covers the three and their layering; docs/08 §8.10 documents the seaborn
19
+ axis-label override — a seaborn call with `x=`/`y=` writes the column names over
20
+ `PlotSettings`.
21
+
22
+ ## 0.2.6
23
+
24
+ ### Added
25
+
26
+ - `NullDistribution` and `NullResult`: shuffle one column, re-measure, and compare the
27
+ observed value against the cloud that produces. The statistic stays the caller's —
28
+ `NullDistribution` takes a callable and ships no ready-made statistics.
29
+ - `NullResult.p_value` counts rather than tests. Both one-sided counts include the observed
30
+ arrangement, so the smallest value it can return is `1 / (n_iter + 1)`.
31
+ - `NullPlot`: the null as a grey `HistogramPlot` with the observed value marked, its value
32
+ and p-value in the legend, stated as a bound (`p < …`) once the count floor is reached,
33
+ since an exact number there would overstate what a thousand shuffles support.
34
+ - `VerticalLine`, the numeric counterpart to `VerticalDate` and the layer `NullPlot`
35
+ composes.
36
+ - docs/06 §6.7 carries the prose, including the two things the shuffle test does not defend
37
+ against: the garden of forking paths, and a confounder that shuffling breaks along with
38
+ the association.
39
+
40
+ ## 0.2.5
41
+
42
+ ### Added
43
+
44
+ - `RegexFeature` transform: one new column derived from a regular expression over a text
45
+ column, in `count` (occurrences per row), `has` (bool) or `extract` (first capture group,
46
+ NaN where nothing matched) mode. The output column is named by `feature`, because
47
+ `Pipeline.add(name=...)` already claims `name` for the step.
48
+ - `mode="extract"` reports its match rate through `loguru`: it is the one mode that fails
49
+ silently, filling with NaN where a wrong pattern in the other two gives a visibly zero or
50
+ all-`False` column.
51
+ - docs/02 §2.5 documents the shipped transform with its mode table and coverage log; §2.4
52
+ keeps `IsWeekend` as the write-your-own example.
53
+
54
+ ## 0.2.4
55
+
56
+ ### Added
57
+
58
+ - `bernoulli`, `binomial`, `nbinom` and `beta` are default registry families. `pareto` is
59
+ deliberately not among them: lesson 4 has the student register it as the extensibility
60
+ exercise, and the class docstring shows the call.
61
+ - `QQPlot`: sample quantiles against a frozen distribution's theoretical quantiles with a
62
+ `y = x` reference line, for the tail mismatches a `PlotFits` histogram is too
63
+ bulk-dominated to show.
64
+ - `ECDFPlot`: one or two empirical CDFs, bin-free.
65
+ - `fit_table`: turns a `fit()` result list into a dataframe ranked by log-likelihood — one
66
+ row per family with params, log-likelihood, KS statistic and p-value, and which criteria
67
+ it won. `FailedFit` entries carry their message and sort last, since a missing likelihood
68
+ is not a small one.
69
+
70
+ ### Fixed
71
+
72
+ - Log-likelihood uses `logpmf` for discrete families and `logpdf` for continuous ones, so
73
+ every discrete family reports a real likelihood and the comparison between them means
74
+ something.
75
+ - The lower bound for a single-parameter discrete family is `1e-3`, low enough for a
76
+ probability (bernoulli's `p`) as well as a count-scale rate (poisson's lambda).
77
+ - `best_likelihood` and `best_ks` are marked independently: a KS p-value that never clears
78
+ the `p > 0` threshold — routine for a discrete family on a large, tied sample — leaves
79
+ the likelihood winner marked. An unknown `criterion` raises `ValueError` whether or not
80
+ a winner was found.
81
+
82
+ ## 0.2.3
83
+
84
+ ### Added
85
+
86
+ - `TimeFeatures` transform: derives `date`, `hour`, `day_name`, `isoweek` and `year_week`
87
+ from a timestamp column, with a `features=[...]` selector. The default is the three
88
+ lesson 1 relies on, and an unknown feature name raises `ValueError`.
89
+ - `DecomposePlot`: `seasonal_decompose` as the four-panel observed/trend/seasonal/residual
90
+ view, composing a `LinePlot` per panel.
91
+ - `ACFPlot`: autocorrelations as bars with a Bartlett confidence band. NaN input raises,
92
+ since `statsmodels.acf` returns an all-NaN result instead of raising.
93
+
94
+ ### Changed
95
+
96
+ - `statsmodels` is a dependency.
97
+
98
+ ## 0.2.2
99
+
100
+ ### Added
101
+
102
+ - Analysis-method rows on exploratory versus confirmatory questions and on mechanism: which
103
+ of the two a question is decides what would count as evidence, and a significant result
104
+ with no nameable mechanism points at a confounder rather than a discovery.
105
+ - Analysis-method rows on the unit of analysis — the unit a claim is about against the unit
106
+ of a row, and the number of independent units behind each group being compared — with
107
+ §9.3 naming pseudoreplication and showing the n=4000/n=5 bar chart.
108
+ - An analysis-method row on how many comparisons were considered, with §9.6 on the garden
109
+ of forking paths: the arithmetic of fifteen tries at a one-in-twenty threshold, and the
110
+ three defences (write the question down, split the data, report the count).
111
+ - A visual-critique row separating "A differs from B" from "A is the highest", since every
112
+ set of numbers has a maximum and a ranking claim needs the ranking to be stable.
113
+ - A counting step at the head of the distribution method: how many observations, and how
114
+ many independent ones.
115
+
116
+ These land in docs/09-analysis-method.md and in the matching `goad_mcp` checklists.
117
+
118
+ ## 0.2.1
119
+
120
+ ### Breaking
121
+
122
+ - **`FileConfig` gained `date_column` and `date_index`.** `FileHandler.load` reads csv and
123
+ parquet (reader picked from the suffix), parses `date_column` as dates and, when
124
+ `date_index` is true, moves it into the index. The defaults (`"date"`, `True`) keep the
125
+ covid pipeline working unchanged, but a `FileConfig` subclass that overrode `load` to
126
+ support another shape can now express it as configuration instead. A file whose suffix has
127
+ no reader, or a `date_column` that is not in the frame, raises `ValueError`. Parquet needs
128
+ `pyarrow`, available as the `parquet` extra.
129
+ - **`DistributionRegistry` is a normal class.** Constructing one no longer returns a shared
130
+ process-wide instance, so a registration affects only the registry it was made on. Code
131
+ that registered a family on one registry and relied on a separately constructed
132
+ `DistributionFitter()` picking it up must now pass it: `DistributionFitter(registry)`.
133
+
134
+ ### Added
135
+
136
+ - `goad` console script: fits every family in the registry to one column of a csv or parquet
137
+ file and prints the fits ranked by log-likelihood (`goad data/residuals.csv residual`).
138
+ - `DistributionFitter` accepts an optional `DistributionRegistry`.
139
+ - `filehandler.reader_for`, `filehandler.write_frame` and `filehandler.parse_dates` as
140
+ reusable pieces; `FileHandler.save` writes parquet as well as csv.
141
+ - A pytest suite under `tests/`.
142
+
143
+ ### Fixed
144
+
145
+ - `BarWithDates.build` and `VerticalDate.build` return `(fig, ax)` like every other `build`.
146
+ - Plots draw on `self.ax` / `self.fig` instead of module-level `plt` calls, so a layer placed
147
+ with `plot_on_axes` lands on the axis it was handed rather than on matplotlib's current one.
148
+ - The `goad` console script points at a module that exists.
149
+
150
+ ### Changed
151
+
152
+ - `__version__` is read from the installed package metadata.
@@ -0,0 +1,3 @@
1
+ # Archive
2
+
3
+ (empty)
@@ -0,0 +1,28 @@
1
+ 10:46:23 [hook] session-start: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
2
+ 10:46:23 [hook] run-consolidation: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
3
+ 10:46:24 [consolidation] start
4
+ 10:46:34 [consolidation] tokens: 9+6254cache→792out ($0.013050)
5
+ 10:46:34 [consolidation] done: 1 files consolidated
6
+ 10:46:36 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
7
+ 10:46:49 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
8
+ 10:46:54 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
9
+ 10:47:00 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
10
+ 10:47:04 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
11
+ 10:47:10 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
12
+ 10:47:15 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
13
+ 10:47:22 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
14
+ 10:47:27 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
15
+ 10:47:29 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
16
+ 10:47:39 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
17
+ 10:47:46 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
18
+ 10:47:50 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
19
+ 10:47:59 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
20
+ 10:47:59 [hook] save-session: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
21
+ 10:47:59 [extract] session 16b4788a-5507-4216-98ec-06bac13a9aba
22
+ 10:47:59 [extract] 17 exchanges (1 human)
23
+ 10:47:59 [extract] 1 human msgs < 3, skip
24
+ 10:48:45 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
25
+ 10:48:50 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
26
+ 10:48:54 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
27
+ 10:49:08 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
28
+ 10:49:13 [hook] post-tool: PROJECT_DIR=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical-solomon-b62511 PIPELINE_DIR=/Users/rgrouls/.claude/plugins/cache/claude-plugins-official/remember/0.19.0 PYTHON=python3 REMEMBER_DIR=/Users/rgrouls/code/courses/goad_toolkit/.remember
@@ -0,0 +1,7 @@
1
+ # Recent
2
+
3
+ ## 2026-08-10
4
+ Fixed 5 defects in goad_toolkit (feat/docs-and-mcp), shipped v0.2.0. Removed DistributionRegistry singleton, parameterized FileHandler.load for csv/parquet with date col support, fixed BasePlot.build return contracts. Refactored visualizer.py from module-level plt.* to instance methods (self.ax/.fig), added CLI.
5
+
6
+ ## Identity Candidates
7
+ - IDENTITY CANDIDATE: Architectural preference for instance methods over module-level state and singleton patterns
@@ -0,0 +1 @@
1
+ 16b4788a-5507-4216-98ec-06bac13a9aba
@@ -0,0 +1 @@
1
+ 1786524479
File without changes
@@ -0,0 +1 @@
1
+ 70230
@@ -4,4 +4,4 @@ project_dir=/Users/rgrouls/code/courses/goad_toolkit/.claude/worktrees/quizzical
4
4
  slug=-Users-rgrouls-code-courses-goad-toolkit--claude-worktrees-quizzical-solomon-b62511
5
5
  sessions_dir=/Users/rgrouls/.claude/projects/-Users-rgrouls-code-courses-goad-toolkit--claude-worktrees-quizzical-solomon-b62511
6
6
  memory_dir=/Users/rgrouls/code/courses/goad_toolkit/.remember
7
- session_id=90574e63-97cd-4c11-87e5-23b656c56fd9
7
+ session_id=16b4788a-5507-4216-98ec-06bac13a9aba
@@ -103,13 +103,13 @@ You pin a version with `GOAD_REF` — a git ref (branch, tag, or commit). That o
103
103
  selects both the script (in the URL) and the docs it fetches (inside the script), so a whole
104
104
  cohort runs exactly the same thing and the two cannot drift apart.
105
105
 
106
- > **Note:** the docs and this server land in **v0.2.0**. Until that tag exists, point
107
- > `GOAD_REF` at the branch (`feat/docs-and-mcp`) or work from a local clone (see below).
106
+ > **Note:** the docs and this server land in **v0.2.2**.
107
+ > You can also point `GOAD_REF` to a branch or work from a local clone (see below).
108
108
 
109
109
  ### Claude Code
110
110
 
111
111
  ```bash
112
- claude mcp add goad -e GOAD_REF=v0.2.0 -- \
112
+ claude mcp add goad -e GOAD_REF=v0.2.2 -- \
113
113
  sh -c 'uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py'
114
114
  ```
115
115
 
@@ -123,7 +123,7 @@ Add to `.cursor/mcp.json` (per-project) or `~/.cursor/mcp.json` (global):
123
123
  "goad": {
124
124
  "command": "sh",
125
125
  "args": ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"],
126
- "env": { "GOAD_REF": "v0.2.0" }
126
+ "env": { "GOAD_REF": "v0.2.2" }
127
127
  }
128
128
  }
129
129
  }
@@ -137,7 +137,7 @@ Add to `~/.codex/config.toml`:
137
137
  [mcp_servers.goad]
138
138
  command = "sh"
139
139
  args = ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"]
140
- env = { GOAD_REF = "v0.2.0" }
140
+ env = { GOAD_REF = "v0.2.2" }
141
141
  ```
142
142
 
143
143
  ### Claude Desktop
@@ -151,7 +151,7 @@ app:
151
151
  "goad": {
152
152
  "command": "sh",
153
153
  "args": ["-c", "uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py"],
154
- "env": { "GOAD_REF": "v0.2.0" }
154
+ "env": { "GOAD_REF": "v0.2.2" }
155
155
  }
156
156
  }
157
157
  }
@@ -187,7 +187,7 @@ add with the new tag:
187
187
 
188
188
  ```bash
189
189
  claude mcp remove goad -s user
190
- claude mcp add goad -s user -e GOAD_REF=v0.2.0 -- \
190
+ claude mcp add goad -s user -e GOAD_REF=v0.2.2 -- \
191
191
  sh -c 'uv run --no-project https://raw.githubusercontent.com/raoulg/goad_toolkit/$GOAD_REF/goad_mcp.py'
192
192
  ```
193
193
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: goad-toolkit
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: An extensible toolkit for Goal Oriented Analysis of Data
5
5
  Project-URL: Github, https://github.com/raoulg/goad_toolkit
6
6
  Author-email: raoul grouls <Raoul.Grouls@han.nl>
@@ -183,6 +183,36 @@ For the [kstest](https://docs.scipy.org/doc/scipy/reference/generated/scipy.stat
183
183
  The plots are sorted by log-likelihood, which means there is no good fit with a distribution in this case.
184
184
  ![residuals](img/distribution_fit.png)
185
185
 
186
+ ### 🎲 Is it real? The shuffle test
187
+
188
+ Shuffle the labels, measure again, and see where the real number lands. No test, no
189
+ assumptions, no table of critical values — just a picture of what your measurement does when
190
+ the label means nothing:
191
+
192
+ ```python
193
+ from goad_toolkit.analytics import NullDistribution
194
+ from goad_toolkit.visualizer import NullPlot, PlotSettings
195
+
196
+ def gap(frame):
197
+ means = frame.groupby("is_bot")["length"].mean()
198
+ return means[True] - means[False]
199
+
200
+ result = NullDistribution(gap, n_iter=2000, seed=1).run(data, label="is_bot")
201
+ settings = PlotSettings(
202
+ title="Mean message length: bots minus humans",
203
+ xlabel="difference in mean length (characters)",
204
+ ylabel="density under shuffled labels",
205
+ )
206
+ NullPlot(settings).plot(result=result)
207
+ ```
208
+
209
+ ![null distribution](img/null-distribution.png)
210
+
211
+ The grey cloud is the statistic under shuffled labels; the line is what the real data did.
212
+ Writing the statistic is your job — that is the claim. See
213
+ [Models and residuals](docs/06-models-and-residuals.md) §6.7 for what the p-value can and
214
+ cannot carry.
215
+
186
216
  ### 🧩 Extending with Custom Distributions
187
217
 
188
218
  You can easily register new distributions:
@@ -58,6 +58,8 @@ input frame is safe, and the steps stay cheap.
58
58
  | `SelectDataRange` | keep rows in a date range | `start_date`, `end_date` |
59
59
  | `RollingAvg` | rolling mean, drops the leading NaNs | `column`, `window`, `rename` |
60
60
  | `ZScaler` | standardise to mean 0, std 1 | `column`, `rename` |
61
+ | `TimeFeatures` | derive calendar columns from a timestamp | `column`, `features` |
62
+ | `RegexFeature` | count / flag / extract a pattern in a text column | `column`, `pattern`, `feature`, `mode` |
61
63
 
62
64
  `rename=True` writes to a new column (`deaths_shifted`, `deaths_zscore`, …) instead of
63
65
  overwriting. Prefer it. An overwritten column is a step you cannot debug, and the whole
@@ -75,20 +77,17 @@ Subclass `TransformBase` and implement `transform`. That is the entire contract.
75
77
  from goad_toolkit.datatransforms import TransformBase
76
78
  import pandas as pd
77
79
 
78
- class TimeFeatures(TransformBase):
79
- """Derive calendar features from a timestamp column."""
80
+ class IsWeekend(TransformBase):
81
+ """Flag Saturday/Sunday from a timestamp column."""
80
82
 
81
83
  def transform(self, data: pd.DataFrame, column: str) -> pd.DataFrame:
82
84
  ts = pd.to_datetime(data[column])
83
- data["date"] = ts.dt.date
84
- data["hour"] = ts.dt.hour
85
- data["day_name"] = ts.dt.day_name()
86
- data["isoweek"] = ts.dt.isocalendar().week
85
+ data["is_weekend"] = ts.dt.dayofweek >= 5
87
86
  return data
88
87
  ```
89
88
 
90
89
  ```python
91
- pipeline.add(TimeFeatures, column="timestamp")
90
+ pipeline.add(IsWeekend, column="timestamp")
92
91
  ```
93
92
 
94
93
  What the base class does for you:
@@ -106,39 +105,39 @@ Two rules for your `transform`:
106
105
  - **Name your parameters explicitly** in the signature. `def transform(self, data, column,
107
106
  window)` documents itself; `**kwargs` does not, and the validation cannot help you.
108
107
 
109
- ## 2.5 A transform for feature enrichment
108
+ ## 2.5 `RegexFeature`: feature enrichment from text
110
109
 
111
- The single most useful custom transform for text data, and a good template:
110
+ The single most useful transform for text data, and the reason `TransformBase` is worth
111
+ subclassing at all. It ships:
112
112
 
113
113
  ```python
114
- class RegexFeature(TransformBase):
115
- """Add a feature extracted from a text column with a regular expression."""
116
-
117
- def transform(
118
- self,
119
- data: pd.DataFrame,
120
- column: str,
121
- pattern: str,
122
- feature: str,
123
- mode: str = "count",
124
- ) -> pd.DataFrame:
125
- text = data[column].fillna("")
126
- if mode == "count":
127
- data[feature] = text.str.count(pattern)
128
- elif mode == "has":
129
- data[feature] = text.str.contains(pattern, regex=True)
130
- elif mode == "extract":
131
- data[feature] = text.str.extract(pattern, expand=False)
132
- else:
133
- raise ValueError(f"mode must be count/has/extract, got {mode!r}")
134
- return data
135
- ```
114
+ from goad_toolkit.datatransforms import RegexFeature
136
115
 
137
- ```python
138
116
  pipeline.add(RegexFeature, name="url_flag",
139
117
  column="message", pattern=r"https?://\S+", feature="has_url", mode="has")
140
118
  ```
141
119
 
120
+ Three modes, each writing one new column named by `feature`:
121
+
122
+ | `mode` | writes | use for |
123
+ |---|---|---|
124
+ | `"count"` | how many times the pattern occurs (int) | how many URLs, how many question marks |
125
+ | `"has"` | whether it occurs at all (bool) | flags you will group or filter on |
126
+ | `"extract"` | the first capture group, NaN where nothing matched | pulling a value *out* of the text |
127
+
128
+ `"extract"` needs exactly one capture group in `pattern`, and it is the mode worth being
129
+ careful with. `count` and `has` fail visibly when a pattern is wrong — a column of all zeros
130
+ or all `False` is hard to miss. Extraction fails *silently*, filling with NaN, so it reports
131
+ its own coverage through `loguru`:
132
+
133
+ ```
134
+ mentions: extracted 'addressed_to' from 92,415/627,172 rows (14.7%); 534,757 rows had no match
135
+ ```
136
+
137
+ That number is the point. A pattern that matches 15% of rows may be exactly right — on IRC,
138
+ most messages do not address anyone — or it may be silently broken. The log line makes you
139
+ decide which, instead of finding out four notebooks later.
140
+
142
141
  Note the two different `name`s: `Pipeline.add(name=...)` names the *step*, and
143
142
  `TransformBase.__init__` consumes it. So the new-column parameter has to be called something
144
143
  else — `feature` here. Any transform that wants to name an output column hits this.
@@ -148,12 +148,26 @@ step 3.
148
148
  | `LinePlot` | `sns.lineplot` | the primitive most compositions build on |
149
149
  | `ComparePlot` | two lines with labels | `LinePlot` twice via `plot_on` |
150
150
  | `VerticalDate` | a labelled vertical line at a date | a layer, not a plot |
151
+ | `VerticalLine` | the same at a number | a layer, not a plot |
151
152
  | `ComparePlotDate` | `ComparePlot` + `VerticalDate` | |
153
+ | `BarPlot` | `sns.barplot`, one bar per category | see [Five families §4.1](04-five-families.md) |
154
+ | `GroupedBarPlot` | bars split by `hue` | two categorical variables at once |
155
+ | `HeatmapPlot` | `sns.heatmap`, pivoting the frame if asked | when both variables have many levels |
156
+ | `BarbellPlot` | before and after per category, joined | the change is the message |
157
+ | `HighlightCategory` | recolours bars: grey, plus the one that matters | a layer, and it needs no help from what drew them |
158
+ | `ScatterPlot` | `sns.scatterplot` | the first plot of any relation question |
159
+ | `RegPlot` | `sns.regplot` — `fit_reg`, `lowess`, `order` | `scatter=False` layers it over a `ScatterPlot` |
160
+ | `CorrelationHeatmap` | `.corr()` as a heatmap, scale pinned to [-1, 1] | where to look next, not a ranking |
152
161
  | `BarWithDates` | bars with a month locator on the x axis | |
153
162
  | `ResidualPlot` | `BarWithDates` + `VerticalDate` | the residual-over-time view |
163
+ | `DecomposePlot` | `seasonal_decompose`, one panel each | observed/trend/seasonal/residual, overrides `plot` |
164
+ | `ACFPlot` | `acf` as bars with a confidence band | see [Five families §4.2](04-five-families.md) |
154
165
  | `HistogramPlot` | `sns.histplot`, `stat="density"` | density so a pdf can be overlaid |
155
166
  | `DistPlot` | a scipy distribution's pdf or pmf | estimates its own x-range from `ppf` |
156
167
  | `PlotFits` | histogram + fitted pdf, one panel per fit | see [Distributions](05-distributions.md) |
168
+ | `QQPlot` | sample quantiles vs. a distribution's theoretical quantiles | the tails, not the bulk — see [Distributions §5.5](05-distributions.md) |
169
+ | `ECDFPlot` | one or two empirical CDFs, bin-free | the right tool for comparing two samples |
170
+ | `NullPlot` | a shuffled statistic, with the observed value marked | `HistogramPlot` + `VerticalLine`, see [Models and residuals §6.7](06-models-and-residuals.md) |
157
171
 
158
172
  `HistogramPlot` defaults to `sqrt(n)` bins capped at 50. That default is a starting point,
159
173
  not an answer — bin count changes what a histogram appears to say, and choosing it is part of
@@ -164,6 +178,30 @@ falling back to `pmf` for discrete families. Left to itself it picks an x-range
164
178
  0.1st to the 99.9th percentile, which is right for most things and wrong for anything with a
165
179
  very heavy tail — pass `x_range` explicitly there.
166
180
 
181
+ `QQPlot` takes a sample and a *frozen* distribution and plots sorted data against the
182
+ distribution's quantiles at matching plotting positions, with a y=x reference line —
183
+ where `PlotFits`' histogram is dominated by the bulk of the data, this is where a tail
184
+ mismatch actually shows up.
185
+
186
+ `ECDFPlot` plots one sample, or two with `compare=`, as step functions of the empirical CDF —
187
+ no bin width to choose, so nothing about the shape is a plotting decision.
188
+
189
+ `HighlightCategory` is the clearest case for why layers beat settings. It recolours the bars
190
+ already on the axis — grey for everything, one colour for the categories you name — so it
191
+ works over `BarPlot`, over a bare `sns.barplot`, over pandas' `.plot.bar`, and over the
192
+ `BasePlot` subclass you wrote yourself. None of them need to know it exists:
193
+
194
+ ```python
195
+ settings = PlotSettings(title="Survival rate by deck", highlight=["D"])
196
+ bars = BarPlot(settings)
197
+ bars.plot(data=decks, x="deck", y="survival")
198
+ bars.plot_on(HighlightCategory(settings))
199
+ ```
200
+
201
+ `highlight`, `base_color` and `highlight_color` live on `PlotSettings` so that the call stays
202
+ about the data, but they are only defaults — pass `categories=` at the call to override.
203
+ Naming a category that is not on the axis raises, and lists the ones that are.
204
+
167
205
  ## 3.7 Writing your own plot
168
206
 
169
207
  Two questions decide the shape:
@@ -216,9 +254,11 @@ lines of code.
216
254
  `self.ax.axvline(...)`, `self.fig.suptitle(...)`. Keeping to them is what makes
217
255
  `plot_on_axes` reliable.
218
256
 
219
- `PlotFits` overrides `plot` rather than `build`, because it needs to know how many panels to
220
- create before the figure exists. Its `build` is a no-op. It is the exception, not a pattern to
221
- copy.
257
+ `PlotFits` and `DecomposePlot` override `plot` rather than `build`, because both need to know
258
+ how many panels to create before the figure exists — variable for `PlotFits` (one per fit),
259
+ fixed at four for `DecomposePlot`. Either way, `build` alone cannot decide the panel count
260
+ before `create_figure` runs, so both leave it as a no-op. This is the exception, not a pattern
261
+ to copy for a plot that only ever needs one axis.
222
262
 
223
263
  ---
224
264
 
@@ -42,9 +42,22 @@ barbell when each group has a before and an after and the *change* is the point.
42
42
  labelled adds nothing and costs attention. Grey, plus colour on the one bar you are talking
43
43
  about.
44
44
 
45
- **In GOAD:** the category counts come from a `Pipeline` (`groupby` in a transform), and the
46
- bars from a `BasePlot` subclass over `sns.barplot`. Ordering and the top-k cut belong in the
47
- pipeline, so they are recorded, not in the plot call, where they are invisible.
45
+ **In GOAD:** `BarPlot`, `GroupedBarPlot`, `HeatmapPlot` and `BarbellPlot`, with
46
+ `HighlightCategory` layered on top when one category is the point.
47
+
48
+ The category counts come from a `Pipeline` (`groupby` in a transform). Ordering and the top-k
49
+ cut belong in the pipeline too, so they are recorded, not in the plot call, where they are
50
+ invisible — which is why `BarPlot` draws the frame in the order it is handed and offers no
51
+ `sort` of its own.
52
+
53
+ The four are the same question at different sizes: one categorical variable is `BarPlot`, two
54
+ is `GroupedBarPlot` until the legend gets long, and past that it is `HeatmapPlot`.
55
+ `BarbellPlot` is the case where each group has a before and an after — two bar charts make the
56
+ reader do the subtraction, a barbell draws it.
57
+
58
+ Lesson 2 has you write a `BarPlot` of your own before any of this. That exercise is about the
59
+ `BasePlot` extension point, and it is not made pointless by the library shipping one, any more
60
+ than writing a sort is made pointless by `sorted` existing.
48
61
 
49
62
  ---
50
63
 
@@ -76,10 +89,12 @@ the period is long relative to your intuition.
76
89
  the same data support different stories. Choose the window from the mechanism you are
77
90
  studying, before you look.
78
91
 
79
- **In GOAD:** `ShiftValues`, `DiffValues`, `RollingAvg`, `SelectDataRange` are all pipeline
80
- steps, so the smoothing window is a named parameter you can change in one line and rerun.
81
- `ComparePlot` puts raw and smoothed on the same axes; `VerticalDate` marks the moment you
82
- claim something changed, with a label, on the plot.
92
+ **In GOAD:** `ShiftValues`, `DiffValues`, `RollingAvg`, `SelectDataRange`, `TimeFeatures` are
93
+ all pipeline steps, so the smoothing window is a named parameter you can change in one line
94
+ and rerun. `ComparePlot` puts raw and smoothed on the same axes; `VerticalDate` marks the
95
+ moment you claim something changed, with a label, on the plot. `DecomposePlot` wraps
96
+ `seasonal_decompose` into the four-panel observed/trend/seasonal/residual view; `ACFPlot`
97
+ wraps `acf` with its confidence band, for finding a period you did not already suspect.
83
98
 
84
99
  ---
85
100
 
@@ -114,7 +129,8 @@ fitted pdf drawn over the histogram to see where it agrees and where it does not
114
129
  sample sizes are near-unreadable. Normalise, or use ECDFs, or do a two-sample test.
115
130
 
116
131
  **In GOAD:** `HistogramPlot` (density-scaled, so a pdf overlays correctly), `DistPlot` for a
117
- parametric curve, and `DistributionFitter` + `PlotFits` for the full ranked comparison. See
132
+ parametric curve, `DistributionFitter` + `PlotFits` for the full ranked comparison, and
133
+ `QQPlot` / `ECDFPlot` for the tail diagnostics a histogram cannot show. See
118
134
  [Distributions](05-distributions.md).
119
135
 
120
136
  ---
@@ -146,9 +162,22 @@ to look.
146
162
  are not independent evidence. This is exactly why regularisation is the better tool for
147
163
  "which variables matter" — see [Models and residuals](06-models-and-residuals.md).
148
164
 
149
- **In GOAD:** the scatter and the fit are `BasePlot` subclasses; the interesting work is in
150
- [Models and residuals](06-models-and-residuals.md), because a relation you can state as a
151
- model is a relation whose failures you can inspect.
165
+ **In GOAD:** `ScatterPlot`, `RegPlot` and `CorrelationHeatmap`, in that order of use.
166
+
167
+ ```python
168
+ scatter = ScatterPlot(settings)
169
+ scatter.plot(data=data, x="messages", y="length", hue="is_bot")
170
+ scatter.plot_on(RegPlot(settings), data=data, x="messages", y="length", scatter=False)
171
+ ```
172
+
173
+ The layering is the point: the scatter is drawn first and the fit goes on top of it, so the
174
+ choice of `fit_reg`, `order` or `lowess` is made after you have seen the shape rather than
175
+ before. `CorrelationHeatmap` is the entry point when there are too many pairs to plot — it
176
+ tells you which scatter to make next, and nothing more than that.
177
+
178
+ The interesting work is still in [Models and residuals](06-models-and-residuals.md), because
179
+ a relation you can state as a model is a relation whose failures you can inspect — and
180
+ §6.7 is how you check that the relation is not something a shuffle would have produced.
152
181
 
153
182
  ---
154
183