ndscape 0.1.1__tar.gz → 0.1.3__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 (76) hide show
  1. ndscape-0.1.3/.gitattributes +10 -0
  2. ndscape-0.1.3/.github/workflows/release.yml +54 -0
  3. ndscape-0.1.3/.gitignore +55 -0
  4. ndscape-0.1.3/CITATION.cff +31 -0
  5. ndscape-0.1.3/MANIFEST.in +1 -0
  6. {ndscape-0.1.1 → ndscape-0.1.3}/PKG-INFO +139 -132
  7. {ndscape-0.1.1 → ndscape-0.1.3}/README.md +78 -69
  8. ndscape-0.1.3/config.py +14 -0
  9. ndscape-0.1.3/configs/artifacts.yml +81 -0
  10. ndscape-0.1.3/core.py +337 -0
  11. ndscape-0.1.3/nd/ACND.py +80 -0
  12. ndscape-0.1.3/nd/BBoK.py +45 -0
  13. ndscape-0.1.3/nd/BestOfK.py +14 -0
  14. ndscape-0.1.3/nd/CBND.py +33 -0
  15. ndscape-0.1.3/nd/NDC.py +50 -0
  16. ndscape-0.1.3/nd/NestedDichotomy.py +165 -0
  17. ndscape-0.1.3/nd/RPND.py +51 -0
  18. ndscape-0.1.3/nd/RandomGeneration.py +143 -0
  19. ndscape-0.1.3/nd/__init__.py +8 -0
  20. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape/README.md +5 -3
  21. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape/api.py +1 -1
  22. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape.egg-info/PKG-INFO +139 -132
  23. ndscape-0.1.3/ndscape.egg-info/SOURCES.txt +62 -0
  24. ndscape-0.1.3/ndscape.egg-info/scm_file_list.json +169 -0
  25. ndscape-0.1.3/ndscape.egg-info/scm_version.json +8 -0
  26. ndscape-0.1.3/paper_artifact.md +138 -0
  27. {ndscape-0.1.1 → ndscape-0.1.3}/pyproject.toml +11 -4
  28. ndscape-0.1.3/pytest.ini +4 -0
  29. ndscape-0.1.3/reproduce.py +175 -0
  30. ndscape-0.1.3/requirements.txt +15 -0
  31. ndscape-0.1.3/run_datasets.py +171 -0
  32. ndscape-0.1.3/run_simulations.py +474 -0
  33. ndscape-0.1.3/scripts/csv_to_configs.py +147 -0
  34. ndscape-0.1.3/scripts/download_zenodo.py +217 -0
  35. ndscape-0.1.3/scripts/make_ablation_summaries.py +49 -0
  36. ndscape-0.1.3/scripts/make_ablation_tables.py +133 -0
  37. ndscape-0.1.3/scripts/make_artifacts.py +81 -0
  38. ndscape-0.1.3/scripts/make_dataset_plots.py +340 -0
  39. ndscape-0.1.3/scripts/make_dataset_tables.py +247 -0
  40. ndscape-0.1.3/scripts/make_paper_tables.py +179 -0
  41. ndscape-0.1.3/scripts/make_simulation_plots.py +192 -0
  42. ndscape-0.1.3/scripts/make_simulation_tables.py +279 -0
  43. ndscape-0.1.3/scripts/moran_simulation_analysis.py +291 -0
  44. ndscape-0.1.3/scripts/verify_artifacts.py +127 -0
  45. {ndscape-0.1.1 → ndscape-0.1.3}/setup.cfg +4 -4
  46. ndscape-0.1.3/zenodo/art_1_C10_N15000_s0_d2.joblib +3 -0
  47. ndscape-0.1.3/zenodo/art_1_C19_N15000_s0_d2.joblib +3 -0
  48. ndscape-0.1.3/zenodo/art_1_C6_N945_s0_d2.joblib +3 -0
  49. ndscape-0.1.3/zenodo/art_1_C7_N10395_s0_d2.joblib +3 -0
  50. ndscape-0.1.3/zenodo/art_1_C8_N15000_s0_d2.joblib +3 -0
  51. ndscape-0.1.3/zenodo/art_1_C9_N15000_s0_d2.joblib +3 -0
  52. ndscape-0.1.3/zenodo/bow_1_C6_N945_s0_d2.joblib +3 -0
  53. ndscape-0.1.3/zenodo/bow_1_C7_N10395_s0_d2.joblib +3 -0
  54. ndscape-0.1.3/zenodo/manifest.csv +39 -0
  55. ndscape-0.1.3/zenodo/rf_1_C6_N945_s0_d2.joblib +3 -0
  56. ndscape-0.1.3/zenodo/rf_1_C7_N10395_s0_d2.joblib +3 -0
  57. ndscape-0.1.3/zenodo/simulation_configs.csv +416 -0
  58. ndscape-0.1.3/zenodo/simulation_configs_dt.csv +151 -0
  59. ndscape-0.1.1/ndscape.egg-info/SOURCES.txt +0 -23
  60. ndscape-0.1.1/tests/test_artifact.py +0 -160
  61. ndscape-0.1.1/tests/test_dt_simulation.py +0 -236
  62. ndscape-0.1.1/tests/test_embed_tree.py +0 -2
  63. ndscape-0.1.1/tests/test_nd_generators.py +0 -135
  64. ndscape-0.1.1/tests/test_nd_predict.py +0 -128
  65. ndscape-0.1.1/tests/test_order_tree.py +0 -100
  66. ndscape-0.1.1/tests/test_real_datasets.py +0 -195
  67. ndscape-0.1.1/tests/test_sample_trees.py +0 -114
  68. ndscape-0.1.1/tests/test_split_analyzer.py +0 -93
  69. ndscape-0.1.1/tests/test_tree_enumeration.py +0 -101
  70. ndscape-0.1.1/tests/test_tree_utilities.py +0 -2
  71. {ndscape-0.1.1 → ndscape-0.1.3}/LICENSE +0 -0
  72. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape/__init__.py +0 -0
  73. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape/plot.py +0 -0
  74. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape.egg-info/dependency_links.txt +0 -0
  75. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape.egg-info/requires.txt +0 -0
  76. {ndscape-0.1.1 → ndscape-0.1.3}/ndscape.egg-info/top_level.txt +0 -0
@@ -0,0 +1,10 @@
1
+ * text=auto eol=lf
2
+ *.py text eol=lf
3
+ *.md text eol=lf
4
+ *.txt text eol=lf
5
+ *.yml text eol=lf
6
+ *.yaml text eol=lf
7
+ *.json text eol=lf
8
+ *.jsonl text eol=lf
9
+ *.tex text eol=lf
10
+ *.joblib filter=lfs diff=lfs merge=lfs -text
@@ -0,0 +1,54 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ name: Build distribution
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ with:
18
+ fetch-depth: 0
19
+
20
+ - uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Build sdist and wheel
25
+ run: |
26
+ python -m pip install --upgrade build
27
+ python -m build
28
+
29
+ - name: Check package metadata
30
+ run: |
31
+ python -m pip install --upgrade twine
32
+ python -m twine check dist/*
33
+
34
+ - uses: actions/upload-artifact@v7
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ publish:
40
+ name: Publish to PyPI
41
+ needs: build
42
+ runs-on: ubuntu-latest
43
+ environment:
44
+ name: pypi
45
+ url: https://pypi.org/p/ndscape
46
+ permissions:
47
+ id-token: write
48
+ steps:
49
+ - uses: actions/download-artifact@v8
50
+ with:
51
+ name: dist
52
+ path: dist/
53
+
54
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,55 @@
1
+ # virtual environments
2
+ .venv/
3
+ .venv_linux/
4
+ venv/
5
+ venv_linux/
6
+
7
+ # IDE
8
+ .vscode/
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.pyc
13
+ .pytest_cache/
14
+ numba_cache/
15
+
16
+ # packaging artifacts
17
+ build/
18
+ dist/
19
+ *.egg-info/
20
+ .eggs/
21
+ *.whl
22
+
23
+ # datasets — download separately via Zenodo (see README)
24
+ data/
25
+ zenodo/data/
26
+
27
+ # zenodo/ subdirs downloaded or generated (not committed)
28
+ zenodo/raw_simulation_results/
29
+ zenodo/raw_simulation_results_dt/
30
+ zenodo/simulation_scores/
31
+ zenodo/simulation_scores_dt/
32
+ zenodo/tree_metrics/
33
+ zenodo/*.zip
34
+
35
+ # generated outputs — reproduced by running the scripts
36
+ real-data-bias-analysis/
37
+ bias-analysis/
38
+ paper_outputs/
39
+ cache_nd/
40
+
41
+ # stale / superseded files
42
+ all_sims.csv
43
+ all_sims_trimmed.csv
44
+ ndscape_repo.7z
45
+ # optional JSON conversion of simulation_configs.csv
46
+ simulation_configs.json
47
+ zenodo/simulation_configs.json
48
+
49
+ # temp file written by reproduce.py --steps sim/moran --classes ...
50
+ _selected_sims.csv
51
+
52
+ # credentials — never commit these
53
+ .env
54
+ .pypirc
55
+ *.pypirc
@@ -0,0 +1,31 @@
1
+ cff-version: 1.1.0
2
+ message: "If you use this software, please cite it as below."
3
+ type: software
4
+ title: "Nested-Dichotomy Tree-Space Embedding"
5
+ abstract: >
6
+ Code for scoring nested-dichotomy (ND) tree structures on
7
+ multiclass datasets, embedding the tree space via metric MDS, and running
8
+ a bias-variance simulation over the space.
9
+ authors:
10
+ - family-names: "Dix-Matthews"
11
+ given-names: "Maxwell"
12
+ orcid: "https://orcid.org/0009-0003-1575-0744"
13
+ - family-names: "Hodkiewicz"
14
+ given-names: "Melinda"
15
+ orcid: "https://orcid.org/0000-0002-7336-3932"
16
+ - family-names: "Polpo"
17
+ given-names: "Adriano"
18
+ orcid: "https://orcid.org/0000-0002-5959-1808"
19
+
20
+ repository-code: "https://github.com/res-lucid/ndscape"
21
+ license: MIT
22
+ doi: "10.5281/zenodo.20456943"
23
+ version: "0.1.3"
24
+ date-released: "2026-06-17"
25
+ keywords:
26
+ - nested-dichotomy
27
+ - tree-space
28
+ - bias-variance
29
+ - MDS
30
+ - spatial-autocorrelation
31
+ - machine-learning
@@ -0,0 +1 @@
1
+ prune tests
@@ -1,132 +1,139 @@
1
- Metadata-Version: 2.4
2
- Name: ndscape
3
- Version: 0.1.1
4
- Summary: Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
5
- Author: Maxwell Dix-Matthews
6
- License-Expression: MIT
7
- Project-URL: Repository, https://github.com/res-lucid/ndscape
8
- Keywords: nested-dichotomy,multi-class,classification,scikit-learn
9
- Classifier: Programming Language :: Python :: 3
10
- Classifier: Intended Audience :: Science/Research
11
- Classifier: Topic :: Scientific/Engineering
12
- Requires-Python: >=3.9
13
- Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
- Requires-Dist: numpy>=1.24
16
- Requires-Dist: scikit-learn>=1.3
17
- Requires-Dist: joblib>=1.2
18
- Provides-Extra: spatial
19
- Requires-Dist: esda>=2.6; extra == "spatial"
20
- Requires-Dist: libpysal>=4.10; extra == "spatial"
21
- Provides-Extra: plot
22
- Requires-Dist: matplotlib>=3.5; extra == "plot"
23
- Requires-Dist: bokeh>=3.0; extra == "plot"
24
- Dynamic: license-file
25
-
26
- # ndscape
27
-
28
- Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
29
-
30
- A nested dichotomy reduces a C-class problem to a tree of binary splits
31
- (e.g. {0,1,2} vs {3,4}, then {0} vs {1,2}, ...). ndscape lets you fit one,
32
- score it, or place a whole population of candidate trees in a 2-D
33
- "tree-space" to see how a property (accuracy, variance, ...) varies across
34
- tree structures.
35
-
36
- ## Install
37
-
38
- ```
39
- pip install ndscape
40
- pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
41
- pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
42
- ```
43
-
44
- ## Quickstart
45
-
46
- ```python
47
- from sklearn.datasets import load_iris
48
- from sklearn.model_selection import train_test_split
49
- import ndscape as nds
50
-
51
- X, y = load_iris(return_X_y=True)
52
- classes = sorted(set(y))
53
- X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
54
-
55
- nd = nds.fit(X_train, y_train, classes=classes, base="lr")
56
- nd.predict(X_test)
57
- nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
58
- ```
59
-
60
- `classes` is the list of class labels in your `y` (`sorted(set(y))` works for
61
- integer or string labels). `nds.fit` samples one ND tree automatically; pass
62
- `tree=...` to use a specific one (see "A `tree` is..." below).
63
-
64
- ## Use cases
65
-
66
- **You have a dataset and a binary classifier.**
67
-
68
- `base` can be the string `"lr"` or `"decisiontree"`, or your own unfitted
69
- scikit-learn estimator — a fresh clone of it is fit at every split.
70
-
71
- ```python
72
- from sklearn.svm import SVC
73
-
74
- nd = nds.fit(X_train, y_train, classes=classes, base=SVC(probability=True, kernel="linear"))
75
- ```
76
-
77
- **You have a train/test split and want a score.**
78
-
79
- ```python
80
- nd = nds.ND(tree, classes).fit(X_train, y_train, base="lr")
81
- nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
82
- ```
83
-
84
- **You already trained the per-split models yourself.**
85
-
86
- ```python
87
- # models in the same order as tree, or a {(left, right): model} dict — either works
88
- nd = nds.ND.from_trained(tree, classes, models=[fitted_model_1, fitted_model_2, ...])
89
- nd.predict_proba(X_test)
90
- ```
91
-
92
- **You already scored a set of trees and want to see where they sit in tree-space.**
93
-
94
- ```python
95
- trees, coords = nds.embed_trees(classes)
96
- nds.spatial_autocorrelation(my_scores, coords) # {"I": ..., "p_sim": ...}
97
- ```
98
-
99
- **You just want the whole picture: fit, score, and embed every candidate tree.**
100
-
101
- ```python
102
- rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base="lr")
103
- # [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
104
- ```
105
-
106
- **You want a picture of that tree-space.**
107
-
108
- ```python
109
- nds.plot(rows, metric="accuracy", path="tree_space.png") # static PNG/PDF
110
- nds.plot_interactive(rows, metric="accuracy", path="tree_space.html") # pan/zoom/hover
111
- ```
112
-
113
- Both color points by `metric` and mark the best tree with a black x. Needs
114
- `ndscape[plot]`.
115
-
116
- **The embedding is slow to recompute and you want to reuse it.**
117
-
118
- ```python
119
- rows = nds.analyze(X_train, y_train, classes, cache="embedding.joblib")
120
- ```
121
-
122
- The MDS step in `embed_trees`/`analyze` is the slow part. Pass `cache=` a
123
- `.joblib` path: the first call computes the embedding and saves it there,
124
- later calls with the same path just load it.
125
-
126
- A `tree` is a list of `(left, right)` tuples of class labels, e.g.
127
- `[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
128
- (exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
129
- to generate candidates.
130
-
131
- `base` accepts `"lr"`, `"decisiontree"`, or any unfitted scikit-learn
132
- estimator with `fit`/`predict_proba`.
1
+ Metadata-Version: 2.4
2
+ Name: ndscape
3
+ Version: 0.1.3
4
+ Summary: Fit, score, and embed nested-dichotomy (ND) trees for multiclass classification.
5
+ Author: Maxwell Dix-Matthews
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/res-lucid/ndscape
8
+ Project-URL: Repository, https://github.com/res-lucid/ndscape
9
+ Project-URL: Paper, https://arxiv.org/abs/2610.18050
10
+ Project-URL: Issues, https://github.com/res-lucid/ndscape/issues
11
+ Project-URL: Interactive maps, https://res-lucid.github.io/ndscape-plots/
12
+ Project-URL: Data archive, https://doi.org/10.5281/zenodo.20456943
13
+ Keywords: nested-dichotomy,multiclass,classification,scikit-learn
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scikit-learn>=1.3
22
+ Requires-Dist: joblib>=1.2
23
+ Provides-Extra: spatial
24
+ Requires-Dist: esda>=2.6; extra == "spatial"
25
+ Requires-Dist: libpysal>=4.10; extra == "spatial"
26
+ Provides-Extra: plot
27
+ Requires-Dist: matplotlib>=3.5; extra == "plot"
28
+ Requires-Dist: bokeh>=3.0; extra == "plot"
29
+ Dynamic: license-file
30
+
31
+ # ndscape
32
+
33
+ Fit, score, and embed nested-dichotomy (ND) trees for multiclass classification.
34
+
35
+ A nested dichotomy reduces an M-class problem to a tree of binary splits
36
+ (e.g. {0,1,2} vs {3,4}, then {0} vs {1,2}, ...). ndscape lets you fit one,
37
+ score it, or place a whole population of candidate trees in a 2-D
38
+ "tree-space" to see how a property (accuracy, variance, ...) varies across
39
+ tree structures.
40
+
41
+ The method is described in [Dix-Matthews, Hodkiewicz and Polpo (2026)](https://arxiv.org/abs/2610.18050).
42
+
43
+ ## Install
44
+
45
+ ```
46
+ pip install ndscape
47
+ pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
48
+ pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
49
+ ```
50
+
51
+ ## Quickstart
52
+
53
+ ```python
54
+ from sklearn.datasets import load_iris
55
+ from sklearn.model_selection import train_test_split
56
+ import ndscape as nds
57
+
58
+ X, y = load_iris(return_X_y=True)
59
+ classes = sorted(set(y))
60
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
61
+
62
+ nd = nds.fit(X_train, y_train, classes=classes, base="lr")
63
+ nd.predict(X_test)
64
+ nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
65
+ ```
66
+
67
+ `classes` is the list of class labels in your `y` (`sorted(set(y))` works for
68
+ integer or string labels). `nds.fit` samples one ND tree automatically; pass
69
+ `tree=...` to use a specific one (see "A `tree` is..." below).
70
+
71
+ ## Use cases
72
+
73
+ **You have a dataset and a binary classifier.**
74
+
75
+ `base` can be the string `"lr"` or `"decisiontree"`, or your own unfitted
76
+ scikit-learn estimator — a fresh clone of it is fit at every split.
77
+
78
+ ```python
79
+ from sklearn.svm import SVC
80
+
81
+ nd = nds.fit(X_train, y_train, classes=classes, base=SVC(probability=True, kernel="linear"))
82
+ ```
83
+
84
+ **You have a train/test split and want a score.**
85
+
86
+ ```python
87
+ nd = nds.ND(tree, classes).fit(X_train, y_train, base="lr")
88
+ nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
89
+ ```
90
+
91
+ **You already trained the per-split models yourself.**
92
+
93
+ ```python
94
+ # models in the same order as tree, or a {(left, right): model} dict — either works
95
+ nd = nds.ND.from_trained(tree, classes, models=[fitted_model_1, fitted_model_2, ...])
96
+ nd.predict_proba(X_test)
97
+ ```
98
+
99
+ **You already scored a set of trees and want to see where they sit in tree-space.**
100
+
101
+ ```python
102
+ trees, coords = nds.embed_trees(classes)
103
+ nds.spatial_autocorrelation(my_scores, coords) # {"I": ..., "p_sim": ...}
104
+ ```
105
+
106
+ **You just want the whole picture: fit, score, and embed every candidate tree.**
107
+
108
+ ```python
109
+ rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base="lr")
110
+ # [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
111
+ ```
112
+
113
+ **You want a picture of that tree-space.**
114
+
115
+ ```python
116
+ nds.plot(rows, metric="accuracy", path="tree_space.png") # static PNG/PDF
117
+ nds.plot_interactive(rows, metric="accuracy", path="tree_space.html") # pan/zoom/hover
118
+ ```
119
+
120
+ Both colour points by `metric` and mark the best tree with a black x. Needs
121
+ `ndscape[plot]`.
122
+
123
+ **The embedding is slow to recompute and you want to reuse it.**
124
+
125
+ ```python
126
+ rows = nds.analyze(X_train, y_train, classes, cache="embedding.joblib")
127
+ ```
128
+
129
+ The MDS step in `embed_trees`/`analyze` is the slow part. Pass `cache=` a
130
+ `.joblib` path: the first call computes the embedding and saves it there,
131
+ later calls with the same path just load it.
132
+
133
+ A `tree` is a list of `(left, right)` tuples of class labels, e.g.
134
+ `[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
135
+ (exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
136
+ to generate candidates.
137
+
138
+ `base` accepts `"lr"`, `"decisiontree"`, or any unfitted scikit-learn
139
+ estimator with `fit`/`predict_proba`.
@@ -1,46 +1,74 @@
1
- # Nested-Dichotomy Tree-Space Embedding
1
+ # ndscape: Nested Dichotomy Tree-Space Embedding
2
2
 
3
- Code for the paper. Embeds the space of nested-dichotomy (ND) structures via depth-weighted tokenisation and metric MDS, then maps bias, variance, and held-out performance across that space.
3
+ [![PyPI](https://img.shields.io/pypi/v/ndscape.svg)](https://pypi.org/project/ndscape/)
4
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.20456943.svg)](https://doi.org/10.5281/zenodo.20456943)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
4
6
 
5
- ## Installation
7
+ A classification model assigns an observation to one of several classes, and when there are more than two, the task becomes a multiclass problem. A nested dichotomy (ND) gives that multiclass problem a visible structure: it arranges the classes in a binary tree and fits a classifier at each split, so the final prediction is built from a sequence of simpler binary decisions.
6
8
 
7
- ```bash
8
- pip install -r requirements.txt
9
- ```
9
+ Any binary classifier can be used at the nodes. With probabilistic classifiers, the probability of each class is the product of the branch probabilities along its path from root to leaf. And because the tree records which classes are grouped together, where they separate, and in what order, you can inspect the fitted model and compare its structure with what you already know about the problem.
10
+
11
+ That structure also matters for prediction. There are (2M−3)!! possible NDs for M classes, each defining a different decomposition of the same classification task, and changing the tree can produce very different fitted models even when the data and binary classifier stay the same. This gives NDs a large, interpretable model space where changes in predictive behaviour can be related directly to changes in structure.
10
12
 
11
- Versions of `numpy`, `scipy`, and `scikit-learn` are pinned in `requirements.txt`. The MDS coordinates in `cache_nd/` were built with those exact versions; changing them will produce different (but statistically equivalent) coordinates.
13
+ `ndscape` embeds ND tree structures using depth-weighted structural tokens and metric MDS, so structurally similar trees sit close together. It can be used as a Python package, and this repository also contains the code needed to reproduce the [paper](https://arxiv.org/abs/2610.18050) outputs.
12
14
 
13
- ## Quick check (~30 s)
15
+ Interactive maps from the [paper](https://arxiv.org/abs/2610.18050): [res-lucid.github.io/ndscape-plots](https://res-lucid.github.io/ndscape-plots/)
16
+
17
+ ## Installation
18
+
19
+ For package use:
14
20
 
15
21
  ```bash
16
- pytest tests/
22
+ pip install ndscape
17
23
  ```
18
24
 
19
- To also run the slower integration tests (require pre-built C=6 and C=7 cache files, or rebuild from scratch):
20
-
25
+ To reproduce the paper outputs, clone the repository and install the pinned requirements:
21
26
  ```bash
22
- pytest tests/ -m integration
27
+ python -m venv venv
28
+ source venv/bin/activate # Windows: venv\Scripts\activate
29
+ pip install -r requirements.txt
23
30
  ```
24
31
 
25
- ## Reproduce paper outputs (fast — no re-simulation)
32
+ Versions of `numpy`, `scipy`, and `scikit-learn` are pinned in `requirements.txt`. The MDS coordinates in `cache_nd/` were built with those exact versions, so changing numpy, scipy, or scikit-learn can produce slightly different coordinates.
33
+
34
+ ## How to use this repository
35
+
36
+ | Goal | Command |
37
+ |------|---------|
38
+ | Use `ndscape` as a package | `pip install ndscape` |
39
+ | Test the repository install | `pytest tests/` |
40
+ | Recreate paper figures and tables from archived outputs | `python scripts/download_zenodo.py` then `python reproduce.py --paper` |
41
+ | Run the full reproduction pipeline | `python reproduce.py` |
42
+ | Run only selected steps | `python reproduce.py --steps artifacts` |
43
+ | Score a new dataset | `python run_datasets.py --datasets my_dataset` |
44
+
45
+ ## Reproduce paper outputs from archived results
26
46
 
27
- Download the Zenodo deposit first, then regenerate all figures and tables in minutes:
47
+ Download the Zenodo deposit, then regenerate the paper figures and tables without re-running the simulations:
28
48
 
29
49
  ```bash
30
50
  python scripts/download_zenodo.py
31
51
  python reproduce.py --paper
32
52
  ```
33
53
 
34
- `--paper` runs four scripts in order:
54
+ <details>
55
+ <summary>Scripts run by <code>python reproduce.py --paper</code></summary>
35
56
 
36
- 1. `scripts/make_dataset_plots.py --cache zenodo --out paper_outputs/figures` — PDF and interactive HTML embedding maps for all 6 real datasets
37
- 2. `scripts/make_simulation_plots.py --out paper_outputs/figures` — simulation bias/variance/depth PDF maps (Figs 2–5 and appendix)
38
- 3. `scripts/make_ablation_summaries.py` — Moran spatial analysis for the tokeniser / BoW / RF ablation (reads `zenodo/raw_simulation_results/`)
39
- 4. `scripts/make_paper_tables.py --all --out paper_outputs/tables` — all LaTeX tables; uses pre-built scores from `zenodo/simulation_scores/` and `zenodo/simulation_scores_dt/` if present, otherwise recomputes from `zenodo/raw_simulation_results{,_dt}/`
57
+ 1. `scripts/make_dataset_plots.py --cache zenodo --out paper_outputs/figures`
58
+ - PDF and interactive HTML embedding maps for all six real datasets.
59
+ 2. `scripts/make_simulation_plots.py --out paper_outputs/figures`
60
+ - Simulation bias, variance, and depth maps.
61
+ 3. `scripts/make_ablation_summaries.py`
62
+ - Moran spatial analysis for the tokeniser, BoW, and RF ablations.
63
+ 4. `scripts/make_paper_tables.py --all --out paper_outputs/tables`
64
+ - LaTeX tables.
40
65
 
41
- Output lands in `paper_outputs/figures/` and `paper_outputs/tables/`.
66
+ Output is saved to `paper_outputs/figures/` and `paper_outputs/tables/`.
67
+ </details>
42
68
 
43
- ## Full reproduction from scratch
69
+ ## Full reproduction pipeline
70
+
71
+ This rebuilds the derived outputs locally, using the archived data and configuration files downloaded into `zenodo/`.
44
72
 
45
73
  ```bash
46
74
  python reproduce.py
@@ -67,18 +95,24 @@ python reproduce.py --steps plots
67
95
 
68
96
  **Note on step 2:** `run_datasets.py` scores every ND tree on each real dataset and writes xlsx results. These are also available pre-built in `zenodo/tree_metrics/` and are used automatically by `make_dataset_plots.py` and `make_dataset_tables.py` when the xlsx files are absent.
69
97
 
70
- ## Real-data tables and figures without re-scoring
98
+ ## Using your own data
99
+
100
+ Drop a CSV in `zenodo/data/` with a `y` column (integer or string class labels) and any number of numeric feature columns. A `split` column (`"train"` / `"test"`) is used if present, otherwise a stratified 70/30 split is applied.
101
+
102
+ ```bash
103
+ python run_datasets.py --datasets my_dataset
104
+ ```
71
105
 
72
- `make_dataset_plots.py` and `make_dataset_tables.py` fall back to `zenodo/tree_metrics/<dataset>.csv` when the step-2 xlsx files are not present. After running `python scripts/download_zenodo.py`, the `--paper` command produces all real-data figures and tables without running step 2.
106
+ Output goes to `real-data-bias-analysis/my_dataset/v_my_dataset_lr_cluster_tables.xlsx`.
73
107
 
74
108
  ## Tests
75
109
 
76
110
  ```bash
77
111
  pytest tests/ # unit + snapshot tests (fast)
78
- pytest tests/ -m integration # integration tests (slow; requires pre-built C=6,7 artifacts)
112
+ pytest tests/ -m integration # integration tests (slow and requires pre-built C=6,7 artifacts)
79
113
  ```
80
114
 
81
- The `tests/fixtures/` directory holds MD5 hashes of the tree lists, tokeniser output, and per-dataset scores.
115
+ The `tests/fixtures/` directory holds golden-master fixtures: MD5 hashes for the larger artifacts, and full tree lists and generator outputs for the smaller ones.
82
116
 
83
117
  ## Datasets
84
118
 
@@ -112,38 +146,18 @@ Or place CSV files (with a `y` column for the class label) in `zenodo/data/`:
112
146
 
113
147
  ## Key files
114
148
 
115
- ```
149
+ ```text
150
+ reproduce.py Main reproduction entry point
116
151
  core.py Tree enumeration, embedding, models, artifact building
117
- generate_data.py Synthetic data generation for the simulation
118
- run_datasets.py Score all trees on each real dataset; write xlsx results
119
- run_simulations.py Bias-variance simulation (numba-accelerated)
120
- reproduce.py Single-command entry point (--paper for fast path)
121
-
122
- nd/ ND algorithm implementations
123
- NestedDichotomy.py Core tree data structure and training
124
- RandomGeneration.py Uniform random tree sampling
125
- BBoK.py Balanced best-of-K generator
126
- BestOfK.py Best-of-K wrapper (K=10)
127
- ACND.py, NDC.py, RPND.py, CBND.py Four further construction heuristics
128
-
129
- scripts/make_artifacts.py Build MDS embeddings from configs/artifacts.yml
130
- scripts/verify_artifacts.py Check built artifacts against configs/artifacts.yml
131
- scripts/make_dataset_plots.py Embedding PDF + HTML maps per real dataset
132
- scripts/make_simulation_plots.py Simulation bias/variance/depth PDF maps
133
- scripts/moran_simulation_analysis.py Spatial statistics on simulation results
134
- scripts/make_dataset_tables.py LaTeX real-data tables (spatial stats + heuristic placement)
135
- scripts/make_simulation_tables.py LaTeX simulation tables
136
- scripts/make_ablation_summaries.py Moran analysis for tokeniser/BoW/RF ablations
137
- scripts/make_ablation_tables.py LaTeX ablation tables
138
- scripts/make_paper_tables.py Orchestrates all tables → paper_outputs/tables/
139
- scripts/download_zenodo.py Download pre-built data from Zenodo
140
- scripts/csv_to_configs.py Convert zenodo/simulation_configs.csv to JSON
141
-
142
- configs/artifacts.yml Manifest of all artifacts to build
143
- cache_nd/ Cached .joblib artifacts (auto-created by step 1)
144
- zenodo/ Pre-built deposit data (see Zenodo section below)
145
- real-data-bias-analysis/ Scoring results (auto-created by step 2)
146
- bias-analysis/raw_simulation_results/ Simulation bootstrap CSVs (auto-created by step 3)
152
+ run_datasets.py Score all trees on real datasets
153
+ run_simulations.py Bias-variance simulation
154
+ nd/ Nested dichotomy implementations
155
+ scripts/ Figure, table, artifact, download, and analysis scripts
156
+ configs/artifacts.yml Artifact specifications
157
+ cache_nd/ Cached embedding artifacts
158
+ zenodo/ Downloaded Zenodo archive
159
+ real-data-bias-analysis/ Real-data scoring outputs
160
+ bias-analysis/raw_simulation_results/ Simulation bootstrap outputs
147
161
  ```
148
162
 
149
163
  ## Zenodo
@@ -202,22 +216,11 @@ To convert the LR CSV to JSON:
202
216
  python scripts/csv_to_configs.py --output simulation_configs.json
203
217
  ```
204
218
 
205
- `zenodo/simulation_configs_dt.csv` is committed as a static artifact; it was generated once from
206
- the original Postgres simulation DB and is not regenerated as part of this pipeline.
207
-
208
- ## Using your own data
209
-
210
- Drop a CSV in `zenodo/data/` with a `y` column (integer or string class labels) and any number of numeric feature columns. A `split` column (`"train"` / `"test"`) is used if present; otherwise a stratified 70/30 split is applied.
211
-
212
- ```bash
213
- python run_datasets.py --datasets my_dataset
214
- ```
215
-
216
- Output goes to `real-data-bias-analysis/my_dataset/v_my_dataset_lr_cluster_tables.xlsx`.
219
+ `zenodo/simulation_configs_dt.csv` is committed as a static artifact and is not regenerated as part of this pipeline.
217
220
 
218
221
  ## Artifacts (cache_nd/)
219
222
 
220
- The `cache_nd/` directory holds `.joblib` artifacts for C=4 through C=19. All artifacts are tracked via Git LFS (`.gitattributes`) and are also available from Zenodo:
223
+ The `cache_nd/` directory holds `.joblib` artifacts for C=4 through C=19. It is not tracked in git — the artifacts are downloaded from Zenodo into `zenodo/` (tracked with Git LFS, see `.gitattributes`) or rebuilt locally:
221
224
 
222
225
  ```bash
223
226
  python scripts/download_zenodo.py --what artifacts
@@ -231,3 +234,9 @@ python scripts/make_artifacts.py --all
231
234
 
232
235
  Artifact specs (class count, tree count, seed) are declared in `configs/artifacts.yml`.
233
236
  Use `scripts/verify_artifacts.py` to check a built artifact against its spec.
237
+
238
+ ## Citation
239
+
240
+ Dix-Matthews, M., Hodkiewicz, M., & Polpo, A. (2026). ndscape: a structural map of the nested dichotomy space. arXiv:2610.18050 [stat.ME]. https://doi.org/10.48550/arXiv.2610.18050
241
+
242
+ Metadata is in [`CITATION.cff`](CITATION.cff); GitHub renders it under "Cite this repository". The archived data and artifacts have their own DOI, [10.5281/zenodo.20456943](https://doi.org/10.5281/zenodo.20456943).