| name | tidymodels-review-patterns |
| description | Review patterns for tidymodels workflows, including leakage, resampling, tuning, metrics, and reproducibility. |
Tidymodels Code Review Patterns
Overview
Anti-pattern detection and best practices for tidymodels workflows based on "Tidy Modeling with R" (TMwR) principles. This skill enables systematic code review for data leakage, resampling violations, workflow issues, evaluation problems, and reproducibility concerns.
Data Leakage Patterns (CRITICAL)
DL-001: Recipe Fitted on Test Data
Severity: CRITICAL
Anti-Pattern:
rec <- recipe(outcome ~ ., data = test_data) |>
prep()
rec <- recipe(outcome ~ ., data = train_data) |>
prep(training = test_data)
Correct Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
prep(training = train_data)
wf <- workflow() |>
add_recipe(rec) |>
add_model(model_spec)
fit <- fit(wf, data = train_data)
Detection: Look for prep() calls with test data or recipes defined on test sets.
DL-002: Preprocessing Before Split
Severity: CRITICAL
Anti-Pattern:
df_normalized <- df |>
mutate(across(where(is.numeric), scale))
split <- initial_split(df_normalized)
important_vars <- df |>
select(where(~cor(.x, df$outcome) > 0.3))
split <- initial_split(important_vars)
Correct Pattern:
split <- initial_split(df, strata = outcome)
train_data <- training(split)
rec <- recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors()) |>
step_corr(all_numeric_predictors(), threshold = 0.9)
Detection: Any transformations (scale, normalize, mutate) applied before initial_split().
DL-003: Target Encoding Without Cross-Validation
Severity: CRITICAL
Anti-Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
step_lencode_mixed(all_nominal_predictors(), outcome = vars(outcome))
prepped <- prep(rec)
Correct Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
step_lencode_mixed(all_nominal_predictors(), outcome = vars(outcome))
wf <- workflow() |>
add_recipe(rec) |>
add_model(model_spec)
cv_results <- fit_resamples(wf, resamples = vfold_cv(train_data))
Detection: step_lencode_* or step_embed used outside workflow with resampling.
DL-004: Feature Selection Using Test Data
Severity: CRITICAL
Anti-Pattern:
test_cors <- cor(test_data[, -1], test_data$outcome)
selected_vars <- names(test_cors[abs(test_cors) > 0.3])
importance <- varImp(model, data = test_data)
Correct Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
step_select_vip(all_predictors(), outcome = "outcome", threshold = 0.8)
rfe_results <- rfe_fit(
wf,
resamples = vfold_cv(train_data),
sizes = c(5, 10, 15, 20)
)
Detection: Variable selection operations referencing test data.
DL-005: prep() Called Before initial_split()
Severity: CRITICAL
Anti-Pattern:
full_rec <- recipe(outcome ~ ., data = full_data) |>
step_normalize(all_numeric_predictors()) |>
prep()
split <- initial_split(full_data)
Correct Pattern:
split <- initial_split(full_data, strata = outcome)
train_data <- training(split)
rec <- recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors())
Detection: Sequence analysis - prep() appearing before initial_split().
Resampling Violations (MAJOR/CRITICAL)
RS-001: Missing Stratified Sampling
Severity: MAJOR (CRITICAL for imbalanced data)
Anti-Pattern:
split <- initial_split(df)
folds <- vfold_cv(train_data, v = 10)
Correct Pattern:
split <- initial_split(df, strata = outcome)
folds <- vfold_cv(train_data, v = 10, strata = outcome)
split <- initial_split(df, strata = outcome, breaks = 4)
Detection: Missing strata = argument with classification outcomes or highly skewed continuous outcomes.
RS-002: Evaluating Model on Training Data
Severity: CRITICAL
Anti-Pattern:
fit <- fit(wf, data = train_data)
preds <- predict(fit, train_data)
metrics <- yardstick::metrics(preds, truth = outcome, estimate = .pred)
Correct Pattern:
fit <- fit(wf, data = train_data)
preds <- predict(fit, test_data)
metrics <- yardstick::metrics(
bind_cols(test_data, preds),
truth = outcome,
estimate = .pred
)
cv_results <- fit_resamples(wf, resamples = folds)
collect_metrics(cv_results)
Detection: predict() and metrics computed on same data used for fit().
RS-003: Tuning Without Nested Cross-Validation
Severity: MAJOR
Anti-Pattern:
folds <- vfold_cv(train_data)
tune_results <- tune_grid(wf, resamples = folds)
best_params <- select_best(tune_results)
final_wf <- finalize_workflow(wf, best_params)
fit_resamples(final_wf, resamples = folds)
Correct Pattern:
split <- initial_split(df, strata = outcome)
train_data <- training(split)
test_data <- testing(split)
inner_folds <- vfold_cv(train_data, strata = outcome)
tune_results <- tune_grid(wf, resamples = inner_folds)
final_fit <- fit(finalize_workflow(wf, select_best(tune_results)), train_data)
predict(final_fit, test_data)
outer_folds <- nested_cv(train_data, outside = vfold_cv(v = 5), inside = vfold_cv(v = 5))
Detection: Same resampling object used for both tuning and final evaluation.
RS-004: Missing Random Seeds
Severity: MAJOR
Anti-Pattern:
split <- initial_split(df)
folds <- vfold_cv(train_data)
boot <- bootstraps(train_data)
Correct Pattern:
set.seed(123)
split <- initial_split(df, strata = outcome)
set.seed(456)
folds <- vfold_cv(training(split), strata = outcome)
ctrl <- control_resamples(save_pred = TRUE)
Detection: Random operations (initial_split, vfold_cv, bootstraps, mc_cv) without preceding set.seed().
RS-005: Validation Set Reuse
Severity: CRITICAL
Anti-Pattern:
val_split <- validation_split(train_data)
results1 <- fit_resamples(wf1, val_split)
results2 <- fit_resamples(wf2, val_split)
tune_results <- tune_grid(wf1, val_split)
final_results <- fit_resamples(final_wf, val_split)
Correct Pattern:
split <- initial_split(df, strata = outcome)
train_data <- training(split)
test_data <- testing(split)
folds <- vfold_cv(train_data, strata = outcome)
results1 <- fit_resamples(wf1, folds)
results2 <- fit_resamples(wf2, folds)
tune_results <- tune_grid(wf1, folds)
final_fit <- last_fit(final_wf, split)
Detection: Same validation/test split used in multiple fit_resamples() or tune_grid() calls.
Workflow Issues (MINOR/MAJOR)
WF-001: Not Using Workflows
Severity: MINOR to MAJOR
Anti-Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors()) |>
prep()
train_baked <- bake(rec, new_data = NULL)
test_baked <- bake(rec, new_data = test_data)
model <- linear_reg() |> set_engine("lm")
fit <- fit(model, outcome ~ ., data = train_baked)
preds <- predict(fit, test_baked)
Correct Pattern:
wf <- workflow() |>
add_recipe(recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors())) |>
add_model(linear_reg() |> set_engine("lm"))
fit <- fit(wf, data = train_data)
preds <- predict(fit, new_data = test_data)
Benefits of workflows:
- Automatic handling of preprocessing on new data
- Proper integration with tuning and resampling
- Cleaner code organization
- Reduced risk of data leakage
WF-002: Inconsistent Preprocessing Train/Test
Severity: MAJOR
Anti-Pattern:
train_processed <- train_data |>
mutate(across(where(is.numeric), ~(.x - mean(.x)) / sd(.x)))
test_processed <- test_data |>
mutate(across(where(is.numeric), ~(.x - mean(.x)) / sd(.x)))
Correct Pattern:
rec <- recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors())
wf <- workflow() |>
add_recipe(rec) |>
add_model(model_spec)
fit <- fit(wf, train_data)
predict(fit, test_data)
Detection: Manual transformations applied separately to train and test data.
WF-003: Not Finalizing Workflow After Tuning
Severity: MAJOR
Anti-Pattern:
tune_results <- tune_grid(wf, resamples = folds, grid = 20)
best <- select_best(tune_results, metric = "rmse")
final_fit <- fit(wf, data = train_data)
Correct Pattern:
tune_results <- tune_grid(wf, resamples = folds, grid = 20)
best <- select_best(tune_results, metric = "rmse")
final_wf <- finalize_workflow(wf, best)
final_fit <- fit(final_wf, data = train_data)
final_results <- last_fit(final_wf, split)
Detection: select_best() or select_by_*() not followed by finalize_workflow().
Evaluation Issues (MAJOR)
ME-001: Only Accuracy for Imbalanced Data
Severity: MAJOR
Anti-Pattern:
metrics <- metric_set(accuracy)
results <- fit_resamples(wf, folds, metrics = metrics)
Correct Pattern:
metrics <- metric_set(
accuracy,
bal_accuracy,
kap,
sens,
spec,
ppv,
npv,
f_meas,
roc_auc,
pr_auc
)
results <- fit_resamples(wf, folds, metrics = metrics)
Detection: metric_set(accuracy) alone with classification tasks.
ME-002: Wrong Metrics for Model Mode
Severity: MAJOR
Anti-Pattern:
model <- logistic_reg() |> set_mode("classification")
metrics <- metric_set(rmse, mae, rsq)
model <- linear_reg() |> set_mode("regression")
metrics <- metric_set(accuracy, roc_auc)
Correct Pattern:
model <- logistic_reg() |> set_mode("classification")
metrics <- metric_set(accuracy, roc_auc, f_meas, kap)
model <- linear_reg() |> set_mode("regression")
metrics <- metric_set(rmse, mae, rsq, mape, huber_loss)
Detection: Mismatch between model mode and metric types.
ME-003: Missing Calibration Assessment
Severity: MINOR to MAJOR
Anti-Pattern:
results <- fit_resamples(wf, folds, metrics = metric_set(roc_auc))
Correct Pattern:
final_fit <- last_fit(wf, split)
preds <- collect_predictions(final_fit)
library(probably)
cal_plot_windowed(preds, truth = outcome, .pred_class)
brier_class(preds, truth = outcome, .pred_positive_class)
cal_estimate <- cal_estimate_logistic(preds, truth = outcome)
Detection: Classification models without calibration assessment (no cal_plot_* or brier_*).
ME-004: Missing Confidence Intervals
Severity: MINOR
Anti-Pattern:
results <- fit_resamples(wf, folds)
collect_metrics(results)
Correct Pattern:
results <- fit_resamples(wf, folds)
collect_metrics(results) |>
select(.metric, mean, std_err, n) |>
mutate(
ci_lower = mean - 1.96 * std_err,
ci_upper = mean + 1.96 * std_err
)
boot_results <- fit_resamples(wf, bootstraps(train_data, times = 1000))
int_pctl(boot_results, metrics)
Detection: Metric summaries without std_err or CI calculations.
ME-005: Different Resamples for Model Comparison
Severity: MAJOR
Anti-Pattern:
set.seed(123)
folds1 <- vfold_cv(train_data)
results1 <- fit_resamples(wf1, folds1)
set.seed(456)
folds2 <- vfold_cv(train_data)
results2 <- fit_resamples(wf2, folds2)
Correct Pattern:
set.seed(123)
folds <- vfold_cv(train_data, strata = outcome)
results1 <- fit_resamples(wf1, folds)
results2 <- fit_resamples(wf2, folds)
results3 <- fit_resamples(wf3, folds)
library(workflowsets)
all_wfs <- workflow_set(
preproc = list(rec1 = rec),
models = list(lm = lm_spec, rf = rf_spec, xgb = xgb_spec)
)
results <- workflow_map(all_wfs, resamples = folds)
autoplot(results)
Detection: Multiple vfold_cv() or mc_cv() calls with different seeds before model comparisons.
Reproducibility Issues (MINOR/MAJOR)
RP-001: Missing set.seed()
Severity: MAJOR
Anti-Pattern:
split <- initial_split(df)
folds <- vfold_cv(train_data)
tune_results <- tune_grid(wf, folds)
Correct Pattern:
set.seed(123)
split <- initial_split(df, strata = outcome)
train_data <- training(split)
set.seed(234)
folds <- vfold_cv(train_data, strata = outcome)
doParallel::registerDoParallel(cores = 4)
tune_results <- tune_grid(
wf,
folds,
control = control_grid(parallel_over = "resamples")
)
RP-002: Missing tidymodels_prefer()
Severity: MINOR
Anti-Pattern:
library(tidymodels)
library(MASS)
library(plyr)
df |> select(a, b)
Correct Pattern:
library(tidymodels)
tidymodels_prefer()
library(MASS)
library(plyr)
df |> select(a, b)
RP-003: Hard-Coded File Paths
Severity: MINOR
Anti-Pattern:
df <- read_csv("/Users/john/Documents/project/data/raw_data.csv")
write_rds(model, "/Users/john/Documents/project/models/final_model.rds")
Correct Pattern:
library(here)
df <- read_csv(here("data", "raw_data.csv"))
write_rds(model, here("models", "final_model.rds"))
df <- read_csv("data/raw_data.csv")
RP-004: Missing renv for Package Management
Severity: MINOR to MAJOR
Anti-Pattern:
library(tidymodels)
Correct Pattern:
RP-005: Missing Session Info
Severity: MINOR
Anti-Pattern:
Correct Pattern:
sessionInfo()
devtools::session_info()
tidymodels::tidymodels_conflicts()
tidymodels::tidymodels_packages()
cat("Analysis completed:", Sys.time(), "\n")
sessionInfo()
Code Review Checklist
Pre-Review Questions
| # | Question | Check |
|---|
| 1 | Is initial_split() called before any preprocessing? | ☐ |
| 2 | Is stratification used for splits and CV? | ☐ |
| 3 | Are seeds set before random operations? | ☐ |
| 4 | Is a workflow used (not manual prep/bake)? | ☐ |
| 5 | Is the workflow finalized after tuning? | ☐ |
| 6 | Are metrics appropriate for the task? | ☐ |
| 7 | Are confidence intervals reported? | ☐ |
| 8 | Is test data used only once at the end? | ☐ |
| 9 | Are model comparisons done on same resamples? | ☐ |
| 10 | Is session info recorded? | ☐ |
TMwR Compliance Score
Calculate compliance score:
- Critical Issues (DL-*, RS-002, RS-003, RS-005): -25 points each
- Major Issues (RS-001, RS-004, WF-002, WF-003, ME-*): -10 points each
- Minor Issues (WF-001, RP-*): -5 points each
Score Interpretation:
- 100: Perfect TMwR compliance
- 80-99: Good compliance, minor issues
- 60-79: Acceptable, some major issues
- Below 60: Significant problems, needs revision
Remediation Examples
Before: Multiple Issues
library(tidymodels)
df <- read_csv("data.csv") |>
mutate(across(where(is.numeric), scale))
split <- initial_split(df)
folds <- vfold_cv(training(split))
rec <- recipe(outcome ~ ., data = training(split)) |>
prep()
train_baked <- bake(rec, new_data = NULL)
model <- rand_forest() |> fit(outcome ~ ., data = train_baked)
predict(model, train_baked) |>
bind_cols(train_baked) |>
metrics(truth = outcome, estimate = .pred)
After: TMwR Compliant
library(tidymodels)
tidymodels_prefer()
set.seed(123)
df <- read_csv(here::here("data", "data.csv"))
split <- initial_split(df, prop = 0.8, strata = outcome)
train_data <- training(split)
test_data <- testing(split)
rec <- recipe(outcome ~ ., data = train_data) |>
step_normalize(all_numeric_predictors())
model_spec <- rand_forest(trees = 500) |>
set_mode("regression") |>
set_engine("ranger")
wf <- workflow() |>
add_recipe(rec) |>
add_model(model_spec)
set.seed(234)
folds <- vfold_cv(train_data, v = 10, strata = outcome)
cv_results <- fit_resamples(
wf,
folds,
metrics = metric_set(rmse, mae, rsq),
control = control_resamples(save_pred = TRUE)
)
collect_metrics(cv_results) |>
mutate(
ci_lower = mean - 1.96 * std_err,
ci_upper = mean + 1.96 * std_err
)
final_fit <- last_fit(wf, split)
collect_metrics(final_fit)
sessionInfo()
Key Packages Summary
| Package | Purpose |
|---|
| tidymodels | Meta-package for modeling framework |
| workflows | Workflow management |
| recipes | Preprocessing pipelines |
| parsnip | Model specifications |
| tune | Hyperparameter tuning |
| rsample | Resampling infrastructure |
| yardstick | Model metrics |
| probably | Calibration and thresholds |
| workflowsets | Comparing multiple workflows |
| stacks | Ensemble methods |
| broom | Tidy model outputs |
| here | Reproducible file paths |
| renv | Package management |
Best Practices Summary
- Always split first:
initial_split() before any data exploration or transformation
- Use workflows: Encapsulate preprocessing and models together
- Stratify everything: Stratify splits and CV folds by outcome
- Set seeds: Before every random operation
- Same folds for comparison: Use identical resamples when comparing models
- Appropriate metrics: Match metrics to problem type and class balance
- Report uncertainty: Include confidence intervals for all metrics
- Touch test once: Only evaluate on test data at the very end
- Finalize workflows: Always
finalize_workflow() after tuning
- Document environment: Record session info for reproducibility