Take predictions as well as models, and MESS a data frame - #1
Merged
Conversation
Every evaluation function here re-predicts. That is the right default - it keeps the scored predictions and the model provably in step - but it assumes the caller holds a model that can reproduce them, and a cross-validated workflow does not. Under k-fold cross-validation each observation is predicted by the one fold model that did not see it. The honest predictions are spread across k models, none of which is the final fit, so by the time a pipeline has a single model to hand it has either discarded them or kept them with nothing to pass them to. Re-predicting from the final model on the same rows answers a different and more flattering question, and evaluation_pairs() offered no way in. held_out(observed, predicted) is that way in. It goes wherever a model goes: pairs <- held_out(cv$observed, cv$predicted) plotROC(pairs, folds = cv$fold) plotThreshold(pairs, folds = cv$fold) plotCalibration(pairs) evaluation_pairs() short-circuits on it, so threshold_metrics(), calibration_estimates() and the three plots over them all work unchanged, folds included. newdata stops being required when the predictions arrive with the object, and the error when it is still missing now names held_out() as the alternative. The test that matters is that the two doors reach the same room: scored through the model and scored through held_out(), the metric table, the AUC, the prevalence and the n all come back identical, with and without folds. A second implementation of the same arithmetic would be worse than no second entry point at all. Two things it deliberately does not do. It cannot verify the predictions are out of sample - nothing in two numeric vectors records what made them - so in.sample is taken on trust, defaults to FALSE, and can be set TRUE to get the same annotation the model path applies when it recognises its own training data. And it cannot serve plotImportance() or permutation_importance(), which shuffle a predictor and re-predict; that needs a model by construction rather than a record of what one once said. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l novel Two generalisations, both of which the surface wants on its own terms. A data frame is now a valid x. Plenty of pipelines hold their projection as a table of cells rather than as a raster - anything that predicts over a joined covariate table does - and requiring a round trip through terra to score one was a tax rather than a service. The return follows the input: a raster in, a raster out; a data frame in, a data frame out. limiting = TRUE reports which covariate each cell's score came from. The surface says a cell is novel; this says what made it so, and that is usually the half worth acting on. "This shelf is extrapolated" is a shrug. "It is extrapolated because its chlorophyll is higher than anything in training" is a decision about whether to widen the training window or clip the map. On the data frame side it is a mess_variable column; on the raster side a categorical mess_variable layer, since a raster cannot hold a character and the names have to live in the levels table. Off by default, so the returned shape is unchanged for every existing caller. The check that matters is that the two paths are one method with two doors: the same cells scored as a raster and as a data frame come back identical, and so do the raster's limiting layer and the frame's limiting column. A second implementation for data frames would have been free to drift, and nothing outside would have noticed. Scored against an independent implementation of the same method too - the one in taupatch, written from Elith, Kearney and Phillips (2010) rather than from this code - and the surfaces and the culprit variables agree exactly. That is the more convincing of the two checks, since it does not share a line of code with this one. One test changes rather than being added: test-spatial.R pinned the restriction that x must be a SpatRaster, which is the restriction being lifted. It now asserts what replaced it - a data frame is read, and a list is still refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two generalisations, both prompted by trying to use fancyfx from a cross-validated pipeline (taupatch) and finding the door shut. Both are things fancyfx wants on its own terms, and neither changes behaviour for any existing caller.
1.
held_out()— evaluate predictions you already haveEvery evaluation function here re-predicts. That is the right default — it keeps the scored predictions and the model provably in step — but it assumes the caller holds a model that can reproduce them, and a cross-validated workflow does not.
Under k-fold CV each observation is predicted by the one fold model that did not see it. The honest predictions are spread across
kmodels, none of which is the final fit. By the time a pipeline has a single model to hand it has either discarded them or kept them with nothing to pass them to, andevaluation_pairs()offered no way in. Re-predicting from the final model on the same rows answers a different and more flattering question.evaluation_pairs()short-circuits on it, sothreshold_metrics(),calibration_estimates()and the three plots over them work unchanged, folds included.newdatastops being required when the predictions arrive with the object, and the error when it is still missing now namesheld_out()as the alternative.Two things it deliberately does not do. It cannot verify the predictions are out of sample — nothing in two numeric vectors records what made them — so
in.sampleis taken on trust, defaults toFALSE, and can be setTRUEto get the same annotation the model path applies when it recognises its own training data. And it cannot serveplotImportance()orpermutation_importance(), which shuffle a predictor and re-predict; that needs a model by construction rather than a record of what one once said.2.
mess()on a data frame, and the limiting covariatexmay now be a data frame of covariate columns as well as aSpatRaster, and the return follows the input. Pipelines that hold a projection as a table of cells rather than as a raster had to round-trip throughterrato score one, which is a tax rather than a service.limiting = TRUEreports which covariate produced each cell's score. The surface says a cell is novel; this says what made it so, which is usually the half worth acting on — "this shelf is extrapolated" is a shrug, "extrapolated because its chlorophyll is higher than anything in training" is a decision about whether to widen the training window or clip the map. Amess_variablecolumn on the data frame side; a categoricalmess_variablelayer on the raster side, since a raster cannot hold a character and the names have to live in the levels table.Off by default, so every existing caller's return shape is unchanged.
Verification
622 tests passing, 0 failures, including 20 new ones.
The checks that carry the weight are the agreement checks, because both changes add a second way to reach existing arithmetic and the risk in that is drift:
held_out(), the metric table, AUC, prevalence and n come back identical — with folds and without. Same forcalibration_estimates(), including the calibration intercept/slope and the Brier score.mess()was also scored against an independent implementation of the same method — the one in taupatch, written from Elith, Kearney & Phillips (2010) rather than from this code — and the surfaces and the culprit variables agree exactly. That is the more convincing of the two, since it shares no line of code with this package.One existing test changed
test-spatial.R:173assertedmess(data.frame(...), training)errors with "must be a SpatRaster" — the exact restriction this lifts. It now asserts what replaced it: a data frame is read, and a list is still refused. Flagged because changing an existing assertion deserves more scrutiny than adding one.🤖 Generated with Claude Code