sifting a record for the grain that carries the signal
Learn predictive representations of time-varying data, in R and Python over one C++ core.
Hand timesift one row per thing to predict and a long table of time-stamped readings belonging
to those rows. It builds each candidate representation of the record, from the readings as recorded
through a calendar week or month to a span anchored on each target, fits the learners you name on
every one they can read, and scores them all on one set of held-out folds. What comes back says at
what grain the prediction needed the record, and what the whole procedure, choosing included,
scores on targets it did not see.
# Install from CRAN
install.packages("timesift")
# Or install the development version from GitHub
# install.packages("pak")
# pak::pak("gcol33/timesift")
install.packages("timesift", repos = c('https://gcol33.r-universe.dev', 'https://cloud.r-project.org'))
# Install from PyPI
pip install timesift
# with the torch encoders, the contrasts and the plots
pip install "timesift[torch,contrasts,plot]"
# Or install the development version from GitHub
pip install git+https://github.com/gcol33/timesift
R
|
Python
|
fit prints every candidate it fitted and the score each reached on the held-out folds, then the
held-out score of the procedure that chooses among them and of the stack, both chosen and weighted
inside each training fold:
fit
#> timesift 80 targets, 4 responses, 10-fold random CV, roc_auc
#>
#> candidates, scored on the outer folds
#> candidate mean won responses
#> elasticnet / day 0.873 0 separate
#> forest / month 0.904 1 separate
#> forest / day 0.924 0 separate
#> forest / week 0.929 1 separate
#> elasticnet / week 0.930 1 separate
#> elasticnet / month 0.938 1 separate
#>
#> procedure, chosen and weighted inside each outer training fold
#> selected 0.934 se 0.015
#> ensemble 0.919 se 0.020
#> selected elasticnet / month in 8, forest / day in 2 of 10 folds
#>
#> choice on every target elasticnet / month
#> weights on every target elasticnet / month 0.98 forest / month 0.02
predict(fit, new_plots, new_logger) rebuilds every member's representation for the new rows and
predicts through the ensemble fitted on every target; candidate = "selected" predicts with the
chosen candidate. type = "binary" cuts each species into presence and absence at a threshold
learned from the fit's own out-of-fold predictions, and type = "spread" gives how far the
ensemble's members disagree, which on the same cells is an uncertainty map.
A sensor records every hour for years. Before any model is fitted, that record is reduced: to
monthly means, to growing-degree-days, to whatever the analyst decides, and the reduction is rarely
revisited. timesift makes it an explicit, testable choice, with every candidate scored on the
same folds and the same cells so the comparison between them is paired.
Species distribution modelling from microclimate loggers is the application the package was built for and the setting its defaults serve: presence-absence, a joint multi-label head, and AUC with the true skill statistic beside it. On 894 alpine plots, 101 species and three years of hourly soil temperature:
The package is what lets the same test run on other records and other responses.
targets is one row per thing to predict, carrying the response and optionally predictors that do not move in time. series is the long record: an identifier, an instant, and one or more value columns. A representation is how that record becomes the array a model reads. A learner is a fit and a predict pair that declares what it can be handed.
A candidate is one representation paired with one learner, and every candidate emits an out-of-fold prediction for every scorable cell over the same folds. Comparison, ensembling and importance read those predictions and nothing else, which is what lets a penalised regression on monthly features and a convolution on the unreduced record be compared and then combined.
The candidates' scores on the outer folds are the comparison, and the best of them was picked out on
the folds it is scored on. The selected and ensemble rows are chosen and weighted inside each
outer training fold, on an inner split of it, and scored on the outer fold once, so they are the
numbers to report.
native() # the record as it was recorded
grain("week") # one calendar grain
multigrain(c("month", "year")) # several grains side by side as one block of features
lookback("30 days", bins = 3) # a fixed span ending at each target's own instant
grains("day", "week", "month") and lookbacks("30 days", "90 days") are sets of them, and
grains("auto") reads off the record every named grain it gives at least two bins. A learner runs
across the whole set, or across one representation it is pinned to:
cnn() # every representation of the run
cnn(data = native()) # the record unreduced only
cnn(data = grain("week")) # weekly only
lookback() serves a unit carrying several targets through time: two targets a fortnight apart on
one sensor read two different stretches of the same series, anchored by target_time.
grain_matrix() and lookback_matrix() return the arrays themselves, [unit, bin, channel], for
use outside the fitting layer.
elasticnet(), linear() (stepwise GLM)tree() (CART), forest(), boosting()
maxent(), envelope(), mars(), discriminant(),
additive(), perceptron()
hierarchical(), a logistic model with unit intercepts and a spatial fieldcnn() and rescnn() reading the sequence of bins, mlp() the
bins flattened, each through a joint multi-label headlearner() takes a fit and a predict pair, and register_learner() adds it to the
registry; it then goes through the same folds, the same cells and the same scoringThe learners run on timesift's own C++ cores, written from the published methods, and those cores
give the numbers of the references a biomod2 user already fits: glmnet's elastic net, MASS's
stepwise search, rpart's tree, earth's MARS, mda's flexible discriminant analysis, mgcv's GAM,
nnet's network, maxnet's Maxent and biomod2's surface range envelope, each pinned against the
reference's own output in the fixtures. Architecture belongs to the constructor and training
belongs to train_control(), so train_control(epochs = 200, device = "cuda") reaches every neural
learner of a run at once.
grain_ladder(): fit at every grain and see where skill saturatesgrain_contrasts(): compare every grain against a learner's best onepaired_contrast(): compare two arms cell by cellselect_grain(): choose the grain inside the training data, and score the whole procedureocclusion(): which part of the record a fitted model readssimulate_records(): sensor records whose response acts at a known temporal grain, to test
the procedure against a truthensemble(): stacking by non-negative weights summing to one, fitted on out-of-fold
predictions alone and minimising the response head's own loss; "mean", "median",
"weighted" and "committee" combine without fittingensemble_weights(): the weights fitted on every target, for predictionproject() and range_change(): a fit onto rasters, and how each response's range
changes between two projectionspseudo_absences(), block_cv(), env_cv(): background draws and spatial or
environmental foldsThe combiner is handed the predictions, the response, the fold map and the mask, and never a model.
A month is 28, 30 or 31 days, and a week starts on a Monday. timesift bins on the calendar, so a
monthly mean over three years stays in its months rather than sliding through the seasons as
730-hour blocks would, and it asserts every unit holds readings in every bin. A record that fails
that assertion is refused rather than padded, and coverage() lays the same binning out as a count
of readings per unit and bin, so the logger that stopped early or the month the whole record skipped
shows as a row or a column of zeros.
attr(grain_matrix(d, plot, t, temp, grain = "month"), "bin_n")[1, 1:3]
#> 2021-09-01T00:00:00Z 2021-10-01T00:00:00Z 2021-11-01T00:00:00Z
#> 720 744 720
A calendar of your own is a function: pass one that returns each reading's bin start, and seasons
cut at the equinoxes bin like any named grain. A record that begins away from a bin boundary gives a
bin the calendar does not fill; bin_partial marks those bins and partial = "drop" removes them.
min and max take the coldest and warmest single reading in a bin. cold_day and warm_day
reduce each day to its own mean first, then take the extreme over days. mean_daily_min and
mean_daily_max take the mean of the daily extremes, the exposure a typical day of the bin
brought. One hour at -50 sets min to -50 outright and reaches the day-level statistics only
through its twenty-fourth of that day's mean.
TSS is read at the threshold that maximises it, chosen on the same held-out units the score is read
on. That inflates the level where presences are thin: on the Schrankogel design, by 0.110 on
average when the truth is 0.60. tss_inflation() measures it for your design and implied_skill()
says what population skill a level you read is consistent with. How large the inflation is depends
on how a model's predictions are distributed, so two models of equal skill can carry different
inflations and a paired TSS difference is not free of it. Runs are therefore scored by AUC by
default, with TSS beside it.
inst/spec/representation.md is normative, and inst/spec/fixtures/ holds a synthetic series with
the digest of every grain-by-statistic combination, alongside the reference coefficients of every
shared learner core. Both test suites assert against the same fixtures, so R and Python give the
same representation and the same baseline fits.
The contract says what each language
carries.
inst/reproduce/schrankogel.R runs the published grid from the Zenodo deposit it was built on, and
asserts the plot count, the species count, the cell count and the bin count of every grain before
fitting anything. Its README
says how to run each stage, what it costs, and how every number compares with the paper.
Bug reports and questions go to the issue tracker.
"Software is like sex: it's better when it's free." — Linus Torvalds
I'm a PhD student who builds R packages in my free time because I believe good tools should be free and open. I started these projects for my own work and figured others might find them useful too.
If this package saved you some time, buying me a coffee is a nice way to say thanks. It helps with my coffee addiction.
@software{timesift,
author = {Colling, Gilles},
title = {timesift: Learn Predictive Representations of Time-Varying Data},
year = {2026},
url = {https://gillescolling.com/timesift/}
}
MIT (see the LICENSE file)