Choosing a setting
Every fit runs the same search and the same screen, and the screen's FDR guarantee covers the same certified set whatever the setting. What changes is which certified formulas the printed equation uses. The default and two options cover what most fits need:
| you want | write | the printed equation | the cost |
|---|---|---|---|
| the best prediction | BeamFeatRegressor() |
the formulas that predict best; on a noisy target, some extra terms | none |
| a short equation | BeamFeatRegressor(parsimony_alpha=0.05) |
usually one to three terms | some accuracy where the target is not one compact formula |
| every column tested | BeamFeatRegressor(input_test=True) |
no formula with a column whose null the test fails to reject | 2 to 7 times the fit time, and some accuracy on real data |
The same parameters work on BeamFeatClassifier and BeamFeatTransformer,
and the two options combine.
The three on one dataset
The target is x0 * x1 with 25% noise, among ten columns that play no part
in it:
import numpy as np
from beamfeat import BeamFeatRegressor
rng = np.random.default_rng(11)
X = rng.uniform(1, 6, (500, 12))
signal = X[:, 0] * X[:, 1]
y = signal + rng.normal(0, 0.25 * np.std(signal), 500)
for options in ({}, {"parsimony_alpha": 0.05}, {"input_test": True}):
print(BeamFeatRegressor(random_state=0, **options).fit(X, y).equation())
y = 1.052*(x0 * x1) - 0.4088*((x0 * x1) / (x0 + x6)) - 0.1452*((x3 / x0) - x1) + 0.1295*((x5 / x0) - x1) + 0.04642 [4 of 50 certified terms; parsimony=None prints all 50; parsimony_alpha=0.05 prints 1, selection-row R^2 0.935 against 0.939]
y = 1.013*(x0 * x1) - 0.1608 [1 of 50 certified terms; parsimony=None prints all 50]
y = 1.013*(x0 * x1) - 0.1608 [1 of 50 certified terms; parsimony=None prints all 8; input_test dropped 42]
The default adds three terms to the product, and each brings in a column the
target does not use: x6, x3 or x5. Each adds a little to R² on the rows the
equation was chosen on, by fitting noise. The note at the end of the line
says how much: parsimony_alpha=0.05 would print one term, for an R² of 0.935
against 0.939 on the selection rows, and it does stop before them.
input_test drops 42 of the 50 certified formulas, those with a column
whose null the test fails to reject, and the forward step then stops at the
product.
The default: the best prediction
BeamFeatRegressor() adds certified formulas to the equation while each adds
at least 0.001 to R² (parsimony_tol). That is the right rule for prediction,
and on a target with little noise the equation is short anyway. On a noisy
target, though, the best of the many remaining formulas clears 0.001 by
fitting noise alone, so the equation grows terms that each add a little.
On y = x0/x1 among ten unrelated columns, with noise as large as the signal,
the default printed a median of 9.5 terms, and 78 of its 92 terms over ten
fits read an unrelated column.
Keep the default when prediction comes first, and read its equation with
equations(), which
shows how much each term adds. When parsimony_alpha=0.05 would print fewer
terms, equation() ends with a note saying how many, and the R² of both
equations on the selection rows; equations() marks that line a.
A short equation: parsimony_alpha=0.05
The forward step also stops at the first term whose gain fails to reject the
null that chance reaches it: the null distribution is the best gain the
remaining formulas reach against noise. On the noisy x0/x1 above it printed
the one true term in every fit, and its predictions were closer to the true
function than the default's (R² 0.998 against 0.981).
Use it for an equation to read or report, or to look for a law. Real targets tend to carry many modest effects, each adding little, so the step stops after a few terms (one to five on 10 of 12 real datasets) and costs some accuracy: held-out score fell on 9 of the 12, by 0.01 to 0.05, and rose a little on eyedata, where the default fits 31 terms to 84 training rows. It is a stopping rule, not a guarantee on the terms it keeps. A short equation has the details.
Every column tested: input_test=True
The screen tests each formula as a whole, so a formula that attaches an
unrelated column to a true one, such as (x0 * x1) / (x0 + x6) when
y = x0 * x1, is a true discovery. input_test=True tests each column
inside each formula that reads more than one, by permuting that column's
rows, and drops a certified formula when the test fails to reject the null
that the column is independent of the target and of the formula's other
columns.
Use it when every column in the equation must be shown to matter. It takes
2.4 to 7 times as long as the default (3.6 times on the real datasets), more
on more rows, and n_jobs threads share the tests. Held-out score stayed
within 0.005 of the default's on 5 of 12 real datasets and fell on 7, by
0.06 at most. It does not remove riders built only from the target's own
columns, as parsimony_alpha does, and a junk column correlated with a real
one is not null in its sense.
Every column tested
has the details.
Both
BeamFeatRegressor(parsimony_alpha=0.05, input_test=True) gives a short
equation whose every column was tested. It is no more accurate than
parsimony_alpha alone and takes as long as input_test.
Tall data: max_search_rows
The search's time and memory grow with its rows. On a tall table,
BeamFeatRegressor(max_search_rows=10_000) runs the search on 10,000 of its
rows, drawn at random, and the screen on every selection row as before, so the
guarantee is unchanged. On 200,000 rows, a search on 10,000 of its 100,000
rows cut the median fit from 110 to 33 seconds. A product carrying 2% of the
signal's variance, x4*x5, came out as x4 and x5 terms in every fit, as
with the full search, and R² against the signal was 0.002 lower
(benchmarks/search_rows/). Memory fell by a sixth only, since the screen and
the final fit still see every row. A term weak enough to need every row to
stand out can be lost.
A target that switches: sign and sign_shift
A target that follows one formula on one side of a threshold and another on
the other side is outside what products, quotients and smooth transforms
build. The sign_shift operator, off by default, fits sign(x + a) with its
threshold a, so that a term such as x2*sign(x0 - 0.7) can be built:
# The default fitted operators and sign_shift.
BeamFeatRegressor(fitted_ops=("exp_scaled", "pow", "inv_shift", "log_shift", "sign_shift"))
On y = s(x0)*x2 + x1, where s is 2 above a threshold on x0 and -1
below it, it printed the switch term in all 20 fits and predicted the
target to R² 0.997 to 0.999, against 0.76 to 0.88 for the default and 0.983
to 0.996 for gradient-boosted trees; on the main suite's threshold_step it
raised held-out score from 0.841 to 0.999. Where the target does not switch
it changes little, though it lowered the classifier's accuracy on
breast_cancer by 0.022. sign, listed in unary_ops, builds a switch at
zero only, which sign_shift also finds.
Switches at a threshold has the
details.
A law with a weak term: residual_slots=10
The search ranks its candidates against the target, so a term much weaker
than the leading one can be left out of the ranking, as log(x3) is from
x0*x1/x2 + 0.5*log(x3). BeamFeatRegressor(residual_slots=10,
parsimony_alpha=0.05) adds to the ranking the terms that explain what the
leading ones leave, and stops the riders they would otherwise bring. On that
law it printed the exact equation in 33 of 60 fits, the default in none. On
the comparison's 11 real regression datasets its held-out score was within
0.005 of parsimony_alpha alone on 10, and 0.012 lower on California
housing.
A weak term beside a dominant one
has the details.
What was measured
benchmarks/three_settings/
fits the default, parsimony_alpha=0.05, input_test=True and both
together on the same data with the same seeds:
| default | parsimony_alpha=0.05 |
input_test=True |
both | |
|---|---|---|---|---|
| Noise sweep, noise as large as the signal | ||||
| median terms | 9.5 | 1 | 2 | 1 |
| terms reading an unrelated column | 78 of 92 | 0 of 10 | 3 of 21 | 0 of 10 |
| R² against the true function | 0.981 | 0.998 | 0.995 | 0.998 |
| 12 real datasets | ||||
| mean held-out score | 0.693 | 0.675 | 0.675 | 0.667 |
| mean terms | 13.8 | 3.5 | 10.5 | 3.1 |
| better / same / worse than the default | — | 1 / 2 / 9 | 0 / 5 / 7 | 1 / 1 / 10 |
| 18 synthetic datasets: within 0.005 of the default | — | 15 | 17 | 16 |
| Fit time, × the default's | 1 | 1.0 | 3.6 | 3.6 |
| The paper's comparison study: mean R² (paper 0.803) | 0.803 | 0.784 | 0.803 | 0.786 |
The noise sweep is ten fits of y = x0/x1 among ten unrelated columns; the
real and synthetic datasets are those of the release comparison, five splits
each. At the paper's own settings all four recover the same formulas, solve
the same Feynman laws and print no formula built only from distractors, as
the paper reports; the benchmark's report has every table.
Whatever the setting, a single fit shows one equation from one random split
of the rows. stability_report
refits on many splits and shows whether that equation is the only good one or
one of many; run it before reading much into which formula was picked.
Other options that change the equation
Two more options trade length against the guarantee:
parsimony=Noneprints every certified formula, so the guarantee covers the printed equation itself; on wide data that is dozens of terms.parsimony_holdoutre-tests the printed terms on rows held back for it, so the equation is short and the guarantee covers it; it costs those rows.
The printed equation describes both.
When beamfeat is the wrong tool
If a tree model beats beamfeat by a wide margin, your signal is likely
piecewise, not algebraic. A single switch is within reach of sign_shift
(above); for anything more piecewise, use the tree. fitted_ops covers one constant
per operator, fitted one operator at a time; if your law needs several
constants fitted jointly (exp(-a·x)·cos(b·x + c)), constants inside sums
or products (a·x0 + b·x1 under a logarithm), or deeper nesting than the
beam reaches, use a symbolic regressor such as PySR. The
guarantees page states every boundary with the measurement
behind it.