tabdgp 0.1.0__tar.gz → 0.2.0__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 (41) hide show
  1. {tabdgp-0.1.0 → tabdgp-0.2.0}/.github/workflows/release.yml +10 -0
  2. {tabdgp-0.1.0 → tabdgp-0.2.0}/.gitignore +6 -1
  3. {tabdgp-0.1.0 → tabdgp-0.2.0}/CITATION.cff +3 -1
  4. {tabdgp-0.1.0 → tabdgp-0.2.0}/CONTEXT.md +59 -8
  5. {tabdgp-0.1.0 → tabdgp-0.2.0}/PKG-INFO +24 -18
  6. {tabdgp-0.1.0 → tabdgp-0.2.0}/README.md +23 -16
  7. {tabdgp-0.1.0 → tabdgp-0.2.0}/docs/adr/0001-one-dataset-per-draw-keyed-in-numpy.md +2 -2
  8. {tabdgp-0.1.0 → tabdgp-0.2.0}/docs/adr/0002-package-release-first-paper-conditional.md +1 -1
  9. tabdgp-0.2.0/docs/adr/0003-grouping-factors-as-units-in-the-graph.md +121 -0
  10. tabdgp-0.2.0/docs/adr/0004-the-sizes-are-the-envelope.md +79 -0
  11. {tabdgp-0.1.0 → tabdgp-0.2.0}/docs/conventions.md +17 -9
  12. {tabdgp-0.1.0 → tabdgp-0.2.0}/pyproject.toml +1 -2
  13. tabdgp-0.2.0/src/tabdgp/column.py +271 -0
  14. tabdgp-0.2.0/src/tabdgp/draw.py +107 -0
  15. tabdgp-0.2.0/src/tabdgp/graph.py +267 -0
  16. {tabdgp-0.1.0 → tabdgp-0.2.0}/src/tabdgp/hyperprior.py +141 -35
  17. tabdgp-0.2.0/src/tabdgp/mechanism.py +165 -0
  18. tabdgp-0.2.0/tests/fixtures/build.py +104 -0
  19. {tabdgp-0.1.0 → tabdgp-0.2.0}/tests/test_column.py +270 -5
  20. tabdgp-0.2.0/tests/test_draw.py +437 -0
  21. tabdgp-0.2.0/tests/test_graph.py +620 -0
  22. tabdgp-0.2.0/tests/test_hyperprior.py +454 -0
  23. tabdgp-0.2.0/tests/test_mechanism.py +440 -0
  24. {tabdgp-0.1.0 → tabdgp-0.2.0}/uv.lock +1 -1
  25. tabdgp-0.1.0/ROADMAP.md +0 -61
  26. tabdgp-0.1.0/src/tabdgp/column.py +0 -149
  27. tabdgp-0.1.0/src/tabdgp/draw.py +0 -80
  28. tabdgp-0.1.0/src/tabdgp/graph.py +0 -143
  29. tabdgp-0.1.0/src/tabdgp/mechanism.py +0 -97
  30. tabdgp-0.1.0/tests/fixtures/build.py +0 -48
  31. tabdgp-0.1.0/tests/test_draw.py +0 -219
  32. tabdgp-0.1.0/tests/test_graph.py +0 -248
  33. tabdgp-0.1.0/tests/test_hyperprior.py +0 -231
  34. tabdgp-0.1.0/tests/test_mechanism.py +0 -168
  35. {tabdgp-0.1.0 → tabdgp-0.2.0}/.github/workflows/ci.yml +0 -0
  36. {tabdgp-0.1.0 → tabdgp-0.2.0}/CONTRIBUTING.md +0 -0
  37. {tabdgp-0.1.0 → tabdgp-0.2.0}/LICENSE +0 -0
  38. {tabdgp-0.1.0 → tabdgp-0.2.0}/docs/adr/.gitkeep +0 -0
  39. {tabdgp-0.1.0 → tabdgp-0.2.0}/src/tabdgp/__init__.py +0 -0
  40. {tabdgp-0.1.0 → tabdgp-0.2.0}/tests/test_citation.py +0 -0
  41. {tabdgp-0.1.0 → tabdgp-0.2.0}/tests/test_readme.py +0 -0
@@ -15,6 +15,16 @@ jobs:
15
15
  steps:
16
16
  - uses: actions/checkout@v7
17
17
  - uses: astral-sh/setup-uv@v10.2.0 # setup-uv publishes no major tag
18
+ # PyPI never takes a version twice, so a tag that names another version stops here
19
+ - if: github.event_name == 'release'
20
+ env:
21
+ TAG: ${{ github.event.release.tag_name }}
22
+ run: |
23
+ expected="v$(uv version --short)"
24
+ if [ "$TAG" != "$expected" ]; then
25
+ echo "::error::the tag $TAG does not name the package version $expected"
26
+ exit 1
27
+ fi
18
28
  - run: uv build
19
29
  - uses: actions/upload-artifact@v7
20
30
  with:
@@ -9,10 +9,15 @@ dist/
9
9
  .ipynb_checkpoints/
10
10
  .DS_Store
11
11
  .claude/
12
- # local agent instructions (docs/conventions.md is the public part)
12
+ # local agent instructions (docs/conventions.md is the public part) and the roadmap
13
13
  CLAUDE.md
14
+ ROADMAP.md
15
+ next.md
14
16
  # design notes kept local: the lab notes and the prior-art survey
15
17
  lab/
16
18
  docs/prior-art.md
17
19
  docs/scamd-audit.md
18
20
  docs/related-simulators.md
21
+ docs/frontier-priors.md
22
+ # the model reports the survey read, kept local for their licences
23
+ docs/reports/
@@ -6,4 +6,6 @@ authors:
6
6
  given-names: Alex
7
7
  repository-code: https://github.com/adkipnis/tabdgp
8
8
  license: MIT
9
- version: 0.1.0
9
+ version: 0.2.0
10
+ date-released: 2026-10-14
11
+ doi: 10.5281/zenodo.23322845
@@ -29,6 +29,14 @@ of some factor (a school's size, a person's sex). A within-group covariate whose
29
29
  between groups is **group-confounded**.
30
30
  _Avoid_: level-1 and level-2 variable, cluster-level, contextual variable
31
31
 
32
+ **Group share**:
33
+ How much of a within-group node's variance comes from its group part, one value per group of
34
+ one grouping factor, rather than its row part, one value per row: the strength of its group
35
+ confounding. The between-group share measured on its values can be larger, since its row part
36
+ may read group-confounded parents.
37
+ _Avoid_: ICC unqualified (the fitter's, on the outcome), between-group share (what the
38
+ measurement pass records), random-intercept variance
39
+
32
40
  **Quirk**:
33
41
  A property of real tables that an independent and identically distributed (i.i.d.) simulator
34
42
  lacks and on which an amortized posterior goes wrong: group confounding, empty cells, rare
@@ -41,7 +49,30 @@ _Avoid_: edge case, noise, messiness
41
49
  One variable of a dataset's structural causal model (SCM): a grouping factor, a group-level or
42
50
  within-group covariate, time, treatment, or the outcome node. A node has a parent set and a
43
51
  mechanism; a node without parents is a root.
44
- _Avoid_: feature, variable unqualified, unit (a neuron of an MLP)
52
+ _Avoid_: feature, variable unqualified, neuron (of an MLP)
53
+
54
+ **Grouping factor**:
55
+ A root node whose values assign every row to one of its G groups, in metabeta's words "a
56
+ categorical variable whose levels index random effects"; a dataset has none (a flat table),
57
+ one or several. Two grouping factors are **crossed** when their groups combine freely and
58
+ **nested** when each group of the subordinate one lies within one group of the superior one,
59
+ which has fewer groups.
60
+ _Avoid_: factor alone (a categorical column), cluster, subject (one kind of grouping factor),
61
+ random-effects term
62
+
63
+ **Superior** / **subordinate**:
64
+ A grouping factor is superior to one nested in it, whose groups lie within its own; the nested
65
+ one is subordinate. The row is subordinate to every factor.
66
+ _Avoid_: coarser and finer, parent and child (the graph's edges), higher and lower level
67
+
68
+ **Unit**:
69
+ What a node is drawn once per: the row, or one grouping factor, in which case the node takes
70
+ one value per group of that factor, repeated across the group's rows (broadcast). A column at
71
+ the row unit is a within-group covariate; one at a grouping factor's unit is group-level. A
72
+ nested grouping factor is a unit subordinate to the one it nests in; crossed ones are units
73
+ without an order between them.
74
+ _Avoid_: level (a factor level, or the multilevel literature's level 1 and 2), hierarchy
75
+ level, grain, scope
45
76
 
46
77
  **Mechanism**:
47
78
  The function that computes a node's values from its parents' values and its own noise, drawn
@@ -64,6 +95,11 @@ ordered), levels, role (grouping factor, covariate, time, treatment, outcome), a
64
95
  parameters its values were drawn with.
65
96
  _Avoid_: feature, variable unqualified
66
97
 
98
+ **Factor level**:
99
+ One of the K categories of a binary, categorical or ordered column, or one group of a
100
+ grouping factor; R's `levels()`.
101
+ _Avoid_: level unqualified, category, class
102
+
67
103
  **Marginal**:
68
104
  The distribution a continuous column's values are mapped onto after the SCM, from a catalogue
69
105
  of families (skewed, heavy-tailed, bounded, zero-inflated), keeping their ranks.
@@ -95,13 +131,21 @@ _Avoid_: batch, table (the columns alone), sample, corpus (the consumer's set of
95
131
 
96
132
  **Hyperprior**:
97
133
  The one config of distributions from which every quantity of a dataset is drawn: sizes,
98
- graph, mechanisms, marginals, types, quirks. Its defaults are to be matched to mixedbench's
99
- registry and then widened; they are what the proofs certify.
134
+ graph, mechanisms, marginals, types, quirks, reached through a preset. The grouped preset's
135
+ defaults are to be matched to mixedbench's registry and then widened; they are what the
136
+ proofs certify.
100
137
  _Avoid_: prior (the fitter's, over parameters), config unqualified, meta-distribution
101
138
 
139
+ **Preset**:
140
+ A named hyperprior for one kind of table, grouped or flat today, item response, survival or
141
+ causal later: a choice of fields, never a new mechanism. Every draw starts from one; metabeta
142
+ draws from the grouped preset.
143
+ _Avoid_: dataset type, kind, mode
144
+
102
145
  **Override**:
103
- A point value or a distribution passed for one field of the hyperprior, replacing its default
104
- for that call. A standalone user passes none; metabeta passes its sizes.
146
+ A point value or a distribution passed for one field of a preset, replacing its default for
147
+ that call. Neither a standalone user nor metabeta passes one by default; an override serves a
148
+ study, an ablation or a size outside the preset.
105
149
  _Avoid_: setting, parameter, hyperparameter
106
150
 
107
151
  **Quirk axis**:
@@ -110,11 +154,18 @@ mixedbench's measurement pass shows that real tables have the quirk strongly eno
110
154
  _Avoid_: noise, perturbation, augmentation
111
155
 
112
156
  **Outcome node**:
113
- An optional node whose parents are drawn from the covariates, with a family, a link and terms
114
- a GLMM lacks, that builds white-box misspecified datasets. Just another node; never drawn
115
- unless asked for.
157
+ An optional node whose parents are drawn from the covariates, with a family, a link, the
158
+ random effects per grouping factor and terms a GLMM lacks, that builds white-box
159
+ misspecified datasets. Just another node; never drawn unless asked for.
116
160
  _Avoid_: y, target, label, response unqualified
117
161
 
162
+ **Random effect**:
163
+ A per-group parameter of the outcome, an intercept or a slope, drawn jointly per group of a
164
+ grouping factor in metabeta's parameterisation; metabeta's term. Only the outcome node draws
165
+ random effects; a group-level node, latent or observed, is covariate structure, which a GLMM
166
+ takes as given.
167
+ _Avoid_: group effect, random intercept for a latent group-level node
168
+
118
169
  ### The proofs
119
170
 
120
171
  **Two-sample test**:
@@ -1,10 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tabdgp
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A data-generating process for grouped and flat tables, to be matched to a registry of real tables
5
5
  Project-URL: Repository, https://github.com/adkipnis/tabdgp
6
6
  Project-URL: Issues, https://github.com/adkipnis/tabdgp/issues
7
- Project-URL: Roadmap, https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md
8
7
  Author-email: Alex Kipnis <adkipnis@users.noreply.github.com>
9
8
  License-Expression: MIT
10
9
  License-File: LICENSE
@@ -24,6 +23,7 @@ Description-Content-Type: text/markdown
24
23
  [![PyPI](https://img.shields.io/pypi/v/tabdgp)](https://pypi.org/project/tabdgp/)
25
24
  [![Python](https://img.shields.io/pypi/pyversions/tabdgp)](https://github.com/adkipnis/tabdgp/blob/main/pyproject.toml)
26
25
  [![Licence](https://img.shields.io/badge/licence-MIT-blue)](https://github.com/adkipnis/tabdgp/blob/main/LICENSE)
26
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23322845.svg)](https://doi.org/10.5281/zenodo.23322845)
27
27
 
28
28
  # TabDGP
29
29
 
@@ -33,12 +33,14 @@ TabDGP simulates tabular datasets from a random structural causal model (SCM) an
33
33
  reproduce the quirks of real data. Each table comes with the graph and the settings that
34
34
  generated it.
35
35
 
36
- > **Alpha.** Release 0.1 draws flat tables from defaults that are round guesses; the rest follows
37
- > the [roadmap](https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md). Before 1.0, a release
38
- > may change the dataset a key draws.
36
+ > **Alpha.** Release 0.2 draws grouped and flat tables from defaults that are round guesses;
37
+ > the rest is planned below. Before 1.0, a release may change the dataset a key draws.
39
38
 
40
39
  ## Highlights
41
40
 
41
+ - **Grouped or flat.** One to three grouping factors by default, nested or crossed, with 2 to
42
+ 400 groups each. A covariate varies by row or is constant within the groups of one factor.
43
+ `Hyperprior.flat()` draws none.
42
44
  - **White-box.** Each dataset carries its graph, latent nodes, column metadata and hyperparameters.
43
45
  - **Reproducible.** A dataset is a pure function of its key (a seed and an index) and the
44
46
  hyperprior, whatever batch or worker draws it.
@@ -55,33 +57,37 @@ pip install tabdgp
55
57
  ```python
56
58
  from tabdgp import Hyperprior, draw
57
59
 
58
- hyperprior = Hyperprior(rows=100, design_columns=5) # two overrides, the rest keep their defaults
60
+ hyperprior = Hyperprior.grouped(rows=100, design_columns=5) # two overrides on the grouped preset
59
61
  dataset = draw(seed=0, index=0, hyperprior=hyperprior) # the key: a seed and an index
62
+ unit = dict(zip(dataset.graph.names, dataset.graph.unit)) # per row, or per group of a factor
60
63
  for name, column in dataset.metadata.items():
61
- print(name, column.type, dataset.columns[name][:3].round(2))
64
+ print(name, column.type, unit[name], dataset.columns[name][:3].round(2))
62
65
  print(len(dataset.latent), 'latent nodes,', dataset.graph.parents.sum(), 'edges')
63
66
  ```
64
67
 
65
68
  ```text
66
- x0 binary [1 1 1]
67
- x1 binary [0 1 0]
68
- x2 categorical [0 0 0]
69
- x3 continuous [0.25 0.42 0.1 ]
70
- 7 latent nodes, 8 edges
69
+ g0 categorical g0 [4 5 2]
70
+ g1 categorical g1 [ 6 22 29]
71
+ g2 categorical g2 [7 7 5]
72
+ x0 binary g0 [1 1 1]
73
+ x1 binary row [1 1 1]
74
+ x2 continuous row [0.36 0.69 0.15]
75
+ x3 binary row [0 0 0]
76
+ x4 binary row [0 1 0]
77
+ 7 latent nodes, 16 edges
71
78
  ```
72
79
 
73
80
  ## Planned
74
81
 
75
- - **Grouping factors**, with group-level and within-group covariates, time and treatment.
76
82
  - **Quirk axes:** group confounding, rare levels, ties, bounds, near dependence and more.
77
- - **Matched defaults**, fitted to [mixedbench](https://github.com/adkipnis/mixedbench)'s real
78
- grouped tables, then widened.
79
- - **An outcome node**, optional, for white-box misspecified datasets.
83
+ - **Matched defaults**, fitted to and checked against
84
+ [mixedbench](https://github.com/adkipnis/mixedbench)'s real grouped tables, then widened.
85
+ - **An outcome node**, optional, for white-box misspecified datasets, with within-group time and
86
+ a binary treatment.
80
87
 
81
88
  ## Learn more
82
89
 
83
- [`CONTEXT.md`](https://github.com/adkipnis/tabdgp/blob/main/CONTEXT.md) defines the vocabulary,
84
- and [`ROADMAP.md`](https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md) gives the roadmap.
90
+ [`CONTEXT.md`](https://github.com/adkipnis/tabdgp/blob/main/CONTEXT.md) defines the vocabulary.
85
91
 
86
92
  ## Contributing
87
93
 
@@ -2,6 +2,7 @@
2
2
  [![PyPI](https://img.shields.io/pypi/v/tabdgp)](https://pypi.org/project/tabdgp/)
3
3
  [![Python](https://img.shields.io/pypi/pyversions/tabdgp)](https://github.com/adkipnis/tabdgp/blob/main/pyproject.toml)
4
4
  [![Licence](https://img.shields.io/badge/licence-MIT-blue)](https://github.com/adkipnis/tabdgp/blob/main/LICENSE)
5
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23322845.svg)](https://doi.org/10.5281/zenodo.23322845)
5
6
 
6
7
  # TabDGP
7
8
 
@@ -11,12 +12,14 @@ TabDGP simulates tabular datasets from a random structural causal model (SCM) an
11
12
  reproduce the quirks of real data. Each table comes with the graph and the settings that
12
13
  generated it.
13
14
 
14
- > **Alpha.** Release 0.1 draws flat tables from defaults that are round guesses; the rest follows
15
- > the [roadmap](https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md). Before 1.0, a release
16
- > may change the dataset a key draws.
15
+ > **Alpha.** Release 0.2 draws grouped and flat tables from defaults that are round guesses;
16
+ > the rest is planned below. Before 1.0, a release may change the dataset a key draws.
17
17
 
18
18
  ## Highlights
19
19
 
20
+ - **Grouped or flat.** One to three grouping factors by default, nested or crossed, with 2 to
21
+ 400 groups each. A covariate varies by row or is constant within the groups of one factor.
22
+ `Hyperprior.flat()` draws none.
20
23
  - **White-box.** Each dataset carries its graph, latent nodes, column metadata and hyperparameters.
21
24
  - **Reproducible.** A dataset is a pure function of its key (a seed and an index) and the
22
25
  hyperprior, whatever batch or worker draws it.
@@ -33,33 +36,37 @@ pip install tabdgp
33
36
  ```python
34
37
  from tabdgp import Hyperprior, draw
35
38
 
36
- hyperprior = Hyperprior(rows=100, design_columns=5) # two overrides, the rest keep their defaults
39
+ hyperprior = Hyperprior.grouped(rows=100, design_columns=5) # two overrides on the grouped preset
37
40
  dataset = draw(seed=0, index=0, hyperprior=hyperprior) # the key: a seed and an index
41
+ unit = dict(zip(dataset.graph.names, dataset.graph.unit)) # per row, or per group of a factor
38
42
  for name, column in dataset.metadata.items():
39
- print(name, column.type, dataset.columns[name][:3].round(2))
43
+ print(name, column.type, unit[name], dataset.columns[name][:3].round(2))
40
44
  print(len(dataset.latent), 'latent nodes,', dataset.graph.parents.sum(), 'edges')
41
45
  ```
42
46
 
43
47
  ```text
44
- x0 binary [1 1 1]
45
- x1 binary [0 1 0]
46
- x2 categorical [0 0 0]
47
- x3 continuous [0.25 0.42 0.1 ]
48
- 7 latent nodes, 8 edges
48
+ g0 categorical g0 [4 5 2]
49
+ g1 categorical g1 [ 6 22 29]
50
+ g2 categorical g2 [7 7 5]
51
+ x0 binary g0 [1 1 1]
52
+ x1 binary row [1 1 1]
53
+ x2 continuous row [0.36 0.69 0.15]
54
+ x3 binary row [0 0 0]
55
+ x4 binary row [0 1 0]
56
+ 7 latent nodes, 16 edges
49
57
  ```
50
58
 
51
59
  ## Planned
52
60
 
53
- - **Grouping factors**, with group-level and within-group covariates, time and treatment.
54
61
  - **Quirk axes:** group confounding, rare levels, ties, bounds, near dependence and more.
55
- - **Matched defaults**, fitted to [mixedbench](https://github.com/adkipnis/mixedbench)'s real
56
- grouped tables, then widened.
57
- - **An outcome node**, optional, for white-box misspecified datasets.
62
+ - **Matched defaults**, fitted to and checked against
63
+ [mixedbench](https://github.com/adkipnis/mixedbench)'s real grouped tables, then widened.
64
+ - **An outcome node**, optional, for white-box misspecified datasets, with within-group time and
65
+ a binary treatment.
58
66
 
59
67
  ## Learn more
60
68
 
61
- [`CONTEXT.md`](https://github.com/adkipnis/tabdgp/blob/main/CONTEXT.md) defines the vocabulary,
62
- and [`ROADMAP.md`](https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md) gives the roadmap.
69
+ [`CONTEXT.md`](https://github.com/adkipnis/tabdgp/blob/main/CONTEXT.md) defines the vocabulary.
63
70
 
64
71
  ## Contributing
65
72
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  status: accepted
3
3
  date: 2026-10-11
4
- amended: 2026-10-11, after the throughput lab note; 2026-10-12, after the column-pooling lab note (#16); 2026-10-12, numpy's version recorded beside TabDGP's
4
+ amended: 2026-10-11, after the throughput lab note; 2026-10-12, after the column-pooling lab note (#16); 2026-10-12, numpy's version recorded beside TabDGP's; 2026-10-14, ADR 0004: metabeta passes no sizes, `draw` takes a preset and has no default
5
5
  ---
6
6
 
7
7
  # 0001 — One dataset per draw, keyed by seed and index, in numpy
@@ -10,7 +10,7 @@ TabDGP's first consumer, `metabeta.mixed` in the private repository metabeta-v3,
10
10
  simulated datasets whose sizes, groups and columns all vary; its roadmap puts padding,
11
11
  masking, collate and the `.npz` corpus on `metabeta.core`, and its Simulator adds the outcome
12
12
  before anything is written. Grilled on
13
- 2026-10-11 (storage, batch layout, tensor library, item 1 of `ROADMAP.md`), the decision is:
13
+ 2026-10-11 (storage, batch layout, tensor library, roadmap item 1), the decision is:
14
14
  **the seed is the corpus.** Dataset `i` is a pure function of its key `(seed, index)`, the
15
15
  hyperprior with its overrides and the installed versions of TabDGP and numpy, whose generator
16
16
  promises no stable streams across its releases; the dataset records both. The public call
@@ -59,7 +59,7 @@ a finding the other papers do not carry.**
59
59
 
60
60
  ## Consequences
61
61
 
62
- - `ROADMAP.md` gains a "Release and paper" section: the release with JOSS, the quirk census
62
+ - The roadmap gains a "Release and paper" section: the release with JOSS, the quirk census
63
63
  ownership, the ablations, and the conditional paper.
64
64
  - The proofs' summary vector and bar are documented as consistency checks, never claimed as
65
65
  realism in the README or a paper.
@@ -0,0 +1,121 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-10-14
4
+ amended: 2026-10-14, #28: a column with more levels than its factor has groups stays at the row; #29: the role is spelled `grouping_factor`, a nested pair is superior and subordinate, not coarser and finer, and an override `group_share = 1` is the known exception to every level appearing
5
+ ---
6
+
7
+ # 0003 — Grouping factors are edgeless roots; every node has a unit; confounding is a two-part mechanism
8
+
9
+ Roadmap item 2 adds grouping to the flat SCM of item 1. Grilled on 2026-10-14 after the
10
+ prior-art search of 2026-10-12 (`docs/frontier-priors.md`, local), the decision is: **a
11
+ grouping factor is a root node without edges whose values assign rows to groups; every other
12
+ node carries a unit, the row or one grouping factor, and is drawn once per element of that
13
+ unit; a row-unit node's group confounding is an explicit share, `group_share`, that mixes a
14
+ group part and a row part.** The words are `CONTEXT.md`'s: grouping factor, unit, factor
15
+ level, group share, random effect.
16
+
17
+ The witnesses, from mixedbench 1.0.1's registry: 31 % of a documented model's covariates are
18
+ group-level; of 714 tables 56 % have one grouping factor, 20 % crossed, 16 % nested, 8 % both;
19
+ 38.8 % of factors are balanced, the rest fitted by a symmetric Dirichlet concentration α with
20
+ p5 0.97 and median 50. The sources of the ideas: PluRel's per-table SCM in topological order
21
+ (group-level nodes before within-group ones), RDB-PFN's parent latent units (latent group-level
22
+ nodes), and Dubey et al. 2026, who found that neither relational prior controls how much of a
23
+ child column its parent explains.
24
+
25
+ ## Decisions
26
+
27
+ - **The unit is a per-node field**, `unit`, `'row'` or a grouping factor's name, checked at
28
+ the graph's construction like the latent mask. The grouping factor itself is a root with
29
+ no edges, its codes its values, the size draw its "noise" (ADR 0004). A node at a grouping
30
+ factor's unit is drawn on that factor's G group rows: its parents' group values, one noise
31
+ per group, the mechanism, its z-score and, for a column, its marginal or level codes, all at
32
+ G rows, then broadcast to the n rows last. Latent values are returned broadcast, so every
33
+ array of a dataset has length n.
34
+ - **One unit per node.** Nested and crossed factors are both expressed: a node at the
35
+ subordinate factor of a chain is constant within the superior one too. What is not
36
+ expressed is a node drawn once per cell of two crossed factors, whose unit would be a set
37
+ of grouping factors; that is the extension, for item 6's item-response data, and nothing
38
+ here precludes it.
39
+ - **Edges across units**: a parent is at the child's unit or at a superior one in its nesting
40
+ chain; no edges between the units of crossed factors; no row-unit parent of a group-level
41
+ node. A mask on the Cauchy logits; the edge draw is otherwise item 1's. Node order: the
42
+ grouping factors, then the units superior first (crossed units in a fixed order), the row
43
+ unit last, latent before observed within a unit.
44
+ - **Which nodes are group-level**: a per-dataset `group_level_share`; each node, latent or
45
+ observed, draws a grouping factor's unit with that share (the factor uniform among the
46
+ dataset's), else the row. A column with more levels than its factor has groups stays at
47
+ the row: it would take one value per group and leave a level no row has, and no real
48
+ table shows a group-level column with K > G. The known exception is an override
49
+ `group_share = 1`: it makes a row-unit column constant within its group factor's groups, so
50
+ one with K > G misses a level. The default never draws 1, and the override asks for a
51
+ column constant within groups, which cannot show more levels than there are groups. A
52
+ within-group column with a latent group-level parent is confounded by nothing in the table.
53
+ - **Group confounding is a two-part mechanism.** A row-unit node is
54
+ `z(sqrt(s) · z(g) + sqrt(1 − s) · z(r))`: g its mechanism over the parents at one grouping
55
+ factor's unit (and the superior ones in that factor's chain), computed at that unit and
56
+ broadcast, pure group-level noise when it has no such parent; r its mechanism over every
57
+ other parent plus its own noise; s its `group_share`, a per-node field. The factor of the
58
+ group part is drawn uniformly per node. Every z-score runs at the unit of the node being
59
+ computed, so s is the node's between-group variance share over rows, the statistic the
60
+ measurement pass records, up to what its row-unit parents carry.
61
+ - **Relations**: each grouping factor after the first nests in the previous one with a
62
+ per-dataset `nested_share`, else is crossed with every earlier one; crossed assignments are
63
+ independent. Nested sizes are the size draw applied twice, bottom-up: the subordinate
64
+ factor's groups over the rows, then the subordinate factor's groups over the superior
65
+ factor's groups.
66
+ - **The grouping factor as a column**: int64 codes, type `categorical`, role
67
+ `grouping_factor`, `levels = G`, outside the design-column budget; the role carries its
68
+ checks (G ≥ 2, no marginal, the size draw's fields as its parameters). The binary /
69
+ categorical split is about design-matrix coding and applies to covariates only.
70
+ - **Random effects stay the outcome node's.** A group-level node is covariate structure, which
71
+ a GLMM takes as given; the outcome node (item 5) draws its random effects per grouping
72
+ factor in metabeta's parameterisation, so that metabeta's model is a special case. Item 5
73
+ also decides whether a latent group-level node may be a parent of the outcome, an
74
+ endogeneity that must be a switch.
75
+ - **Time and treatment are deferred** to the item that reads them: a treatment is a binary
76
+ column with a label nobody in this item reads, and the questions the label raises
77
+ (positivity, randomised or confounded) are causal-design questions; time's reader is the
78
+ outcome's random slope.
79
+
80
+ ## Considered options
81
+
82
+ - **Where the unit lives.** (a) A per-node field, chosen. (b) An edge from the grouping factor
83
+ to each node at its unit: duplicates the field, and the mechanism must skip a nominal
84
+ parent. (c) No field, group-level-ness emergent from a lookup mechanism (one random value
85
+ per factor level) with zero row noise, the random-effects view: cannot promise a constant
86
+ column, the column module needs the unit anyway, and the group-level share has no knob.
87
+ - **Confounding strength.** (a) Emergent through group-level parents: no knob, and item 9's
88
+ ablation could not switch it off. (b) A per-node random intercept added with share s:
89
+ parents' confounding stays uncontrolled, so s is not the node's share. (c) The two-part
90
+ mechanism, chosen: `group_share = 0` as an override switches confounding off for the whole
91
+ dataset, by induction over the graph.
92
+ - **Group part per factor** (a simplex of shares per node) over one factor per node: finer,
93
+ two fields to fit; item 4 may promote it if the pass shows many columns confounded with
94
+ respect to two factors.
95
+ - **A count of group-level nodes** over a share: the share is the registry's statistic and
96
+ works whatever the node count.
97
+ - **Dependence between crossed assignments** (RDB-PFN's affinity, a Dirichlet tilt per group
98
+ of one factor over the other's): a quirk-2 axis without a registry statistic; waits for the
99
+ measurement pass.
100
+ - **Where a group-level node is z-scored**, G rows or n rows: best practice (Enders and
101
+ Tofighi 2007 against `scale()` on the long table) is split and binds the fitter, not the
102
+ generator, since the column's scale is its marginal's. The rule "at the unit of the node
103
+ being computed" settles it and makes s the row-level share.
104
+
105
+ ## Consequences
106
+
107
+ - `graph` gains the per-node `unit` and `group_share`, the mask and the order; `mechanism`
108
+ the arithmetic at a unit, the broadcast and the two parts; `column` the role, the codes of
109
+ a grouping factor and the marginal at G values; `hyperprior` the fields and the size draw
110
+ (ADR 0004). Every dataset a key draws changes: a version change.
111
+ - Group confounding is the first quirk axis granted before the measurement pass, since the
112
+ registry already witnesses it (31 % group-level columns); its default is a round guess,
113
+ `Beta(0.5, 2)`, fitted in item 4.
114
+ - mixedbench#71 is asked for three statistics: per within-group column × grouping factor the
115
+ adjusted ICC(1) (the total group share) and the R² on the factor's observed group-level
116
+ columns (the observed share, after Dubey et al.'s parent gain), whose gap is the latent
117
+ share; per pair of crossed factors the empty-cell share.
118
+ - Extension points, none built: a latent group-level root "log group size" that group-level
119
+ nodes may take as a parent (quirk 8, allowed by the edge rule); a set-valued unit for cell
120
+ effects; per-factor group shares; the crossed-assignment dependence.
121
+ - `mechanism.py`'s "a bias per unit" becomes "per neuron", since unit now has a meaning.
@@ -0,0 +1,79 @@
1
+ ---
2
+ status: accepted
3
+ date: 2026-10-14
4
+ amended: 2026-10-14, #24: a superior factor has fewer groups than the subordinate one, and a point `groups` cannot nest; #28: the floor starts at the lowest value of `groups`, and a range too narrow for the longest chain fails like a point; #29: superior and subordinate, not coarser and finer
5
+ ---
6
+
7
+ # 0004 — The size defaults are metabeta's envelope; presets, not overrides, choose the kind of table
8
+
9
+ ADR 0001 and root ADR 0005 of metabeta-v3 let `metabeta.mixed` pass the sizes of its envelope
10
+ rules 7 and 8 as overrides. Decided on 2026-10-14, with ADR 0003: all simulation happens in
11
+ TabDGP, and metabeta draws no size of its own. So **rules 7 and 8 are TabDGP's size defaults,
12
+ a count bounded by another field is drawn from its distribution truncated at that field's
13
+ resolved value, and the kind of table is a preset of the hyperprior: `Hyperprior.grouped()`,
14
+ metabeta's corpus, and `Hyperprior.flat()`; `draw(seed, index, hyperprior)` takes a preset
15
+ and has no default.** What the proofs certify is `grouped()`.
16
+
17
+ ## Decisions
18
+
19
+ - **The group-size draw is rule 7**, since the Gamma(κ) weights of the prior-art search
20
+ normalise to the symmetric Dirichlet(α) the registry fitted per factor: with probability
21
+ `balanced_share` the groups are balanced (n / G each, the remainder one more for the first
22
+ groups); otherwise one row per group, and the remaining n − G rows multinomial over
23
+ Dirichlet(α) proportions. Two per-factor fields; G non-empty groups always; no retry.
24
+ - **Truncation at resolve.** `rows` resolves first; `groups` draws from its distribution
25
+ truncated at `rows`, a superior factor's `groups` below the subordinate factor's, and
26
+ `design_columns` at `rows / 2` (rule 8's one floor, n ≥ 2d, which item 1 did not enforce).
27
+ A draw clipped afterwards piles mass on the bound: from `LogUniformInt(2, 400)` at
28
+ n = 20, 57 % of draws would become all-singleton tables.
29
+ - **A superior factor has fewer groups than the subordinate one.** Its `groups` is
30
+ truncated at one below the subordinate factor's, so a nested pair is never one partition
31
+ relabelled. Each factor's floor is the lowest value of `groups` (2 by default) plus the
32
+ number of factors superior to it, so that every superior one still gets that lowest value;
33
+ a chain of L factors needs at least L + 1 rows by default, and a `rows` override below that
34
+ fails at resolve. A chain of L factors needs L counts, each below the subordinate one's, so
35
+ a `groups` with fewer values than the largest `factors` (a point has one) fails at construction
36
+ unless `factors` stays that small or `nested_share` at 0.
37
+ - **Presets.** `factors` allows 0, and `Hyperprior.flat()` sets it to 0; `grouped()` is the
38
+ default below. A later kind of table (item response, survival, causal) is a constructor
39
+ that fixes fields, never a branch in `draw` and never a new mechanism. Neither metabeta nor a
40
+ standalone user passes an override; an override serves a study, an ablation or a size
41
+ outside a preset.
42
+ - **The defaults of `grouped()`**, round numbers, the first set agreed in the grill:
43
+
44
+ | field | default | source |
45
+ |---|---|---|
46
+ | `factors` | `UniformInt(1, 3)` | rule 3 (counts uniform); registry p90 2, max 5; v1 edge 3 |
47
+ | `groups`, per factor | `LogUniformInt(2, 400)`, truncated at `rows` or below the subordinate factor's | rule 8; registry p50 32, p90 361 |
48
+ | `balanced_share` | `1/3` | rule 7; registry 0.388 |
49
+ | `size_alpha`, per factor | `LogUniform(0.5, 500)` | rule 7; registry p5 0.97, median 50 |
50
+ | `nested_share` | `Uniform(0.2, 0.6)` | a guess; registry 134 nested to 184 crossed among two-factor tables |
51
+ | `group_level_share` | `Beta(2, 4)` | a guess at the registry's 31 % |
52
+ | `group_share`, per node | `Beta(0.5, 2)` | a guess, mass near zero |
53
+
54
+ The registry's smallest α, 0.025, lies below the floor; item 4's containment test decides
55
+ whether to widen it.
56
+
57
+ ## Considered options
58
+
59
+ - **Clip after the draw** over truncation: rejected above.
60
+ - **A superior factor with at most as many groups as the subordinate one**, the first
61
+ draft: equal counts put one subordinate group in each superior group, the same partition
62
+ relabelled, two random effects no generalized linear mixed model (GLMM) tells apart.
63
+ Forced when the subordinate factor has 2 groups, it hit 15.9 % of nested pairs under
64
+ `grouped()`. Turning such a pair crossed was the other fix: it honours `nested_share` only
65
+ sometimes.
66
+ - **A kwarg on `draw`** for the kind of table, `flat: bool` or a `Literal` of kinds: a bool
67
+ is not future-proof, a `Literal` makes every kind a branch inside `draw` that can contradict
68
+ an override. A preset is the hyperprior itself.
69
+ - **`factors` from 0 by default**, metabeta overriding to 1: the defaults are what metabeta
70
+ trains on, so the grouped corpus is the preset and the flat table the other preset.
71
+
72
+ ## Consequences
73
+
74
+ - ADR 0001's "metabeta passes its sizes" is amended: metabeta passes nothing and calls
75
+ `draw(seed, index, Hyperprior.grouped())`. metabeta's `envelope.md` (rules 7–9) and its
76
+ `packages/mixed` ADR 0005 say the same on their side.
77
+ - The proofs run on `grouped()`; mixedbench#72's flat sample tests `flat()`.
78
+ - `hyperprior` gains the per-factor fields, the presets and the truncation; a bound names
79
+ the field it depends on, and `resolveDataset` resolves in dependency order.
@@ -19,11 +19,11 @@ Each module owns one design decision, hidden from the others:
19
19
 
20
20
  | module | owns | imported by |
21
21
  |---|---|---|
22
- | `hyperprior` | how a field is a point or a distribution, its bounds, its resolution to points, and which fields each column or node draws, by marginal family for a continuous column | every module |
23
- | `column` | the design-column budget and the post-hoc types: each column's name, type, K and own parameters, what its metadata guarantees, and its values: a sample of its marginal family or its codes, placed by its node's ranks | draw |
24
- | `graph` | the generating graph: the Cauchy DAG over latent and observed nodes, latent nodes first, the block bonus, each node's family, activation and scales, and what the graph guarantees | mechanism, draw |
25
- | `mechanism` | how a node's values follow from its parents and its noise: linear and MLP families, relative noise, the z-score | draw |
26
- | `draw` | the pipeline and its output: the key's generator, threaded through the modules in a fixed order, nodes joined to columns by name, and what one draw returns | the package's `__init__` |
22
+ | `hyperprior` | how a field is a point or a distribution, its bounds, its resolution to points, a count truncated at another field's value, the presets, and which fields each column, node or grouping factor draws, by marginal family for a continuous column | every module |
23
+ | `column` | the design-column budget and the post-hoc types: each column's name, type, K and own parameters, what its metadata guarantees, and its values: a sample of its marginal family or its codes, placed by its node's ranks; the grouping factor as a column, its relation, size draw and codes, and `Grouping`, the unit arithmetic (a unit's chain, the broadcast between units) | graph, mechanism, draw |
24
+ | `graph` | the generating graph: the Cauchy DAG over latent and observed nodes, the block bonus, each node's unit and the mask on edges out of its chain, the order (the grouping factors, then the units superior first, the row last, latent before observed), each node's family, activation and scales, a row-unit node's group share and group factor, and what the graph guarantees | mechanism, draw |
25
+ | `mechanism` | how a node's values follow from its parents and its noise, at a unit: linear and MLP families, the parents read at the unit, relative noise, the z-score at the node's own unit, and a row-unit node's group part and row part mixed by its group share | draw |
26
+ | `draw` | the pipeline and its output: the key's generator, threaded through the modules in a fixed order, the grouping before the graph, nodes joined to columns by name, every column and latent node broadcast to the rows last, and what one draw returns, the grouping factors first | the package's `__init__` |
27
27
 
28
28
  ## Design
29
29
 
@@ -34,6 +34,10 @@ harder to understand or change; it accrues one small edit at a time.
34
34
  implementation folds into its caller or callee.
35
35
  - **Information hiding.** Split code by what it knows (a node mechanism, a tensor layout, the
36
36
  hyperprior's parameterisation), not by the order it runs in.
37
+ - **One home per fact**, in code as in the docs. A rule written out twice (a truncation, the
38
+ unit order, a loop's initial state) becomes one function, one named value, or a call to the
39
+ module that owns it. No utils module: a helper two modules need lives in the module that
40
+ owns its fact.
37
41
  - **No pass-throughs.** A method that forwards its arguments unchanged adds nothing; call the
38
42
  target directly.
39
43
  - **Pull complexity down.** The module absorbs the awkward case so its callers stay simple. A
@@ -60,7 +64,8 @@ harder to understand or change; it accrues one small edit at a time.
60
64
  - A module is one lowercase word. A package's `__init__.py` lists its public names in
61
65
  `__all__`.
62
66
  - `PascalCase` for classes and `Literal` aliases, `camelCase` for functions and methods,
63
- `snake_case` for variables, `UPPER_SNAKE_CASE` for constants.
67
+ `snake_case` for variables and a `Literal`'s values (`'grouping_factor'`), `UPPER_SNAKE_CASE`
68
+ for constants.
64
69
  - A config field that counts is a plural noun (`rows`, `latent_nodes`), without an `n_` prefix.
65
70
  - A function in `src/` is named for what it does, a verb and its object (`_resolveColumn`),
66
71
  not for what it returns (`_column`). A test helper is read inside an assert, so it may name
@@ -89,6 +94,8 @@ harder to understand or change; it accrues one small edit at a time.
89
94
  ### Control flow
90
95
 
91
96
  - f-strings, one statement per line, a long expression named on the line before.
97
+ - One operation per line: a draw is named before it is cast or transformed. The Bernoulli
98
+ idiom `rng.random() < share` counts as one draw.
92
99
  - A method lists its steps in the order you would describe them; a step with its own `if`
93
100
  ladder or loop becomes a method. A search is a plain loop with an explicit check after it,
94
101
  not `for`/`else`, `while True` or `next()` on a generator. A loop fills its result as it
@@ -109,9 +116,10 @@ harder to understand or change; it accrues one small edit at a time.
109
116
 
110
117
  ### Arrays and tensors
111
118
 
112
- - Annotate shapes inline (`# (B, n, p)` for B datasets of n rows and p columns), reshape with
113
- `einops` where the library has it, never mutate a reused array in place. Vectorise with
114
- broadcasting and advanced indexing, not loops; batch over datasets.
119
+ - Annotate a shape inline on the line that returns it (`# (B, n, p)` for B datasets of n rows
120
+ and p columns), never in a docstring. Reshape with `einops` where the library has it, never
121
+ mutate a reused array in place. Vectorise with broadcasting and advanced indexing, not
122
+ loops; batch over datasets.
115
123
  - One letter per quantity, whatever its case: `n` counts rows, in code and in shape comments
116
124
  alike, and `d` counts nodes.
117
125
  - Every draw takes an explicit generator or key; nothing reads global random state.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tabdgp"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "A data-generating process for grouped and flat tables, to be matched to a registry of real tables"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -24,7 +24,6 @@ dependencies = [
24
24
  [project.urls]
25
25
  Repository = "https://github.com/adkipnis/tabdgp"
26
26
  Issues = "https://github.com/adkipnis/tabdgp/issues"
27
- Roadmap = "https://github.com/adkipnis/tabdgp/blob/main/ROADMAP.md"
28
27
 
29
28
  [dependency-groups]
30
29
  dev = [