ndscape 0.1.1__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.
@@ -0,0 +1 @@
1
+ prune tests
@@ -1,132 +1,136 @@
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.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
+
30
+ # ndscape
31
+
32
+ Fit, score, and embed nested-dichotomy (ND) trees for multi-class classification.
33
+
34
+ A nested dichotomy reduces a C-class problem to a tree of binary splits
35
+ (e.g. {0,1,2} vs {3,4}, then {0} vs {1,2}, ...). ndscape lets you fit one,
36
+ score it, or place a whole population of candidate trees in a 2-D
37
+ "tree-space" to see how a property (accuracy, variance, ...) varies across
38
+ tree structures.
39
+
40
+ ## Install
41
+
42
+ ```
43
+ pip install ndscape
44
+ pip install ndscape[spatial] # adds Moran's I support (esda, libpysal)
45
+ pip install ndscape[plot] # adds plotting (matplotlib, bokeh)
46
+ ```
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from sklearn.datasets import load_iris
52
+ from sklearn.model_selection import train_test_split
53
+ import ndscape as nds
54
+
55
+ X, y = load_iris(return_X_y=True)
56
+ classes = sorted(set(y))
57
+ X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
58
+
59
+ nd = nds.fit(X_train, y_train, classes=classes, base="lr")
60
+ nd.predict(X_test)
61
+ nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
62
+ ```
63
+
64
+ `classes` is the list of class labels in your `y` (`sorted(set(y))` works for
65
+ integer or string labels). `nds.fit` samples one ND tree automatically; pass
66
+ `tree=...` to use a specific one (see "A `tree` is..." below).
67
+
68
+ ## Use cases
69
+
70
+ **You have a dataset and a binary classifier.**
71
+
72
+ `base` can be the string `"lr"` or `"decisiontree"`, or your own unfitted
73
+ scikit-learn estimator — a fresh clone of it is fit at every split.
74
+
75
+ ```python
76
+ from sklearn.svm import SVC
77
+
78
+ nd = nds.fit(X_train, y_train, classes=classes, base=SVC(probability=True, kernel="linear"))
79
+ ```
80
+
81
+ **You have a train/test split and want a score.**
82
+
83
+ ```python
84
+ nd = nds.ND(tree, classes).fit(X_train, y_train, base="lr")
85
+ nd.score(X_test, y_test) # {"accuracy": ..., "logloss": ...}
86
+ ```
87
+
88
+ **You already trained the per-split models yourself.**
89
+
90
+ ```python
91
+ # models in the same order as tree, or a {(left, right): model} dict — either works
92
+ nd = nds.ND.from_trained(tree, classes, models=[fitted_model_1, fitted_model_2, ...])
93
+ nd.predict_proba(X_test)
94
+ ```
95
+
96
+ **You already scored a set of trees and want to see where they sit in tree-space.**
97
+
98
+ ```python
99
+ trees, coords = nds.embed_trees(classes)
100
+ nds.spatial_autocorrelation(my_scores, coords) # {"I": ..., "p_sim": ...}
101
+ ```
102
+
103
+ **You just want the whole picture: fit, score, and embed every candidate tree.**
104
+
105
+ ```python
106
+ rows = nds.analyze(X_train, y_train, classes, X_test=X_test, y_test=y_test, base="lr")
107
+ # [{"tree": ..., "accuracy": ..., "logloss": ..., "coord": array([...])}, ...]
108
+ ```
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
+
130
+ A `tree` is a list of `(left, right)` tuples of class labels, e.g.
131
+ `[((0, 1), (2, 3)), ((0,), (1,)), ((2,), (3,))]`. Use `nds.all_trees(classes)`
132
+ (exhaustive, for small C) or `nds.sample_trees(classes, N)` (for larger C)
133
+ to generate candidates.
134
+
135
+ `base` accepts `"lr"`, `"decisiontree"`, or any unfitted scikit-learn
136
+ 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 outputs.
12
14
 
13
- ## Quick check (~30 s)
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:
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
71
99
 
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.
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`.
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,7 @@ 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
+ 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).
@@ -35,7 +35,7 @@ def _get_model(base):
35
35
  if isinstance(base, str):
36
36
  if base == "lr":
37
37
  return LogisticRegression(
38
- penalty="l2", solver="newton-cholesky", C=0.1, max_iter=2000
38
+ solver="newton-cholesky", C=0.1, max_iter=2000
39
39
  )
40
40
  if base in ("decisiontree", "dt"):
41
41
  return DecisionTreeClassifier(