beamfeat 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.
- beamfeat-0.1.0/.gitignore +11 -0
- beamfeat-0.1.0/CITATION.cff +29 -0
- beamfeat-0.1.0/LICENSE +21 -0
- beamfeat-0.1.0/PKG-INFO +250 -0
- beamfeat-0.1.0/README.md +209 -0
- beamfeat-0.1.0/benchmarks/README.md +24 -0
- beamfeat-0.1.0/benchmarks/independent/README.md +58 -0
- beamfeat-0.1.0/pyproject.toml +84 -0
- beamfeat-0.1.0/src/beamfeat/__init__.py +54 -0
- beamfeat-0.1.0/src/beamfeat/estimators.py +975 -0
- beamfeat-0.1.0/src/beamfeat/expression.py +1095 -0
- beamfeat-0.1.0/src/beamfeat/py.typed +0 -0
- beamfeat-0.1.0/src/beamfeat/scoring.py +585 -0
- beamfeat-0.1.0/src/beamfeat/search.py +525 -0
- beamfeat-0.1.0/src/beamfeat/selection.py +795 -0
- beamfeat-0.1.0/tests/__init__.py +0 -0
- beamfeat-0.1.0/tests/test_estimators.py +656 -0
- beamfeat-0.1.0/tests/test_expression.py +734 -0
- beamfeat-0.1.0/tests/test_scoring.py +384 -0
- beamfeat-0.1.0/tests/test_search.py +408 -0
- beamfeat-0.1.0/tests/test_selection.py +494 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
type: software
|
|
4
|
+
title: "beamfeat: beam-search feature construction with FDR-controlled selection"
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
date-released: 2026-07-23
|
|
7
|
+
license: MIT
|
|
8
|
+
authors:
|
|
9
|
+
- family-names: "Daramola"
|
|
10
|
+
given-names: "Olanrewaju M."
|
|
11
|
+
orcid: "https://orcid.org/0009-0006-3327-2047"
|
|
12
|
+
repository-code: "https://github.com/LD-Shell/beamfeat"
|
|
13
|
+
keywords:
|
|
14
|
+
- feature engineering
|
|
15
|
+
- feature selection
|
|
16
|
+
- false discovery rate
|
|
17
|
+
- knockoffs
|
|
18
|
+
# After archiving the release on Zenodo, add the concept DOI:
|
|
19
|
+
# doi: 10.5281/zenodo.XXXXXXX
|
|
20
|
+
# After JOSS acceptance, add:
|
|
21
|
+
# preferred-citation:
|
|
22
|
+
# type: article
|
|
23
|
+
# title: "beamfeat: beam-search feature construction with false-discovery-rate controlled selection"
|
|
24
|
+
# authors:
|
|
25
|
+
# - family-names: "Daramola"
|
|
26
|
+
# given-names: "Olanrewaju M."
|
|
27
|
+
# journal: "Journal of Open Source Software"
|
|
28
|
+
# doi: <JOSS DOI>
|
|
29
|
+
# year: 2026
|
beamfeat-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Olanrewaju M. Daramola
|
|
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.
|
beamfeat-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: beamfeat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Beam-search feature construction with false-discovery-rate controlled selection
|
|
5
|
+
Project-URL: Homepage, https://github.com/LD-Shell/beamfeat
|
|
6
|
+
Project-URL: Documentation, https://github.com/LD-Shell/beamfeat#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/LD-Shell/beamfeat/issues
|
|
8
|
+
Author: Olanrewaju M. Daramola
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: automl,false-discovery-rate,feature-engineering,feature-selection,knockoffs,symbolic-features
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: numpy>=1.26
|
|
22
|
+
Requires-Dist: scikit-learn>=1.6
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: pint>=0.24; extra == 'all'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'all'
|
|
26
|
+
Requires-Dist: sympy>=1.13; extra == 'all'
|
|
27
|
+
Provides-Extra: benchmarks
|
|
28
|
+
Requires-Dist: lightgbm>=4; extra == 'benchmarks'
|
|
29
|
+
Provides-Extra: display
|
|
30
|
+
Requires-Dist: sympy>=1.13; extra == 'display'
|
|
31
|
+
Provides-Extra: docs
|
|
32
|
+
Requires-Dist: mkdocs-jupyter; extra == 'docs'
|
|
33
|
+
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
34
|
+
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
|
|
35
|
+
Provides-Extra: test
|
|
36
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
37
|
+
Requires-Dist: sympy>=1.13; extra == 'test'
|
|
38
|
+
Provides-Extra: units
|
|
39
|
+
Requires-Dist: pint>=0.24; extra == 'units'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# beamfeat
|
|
43
|
+
|
|
44
|
+
[](https://github.com/LD-Shell/beamfeat/actions/workflows/ci.yml)
|
|
45
|
+
[](https://pypi.org/project/beamfeat/)
|
|
46
|
+
[](https://github.com/LD-Shell/beamfeat)
|
|
47
|
+
[](https://github.com/LD-Shell/beamfeat/blob/main/LICENSE)
|
|
48
|
+
<!-- After the Zenodo archive, add:
|
|
49
|
+
[](https://doi.org/10.5281/zenodo.XXXXXXX)
|
|
50
|
+
-->
|
|
51
|
+
|
|
52
|
+
Beam-search feature construction with false-discovery-rate (FDR) controlled
|
|
53
|
+
selection, packaged as scikit-learn estimators.
|
|
54
|
+
|
|
55
|
+
beamfeat builds interpretable mathematical expressions from your columns —
|
|
56
|
+
`(x0 / x2) * x1`, `log(a) * log(b)` — searches for the ones that explain the
|
|
57
|
+
target, and then screens them with a selection procedure whose statistical
|
|
58
|
+
guarantee is stated, tested, and honestly reported when it cannot be given.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from beamfeat import BeamFeatRegressor
|
|
62
|
+
|
|
63
|
+
model = BeamFeatRegressor(max_depth=2, beam_width=30).fit(X, y)
|
|
64
|
+
|
|
65
|
+
model.formulas() # ['((x0 / x2) * x1)', ...]
|
|
66
|
+
model.equation() # 'y = 0.9847*((x0 / x2) * x1) + ...'
|
|
67
|
+
model.fdr_controlled_ # True: the features carry the stated FDR guarantee
|
|
68
|
+
model.predict(X_new)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install beamfeat # core: numpy + scikit-learn only
|
|
75
|
+
# before the first PyPI release, install from source:
|
|
76
|
+
# pip install git+https://github.com/LD-Shell/beamfeat
|
|
77
|
+
pip install "beamfeat[units]" # + pint, for unit-aware search (units as
|
|
78
|
+
# strings like "kg" or as pint quantities)
|
|
79
|
+
pip install "beamfeat[all]" # + sympy display support and test deps
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Python ≥ 3.10. There are **no upper version pins**: the test suite runs
|
|
83
|
+
against the oldest supported stack (numpy 1.26 / scikit-learn 1.6) and
|
|
84
|
+
through the current releases (verified up to scikit-learn 1.9 / numpy 2.5).
|
|
85
|
+
|
|
86
|
+
## What it does
|
|
87
|
+
|
|
88
|
+
1. **Search.** A beam search over an expression DAG proposes and scores
|
|
89
|
+
compositions of unary (`log`, `sqrt`, `reciprocal`, `square`, `abs`) and
|
|
90
|
+
binary (`*`, `/`, `+`, `-`) operators, with structural deduplication,
|
|
91
|
+
redundancy pruning, and cost O(depth × beam²) rather than the exhaustive
|
|
92
|
+
O(operators^depth).
|
|
93
|
+
2. **Selection.** Candidates are tested for association with the target under
|
|
94
|
+
procedures that control the false discovery rate (details below), on a
|
|
95
|
+
held-out split of the training rows that the search never saw.
|
|
96
|
+
3. **Fit.** A parsimony step (greedy forward selection within the screened
|
|
97
|
+
set, on by default) keeps the compact subset the equation uses — the full
|
|
98
|
+
screened set with per-candidate p- and q-values stays auditable in
|
|
99
|
+
`selection_report_` — then a linear model fits it, readable via
|
|
100
|
+
`.equation()`.
|
|
101
|
+
|
|
102
|
+
Numerical failures (overflow, domain errors) are recorded and excluded, never
|
|
103
|
+
silently masked. Optional unit propagation — units given as pint quantities
|
|
104
|
+
or as plain strings like `"kg"` or `"m / s**2"` — rejects dimensionally
|
|
105
|
+
invalid expressions at construction time — on a mechanics example this pruned 62% of
|
|
106
|
+
proposals before any data work.
|
|
107
|
+
|
|
108
|
+
## The guarantees, stated precisely
|
|
109
|
+
|
|
110
|
+
- **Permutation selector (default).** Tests marginal association with an
|
|
111
|
+
exact permutation test: the statistic (|Pearson r| for regression,
|
|
112
|
+
eta-squared for classification) is a fixed function of the data, and
|
|
113
|
+
p-values use the add-one estimator of Phipson & Smyth (2010). Multiplicity
|
|
114
|
+
is corrected with Benjamini–Yekutieli by default (valid under arbitrary
|
|
115
|
+
dependence); Benjamini–Hochberg is available and is valid under positive
|
|
116
|
+
dependence (PRDS).
|
|
117
|
+
- **Knockoff selector.** Fixed-X knockoffs (Barber & Candès, 2015) when
|
|
118
|
+
`n ≥ 2p`: no distributional assumption on the features — engineered columns
|
|
119
|
+
are fine — with the guarantee requiring Gaussian noise in the linear model.
|
|
120
|
+
Model-X Gaussian knockoffs (Candès et al., 2018) only as an `n < 2p`
|
|
121
|
+
fallback, with a warning, since engineered features violate its Gaussianity
|
|
122
|
+
assumption.
|
|
123
|
+
- **Selective inference.** Search retains candidates *because* they correlate
|
|
124
|
+
with the target in its sample, so testing them on the same rows would bias
|
|
125
|
+
p-values optimistically. By default the estimators search on one half of
|
|
126
|
+
the training rows and select on the other (`selection_holdout=0.5`).
|
|
127
|
+
- **Association ≠ generalisation.** A post-fit check on the selection
|
|
128
|
+
holdout raises a visible `DegenerateFitWarning` if the assembled model
|
|
129
|
+
fails to generalise (negative held-out R², or accuracy below the majority
|
|
130
|
+
class) — FDR certifies the features' association, and this check covers
|
|
131
|
+
the separate claim that the equation built on them is sound.
|
|
132
|
+
- **Honest failure.** If nothing passes selection, the default
|
|
133
|
+
(`on_no_discoveries="empty"`) keeps no constructed features: the model
|
|
134
|
+
degrades to an intercept-only fit and raises a visible
|
|
135
|
+
`NoDiscoveriesWarning`. `"fallback"` (keep the unfiltered search output,
|
|
136
|
+
flagged `fdr_controlled_ = False`) and `"raise"` are available. It never
|
|
137
|
+
silently claims a guarantee it does not have.
|
|
138
|
+
|
|
139
|
+
Three readings of these guarantees matter. FDR is an **expectation over
|
|
140
|
+
repetitions**, not a per-run bound. The marginal null counts a near-duplicate
|
|
141
|
+
of a real signal as a **true** discovery — de-duplication is the redundancy
|
|
142
|
+
threshold's job, not the error-control procedure's. And every number below
|
|
143
|
+
carries Monte Carlo error from finite trials.
|
|
144
|
+
|
|
145
|
+
## Measured, not assumed
|
|
146
|
+
|
|
147
|
+
All of the following are measured by the test suite or the committed
|
|
148
|
+
benchmark scripts, not asserted:
|
|
149
|
+
|
|
150
|
+
- Selector calibration (Gaussian designs, 25 trials): realised FDR
|
|
151
|
+
0.065/0.120/0.210 at nominal 0.05/0.10/0.20 for BH, 0.020/0.033/0.065 for
|
|
152
|
+
BY, power 1.00 throughout. Fixed-X knockoff+ realised 0.201 at nominal 0.2.
|
|
153
|
+
- End-to-end pipeline (search + holdout selection) at nominal FDR 0.10:
|
|
154
|
+
empirical FDR 0.0000 with power 1.000 over 200 replicates, zero fallbacks;
|
|
155
|
+
zero selections over 60 global-null replicates.
|
|
156
|
+
- Feynman-equation panel (12 physics laws, 0.1% noise): 9–10/12 solved (a borderline surrogate flips across numeric stacks) at the
|
|
157
|
+
SRBench criterion and a stable 8/12 in exact symbolic form, ~0.9 s per
|
|
158
|
+
equation; the misses are named boundaries (a depth-3 rational, a literal
|
|
159
|
+
constant, depth-4 nesting, the Gaussian's exponential).
|
|
160
|
+
- Pure-noise stress: BH breached in 3/25 trials (mean FDP 0.12 vs nominal
|
|
161
|
+
0.10, the PRDS caveat materialising on correlated candidates); BY in 1/25
|
|
162
|
+
(mean FDP 0.04). This measurement is why BY is the estimator default.
|
|
163
|
+
- Stress suite (signal among ten distractor columns): beamfeat's
|
|
164
|
+
false-feature rate — returned formulas touching only irrelevant columns —
|
|
165
|
+
is 0.000 at noise levels from 5% to 50% of the signal scale and at n as
|
|
166
|
+
small as 120, with recovery and the guarantee intact throughout; autofeat
|
|
167
|
+
recovers the formula equally often but returns distractor-only features at
|
|
168
|
+
a mean rate of 0.29 (worst 0.75 at 50% noise).
|
|
169
|
+
- Real data (six datasets, numeric columns): best on mpg (0.871 vs LightGBM
|
|
170
|
+
0.853) and tips — where gradient boosting overfits below ridge while the
|
|
171
|
+
FDR gate holds beamfeat at the ceiling — tied with ridge on diamonds from
|
|
172
|
+
one interpretable feature, behind LightGBM on penguins and breast cancer,
|
|
173
|
+
and behind ridge on diabetes (0.376; autofeat 0.303, OpenFE 0.235): no
|
|
174
|
+
nonlinear structure exists there to find, and beamfeat does not invent
|
|
175
|
+
any.
|
|
176
|
+
- OpenFE on the same stress suite (its features + LightGBM, after shimming a
|
|
177
|
+
removed sklearn parameter): false-feature rate also 0.000, but 0/5 formulas
|
|
178
|
+
recovered and lower R² throughout — it optimises tree-model lift, not
|
|
179
|
+
equations.
|
|
180
|
+
- Heavy-tailed noise (Student t(2), infinite variance): exact formula
|
|
181
|
+
recovered at R² 0.956 vs LightGBM 0.866 — the permutation test is
|
|
182
|
+
distribution-free, so heavy tails cost it nothing.
|
|
183
|
+
- Known boundaries, measured: on piecewise targets trees win outright
|
|
184
|
+
(LightGBM 0.998 vs 0.808 — if a tree model beats beamfeat by a wide
|
|
185
|
+
margin, the signal is likely piecewise, not algebraic), and on Friedman #1
|
|
186
|
+
the 0.744-vs-0.964-oracle gap splits into a measured 0.875
|
|
187
|
+
screening-admissible ceiling (the marginally-quiet quadratic is priced by
|
|
188
|
+
the guarantee itself) and a search shortfall below it that is insensitive
|
|
189
|
+
to beam width and diversity settings — stated rather than hidden.
|
|
190
|
+
- Test suite: 370 tests at 95% statement coverage (a 90% floor is enforced
|
|
191
|
+
in CI), including scikit-learn's full estimator-conformance checks.
|
|
192
|
+
- Selection-only baseline (knockpy on raw columns, modified-FDR offset —
|
|
193
|
+
the only satisfiable configuration at these dimensionalities): 0/15
|
|
194
|
+
formulas recovered, core-suite R² at the ridge level (0.845), and a 0.11
|
|
195
|
+
mean false-feature rate on the stress suite — the construction step and
|
|
196
|
+
the holdout are the delta.
|
|
197
|
+
- Independent 315-fit study across nine datasets and seven methods
|
|
198
|
+
(`benchmarks/independent/`): mean R² 0.803 against random forest 0.810,
|
|
199
|
+
LightGBM 0.798, autofeat 0.751, OpenFE 0.736, featuretools −2.478 — and a
|
|
200
|
+
worst single fit of 0.355 where autofeat reached −2.28 and featuretools
|
|
201
|
+
−57.3, at ~48× autofeat's speed, with the FDR flag delivered on 45/45
|
|
202
|
+
fits.
|
|
203
|
+
- Benchmarks (10 synthetic problems with known formulas, 70/30 splits):
|
|
204
|
+
beamfeat mean R² 0.9993, ~0.4 s fits, 9/9 formulas recovered; autofeat
|
|
205
|
+
0.9974, ~9 s, 8/9; LightGBM 0.9798; ridge 0.8442. On the purely linear
|
|
206
|
+
problem — where feature construction should not win — ridge ties beamfeat.
|
|
207
|
+
|
|
208
|
+
## Relationship to autofeat
|
|
209
|
+
|
|
210
|
+
beamfeat's design began from a code review of
|
|
211
|
+
[autofeat](https://github.com/cod3licious/autofeat) (Horn et al., 2019) and
|
|
212
|
+
keeps its best idea — compact symbolic features feeding a linear model —
|
|
213
|
+
while replacing exhaustive expansion with beam search, heuristic
|
|
214
|
+
noise-threshold selection with FDR-controlled procedures, and upper-pinned
|
|
215
|
+
dependencies with tested floors. The generate-then-FDR-filter design has a
|
|
216
|
+
direct precedent in [tsfresh](https://github.com/blue-yonder/tsfresh)
|
|
217
|
+
(Christ et al., 2018), which filters mass-generated time-series features
|
|
218
|
+
under Benjamini–Yekutieli control; beamfeat applies that design to symbolic
|
|
219
|
+
expressions on tabular columns.
|
|
220
|
+
|
|
221
|
+
## Independent comparison
|
|
222
|
+
|
|
223
|
+
`benchmarks/independent/` holds a 315-fit study (nine datasets, seven
|
|
224
|
+
methods, five splits, average ranks plus a Friedman test) with its harness,
|
|
225
|
+
pinned environment, data, and a notebook that reproduces the analysis. Mean
|
|
226
|
+
held-out R²: beamfeat 0.803, random forest 0.810, LightGBM 0.798, autofeat
|
|
227
|
+
0.751, OpenFE 0.736, ridge 0.704, featuretools −2.478 — and worst single fit
|
|
228
|
+
of 45: beamfeat 0.355, autofeat −2.28, featuretools −57.3, with the FDR
|
|
229
|
+
guarantee delivered on 45/45. Read `PROVENANCE.md` first: it records what is
|
|
230
|
+
reproducible (beamfeat, bit-identically) and what is not (autofeat seeds
|
|
231
|
+
nothing internally and swung from +0.955 to −77.86 on one identical split).
|
|
232
|
+
|
|
233
|
+
## Documentation
|
|
234
|
+
|
|
235
|
+
Tutorial notebooks live in `notebooks/` (getting started; search and scoring;
|
|
236
|
+
selection and units). The statistical guarantees are documented in detail in
|
|
237
|
+
`docs/guarantees.md` and in the module docstrings of `beamfeat.selection`. Users coming
|
|
238
|
+
from autofeat will find a full API mapping and version-compatibility notes
|
|
239
|
+
in `docs/migrating-from-autofeat.md`.
|
|
240
|
+
|
|
241
|
+
## Citing
|
|
242
|
+
|
|
243
|
+
See `CITATION.cff`. If you use the selection procedures, please also cite the
|
|
244
|
+
underlying statistics: Benjamini & Hochberg (1995), Benjamini & Yekutieli
|
|
245
|
+
(2001), Barber & Candès (2015), Candès et al. (2018), and Phipson & Smyth
|
|
246
|
+
(2010).
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT.
|
beamfeat-0.1.0/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# beamfeat
|
|
2
|
+
|
|
3
|
+
[](https://github.com/LD-Shell/beamfeat/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/beamfeat/)
|
|
5
|
+
[](https://github.com/LD-Shell/beamfeat)
|
|
6
|
+
[](https://github.com/LD-Shell/beamfeat/blob/main/LICENSE)
|
|
7
|
+
<!-- After the Zenodo archive, add:
|
|
8
|
+
[](https://doi.org/10.5281/zenodo.XXXXXXX)
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
Beam-search feature construction with false-discovery-rate (FDR) controlled
|
|
12
|
+
selection, packaged as scikit-learn estimators.
|
|
13
|
+
|
|
14
|
+
beamfeat builds interpretable mathematical expressions from your columns —
|
|
15
|
+
`(x0 / x2) * x1`, `log(a) * log(b)` — searches for the ones that explain the
|
|
16
|
+
target, and then screens them with a selection procedure whose statistical
|
|
17
|
+
guarantee is stated, tested, and honestly reported when it cannot be given.
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from beamfeat import BeamFeatRegressor
|
|
21
|
+
|
|
22
|
+
model = BeamFeatRegressor(max_depth=2, beam_width=30).fit(X, y)
|
|
23
|
+
|
|
24
|
+
model.formulas() # ['((x0 / x2) * x1)', ...]
|
|
25
|
+
model.equation() # 'y = 0.9847*((x0 / x2) * x1) + ...'
|
|
26
|
+
model.fdr_controlled_ # True: the features carry the stated FDR guarantee
|
|
27
|
+
model.predict(X_new)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install beamfeat # core: numpy + scikit-learn only
|
|
34
|
+
# before the first PyPI release, install from source:
|
|
35
|
+
# pip install git+https://github.com/LD-Shell/beamfeat
|
|
36
|
+
pip install "beamfeat[units]" # + pint, for unit-aware search (units as
|
|
37
|
+
# strings like "kg" or as pint quantities)
|
|
38
|
+
pip install "beamfeat[all]" # + sympy display support and test deps
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python ≥ 3.10. There are **no upper version pins**: the test suite runs
|
|
42
|
+
against the oldest supported stack (numpy 1.26 / scikit-learn 1.6) and
|
|
43
|
+
through the current releases (verified up to scikit-learn 1.9 / numpy 2.5).
|
|
44
|
+
|
|
45
|
+
## What it does
|
|
46
|
+
|
|
47
|
+
1. **Search.** A beam search over an expression DAG proposes and scores
|
|
48
|
+
compositions of unary (`log`, `sqrt`, `reciprocal`, `square`, `abs`) and
|
|
49
|
+
binary (`*`, `/`, `+`, `-`) operators, with structural deduplication,
|
|
50
|
+
redundancy pruning, and cost O(depth × beam²) rather than the exhaustive
|
|
51
|
+
O(operators^depth).
|
|
52
|
+
2. **Selection.** Candidates are tested for association with the target under
|
|
53
|
+
procedures that control the false discovery rate (details below), on a
|
|
54
|
+
held-out split of the training rows that the search never saw.
|
|
55
|
+
3. **Fit.** A parsimony step (greedy forward selection within the screened
|
|
56
|
+
set, on by default) keeps the compact subset the equation uses — the full
|
|
57
|
+
screened set with per-candidate p- and q-values stays auditable in
|
|
58
|
+
`selection_report_` — then a linear model fits it, readable via
|
|
59
|
+
`.equation()`.
|
|
60
|
+
|
|
61
|
+
Numerical failures (overflow, domain errors) are recorded and excluded, never
|
|
62
|
+
silently masked. Optional unit propagation — units given as pint quantities
|
|
63
|
+
or as plain strings like `"kg"` or `"m / s**2"` — rejects dimensionally
|
|
64
|
+
invalid expressions at construction time — on a mechanics example this pruned 62% of
|
|
65
|
+
proposals before any data work.
|
|
66
|
+
|
|
67
|
+
## The guarantees, stated precisely
|
|
68
|
+
|
|
69
|
+
- **Permutation selector (default).** Tests marginal association with an
|
|
70
|
+
exact permutation test: the statistic (|Pearson r| for regression,
|
|
71
|
+
eta-squared for classification) is a fixed function of the data, and
|
|
72
|
+
p-values use the add-one estimator of Phipson & Smyth (2010). Multiplicity
|
|
73
|
+
is corrected with Benjamini–Yekutieli by default (valid under arbitrary
|
|
74
|
+
dependence); Benjamini–Hochberg is available and is valid under positive
|
|
75
|
+
dependence (PRDS).
|
|
76
|
+
- **Knockoff selector.** Fixed-X knockoffs (Barber & Candès, 2015) when
|
|
77
|
+
`n ≥ 2p`: no distributional assumption on the features — engineered columns
|
|
78
|
+
are fine — with the guarantee requiring Gaussian noise in the linear model.
|
|
79
|
+
Model-X Gaussian knockoffs (Candès et al., 2018) only as an `n < 2p`
|
|
80
|
+
fallback, with a warning, since engineered features violate its Gaussianity
|
|
81
|
+
assumption.
|
|
82
|
+
- **Selective inference.** Search retains candidates *because* they correlate
|
|
83
|
+
with the target in its sample, so testing them on the same rows would bias
|
|
84
|
+
p-values optimistically. By default the estimators search on one half of
|
|
85
|
+
the training rows and select on the other (`selection_holdout=0.5`).
|
|
86
|
+
- **Association ≠ generalisation.** A post-fit check on the selection
|
|
87
|
+
holdout raises a visible `DegenerateFitWarning` if the assembled model
|
|
88
|
+
fails to generalise (negative held-out R², or accuracy below the majority
|
|
89
|
+
class) — FDR certifies the features' association, and this check covers
|
|
90
|
+
the separate claim that the equation built on them is sound.
|
|
91
|
+
- **Honest failure.** If nothing passes selection, the default
|
|
92
|
+
(`on_no_discoveries="empty"`) keeps no constructed features: the model
|
|
93
|
+
degrades to an intercept-only fit and raises a visible
|
|
94
|
+
`NoDiscoveriesWarning`. `"fallback"` (keep the unfiltered search output,
|
|
95
|
+
flagged `fdr_controlled_ = False`) and `"raise"` are available. It never
|
|
96
|
+
silently claims a guarantee it does not have.
|
|
97
|
+
|
|
98
|
+
Three readings of these guarantees matter. FDR is an **expectation over
|
|
99
|
+
repetitions**, not a per-run bound. The marginal null counts a near-duplicate
|
|
100
|
+
of a real signal as a **true** discovery — de-duplication is the redundancy
|
|
101
|
+
threshold's job, not the error-control procedure's. And every number below
|
|
102
|
+
carries Monte Carlo error from finite trials.
|
|
103
|
+
|
|
104
|
+
## Measured, not assumed
|
|
105
|
+
|
|
106
|
+
All of the following are measured by the test suite or the committed
|
|
107
|
+
benchmark scripts, not asserted:
|
|
108
|
+
|
|
109
|
+
- Selector calibration (Gaussian designs, 25 trials): realised FDR
|
|
110
|
+
0.065/0.120/0.210 at nominal 0.05/0.10/0.20 for BH, 0.020/0.033/0.065 for
|
|
111
|
+
BY, power 1.00 throughout. Fixed-X knockoff+ realised 0.201 at nominal 0.2.
|
|
112
|
+
- End-to-end pipeline (search + holdout selection) at nominal FDR 0.10:
|
|
113
|
+
empirical FDR 0.0000 with power 1.000 over 200 replicates, zero fallbacks;
|
|
114
|
+
zero selections over 60 global-null replicates.
|
|
115
|
+
- Feynman-equation panel (12 physics laws, 0.1% noise): 9–10/12 solved (a borderline surrogate flips across numeric stacks) at the
|
|
116
|
+
SRBench criterion and a stable 8/12 in exact symbolic form, ~0.9 s per
|
|
117
|
+
equation; the misses are named boundaries (a depth-3 rational, a literal
|
|
118
|
+
constant, depth-4 nesting, the Gaussian's exponential).
|
|
119
|
+
- Pure-noise stress: BH breached in 3/25 trials (mean FDP 0.12 vs nominal
|
|
120
|
+
0.10, the PRDS caveat materialising on correlated candidates); BY in 1/25
|
|
121
|
+
(mean FDP 0.04). This measurement is why BY is the estimator default.
|
|
122
|
+
- Stress suite (signal among ten distractor columns): beamfeat's
|
|
123
|
+
false-feature rate — returned formulas touching only irrelevant columns —
|
|
124
|
+
is 0.000 at noise levels from 5% to 50% of the signal scale and at n as
|
|
125
|
+
small as 120, with recovery and the guarantee intact throughout; autofeat
|
|
126
|
+
recovers the formula equally often but returns distractor-only features at
|
|
127
|
+
a mean rate of 0.29 (worst 0.75 at 50% noise).
|
|
128
|
+
- Real data (six datasets, numeric columns): best on mpg (0.871 vs LightGBM
|
|
129
|
+
0.853) and tips — where gradient boosting overfits below ridge while the
|
|
130
|
+
FDR gate holds beamfeat at the ceiling — tied with ridge on diamonds from
|
|
131
|
+
one interpretable feature, behind LightGBM on penguins and breast cancer,
|
|
132
|
+
and behind ridge on diabetes (0.376; autofeat 0.303, OpenFE 0.235): no
|
|
133
|
+
nonlinear structure exists there to find, and beamfeat does not invent
|
|
134
|
+
any.
|
|
135
|
+
- OpenFE on the same stress suite (its features + LightGBM, after shimming a
|
|
136
|
+
removed sklearn parameter): false-feature rate also 0.000, but 0/5 formulas
|
|
137
|
+
recovered and lower R² throughout — it optimises tree-model lift, not
|
|
138
|
+
equations.
|
|
139
|
+
- Heavy-tailed noise (Student t(2), infinite variance): exact formula
|
|
140
|
+
recovered at R² 0.956 vs LightGBM 0.866 — the permutation test is
|
|
141
|
+
distribution-free, so heavy tails cost it nothing.
|
|
142
|
+
- Known boundaries, measured: on piecewise targets trees win outright
|
|
143
|
+
(LightGBM 0.998 vs 0.808 — if a tree model beats beamfeat by a wide
|
|
144
|
+
margin, the signal is likely piecewise, not algebraic), and on Friedman #1
|
|
145
|
+
the 0.744-vs-0.964-oracle gap splits into a measured 0.875
|
|
146
|
+
screening-admissible ceiling (the marginally-quiet quadratic is priced by
|
|
147
|
+
the guarantee itself) and a search shortfall below it that is insensitive
|
|
148
|
+
to beam width and diversity settings — stated rather than hidden.
|
|
149
|
+
- Test suite: 370 tests at 95% statement coverage (a 90% floor is enforced
|
|
150
|
+
in CI), including scikit-learn's full estimator-conformance checks.
|
|
151
|
+
- Selection-only baseline (knockpy on raw columns, modified-FDR offset —
|
|
152
|
+
the only satisfiable configuration at these dimensionalities): 0/15
|
|
153
|
+
formulas recovered, core-suite R² at the ridge level (0.845), and a 0.11
|
|
154
|
+
mean false-feature rate on the stress suite — the construction step and
|
|
155
|
+
the holdout are the delta.
|
|
156
|
+
- Independent 315-fit study across nine datasets and seven methods
|
|
157
|
+
(`benchmarks/independent/`): mean R² 0.803 against random forest 0.810,
|
|
158
|
+
LightGBM 0.798, autofeat 0.751, OpenFE 0.736, featuretools −2.478 — and a
|
|
159
|
+
worst single fit of 0.355 where autofeat reached −2.28 and featuretools
|
|
160
|
+
−57.3, at ~48× autofeat's speed, with the FDR flag delivered on 45/45
|
|
161
|
+
fits.
|
|
162
|
+
- Benchmarks (10 synthetic problems with known formulas, 70/30 splits):
|
|
163
|
+
beamfeat mean R² 0.9993, ~0.4 s fits, 9/9 formulas recovered; autofeat
|
|
164
|
+
0.9974, ~9 s, 8/9; LightGBM 0.9798; ridge 0.8442. On the purely linear
|
|
165
|
+
problem — where feature construction should not win — ridge ties beamfeat.
|
|
166
|
+
|
|
167
|
+
## Relationship to autofeat
|
|
168
|
+
|
|
169
|
+
beamfeat's design began from a code review of
|
|
170
|
+
[autofeat](https://github.com/cod3licious/autofeat) (Horn et al., 2019) and
|
|
171
|
+
keeps its best idea — compact symbolic features feeding a linear model —
|
|
172
|
+
while replacing exhaustive expansion with beam search, heuristic
|
|
173
|
+
noise-threshold selection with FDR-controlled procedures, and upper-pinned
|
|
174
|
+
dependencies with tested floors. The generate-then-FDR-filter design has a
|
|
175
|
+
direct precedent in [tsfresh](https://github.com/blue-yonder/tsfresh)
|
|
176
|
+
(Christ et al., 2018), which filters mass-generated time-series features
|
|
177
|
+
under Benjamini–Yekutieli control; beamfeat applies that design to symbolic
|
|
178
|
+
expressions on tabular columns.
|
|
179
|
+
|
|
180
|
+
## Independent comparison
|
|
181
|
+
|
|
182
|
+
`benchmarks/independent/` holds a 315-fit study (nine datasets, seven
|
|
183
|
+
methods, five splits, average ranks plus a Friedman test) with its harness,
|
|
184
|
+
pinned environment, data, and a notebook that reproduces the analysis. Mean
|
|
185
|
+
held-out R²: beamfeat 0.803, random forest 0.810, LightGBM 0.798, autofeat
|
|
186
|
+
0.751, OpenFE 0.736, ridge 0.704, featuretools −2.478 — and worst single fit
|
|
187
|
+
of 45: beamfeat 0.355, autofeat −2.28, featuretools −57.3, with the FDR
|
|
188
|
+
guarantee delivered on 45/45. Read `PROVENANCE.md` first: it records what is
|
|
189
|
+
reproducible (beamfeat, bit-identically) and what is not (autofeat seeds
|
|
190
|
+
nothing internally and swung from +0.955 to −77.86 on one identical split).
|
|
191
|
+
|
|
192
|
+
## Documentation
|
|
193
|
+
|
|
194
|
+
Tutorial notebooks live in `notebooks/` (getting started; search and scoring;
|
|
195
|
+
selection and units). The statistical guarantees are documented in detail in
|
|
196
|
+
`docs/guarantees.md` and in the module docstrings of `beamfeat.selection`. Users coming
|
|
197
|
+
from autofeat will find a full API mapping and version-compatibility notes
|
|
198
|
+
in `docs/migrating-from-autofeat.md`.
|
|
199
|
+
|
|
200
|
+
## Citing
|
|
201
|
+
|
|
202
|
+
See `CITATION.cff`. If you use the selection procedures, please also cite the
|
|
203
|
+
underlying statistics: Benjamini & Hochberg (1995), Benjamini & Yekutieli
|
|
204
|
+
(2001), Barber & Candès (2015), Candès et al. (2018), and Phipson & Smyth
|
|
205
|
+
(2010).
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Benchmark artifacts
|
|
2
|
+
|
|
3
|
+
Every figure in the paper and README traces to a committed script and an
|
|
4
|
+
archived result file. The map:
|
|
5
|
+
|
|
6
|
+
| artifact | produced by | supports |
|
|
7
|
+
|---|---|---|
|
|
8
|
+
| `results.{json,csv}` | `run_benchmarks.py --suite core --synthetic-only --methods ridge lightgbm beamfeat` | core-suite recovery 9/9, mean R² 0.9989, mean 1.7 features |
|
|
9
|
+
| `results_all.{json,csv}` | `run_benchmarks.py --suite all --methods ridge lightgbm beamfeat` | Friedman #1 (0.744), heavy-tail t(2) (0.956), threshold/piecewise (0.808) |
|
|
10
|
+
| `results_robustness.{json,csv}` | `run_benchmarks.py --suite robustness --synthetic-only --methods beamfeat openfe` | stress-suite false-feature rates; OpenFE 0/5 recovery |
|
|
11
|
+
| `results_real.{json,csv}` | `run_benchmarks.py --real-only --methods ridge lightgbm beamfeat` | six-real-dataset panel (mpg, tips, diamonds, penguins, diabetes, breast cancer) |
|
|
12
|
+
| `results_autofeat_venv.json` | `run_benchmarks.py --suite core --methods autofeat` in the pinned environment described in `independent/requirements.txt` | autofeat core-suite comparison (0.9974, ~9 s, 8/9) |
|
|
13
|
+
| `results_autofeat_robustness.json` | as above, `--suite robustness` | autofeat stress false-feature rate (mean 0.29, worst 0.75) |
|
|
14
|
+
| `results_knockpy.{json,csv}` | `run_benchmarks.py --suite all --synthetic-only --methods knockpy` | selection-only baseline: 0/15 recovery, stress false-feature rate 0.11 |
|
|
15
|
+
| `results_knockpy_real.{json,csv}` | `run_benchmarks.py --real-only --methods knockpy` | selection-only baseline on the real panel |
|
|
16
|
+
| `feynman_results.json` | `feynman_panel.py` | physics panel: 9/12 solved, 8/12 exact form |
|
|
17
|
+
| `calibration_study.py` | `python benchmarks/calibration_study.py` | 200-replicate FDR 0.0000 / power 1.000; 60-null zero selections |
|
|
18
|
+
| `friedman_decomposition.py` | `python benchmarks/friedman_decomposition.py` | the 0.964 oracle / 0.875 admissible-ceiling decomposition |
|
|
19
|
+
| `reproduce_all.ipynb` | run top to bottom | orchestrates the tests, both studies, the harness, and the panel in one traceable session |
|
|
20
|
+
| `independent/` | see `independent/README.md` | the 315-fit external comparison study |
|
|
21
|
+
|
|
22
|
+
`beamfeat` rows regenerate bit-identically from a fresh environment;
|
|
23
|
+
competitor rows require the isolated environments the files above name (see
|
|
24
|
+
the dependency notes in `independent/PROVENANCE.md`).
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Independent comparison study
|
|
2
|
+
|
|
3
|
+
A 315-fit comparison of `beamfeat` against `autofeat`, `OpenFE`,
|
|
4
|
+
`featuretools`, and raw-feature baselines (ridge, random forest, LightGBM)
|
|
5
|
+
across nine datasets and five seeds, with average-rank and Friedman-test
|
|
6
|
+
analysis.
|
|
7
|
+
|
|
8
|
+
| file | contents |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `REPORT.md` | the full write-up: protocol, results, statistics, conclusions |
|
|
11
|
+
| `PROVENANCE.md` | what is reproducible and what is not — read before citing |
|
|
12
|
+
| `beamfeat_benchmark.ipynb` | the study as a runnable notebook (`FULL_RUN = False` analyses the archived results in minutes; `True` regenerates them) |
|
|
13
|
+
| `bench.py`, `aggregate.py`, `run_all.sh` | the standalone harness |
|
|
14
|
+
| `requirements.txt`, `patch_openfe.py` | pinned environment and the OpenFE source patch it requires |
|
|
15
|
+
| `data/`, `results_as_reported/` | inputs and archived per-fit results |
|
|
16
|
+
|
|
17
|
+
## Headline figures
|
|
18
|
+
|
|
19
|
+
Mean out-of-sample R² across the nine datasets, and the worst of the 45
|
|
20
|
+
individual fits per method:
|
|
21
|
+
|
|
22
|
+
| method | mean R² | worst fit | mean seconds | mean new features |
|
|
23
|
+
|---|---|---|---|---|
|
|
24
|
+
| random forest (raw) | 0.810 | 0.247 | 0.86 | — |
|
|
25
|
+
| **beamfeat** | **0.803** | **0.355** | **0.61** | 9.2 |
|
|
26
|
+
| LightGBM (raw) | 0.798 | 0.117 | 0.07 | — |
|
|
27
|
+
| autofeat | 0.751 | −2.282 | 29.30 | 17.1 |
|
|
28
|
+
| OpenFE + LightGBM | 0.736 | 0.360 | 4.40 | 7.3 |
|
|
29
|
+
| ridge (raw) | 0.704 | 0.353 | 0.003 | — |
|
|
30
|
+
| featuretools | −2.478 | −57.320 | 0.11 | 104.7 |
|
|
31
|
+
|
|
32
|
+
`beamfeat` reported `fdr_controlled_ = True` on 45/45 fits, recovered the
|
|
33
|
+
generating formula on every ground-truth problem, and produced no fit below
|
|
34
|
+
R² 0.355. Its Friedman #1 result here, 0.745, independently reproduces the
|
|
35
|
+
0.744 measured by this repository's own harness.
|
|
36
|
+
|
|
37
|
+
## Caveats that travel with these numbers
|
|
38
|
+
|
|
39
|
+
Both are documented in full in `PROVENANCE.md`; in brief:
|
|
40
|
+
|
|
41
|
+
1. **One value was inserted by hand.** The `autofeat` Friedman #1 seed-2
|
|
42
|
+
result came from an interrupted run. It affects `autofeat`'s Friedman #1
|
|
43
|
+
mean and slightly affects the omnibus test; it touches no `beamfeat`
|
|
44
|
+
figure. Because `autofeat` does not seed its internal subsampling, a
|
|
45
|
+
re-run yields a different draw rather than the same value.
|
|
46
|
+
2. **Dataset-overlap precision.** The report's comparability note is
|
|
47
|
+
slightly imprecise against autofeat's Table 1: diabetes also overlaps
|
|
48
|
+
(four of this study's four real datasets appear there), and the wine
|
|
49
|
+
dataset here is the red-only variant (1598×11) where autofeat evaluated
|
|
50
|
+
red and white combined (6497×12). Boston housing is included for that
|
|
51
|
+
comparability despite its removal from scikit-learn over documented
|
|
52
|
+
ethical concerns; the repository's own six-dataset real panel contains
|
|
53
|
+
no contested data.
|
|
54
|
+
3. **The protocol favours linear consumers.** Every construction tool feeds
|
|
55
|
+
the same ridge model, which understates `OpenFE` in its intended
|
|
56
|
+
gradient-boosted setting and `featuretools` outside its relational use
|
|
57
|
+
case. `beamfeat` does not win real-tabular prediction against tuned tree
|
|
58
|
+
models on individual datasets (Concrete: 0.845 against LightGBM's 0.932).
|