tidypredict (development version)
The Cubist article now documents two limits on how closely
tidypredict_fit()can matchCubist::predict(). The instance-based correction thatpredict()applies whenneighborsis greater than zero is not reproduced, because it adjusts each prediction using training rows that are not part of the fitted model. Separately, Cubist stores its coefficients as 32-bit floats, so the agreement has a relative ceiling near 1e-7 rather than an absolute one, and an outcome on a large scale leaves a proportionally large absolute difference. (#375)The glm article now documents the one inverse link
tidypredict_fit()does not reproduce exactly:probit, whose inverse ispnorm(), is written as the Bowling et al. logistic approximation to the normal CDF because no SQL backend has a normal CDF. It costs about 1e-4 of probability, which is enough fortidypredict_test()to report a probit model as failing at its default threshold. (#355)The naive Bayes article now documents the one case where
tidypredict_fit()does not reproducepredict()forklaR::NaiveBayes()andnaivebayes::naive_bayes()models: both replace a normal density that underflowed to zero with theirthresholdargument, which takes a value roughly 38 standard deviations from the class mean, and the log scale used throughout never underflows. (#300)The models article now documents a limit on
kernlab::ksvm()models fitted through the matrix interface,ksvm(x, y).ksvm()mangles its model matrix column names withmake.names()and keeps no record of the originals, and unlike the formula interface there is notermsobject to detect this against, so a non-syntactic column name such asa:byields a formula referring to a column the data does not have. This cannot be caught automatically, because every namemake.names()produces is also a name it leaves alone, soa.bfrom a mangleda:bis indistinguishable from a correct model with a column genuinely nameda.b. (#418)acceptable_formula()now checks the contrast of every factor predictor. A model that used the treatment contrast for one field and something else for another was accepted and then silently mis-parsed; such a model now aborts with the usual “the treatment contrast is the only one supported” error, which also names the offending field rather than the contrast. (#291)acceptable_formula()no longer rejects aMASS::lda(),MASS::qda()orearth::earth()fit whose factor has a level containing a colon. The contrast check split the model matrix column names on:to find interactions, so such a level looked like one and the fit was refused as using an unsupported contrast; the check now decomposes each column against the model’s own term structure. (#391)parse_model()andtidypredict_fit()now reject an ordered predictor in a parsnip fit whose engine issda,sparsediscrimormixOmics, with the same “the treatment contrast is the only one supported” error thatMASS::lda(),MASS::qda()andmda::fda()already gave. parsnip builds the model matrix withcontr.poly, whose.Land.Qcolumns were read as level names, so the predictions were silently wrong by as much as 1.34. (#393)parse_model()andtidypredict_fit()now reject asparsediscrimfit whose model matrix has two columns with the same name, which happens when a factor level and another predictor expand into the same name, such as levely2ofgagainst a predictorgy2.sparsediscrim::predict()selects those columns back out by name and so silently uses one of them twice, disagreeing with the fit it came from by as much as 0.84, and errors outright on a parsnip fit; there is no answer that matches both. (#398)tidypredict_fit()now handles amixOmicspredictor that never varied, which is what an unused factor level expands into. Such a column has a scale of zero and so a coefficient ofNaN, which made the fit fail with “missing value where TRUE/FALSE needed”;predict()drops the column, and the generated formula now does too. (#398)tidypredict_fit()now fills a missingmixOmicspredictor in at its training mean, aspredict.mixo_pls()does, rather than returningNAfor the row. Each predictor is now wrapped in anifelse(is.na(x), mean, x), so the generated formula is longer than before but agrees withpredict()on data with missing values. (#398)acceptable_formula()andparse_model()now report a model class they do not support, rather than failing with R’s “no applicable method” error. (#313)as_parsed_model()now rejects an object that is not a parsed model. A list without ageneral$typeelement was given a class ofpm_that no method matches, so the failure surfaced much later and said nothing about the real problem. (#313)tidypredict_fit()now works on a LightGBM model whose trees are bare leaves, which is what LightGBM emits when it cannot make a single split, such as with a constant outcome, a single training row, or a lone factor predictor whose splits the categorical guards reject.lightgbm::lgb.model.dt.tree()reports no rows at all for such a tree, so the model parsed to no trees and failed with “Model has no trees.”; the leaf values are now read from the model’s JSON dump. A multiclass model in which only some trees are bare leaves silently assigned trees to the wrong classes, and now matchespredict(). (#401)parse_model()now aborts on akernlab::ksvm()model with a factor level that is not a syntactic name, such asc:d.ksvm()only keeps themake.names()form of its model matrix column names, so the level was read back asc.dand the formula compared against a value that matches no row, silently dropping that dummy term from the prediction. (#390)parse_model()now aborts on abaguette::bagger()ensemble of C5.0 models fitted withfuzzyThreshold = TRUE. Fuzzy thresholds send a case near a split point partly down both branches, which a hard<=comparison cannot express, and baguette runs its base fits throughbutcher(), which empties thecontrolelement the existing check read; the option is now detected from the tree itself. (#395)parse_model()now maps the dummy columns of anxrf::xrf()model against the model matrix the fit was built from. A dummy column whose name matched a separate predictor, such as acyl4dummy of a factorcylalongside a column literally namedcyl4, was read as that predictor, so the formula multiplied a factor by a coefficient and every prediction came back asNA. (#396)tidypredict_fit()now returns predictions on the response scale for CatBoost models fit with thePoissonorTweedieobjective, applyingexp()to the raw score as the other CatBoost objectives already invert their own links. Anyone using such a model will see their predictions change from the log scale to the count or mean scale; they now matchcatboost.predict(prediction_type = "Exponent")instead of the"RawFormulaVal"default. (#356)tidypredict_fit()now returns one prediction per row for aranger::ranger(),xgboost,baguette::bagger(), orxrf::xrf()model in which every tree collapsed to a single leaf, or in which the lasso kept only the intercept. Such a model produced a formula that mentioned no column at all, so evaluating it returned a single value rather than one per row. The value was always correct; only its length was wrong. (#397)tidypredict_fit()now applies the bias correction of arandomForest::randomForest()model fitted withcorr.bias = TRUE.predict()rescales the forest average by the two coefficients stored inmodel$coefs, which the parser never read, so the predictions were off by as much as 0.21 for a model ofmpgonmtcars. (#395)tidypredict_fit()now sends a split threshold that is not finite, or that overflows the 32-bit float range, down the branch the model does. Such a threshold was moved to a boundary ofNaN, which makes every comparisonFALSE, so the model silently mispredicted. (#313)tidypredict_fit()now works on a parsed LightGBM model fit withlinear_tree = TRUE. A leaf of a linear tree stores its coefficients separately and leaves its constant prediction empty, which the parsed path never read, so the formula failed with “..1 (right)must be a vector, notNULL”. This also affected such a model saved withtidypredict_save()and read back withtidypredict_load(). (#346)tidypredict_fit()now matchespredict()for a single-treeC50::C5.0()model when new data is missing a split value. C5.0 does not send such a row down one branch: it descends every branch of the node, weighting each by the training cases it holds, and returns the class with the largest combined leaf distribution. The generated formula instead routed the missing value to the.defaultbranch, so the row could come back as a different class. (#387)tidypredict_fit()now matchespredict()for a boostedC50::C5.0()model (trials > 1) when new data is missing a split value. Each trial picks its class by the same weighted descent a single tree uses and votes with the confidence of that class, both of which the generated formula worked out as if the row had reached one leaf. The weighted form is only reached by rows that are actually missing a split value, but it is stated once per class per trial, so the formula for a boosted model grows quadratically in the number of outcome levels. (#416)tidypredict_fit()now matchespredict()for a rule-basedC50::C5.0()model, the engine behind parsnip’sC5_rules(), when new data is missing a value a rule tests. Such a rule does not fire in C5.0, but R returnsNArather thanFALSEfor a comparison against a missing value, which spread through the vote sum and dropped the row to the last class: 65 rows in 400 came back wrong in the original report. (#415)tidypredict_fit()now matchespredict(type = "prob")for the class probabilities of aC50::C5.0()model when new data is missing a split value, which abaguette::bagger()ensemble of C5.0 models averages to pick its class. Only the predicted class was made aware of C5.0’s weighted descent; the probabilities still routed a missing value to the.defaultbranch, and were wrong by as much as 0.75 on the rows affected. (#417)tidypredict_fit()now follows the per-node missing value direction aranger::ranger()model learns when its training data containsNA. Since ranger 0.17.0 the defaultna.action = "na.learn"picks a side for missing values at each node it saw one at and saves it for prediction, but the generated formula always sent them left, so rows withNAcould be predicted at the wrong leaf. (#394)tidypredict_fit()now matchespredict()for aranger::ranger()model fitted withrespect.unordered.factors = "partition"on a factor with more than 31 levels. Such a split is stored as a bit mask naming the levels that go right, and the levels were read fromranger::treeInfo(), which can only render a mask of up to 31 levels and silently blanks a wider one out; the mask is now decoded from the value stored on the forest, whichrangerallows up to 53 levels. (#414)tidypredict_interval()now rejects anintervalthat is not a single number strictly between 0 and 1. Anintervalof 1.5 gave a formula beginning withNaN, so every prediction bound came back missing. (#313)tidypredict_interval()now reports a parsed model of a type it does not support with the same message it gives for a fitted model, rather than “Model type not supported.”, and reports a list that is not a parsed model rather than failing with “argument is of length zero”. (#313)tidypredict_sql()now returns a single query for an intercept-only model. Such a model’s formula is a bare number rather than a call, which was mistaken for the list of formulas a multiclass model produces, so the query came back wrapped in a one element list. (#313)tidypredict_to_column()now validatesvars,add_intervalandinterval. Passing fewer than three names invarsalongsideadd_interval = TRUEproduced a data frame with a column literally namedNA. (#313)tidypredict_fit()now produces a formula R can evaluate for adbarts::bart()fit at the package defaultntree. Terms are summed left to right, which nests the+calls as deeply as there are terms, and a bart fit sumsndpost * ntreeleaf values: at the defaults R gave up with “evaluation nested too deeply”. A model with 1000 terms or more is now summed in a balanced shape instead, nestinglog2(n)deep. Only a large ensemble reaches that, so every other model keeps the flat left-to-right sum it had before, along with the exact result and the formula layout that go with it. (#305).build_case_when_tree(), which {orbital} calls, now returns the bare prediction of a stump tree whether that prediction is a number or a class label. A classification stump previously producedcase_when(.default = "a"), which dplyr rejects with “...can’t be empty”. (#310)tidypredict_fit()no longer returnsNULLfor a parsed model saved by tidypredict 1.0.1 or earlier that came from apartykitorrpartsingle tree. The handler for single trees was removed as apparently dead code, leaving those models to fall off the end of a whitelist, sotidypredict_to_column()returned the data frame unchanged andtidypredict_sql()returned an empty list. Any parsed model type that is still unhandled now raises an error rather than returningNULL. (#304)tidypredict_fit()no longer fails with “xmust be a formula” on a parsed model saved by tidypredict 1.0.1 or earlier that contains aranger::ranger()orrandomForest::randomForest()stump, a tree whose root is its only node. Such a tree is now written as its constant prediction. (#310)tidypredict_fit()now handles three parsed model shapes that no releasedparse_model()writes but that a hand-written or edited parsed model can contain: a path that mixes atype = "all"element with real conditions, which aborted with an internal error; a rule whose linear prediction is a single non-intercept term, which produced a garbled formula; and a rule whose terms are all zero, which aborted with “.xmust not be empty” and is now written as0. (#310)tidypredict_fit()now returns correct predictions forkernlab::ksvm()models with a single numeric predictor, which previously produced a bare constant. kernlab leaves the column names of a one-column model matrix empty, so every term was dropped and only the intercept remained. (#289)tidypredict_fit()now undoes kernlab’s predictor scaling when exactly one column was scaled forkernlab::ksvm()models. This covers any fit with one numeric predictor plus factor predictors, since kernlab does not scale dummy columns, and the weights were left on the scaled scale because the centers and scales lose their names in that case. (#289)tidypredict_fit()now assigns rules to the right committee forCubist::cubist()models fitted with more than 20 committees. The committee each rule belonged to was scraped from the printed model, whose “Number of rules per committee” line is truncated at 20 committees, so the rules beyond that point were recycled across the wrong committees and the average was taken over 20 committees instead of the number requested. (#286)tidypredict_fit()now applies the per-rule extrapolation limits forCubist::cubist()models. Cubist holds each rule to the span of the training outcomes it covers, widened at both ends byextraptimes that span and never crossing zero; without it a rule’s linear model runs away on data outside its range. This engages on rows of the training data too, not only on extrapolation. (#285)tidypredict_fit()now supports factor predictors forCubist::cubist()models, which previously produced a formula that could not be evaluated (object '"f"' not found). Rule conditions are now read from the model text rather than frommodel$splits, which records neither the quoted column name nor a condition naming a single level, so such a rule silently applied to every row. (#322)tidypredict_fit()now reads the coefficient labels of anlm(),glm()orquantreg::rq()model from the model’s own term structure rather than from the spelling of the label. A factor level containing a:was taken apart as if it were an interaction, giving a formula that could not be evaluated, and a label that happened to equal another predictor’s name was read as that predictor, silently giving wrong predictions. A label that still cannot be resolved to one combination of levels is now reported instead of guessed at. (#308)tidypredict_fit()now reads coefficient labels from the model’s own term structure fornnet::multinom(),nnet::nnet(),kernlab::ksvm(),MASS::lda(),MASS::qda(),mda::fda()andsda::sda()models too, extending the fix that landed forlm(),glm()andquantreg::rq(). A dummy column whose name happened to equal another predictor’s name was read as that predictor, silently giving wrong predictions: ay ~ g + gy2fit where the factorghas a levely2was out by a full unit of probability. The levels are worked out from how many columns each term expanded into for the models that record noxlevels, which also fixeskernlab::ksvm()fits whose duplicate model matrix column names were made unique. (#376)set_catboost_categories()now names every category of acatboostmodel, for any number of factor levels. It used to discover the hash CatBoost stores for a level by training probe models and reading back a split, which only worked reliably for a three-level factor; a factor with four or more levels errored with “No category mapping found for hash”, and a two-level one could silently name the levels the wrong way round. Hashes are now taken from CatBoost’s own hash function, and a level that cannot be named is reported at once rather than at fit time. This also affectstidypredict_fit()on a parsnip or bonsaicatboostfit. (#297)tidypredict_fit()now rejects anearth::earth()model fit with a contrast other than the treatment one, with the same message the rest of the linear family gives. An ordered factor, which R fits withcontr.polyby default, previously produced a formula comparing the factor column against contrast values such as-0.2236, which could not be evaluated.earthrecords no contrasts, so they are now read back off the names it gave the columns each factor expanded into. (#323)tidypredict_fit()now rejects anh2omodel fit with an algorithm other than GBM or RuleFit. Every h2o algorithm returns one of the three model classes tidypredict dispatches on, so nothing had been checking which one was used:h2o.randomForest()silently gave predictions that were wrong by a factor of the number of trees, because h2o averages tree predictions where the code summed them, and classification forests use vote proportions rather than a logistic link. The tree-free algorithms, among themh2o.glm(),h2o.deeplearning()andh2o.naiveBayes(), failed with the unhelpful “argument must be coercible to non-negative integer”. (#284)tidypredict_fit()now rejects aMASS::lda(),MASS::qda()ormda::fda()model fit with a contrast other than the treatment one. None of the three records the contrasts it used, so the existing check was a no-op and an ordered factor, which R fits withcontr.polyby default, silently produced wrong posterior probabilities: the level recovered from a column namedf.Lmatches no row, so the term was dropped without complaint. (#343)tidypredict_fit()now rejects annnet::nnet()model fit with the matrix interface instead of returning an unusable formula. Such a fit keeps neithertermsnorcoefnames, so the names of the predictors are lost and every reference to an input unit was written asNULL. The formula did not error: it evaluated to a zero length result. Refit the model with the formula interface. (#303)tidypredict_fit()now routes missing values by each node’smissing_typeforlightgbmmodels, matchingpredict(). LightGBM consultsdefault_leftonly whenmissing_typeisNaNorZero; a feature with no missing value in the training data getsNone, where a missing value is coerced to0and compared against the threshold like any other. Routing purely bydefault_leftwas wrong for every model trained without missing data, which is the common case. (#288)tidypredict_fit()now honourszero_as_missingforlightgbmmodels, where an exact zero takes the same branch as a missing value. Predictions were wrong on the training data itself, not only on new zeros. (#288)tidypredict_fit()no longer sends a missing value down the left branch of a categorical split forlightgbmmodels. LightGBM sends it right whateverdefault_leftsays. (#288)tidypredict_fit()no longer returnsNaNfor every class probability of a row whose class scores are large, for any model whose prediction is a softmax:MASS::lda(),MASS::qda(),mda::fda(),sparsediscrim,sda,mixOmics,nnet::multinom(),nnet::nnet(), multinomialglmnet, naive Bayes,h2o,lightgbmandcatboost. The probabilities were written asexp(s) / sum(exp(s)), which isInf / Infonce a score passes about 710. They are now written as1 / sum(exp(s_j - s_k)), which is the same quantity and cannot overflow. (#299)tidypredict_fit()now rejects aglmnetmodel fit with anoffsetrather than silently dropping it, for both the single-outcome and the multinomial paths. glmnet records only whether an offset was used, never the values, andpredict()requires them again asnewoffset, so the prediction cannot be reproduced. Predictions were previously wrong by the size of the offset. (#296)tidypredict_fit()now rejects aranger::ranger()probability or survival forest rather than producing an unusable formula. Neither records a value per leaf, so a guard that read one let both through and emittedcase_when(x <= 0.0066 ~ NULL, .default = NULL), which failed later with an unrelated vctrs error;parse_model()returned a parsed model with no predictions and no error at all. The forest type is now read fromtreetype. (#301)tidypredict_fit()now honours anmstopreduced after fitting formboostmodels, asmodel[m]does. Subsetting a fitted model, which is the standardcvrisk()workflow, setsmstopbut leaves the stored ensemble at its full length, so every boosting iteration was used regardless. (#306)tidypredict_fit()now sends a value sitting exactly on a split boundary the way the model does, for the backends that compare split thresholds as 32-bit floats:xgboost,lightgbm,catboost,Cubist::cubist()andC50::C5.0(). The boundary is the midpoint between the stored threshold and the adjacent float, and a value can land precisely on it, where rounding to a float is a tie broken towards the even mantissa. About half of all thresholds resolve that tie towards the neighbour rather than the threshold, and those sent such a value down the wrong branch. (#350)tidypredict_fit()now handles aMASS::lda()ornnet::nnet()model whose outcome factor has a level no observation fell in. Both drop the empty group when fitting but keep the full level set inlev, which the code used to name the classes, soMASS::lda()failed with “subscript out of bounds” and a classificationnnet::nnet()with “‘names’ attribute [4] must be the same length as the vector [3]”. The classes are now read from the fitted quantities, which is whatpredict()labels its output with. (#302)tidypredict_fit()now honourssigmoidforlightgbmmodels fit with thebinaryormulticlassovaobjective, which apply1 / (1 + exp(-sigmoid * x))rather than a plain logistic. Every probability of a model fit with any other value was rescaled.cross_entropyaccepts the parameter but never applies it, and is left alone. (#288)tidypredict_fit()now honoursreg_sqrtforlightgbmmodels, which trains onsqrt(|y|)keeping the sign and squares the raw score back onto the response scale. Predictions were left on the square-root scale, which can be further frompredict()than the response itself. Thehuberobjective accepts the parameter but does not act on it, and is left alone. (#288)tidypredict_fit()andparse_model()now work on anxgboostbooster that has been saved and reloaded withxgb.save()/xgb.load(). Such a booster was routed to the pre-2.0 code path and failed withargument "model" is missing, with no default, because the attribute used to tell the two APIs apart is set byxgb.train()but not byxgb.load(). The objective is also recovered from the saved model now, which a reloaded booster records nowhere else; without it the raw margin was returned as though it were a probability, behind a warning about custom objectives. (#292)tidypredict_fit()now combines the trials of a boostedC50::C5.0()model with the confidence C5.0 votes with,(freq + prior) / (n_leaf + 1), whereprioris the class proportion at the root of that trial’s own tree. It used the Laplace ratio(freq + 1) / (n_leaf + 2)instead, which changed the predicted class for 72 of 720 swept configurations. A tie in the total vote now goes to the default class, asSelectClassdoes. (#287)tidypredict_fit()no longer reads C5.0’s[ordered]marker as part of the first level of an ordered predictor. (#287)tidypredict_fit()now reports aC50::C5.0()model that records no tree, rather than failing with “subscript out of bounds”.C5.0()leaves the tree empty when fitting failed, which a predictor name or level containing,or:causes. (#287)tidypredict_fit()now works for rank-deficientlm()andglm()models, which aborted with “Unable to calculate inverse of QR decomposition” even though it needs no QR decomposition at all. Two everyday shapes hit this: a duplicated predictor column, and a predictor with no variance. The aliased coefficients R leaves asNAare now dropped, aspredict()drops them, and the QR decomposition the prediction interval needs is built from the columns the fit actually identified, sotidypredict_interval()keeps working for these models too. (#308)tidypredict_interval()now reports a parsed model that carries no QR decomposition, instead of failing with “Must supply.initwhen.xis empty”. (#308)tidypredict_interval()now works forglm()models. It returnednumeric(0)for every gaussian glm, because the residual variance was read fromsummary()$sigma, which onlysummary.lm()has;summary.glm()reports it asdispersion.tidypredict_to_column(add_interval = TRUE)errored as a result. (#293)tidypredict_sql()andtidypredict_sql_interval()now check that dbplyr is installed before using it, and are no longer marked as internal in the documentation index. (#314)tidypredict_fit()now supports splits with more than two branches forpartykitmodels, such as those fromctree_control(multiway = TRUE)or apartysplit()with several breaks. Every branch after the second was previously dropped, silently for a factor split and with a warning for a numeric one. (#295)tidypredict_fit()now honourspartysplit(right = FALSE)forpartykitmodels, where the left branch isx < breakrather thanx <= break. A value falling exactly on the break took the wrong branch. (#295)tidypredict_fit()now handles ordered factor predictors forpartykitmodels, which previously errored with “Result must be length 1, not 2”.partykitsplits an ordered factor with a break on the level’s integer code rather than with a set of levels. (#295)tidypredict_fit()no longer swaps the two branches of everypartykit::partyconverted from anrpartmodel.as.party.rpart()maps the interval below the break to the second child, and the child order was read directly instead of through that mapping. (#295)tidypredict_fit()now decodes factor splits forranger::ranger()models, in all threerespect.unordered.factorsmodes and for ordered factors. The split value names a position in the level order stored on the model, or under"partition"lists the level indices going right; it was compared as a numeric threshold against the factor column itself. (#283)tidypredict_fit()now decodes factor splits forrandomForest::randomForest()models. An unordered factor’s split point is an integer whose bits name the levels going left, and an ordered factor’s is compared against the level’s integer code; both were read as a numeric threshold on the column itself, which silently producedNAor a wrong branch. (#282)tidypredict_fit()andparse_model()now handle a stump, a tree with a single root node and no split, in arandomForest::randomForest()forest, instead of aborting with “argument of length 0”.randomForest::getTree()drops its node table to a vector for such a tree and then fails on its own1:nrow(), so the table is now assembled directly. A stump appears whenever the outcome is constant within a bootstrap sample, which a constant outcome or a zero-variance predictor makes routine. (#362)tidypredict_fit()now skips a feature whose value is missing or whose factor level was not seen while fitting, forklaR::NaiveBayes()andnaivebayes::naive_bayes()models, matching both packages’predict()instead of returningNAfor the whole row. A row missing every predictor falls back on the class prior alone. (#300)tidypredict_fit()no longer errors with “missing value where TRUE/FALSE needed” for anaivebayes::naive_bayes()model with an outcome class of fewer than two observations. Such a class has no standard deviation, and the resultingNAprobabilities now matchpredict(). (#300)tidypredict_fit()now substitutes the training mean for a missing predictor inCubist::cubist()models, matchingpredict(). The mean is read from the model text at the precision Cubist itself stores it, and is used in the rule conditions as well as in the linear models. (#294)tidypredict_fit()now sends a missing predictor down the left branch forranger::ranger()models, matchingpredict().rangercompares asvalue > splitval, which a missing value fails, so it takes the same branch as a value at or below the split point. (#294)tidypredict_fit()now routes missing values through surrogate splits forrpart::rpart()models, and forbaguette::bagger()models using the"CART"base model, matchingpredict()instead of sending every missing value down the right branch. All threeusesurrogatemodes are followed, including stopping at the node when no surrogate resolves the row and there is no majority to go with. (#294)tidypredict_fit()now returnsNAfor a row that reaches a split on a predictor it is missing, forpartykit::ctree(),partykit::cforest()andmboost::blackboost()models. These backends resolve a missing value by randomly sampling the split probabilities, sopredict()returns a different answer on each call and there is no value to reproduce. A row whose path never reaches a split on the missing column is unaffected. (#294)tidypredict_fit()now returnsNAfor a row with a missing predictor forrandomForest::randomForest()andaorsf::orsf()models, rather than a confident value the model itself would never produce.randomForest::predict()returnsNAfor any incomplete row andaorsfrefuses to predict from one at all, so there is no value to match. Rows are kept rather than dropped. (#294, #325)tidypredict_test()now handles missing predictions instead of erroring with “missing value where TRUE/FALSE needed”. A row where both the model and tidypredict returnNAcounts as a match, and a row where only one of them does is reported as a mismatch, so the function can be used to check how a model behaves on missing data. (#309)tidypredict_test()now errors when given data with no rows, rather than reporting that all results are within the difference threshold. (#309)tidypredict_fit()now returns correct predictions forC50::C5.0()models whose predictor values fall on a split cut point. C5.0 compares cut points as 32-bit floats, so values between a cut and its float image were sent down the wrong branch. (#287)tidypredict_fit()now returns correct predictions forcatboostmodels whose predictor values fall on a split border. catboost compares borders as 32-bit floats, so a value a fraction above a border was sent down the wrong branch. (#298)tidypredict_save()andtidypredict_load()write a parsed model to a YAML file and read it back. Use them instead ofyaml::write_yaml(), which stores only 7 significant digits by default and so rounds split thresholds enough to send rows down a different branch when the model is re-loaded. (#307)tidypredict_fit()now picks the right factor predictor when three or more variable names are nested prefixes of one another, such asx,xyandxyz. The longest match was selected by indexing withrank(), which silently chose the wrong variable and produced wrong predictions forlm(),glm(),quantreg::rq(),nnet::multinom(),nnet::nnet()andearth::earth(). (#290)Added support for
baguette::bagger()bagged tree ensembles fit with the"CART"or"C5.0"base model, includingbag_tree()parsnip models fitted with the"rpart"or"C5.0"engine. Regression predictions average the individual trees, and classification predictions return the class with the largest average class probability. (#232)tidypredict_fit()now supportsC50::C5.0()models that split a discrete predictor into one branch per level. (#232)tidypredict_fit()now uses a strict inequality (<) for the continuous splits ofrpart::rpart()models, matching howrpartassigns values that are exactly equal to a cut point. (#232)tidypredict_fit()now returns correct predictions forrandomForest::randomForest()models that have been saved and reloaded withparse_model()andas_parsed_model(). Every split variable after the first leaf in a tree was named incorrectly, so the reloaded model split on the wrong columns. (#232)tidypredict_fit()now returns correct predictions forCubist::cubist()models whose predictor values fall exactly on a split threshold. Cubist compares split thresholds as 32-bit floats, so adispof 95.1 was sent down the wrong branch when the comparison was made in R’s doubles. (#232)tidypredict_test()now supportsC50::C5.0()models, including boosted and rule-based ones. (#232)tidypredict_interval()now honours itsintervalargument. It was hardcoded to 0.95, sotidypredict_interval(),tidypredict_to_column(add_interval = TRUE), andtidypredict_sql_interval()all returned a 95% interval regardless of what was asked for. (#232)tidypredict_interval()now reports an unsupported model class with a message naming the class, rather than R’s default “no applicable method” error. (#232)tidypredict_to_column()now explains that a model returning more than one formula is unsupported, instead of incorrectly claiming that tree based models are unsupported. (#232)tidypredict_fit()now reports an unsupported model class with a message naming the class, rather than R’s default “no applicable method” error. (#232)tidypredict_fit()now keeps small probabilities for models with a logit link, such asglm()withfamily = binomialandLiblineaR::LiblineaR(). The inverse link was written in a form that rounded to exactly 0 once the linear predictor fell below about -37. (#232)Added support for
dbarts::bart()Bayesian additive regression trees, includingbart()parsnip models fitted with the"dbarts"engine. The model has to be fit withkeeptrees = TRUE, and only continuous outcomes are supported since binary outcomes are fit with a probit link. (#232)Added support for
klaR::NaiveBayes()naive Bayes models with Gaussian densities (usekernel = FALSE), includingnaive_Bayes()parsnip models fitted with the"klaR"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax of the summed log densities), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
naivebayes::naive_bayes()naive Bayes models fit without kernel density estimates (usekernel = FALSE), including Gaussian, categorical, Bernoulli, and Poisson conditional distributions, andnaive_Bayes()parsnip models fitted with the"naivebayes"engine.tidypredict_fit()returns a named list of class-probability expressions, andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
parsnip::nullmodel()models, includingnull_model()parsnip models fitted with the"parsnip"engine. Regression models return the outcome mean as a single expression, and classification models return a named list of constant class-probability expressions, for whichtidypredict_test()is not supported. (#232)Added support for the partial least squares models in
mixOmics(pls(),spls(),plsda(), andsplsda()), includingpls()parsnip models fitted with the"mixOmics"engine, for regression and classification. Single-outcome regression models return one expression, multivariate outcomes return a named list of expressions, and the discriminant variants return a named list of class-probability expressions (softmax), for whichtidypredict_test()is not supported. (#232)Added support for multinomial
glmnet::glmnet()models (family = "multinomial"), includingmultinom_reg()parsnip models fitted with the"glmnet"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax), andtidypredict_test()is not supported for these multiclass models. (#198)Added support for
nnet::multinom()multinomial log-linear models, includingmultinom_reg()parsnip models fitted with the"nnet"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
nnet::nnet()single hidden layer neural networks, includingmlp()parsnip models fitted with the"nnet"engine, for regression and classification. Regression models return a single expression, and classification models return a named list of class-probability expressions, for whichtidypredict_test()is not supported. (#232)Added support for
sda::sda()shrinkage discriminant analysis models, includingdiscrim_linear()parsnip models fitted with the"sda"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for the regularized linear discriminant analysis models in
sparsediscrim(lda_diag(),lda_shrink_mean(),lda_shrink_cov(), andlda_emp_bayes_eigen()), includingdiscrim_linear()parsnip models fitted with the"sparsediscrim"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax of the per-class discriminant scores), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
mda::fda()flexible discriminant analysis models fit with a linear regression method (mda::polyreg()withdegree = 1ormda::gen.ridge()), includingdiscrim_linear()parsnip models fitted with the"mda"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax of the per-class discriminant scores), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
MASS::lda()linear discriminant analysis models, includingdiscrim_linear()parsnip models fitted with the"MASS"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax of the per-class discriminant scores), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
MASS::qda()quadratic discriminant analysis models, includingdiscrim_quad()parsnip models fitted with the"MASS"engine.tidypredict_fit()returns a named list of class-probability expressions (softmax of the per-class quadratic discriminant scores), andtidypredict_test()is not supported for these multiclass models. (#232)Added support for
kernlab::ksvm()linear support vector machine models (vanilladotkernel), includingsvm_linear()parsnip models fitted with the"kernlab"engine, for regression and binary classification. Non-linear kernels and multiclass classification are not supported, and classification requires a probability model (prob.model = TRUE). (#232)Added support for H2O gradient boosting models (
H2ORegressionModel,H2OBinomialModel, andH2OMultinomialModel), includingboost_tree()parsnip models fitted with the"h2o_gbm"engine, for regression and classification. Only GBM models are supported (not H2O’s XGBoost), predictions require a running H2O cluster, and gaussian, bernoulli, and multinomial distributions are supported. (#232)Added support for H2O RuleFit models (
h2o::h2o.rulefit()), includingrule_fit()parsnip models fitted with the"h2o"engine, for regression and binary classification. Predictions require a running H2O cluster, and multiclass models are not supported becauseh2o.rule_importance()does not expose the per-class coefficients. (#232)Added support for
mboost::blackboost()gradient boosting regression models, includingboost_tree()parsnip models fitted with the"mboost"engine. Only theGaussian()family is supported. (#232)Added support for
aorsf::orsf()oblique random forest regression models, includingrand_forest()parsnip models fitted with the"aorsf"engine. Only numeric predictors are supported and classification is not supported. (#232)Added support for
C50::C5.0()classification tree models, includingdecision_tree()andboost_tree()parsnip models fitted with the"C5.0"engine. Boosted models (trials > 1) combine trials by confidence-weighted voting. Fuzzy thresholds (fuzzyThreshold = TRUE) and cost matrices (costs) are not supported. (#232)Added support for rule-based
C50::C5.0()classification models (rules = TRUE), includingC5_rules()parsnip models fitted with the"C5.0"engine. Boosted rule-based models (trials > 1) are not supported. (#232)Added support for
partykit::cforest()random forest regression models, includingrand_forest()parsnip models fitted with the"partykit"engine. Classification is not supported. (#232)Added support for
LiblineaR::LiblineaR()binary logistic regression models (type0, 6, 7), includinglogistic_reg()parsnip models fitted with the"LiblineaR"engine. Also added support for linear support vector machine models, includingsvm_linear()parsnip models fitted with the"LiblineaR"engine, for regression (type11, 12, 13) and binary classification (type1-5). Classification returns the SVM decision value rather than a probability. (#232)Added support for
xrf::xrf()rule-based models (RuleFit), includingrule_fit()parsnip models fitted with the"xrf"engine, for regression (family = "gaussian") and binary classification (family = "binomial"). Multinomial models are not supported. (#232)Added support for
decision_tree()parsnip models fitted with the"rpart"engine. (#232)Added support for
linear_reg()parsnip models fitted with the"glm"engine. (#232)Added support for
quantreg::rq()quantile regression models, includinglinear_reg()parsnip models fitted with the"quantreg"engine. Models fitted with multiple quantiles return one fit expression per quantile, named by the quantile level. (#232)tidypredict_fit()now returns correct predictions for xgboost models whose feature values fall exactly on a split threshold. xgboost compares split thresholds as 32-bit floats, so a value such as awtof 3.19 was sent down the wrong branch when the comparison was made in R’s doubles. (#232)tidypredict_fit()now returns correct predictions for xgboost models that have been saved and reloaded withparse_model()andas_parsed_model(). Previously every tree collapsed to a single leaf value. (#232)tidypredict_test()now flags rows where the fitted value is above the model’s own prediction for xgboost models. Previously only differences in one direction were reported, so real disagreements could go unnoticed. (#232)tidypredict_test()now reports the maximum fit, lower, and upper differences under the correct labels, and reports absolute rather than signed differences. Previously the fit and upper values were swapped, and the fit value was omitted entirely wheninclude_intervals = FALSE. (#232)tidypredict_test()now reports a failure message for multiclass CatBoost models when results exceed the threshold. Previously it always claimed that all results were within the threshold, even whenalertwasTRUE. (#232)tidypredict_test()now compares ranger models againstpredict(). Previously the comparison silently measured tidypredict’s predictions against themselves and so always reported a difference of zero. (#232)tidypredict_test()now reports an absolute maximum difference for glmnet models, which could previously be negative. (#232)tidypredict_test()now names the model’s own predictionsfitinraw_resultsfor XGBoost, LightGBM, CatBoost and h2o models, matching every other model type. The column was previously calledbase. (#232)tidypredict_test()now reportsfit_diffas a signed difference for LightGBM, CatBoost and h2o models, so the direction of the error is visible. The threshold is applied to its absolute value, as before. (#232)tidypredict_test()results for classification models are now reported consistently:fit_diffis a 0/1 indicator, the threshold is reported as 0 since labels are compared exactly, and the message counts records that do not match rather than quoting a maximum difference. (#232)
tidypredict 1.1.0
CRAN release: 2026-02-27
New Model Supports
Added support for rpart decision tree models (
rpart). (#226)-
Added support for CatBoost models (
catboost.Model). (#179, #187, #188)- Objectives: RMSE, MAE, Quantile, MAPE, Poisson, Huber, LogCosh, Expectile, Tweedie, Logloss, CrossEntropy, MultiClass, and MultiClassOneVsAll.
- Tree types: oblivious (default
SymmetricTree) and non-oblivious (DepthwiseorLossguidegrow policy). - Categorical features are handled automatically for parsnip/bonsai models; for raw CatBoost models use
set_catboost_categories().
-
Added support for LightGBM models (
lgb.Booster). (#177, #186)- Objectives: regression, binary classification, and multiclass classification.
- Supports categorical features.
- Supports linear trees (
linear_tree = TRUE), which fit a linear model at each leaf instead of a constant.
Improvements
Tree models (rpart, partykit, ranger, randomForest, xgboost, lightgbm, catboost) now generate nested
case_when()expressions that mirror the tree structure, instead of flat expressions with all leaf conditions at the same level. This produces more efficient SQL and R code because conditions are evaluated hierarchically. (#227)parse_model()now documents the parsed model version system (v1/v2/v3) and model type classes in its help page. (#227)earth()models now support additional GLM families and link functions: Gamma, inverse.gaussian, probit, and cloglog. (#194, #195)glm()models now support additional families and link functions: Gamma family with inverse link, inverse.gaussian family with 1/mu^2 link, probit link, cloglog link, and sqrt link. (#203, #204, #205, #206, #207)glmnet()models now supportGammafamily and Cox proportional hazards (family = "cox") models. (#200, #201)xgboost support now includes additional objectives:
binary:hinge,reg:absoluteerror,reg:gamma,reg:pseudohubererror, andreg:squaredlogerror. (#184)Added a vignette on floating-point precision issues with tree-based models. (#231)
Bug Fixes
tidypredict_fit()now correctly handles xgboost models with stump trees (single leaf, no splits). (#182)tidypredict_fit()now correctly handles xgboost DART booster models withrate_drop > 0. DART uses tree weight normalization during training, and these weights are now properly applied to each tree’s predictions. (#183)tidypredict_fit()now correctly incorporatesbase_scorefor xgboost models withcount:poissonandreg:tweedieobjectives. Previously, predictions were incorrect whenbase_scorewas not the default value. (#184)tidypredict_fit()now correctly averages tree predictions for LightGBM models withboosting="rf"instead of summing them. (#185)tidypredict_fit()now uses the correct split operator (<=instead of<) for ranger models. Previously, predictions were incorrect when data values exactly matched split values. (#189)tidypredict_fit()now correctly averages tree predictions for ranger models instead of summing them. Previously, predictions werenum.treestimes too large. (#190)tidypredict_fit()now throws a clear error for ranger and randomForest classification models, which are not supported. (#191, #193)tidypredict_fit()now uses the correct split operator (<=instead of<) for randomForest models. (#192)tidypredict_fit()now correctly handles partykit stump trees (models with no splits). (#196)tidypredict_fit()now works withglmnet()models that use family function syntax (e.g.,family = gaussian()) instead of string syntax (e.g.,family = "gaussian"). (#197)tidypredict_fit()now works with models that use family function syntax (e.g.,family = gaussian()) instead of string syntax (e.g.,family = "gaussian"). (#202)
tidypredict 1.0.1
CRAN release: 2025-12-13
Bug Fixes
- Fixed bug where
base_scorewasn’t extracted correctly xgboost for version 3 or higher. (#173)
tidypredict 1.0.0
CRAN release: 2025-11-29
Breaking Changes
- Random forest implementations (ranger and randomForest) will now produce a single formula instead of a list of expressions. (#84)
New Model Supports
- Added support for glmnet models. (#165)
Improvements
xgboost models with objectives
"reg:tweedie"and"count:poisson"are now supported. (#72, @SimonCoulombe)tree based models now uses
.defaultargument in producedcase_when()code when applicable. (#153)Speed up
tidypredict_fit()for partykit and ranger packages. (#125)Speed up
tidypredict_fit()for xgboost models. (#130)randomForest models now support regression outcomes. (#77)
An informative error will now be thrown if a lm model cannot be processed due to having linear combinations of predictors. (#124)
linear models such as
lm()andglm()now work with interactions created with*and:. (#74)Cubist rules will return simplified rules whenever possible to avoid multiplying by 0 and 1. (#152)
Make work with xgboost version > 2.0.0.0. (#169)
tidypredict 0.5.1
CRAN release: 2024-12-19
- Exported a number of internal functions to be used in {orbital} package
tidypredict 0.5
CRAN release: 2023-01-18
Changes maintainer to Edgar Ruiz
Updates author’s email addresses.
Removes dependency with
stringrFixes issue with
earthparsed_models (#108)Addresses issues with XGBoost models
Improvements to XGBoosts tests
tidypredict 0.4.9
CRAN release: 2022-05-25
Fixes issue handling GLM Binomial earth models (#97)
Adds capability to handle single simple Cubist models (#57)
Fixed parenthesis issue in the creation of the interval formula (#76)
Fixed bug in SQL query generation for XGBoost models with objective
binary:logistic.Re-licensed package from GPL-3 to MIT. See consent from copyright holders here.
tidypredict 0.4.7
CRAN release: 2020-10-05
- Change to with with version 5.1.2 and above of the
earthpackage. As a result,tidypredictwill only parse objects created by this and later versions ofearth.
tidypredict 0.4.3
CRAN release: 2019-09-03
Adds support for categorical predictors in
partykitFixes
parsniptests to meet standards of new CRAN version
tidypredict 0.4.2
CRAN release: 2019-07-15
Simplifies tests that verify
rangerAdds fit method for parsed
xgboostmodelsSets conditional requirement for
xgboost, for test and vignette
tidypredict 0.4.0
CRAN release: 2019-07-12
New features
Parses
rangerclassification models.Adds method support for
broom’stidy()function. Regression models onlyAdds
as_parsed_model()function. It adds the proper class components to the list.Adds initial support for
partykit’sctree()modelAdds support for
parsnipfitted models:lm,randomForest,ranger, andearthAdds support for xgb.Booster models provided by the
xgboostpackage (@Athospd, #43)Adds support for
Cubist::cubist()models (# 36)
tidypredict 0.3.0
CRAN release: 2019-01-10
Improvements
New parsed models are now list objects as opposed to data frames.
tidypredict_to_column() no longer supports
rangerandrandomForestbecause of the multiple queries generated by multiple trees.All functions that read the parsed models and create the tidy eval formula now use the list object.
Most of the code that depends on dplyr programming has been removed.
Removes dependencies on: tidyr, tibble
The
x/yinterface forearthmodels can now be used.
Bug Fixes
- It now returns all of the trees instead of just one for tree based models (
randomForest&ranger) (#29)
tidypredict 0.2.0
CRAN release: 2018-02-25
New features
- Add support for
ranger()models.
Bug fixes
- Using
x ~.in a randomForest() formula fails (#18 @washcycle).
