Quantitative Parameters
Creating numeric tuning parameters for continuous and integer values
This guide covers everything you need to create quantitative parameters with new_quant_param().
Note for Source Development: If contributing to dials, you can use internal validation and helper functions. See the Source Development Guide for dials-specific patterns.
Overview
Quantitative parameters represent numeric values that can vary continuously or discretely across a defined range. These are the most common type of tuning parameters in machine learning.
Reference implementations in dials:
Simple quantitative:
R/param_penalty.R(regularization penalty),R/param_learn_rate.R(learning rate)With transformations:
R/param_penalty.R(log10 transformation),R/param_sample_size.R(log2 transformation)Data-dependent:
R/param_mtry.R(finalize withget_p),R/param_num_comp.R(finalize withget_p)Integer parameters:
R/param_num_trees.R,R/param_num_terms.R
Test patterns:
Basic parameter tests:
tests/testthat/test-param_penalty.RFinalization tests:
tests/testthat/test-param_mtry.RTransformation tests:
tests/testthat/test-param_learn_rate.R
When to Use Quantitative Parameters
Use quantitative parameters when your tuning parameter:
✅ Takes numeric values (continuous or integer)
✅ Has a natural ordering (more vs less makes sense)
✅ Can be interpolated (values between bounds are meaningful)
✅ Benefits from regular spacing in grid search
Common examples:
Regularization amounts (penalty, cost, lambda)
Learning rates and decay factors
Number of features, neighbors, trees, layers
Thresholds and cutoffs
Proportions, fractions, mixtures
Degrees of freedom, polynomial degrees
When NOT to use:
❌ Categorical choices (use Qualitative Parameters)
❌ Text-based options (method names, algorithms)
❌ Unordered discrete options
Parameter Function Structure
Basic Pattern
# Extension pattern (use dials:: prefix)
my_parameter <- function(range = c(lower, upper), trans = NULL) {
dials::new_quant_param(
type = "double" or "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(my_parameter = "Display Label"),
finalize = NULL
)
}
# Source pattern (no dials:: prefix)
my_parameter <- function(range = c(lower, upper), trans = NULL) {
new_quant_param(
type = "double" or "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(my_parameter = "Display Label"),
finalize = NULL
)
}Function Arguments
Standard arguments:
range: Allow users to customize boundstrans: Allow users to change or remove transformation
Default values:
Choose sensible defaults that work for most cases
Wide ranges are better (users can narrow)
Match transformations to how parameter is used in practice
Required Arguments
type
Controls numeric precision:
type = "double" # Continuous values (floating-point)
type = "integer" # Discrete whole numbersUse “double” for:
Continuous parameters (penalties, rates, proportions)
Parameters that can take any real value
When precision matters (learning rates, tolerances)
Use “integer” for:
Count-based parameters (number of trees, neighbors, features)
Parameters that must be whole numbers
When fractional values don’t make sense
Examples:
# Double: penalty can be any value from 10^-10 to 1
new_quant_param(type = "double", range = c(-10, 0), ...)
# Integer: number of neighbors must be whole number
new_quant_param(type = "integer", range = c(1L, 15L), ...)range
Specifies parameter bounds:
range = c(lower, upper)Rules:
Must be two-element vector
lowermust be less thanupperCan include
unknown()for data-dependent boundsIf
transis provided, range is in transformed space
Fixed range examples:
range = c(0, 1) # Proportion from 0 to 1
range = c(1L, 100L) # Integer count from 1 to 100
range = c(-10, 0) # Log-scale: 10^-10 to 10^0Data-dependent range examples:
range = c(1L, unknown()) # Upper bound depends on data
range = c(0.01, unknown()) # Lower fixed, upper from dataSee Data-Dependent Parameters for unknown() details.
inclusive
Controls whether endpoints can be sampled:
inclusive = c(lower_inclusive, upper_inclusive)Options:
c(TRUE, TRUE): Both endpoints included (most common)c(FALSE, FALSE): Both endpoints excludedc(TRUE, FALSE): Lower included, upper excludedc(FALSE, TRUE): Lower excluded, upper included
Common patterns:
# Most parameters: include both endpoints
inclusive = c(TRUE, TRUE)
# Example: neighbors from 1 to 15, both valid
# Probabilities strictly between 0 and 1
inclusive = c(FALSE, FALSE)
# Example: dropout rate from 0.001 to 0.499
# Rates where zero is valid but upper bound is exclusive
inclusive = c(TRUE, FALSE)
# Example: learning rate from 0 to just under 1⚠️ Warning with integer ranges:
With inclusive = c(FALSE, FALSE) and range = c(1L, 3L), only value 2 is valid. Be careful with small integer ranges!
Optional Arguments
trans
Apply scale transformation:
trans = NULL # No transformation (default)
trans = scales::transform_log10() # Base-10 logarithm
trans = scales::transform_log() # Natural logarithm
trans = scales::transform_sqrt() # Square rootWhen to use transformations:
Parameter spans multiple orders of magnitude
Equal steps in transformed space are more meaningful
Literature commonly discusses parameter on that scale
Key point: When trans is provided, range is in transformed space
Example:
# Range in log10 space: -10 to 0
# Actual values: 10^-10 to 10^0 = 0.0000000001 to 1
penalty <- function(range = c(-10, 0), trans = scales::transform_log10()) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(penalty = "Amount of Regularization"),
finalize = NULL
)
}See Transformations for comprehensive guide.
finalize
Resolve data-dependent ranges:
finalize = NULL # No finalization (default)
finalize = dials::get_p # Built-in: set upper to # predictors
finalize = dials::get_n # Built-in: set upper to # observations
finalize = custom_fn # Custom finalize functionWhen to use:
Upper bound depends on dataset size
Number of features/observations matters
Parameter meaningfulness depends on data dimensions
Built-in finalize functions:
get_p(): Number of predictors (ncol)get_n(): Number of observations (nrow)get_n_frac(): Fraction of observationsget_log_p(): Log of predictors
See Data-Dependent Parameters for details.
label
Display name for parameter:
label = c(parameter_name = "Display Label")Conventions:
Name matches function name
Label uses title case
Concise but descriptive
Describes what parameter controls
Examples:
label = c(penalty = "Amount of Regularization")
label = c(mtry = "# Randomly Selected Predictors")
label = c(learn_rate = "Learning Rate")
label = c(num_comp = "# Principal Components")Common Patterns
Pattern 1: Simple Integer Parameter
Count-based parameters with fixed range:
# Extension pattern
num_neighbors <- function(range = c(1L, 15L), trans = NULL) {
dials::new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(num_neighbors = "# Nearest Neighbors"),
finalize = NULL
)
}
# Source pattern
num_neighbors <- function(range = c(1L, 15L), trans = NULL) {
new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(num_neighbors = "# Nearest Neighbors"),
finalize = NULL
)
}Use for: tree depth, number of layers, number of iterations
Pattern 2: Simple Double Parameter
Continuous parameters with fixed range:
# Extension pattern
threshold <- function(range = c(0, 1), trans = NULL) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(threshold = "Classification Threshold"),
finalize = NULL
)
}
# Source pattern
threshold <- function(range = c(0, 1), trans = NULL) {
new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(threshold = "Classification Threshold"),
finalize = NULL
)
}Use for: proportions, mixtures, rates (when range is narrow)
Pattern 3: Transformed Parameter (Log Scale)
Parameters spanning orders of magnitude:
# Extension pattern
penalty <- function(range = c(-10, 0), trans = scales::transform_log10()) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(penalty = "Amount of Regularization"),
finalize = NULL
)
}
# Source pattern
penalty <- function(range = c(-10, 0), trans = transform_log10()) {
new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(penalty = "Amount of Regularization"),
finalize = NULL
)
}Use for: penalties, costs, learning rates, decay factors
Pattern 4: Data-Dependent Parameter
Parameters with unknown upper bound:
# Extension pattern
mtry <- function(range = c(1L, dials::unknown()), trans = NULL) {
dials::new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(mtry = "# Randomly Selected Predictors"),
finalize = dials::get_p
)
}
# Source pattern
mtry <- function(range = c(1L, unknown()), trans = NULL) {
new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(mtry = "# Randomly Selected Predictors"),
finalize = get_p
)
}Use for: feature counts, component counts, sample sizes
Complete Examples
Example 1: Simple Integer Parameter
Number of trees in a random forest:
# Extension pattern
num_trees <- function(range = c(1L, 2000L), trans = NULL) {
dials::new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(num_trees = "# Trees"),
finalize = NULL
)
}
# Usage
num_trees()
#> # Trees (quantitative)
#> Range: [1, 2000]
# Custom range
num_trees(range = c(100L, 500L))
#> # Trees (quantitative)
#> Range: [100, 500]
# Generate grid
dials::grid_regular(num_trees(), levels = 5)
#> # A tibble: 5 × 1
#> num_trees
#> <int>
#> 1 1
#> 2 500
#> 3 1000
#> 4 1500
#> 5 2000Example 2: Simple Double Parameter
Mixture proportion for elastic net:
# Extension pattern
mixture <- function(range = c(0, 1), trans = NULL) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(mixture = "Proportion of Lasso Penalty"),
finalize = NULL
)
}
# Usage
mixture()
#> Proportion of Lasso Penalty (quantitative)
#> Range: [0, 1]
# Sample values
set.seed(123)
dials::value_sample(mixture(), n = 5)
#> [1] 0.287 0.788 0.409 0.883 0.940
# Generate sequence
dials::value_seq(mixture(), n = 5)
#> [1] 0.00 0.25 0.50 0.75 1.00Example 3: Transformed Parameter
Learning rate on log scale:
# Extension pattern
learn_rate <- function(range = c(-5, -1), trans = scales::transform_log10()) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(learn_rate = "Learning Rate"),
finalize = NULL
)
}
# Usage
learn_rate()
#> Learning Rate (quantitative)
#> Transformer: log-10
#> Range (transformed scale): [-5, -1]
# Actual values: 10^-5 to 10^-1
learn_rate_param <- learn_rate()
# Regular grid in transformed space
grid <- dials::grid_regular(learn_rate_param, levels = 5)
grid
#> # A tibble: 5 × 1
#> learn_rate
#> <dbl>
#> 1 0.00001 # 10^-5
#> 2 0.0001 # 10^-4
#> 3 0.001 # 10^-3
#> 4 0.01 # 10^-2
#> 5 0.1 # 10^-1
# Even spacing on log scale!Example 4: Data-Dependent Parameter
Maximum number of features to select:
# Extension pattern
max_features <- function(range = c(1L, dials::unknown()), trans = NULL) {
dials::new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(max_features = "# Maximum Features"),
finalize = dials::get_p
)
}
# Usage
max_features()
#> # Maximum Features (quantitative)
#> Range: [1, ?]
# Finalize with data
param <- max_features()
finalized <- dials::finalize(param, mtcars[, -1]) # 10 predictors
finalized
#> # Maximum Features (quantitative)
#> Range: [1, 10]
# Now can generate grid
dials::grid_regular(finalized, levels = 5)
#> # A tibble: 5 × 1
#> max_features
#> <int>
#> 1 1
#> 2 3
#> 3 5
#> 4 8
#> 5 10Example 5: Custom Finalize Function
Number of initial MARS terms:
# Extension pattern
num_initial_terms <- function(range = c(1L, dials::unknown()), trans = NULL) {
dials::new_quant_param(
type = "integer",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(num_initial_terms = "# Initial MARS Terms"),
finalize = get_initial_mars_terms
)
}
# Custom finalize function
get_initial_mars_terms <- function(object, x) {
# Earth package formula: min(200, max(20, 2 * ncol(x))) + 1
upper_bound <- min(200, max(20, 2 * ncol(x))) + 1
upper_bound <- as.integer(upper_bound)
# Update range
bounds <- dials::range_get(object)
bounds$upper <- upper_bound
dials::range_set(object, bounds)
}
# Usage
num_initial_terms()
#> # Initial MARS Terms (quantitative)
#> Range: [1, ?]
# Finalize with small dataset (10 predictors)
param <- num_initial_terms()
finalized <- dials::finalize(param, mtcars[, -1])
finalized
#> # Initial MARS Terms (quantitative)
#> Range: [1, 41] # min(200, max(20, 2*10)) + 1 = 41
# Finalize with large dataset (100 predictors)
large_data <- matrix(rnorm(100 * 100), ncol = 100)
finalized_large <- dials::finalize(param, large_data)
finalized_large
#> # Initial MARS Terms (quantitative)
#> Range: [1, 201] # min(200, max(20, 2*100)) + 1 = 201Extension vs Source Patterns
Extension Development
Always use dials:: prefix:
my_param <- function(range = c(0, 1), trans = NULL) {
dials::new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(my_param = "My Parameter"),
finalize = NULL
)
}
# Use dials:: for all functions
dials::unknown()
dials::get_p
dials::range_get()
dials::range_set()
scales::transform_log10() # scales is separate packageSee Extension Development Guide for complete guide.
Source Development
No dials:: prefix needed:
my_param <- function(range = c(0, 1), trans = NULL) {
new_quant_param(
type = "double",
range = range,
inclusive = c(TRUE, TRUE),
trans = trans,
label = c(my_param = "My Parameter"),
finalize = NULL
)
}
# Direct function calls
unknown()
get_p
range_get()
range_set()
transform_log10() # Available in dials namespaceSee Source Development Guide for complete guide.
Testing Considerations
Essential Tests
All quantitative parameters should test:
- Parameter creation: Valid object structure
- Custom ranges: Accepts user-provided bounds
- Type enforcement: Correct numeric type
- Grid integration: Works with
grid_regular(),grid_random() - Value utilities:
value_sample()andvalue_seq()work - Range validation: Rejects invalid ranges
- Transformation: If applicable, transformed values are correct
- Finalization: If applicable, data-dependent bounds resolve
Example Test Suite
# tests/testthat/test-my-parameter.R
test_that("my_parameter creates valid parameter", {
param <- my_parameter()
expect_s3_class(param, "quant_param")
expect_equal(param$type, "double")
expect_equal(param$range$lower, 0)
expect_equal(param$range$upper, 1)
})
test_that("my_parameter accepts custom range", {
param <- my_parameter(range = c(0.2, 0.8))
expect_equal(param$range$lower, 0.2)
expect_equal(param$range$upper, 0.8)
})
test_that("my_parameter works with grid_regular", {
param <- my_parameter()
grid <- dials::grid_regular(param, levels = 5)
expect_equal(nrow(grid), 5)
expect_true(all(grid$my_parameter >= 0))
expect_true(all(grid$my_parameter <= 1))
})
test_that("my_parameter works with grid_random", {
set.seed(123)
param <- my_parameter()
grid <- dials::grid_random(param, size = 10)
expect_equal(nrow(grid), 10)
expect_true(all(grid$my_parameter >= 0))
expect_true(all(grid$my_parameter <= 1))
})
test_that("my_parameter works with value utilities", {
param <- my_parameter()
# value_sample
set.seed(456)
samples <- dials::value_sample(param, n = 5)
expect_length(samples, 5)
expect_true(all(samples >= 0 & samples <= 1))
# value_seq
seq_vals <- dials::value_seq(param, n = 5)
expect_length(seq_vals, 5)
expect_true(all(seq_vals >= 0 & seq_vals <= 1))
})
test_that("my_parameter rejects invalid ranges", {
expect_error(my_parameter(range = c(1, 0))) # lower > upper
expect_error(my_parameter(range = c(0, NA))) # NA value
expect_error(my_parameter(range = 0)) # length != 2
})For extension development, see Testing Requirements.
For source development, see Testing Patterns (Source).
Next Steps
Learn Advanced Features
Transformations: Transformations Guide
Data-dependent ranges: Data-Dependent Parameters Guide
Grid integration: Grid Integration Guide
Implementation Guides
Extension development: Extension Development Guide
Source development: Source Development Guide
Last Updated: 2026-03-31