add-recipe-step — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited add-recipe-step (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Guide for developing new preprocessing steps that extend the recipes package. This skill provides best practices, complete code templates, and testing patterns for creating custom recipe steps.
--------------------------------------------------------------------------------
This skill supports two distinct development contexts:
Creating a new R package that extends recipes with custom steps.
recipes:: prefix for all functionsContributing directly to recipes via pull requests.
--------------------------------------------------------------------------------
INSTRUCTIONS FOR CLAUDE: Run the verification script first to determine the development context:
Rscript -e 'source(Sys.glob(path.expand("~/.claude/plugins/cache/tidymodels-skills/tidymodels-dev/*/tidymodels/shared-references/scripts/verify-setup.R"))[1])'Then follow the appropriate path based on the output:
Development Guide](references/source-guide.md)
Go to Extension Development Guide
Prerequisites](references/package-extension-prerequisites.md) to resolve warnings first
--------------------------------------------------------------------------------
INSTRUCTIONS FOR CLAUDE: Load references progressively based on step type.
Determine step type from user requirements, then read ONLY that reference:
create-new-columns-steps.md ONLY
Do NOT read all three references. Read only the one needed for this step type.
Read other references ONLY if specifically mentioned or needed:
Default: Don't pre-load optional references.
--------------------------------------------------------------------------------
Creating a custom recipe step provides:
INSTRUCTIONS FOR CLAUDE: Check if repos/recipes/ exists in the current working directory. Use this to guide development:
If `repos/recipes/` exists:
repos/recipes/R/center.R) to study implementationpatterns
repos/recipes/tests/testthat/test-step_center.R) fortesting patterns
If `repos/recipes/` does NOT exist:
Guide](references/package-repository-access.md)
https://github.com/tidymodels/recipes/blob/main/R/[file-name].RWhen to use repository references:
roles?")
See Repository Access Guide for setup instructions.
Development Guides:
packages that extend recipes
recipes itself
Reference Files:
prep/bake workflow, step types
existing columns
new columns
rows
required_pkgs(), sparsity methods
reference
Shared References (Extension Development):
Extension prerequisites
iteration cycle
Extension testing guide
Documentation templates
code style
Extension issues
Source Development Specific:
internal helpers
internal functions
Source-specific issues
See Development Workflow for complete details.
Fast iteration cycle (run repeatedly):
devtools::document() - Generate documentationdevtools::load_all() - Load your packagedevtools::test() - Run testsFinal validation (run once at end):
devtools::check() - Full R CMD checkWARNING: Do NOT run check() during iteration. It takes 1-2 minutes and is unnecessary until you're done.
See Step Architecture for complete details.
Every recipe step consists of three functions:
step_center()) - User-facing functionenquos(...) to capture variable selectionsadd_step()step_center_new()) - Internal constructorstep(subclass = "name", ...) to create S3 objectprep.step_*() - Estimates parameters from training databake.step_*() - Applies transformation to new dataprint.step_*() - Displays step in recipe summarytidy.step_*() - Returns step information as tibbleprep() - Training phase:
all_numeric() → actual column names)trained = TRUEbake() - Application phase:
Example workflow:
# Define recipe with step
rec <- recipe(mpg ~ ., data = mtcars) |>
step_center(all_numeric_predictors())
# prep() trains the step (calculates means)
trained_rec <- prep(rec, training = mtcars)
# bake() applies the step (subtracts means)
new_data <- bake(trained_rec, new_data = mtcars)Choose the appropriate template based on what your step does:
┌─────────────────────────────────────────────────────────────┐
│ What does your step do? │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────────┼───────────────────┐
│ │ │
▼ ▼ ▼
Transform Create new Remove/filter
existing columns rows
columns
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ MODIFY IN │ │ CREATE │ │ ROW │
│ PLACE │ │ NEW COLUMNS │ │ OPERATION │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
▼ ▼ ▼
role = NA role = skip = TRUE
No keep_cols "predictor" (usually)
keep_original_cols
│ │ │
▼ ▼ ▼
Examples: Examples: Examples:
- center - dummy - filter
- scale - pca - sample
- normalize - interact - naomit
- log - poly - sliceDecision guide:
Steps](references/modify-in-place-steps.md)
Steps](references/row-operation-steps.md)
This example shows all required components for a modify-in-place step using extension development patterns (with recipes:: prefix).
For source development, see Source Development Guide for examples using internal functions directly.
Reference implementation: R/center.R in recipes repository
# R/step_center.R
#' Center numeric variables
#'
#' `step_center()` creates a *specification* of a recipe step that will
#' normalize numeric data to have a mean of zero.
#'
#' @inheritParams step_normalize
#' @param ... One or more selector functions to choose variables for this step.
#' See [recipes::selections()] for more details.
#' @param role Not used by this step since no new variables are created.
#' @param na_rm A logical value indicating whether NA values should be removed
#' when computing means.
#' @param means A named numeric vector of means. This is `NULL` until computed
#' by [prep()].
#'
#' @return An updated version of `recipe` with the new step added to the
#' sequence of any existing operations.
#'
#' @family normalization steps
#' @export
#'
#' @details
#' Centering data means that the average of the variable is subtracted from the
#' data. `step_center` estimates the variable means from the data used in the
#' `training` argument of [prep()]. [bake()] then applies the centering to new
#' data sets using these means.
#'
#' # Tidying
#'
#' When you [`tidy()`][recipes::tidy.recipe()] this step, a tibble is returned
#' with columns `terms`, `value`, and `id`:
#'
#' \describe{
#' \item{terms}{character, the selectors or variables selected}
#' \item{value}{numeric, the means}
#' \item{id}{character, id of this step}
#' }
#'
#' # Case weights
#'
#' This step performs an unsupervised operation that can utilize case weights.
#' As a result, case weights are used with frequency weights as well as
#' importance weights. For more information, see the documentation in
#' [recipes::case_weights] and the examples on `tidymodels.org`.
#'
#' @examplesIf rlang::is_installed("modeldata")
#' data(biomass, package = "modeldata")
#'
#' biomass_tr <- biomass[biomass$dataset == "Training", ]
#' biomass_te <- biomass[biomass$dataset == "Testing", ]
#'
#' rec <- recipe(
#' HHV ~ carbon + hydrogen + oxygen + nitrogen + sulfur,
#' data = biomass_tr
#' )
#'
#' center_trans <- rec |>
#' step_center(carbon, hydrogen)
#'
#' center_obj <- prep(center_trans, training = biomass_tr)
#'
#' transformed_te <- bake(center_obj, biomass_te)
#'
#' biomass_te[1:10, names(transformed_te)]
#' transformed_te
#'
#' tidy(center_trans, number = 1)
#' tidy(center_obj, number = 1)
step_center <- function(
recipe,
...,
role = NA,
trained = FALSE,
means = NULL,
na_rm = TRUE,
skip = FALSE,
id = recipes::rand_id("center")
) {
recipes::add_step(
recipe,
step_center_new(
terms = rlang::enquos(...),
trained = trained,
role = role,
means = means,
na_rm = na_rm,
skip = skip,
id = id,
case_weights = NULL
)
)
}
step_center_new <- function(terms, role, trained, means, na_rm, skip, id,
case_weights) {
recipes::step(
subclass = "center",
terms = terms,
role = role,
trained = trained,
means = means,
na_rm = na_rm,
skip = skip,
id = id,
case_weights = case_weights
)
}#' @export
prep.step_center <- function(x, training, info = NULL, ...) {
# 1. Resolve variable selections to actual column names
col_names <- recipes::recipes_eval_select(x$terms, training, info)
# 2. Validate column types
recipes::check_type(training[, col_names], types = c("double", "integer"))
# 3. Extract case weights if applicable
wts <- recipes::get_case_weights(info, training)
were_weights_used <- recipes::are_weights_used(wts, unsupervised = TRUE)
if (isFALSE(were_weights_used)) {
wts <- NULL
}
# 4. Compute means for each column
means <- vapply(
training[, col_names],
function(col) {
if (is.null(wts)) {
mean(col, na.rm = x$na_rm)
} else {
weighted.mean(col, w = as.double(wts), na.rm = x$na_rm)
}
},
numeric(1)
)
# 5. Check for issues
inf_cols <- col_names[is.infinite(means)]
if (length(inf_cols) > 0) {
cli::cli_warn(
"Column{?s} {.var {inf_cols}} returned Inf or NaN. \\
Consider checking your data before preprocessing."
)
}
# 6. Return updated step with trained = TRUE
step_center_new(
terms = x$terms,
role = x$role,
trained = TRUE,
means = means,
na_rm = x$na_rm,
skip = x$skip,
id = x$id,
case_weights = were_weights_used
)
}#' @export
bake.step_center <- function(object, new_data, ...) {
# 1. Get column names from trained step
col_names <- names(object$means)
# 2. Validate required columns exist in new data
recipes::check_new_data(col_names, object, new_data)
# 3. Apply transformation
for (col_name in col_names) {
new_data[[col_name]] <- new_data[[col_name]] - object$means[[col_name]]
}
# 4. Return modified data
new_data
}#' @export
print.step_center <- function(x, width = max(20, options()$width - 30), ...) {
title <- "Centering for "
recipes::print_step(
x$columns,
x$terms,
x$trained,
title,
width,
case_weights = x$case_weights
)
invisible(x)
}
#' @rdname tidy.recipe
#' @export
tidy.step_center <- function(x, ...) {
if (recipes::is_trained(x)) {
res <- tibble::tibble(
terms = names(x$means),
value = unname(x$means)
)
} else {
term_names <- recipes::sel2char(x$terms)
res <- tibble::tibble(
terms = term_names,
value = rlang::na_dbl
)
}
res$id <- x$id
res
}# tests/testthat/test-center.R
test_that("centering works correctly", {
rec <- recipe(mpg ~ ., data = mtcars) |>
step_center(disp, hp)
prepped <- prep(rec, training = mtcars)
results <- bake(prepped, mtcars)
# Check means are approximately zero
expect_equal(mean(results$disp), 0, tolerance = 1e-7)
expect_equal(mean(results$hp), 0, tolerance = 1e-7)
# Check tidy output
trained_tidy <- tidy(prepped, 1)
expect_equal(trained_tidy$value, c(mean(mtcars$disp), mean(mtcars$hp)))
})
test_that("centering handles NA correctly", {
df <- mtcars
df$disp[1:3] <- NA
# With na_rm = TRUE (default)
rec_remove <- recipe(mpg ~ ., data = df) |>
step_center(disp, na_rm = TRUE)
prepped <- prep(rec_remove, training = df)
results <- bake(prepped, df)
# Mean should be computed ignoring NAs, then subtracted
expect_true(all(is.na(results$disp[1:3])))
expect_false(any(is.na(results$disp[4:nrow(df)])))
})
test_that("centering validates input types", {
df <- data.frame(
x = 1:5,
y = letters[1:5]
)
rec <- recipe(~ ., data = df) |>
step_center(y) # Character column
expect_error(prep(rec, training = df))
})
test_that("centering works with case weights", {
df <- mtcars[1:10, ]
df$weights <- c(rep(1, 5), rep(10, 5)) # Heavy weight on last 5
rec <- recipe(mpg ~ ., data = df) |>
step_center(disp)
# Without weights
rec_unweighted <- prep(rec, training = df)
# With weights (need to add case_weights role)
df_weighted <- df
df_weighted$weights <- hardhat::importance_weights(df_weighted$weights)
rec_weighted <- recipe(mpg ~ ., data = df_weighted) |>
update_role(weights, new_role = "case_weights") |>
step_center(disp)
rec_weighted <- prep(rec_weighted, training = df_weighted)
# Weighted and unweighted should differ
expect_false(
tidy(rec_unweighted, 1)$value[1] == tidy(rec_weighted, 1)$value[1]
)
})Reference test pattern: tests/testthat/test-center.R in recipes repository
See Testing Patterns for comprehensive testing guide.
Use for: Transform existing columns without creating new ones.
Pattern: role = NA, no keep_original_cols parameter
Complete guide: Modify-in-Place Steps
Key points:
role = NArecipes_eval_select() to resolve selectionscheck_type() in prep()Examples: center, scale, normalize, log
Reference implementations:
R/center.R, R/scale.R, R/normalize.RR/log.R, R/sqrt.R, R/logit.RR/BoxCox.R (power transformation with lambda)Use for: Generate new columns from existing ones.
Pattern: role = "predictor", keep_original_cols parameter
Complete guide: Create-New-Columns Steps
Key points:
role = "predictor"keep_original_cols parameter (default FALSE)remove_original_cols() helper in bake().recipes_estimate_sparsity() for sparse columnsExamples: dummy, pca, interact, poly
Reference implementations:
R/dummy.R (one-hot encoding)R/pca.R, R/ica.RR/interact.R, R/poly.RUse for: Filter or remove rows from data.
Pattern: skip = TRUE by default
Complete guide: Row-Operation Steps
Key points:
skip = TRUE since row ops usually only for trainingExamples: filter, sample, naomit, slice
Reference implementations:
R/filter.R, R/filter_missing.RR/sample.RR/naomit.R, R/slice.RSee Helper Functions for complete reference.
Essential helpers:
recipes_eval_select() - Convert selections to column names (prep)check_type() - Validate column types (prep)check_new_data() - Verify columns exist (bake)get_case_weights() - Extract case weights (prep)are_weights_used() - Check if weights apply (prep)remove_original_cols() - Handle keep_original_cols (bake)print_step() - Standard printing (print)sel2char() - Convert selections to strings (tidy)See Optional Methods for complete details.
Optional S3 methods:
tunable() - Declare parameters for tune packagerequired_pkgs() - Declare external package dependencies.recipes_preserve_sparsity() - Indicate sparse preservation.recipes_estimate_sparsity() - Estimate sparsity of new columnsSee Roxygen Documentation for complete templates.
Required roxygen tags:
#' @inheritParams step_center
#' @param ... One or more selector functions
#' @param role Role for new variables (or NA)
#' @param trained Logical for training status
#' @param [params] Step-specific parameters
#' @return Updated recipe object
#' @family [category] steps
#' @exportSee Testing Patterns (Extension) for comprehensive guide.
Required test categories: 1. Correctness: Step transforms data correctly
NA handling: Both na_rm = TRUE and FALSE 4. Case weights: Weighted and unweighted differ 5. Infrastructure: Works in full recipe pipeline 6. Edge cases: Empty data, all same values, etc.
If you're contributing to recipes itself, you have access to internal functions and conventions not available in extension development.
Recipes organizes steps by category:
R/center.R, R/scale.R, R/normalize.RR/dummy.R, R/novel.R, R/other.RR/pca.R, R/ica.RR/filter.R, R/sample.Rtests/testthat/test-center.RWhen developing recipes itself, you can use functions directly (no recipes:: prefix):
recipes_eval_select() - Variable selectionget_case_weights(), are_weights_used() - Case weight handlingcheck_type() - Column type validationcheck_new_data() - Verify columns exist in new dataremove_original_cols() - Handle keep_original_colsprint_step() - Standard printingsel2char() - Convert selections to stringsRecipes uses extensive parameter inheritance:
#' @inheritParams step_normalize
#' @template step-return
#' @template case-weights-unsupervised# 1. Constructor (exported)
step_center <- function(recipe, ..., role = NA, ...) {
add_step(recipe, step_center_new(...))
}
# 2. Initialization (internal)
step_center_new <- function(terms, role, trained, ...) {
step(subclass = "center", ...)
}
# 3. Methods (all exported)
prep.step_center <- function(x, training, info = NULL, ...) {
# Use internal functions directly
col_names <- recipes_eval_select(x$terms, training, info)
check_type(training[, col_names], types = c("double", "integer"))
# ...
}Complete source development guide: Source Development Guide
--------------------------------------------------------------------------------
See Best Practices for complete guide.
Key principles:
|> not magrittr pipe %>%purrr::map() for better error messagescli::cli_abort() for error messagesSee Troubleshooting (Extension) for complete guide.
Common issues:
devtools::load_all() before testingrecipes_eval_select() usagemay generate outputs that need custom metrics
have tunable parameters that can be optimized
into model specifications
data for model engines
For Extension Development (creating new packages):
Prerequisites](references/package-extension-prerequisites.md) - START HERE
For Source Development (contributing to recipes):
Access](references/package-repository-access.md)
R/center.R, R/dummy.R, R/pca.R, etc.three-function pattern
(Source)](references/testing-patterns-source.md)
PR process
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.