Migrating from autofeat
Examined against autofeat 2.1.3, the current release as of July 2026, in the pinned comparison environment (scikit-learn 1.7.2); the version-compatibility failures below were reproduced on scikit-learn 1.8 and 1.9.
beamfeat's design began from a review of
autofeat (Horn et al., 2019) and
keeps its central idea — compact symbolic features feeding a linear model.
The APIs are close enough that most code ports in a few lines. This page is
a reference for doing so; it is not a claim that one tool supersedes the
other, and autofeat's exhaustive expansion still finds structure a beam
search prunes (see the Where autofeat does better section below).
Version compatibility
If you arrived here from an error, this is likely the one:
TypeError: check_array() got an unexpected keyword argument 'force_all_finite'
autofeat 2.1.3 raises this when transforming or predicting on unseen data
after engineering at least one feature, on scikit-learn ≥ 1.8 — the
argument was renamed ensure_all_finite in 1.6 and removed in 1.8. fit
still succeeds, so the failure appears only when you apply the fitted model
to a test set. Its declared pins (numpy<2.0, pandas<3.0) will also
downgrade a current environment on installation.
beamfeat declares no upper version bounds and its test suite runs
unpatched from scikit-learn 1.6 through 1.9.
Class mapping
| autofeat | beamfeat |
|---|---|
AutoFeatRegressor |
BeamFeatRegressor |
AutoFeatClassifier |
BeamFeatClassifier |
AutoFeatModel, FeatureSelector |
BeamFeatTransformer |
Parameter mapping
| autofeat | beamfeat | notes |
|---|---|---|
feateng_steps=2 |
max_depth=2 |
same meaning: composition depth |
featsel_runs=5 |
(no equivalent) | selection is a single FDR-controlled test; tune target_fdr (default 0.1) instead of a repetition count |
transformations=('1/','exp','log','abs','sqrt','^2','^3') |
unary_ops=("reciprocal","exp","log","abs","sqrt","square","cube") |
all seven exist; beamfeat's default omits exp and cube |
| (no equivalent) | binary_ops=("mul","div","add","sub") |
binary operators are configurable |
units={...}, apply_pi_theorem=True |
units={...} |
accepts pint quantities or strings ("kg", "m / s**2"); enforced at expression construction rather than as a separate theorem pass |
categorical_cols=[...] |
(not supported) | compose explicitly: make_pipeline(ColumnTransformer(...), BeamFeatRegressor(...)) |
feateng_cols=[...] |
(not supported) | subset X before fitting |
n_jobs=1 |
(not supported) | search is typically sub-second; permutation tests are vectorised |
verbose=0 |
verbose=0 |
same |
Fitted attributes
| autofeat | beamfeat |
|---|---|
new_feat_cols_ |
feature_formulas_, or formulas() |
good_cols_ |
formulas() (post-selection) |
prediction_model_ |
model_ |
| (no equivalent) | fdr_controlled_ — whether the returned features carry the FDR guarantee |
| (no equivalent) | selection_report_ — per-candidate p-values, q-values, and screening decisions |
| (no equivalent) | equation() — the fitted model as a readable equation |
Worked example
# autofeat
from autofeat import AutoFeatRegressor
model = AutoFeatRegressor(feateng_steps=2, featsel_runs=5, verbose=0)
model.fit(X_train, y_train)
print(model.new_feat_cols_)
predictions = model.predict(X_test)
# beamfeat
from beamfeat import BeamFeatRegressor
model = BeamFeatRegressor(max_depth=2, target_fdr=0.1, random_state=0)
model.fit(X_train, y_train)
print(model.formulas())
print(model.equation()) # the fitted model, readable
print(model.fdr_controlled_) # check before treating features as vetted
predictions = model.predict(X_test)
Where autofeat does better
Exhaustive expansion does not need to see a signal marginally in order to
generate the right basis. On targets with a strong non-monotone interaction —
Friedman #1 is the standard example — autofeat's brute-force expansion finds
structure that correlation-guided beam search prunes. On four of five splits
of Friedman #1 it reaches R² 0.94 to 0.96 where beamfeat reaches 0.72 to
0.79; on the fifth it returns −103. Take the win on the typical split as real
and the mean as unusable, which is the shape of the trade: beamfeat accepts
a lower ceiling as the price of a guarantee and a floor. Quantified in
Statistical guarantees.
Behavioural differences to expect
- Fewer features. Selection is a hypothesis test rather than a noise-threshold heuristic, and a parsimony step keeps a compact subset of what passes. Expect single-digit feature counts where autofeat returns tens.
- A guarantee, and an honest failure. If nothing passes selection,
beamfeatreturns no constructed features, warns, and setsfdr_controlled_ = Falserather than returning unvetted candidates. Check that flag. - Determinism.
random_statemakes the whole pipeline reproducible. autofeat exposes none, and cannot be made reproducible from outside: its noise-injection screen draws decoy features from the global NumPy generator before its own internal seeding applies, so each process starts from different entropy. Six runs of one identical split returned R² from +0.952 to −109.8. - No constants inside expressions. Neither tool fits them; if you need
exp(-3.2 x), a symbolic regressor such as PySR is the right instrument.