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.
Files changed (34) hide show
  1. decoyfs-0.1.0/LICENSE +21 -0
  2. decoyfs-0.1.0/PKG-INFO +161 -0
  3. decoyfs-0.1.0/README.md +120 -0
  4. decoyfs-0.1.0/pyproject.toml +68 -0
  5. decoyfs-0.1.0/setup.cfg +4 -0
  6. decoyfs-0.1.0/src/decoyfs/__init__.py +25 -0
  7. decoyfs-0.1.0/src/decoyfs/_base.py +82 -0
  8. decoyfs-0.1.0/src/decoyfs/bench/__init__.py +4 -0
  9. decoyfs-0.1.0/src/decoyfs/bench/data.py +258 -0
  10. decoyfs-0.1.0/src/decoyfs/bench/models.py +225 -0
  11. decoyfs-0.1.0/src/decoyfs/bench/plots.py +42 -0
  12. decoyfs-0.1.0/src/decoyfs/bench/presets.py +21 -0
  13. decoyfs-0.1.0/src/decoyfs/bench/progress.py +237 -0
  14. decoyfs-0.1.0/src/decoyfs/bench/runner.py +240 -0
  15. decoyfs-0.1.0/src/decoyfs/bench/stats.py +282 -0
  16. decoyfs-0.1.0/src/decoyfs/comparators.py +127 -0
  17. decoyfs-0.1.0/src/decoyfs/compare.py +60 -0
  18. decoyfs-0.1.0/src/decoyfs/controls.py +102 -0
  19. decoyfs-0.1.0/src/decoyfs/decoy.py +200 -0
  20. decoyfs-0.1.0/src/decoyfs/dependence.py +93 -0
  21. decoyfs-0.1.0/src/decoyfs/device.py +68 -0
  22. decoyfs-0.1.0/src/decoyfs/metrics.py +46 -0
  23. decoyfs-0.1.0/src/decoyfs/policies.py +248 -0
  24. decoyfs-0.1.0/src/decoyfs/preprocessing.py +42 -0
  25. decoyfs-0.1.0/src/decoyfs/py.typed +0 -0
  26. decoyfs-0.1.0/src/decoyfs/rankers.py +211 -0
  27. decoyfs-0.1.0/src/decoyfs/selectors.py +94 -0
  28. decoyfs-0.1.0/src/decoyfs/validation.py +97 -0
  29. decoyfs-0.1.0/src/decoyfs.egg-info/PKG-INFO +161 -0
  30. decoyfs-0.1.0/src/decoyfs.egg-info/SOURCES.txt +32 -0
  31. decoyfs-0.1.0/src/decoyfs.egg-info/dependency_links.txt +1 -0
  32. decoyfs-0.1.0/src/decoyfs.egg-info/requires.txt +22 -0
  33. decoyfs-0.1.0/src/decoyfs.egg-info/top_level.txt +1 -0
  34. 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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)
@@ -0,0 +1,4 @@
1
+ """Benchmark harness used for the DecoyFS paper (74 OpenML datasets, 4 downstream models, paired statistics).
2
+
3
+ Requires the ``bench`` extra: ``pip install "decoyfs[bench]"``.
4
+ """