ndscape 0.1.0__tar.gz → 0.1.2__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.
- ndscape-0.1.2/LICENSE +21 -0
- ndscape-0.1.2/MANIFEST.in +1 -0
- {ndscape-0.1.0/ndscape.egg-info → ndscape-0.1.2}/PKG-INFO +32 -1
- ndscape-0.1.2/README.md +240 -0
- ndscape-0.1.0/PKG-INFO → ndscape-0.1.2/ndscape/README.md +21 -19
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape/__init__.py +3 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape/api.py +19 -4
- ndscape-0.1.2/ndscape/plot.py +78 -0
- ndscape-0.1.0/ndscape/README.md → ndscape-0.1.2/ndscape.egg-info/PKG-INFO +50 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape.egg-info/SOURCES.txt +4 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape.egg-info/requires.txt +5 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/pyproject.toml +8 -2
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape.egg-info/dependency_links.txt +0 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/ndscape.egg-info/top_level.txt +0 -0
- {ndscape-0.1.0 → ndscape-0.1.2}/setup.cfg +0 -0
ndscape-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxwell Dix-Matthews
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ndscape
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
|
|
5
5
|
Author: Maxwell Dix-Matthews
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/res-lucid/ndscape
|
|
7
8
|
Project-URL: Repository, https://github.com/res-lucid/ndscape
|
|
9
|
+
Project-URL: Issues, https://github.com/res-lucid/ndscape/issues
|
|
10
|
+
Project-URL: Interactive maps, https://res-lucid.github.io/ndscape-plots/
|
|
11
|
+
Project-URL: Data archive, https://doi.org/10.5281/zenodo.20456943
|
|
8
12
|
Keywords: nested-dichotomy,multi-class,classification,scikit-learn
|
|
9
13
|
Classifier: Programming Language :: Python :: 3
|
|
10
14
|
Classifier: Intended Audience :: Science/Research
|
|
11
15
|
Classifier: Topic :: Scientific/Engineering
|
|
12
16
|
Requires-Python: >=3.9
|
|
13
17
|
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
14
19
|
Requires-Dist: numpy>=1.24
|
|
15
20
|
Requires-Dist: scikit-learn>=1.3
|
|
21
|
+
Requires-Dist: joblib>=1.2
|
|
16
22
|
Provides-Extra: spatial
|
|
17
23
|
Requires-Dist: esda>=2.6; extra == "spatial"
|
|
18
24
|
Requires-Dist: libpysal>=4.10; extra == "spatial"
|
|
25
|
+
Provides-Extra: plot
|
|
26
|
+
Requires-Dist: matplotlib>=3.5; extra == "plot"
|
|
27
|
+
Requires-Dist: bokeh>=3.0; extra == "plot"
|
|
28
|
+
Dynamic: license-file
|
|
19
29
|
|
|
20
30
|
# ndscape
|
|
21
31
|
|
|
@@ -32,6 +42,7 @@ tree structures.
|
|
|
32
42
|
```
|
|
33
43
|
pip install ndscape
|
|
34
44
|
pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
|
|
45
|
+
pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
|
|
35
46
|
```
|
|
36
47
|
|
|
37
48
|
## Quickstart
|
|
@@ -96,6 +107,26 @@ rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base
|
|
|
96
107
|
# [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
|
|
97
108
|
```
|
|
98
109
|
|
|
110
|
+
**You want a picture of that tree-space.**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
nds.plot(rows, metric="accuracy", path="tree_space.png") # static PNG/PDF
|
|
114
|
+
nds.plot_interactive(rows, metric="accuracy", path="tree_space.html") # pan/zoom/hover
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Both color points by `metric` and mark the best tree with a black x. Needs
|
|
118
|
+
`ndscape[plot]`.
|
|
119
|
+
|
|
120
|
+
**The embedding is slow to recompute and you want to reuse it.**
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
rows = nds.analyze(X_train, y_train, classes, cache="embedding.joblib")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The MDS step in `embed_trees`/`analyze` is the slow part. Pass `cache=` a
|
|
127
|
+
`.joblib` path: the first call computes the embedding and saves it there,
|
|
128
|
+
later calls with the same path just load it.
|
|
129
|
+
|
|
99
130
|
A `tree` is a list of `(left, right)` tuples of class labels, e.g.
|
|
100
131
|
`[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
|
|
101
132
|
(exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
|
ndscape-0.1.2/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# ndscape: Nested Dichotomy Tree-Space Embedding
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/ndscape/)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.20456943)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
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.
|
|
12
|
+
|
|
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 outputs.
|
|
14
|
+
|
|
15
|
+
Interactive maps from the paper: [res-lucid.github.io/ndscape-plots](https://res-lucid.github.io/ndscape-plots/)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
For package use:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ndscape
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To reproduce the paper outputs, clone the repository and install the pinned requirements:
|
|
26
|
+
```bash
|
|
27
|
+
python -m venv venv
|
|
28
|
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
29
|
+
pip install -r requirements.txt
|
|
30
|
+
```
|
|
31
|
+
|
|
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
|
|
46
|
+
|
|
47
|
+
Download the Zenodo deposit, then regenerate the paper figures and tables without re-running the simulations:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python scripts/download_zenodo.py
|
|
51
|
+
python reproduce.py --paper
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
<details>
|
|
55
|
+
<summary>Scripts run by <code>python reproduce.py --paper</code></summary>
|
|
56
|
+
|
|
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.
|
|
65
|
+
|
|
66
|
+
Output is saved to `paper_outputs/figures/` and `paper_outputs/tables/`.
|
|
67
|
+
</details>
|
|
68
|
+
|
|
69
|
+
## Full reproduction pipeline
|
|
70
|
+
|
|
71
|
+
This rebuilds the derived outputs locally, using the archived data and configuration files downloaded into `zenodo/`.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python reproduce.py
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Step | Name | Script | Output |
|
|
78
|
+
|------|------|--------|--------|
|
|
79
|
+
| 0 | `download` | `scripts/download_zenodo.py` | `zenodo/` (artifacts, configs, tree\_metrics, simulation\_scores, data, boot results) |
|
|
80
|
+
| 1 | `artifacts` | `scripts/make_artifacts.py --all` | `cache_nd/*.joblib` |
|
|
81
|
+
| 2 | `score` | `run_datasets.py` | `real-data-bias-analysis/<dataset>/v_<dataset>_lr_cluster_tables.xlsx` |
|
|
82
|
+
| 3 | `sim` | `run_simulations.py` | `bias-analysis/raw_simulation_results/` (LR) and `bias-analysis/raw_simulation_results_dt/` (DT) |
|
|
83
|
+
| 4 | `moran` | `scripts/moran_simulation_analysis.py` | `zenodo/simulation_scores/` (LR) and `zenodo/simulation_scores_dt/` (DT) |
|
|
84
|
+
| 5 | `tables` | `scripts/make_paper_tables.py --all` | `paper_outputs/tables/*.tex` |
|
|
85
|
+
| 6 | `plots` | `scripts/make_dataset_plots.py`, `scripts/make_simulation_plots.py` | `paper_outputs/figures/` |
|
|
86
|
+
|
|
87
|
+
Run individual steps or subsets:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
python reproduce.py --steps artifacts
|
|
91
|
+
python reproduce.py --steps score --bootstrap 20 # quick test with fewer bootstrap reps
|
|
92
|
+
python reproduce.py --steps sim moran --classes 6 7 # 6- and 7-class only
|
|
93
|
+
python reproduce.py --steps plots
|
|
94
|
+
```
|
|
95
|
+
|
|
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.
|
|
97
|
+
|
|
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
|
+
```
|
|
105
|
+
|
|
106
|
+
Output goes to `real-data-bias-analysis/my_dataset/v_my_dataset_lr_cluster_tables.xlsx`.
|
|
107
|
+
|
|
108
|
+
## Tests
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pytest tests/ # unit + snapshot tests (fast)
|
|
112
|
+
pytest tests/ -m integration # integration tests (slow and requires pre-built C=6,7 artifacts)
|
|
113
|
+
```
|
|
114
|
+
|
|
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.
|
|
116
|
+
|
|
117
|
+
## Datasets
|
|
118
|
+
|
|
119
|
+
The six datasets are from the UCI Machine Learning Repository (CC BY 4.0) and are archived on Zenodo. Download:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
python scripts/download_zenodo.py --what rawdata
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Or place CSV files (with a `y` column for the class label) in `zenodo/data/`:
|
|
126
|
+
|
|
127
|
+
| File | Classes | Trees evaluated |
|
|
128
|
+
|------|---------|-----------------|
|
|
129
|
+
| `glass_identification.csv` | 6 | 945 (exhaustive) |
|
|
130
|
+
| `steel_plates_faults.csv` | 7 | 10 395 (exhaustive) |
|
|
131
|
+
| `mice_protein.csv` | 8 | 15 000 (sampled) |
|
|
132
|
+
| `urban_land_cover.csv` | 9 | 15 000 (sampled) |
|
|
133
|
+
| `pen_based_recognition_of_handwritten_digits_81.csv` | 10 | 15 000 (sampled) |
|
|
134
|
+
| `soybean_large_122.csv` | 19 | 15 000 (sampled) |
|
|
135
|
+
|
|
136
|
+
### Data sources
|
|
137
|
+
|
|
138
|
+
| Dataset | UCI URL |
|
|
139
|
+
|---------|---------|
|
|
140
|
+
| Glass Identification | https://archive.ics.uci.edu/dataset/42 |
|
|
141
|
+
| Steel Plates Faults | https://archive.ics.uci.edu/dataset/198 |
|
|
142
|
+
| Mice Protein Expression | https://archive.ics.uci.edu/dataset/342 |
|
|
143
|
+
| Urban Land Cover | https://archive.ics.uci.edu/dataset/295 |
|
|
144
|
+
| Pen-Based Digit Recognition | https://archive.ics.uci.edu/dataset/81 |
|
|
145
|
+
| Soybean (Large) | https://archive.ics.uci.edu/dataset/90 |
|
|
146
|
+
|
|
147
|
+
## Key files
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
reproduce.py Main reproduction entry point
|
|
151
|
+
core.py Tree enumeration, embedding, models, artifact building
|
|
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
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Zenodo
|
|
164
|
+
|
|
165
|
+
Pre-built data is archived on Zenodo (doi:10.5281/zenodo.20456943). The deposit maps to the local `zenodo/` directory:
|
|
166
|
+
|
|
167
|
+
| Zenodo path | Contents | Download group |
|
|
168
|
+
|-------------|----------|----------------|
|
|
169
|
+
| `*.joblib` | Embedding artifacts for all class counts + ablations | `artifacts` |
|
|
170
|
+
| `simulation_scores/` | LR Moran summary CSVs (one per experiment) | `simulation_scores` |
|
|
171
|
+
| `simulation_scores_dt/` | DT Moran summary CSVs (one per experiment) | `simulation_scores_dt` |
|
|
172
|
+
| `tree_metrics/` | Per-tree scores for all 6 real datasets | `tree_metrics` |
|
|
173
|
+
| `raw_simulation_results/` | LR bootstrap CSVs (415 files, ~1 GB) | `boot` |
|
|
174
|
+
| `raw_simulation_results_dt/` | DT bootstrap CSVs (150 files) | `boot_dt` |
|
|
175
|
+
| `data/` | Raw UCI dataset CSVs | `rawdata` |
|
|
176
|
+
| `simulation_configs.csv` | 415 LR-generator simulation configurations | `sims` |
|
|
177
|
+
| `simulation_configs_dt.csv` | Decision-tree generator simulation configurations | `sims_dt` |
|
|
178
|
+
|
|
179
|
+
Download everything (all groups included by default):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
python scripts/download_zenodo.py
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Download specific groups:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
python scripts/download_zenodo.py --what artifacts
|
|
189
|
+
python scripts/download_zenodo.py --what rawdata
|
|
190
|
+
python scripts/download_zenodo.py --what tree_metrics
|
|
191
|
+
python scripts/download_zenodo.py --what simulation_scores
|
|
192
|
+
python scripts/download_zenodo.py --what simulation_scores_dt
|
|
193
|
+
python scripts/download_zenodo.py --what sims
|
|
194
|
+
python scripts/download_zenodo.py --what sims_dt
|
|
195
|
+
python scripts/download_zenodo.py --what boot
|
|
196
|
+
python scripts/download_zenodo.py --what boot_dt
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
See `zenodo/manifest.csv` for a full listing of every file and what uses it.
|
|
200
|
+
|
|
201
|
+
## Simulation input
|
|
202
|
+
|
|
203
|
+
Steps 3 and 4 read from `zenodo/simulation_configs.csv` (LR-generator configs) and, if present, `zenodo/simulation_configs_dt.csv` (decision-tree generator configs). Both are processed automatically:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
python reproduce.py --steps sim moran # runs LR and DT sims if configs present
|
|
207
|
+
python reproduce.py --steps sim --classes 6 7 # 6- and 7-class only
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
LR boot results land in `bias-analysis/raw_simulation_results/` and Moran scores in `zenodo/simulation_scores/`.
|
|
211
|
+
DT boot results land in `bias-analysis/raw_simulation_results_dt/` and Moran scores in `zenodo/simulation_scores_dt/`.
|
|
212
|
+
|
|
213
|
+
To convert the LR CSV to JSON:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
python scripts/csv_to_configs.py --output simulation_configs.json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`zenodo/simulation_configs_dt.csv` is committed as a static artifact and is not regenerated as part of this pipeline.
|
|
220
|
+
|
|
221
|
+
## Artifacts (cache_nd/)
|
|
222
|
+
|
|
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:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
python scripts/download_zenodo.py --what artifacts
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
To rebuild from scratch (re-runs MDS, requires pinned package versions):
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
python scripts/make_artifacts.py --all
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Artifact specs (class count, tree count, seed) are declared in `configs/artifacts.yml`.
|
|
236
|
+
Use `scripts/verify_artifacts.py` to check a built artifact against its spec.
|
|
237
|
+
|
|
238
|
+
## Citation
|
|
239
|
+
|
|
240
|
+
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).
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ndscape
|
|
3
|
-
Version: 0.1.0
|
|
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
|
-
Requires-Dist: numpy>=1.24
|
|
15
|
-
Requires-Dist: scikit-learn>=1.3
|
|
16
|
-
Provides-Extra: spatial
|
|
17
|
-
Requires-Dist: esda>=2.6; extra == "spatial"
|
|
18
|
-
Requires-Dist: libpysal>=4.10; extra == "spatial"
|
|
19
|
-
|
|
20
1
|
# ndscape
|
|
21
2
|
|
|
22
3
|
Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
|
|
@@ -32,6 +13,7 @@ tree structures.
|
|
|
32
13
|
```
|
|
33
14
|
pip install ndscape
|
|
34
15
|
pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
|
|
16
|
+
pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
|
|
35
17
|
```
|
|
36
18
|
|
|
37
19
|
## Quickstart
|
|
@@ -96,6 +78,26 @@ rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base
|
|
|
96
78
|
# [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
|
|
97
79
|
```
|
|
98
80
|
|
|
81
|
+
**You want a picture of that tree-space.**
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
nds.plot(rows, metric="accuracy", path="tree_space.png") # static PNG/PDF
|
|
85
|
+
nds.plot_interactive(rows, metric="accuracy", path="tree_space.html") # pan/zoom/hover
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Both color points by `metric` and mark the best tree with a black x. Needs
|
|
89
|
+
`ndscape[plot]`.
|
|
90
|
+
|
|
91
|
+
**The embedding is slow to recompute and you want to reuse it.**
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
rows = nds.analyze(X_train, y_train, classes, cache="embedding.joblib")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The MDS step in `embed_trees`/`analyze` is the slow part. Pass `cache=` a
|
|
98
|
+
`.joblib` path: the first call computes the embedding and saves it there,
|
|
99
|
+
later calls with the same path just load it.
|
|
100
|
+
|
|
99
101
|
A `tree` is a list of `(left, right)` tuples of class labels, e.g.
|
|
100
102
|
`[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
|
|
101
103
|
(exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
|
|
@@ -9,6 +9,7 @@ from .api import (
|
|
|
9
9
|
sample_trees,
|
|
10
10
|
spatial_autocorrelation,
|
|
11
11
|
)
|
|
12
|
+
from .plot import plot, plot_interactive
|
|
12
13
|
|
|
13
14
|
__all__ = [
|
|
14
15
|
"ND",
|
|
@@ -18,4 +19,6 @@ __all__ = [
|
|
|
18
19
|
"fit",
|
|
19
20
|
"sample_trees",
|
|
20
21
|
"spatial_autocorrelation",
|
|
22
|
+
"plot",
|
|
23
|
+
"plot_interactive",
|
|
21
24
|
]
|
|
@@ -11,7 +11,9 @@ read top-down: first split {0,1} vs {2,3}, then split each side further.
|
|
|
11
11
|
|
|
12
12
|
from itertools import combinations
|
|
13
13
|
from math import comb, sqrt
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
|
|
16
|
+
import joblib
|
|
15
17
|
import numpy as np
|
|
16
18
|
from sklearn.base import clone
|
|
17
19
|
from sklearn.feature_extraction.text import TfidfVectorizer
|
|
@@ -33,7 +35,7 @@ def _get_model(base):
|
|
|
33
35
|
if isinstance(base, str):
|
|
34
36
|
if base == "lr":
|
|
35
37
|
return LogisticRegression(
|
|
36
|
-
|
|
38
|
+
solver="newton-cholesky", C=0.1, max_iter=2000
|
|
37
39
|
)
|
|
38
40
|
if base in ("decisiontree", "dt"):
|
|
39
41
|
return DecisionTreeClassifier(
|
|
@@ -298,13 +300,20 @@ def _split_analyzer(n_classes):
|
|
|
298
300
|
return _analyze
|
|
299
301
|
|
|
300
302
|
|
|
301
|
-
def embed_trees(classes, trees=None, N=2000, seed=0, dim=2):
|
|
303
|
+
def embed_trees(classes, trees=None, N=2000, seed=0, dim=2, cache=None):
|
|
302
304
|
"""Place a set of ND trees in `dim`-D tree-space (TF-IDF + cosine distance + MDS).
|
|
303
305
|
|
|
304
306
|
If `trees` is omitted, all trees are used for <=7 classes and a balanced
|
|
305
307
|
sample of N otherwise. Returns (trees, coords); coords[i] is the
|
|
306
308
|
embedding of trees[i].
|
|
309
|
+
|
|
310
|
+
`cache` is an optional .joblib path. If it exists, (trees, coords) are
|
|
311
|
+
loaded from it instead of recomputed (the MDS step is the slow part).
|
|
312
|
+
Otherwise they're computed as usual and saved there for next time.
|
|
307
313
|
"""
|
|
314
|
+
if cache is not None and Path(cache).exists():
|
|
315
|
+
return joblib.load(cache)
|
|
316
|
+
|
|
308
317
|
classes = list(classes)
|
|
309
318
|
n = len(classes)
|
|
310
319
|
if trees is None:
|
|
@@ -321,6 +330,9 @@ def embed_trees(classes, trees=None, N=2000, seed=0, dim=2):
|
|
|
321
330
|
random_state=seed, n_init=1, max_iter=1000, eps=1e-6, n_jobs=-1,
|
|
322
331
|
)
|
|
323
332
|
coords = mds.fit_transform(dist)
|
|
333
|
+
|
|
334
|
+
if cache is not None:
|
|
335
|
+
joblib.dump((trees, coords), cache)
|
|
324
336
|
return trees, coords
|
|
325
337
|
|
|
326
338
|
|
|
@@ -348,17 +360,20 @@ def spatial_autocorrelation(values, coords, k=50, permutations=999):
|
|
|
348
360
|
return {"I": mi.I, "p_sim": mi.p_sim}
|
|
349
361
|
|
|
350
362
|
|
|
351
|
-
def analyze(X, y, classes, X_test=None, y_test=None, base="lr", N=2000, seed=0):
|
|
363
|
+
def analyze(X, y, classes, X_test=None, y_test=None, base="lr", N=2000, seed=0, cache=None):
|
|
352
364
|
"""Fit every candidate ND tree on (X, y), score it, and place it in tree-space.
|
|
353
365
|
|
|
354
366
|
If X_test/y_test are omitted, scores are computed on (X, y). Returns a
|
|
355
367
|
list of dicts: tree, accuracy, logloss, coord.
|
|
368
|
+
|
|
369
|
+
`cache` is an optional .joblib path for the embedding (see `embed_trees`)
|
|
370
|
+
— reuse it across calls so the MDS step doesn't rerun every time.
|
|
356
371
|
"""
|
|
357
372
|
classes = list(classes)
|
|
358
373
|
Xte = X if X_test is None else X_test
|
|
359
374
|
yte = y if y_test is None else y_test
|
|
360
375
|
|
|
361
|
-
trees, coords = embed_trees(classes, N=N, seed=seed)
|
|
376
|
+
trees, coords = embed_trees(classes, N=N, seed=seed, cache=cache)
|
|
362
377
|
rows = []
|
|
363
378
|
for tree, coord in zip(trees, coords):
|
|
364
379
|
nd = ND(tree, classes).fit(X, y, base=base)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Plot a tree-space embedding (the kind of figure used in the paper).
|
|
2
|
+
|
|
3
|
+
Both functions take `rows`, the list of dicts returned by `ndscape.analyze()`
|
|
4
|
+
(each needs "coord" and the metric you're plotting). Needs matplotlib for
|
|
5
|
+
`plot`, and bokeh as well for `plot_interactive`:
|
|
6
|
+
|
|
7
|
+
pip install ndscape[plot]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from matplotlib.colors import LinearSegmentedColormap
|
|
12
|
+
|
|
13
|
+
# red -> yellow -> green, same palette used for the paper's accuracy/variance plots
|
|
14
|
+
BRIGHT = LinearSegmentedColormap.from_list("bright_RYG", ["#ff2d2d", "#fff176", "#00e676"], N=100)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def plot(rows, metric="accuracy", path=None, ax=None):
|
|
18
|
+
"""Static scatter of the embedding, colored by `metric`. Marks the best tree with a black x.
|
|
19
|
+
|
|
20
|
+
Pass `path` to save a PNG/PDF, or `ax` to draw onto an existing matplotlib axes.
|
|
21
|
+
Returns (fig, ax).
|
|
22
|
+
"""
|
|
23
|
+
import matplotlib.pyplot as plt
|
|
24
|
+
|
|
25
|
+
coords = np.array([r["coord"] for r in rows])
|
|
26
|
+
values = np.array([r[metric] for r in rows], dtype=float)
|
|
27
|
+
best = int(np.argmin(values)) if metric == "logloss" else int(np.argmax(values))
|
|
28
|
+
|
|
29
|
+
if ax is None:
|
|
30
|
+
fig, ax = plt.subplots(figsize=(8.0, 5.2))
|
|
31
|
+
else:
|
|
32
|
+
fig = ax.figure
|
|
33
|
+
|
|
34
|
+
sc = ax.scatter(coords[:, 0], coords[:, 1], c=values, cmap=BRIGHT, s=30, linewidths=0)
|
|
35
|
+
fig.colorbar(sc, ax=ax, fraction=0.046, pad=0.02).set_label(metric)
|
|
36
|
+
ax.scatter(coords[best, 0], coords[best, 1], marker="x", s=220, c="black", linewidths=2.2, zorder=12)
|
|
37
|
+
ax.set_xlabel("MDS-1")
|
|
38
|
+
ax.set_ylabel("MDS-2")
|
|
39
|
+
ax.grid(True, lw=0.3, alpha=0.4)
|
|
40
|
+
fig.tight_layout()
|
|
41
|
+
|
|
42
|
+
if path is not None:
|
|
43
|
+
fig.savefig(path, dpi=300, bbox_inches="tight")
|
|
44
|
+
return fig, ax
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def plot_interactive(rows, metric="accuracy", path="tree_space.html"):
|
|
48
|
+
"""Same plot as `plot`, but as an HTML page with pan/zoom/hover (accuracy + logloss).
|
|
49
|
+
|
|
50
|
+
Needs bokeh: pip install ndscape[plot]. Returns the bokeh figure.
|
|
51
|
+
"""
|
|
52
|
+
from bokeh.models import ColorBar, ColumnDataSource, HoverTool, LinearColorMapper
|
|
53
|
+
from bokeh.plotting import figure, output_file, save
|
|
54
|
+
from matplotlib.colors import to_hex
|
|
55
|
+
|
|
56
|
+
coords = np.array([r["coord"] for r in rows])
|
|
57
|
+
values = np.array([r[metric] for r in rows], dtype=float)
|
|
58
|
+
palette = [to_hex(BRIGHT(i / 99)) for i in range(100)]
|
|
59
|
+
mapper = LinearColorMapper(palette=palette, low=float(values.min()), high=float(values.max()))
|
|
60
|
+
|
|
61
|
+
source = ColumnDataSource(dict(
|
|
62
|
+
x=coords[:, 0], y=coords[:, 1], value=values,
|
|
63
|
+
accuracy=[r["accuracy"] for r in rows],
|
|
64
|
+
logloss=[r["logloss"] for r in rows],
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
p = figure(width=800, height=520, x_axis_label="MDS-1", y_axis_label="MDS-2",
|
|
68
|
+
tools="pan,wheel_zoom,box_zoom,reset,save")
|
|
69
|
+
points = p.scatter("x", "y", source=source, size=6, line_color="black", line_width=0.4,
|
|
70
|
+
fill_color={"field": "value", "transform": mapper}, fill_alpha=0.9)
|
|
71
|
+
p.add_tools(HoverTool(renderers=[points], tooltips=[
|
|
72
|
+
("accuracy", "@accuracy{0.000}"), ("logloss", "@logloss{0.000}"),
|
|
73
|
+
]))
|
|
74
|
+
p.add_layout(ColorBar(color_mapper=mapper, label_standoff=8, title=metric), "right")
|
|
75
|
+
|
|
76
|
+
output_file(path)
|
|
77
|
+
save(p)
|
|
78
|
+
return p
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ndscape
|
|
3
|
+
Version: 0.1.2
|
|
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: Homepage, https://github.com/res-lucid/ndscape
|
|
8
|
+
Project-URL: Repository, https://github.com/res-lucid/ndscape
|
|
9
|
+
Project-URL: Issues, https://github.com/res-lucid/ndscape/issues
|
|
10
|
+
Project-URL: Interactive maps, https://res-lucid.github.io/ndscape-plots/
|
|
11
|
+
Project-URL: Data archive, https://doi.org/10.5281/zenodo.20456943
|
|
12
|
+
Keywords: nested-dichotomy,multi-class,classification,scikit-learn
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Requires-Dist: scikit-learn>=1.3
|
|
21
|
+
Requires-Dist: joblib>=1.2
|
|
22
|
+
Provides-Extra: spatial
|
|
23
|
+
Requires-Dist: esda>=2.6; extra == "spatial"
|
|
24
|
+
Requires-Dist: libpysal>=4.10; extra == "spatial"
|
|
25
|
+
Provides-Extra: plot
|
|
26
|
+
Requires-Dist: matplotlib>=3.5; extra == "plot"
|
|
27
|
+
Requires-Dist: bokeh>=3.0; extra == "plot"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
1
30
|
# ndscape
|
|
2
31
|
|
|
3
32
|
Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
|
|
@@ -13,6 +42,7 @@ tree structures.
|
|
|
13
42
|
```
|
|
14
43
|
pip install ndscape
|
|
15
44
|
pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
|
|
45
|
+
pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
|
|
16
46
|
```
|
|
17
47
|
|
|
18
48
|
## Quickstart
|
|
@@ -77,6 +107,26 @@ rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base
|
|
|
77
107
|
# [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
|
|
78
108
|
```
|
|
79
109
|
|
|
110
|
+
**You want a picture of that tree-space.**
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
nds.plot(rows, metric="accuracy", path="tree_space.png") # static PNG/PDF
|
|
114
|
+
nds.plot_interactive(rows, metric="accuracy", path="tree_space.html") # pan/zoom/hover
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Both color points by `metric` and mark the best tree with a black x. Needs
|
|
118
|
+
`ndscape[plot]`.
|
|
119
|
+
|
|
120
|
+
**The embedding is slow to recompute and you want to reuse it.**
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
rows = nds.analyze(X_train, y_train, classes, cache="embedding.joblib")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The MDS step in `embed_trees`/`analyze` is the slow part. Pass `cache=` a
|
|
127
|
+
`.joblib` path: the first call computes the embedding and saves it there,
|
|
128
|
+
later calls with the same path just load it.
|
|
129
|
+
|
|
80
130
|
A `tree` is a list of `(left, right)` tuples of class labels, e.g.
|
|
81
131
|
`[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
|
|
82
132
|
(exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ndscape"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification."
|
|
9
9
|
readme = "ndscape/README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -21,13 +21,19 @@ classifiers = [
|
|
|
21
21
|
dependencies = [
|
|
22
22
|
"numpy>=1.24",
|
|
23
23
|
"scikit-learn>=1.3",
|
|
24
|
+
"joblib>=1.2",
|
|
24
25
|
]
|
|
25
26
|
|
|
26
27
|
[project.optional-dependencies]
|
|
27
28
|
spatial = ["esda>=2.6", "libpysal>=4.10"]
|
|
29
|
+
plot = ["matplotlib>=3.5", "bokeh>=3.0"]
|
|
28
30
|
|
|
29
31
|
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/res-lucid/ndscape"
|
|
30
33
|
Repository = "https://github.com/res-lucid/ndscape"
|
|
34
|
+
Issues = "https://github.com/res-lucid/ndscape/issues"
|
|
35
|
+
"Interactive maps" = "https://res-lucid.github.io/ndscape-plots/"
|
|
36
|
+
"Data archive" = "https://doi.org/10.5281/zenodo.20456943"
|
|
31
37
|
|
|
32
38
|
[tool.setuptools.packages.find]
|
|
33
39
|
include = ["ndscape*"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|