std::exception_ptr.stepwise() is linear(), named after the
model it fits as additive() is, and its defaults are
biomod2’s GLM: every column as x + I(x^2),
searched both ways by AIC as MASS::stepAIC() searches, with
no bound on the terms kept. The search is the select
argument ("both", "forward",
"backward" or "none"), formerly
direction. The old defaults were the study’s forward arm,
which is now
linear(select = "forward", terms = "column", max_terms = 3).
The learner registers and reports as linear.maxnet() is maxent(), named after the
model rather than the package whose formulation it follows. Its
arguments, defaults and numbers are unchanged; it registers and reports
as maxent.boosting(newton = TRUE) is
boosting(method = "xgboost"). The flag switched the whole
algorithm, the tree growth, the split criterion, what depth
and min_leaf count and the default set, where its name
described the leaf step alone, which gbm’s trees take as well.
method = "gbm", Friedman’s gradient boosting machine, is
the default as before.year_start defaults to "01-01", the
calendar year, in grain_matrix(), grain(),
multigrain(), grains() and
coverage(), where it was the Schrankogel deposit’s
hydrological year, "09-01". A season or year bin now starts
on 1 January unless a boundary is given;
inst/reproduce/schrankogel.R passes
"09-01".timesift() takes learners for
models, n_inner for inner and
choose for rule; select_grain()
takes n_inner, choose, and
cut_rule for threshold, and its result carries
choose and cut_rule where it carried
rule and threshold; tune() takes
n_inner. A fit’s fitted models are still
fit$models.elasticnet() defaults to cv.glmnet()’s ten
inner folds and s = "lambda.1se", where it took five folds
and "lambda.min". The reproduction driver and the selection
benchmark pass their own settings, so their numbers are unchanged.mars() and discriminant() take
max_terms for nk and min_gain for
thresh; elasticnet() and maxent()
take tol for thresh. A convergence tolerance
and the least gain a forward step is kept for were sharing one
name.preset = "package" is preset = "default"
on tree(), forest() and
boosting().kappa_score() cuts at "prevalence" by
default, the rule the registered kappa metric reads, where
it cut at "youden".train_control(early_stopping) defaults to
Inf, which never stops. It is read only where
val_frac holds a validation set back, and at
val_frac = 0 it was never read.decision_threshold() on a fit takes perc
for the "mpa" rule in both languages; in Python
candidate and perc are keyword-only, so the R
call decision_threshold(fit, "ensemble") errors there
instead of reading "ensemble" as predictions.perceptron() fits one network of a single hidden layer
per response, as nnet fits it and biomod2 fits ANN:
logistic hidden units, a bias and optional skip-layer connections, the
weights fitted by Nash’s variable metric (BFGS) method on the head’s
loss plus a decay on the squared weights. It runs on a new core,
src/ts_perceptron.cpp over the minimiser in
src/ts_quasi_newton.cpp, that both languages call, and from
the same starting weights it ends at nnet’s weights to the bit on the
fixture design. preset = "default" is biomod2’s default
option set (two hidden units, nnet’s decay = 0,
rang = 0.7, maxit = 100) and
preset = "bigboss" its tuned one; tune()
searches biomod2’s ANN grid of hidden and
decay. Under a presence-absence head it fits the
cross-entropy, nnet’s entropy = TRUE, where biomod2 leaves
nnet’s least squares on the logistic output. threads fits
that many responses at once, each network the one it gets alone, and
standardise = TRUE centres and scales the columns first,
which nnet and the default do not.maxent(threads) fits that many responses at once under
either formulation, each the fit it gets alone; it reached only the
absence formulation’s inner folds, so a background fit ran on one
thread. The designs held at once share max_design.elasticnet(), linear(),
tree(), forest(), boosting(),
mars() and discriminant() hand their core
every response at once, as perceptron(),
maxent() and additive() do, and
threads fits that many responses at once. With one response
to fit it goes where it went before: the inner folds, a step’s candidate
fits, a forest’s trees or the column search. tree() takes
threads. Each response is the fit it gets alone, so no
number moves.train_control() takes optimizer
("adamw", the default, "adam" or
"sgd"), a penalty with its alpha,
cito’s lambda and alpha with
alpha read as elasticnet() reads it, and a
schedule ("cosine", the default,
"constant" or "plateau" with
plateau_factor and plateau_patience).
biomod2’s DNN is cito’s dnn(), and both its
option sets are now settings of mlp(); the biomod2 article
spells them out. The defaults train as before.min_score no longer
fails with “numbers of columns of arguments do not match” when an outer
fold’s stack leaves a candidate out. fold_weights has a
column per candidate in every fold, 0 where that fold left it out; in
Python every row carries every candidate, where a fold’s row named only
the candidates it kept.plot() on a range_change()
labels its classes lost, kept, absent and gained, and colours a map
holding only some of them by the class each cell is in.summary() of a run, or an
occlusion() result, returns a plain data frame where it
printed empty headers.plot() on a response curve takes legend,
its position or NULL, and lays more than four responses out
in columns.elasticnet() under block_cv(v = 5), is told to
lower that learner’s n_inner.predict(), plot() and
summary() of a run, build_representation(),
and the plots of a response curve and of a change in range.mlp() is described as what it is, a fully connected
encoder reading the bins flattened into one block; only
cnn() and rescnn() read them as a
sequence.Every learner whose defaults follow a reference package says that
under the shipped presence-absence head it is fitted under
positive_weights(), so a default tree() is
rpart’s specification under those case weights.
hierarchical() says that at spatial = "none"
and random = FALSE it is a penalised logistic
model.
hierarchical() finds the mode of its hyperparameters
with the variable metric minimiser perceptron() uses, where
it carried a second BFGS of its own with a capped step. The mode, and
with it the nodes and the fit, can move at the size of the search’s
tolerance.
In Python a representation, a learner and a training control print as they do in R, where they printed as their dataclass fields and a learner’s functions.
The site’s Python side has a Get started article, the R walkthrough section by section and run against the package, and a reference laid out under the sections of the R reference with an index page, so the two language menus carry the same pages.
The reproducing-schrankogel vignette is gone.
inst/reproduce/README.md is where the reproduction is
documented: how to run each stage, what it costs, and the comparison
with the paper, which the vignette carried an older copy of.
A count response. response = "count" is whole
numbers of zero or more, fitted under the Poisson deviance through a log
link and an exponential output, and read by
neg_poisson_deviance
(regression_metric(y, p, "poisson_deviance")). Every core
that follows a head’s loss takes the family: the penalised fit (glmnet’s
family = "poisson"), the stepwise search and the MARS refit
(glm(family = poisson), MASS’s stepAIC() with
the Poisson likelihood, earth’s
glm = list(family = poisson)), the additive model (mgcv’s
gam(family = poisson), whose scale is known, so the
criterion is the unbiased risk estimator), the tree (rpart’s
method = "poisson", with the gamma shrinkage of a leaf’s
rate as tree(shrink = )) and the boosted trees (gbm’s
poisson distribution, and xgboost’s
count:poisson under newton = TRUE). A forest
cuts a count on its variance and a leaf holds the mean count, the
encoders train under the deviance, and the combiner minimises it.
maxnet(), envelope() and
discriminant() refuse a count head as they refuse any
numeric one. Both languages, pinned core by core in the fixtures, with
the response in count_response.csv.
cv(repeats = ) and
grouped_cv(repeats = ) draw the outer split several times,
each with its own seed, as biomod2’s nb.rep does.
timesift() makes the run once per repeat: a score stays one
fit’s held-out score, scores and cells carry a
repeat column and every repeat’s folds under numbers of
their own, a response is averaged over its folds and its repeats,
estimate is read off all the repeats’ held-out predictions,
and the stack is fitted on the repeats’ out-of-fold predictions
together. oof is a target’s mean prediction over the
repeats, and the models, the per-fold fits and folds are
the first repeat’s. Both languages.
pseudo_absences() draws units where a response is
zero from a pool of background units, by
bm_PseudoAbsences()’s strategies: "random",
"sre" (outside the envelope of the presences) and
"disk" (a distance band from the nearest presence, planar
or on the sphere), with repeated draws. The units a strategy admits are
the same in both languages and pinned in pa_candidates.csv;
which are drawn depends on the language’s generator.
Three response heads for a numeric response:
"continuous", "abundance" (not negative) and
"ordinal" (whole-number classes), fitted under squared
error through an identity output by every learner but
maxnet(), envelope() and
discriminant(). regression_metric() carries
biomod2’s RMSE, MSE, MAE and
Max_error and R^2 and the correlation, and
ordinal_metric() its Accuracy,
Recall, Precision and F1, all
registered by name. Both languages, pinned in
numeric_metrics.csv.
project() applies a fit to one target per cell of a
raster and returns the prediction as a raster with a layer per response:
type = "binary" the binary map at the fit’s own cuts and
type = "spread" the ensemble’s disagreement, which is
BIOMOD_Projection() and
BIOMOD_EnsembleForecasting(). Rasters are
terra in R (in Suggests) and xarray in Python
(the grid extra). range_change() counts the
cells lost, kept and gained between two such maps, as
BIOMOD_RangeSize() does, and returns the map of
codes.
tune() wraps a learner so that, whenever it is
fitted, it searches a grid of its settings by cross-validation inside
the units it is handed and fits the best one. Inside a run each outer
fold chooses from its own training units. What was chosen is on the
fitted model and in the new settings column of the
candidate table. This is biomod2’s BIOMOD_Tuning(). Both
languages. tune(learner) with no grid searches the one
registered for it, which for forest(),
boosting(), mars(),
discriminant(), maxnet(),
envelope() and mlp() is the grid
BIOMOD_Tuning() searches, and
register_tuning() adds a grid for a learner of one’s
own.
response_curve() varies one predictor of a fitted
candidate across the range it takes while the others are held at the
mean, median, minimum or maximum of their cells, and returns the
prediction, as biomod2’s response curves do. A predictor is a channel
(moved in every bin, as a static column is), a bin of a
one-channel representation, or one cell; a second predictor gives a
surface, and for the ensemble spread = TRUE adds the
members’ standard deviation and interval. plot() draws it.
Both languages.
occlusion() reports importance beside
the fall in score: one minus the correlation of the held-out predictions
with and without the part, which is biomod2’s variable importance and
needs no response.
occlusion(fit, "ensemble", over = "channel") reads the
ensemble, withholding a channel from every member that carries it and
combining their predictions by the run’s stack. Both languages.
block_cv() and env_cv() hold a block of
targets out whole, cut on columns of targets by halving at
the median of the widest column, in space or, on scaled columns, in
predictor space. Nothing is drawn, so R and Python return the same
blocks, pinned in blocks.csv. cv(by = )
stratifies on a column of targets instead of the richness
of the response, which is biomod2’s strat
strategy.
table_metric() reads biomod2’s evaluation statistics
off the table of decisions against observations: pod,
pofd, far, sr,
accuracy, bias, or,
orss, csi and ets, at the cut of
any rule of decision_threshold() or at one given. Each is
registered by name, so grain_ladder(metric = "csi") reads
it. boyce_index() is the continuous Boyce index, registered
as "boyce", and
decision_threshold(rule = "mpa", perc = ) is the minimum
predicted area cut. Both languages, pinned in
metrics.csv.
hierarchical() fits a Bayesian logistic model with
an optional nearest-neighbour or Hilbert-space Gaussian-process field
over the targets’ coordinates, named in the new
timesift(coords = ), and an optional intercept for each
unit. Laplace’s method over the coefficients, the field and the
intercepts together, with the hyperparameters integrated on a grid
around their posterior mode, in a C++ core both languages compile
(src/ts_hierarchical.cpp), the nearest-neighbour precision
factored by a sparse Cholesky decomposition under a minimum-degree
ordering (src/ts_sparse.cpp) so the cost follows the number
of locations. The case weights enter every configuration. The
coordinates and the unit names travel with every representation and are
split with its units. Pinned in the fixtures against tulpa’s posterior
mode, empirical-Bayes fit and each node of its nested grid, and in both
suites against the same marginal written out densely. Both languages;
tulpa is not a dependency.
mlp(activation = ) sets the nonlinearity between the
hidden layers: "relu" (the default), "gelu" or
"selu", in both languages.
The article “Coming from biomod2” maps every biomod2 algorithm
and ensemble to a learner or an option of ensemble() and
fits the whole set across weekly, monthly and seasonal grains on one
simulated record.
additive() fits one generalised additive model per
response, which is what biomod2 fits as GAM: mgcv’s
gam(y ~ s(x1) + s(x2) + ..., method = "GCV.Cp"), a thin
plate regression spline of k = 10 basis functions per
column with its smoothing parameters chosen by the unbiased risk
estimator under presence-absence and generalised cross-validation under
a squared-error head. Past 2000 distinct values a column’s knots are the
subsample mgcv draws, R’s sample() after
set.seed(1). A column of fewer distinct values than
k takes as many basis functions as it holds, and one of two
enters linearly, where mgcv refuses both. It runs on a new core,
src/ts_additive.cpp, compiled into both languages: the
basis from a Lanczos eigensolver, penalised IRLS over the Householder QR
of src/ts_glm.cpp, and Newton’s method on the smoothing
parameters with the exact gradient and Hessian. On the contract’s seven
cases it settles where mgcv does to 5e-10 in the criterion
and 1e-8 in the fitted means. Those cases are ones where
the criterion has a single minimum. On the fixtures’ 80 units of weekly
columns, mgcv restarted from four smoothing parameters settled in more
than one minimum on 15 of 20 binomial designs and 1 of 7 Gaussian ones;
mgcv’s own start reads its own parametrisation, so where there are
several the two searches can settle in different ones. On 894 units and
20 columns a fit takes 5.2 s against mgcv’s 3.1 s, and 2.6 s on four
threads, which run the responses at once or, for one response, the
pieces of its derivatives. Both languages.
discriminant() fits one flexible discriminant
analysis per response, which is what biomod2 fits as FDA:
mda’s fda(method = mars), optimal scoring of presence and
absence regressed on mda’s own MARS basis, and the posterior of two
normal classes on the one canonical variate. The basis is mda’s Fortran
marss, not earth’s, and like it runs its forward pass
unweighted; the head’s case weights set the scores and the variate.
calibrate = TRUE predicts through the probit recalibration
biomod2 always applies to FDA, fitted on the fitting units.
degree, penalty, nk,
thresh and prune are mda’s settings under
mda’s defaults, which biomod2 passes unchanged. It runs on a new core,
src/ts_fda.cpp, compiled into both languages, with R’s
normal distribution in src/ts_normal.cpp and a probit link
in the shared glm.fit; on the contract’s five cases it
keeps mda’s terms exactly and its coefficients and predictions agree
with mda’s and with R’s glm() to 1.4e-14. On
560 units and 150 columns a fit takes about what mda takes, and
threads searches the columns at once. Both
languages.
mars() fits one multivariate adaptive regression
spline per response, as the earth package fits it and biomod2 fits
MARS: earth’s forward pass of hinge pairs with Fast MARS,
leaps’ backward pruning by generalised cross-validation, and the kept
terms refitted as a logistic model under a presence-absence head
(earth’s glm = list(family = binomial)) or by least squares
under a squared-error one. degree, penalty,
nk, thresh, minspan,
endspan, fast_k, fast_beta,
prune and nprune are earth’s settings under
earth’s defaults, which biomod2 uses under both of its option sets. It
runs on a new core, src/ts_mars.cpp, compiled into both
languages; on the contract’s ten cases, weighted ones included, it keeps
earth’s terms exactly and its coefficients and predictions agree with
earth’s to 8e-14. A weighted fit reaches the residual sums
earth refits by QR at every candidate knot with Friedman’s running
updates: on 715 units and 471 columns under presence weights it takes
0.1 s against earth’s 17 s. Both languages.
tree() grows one classification or regression tree
per response under rpart’s rules: the Gini index under a
presence-absence head and the sum of squares under a squared-error one,
min_split, min_leaf, cp and
max_depth as rpart.control() has them, and the
cost-complexity table with its cross-validated error. The tree is pruned
back by an inner cross-validation read by prune:
"se_sum", the rule biomod2 prunes its classification tree
by, "one_se", "min" or "none".
preset = "bigboss" takes biomod2’s tuned option set for
every setting left open. It runs on a new core,
src/ts_tree.cpp, compiled into both languages, so the two
grow the same tree; on the contract’s twelve cases it reproduces rpart’s
node table, complexity table, cross-validated error and pruned
predictions to 1.1e-16. Both languages.
boosting() fits one gradient boosted model per
response, gbm’s trees by default and xgboost’s exact greedy second-order
trees under newton = TRUE, which are what biomod2 fits as
GBM and XGBOOST. n_inner folds
choose how many trees are kept as gbm’s cv.folds does, and
preset = "bigboss" takes biomod2’s tuned option set for
either. It runs on a new core, src/ts_boost.cpp, compiled
into both languages. With every unit in the bag it reproduces gbm’s
predictions and cross-validated error to 6.7e-16, and
xgboost’s predictions to its single-precision storage; the subsample and
the column draw come from the forest’s generator and are pinned against
a fit grown from the contract’s text in R alone. On 894 units and 471
columns, one thread, biomod2’s tuned gbm fit (2500 trees of seven
splits) takes 56 s against gbm’s 68 s, and xgboost’s defaults 1.2 s
against xgboost’s exact method’s 2.6 s. Both languages.
maxnet() fits one maxnet model per response, which
is what biomod2 fits as MAXNET: maxnet’s linear, quadratic,
product, hinge and threshold features, chosen by the presence count as
maxnet.formula() chooses them or named in
classes, its regularisation of each feature times
regmult, and a lasso over them on the penalised core
elasticnet() runs on, so the two languages fit the same
model. formulation = "background" is maxnet’s
presence-background model and predicts its cloglog output;
formulation = "absence" reads the absences as absences, a
logistic lasso over the same features under the head’s case weights with
the penalty chosen by an inner cross-validation. On the contract’s
twelve cases the features and penalty factors are maxnet’s to
7.4e-16 and the objective glmnet’s to 5.5e-14.
The core is src/ts_maxnet.cpp. On 900 units a weekly
three-channel representation, 47,100 features under maxnet’s
"lqh", fits in 8 s; a design above max_design
gigabytes is refused with its size. Both languages.
forest(balance = TRUE) is the down-sampled forest
biomod2 fits as RFd: each tree draws as many units from
each class as the smaller class holds. forest() also takes
preset ("package", randomForest’s defaults, or
"bigboss", biomod2’s tuned set) and threads.
Both languages.
ensemble() takes biomod2’s ensemble set.
ensemble("committee", rule =) is committee averaging: each
member cuts each response at its own decision_threshold(),
learned from its out-of-fold predictions, and the combination is the
share of members voting presence; inside timesift() each
outer fold’s cuts are learned on its inner out-of-fold predictions, so
the estimate reads no cut chosen on the targets it scores.
min_score leaves out the candidates scoring below it before
scope picks, and
ensemble("weighted", decay = d) weighs each rank
1 / d of the one above it, as EMwmean.decay
does. Both languages.
ensemble_spread() and
predict(type = "spread") read how far an ensemble’s members
disagree: their weighted mean, standard deviation, coefficient of
variation and t interval at alpha, biomod2’s
EMcv and EMci. On one target per map cell it
is an uncertainty map. Both languages.
envelope() draws biomod2’s surface range envelope,
SRE, around each response’s presences: every column’s
quantile and 1 - quantile quantiles over the
presences, R’s type 7 as bm_SRE() reads them, and a
prediction of one where every column lies inside its band. On the
contract’s six cases its bounds and projections are
bm_SRE()’s to the last bit. The core is
src/ts_envelope.cpp. Both languages.
stepwise() takes
direction = c("forward", "both", "backward", "none") and
terms = c("column", "power"). "both" and
"backward" are MASS’s stepAIC() step for step,
and terms = "power" makes each power of a column a term of
its own, as biomod2’s quadratic formula writes them;
direction = "none" with it is the model biomod2 fits as
GLM, x + I(x^2) over every column.
max_terms = Inf lifts the budget, and threads
runs one step’s candidate fits at once. On the contract’s thirteen cases
the core chooses MASS’s, glm()’s and the R oracle’s terms,
in the same order, with the deviance to about 1e-14. The
defaults are the forward search over column terms it always ran. Both
languages.
stepwise() runs on a new core,
src/ts_stepwise.cpp, compiled into both languages: R’s
glm.fit iteration over LINPACK’s dqrdc2, which
sets the rank R reports, and MASS’s step rules. It replaces
stats::glm() in R and a separate NumPy fitter in Python,
which differed on what they refused: R took any fit that settled within
25 iterations, while Python also refused one whose fitted probabilities
came within 1e-8 of zero or one. Both now refuse only a fit
that has not settled, as R did. On the fixture’s design and on simulated
records the core chooses the columns R’s glm() search
chose, with predictions equal to about 1e-15. A fit saved
from an earlier version does not predict under this one; refit
it.
A stepwise() fit names every response whose final
model did not settle in stopped.
A "weighted" ensemble in Python gives every member
the same weight where none scores above zero, as R’s always did, where
it used to stop; ensemble(metric =) in Python now
recomputes the scores it weighs and filters by, as R’s does, and a
scope’s best candidate is the first offered among any on its score on
both sides.
forest() is grown by the core
src/ts_tree.cpp on both sides, in place of ranger in R and
scikit-learn in Python. Its split search is the tree’s, and its
bootstrap and column draws come from one generator the contract defines
(SplitMix32 seeding xoshiro128**, one stream per tree), so R and Python
grow the same forest, bit for bit, on any number of threads. The forest
is asserted node for node against one grown from the contract’s text in
R alone. ranger leaves Suggests and the Python sklearn
extra is gone. This changes forest()’s numbers on both
sides. Scored by TSS on the vignette’s simulated record, on the
vignette’s folds, the old forest and the new one read 0.718 and 0.733
weekly and 0.737 and 0.752 monthly on 60 plots, and 0.762 and 0.760
weekly and 0.748 and 0.742 monthly on 300, every difference inside one
standard error. On simulated presence-absence data its held-out AUC is
level with randomForest’s under the same settings (a mean difference of
-0.0025 over ten draws, paired t -0.62). One forest on 894 units and 471
columns takes 5.3 s on one thread against ranger’s 3.3 s. Nothing in the
reproduction or the selection benchmark fits a forest.
min_node is the fewest units each side of a split
keeps, randomForest’s nodesize; it had been ranger’s
smallest node split in R and scikit-learn’s smallest leaf in
Python.
Python’s internal module timesift.tree is
timesift._tree. Importing the submodule replaced the
exported tree() on the package with the module.
The Python source distribution carries
src/ts_tree.cpp and src/ts_tree.h, which a
wheel built from it needs.
predict() on a fit takes type = "binary",
cutting each response into presence and absence at the threshold learned
under rule ("youden", "kappa" or
"prevalence") from the same candidate’s out-of-fold
predictions of the fit’s own targets. For the ensemble those are the
members’ combined under the refitted stack’s weights. A binary map is
this prediction on one target per map cell. Both languages.decision_threshold() takes a fit in place of the
response and returns one cut per response, the cut
type = "binary" applies. In R it is an S3 generic with a
method on a fit.weights argument is
handed the response head’s case weights, a matrix of the response’s
shape, as one declaring head, control or
group is handed those. A learner of one’s own now fits a
rare response under the weight every shipped learner fits it under. Both
languages.as.matrix() on a representation lays it out as the
block of predictors the tabular learners read, one column per bin of
each channel, named channel@bin, and one column per static
predictor. It is Python’s flatten(), which was already
exported there. The example in ?learner used to average the
bins away and now reads this block.grain_contrasts() fits
score ~ grain + (1 | variable) + (1 | fold) by restricted
maximum likelihood, reads each contrast’s degrees of freedom as lmerTest
does and adjusts by the multivariate t as emmeans’ "mvt"
does, written out in numpy with scipy’s integrator and optimiser beside
it; on the pinned scores it returns R’s differences to 1e-6 and R’s
intervals and p-values to the integrator’s error.
simulate_records() builds the same design as R’s, anchors,
weights, driver scale and link, and draws the units from numpy’s stream.
plot() draws a ladder, a run or a selection on matplotlib
in R’s colours and returns the table it drew from. scipy and matplotlib
are the contrasts and plot extras of the
wheel, and the numpy-only install is unchanged.grain_contrast_cells.csv,
grain_contrast.csv and simulate_design.csv
join the contract’s fixtures, and both suites assert them.inst/reproduce/schrankogel.R runs the paper’s headline
arm, the eleven-member ensemble with its window chosen inside every
outer training set, as the ensemble_selection stage. The
eleven members pinned to one window are one learner, whose prediction is
their mean probability, and the stage is select_grain()
over the eleven rungs the study chose among: the window mean at all
seven windows and the coldest-day, mean and warmest-day reading from the
weekly window up, on the study’s five inner folds by AUC. It is compared
with the published 0.889 AUC and 0.729 TSS. The ensemble’s members keep
the paper’s seeds at --seed=1 and move from them by
seed - 1 at any other, so a run per seed refits the
ensemble too. The reproducing-schrankogel vignette carries
the same call (#82).calendar_channels() gains cycles.
"day" adds day_sin and day_cos,
the place of each bin’s midpoint in the UTC day, beside or instead of
the year’s pair, in the order named. It is read in UTC for the reason
the year is: a zone’s offset and a site’s longitude are the same shift
for every bin, where a clock’s summer time would move the phase by an
hour twice a year. It is refused on bins a day or more apart, where
every bin would sit at one place in the day. One definition in the
shared core serves both languages, and the fixtures pin it as they pin
the year’s.calendar_channels()
at their own amplitude, where they used to standardise them like a
reading. The channels lie on the unit circle already, and being
identical across units they reach a network as a bias, whose step under
AdamW grows with the scale of the input carrying it; standardising
multiplied them by 1.41. At the hourly rung they are four of the fully
connected encoder’s five channels, and on the Schrankogel record they
cost it 0.044 TSS against the study, which fed them unscaled.
calendar_channels() records its channels in a
position attribute (a field in Python), and
bind_channels() carries position and
static from every argument; R’s used to drop
static and Python’s to keep the first argument’s alone
(#81).plot() of a select_grain() result drew
empty axes. The inner scores were matched to their candidates on a label
joined by a space where the candidates’ was joined by a bar, so every
line and every circle was placed at no position. One helper now writes
the label wherever a selection’s tables are matched, the bottom margin
is sized to the longest candidate name, and the table returned says
where each score was drawn and which one its fold selected (#79).occlusion() with substitute = "permute"
permuted each channel of a held-back bin separately, so a unit could be
shown the coldest day of one unit beside the warmest day of another, a
reading no unit made. A bin is now held back whole, by one permutation
per draw. A channel that is the same for every unit, as the calendar
channels are, is left in place when a bin is held back;
unit_mean used to replace it too, which took the bin’s
place in the year away along with its reading (#79).occlusion() scored every cell, including the
ones the scorable-cell mask leaves out, so its weights could rest on
cells R’s did not. It now reads the mask as every score does (#79).grain_contrasts() read each grain’s name back out of
the contrast label emmeans writes, which puts parentheses around a level
holding a character it treats as an operator: a grain called
month-mean came back as (month-mean). The name
is now taken from the factor’s levels (#79).grain_contrasts() stopped with “undefined columns
selected” on a ladder of more than 3000 scored cells, which the
Schrankogel grid is at 7070 per architecture: emmeans stops computing
Satterthwaite’s degrees of freedom past that size and names its interval
columns differently. The limit is now the ladder’s own size (#79).grain_contrasts(): a planted effect
per grain is recovered and its intervals cover it at their stated rate
over twenty seeds, and with no effect the contrast finds none
(#79).inst/reproduce/schrankogel.R compares the stepwise arm
and the network grid with the paper, where it used to write them without
a reference (#80). The stepwise arm is fitted unweighted, as the study’s
forward selection was, under a head that differs from the shipped one in
that alone. The eleven-member ensemble is the study’s: each member at
its own window, channels, kernel, dropout and seed, where the driver
used to run one generic set at every window. The hourly rung reads the
five channels the study’s hourly networks read. The grain contrast is
read in AUC against the window the study took as each architecture’s
best, as its table is.elasticnet() is one implementation in both languages.
The elastic net is now src/ts_penalised.cpp, compiled into
the R package and into the Python extension beside the binning, in place
of glmnet::cv.glmnet() on one side and scikit-learn’s
LogisticRegressionCV on the other. Those were different
algorithms: a different optimiser, a different parametrisation of the
penalty, ten penalty values against a hundred, and different conventions
for standardisation, so the same call returned different coefficients in
the two languages. The penalised arm is what the networks are measured
against, which made it the worst place for the twin to diverge.
glmnet and scikit-learn drop out of the
learner’s needs; scikit-learn is still what
forest() fits on (#76).1e-14 the path’s length
and its penalties agree exactly, the cross-validated
lambda.min and lambda.1se are the same point
of the same path in all twelve fixture cases, and the penalised
objective sits between 3e-13 and 2e-7 of
glmnet’s. Coefficients agree to 2e-6 where no two columns
are collinear and to 1e-4 on the collinear fixture design,
where the split between two nearly identical columns is not determined
to the precision the fit is. inst/spec/representation.md
states all of it, and inst/spec/fixtures/penalised_*.csv
hold glmnet’s own numbers for both suites to assert against.elasticnet() gains threads, how many fits
of one response’s inner cross-validation run at once. The path on every
fitting unit and the path of each inner fold are one independent fit
each, reading the design and sharing nothing, so they parallelise
without any of them seeing another: the result is bit-identical to the
serial one, which both suites assert. The default is serial.elasticnet() gains n_lambda and
thresh, and s now reads on the Python side
too: the whole penalty path is kept on both, so
"lambda.1se" or a penalty of a caller’s own naming a point
between two of the path’s is read the way predict(s =)
reads one.saveRDS() and pickle
and predicts on a machine that has neither glmnet nor
scikit-learn.inst/reproduce/schrankogel.R --stages=baseline against the
deposit gives 0.68605 on the 188 aggregates, 0.00095 from the published
0.687 and inside the 0.002 the driver compares at.thresh = 1e-8, one notch tighter
than glmnet’s default. The path reads the columns once per penalty
rather than twice, a binomial fit ends each penalty on the one
reweighting the next penalty opens with, and a reweighted least squares
step is judged on its own move, which is glmnet’s test (#77). A Gaussian
fit folds the root of each case weight into the design, so a
coordinate’s step reads two streams where it read three.cv.glmnet at its default on the same run: 0.47 s against
0.47 to 0.51 s for a binomial fit on the Schrankogel shape (894 units,
942 columns, five inner folds), 264 ms against 228 ms for a Gaussian one
there (190 ms at glmnet’s own threshold of 1e-7), and 0.12
s against 0.25 s for the four-response learner on a small
ill-conditioned design (60 units, 18 columns). The reproduction’s
elastic-net row reads 0.68606 and runs in 21 minutes.stalled, a
cross-validation records each fold’s in fold_stalled, and
elasticnet() names every response whose path ended that way
in stopped, beside unfitted (#78).train_control() holds no inner validation set back by
default (val_frac = 0, was 0.15): an encoder trains every
fitting target for the whole epoch budget and keeps the last epoch,
where the cosine schedule has annealed the learning rate to zero. On the
Schrankogel weekly three-channel arm that epoch reads 0.8803 AUC against
0.8729 for early stopping on a 15 percent split and 0.8813 for the best
epoch read on the test folds. early_stopping = Inf with a
split held back still restores the epoch of lowest validation loss
(0.8760 there). The study’s rule is
train_control(val_frac = 0.15, early_stopping = 10), which
the reproduction driver passes. On both sides.weights now takes (y, fitting), with
fitting marking the rows the model is fitted on, and
positive_weights() gains the same argument. Read
unweighted, the validation loss of a presence-absence fit is dominated
by the absences of the rare responses the fit was told to weigh up and
stops at an earlier epoch: on the Schrankogel weekly three-channel arm
the change is worth about 0.003 AUC, which is the gap to the analysis
pipeline’s encoder (#75). On both sides.val_frac holds back is now
drawn by a plain random permutation, not by one unit from each of
n_val equal-count strata of the response total. Five seeds
each of the fixed Schrankogel weekly three-channel arm, package recipe
otherwise unchanged: the plain draw reads 0.8721 AUC against the
stratified draw’s 0.8674 (+0.0047, Welch p = 0.019), the same margin the
validation-loss weighting is worth. The stratified draw guaranteed a
rare response a presence in every validation set; the plain draw can
still miss one, and no longer guarantees it.
train_control(val_frac = 0.15, early_stopping = 10) is
unaffected by name, only by what the split under it now does (#75). On
both sides.timesift() now estimates the procedure it runs. Inside
each outer fold of resampling the training targets are
split again into inner folds (5 by default), every
candidate is cross-validated there, rule chooses one on its
inner score and the stack’s weights are fitted on the inner out-of-fold
predictions; the choice and the weights then predict the outer test fold
once. estimate holds the held-out score of the selected
candidate and of the stack under every registered metric,
selected and inner the choice in every outer
fold, fold_weights the weights, and
predictions both held-out prediction matrices. The
candidates’ scores on the outer folds are unchanged and are reported
apart from the estimate, because the best of them was picked out on the
folds it is scored on. inner = NULL compares the candidates
without an estimate. timesift() and
select_grain() share one inner search and one refit, so on
the same candidates, fold map, inner count and seed the selected arm is
select_grain()’s selection. On both sides.summary() prints the
candidates and the procedure as two tables; the Python side’s
ensemble_row() is replaced by
procedure_table().predict(candidate = "selected") predicts with
choice, the candidate the rule takes on every target.roc_auc unless a
metric is named, and score_predictions() defaults to it
too. TSS read at its best cut is inflated by an amount that depends on
how a model’s predictions are distributed, so it stays beside AUC rather
than ahead of it. On both sides.average_precision(), registered as a metric and pinned
in the metric fixtures. On both sides.inst/reproduce/reference.csv carries, per
species, the analysis pipeline’s five-inner-fold selection and its
weekly series elastic net under AUC and TSS, and the spread of a single
fitted encoder at that species over the pipeline’s eleven runs of the
fixed weekly arm; reference_runs.csv carries those runs’
levels. Every tolerance is derived from those two files rather than
written into the script: a level or a margin of the encoder is two
fitted runs against each other, so its tolerance is three times sqrt(2)
times the spread of the level, and a species is held to the same rule at
its own spread, with the check being how many of the 101 sit inside. Run
on the deposit with the study’s fold map and inner partition on a
graphics processor (dev_notes/repro-lisc/ holds the run’s
files): the series elastic net reproduces to +0.001 TSS and +0.0002 AUC
with every species inside; the selection lands on a weekly candidate in
nine of the ten outer folds and on a monthly one in the tenth; the
procedure’s level is 0.0065 AUC and 0.0086 TSS below the pipeline’s,
inside the tolerances of 0.0087 and 0.0145, with 100 and 99 of 101
species inside their own bands; and its margin over the series elastic
net is +0.002 AUC against the pipeline’s +0.009, inside the tolerance
but, unlike the pipeline’s, not separated from zero across species.interval = "nested_cv" is the mean squared
error of the bias-corrected estimate it is centred on, from a nested
cross-validation one level further down (#73). The paper’s width is the
mean squared error of the plain cross-validation estimate and adds its
bias correction to the centre as a shift with no spread of its own; in
the package’s benchmark that correction moved from sample to sample by
more than the estimate it corrects where the sample was small or the
signal absent, and the paper’s interval covered a nominal 95% at 0.86 to
0.93 on the two penalised cells rerun at ten repetitions (300 units, no
signal and the weekly lag; 174 and 183 replicates) and at 0.84 to 0.96
across the twelve cells at one repetition. Inside every outer training
set the same nested cross-validation now runs once more, over the
unordered triples of outer folds, which gives the bias-corrected
estimate that training set alone would report, and the squared gap
between that and the held-out fold’s score is what the width is read
off; its root is held between the corrected centre’s own jackknife
standard error and sqrt(K) times it, where the paper holds the plain
estimate’s root between the plain estimate’s. On the package’s cheap
recovery design (150 units, five outer folds, one repetition, 200
replicates) the interval covers a nominal 95% at 0.96 with a signal and
0.94 without, against 0.955 and 0.895 for the paper’s width around the
same centre. The paper’s width is kept beside it as
se_bates in nested_cv, and the benchmark
writes both intervals and reports both coverages. Nested
cross-validation needs four outer folds now rather than three. On both
sides.select_grain(rule = "coarsest_adequate") takes,
inside each outer fold, the coarsest candidate whose inner score lies
within one standard error of the highest, where "argmax",
still the default, takes the highest. Coarser is fewer bins, then fewer
channels. The standard error is the spread over the inner folds of each
fold’s score, so the rule reads nothing the selection did not already
compute. Where the inner profile is flat it returns the least storage
the record can be kept at without a measured loss inside the training
data; where one candidate separates by more than a standard error the
two rules agree. Every outer fold now reports the highest inner score
(inner_best) and its standard error (inner_se)
beside the chosen one, and inner carries each candidate’s
standard error. On both sides.
select_grain(threshold = "youden") learns a
presence-absence cut inside the training data. Each outer fold takes one
cut per variable from the inner out-of-fold predictions of the candidate
it selected, which the inner search already made for every outer
training unit and for no other, by decision_threshold()
under the rule named, and reads its test fold at that cut, frozen. The
estimate gains a row tss_inner_cut, and the selection
carries thresholds and the per-cell
cut_scores. tss() takes the cut it is read at
as threshold, left NULL for the maximum over
cuts as before. On a binormal design with a planted skill of 0.60 the
learned cut reads it back within Monte Carlo error, where the maximum
over cuts on the same cells does not. On both sides.
An interval for the procedure’s risk, by the nested
cross-validation of Bates, Hastie and Tibshirani (2024).
select_grain(interval = "nested_cv") cross-validates each
outer training set again over the remaining folds of the same map, over
repeats maps, and reports the estimate’s mean squared error
the way the paper’s Algorithm 1 does, rescaled and bounded as its
section 4.3.2 states and centred on its bias correction;
final is then the procedure fitted on every unit, whose
risk the interval is for.
grain_ladder(interval = "nested_cv") does the same for
every arm, and paired_contrast(interval = "nested_cv")
reads it on the difference between two. The paper’s error is a mean of
per-unit losses, so a fold’s score here is the mean over the variables
scorable in it and the variance of that score is its delete-one
jackknife variance, which for a mean of per-unit losses is exactly the
paper’s var(e) / |I_k|.
The estimate and the contrast now name what their interval is
for. The across-variable interval is still reported and now carries
interval = "variables", lower and
upper: it is the spread across the response variables of
one dataset, all of them fitted and scored on the same units and folds,
and not an interval for a new sample. On a simulated design with a
measured truth (150 replicates, five outer folds, AUC) it covered 0.925
of the time with an error-SD to standard-error ratio of 1.23, where the
nested interval covered 0.955 at one repetition (#73).
inst/reproduce/schrankogel.R runs the
demonstration’s own procedure through the public interface: a
selection stage searching the study’s 33 candidates, a
window and a summary each, with select_grain() on the
convolutional encoder, over the study’s outer fold map and its own inner
partition, which ships beside the script as
inner_folds.csv. Beside it a series arm fits
the penalised model on the weekly coldest-day, mean and warmest-day
reading of the record rather than on summaries of it, which is the
comparison the demonstration is read against. Every comparison with a
published number states its tolerance before anything is fitted and
lands in checks.csv; run.meta now records the
torch and libtorch versions and the device; and
--smoke=<folds>,<species> runs the stages on a
few of each, names its output smoke_ and compares nothing
(#74).
grain_matrix() documents what a day is in a zone
that keeps daylight saving time: a wall-clock day, 23 hours on the day
the clock goes forward and 25 on the day it goes back, with the week and
the month holding them an hour shorter or longer, and 24-hour days on a
record kept on a fixed offset or carried in one such as
"Etc/GMT-1". Nothing in the binning changed; a test on both
sides pins the Europe/Vienna transitions of 2021 at both
offsets.
coverage() lays the binning out as a count of
readings per (unit, bin), over every bin the calendar tiles
the record with. grain_matrix() refuses a record where a
unit misses a bin rather than padding it, and this is the table that
decision is made on: a logger that started late, stopped early or lost a
month is a row with zeros in it, and a bin the whole record skips is a
column of zeros. Nothing here fills a cell. On both sides, and pinned by
inst/spec/fixtures/coverage.csv.
paired_contrast() takes each arm whole, as
"grain|learner". A learner named alone used to take its
best grain, chosen on the held-out scores the contrast was then read
off, so the difference was one between two maxima and its p-value
optimistic by an amount nothing in the output recorded. It is refused
now, with a message naming the whole arm; select_grain()
chooses a grain on inner folds and contrasts the selection through
compare. occlusion() still reads a learner
named alone at its best grain, since it describes a fitted model rather
than testing a difference. On both sides (#67).p_method is
"exact" below fifty per-variable differences holding no
zero and no tie, and "normal", the approximation with
continuity and tie corrections, otherwise. The Python side took the
exact distribution where the differences held a zero and R the
approximation; both take the approximation now.
inst/spec/fixtures/contrast.csv pins the interval and the
method, and the Python side carries its own Student’s t quantile, which
agrees with qt() to within about 1e-14 (#68).loss and an activation,
and they are what every shipped learner fits toward. The encoders train
under the loss and predict through the activation, the per-response
learners take the family the loss names, and the combiner minimises the
same loss. A registered head other than presence-absence is therefore
fitted as itself rather than as clamped binary cross-entropy under
another name.fit that declares a head argument is
handed the head, as one that declares control is handed the
control. Both are read from the fit’s own signature, at the one point
every fold loop fits through, so a learner whose fit takes
(x, y) is called with (x, y).elasticnet(), stepwise() and
forest() fit the Gaussian family where the head’s loss is
squared error.ensemble() left without a response takes the run’s
head; one naming a different head is refused before a candidate is
fitted.elasticnet() deals its inner folds for each response
and stratified on it, so a rare outcome is spread over them as evenly as
its count allows rather than wherever a plain deal dropped it. A
presence-absence response whose inner training sets cannot each hold two
of each outcome, the fewest a logistic path is fitted to, is predicted
its share among the fitting units, as a response holding one outcome
already was, and the fit names it in unfitted. Such a
response used to stop the whole run from inside
cv.glmnet(), which cost a benchmark cell a replicate it
could not recover by rerunning. The inner fold draw is not the one
before, so a penalised fit’s chosen penalty can differ from an earlier
run’s. On both sides (#72).cv.glmnet() chooses on the weighted deviance. It was
choosing on accuracy, scikit-learn’s default, a step function of the
penalty whose choice moved by orders of magnitude with the fold
draw.models takes one learner as well as
a list of them, which is the form R has always taken; a single learner
was reaching the fitting layer as a sequence of nothing.joint or separate, and the three that fit one
model per response already did that inside their own fit. The fitting
layer was splitting the responses as well, so the flattened block of
predictors was rebuilt once for every response: on the published grid,
101 copies of a 47 MB matrix per fold and per arm. multi is
now what the learner says it does with the matrix and what a report says
of the candidate. The wrapper that reassembled the columns is gone from
both sides, and with it the Python CandidateFit._check_bins and one _check_grain; and one
baseline prediction per fold in the occlusion, which was rebuilding an
encoder from its arrays once per response.saveRDS() on a fit copied every function that produced it
and a fit read back after an upgrade rebuilt the old encoder and loaded
the new weights into it. A fitted encoder now stores the name of its
module builder, and a fit stores the name and the settings of its
learner wherever the registry can rebuild it; a learner defined outside
any registry still travels whole, because there is nowhere else for it
to come from. The Python side stores the encoder’s name for the same
reason.NA on a cell
whose predictions are NaN or infinite, which is the same
NA a one-class cell returns, so a network whose training
diverged was reported as an unscorable cell and dropped from the
combiner without a word; the stack was then fitted on fewer cells than
the mask said and nothing showed it. The combiner is now fitted on every
scorable cell and refuses to drop one. score_predictions()
refuses the same predictions on both sides.grain_ladder() and select_grain() take a
control, so the training settings of a ladder are given
once for the run rather than restated on every neural learner in it. A
selection hands the same control to the inner search and to the refit,
and a learner carrying settings of its own still overrides it on the
ones it names.elasticnet() and forest() both
did. Fitting a response alone, or beside others, or in a different order
now gives the same model, which
tests/testthat/test-variable-seeds.R holds to. This changes
the numbers those two produce; stepwise() spends no
randomness and the encoders cover the responses jointly, so neither
moves.metric takes a registered name or a function of
(y, p) everywhere the contract says it does. What scores
and what a report prints travel with the fit, so a function reads as
<function> in a report rather than as whatever each
language calls an anonymous one, and the occlusion profile rescores
under the metric the fit was scored with. select_grain() is
the one door that takes a name only, because it reports its estimate
under every registered metric and selects on a row of that table.timesift() sorts the targets by
identifier before it builds the fold map, so such a vector was landing
on whichever unit had taken that position.elasticnet() takes s, the point of the
penalty path to predict at. It also raises what cv.glmnet()
raises rather than turning any failure into the response’s own mean, and
the forward search leaves out a column holding a single value rather
than reaching a fitter’s error on it.elasticnet() honours its mixing –
the scikit-learn floor is 1.8, where l1_ratios alone
selects the elastic net – and standardises its design before penalising
it, as glmnet does. The scaler travels with the fit.calendar_channels() reads the shared core. The year
fraction, the midpoint of the record a bin holds and the calendar the
year is read on used to be written once per language, and the two were
written differently at the two points that decide the number: R took the
phase through sinpi() and the midpoint as a double, Python
took sin(2 * pi * x) and truncated the midpoint on
datetime64. inst/spec/representation.md now
defines all of it, and channels_digests.csv and
channels_guards.csv pin it: what is digested is the year
fraction, which is arithmetic on the calendar, with a stated tolerance
of 1e-12 on the sine and the cosine, which are the platform’s library.
Both suites read the fixtures.
calendar_channels() refuses a lookback, whose bins
are placed relative to a target rather than on the calendar and have no
position in the year. It used to reach for a bin_start that
is not there.
bind_channels() reads every argument as a
representation rather than only the first, and both languages number the
arguments from one and raise the same message. A bare array reached the
R side and came back carrying attributes describing a binning it had
never been through.
A supplied calendar is checked before its bins are read as bins: a bin begins at or before every reading it holds, and a bin’s readings are a stretch of the record. A calendar shifted by one boundary, and one interleaving consecutive readings, used to produce an array that looked like any other.
An identifier is written by one rule in both languages. A whole number is its digits, a character id is itself, a factor is its label, and anything else is refused naming the column.
A time column carrying a zone names the calendar to bin by, in
grain_matrix(), coverage() and
lookback_matrix() alike; a tz naming a
different zone beside it is refused rather than silently
preferred.
The lookback’s target table is read by name, id and
at, as every other alignment in the package is, and a
lookback reaching the fitting layer without an anchor is refused with a
message that says so.
A reading that is not a finite number is refused by the shared
core, naming the unit and the instant of the first one, so the rule is
written once rather than once per wrapper. digest_array()
refuses an array that is not finite for the same reason the contract
gives.
A record and any permutation of its rows now reduce to the same bytes. Both reductions walk the record by unit and then by instant; the calendar reduction accumulated in the caller’s order, which moved a mean in its last bits under a shuffle that changed nothing about the record.
The native grain is read on the instant rather than
on the local clock, so the two readings of the hour a zone repeats when
it sets its clock back are two bins. They used to share a local second
and were merged into one bin holding their mean, on a grain the contract
defines as the record unreduced; the Europe/Vienna native
fixture pinned 9599 bins for 9600 readings, and is regenerated. The walk
order both reductions accumulate in carries the instant as a third key,
so those two readings are no longer a tie the sort resolved as it liked,
and a zoned record already in order is no longer sorted again.
A supplied calendar that returns no bin start for a reading,
NA in R or NaT in Python, is refused naming
the first such reading. It used to reach the core as a bin at the
beginning of time, holding the reading its real bin then lacked, with
nothing raised; the guard fixture carries the case as
missing.
A time column carried in a zone the database does not know is an error in R, as it was in Python. R’s zone resolution used to warn and read the clock in UTC.
The contract now says that a lookback’s length is measured on the local clock, as everything below the zone boundary is: a one-day lookback ending at a local midnight holds 25 hours of record on the night a zone sets its clock back and 23 on the night it sets it forward. That is what keeps a calendar day whole inside a bin for the day-level statistics; both suites pin it.
The guards above the core name what they refuse the same way on
both sides: a missing identifier or instant names its column in Python
as it did in R, a duplicated reading is named by its instant in UTC in R
as it was in Python and the noun agrees with the count on both, and
Python reads bins = 3.0 as R reads bins = 3. A
zoned record from before 1970 no longer raises OSError on
Windows in Python. A lookback’s rows are named by the row names of
at whatever kind of row names the frame carries, which is
what the contract already said.
The zoned digests have a witness that is not the core. Each
oracle reads a zoned series as a clock in its zone and bins that clock
with its own calendar, and both suites assert every
Europe/Vienna and America/Sao_Paulo digest
against it. The core’s negative-lag guard, its out-of-range unit index
and its bound on the day table are exercised by both suites as
well.
The two orderings among the seven statistics,
min <= mean_daily_min <= mean <= mean_daily_max <= max
and min <= cold_day <= mean <= warm_day <= max,
are asserted by both test suites on the core’s output and the oracle’s.
They used to sit in a block of the core that neither build
compiled.
grain_matrix() and lookback_matrix() on
the Python side refuse a call that names no value column
before reaching the compiled core, and a lookback’s label reads a zero
lag as zero however it is spelled.
as_sift() is the coercer to a set of representations on
both sides, in place of R’s timesift_sift(); the class
keeps its name.resolve_metric beside
get_learner, as the contract had said it did.weights(y) returns
one case weight per cell of the response, and the encoders, the
penalised fit, the forest and the forward search all fit under it: the
encoders as an elementwise weight on the loss, the penalised fit and the
forward search as case weights, the forest as the probability a unit is
drawn into a tree’s bootstrap. The shipped presence-absence head carries
positive_weights(), exported on both sides: each presence
weighs the ratio of absences to presences among the fitting units,
capped at 50, and each absence weighs one. A head registered with
weights = function(y) positive_weights(y, cap = 20) weights
every learner by that cap, and a head without weights fits
unweighted.pos_weight_cap from
train_control(), the elastic net and the forest weighted
uncapped under weight_positives, and the forward search did
not weight at all, so a response present in one target of a hundred
weighed 99 in two learners, 50 in a third and 1 in the fourth, in the
rare regime the network-against-aggregates comparison is about.
pos_weight_cap and weight_positives are gone.
On the study’s data, whose rarest species has 26 presences in 894 plots,
the cap never binds, so the reproduced elastic net is the same
number.case.weights does. scikit-learn’s forest takes
a sample weight into its impurities and leaf values, and a tree grown to
pure leaves is the same tree under any weight, so the Python forest was
fitting a rare response unweighted while the R forest was not.timesift_fit, against the
bins and the channels it was made on, before any learner sees the
representation it is asked to predict. A calendar grain’s bins are named
by their instants, so a record from another period with the same number
of bins is refused by the first bin that differs rather than read by
position, as cnn() fitted on September to November used to
read March to May. A lookback’s bins are relative to each target and
predict any period. The check that each learner carried is gone; every
learner, including one of your own, is held to this one. On both
sides.group carries it, a
fit that declares a group argument is handed
the grouping of the units it is fitted on, and the encoders’ inner
validation set, the elastic net’s inner folds and
select_grain()’s inner map are all dealt by it. Under
grouped_cv() with target_time a unit kept
whole across the outer folds used to be split across the inner ones, so
the epoch and the penalty were chosen on rows the fit had already seen a
near-copy of. On both sides. The elastic net deals its own inner folds
now, through fold_map(), rather than leaving the draw to
cv.glmnet() or to scikit-learn.fold_map() deals with one round-robin counter that runs
on from each stratum into the next, on both sides, so the folds are
equal in size to within one unit whatever v is and
leave-one-out is every unit in a fold of its own. A counter restarted in
every stratum used the first labels once more than the last in every
stratum, and a stratum smaller than v never reached the
last labels at all: ten folds over thirty units came back holding two to
five units each. The fold-map fixture, and the mask and the inflation
built on it, are regenerated.resampling that holds one fold is
refused, and one built by fold_map() keeps its
grouped, seed and strata when it
reaches the run, so a grouped design reports as one. A run under which
no (response, fold) cell is scorable stops before anything
is fitted, and ensemble_fit() handed no scorable cell says
so rather than fitting a combiner on nothing..simplex_weights() stops at a relative gain of 1e-14
rather than 1e-12, on both sides. Near the minimum the loss is flat to
second order, so the stop settles the gradient to about the square root
of the tolerance, and the looser one left the carried members’ gradients
apart by more than a millionth on some boards..cell_means(), read by the ladder, the report, the stack’s
member scores and the selection’s estimate; the five copies had four
spellings.models and learners take a
character vector of registered names, as the docs said; a fit read back
through its reference keeps a setting held at NULL, so a
forest refits with its default mtry;
train_control(early_stopping = Inf) is a patience that
never runs out; predict() on a timesift fit
holds the new targets to one row per identifier, as the fit’s own were
held; the elastic net refuses a design of one column in its own words;
and grain_contrasts() names the reference it cannot find
rather than failing on a length-zero test.auto_grains() counts the bins in the zone the
time column carries, the clock the representation is then binned by. It
used to probe in UTC, so a zoned record could be offered a grain it does
not support or denied one it does.grain() takes a supplied calendar as R does,
reported as custom; multigrain() refuses one
on both sides.grouped_cv() deals the groups unstratified on
both sides. A lookback is labelled
<span> x<bins> lag <lag> on both, and a
numeric span as a duration. lookback() refuses a span that
is not positive when it is built. forest() and
elasticnet() derive one seed per response from the
response’s name in Python as in R, so two species no longer share their
bootstrap draws.feature_matrix() names unnamed rows from 1, as
every other unit label does, and a fold map prints every fold level it
holds.|, so a learner or a sift whose own name holds
one is still found, on both sides.select_grain()’s inner is checked before
it is coerced, so a value that is not a count is named in the error
rather than printed as NA.inst/benchmark/design.R states the definition and
what of the selection term remains. The interval whose coverage is read
is Student’s t, as the package’s are (#65).inst/CITATION cites the package, and
citation("timesift") prints it; the methods article’s entry
is added once it has a DOI. codemeta.json is generated from
DESCRIPTION with codemetar and regenerated at
a release. DESCRIPTION declares
Language: en-GB, and inst/WORDLIST holds the
proper nouns and API names the spell check would otherwise flag.
tests/spelling.R runs the check against it and fails on a
word it does not know, and the R-CMD-check workflow sets
NOT_CRAN so it runs there; it is skipped on CRAN, whose
dictionaries are not this machine’s (#70).python-dist workflow builds the source distribution,
installs it into a clean environment, and runs the suite it ships from
the unpacked tarball, which is what holds the manifest in
pyproject.toml to the sources. It builds wheels for Python
3.11 to 3.13 on Linux, Windows and macOS with cibuildwheel,
each tested on an interpreter that did not build it, and passes every
artefact through twine check. A published GitHub release
uploads them to PyPI by trusted publishing (#69).build_site.R
writes a digest over everything the site is rendered from into
docs/site-digest.txt, and the site workflow
recomputes it from the checkout on every push, so a push that changes
the reference, the news or an article without rebuilding the site fails
there rather than serving last week’s pages under this week’s sources.
The site stays built locally, because the build post-processes its
figures in a way a plain build in CI would not.sklearn extra pins the
scikit-learn release where the mixing parameter alone selects the
elastic net, and that release has no build for 3.10, so the 3.10 job had
failed at install on every push. The test extra carries
pandas, so the suite runs timesift() on a data frame with a
categorical identifier, a nullable-integer response and a zone-aware
time column rather than skipping that test on every runner.DESCRIPTION is where the version is written, and
pyproject.toml reads it from there..gitattributes holds the repository to one line
ending.py.typed.timesift() is the whole entry point. A table of targets,
a table of time-stamped series belonging to them, and one call builds
every candidate representation, fits the learners that can read each
one, scores them all on one set of held-out folds, and stacks the
out-of-fold predictions.
fit <- timesift(plots, logger, y = starts_with("sp_"), id = plot_id, time = datetime,
models = c(elasticnet(), forest(), cnn()),
sift = grains("day", "week", "month"))
summary(fit)The version reads 0.1.0 because this is a first release: the package
fits any time-varying record against any prediction target, where its
predecessors fitted a climate record at a climate grain. Species
distribution modelling from microclimate loggers is the application it
ships defaults for, and the Schrankogel grid it was built on still
reproduces from inst/reproduce/schrankogel.R.
y, x and static take
tidyselect expressions over their own table, and
starts_with(), ends_with(),
contains(), matches(), all_of(),
any_of(), everything() and
where() are re-exported rather than redefined. A column of
targets that is neither the response nor the identifier nor
the anchor is a predictor only where static names it.predict() on a fit rebuilds each member’s
representation for the new targets from the settings its own arm was
built with, and combines them through the ensemble.native(), grain(),
multigrain() and lookback() are what a
representation is before any record has been read; grains()
and lookbacks() are sets of them, and
grains("auto") reads off the record every named grain it
gives at least two bins.c() combines learners, or representations, into a set:
c(elasticnet(), forest()) and
c(grains("day", "week"), lookback("30 days")). A set handed
back to c() splices, so a set can be added to rather than
rewritten. models and sift take that, a bare
spec, or a list.lookback() is a fixed span of record ending a fixed lag
before each target’s own instant, which is what a unit carrying several
targets through time needs. It is one entry point in src/
beside the calendar reduction, so both languages read it from the same
implementation, and lookback_matrix() exposes it
directly.build_representation() is the one place the fitting
layer turns a representation and the two tables into an array, so a run
and a prediction on new targets reach a record the same way.data = grain("week") runs at that
representation alone; left open it runs across the whole sift. A learner
that reads a block of features and one that reads a sequence say so, and
a pairing neither can carry is reported by name rather than fitted.elasticnet(), stepwise(),
mlp(), cnn() and rescnn() drop
the _learner suffix and gain data.
forest() joins them, a probability forest on
ranger in R and on scikit-learn in Python.
reads and multi are learner()’s,
where a learner of your own declares what it reads and whether one
fitted model covers every response.train_control() is the one place a training setting is
defaulted. The architecture constructors carry architecture, a run gives
one control to every neural learner, and a learner given its own control
overrides that on the settings it names.[target, response] matrix, so nothing above the learner
layer has to know which it was.cv() and grouped_cv();
resampling also takes a fold vector or a
fold_map() result, which is how a split the package has no
constructor for reaches the same fitting path.ensemble() fits non-negative weights summing to one on
the out-of-fold predictions alone, minimising the response head’s own
loss over the scorable cells, solved by an exponentiated- gradient loop
in the package. "mean", "median" and
"weighted" combine without fitting.
ensemble_fit() is handed the predictions, the response, the
mask and the fold map, and never a model.summary() reports each candidate’s mean, how many
responses it scored highest on, whether one model covered them, and the
level the combination reached under the weights it reached it with.native() rather than raw() and
lookback() rather than window(), which would
have masked base::raw() and stats::window().
occlusion() is one generic over a run and a ladder, and
bin_occlusion() is gone. ensemble_learner() is
gone with it: it fitted its members and averaged them, which the stack
does over any candidates at all and with the weights fitted rather than
assumed.The package is renamed from timegrain to
timesift. With it go the C++ prefix (tg_ to
ts_ and the four files that carried it), the S3 classes
(timegrain_matrix and its siblings to
timesift_matrix), timegrain_set() to
timesift_set(), and the Python module. No function
signature, default or return type changed, and the fixture digests are
untouched.
What the user chooses is the temporal grain of a climate representation, and the name now carries the thing whose grain it is. The title reads “Temporal Climate Resolution for Ecological Prediction”.
The binning and the reduction are now one implementation,
src/ts_core.cpp and src/ts_calendar.cpp,
compiled into the R package by R itself and into the Python extension by
CMake. The two languages agree by construction rather than by two
implementations being checked against each other after the fact. What
each side keeps above it is the boundary: resolving the columns,
resolving the zone, and wrapping the result.
Four bugs that existed twice, once per language, are closed by that (#1, #2, #4, #5), and a fifth on the Python side with them (#3).
America/Sao_Paulo
before 2019, no longer produces NA bin starts and a failure
from inside the reduction, and a year_start landing on such
a night is an argument rather than an error (#1).grain_matrix() in Python takes a tz
argument. The same instants and the same zone now give the same answer
in both languages, and the fixtures pin it rather than leaving it
assumed (#5).native,
whose bin is the reading itself, nor for a supplied calendar, which
declares its own bin lengths.NA
array in R and a numpy IndexError in Python (#2).nan and no
longer misses a genuinely missing id (#3).tests/testthat/helper-oracle.R and
python/tests/oracle.py. Neither package reaches them at
runtime; both suites check them against the core on the fixtures and on
random series. The NumPy one was written from
inst/spec/representation.md rather than from the R source,
which is what makes it evidence that the document is complete.tz column in
digests.csv: every grain of the aligned series read as a
Europe/Vienna clock, and a short series across the night
America/Sao_Paulo moved its clock at midnight.The names, the defaults and the extension points had drifted, so a
script moved from one side to the other met them one at a time (#8). One
name per concept now, and where the two still differ the difference is a
row in inst/spec/representation.md rather than something to
be discovered at a call site.
glmnet_learner() is elasticnet_learner(),
and its nfolds is n_inner. The name says which
model is fitted rather than which package fits it, which is also what
the Python side already called it. It is registered as
"elasticnet".learners(),
metrics() and responses() list what a session
has, in C collation.stepwise_learner() and select_grain() are
on the Python side. The selector’s orthogonal polynomial basis and its
logistic fits agree with R’s poly() and glm()
to twelve decimals on the same design; the selection procedure is the
same nested one, reporting its estimate under every registered
metric.grain_matrix() returns one representation when
one grain is named, whether as a string or as a sequence of one, and a
timesift_set() for two or more. Its fold map carries the
units it was drawn for, so aligning one is by name there as it already
was in R.swa and swa_start
on both sides, and both refuse a setting the learner does not have
rather than ignoring it. A misspelled argument used to be dropped in
silence.select_grain() searches its candidates in the order
they were declared. It read them off a join on the names before, which
is the collation the rest of the package stopped depending on in this
version, so an exact tie on the inner score could fall to a different
candidate on a different machine.LinkingTo: cpp11, and flat .cpp under
src/, so R compiles the core with no
Makevars.pyproject.toml and CMakeLists.txt sit at
the repository root rather than under python/, because a
Python source distribution cannot reach above its own project directory
and the shared sources must not be vendored into a second copy. The
Python build is scikit-build-core and nanobind; the wheel carries
python/timesift as timesift.tzdata on Windows, which ships no
IANA database of its own, so a zoned record bins there as it does
everywhere else.R-CMD-check, pytest and
contract run on push and on pull requests, the last of them
running both fixture suites against one inst/spec/fixtures/
in a single job.tools/python_reference.py, and
inst/spec/representation.md rendered as a page of its own,
so the document the two answer to is read where the calls are. The
pytest workflow rewrites the Python pages and fails if what
is on disk differs, so a docstring cannot change without the page
changing with it.First release. The package builds the representation, fits at every grain, and reports where predictive skill saturates.
grain_matrix(): reduces a long table of sensor readings
to a [unit, bin, channel] array at one of seven temporal
grains, from the unreduced record to a single value per hydrological
year. Naming several grains returns one representation per grain;
passing a function bins by a calendar the package does not carry, such
as seasons cut at the equinoxes.(unit, bin) cell is asserted
to hold readings.bin_partial and kept or removed by the
partial argument, so a record that begins away from a bin
boundary says so rather than carrying a short bin that looks like any
other.mean, min,
max, the day-level cold_day and
warm_day, which reduce each day to its own mean before
taking the extreme over days, and mean_daily_min and
mean_daily_max, which take the mean of the daily
extremes.calendar_channels() and bind_channels()
supply the position of each bin in the year to an encoder that would
otherwise pool it away.feature_matrix() brings an already-reduced feature
table in as an arm of the same ladder.(unit, time) pairs and missing values
are errors rather than silent padding.fold_map() and scorable_cells(): one split
read by everything that scores, and the mask of cells a score is defined
on, computed from the response and the fold map with no model involved,
so every arm shares one denominator and every paired difference runs on
matched cells.grain_ladder() fits every learner at every grain,
plot() draws the curve, and paired_contrast()
compares two arms inside each cell both scored.bin_occlusion() holds each bin of the record back and
rescores, without refitting, so a fitted model says which part of the
year its skill rests on.grain_contrasts() fits
score ~ grain + (1 | variable) + (1 | fold) and compares
every grain against a learner’s best by Dunnett’s procedure. Needs
lme4, lmerTest and emmeans.tss(), roc_auc(),
kappa_score(), model_agreement() and
decision_threshold().tss_inflation() measures how much a self-selected
threshold inflates a reported level at the user’s own presence counts,
and implied_skill() inverts that map to say what population
skill a level actually read is consistent with.elasticnet_learner() and
stepwise_learner() on the flattened representation, both
redoing their selection inside whichever units they are handed.mlp_learner(), cnn_learner() and
rescnn_learner(), joint multi-label encoders on
torch, sharing one training recipe.ensemble_learner() averages several members’ predicted
probabilities before the threshold is chosen, and the torch learners
take swa = TRUE to average the weights of their tail
epochs.learner() takes a fit and a predict pair of your own;
register_learner(), register_response() and
register_metric() extend the three registries the fitting
path reads.inst/reproduce/schrankogel.R runs the published grid
from the Zenodo deposit it was built on, asserting the plot count, the
species count, the cell count and the bin count of every grain before
fitting anything. See
vignette("reproducing-schrankogel").inst/spec/representation.md is normative for both the R
and the Python implementation.inst/spec/fixtures/ carries a synthetic series and the
digest of every grain-by-statistic combination; both test suites assert
against the same digests.