A Unified Tidy Interface to R's Machine Learning Ecosystem
Overview
Provides a unified tidyverse-compatible interface to R's machine learning ecosystem - from data ingestion to model publishing. The tl_read() family reads data from files ('CSV', 'Excel', 'Parquet', 'JSON'), databases ('SQLite', 'PostgreSQL', 'MySQL', 'BigQuery'), and cloud sources ('S3', 'GitHub', 'Kaggle'). The tl_model() function wraps established implementations from 'glmnet', 'randomForest', 'xgboost', 'e1071', 'rpart', 'gbm', 'nnet', 'cluster', 'dbscan', and others with consistent function signatures and tidy tibble output. Results flow into unified 'ggplot2'-based visualization and optional formatted 'gt' tables via the tl_table() family. The underlying algorithms are unchanged; 'tidylearn' simply makes them easier to use together. Access raw model objects via the $fit slot for a supervised method, or $fit$model for an unsupervised one. Methods include random forests Breiman (2001) doi:10.1023/A:1010933404324, LASSO regression Tibshirani (1996) doi:10.1111/j.2517-6161.1996.tb02080.x, elastic net Zou and Hastie (2005) doi:10.1111/j.1467-9868.2005.00503.x, support vector machines Cortes and Vapnik (1995) doi:10.1007/BF00994018, and gradient boosting Friedman (2001) doi:10.1214/aos/1013203451.
Breaking Changes
- tidylearn requires R 4.1.0 or later. The package, its README and the vignettes use the native
|>pipe, which R 3.6 and 4.0 cannot parse. magrittr's%>%is still re-exported, so existing code that pipes with it afterlibrary(tidylearn)keeps working. - tidylearn requires ggplot2 3.4.0 or later. The plots set line widths with the
linewidthaesthetic, which ggplot2 3.3 ignores with an unknown-parameter warning and draws at the default width. The rlang floor rises from 0.4.0 to 1.0.0, the version ggplot2 3.4.0 imports, so it changes nothing for an install that meets the ggplot2 one. - Some calls that ran in 0.5.0 are now errors, with a message naming the problem: each passed an argument tidylearn ignored or returned a meaningless result. Bug Fixes describes all of them. Among them are:
Show all changes in 0.6.0Hide the rest of the changes in 0.6.0
- an argument a method cannot use, such as
maxdeth = 1oroffset =withmethod = "tree", a name glmnet does not take with"ridge","lasso"or"elastic_net", and a misspelt argument totl_pipeline(); - an
offset()term with a method that cannot apply it atpredict(), which is every method but"linear","polynomial"and"logistic"; - a metric name
tl_evaluate()ortl_cv()does not compute, such as"RMSE"; predict(type = "prob")ortype = "class"on a regression model, and a misspelttype;tl_prepare_data(impute_method = "knn"), andtl_split()with apropoutside (0, 1);- repeated model names in
tl_compare_cv()andtl_plot_importance_comparison(); typeintl_plot_interaction(), and anexclude_varsintl_auto_interactions()that is not a predictor;- a numeric response in
tl_semisupervised(), andtl_anomaly_aware(action = "downweight")with"svm"or"deep". tl_model()makes a text predictor a factor before fitting, and stores it as one in$data.method = "forest"coded a character column as numbers by the values present in whichever frame it was handed, so a row's prediction depended on the rows scored with it: on a 120-row frame drawn afterset.seed(7), in which group"c"sits near 10, three"c"rows scored alone predicted -0.11, -0.15 and 0.45, and 9.47, 9.26 and 9.78 inside the full frame. A forest now splits on the column as a category, so its fitted values change, and a text column with more than 53 distinct values is refused by randomForest ("Can not handle categorical predictors with more than 53 categories"); exclude identifiers withy ~ . - id.method = "boost"fits a text predictor, where gbm refused it ("variable 1: grp is not of type numeric, ordered, or factor"), and"xgboost"predicts a single row of one, where it failed with "contrasts can be applied only to factors with 2 or more levels". A text column used only inside a term, such asas.numeric(code), keeps its type.predict()reads a supervised model's categorical columns against the levels it was trained on, before any method sees them. New data holding only some of the categories works with"forest", which failed with "Type of predictors in new data do not match that of the training data" fordata.frame(cyl = factor("6"), wt = 3). A value given as text, or as the number a factor was made from, reads the same. A level the model was not trained on is an error naming the column, the level and the training levels;"boost"used to return a prediction for it, and the other methods' errors came from their backends.- **
predict()on a"ridge","lasso"or"elastic_net"model now useslambda_1se, so its predictions change.** It usedlambda_min, whiletl_coefficients(),tl_table_coefficients()and importance all reportlambda_1seby default, so the coefficients on display were not the ones behind the predictions.lambda_1seis the penalty the fit itself was documented as preferring for generalisation. Metrics, cross-validation and tuning scores for these methods move with it. - A
"ridge","lasso"or"elastic_net"model is now the full-data fitcv.glmnet()makes itself, in place of a secondglmnet()run along the same penalties. The two agreed only to glmnet's convergence tolerance, so coefficients and predictions move slightly -- by up to 0.004 atlambda_1seformpg ~ .onmtcarswithset.seed(1)-- and now matchcoef()andpredict()on thecv.glmnetobject kept inattr(model$fit, "cv_results"). When cross-validation chooses the largest penalty, every slope is now exactly zero: the second fit left one at about -6e-17, whichtl_table_importance()ranked at importance 100 andtl_plot_importance_regularized()drew as a bar of 5.9e-17 (predictorbof 40 rows of noise drawn afterset.seed(3)). Both now stop with the message that the penalty dropped every predictor, as for any penalty that keeps none. method = "polynomial"keeps a numeric term that is also part of an interaction as it is and adds its powers asI(x^2)up to the degree. Formpg ~ wt * hpthe fit and its predictions are those of 0.5.0, but the terms are nowwt,hp,I(wt^2),I(hp^2)andwt:hp, where 0.5.0 named thempoly(wt, degree = 2, raw = TRUE)1,poly(wt, degree = 2, raw = TRUE)2and so on, sotl_coefficients()andtl_table_coefficients()list other names in another order.predict()on an unsupervised model returns.obs_id, the row names of the data predicted on, and as many components as the model keeps, on both the training and the new-data path. A two-dimensional MDS fit fromtl_reduce_dimensions(n_components = 2)predicted.obs_idandDim1only, andDim1andDim2without.obs_idfor tibble input. PCA's training projection returned every component however many were kept. k-means assignments of new data had no.obs_id, and PCA projections of new data numbered the rows"1","2"whatever they were called.predict(type = "prob")on an"xgboost"model returns a tibble, as every other method does. It returned a data frame.tl_split(stratify = )cuts a numeric column with more than five distinct values at its quartiles. Each distinct value used to be a stratum, and a stratum holding a single row corrupted the split:sample()on one number draws from1:n, so that stratum drew a row from elsewhere in the data -- sometimes one already drawn -- and left its own row in test. On 50 rows of a continuousywithseed = 1, 0.5.0 drew 50 training rows holding only 22 distinct observations and put 28 in test; the same call now splits 38/12, each quartile atprop. Onmtcarsstratified bympg, most of whose values occur once, the split withseed = 1returned 43 rows from 32, and now splits 24/8. A numeric column with five or fewer distinct values, such ascyl, is still split by value. An integer ID such as1:50has more than five, so it is cut at its quartiles like any other numeric column. A stratum of a single row goes to the training set, so a level seen once is one the model was trained on. When such strata together hold more than a tenth of the rows, as in a character ID column, they are pooled into one stratum and split together; for a column of 50 distinct strings that is the unstratified draw, 40/10. A split that leaves the test set empty, which only a single row now does, warns.tl_model(method = "deep")holds out a random set of rows forvalidation_split, recorded in$fit$validation_rows. keras holds out the last rows as given, which oniris, sorted by species, were rows 121 to 150: 30 of the 50 virginica rows, so the model trained on 20 virginica against 50 of each other class and was validated on virginica alone. A row with a missing value is dropped from the predictors and the response together; the response kept it, so keras trained on pairs shifted by one row. A constant predictor is no longer scaled by its standard deviation of 0, which turned it intoNaNand gave every row the same prediction.tl_tune_xgboost(),tl_tune_nn()andtl_tune_deep()read the task from the response, astl_model()does.is_classificationdefaults toNULL, which treats a factor or character response, including one the formula computes such asfactor(am), as classification. With the old default ofFALSE,tl_tune_xgboost(iris, Species ~ .)was tuned as a regression on the class codes without a message, andtl_tune_nn()andtl_tune_deep()failed.is_classification = FALSEwith a factor response is an error, and so isis_classification = TRUEwith a computed response that is not a factor, such asI(mpg > 20), whichtl_tune_nn()ran; the message points tofactor()on the left-hand side. The response is reduced to the classes it holds, so a two-class subset ofiristhat still declares setosa is tuned as two classes;tl_tune_xgboost()had tuned it withmulti:softproband a setosa probability column.tl_tune_deep()returns the best configuration as atidylearn_modelin$model, sopredict()and the deep plots take it; the keras model is at$model$fit$model. It returned the internal list, andpredict()on it failed withno applicable method. Every configuration is scored on the same randomly held-out rows.tl_default_param_grid("xgboost")returns a grid: the valuestl_tune_xgboost()searches by default, plusnrounds, which that function picks by early stopping andtl_tune_grid()has to tune. It warned "Unknown method" and returned an empty list.is_classification, which changed no grid, now changes two regression grids:"svm"also tunesepsilon, and the large"forest"grid usesnodesize = c(3, 5, 10)around randomForest's regression default of 5 in place ofc(1, 3, 5). With the defaultis_classification = TRUEthe grids are unchanged.tl_plot_xgboost_importance()returns a ggplot, as documented, and ranks by the measureimportance_typenames. It returnedxgb.plot.importance()'s data.table, drew with base graphics, and ignoredimportance_type. A linear booster (booster = "gblinear") has only coefficients, whichimportance_type = "weight"draws, ranked by size; left at its default,importance_typetakes them for such a model.tl_plot_partial_dependence()on a classification model draws one line per class when there are three or more, and the positive class (the second level) for two, named on the y axis. Its data has aclasscolumn for the class each row describes. The curve was the mean probability of the second class alone, and the multiclassclasscolumn held whichever class had the highest mean probability, which was not the class drawn....intl_semisupervised()andtl_stratified_models()reaches the supervised model only, and the newcluster_argstakes the clustering step's settings. Both stages got..., socp = 0.001failed in k-means with "unused argument", andnstart = 5reached k-means and then brokeglm(). A clustering setting now goes incluster_args, e.g.cluster_args = list(nstart = 5); one left in..., such asnstart,iter.max,algorithm,hclust_methodor pam'svariant, is refused with a message saying so.sampsize,keep.dataandtrace, which randomForest, gbm and nnet take, still reach the supervised model.tl_semisupervised()with a response the formula computes, such asfactor(am) ~ wt + hp + qsec, writes each propagated label to the column the response is computed from, as that column's own value in a labelled row of the same class. The returned$data$amtherefore holds 0 and 1, where it held a factor.tl_interaction_effects()holds a variable it does not vary at its most frequent value, astl_plot_interaction()does. It held a factor at its first level: withcarba factor,mpg ~ wt * hp + carbwas evaluated atcarb = 1by one function andcarb = 2by the other.tl_interaction_effects()returns a factor variable in its results --var,by_varor a variable it holds fixed -- as a factor with the model's levels, where it was a character vector; the values are unchanged. That is what lets it work for a forest with a factor predictor, which failed withType of predictors in new data do not match that of the training databecause the grid held the factor as text. Both it andtl_plot_interaction()leave out a factor level no row uses: on iris without setosa, both predicted at setosa, whichpredict()refused as a new level. They also leave out a missing value of a character variable, whichtl_interaction_effects()failed on with0 (non-NA) cases. A logical variable is treated as two-level; both functions failed withvariable 'lg' was fitted with type "logical" but type "numeric" was supplied. A factor value given inat_valuesorfixed_valuestakes the column's levels, and one that is not a level is refused.mpg ~ log(wt) * hpno longer warns that the model has nowt:hpinteraction.tl_interaction_effects()reportsfit,lower,upperandslopeon the response scale whether or notintervals = TRUE, and itssecolumn is removed. With intervals a logistic model's values were log odds (am ~ wt * hpgavefitfrom -32 to 40) and without them probabilities. A glm's interval is now built on the link scale and transformed back, and a linear model's is the t-based intervalpredict.lm()gives. For a glm thesecolumn was on the link scale. A model with no standard errors, such as a tree, failed with$ operator is invalid for atomic vectorsand now returns point estimates with a message.augment_hclust()attaches clusters by position. It joined onrow_number(), which counts within each group, so on a grouped tibble ofUSArrestscut atk = 334 of 50 rows received another row's cluster. Data with a different number of rows from the tree was padded withNAor cut short without a message; it is now an error naming both counts. The caller's grouping stays on the returned data, and so do its row names, which the join reset to"1","2", ...; aclustercolumn the data already has is kept beside the new one under dplyr's repaired names (cluster...5andcluster...6forUSArrests), where the join gavecluster.xandcluster.y.tidy_rules(), and sotidy_apriori()'srules_tbl, gains the list columnslhs_itemsandrhs_items, each side's items as a character vector, after the quality measures. The item-matching helpers read them; a table of rules without them is refused with a message saying to keep them, and atidy_apriori()result saved without them is rebuilt from the rules it carries. An empty rule set now gives a zero-row tibble with the usual columns. It gave a tibble with no columns, sorecommend_products()failed withobject 'confidence' not found,filter_rules_by_item()andfind_related_items()withobject 'lhs' not found, andvisualize_rules()when the plot was drawn; they now return zero rows, or an empty plot.tidy_pam()'smedoid_indexis the medoid's integer row position.pam()reports medoids by label when the distances carry labels, sotidy_pam(mtcars, k = 3)gave"Toyota Corona","Merc 450SE"and"Duster 360"where the same data as a tibble gave 21, 12 and 7. Both now give 21, 12 and 7.calc_validation_metrics()andcompare_clusterings()gain ann_noisecolumn, afteravg_size, counting the points labelled 0, DBSCAN's label for noise. The other clustering methods number their clusters from 1, so it is 0 for them. The columns after it move one place along.- Counts that ran a range backwards or reached
cutree()as a vector are refused with a message naming the argument.get_pca_loadings()andaugment_pca()withn_components = 0returned PC1, since1:0isc(1, 0), andget_pca_loadings(n_components = 5)on a four-component PCA returned all four;n_componentsmust now be between 1 and the number of components.tidy_cutree(k = 2:3)returned 100 rows
The NEWS section is longer than shown here.
From NEWS.md in the release
Install
Health
- OK13 OK2026-08-04
- NOTE12 OK, 1 NOTE2026-08-01
- OK13 OK2026-06-09
- ERROR12 OK, 1 ERROR2026-06-08
- OK14 OK2026-04-14
Show 2 earlier changesHide 2 earlier changes
- ERROR13 OK, 1 ERROR2026-04-10
- OK14 OK2026-03-10
Documentation
- Help pages
- 243 (39 internal)includes a package overview page
- Examples that run
- 17%30 run in a plain check (6 of them under a condition) · 140 only when donttest examples are enabled (R CMD check --as-cran) · 9 never run
- Exported functions with examples
- 99%
- Documented parameters
- 100%
- Return-value docs
- 100%
- Help pages citing references
- 0%
- Automated Machine Learning with tidylearn HTML
- Compute Backends: CPU, GPU and Cloud HTML
- Data Ingestion with tidylearn HTML
- Diagnostics HTML
- Getting Started with tidylearn HTML
- Integration Workflows: Combining Supervised and Unsupervised Learning HTML
- Market Basket Analysis HTML
- Reporting with tidylearn HTML
- Supervised Learning with tidylearn HTML
- Tuning and Pipelines HTML
Show 1 moreHide 1 vignette
Downloads
Repository
- PRs merged (all time)
- 100%
- Median days to close, 50 most recently updated closed issues
- 26
- Median days to close, 50 most recently updated closed PRs
- 0
Stars over time
Forks over time
Issues over time
PRs over time
Commits over time
10 development-tooling and community-health practices detected across 5 families in the upstream repository
Checks run against github.com/ces0491/tidylearn on 2026-10-18.
The R CMD check workflow runs on Linux, macOS and Windows, and on R-devel
GitHub Pages site last deployed 2026-10-09 · The repository has a site deploy workflow
pkgdown site last built 2026-10-09
Show all practicesHide 1 practice
This repository shows signs of AI coding tools.
- Claude Codefirst seen 2026-01-19Named in .Rbuildignore: .claudeNamed in .gitignore: .claudePull request it wrote: opened by a project memberCommits: at least 134 crediting it in the message
- Claude Opus 547 commits · Aug to Sep 2026
- Claude Opus 5.542 commits · Oct 2026
- Claude Opus 4.68 commits · Mar to Apr 2026
- Claude Opus 4.73 commits · May 2026
Some of these counts are a minimum, because the search stopped before the oldest commits.
Dependencies
Installing tidylearn adds 122 packages (67 with compiled code, 95 MB of source tarballs). Heaviest: stringi 11 MB, ggplot2 6.0 MB, data.table 5.7 MB.
Code & Tests
People & History
Pick two releases to compare their code metrics.
- 0.6.0Latest
- 0.5.02026-09-02 · diff ↗ ·
added 4 exports
tl_cloud_allow_host, tl_cloud_allowed_hosts, tl_cloud_consent, tl_cloud_jobs - 0.4.0
- 0.3.12026-05-19 · diff ↗
- RR 4.6.0 released · 2026-04-24
- 0.3.02026-04-09 · diff ↗ ·
added 18 exports
tl_read, tl_read_bigquery, tl_read_csv, tl_read_db, tl_read_dir, tl_read_excel, tl_read_github, tl_read_json, tl_read_kaggle, tl_read_mysql, tl_read_parquet, tl_read_postgres, tl_read_rdata, tl_read_rds, tl_read_s3, tl_read_sqlite, tl_read_tsv, tl_read_zip - 0.2.02026-03-16 · diff ↗ ·
added 9 exports
tl_table, tl_table_clusters, tl_table_coefficients, tl_table_comparison, tl_table_confusion, tl_table_importance, tl_table_loadings, tl_table_metrics, tl_table_variance - 0.1.12026-03-13 · diff ↗
- 0.1.02026-02-06
- RR 4.5.0 released · 2025-04-11
Package metadata
- First published
- 2026-02-06
- Total releases
- 8 / 1 yrs
- License
- MIT + file LICENSE OSI
- Minimum R
- ≥ 4.1.0
- Download size
- 1.3 MB
Cite
Cite this package
Run in R for the authors' preferred citation:
citation("tidylearn")The package provides a citation file that builds its entry when R runs it. Run citation() to see it.
Cite the R Observatory
For a number measured here: a download total, a coverage figure, an archival date.
From data release v2026-10-22, which the citation names so these numbers can be found later. More on citing and the projects behind them.