The printed equation
The screen certifies a set of formulas, and the equation is chosen from that
set. By default a forward step adds the certified formula that improves the
fit most, one at a time, and stops when the next would add less than
parsimony_tol (0.001) to R². The options on this page change that choice.
None of them changes the search or the screen, so the FDR guarantee on the
certified set is the same under each. Choosing a setting
compares the default with the two options most fits need, parsimony_alpha
and input_test.
A short equation (parsimony_alpha)
On a noisy target the default keeps riders: the best of the many remaining
formulas adds more than parsimony_tol to R² by fitting noise alone.
parsimony_alpha=0.05 also stops the forward step at the first term whose
gain fails to reject the null that chance reaches it. Here the target is
x0 * x1 with 25% noise, among ten unrelated columns:
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)
print(BeamFeatRegressor(random_state=0).fit(X, y).equation())
print(BeamFeatRegressor(random_state=0, parsimony_alpha=0.05).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]
The default's other three terms attach unrelated columns, x6, x3 and
x5, to the product. At a signal-to-noise ratio of 1 the default printed 18
terms on average where one was true, and the check printed 1.1, with held-out
R² slightly higher on average and a little lower in a few fits.
On real data the check costs accuracy. 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 held-out score fell on 9 of the
12 (diabetes 0.433 to 0.381, tips 0.472 to 0.433). Use it to look for a short
law, not for prediction. It is a stopping rule, not a guarantee on the terms
it keeps; the path in equations() is the same either way, and only the
marked line moves.
Choosing a setting and the
guarantees page have
the figures.
Every column tested (input_test)
The screen tests each term as a whole, so a term that attaches a junk column
to the true one is still a true discovery: ((x0 * x1) / (x0 + x6)) is
associated with the target through x0 * x1, however unrelated x6 is.
input_test=True tests each column inside each term that reads more than
one. The column's rows are permuted wherever it occurs in the expression,
and its null is that it is independent of the target and of the term's
other columns. A screened term is dropped before the forward step when the
test fails to reject the null for any of its columns. On the same data:
print(BeamFeatRegressor(random_state=0, input_test=True).fit(X, y).equation())
y = 1.013*(x0 * x1) - 0.1608 [1 of 50 certified terms; parsimony=None prints all 8; input_test dropped 42]
input_report_ lists every (term, column) pair with its p- and q-value, and
null_rejected marks the pairs whose null the test rejected. A junk column
correlated with a real one is not null in this sense, so its null can be
rejected and the term kept; the
guarantees page states
what the test covers. It is opt-in because its time grows with the rows:
here the fit took about four times as long, and n_jobs threads share the
tests.
The whole certified set (parsimony=None)
parsimony=None skips the forward step and fits every certified formula.
The guarantee then covers the printed equation itself (fdr_scope_ is
"printed equation"), and the equation is long: 25 terms in the
first example of Reading a
fit, dozens on wide data. equation(max_terms=...) shortens what is
displayed without changing the model.
Re-testing the printed terms (parsimony_holdout)
parsimony_holdout holds back that fraction of the selection rows.
Screening and the forward step run on the rest, and the terms they return,
fixed at that point, are tested again on the rows held back; terms whose
null the re-test fails to reject are dropped. By default the re-test is the
screen again (certify_test="marginal"), which tests each term's
association with the target on its own. It rejects the null for a term that
only rides on a true one, since such a term is associated with the target
through the true one. certify_test="conditional" tests each term given the
other printed terms instead:
import numpy as np
from beamfeat import BeamFeatRegressor
rng = np.random.default_rng(0)
X = rng.uniform(0.5, 3.0, (600, 8))
y = 2 * X[:, 0] + rng.normal(0, 0.5, 600) # x0 alone
# Products only, and the whole screened set: every product of x0 with
# another column rides on x0, and the re-test sees them all.
options = dict(max_depth=1, binary_ops=("mul",), unary_ops=(), parsimony=None,
parsimony_holdout=0.5, random_state=0)
marginal = BeamFeatRegressor(**options).fit(X, y)
conditional = BeamFeatRegressor(**options, certify_test="conditional").fit(X, y)
print(marginal.formulas())
print(conditional.equation())
['x0', '(x0 * x7)', '(x0 * x6)', '(x0 * x3)', '(x0 * x2)', '(x0 * x1)', '(x0 * x5)', '(x0 * x4)']
y = 2.009*x0 - 0.003251
The conditional re-test is a permutation test for regression only, and its
p-values are approximate; the guarantees page states its null and what its
validity rests on. It works when the subset it receives is short. On a long
subset whose terms nearly reproduce one another, no term adds anything given
the rest, the re-test rejects no null, and the estimator returns the whole
screened set with only the screen's marginal guarantee
(certification_result_.n_selected is 0, and a NoDiscoveriesWarning says
so). After the forward step that happened in about nine fits in ten at a
signal-to-noise ratio of 1 (benchmarks/parsimony_holdout/). So after the
forward step the conditional re-test needs parsimony_alpha (above), which
brings the fallback under one fit in a hundred; without it the combination
raises a ValueError. The example above
re-tests the whole screened set (parsimony=None) and needs no
parsimony_alpha. Both re-tests cost rows: hold back a fraction only when
the selection rows number a few hundred or more.