| 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 |>