decoyfs 0.1.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.
- decoyfs-0.1.0/LICENSE +21 -0
- decoyfs-0.1.0/PKG-INFO +161 -0
- decoyfs-0.1.0/README.md +120 -0
- decoyfs-0.1.0/pyproject.toml +68 -0
- decoyfs-0.1.0/setup.cfg +4 -0
- decoyfs-0.1.0/src/decoyfs/__init__.py +25 -0
- decoyfs-0.1.0/src/decoyfs/_base.py +82 -0
- decoyfs-0.1.0/src/decoyfs/bench/__init__.py +4 -0
- decoyfs-0.1.0/src/decoyfs/bench/data.py +258 -0
- decoyfs-0.1.0/src/decoyfs/bench/models.py +225 -0
- decoyfs-0.1.0/src/decoyfs/bench/plots.py +42 -0
- decoyfs-0.1.0/src/decoyfs/bench/presets.py +21 -0
- decoyfs-0.1.0/src/decoyfs/bench/progress.py +237 -0
- decoyfs-0.1.0/src/decoyfs/bench/runner.py +240 -0
- decoyfs-0.1.0/src/decoyfs/bench/stats.py +282 -0
- decoyfs-0.1.0/src/decoyfs/comparators.py +127 -0
- decoyfs-0.1.0/src/decoyfs/compare.py +60 -0
- decoyfs-0.1.0/src/decoyfs/controls.py +102 -0
- decoyfs-0.1.0/src/decoyfs/decoy.py +200 -0
- decoyfs-0.1.0/src/decoyfs/dependence.py +93 -0
- decoyfs-0.1.0/src/decoyfs/device.py +68 -0
- decoyfs-0.1.0/src/decoyfs/metrics.py +46 -0
- decoyfs-0.1.0/src/decoyfs/policies.py +248 -0
- decoyfs-0.1.0/src/decoyfs/preprocessing.py +42 -0
- decoyfs-0.1.0/src/decoyfs/py.typed +0 -0
- decoyfs-0.1.0/src/decoyfs/rankers.py +211 -0
- decoyfs-0.1.0/src/decoyfs/selectors.py +94 -0
- decoyfs-0.1.0/src/decoyfs/validation.py +97 -0
- decoyfs-0.1.0/src/decoyfs.egg-info/PKG-INFO +161 -0
- decoyfs-0.1.0/src/decoyfs.egg-info/SOURCES.txt +32 -0
- decoyfs-0.1.0/src/decoyfs.egg-info/dependency_links.txt +1 -0
- decoyfs-0.1.0/src/decoyfs.egg-info/requires.txt +22 -0
- decoyfs-0.1.0/src/decoyfs.egg-info/top_level.txt +1 -0
- decoyfs-0.1.0/tests/test_decoyfs.py +221 -0
decoyfs-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nestor A. Zermeño Campos
|
|
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.
|
decoyfs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: decoyfs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Decoy-referenced feature selection: Random and Shadow control features, swappable rankers and auditable vote policies.
|
|
5
|
+
Author-email: "Nestor A. Zermeño Campos" <nestor.zermeno@uabc.edu.mx>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/alexanderzrm/decoyfs
|
|
8
|
+
Project-URL: Issues, https://github.com/alexanderzrm/decoyfs/issues
|
|
9
|
+
Keywords: feature selection,decoy features,shadow features,random probes,boruta,scikit-learn,tabular
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpy<3,>=1.26
|
|
22
|
+
Requires-Dist: scipy<2,>=1.11
|
|
23
|
+
Requires-Dist: pandas<3,>=2.0
|
|
24
|
+
Requires-Dist: scikit-learn<2,>=1.4
|
|
25
|
+
Requires-Dist: xgboost<4,>=2.0
|
|
26
|
+
Requires-Dist: torch>=2.2
|
|
27
|
+
Provides-Extra: bench
|
|
28
|
+
Requires-Dist: statsmodels>=0.14; extra == "bench"
|
|
29
|
+
Requires-Dist: matplotlib>=3.8; extra == "bench"
|
|
30
|
+
Requires-Dist: openml>=0.14; extra == "bench"
|
|
31
|
+
Requires-Dist: nbformat>=5.9; extra == "bench"
|
|
32
|
+
Requires-Dist: ipykernel>=6.29; extra == "bench"
|
|
33
|
+
Provides-Extra: bench-extra
|
|
34
|
+
Requires-Dist: pytabkit>=1.5; extra == "bench-extra"
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
37
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
38
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
39
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# DecoyFS
|
|
43
|
+
|
|
44
|
+
**Decoy-referenced feature selection** for tabular classification. DecoyFS keeps a feature only if it beats *decoys* — columns whose relationship with the target is known to be null — according to several heterogeneous rankers. The result is a small, auditable subset: for every feature you can see which rankers voted for it and how it compared with the decoys.
|
|
45
|
+
|
|
46
|
+
* **Random probes**: pure-noise columns. A feature must outscore the best probe.
|
|
47
|
+
* **Shadow features**: each real column independently permuted. A feature must outscore the `1 - alpha` quantile of its shadows.
|
|
48
|
+
* **Heterogeneous rankers** (ANOVA, chi², mutual information, ReliefF, random forest, XGBoost gain, L1 logistic regression) vote; a **policy** (`all`, `majority`, `any`, an integer or your own function) turns the votes into the subset.
|
|
49
|
+
* **Every block is swappable**: add or remove rankers, controls and policies. Boruta-style, CV-k and fixed-k comparators are bundled so you can compare on your own data.
|
|
50
|
+
|
|
51
|
+
> Honest scope. In our benchmark (74 datasets, 5×2 CV, 4 downstream models) DecoyFS is **non-inferior (margin 0.01 AUROC) to using all features and to the comparators**, keeps roughly half of the features (median), is **~10× cheaper than Boruta-style selection** (median time ratio), and is **auditable and tunable** (`policy` moves you along the precision–recall frontier). It does **not** beat Boruta in false-discovery proportion or parsimony; the empirical null-pass rate it reports is a proxy, not a formal FDR guarantee. See the paper for details.
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install decoyfs # core (numpy, scipy, pandas, scikit-learn, xgboost, torch)
|
|
57
|
+
pip install "decoyfs[bench]" # + benchmark harness (OpenML, statsmodels, matplotlib, jupyter)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Python ≥ 3.10. A GPU is optional: if `torch` / `xgboost` can use CUDA they will, otherwise everything runs on CPU. For exactly pinned versions use `benchmarks/requirements-lock.txt` (tested on Python 3.11).
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from decoyfs import DecoySelector
|
|
66
|
+
|
|
67
|
+
sel = DecoySelector(policy="majority", alpha=0.05, random_state=0).fit(X_train, y_train)
|
|
68
|
+
X_small = sel.transform(X_test)
|
|
69
|
+
|
|
70
|
+
sel.selected_features_ # names (DataFrame input) or column indices
|
|
71
|
+
sel.report_ # per-feature audit trail: votes, beat_random / beat_shadow fractions, per-ranker scores
|
|
72
|
+
sel.decoy_thresholds_ # the bar each ranker set (Random max, Shadow quantile)
|
|
73
|
+
sel.null_pass_rate_ # fraction of decoys that would have been selected (empirical FPR proxy)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It is a scikit-learn transformer, so it works in a `Pipeline`:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from sklearn.pipeline import make_pipeline
|
|
80
|
+
from sklearn.linear_model import LogisticRegression
|
|
81
|
+
model = make_pipeline(DecoySelector(), LogisticRegression(max_iter=500)).fit(X_train, y_train)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Choose your trade-off
|
|
85
|
+
|
|
86
|
+
| `policy` | Rule | Use when |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `"all"` | every ranker must beat the decoys | precision first (few false features) |
|
|
89
|
+
| `"majority"` | at least half of the rankers (reference) | balanced default |
|
|
90
|
+
| `"any"` | at least one ranker | recall first, e.g. you must not lose a critical signal (screening, quality control) |
|
|
91
|
+
| `3` | at least 3 rankers | custom |
|
|
92
|
+
| callable | `(votes, n_rankers, consensus) -> indices` | anything else |
|
|
93
|
+
|
|
94
|
+
`stability_B=5` adds a subsampling stability filter; `controls=("random", "block")` replaces Shadow by Block-Shadow for strongly correlated features.
|
|
95
|
+
|
|
96
|
+
### Swap the pieces
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import numpy as np
|
|
100
|
+
from decoyfs import register_ranker, register_policy, DecoySelector, list_rankers
|
|
101
|
+
|
|
102
|
+
register_ranker("mean_gap", lambda X, y: np.abs(X[y == 1].mean(0) - X[y == 0].mean(0)))
|
|
103
|
+
register_policy("top10", lambda votes, M, consensus: np.argsort(-consensus)[:10])
|
|
104
|
+
|
|
105
|
+
DecoySelector(rankers=("anova", "rf", "mean_gap"), policy="top10").fit(X, y)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A ranker is any function `fn(X, y) -> one score per column` (higher = more relevant). See [docs/extending.md](docs/extending.md) and the block diagram in [docs/architecture.md](docs/architecture.md).
|
|
109
|
+
|
|
110
|
+
### Compare with other selectors on your data
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from decoyfs import DecoySelector, FixedKSelector, CVKSelector, BorutaSelector, compare_selectors
|
|
114
|
+
|
|
115
|
+
df = compare_selectors(X, y, {
|
|
116
|
+
"all": None,
|
|
117
|
+
"decoyfs": DecoySelector(),
|
|
118
|
+
"fixed25": FixedKSelector(k=0.25),
|
|
119
|
+
"cvk_1se": CVKSelector(),
|
|
120
|
+
"boruta_style": BorutaSelector(),
|
|
121
|
+
}, model="xgb", cv=5)
|
|
122
|
+
df.groupby("selector").mean(numeric_only=True)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`BorutaSelector` is a **Boruta-style re-implementation** (XGBoost gain importance, shadow features, binomial test with Bonferroni correction), the one used in the paper. It is not the `BorutaPy` package.
|
|
126
|
+
|
|
127
|
+
## Input rules
|
|
128
|
+
|
|
129
|
+
Failing early beats failing silently. DecoyFS raises `DecoyInputError` with an actionable message when:
|
|
130
|
+
|
|
131
|
+
* `X` is not numeric (encode categoricals first), is not 2-D, or contains `±inf` (NaN is allowed: median imputation is applied inside the selector);
|
|
132
|
+
* `y` is not 1-D, has missing values, looks continuous (regression is not supported), or has fewer than 2 classes or fewer than 5 samples in a class;
|
|
133
|
+
* there are fewer than 20 rows or fewer than 2 columns; feature names are duplicated;
|
|
134
|
+
* `transform` receives different columns than `fit` (count or names).
|
|
135
|
+
|
|
136
|
+
Constant columns are dropped (reported with `NaN` in `report_`). If a policy returns an empty set, the 3 top-consensus features are returned with a warning (`fallback_ = True`).
|
|
137
|
+
|
|
138
|
+
## GPU
|
|
139
|
+
|
|
140
|
+
The rankers and the downstream models in the benchmark use `torch` / `xgboost` on CUDA when available (`decoyfs.device.env_report()` shows what was detected). On NVIDIA Blackwell cards (RTX 50xx) install a CUDA 12.8 build of torch: `pip install torch --index-url https://download.pytorch.org/whl/cu128`.
|
|
141
|
+
|
|
142
|
+
## Reproduce the paper
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
git clone <this repo> && cd decoyfs
|
|
146
|
+
pip install -e ".[bench]"
|
|
147
|
+
python benchmarks/build_notebook.py # creates benchmarks/notebooks/DecoyFS_benchmark.ipynb
|
|
148
|
+
python benchmarks/run_benchmark.py --tier smoke # minutes
|
|
149
|
+
python benchmarks/run_benchmark.py --tier full --force-gpu # 74 datasets, 5x2 CV (about 23 h on an RTX 5060 Ti)
|
|
150
|
+
python benchmarks/analysis/decoy_vs_comparators.py # paired comparison vs Boruta-style and CV-k
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Datasets are downloaded from OpenML into `~/.cache/decoyfs` (override with `DECOYFS_CACHE`). The benchmark's policy identifiers (`CTRL_VOTE_MAJ`, `CTRL_STAB`, `BORUTA`, `CVK_1SE`, …) are kept as in the released result tables. The Spanish protocol design document is in [docs/DISENO_PROTOCOLO.es.md](docs/DISENO_PROTOCOLO.es.md).
|
|
154
|
+
|
|
155
|
+
## Cite
|
|
156
|
+
|
|
157
|
+
See [CITATION.cff](CITATION.cff). A software DOI is minted by Zenodo for every release.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT © Nestor A. Zermeño Campos, Universidad Autónoma de Baja California.
|
decoyfs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# DecoyFS
|
|
2
|
+
|
|
3
|
+
**Decoy-referenced feature selection** for tabular classification. DecoyFS keeps a feature only if it beats *decoys* — columns whose relationship with the target is known to be null — according to several heterogeneous rankers. The result is a small, auditable subset: for every feature you can see which rankers voted for it and how it compared with the decoys.
|
|
4
|
+
|
|
5
|
+
* **Random probes**: pure-noise columns. A feature must outscore the best probe.
|
|
6
|
+
* **Shadow features**: each real column independently permuted. A feature must outscore the `1 - alpha` quantile of its shadows.
|
|
7
|
+
* **Heterogeneous rankers** (ANOVA, chi², mutual information, ReliefF, random forest, XGBoost gain, L1 logistic regression) vote; a **policy** (`all`, `majority`, `any`, an integer or your own function) turns the votes into the subset.
|
|
8
|
+
* **Every block is swappable**: add or remove rankers, controls and policies. Boruta-style, CV-k and fixed-k comparators are bundled so you can compare on your own data.
|
|
9
|
+
|
|
10
|
+
> Honest scope. In our benchmark (74 datasets, 5×2 CV, 4 downstream models) DecoyFS is **non-inferior (margin 0.01 AUROC) to using all features and to the comparators**, keeps roughly half of the features (median), is **~10× cheaper than Boruta-style selection** (median time ratio), and is **auditable and tunable** (`policy` moves you along the precision–recall frontier). It does **not** beat Boruta in false-discovery proportion or parsimony; the empirical null-pass rate it reports is a proxy, not a formal FDR guarantee. See the paper for details.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install decoyfs # core (numpy, scipy, pandas, scikit-learn, xgboost, torch)
|
|
16
|
+
pip install "decoyfs[bench]" # + benchmark harness (OpenML, statsmodels, matplotlib, jupyter)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Python ≥ 3.10. A GPU is optional: if `torch` / `xgboost` can use CUDA they will, otherwise everything runs on CPU. For exactly pinned versions use `benchmarks/requirements-lock.txt` (tested on Python 3.11).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from decoyfs import DecoySelector
|
|
25
|
+
|
|
26
|
+
sel = DecoySelector(policy="majority", alpha=0.05, random_state=0).fit(X_train, y_train)
|
|
27
|
+
X_small = sel.transform(X_test)
|
|
28
|
+
|
|
29
|
+
sel.selected_features_ # names (DataFrame input) or column indices
|
|
30
|
+
sel.report_ # per-feature audit trail: votes, beat_random / beat_shadow fractions, per-ranker scores
|
|
31
|
+
sel.decoy_thresholds_ # the bar each ranker set (Random max, Shadow quantile)
|
|
32
|
+
sel.null_pass_rate_ # fraction of decoys that would have been selected (empirical FPR proxy)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It is a scikit-learn transformer, so it works in a `Pipeline`:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sklearn.pipeline import make_pipeline
|
|
39
|
+
from sklearn.linear_model import LogisticRegression
|
|
40
|
+
model = make_pipeline(DecoySelector(), LogisticRegression(max_iter=500)).fit(X_train, y_train)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Choose your trade-off
|
|
44
|
+
|
|
45
|
+
| `policy` | Rule | Use when |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `"all"` | every ranker must beat the decoys | precision first (few false features) |
|
|
48
|
+
| `"majority"` | at least half of the rankers (reference) | balanced default |
|
|
49
|
+
| `"any"` | at least one ranker | recall first, e.g. you must not lose a critical signal (screening, quality control) |
|
|
50
|
+
| `3` | at least 3 rankers | custom |
|
|
51
|
+
| callable | `(votes, n_rankers, consensus) -> indices` | anything else |
|
|
52
|
+
|
|
53
|
+
`stability_B=5` adds a subsampling stability filter; `controls=("random", "block")` replaces Shadow by Block-Shadow for strongly correlated features.
|
|
54
|
+
|
|
55
|
+
### Swap the pieces
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import numpy as np
|
|
59
|
+
from decoyfs import register_ranker, register_policy, DecoySelector, list_rankers
|
|
60
|
+
|
|
61
|
+
register_ranker("mean_gap", lambda X, y: np.abs(X[y == 1].mean(0) - X[y == 0].mean(0)))
|
|
62
|
+
register_policy("top10", lambda votes, M, consensus: np.argsort(-consensus)[:10])
|
|
63
|
+
|
|
64
|
+
DecoySelector(rankers=("anova", "rf", "mean_gap"), policy="top10").fit(X, y)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
A ranker is any function `fn(X, y) -> one score per column` (higher = more relevant). See [docs/extending.md](docs/extending.md) and the block diagram in [docs/architecture.md](docs/architecture.md).
|
|
68
|
+
|
|
69
|
+
### Compare with other selectors on your data
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from decoyfs import DecoySelector, FixedKSelector, CVKSelector, BorutaSelector, compare_selectors
|
|
73
|
+
|
|
74
|
+
df = compare_selectors(X, y, {
|
|
75
|
+
"all": None,
|
|
76
|
+
"decoyfs": DecoySelector(),
|
|
77
|
+
"fixed25": FixedKSelector(k=0.25),
|
|
78
|
+
"cvk_1se": CVKSelector(),
|
|
79
|
+
"boruta_style": BorutaSelector(),
|
|
80
|
+
}, model="xgb", cv=5)
|
|
81
|
+
df.groupby("selector").mean(numeric_only=True)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`BorutaSelector` is a **Boruta-style re-implementation** (XGBoost gain importance, shadow features, binomial test with Bonferroni correction), the one used in the paper. It is not the `BorutaPy` package.
|
|
85
|
+
|
|
86
|
+
## Input rules
|
|
87
|
+
|
|
88
|
+
Failing early beats failing silently. DecoyFS raises `DecoyInputError` with an actionable message when:
|
|
89
|
+
|
|
90
|
+
* `X` is not numeric (encode categoricals first), is not 2-D, or contains `±inf` (NaN is allowed: median imputation is applied inside the selector);
|
|
91
|
+
* `y` is not 1-D, has missing values, looks continuous (regression is not supported), or has fewer than 2 classes or fewer than 5 samples in a class;
|
|
92
|
+
* there are fewer than 20 rows or fewer than 2 columns; feature names are duplicated;
|
|
93
|
+
* `transform` receives different columns than `fit` (count or names).
|
|
94
|
+
|
|
95
|
+
Constant columns are dropped (reported with `NaN` in `report_`). If a policy returns an empty set, the 3 top-consensus features are returned with a warning (`fallback_ = True`).
|
|
96
|
+
|
|
97
|
+
## GPU
|
|
98
|
+
|
|
99
|
+
The rankers and the downstream models in the benchmark use `torch` / `xgboost` on CUDA when available (`decoyfs.device.env_report()` shows what was detected). On NVIDIA Blackwell cards (RTX 50xx) install a CUDA 12.8 build of torch: `pip install torch --index-url https://download.pytorch.org/whl/cu128`.
|
|
100
|
+
|
|
101
|
+
## Reproduce the paper
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git clone <this repo> && cd decoyfs
|
|
105
|
+
pip install -e ".[bench]"
|
|
106
|
+
python benchmarks/build_notebook.py # creates benchmarks/notebooks/DecoyFS_benchmark.ipynb
|
|
107
|
+
python benchmarks/run_benchmark.py --tier smoke # minutes
|
|
108
|
+
python benchmarks/run_benchmark.py --tier full --force-gpu # 74 datasets, 5x2 CV (about 23 h on an RTX 5060 Ti)
|
|
109
|
+
python benchmarks/analysis/decoy_vs_comparators.py # paired comparison vs Boruta-style and CV-k
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Datasets are downloaded from OpenML into `~/.cache/decoyfs` (override with `DECOYFS_CACHE`). The benchmark's policy identifiers (`CTRL_VOTE_MAJ`, `CTRL_STAB`, `BORUTA`, `CVK_1SE`, …) are kept as in the released result tables. The Spanish protocol design document is in [docs/DISENO_PROTOCOLO.es.md](docs/DISENO_PROTOCOLO.es.md).
|
|
113
|
+
|
|
114
|
+
## Cite
|
|
115
|
+
|
|
116
|
+
See [CITATION.cff](CITATION.cff). A software DOI is minted by Zenodo for every release.
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT © Nestor A. Zermeño Campos, Universidad Autónoma de Baja California.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "decoyfs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Decoy-referenced feature selection: Random and Shadow control features, swappable rankers and auditable vote policies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Nestor A. Zermeño Campos", email = "nestor.zermeno@uabc.edu.mx" }]
|
|
13
|
+
keywords = ["feature selection", "decoy features", "shadow features", "random probes", "boruta", "scikit-learn", "tabular"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.26,<3",
|
|
26
|
+
"scipy>=1.11,<2",
|
|
27
|
+
"pandas>=2.0,<3",
|
|
28
|
+
"scikit-learn>=1.4,<2",
|
|
29
|
+
"xgboost>=2.0,<4",
|
|
30
|
+
"torch>=2.2",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
bench = [
|
|
35
|
+
"statsmodels>=0.14",
|
|
36
|
+
"matplotlib>=3.8",
|
|
37
|
+
"openml>=0.14",
|
|
38
|
+
"nbformat>=5.9",
|
|
39
|
+
"ipykernel>=6.29",
|
|
40
|
+
]
|
|
41
|
+
bench-extra = ["pytabkit>=1.5"]
|
|
42
|
+
dev = ["pytest>=8", "ruff>=0.5", "build>=1.2", "twine>=5"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://github.com/alexanderzrm/decoyfs"
|
|
46
|
+
Issues = "https://github.com/alexanderzrm/decoyfs/issues"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.dynamic]
|
|
49
|
+
version = { attr = "decoyfs.__version__" }
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.package-data]
|
|
55
|
+
decoyfs = ["py.typed"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-q"
|
|
60
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 120
|
|
64
|
+
target-version = "py310"
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["E", "F", "W"]
|
|
68
|
+
ignore = ["E501", "E702", "E731", "E741", "F841"]
|
decoyfs-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""DecoyFS: decoy-referenced feature selection.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
from decoyfs import DecoySelector
|
|
6
|
+
sel = DecoySelector().fit(X_train, y_train)
|
|
7
|
+
X_small = sel.transform(X_test)
|
|
8
|
+
sel.report_ # audit trail: votes and decoy comparison per feature
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from .compare import compare_selectors
|
|
13
|
+
from .decoy import DecoySelector
|
|
14
|
+
from .policies import list_policies, register_policy, unregister_policy
|
|
15
|
+
from .rankers import list_rankers, register_ranker, unregister_ranker
|
|
16
|
+
from .selectors import BorutaSelector, CVKSelector, FixedKSelector, NABFSSelector
|
|
17
|
+
from .validation import DecoyInputError
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"DecoySelector", "FixedKSelector", "CVKSelector", "BorutaSelector", "NABFSSelector", "compare_selectors",
|
|
23
|
+
"register_ranker", "unregister_ranker", "list_rankers", "register_policy", "unregister_policy", "list_policies",
|
|
24
|
+
"DecoyInputError", "__version__",
|
|
25
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Common machinery of the selectors: validation, per-fold preprocessing and a scikit-learn compatible interface."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from sklearn.base import BaseEstimator, TransformerMixin
|
|
7
|
+
from sklearn.utils.validation import check_is_fitted
|
|
8
|
+
|
|
9
|
+
from .preprocessing import FoldPrep
|
|
10
|
+
from .validation import DecoyInputError, check_transform_X, check_X_y
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseSelector(TransformerMixin, BaseEstimator):
|
|
14
|
+
"""Base class: ``fit`` validates and preprocesses, subclasses implement :meth:`_select` on the preprocessed matrix.
|
|
15
|
+
|
|
16
|
+
After ``fit``: ``support_`` (boolean mask over the ORIGINAL columns), ``selected_features_`` (names or indices),
|
|
17
|
+
``n_features_in_``, ``classes_``, ``preprocessor_`` and ``fit_time_`` (seconds).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def _select(self, Z, y, k):
|
|
21
|
+
"""Return the indices (into the columns of ``Z``) of the selected features."""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def fit(self, X, y):
|
|
25
|
+
import time
|
|
26
|
+
t0 = time.perf_counter()
|
|
27
|
+
Xa, yc, classes, names = check_X_y(X, y)
|
|
28
|
+
self.n_features_in_ = Xa.shape[1]
|
|
29
|
+
if names is not None:
|
|
30
|
+
self.feature_names_in_ = names
|
|
31
|
+
elif hasattr(self, "feature_names_in_"):
|
|
32
|
+
del self.feature_names_in_
|
|
33
|
+
self.classes_ = classes
|
|
34
|
+
self.preprocessor_ = FoldPrep().fit(Xa)
|
|
35
|
+
keep = self.preprocessor_.keep_
|
|
36
|
+
if len(keep) < 2:
|
|
37
|
+
raise DecoyInputError("Fewer than two non-constant features remain after preprocessing.")
|
|
38
|
+
Z = self.preprocessor_.transform(Xa)
|
|
39
|
+
sel = np.asarray(self._select(Z, yc, len(classes)), dtype=int)
|
|
40
|
+
mask = np.zeros(Xa.shape[1], bool)
|
|
41
|
+
mask[keep[sel]] = True
|
|
42
|
+
self.support_ = mask
|
|
43
|
+
self.fit_time_ = time.perf_counter() - t0
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def get_support(self, indices=False):
|
|
47
|
+
check_is_fitted(self, "support_")
|
|
48
|
+
return np.where(self.support_)[0] if indices else self.support_.copy()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def selected_features_(self):
|
|
52
|
+
check_is_fitted(self, "support_")
|
|
53
|
+
if hasattr(self, "feature_names_in_"):
|
|
54
|
+
return list(self.feature_names_in_[self.support_])
|
|
55
|
+
return list(np.where(self.support_)[0])
|
|
56
|
+
|
|
57
|
+
def get_feature_names_out(self, input_features=None):
|
|
58
|
+
check_is_fitted(self, "support_")
|
|
59
|
+
if input_features is not None:
|
|
60
|
+
names = np.asarray(input_features, dtype=object)
|
|
61
|
+
elif hasattr(self, "feature_names_in_"):
|
|
62
|
+
names = self.feature_names_in_
|
|
63
|
+
else:
|
|
64
|
+
names = np.array([f"x{i}" for i in range(self.n_features_in_)], dtype=object)
|
|
65
|
+
return np.asarray(names[self.support_], dtype=object)
|
|
66
|
+
|
|
67
|
+
def transform(self, X):
|
|
68
|
+
check_is_fitted(self, "support_")
|
|
69
|
+
Xo = check_transform_X(X, self.n_features_in_, getattr(self, "feature_names_in_", None))
|
|
70
|
+
if isinstance(Xo, pd.DataFrame):
|
|
71
|
+
return Xo.loc[:, self.support_]
|
|
72
|
+
return Xo[:, self.support_]
|
|
73
|
+
|
|
74
|
+
def inverse_transform(self, X):
|
|
75
|
+
check_is_fitted(self, "support_")
|
|
76
|
+
X = np.asarray(X)
|
|
77
|
+
out = np.zeros((X.shape[0], self.n_features_in_), dtype=X.dtype)
|
|
78
|
+
out[:, self.support_] = X
|
|
79
|
+
return out
|
|
80
|
+
|
|
81
|
+
def fit_transform(self, X, y=None, **fit_params):
|
|
82
|
+
return self.fit(X, y).transform(X)
|