Qualitative Parameters
Creating categorical tuning parameters with discrete options
This guide covers everything you need to create qualitative parameters with new_qual_param().
When to Use Qualitative Parameters
Use qualitative parameters when your tuning parameter:
- ✅ Represents discrete categorical choices
- ✅ Has no natural ordering (options are not “more” or “less”)
- ✅ Uses non-numeric values or symbolic names
- ✅ Consists of fundamentally different options
Common examples:
- Activation functions (relu, sigmoid, tanh, softmax)
- Optimization algorithms (adam, sgd, rmsprop, adagrad)
- Distance metrics (euclidean, manhattan, cosine)
- Aggregation methods (mean, median, min, max, sum)
- Model modes or variants (classification, regression)
- Loss functions (cross-entropy, mse, hinge)
When NOT to use:
- ❌ Numeric values with ordering (use Quantitative Parameters)
- ❌ Counts or continuous values
- ❌ Parameters where interpolation makes sense
Parameter Function Structure
Basic Pattern
# Extension pattern (use dials:: prefix)
my_parameter <- function(values = values_my_parameter) {
dials::new_qual_param(
type = "character",
values = values,
default = "default_value", # Optional
label = c(my_parameter = "Display Label")
)
}
#' @rdname my_parameter
#' @export
values_my_parameter <- c("option1", "option2", "option3")
# Source pattern (no dials:: prefix)
my_parameter <- function(values = values_my_parameter) {
new_qual_param(
type = "character",
values = values,
default = "default_value", # Optional
label = c(my_parameter = "Display Label")
)
}
#' @rdname my_parameter
#' @export
values_my_parameter <- c("option1", "option2", "option3")Function Arguments
Standard arguments:
values: Allow users to customize available options
Default values:
- Reference companion
values_*vector - Let users subset or replace options
Required Arguments
type
Controls data type of options:
type = "character" # Text-based options (most common)
type = "logical" # TRUE/FALSE options (rare)Use “character” for:
- Method names (activation functions, optimizers)
- Algorithm choices (distance metrics, aggregation methods)
- Most categorical parameters
Use “logical” for:
- Binary flags
- TRUE/FALSE settings
- Very rare in practice (usually use character with two values instead)
Examples:
# Character: activation functions
new_qual_param(
type = "character",
values = c("relu", "sigmoid", "tanh"),
...
)
# Logical: binary flag (rare)
new_qual_param(
type = "logical",
values = c(TRUE, FALSE),
...
)values
Vector of all possible options:
values = c("option1", "option2", "option3")Rules:
- Must be non-empty
- Type must match
typeargument - All values must be unique
- Order matters (first value is default if
defaultnot specified)
Best practice: Create companion values_* vector:
#' @rdname my_parameter
#' @export
values_my_parameter <- c("option1", "option2", "option3")
my_parameter <- function(values = values_my_parameter) {
dials::new_qual_param(
type = "character",
values = values,
label = c(my_parameter = "My Parameter")
)
}This pattern: - ✅ Documents options clearly - ✅ Allows users to see available values - ✅ Enables subsetting: my_parameter(values = values_my_parameter[1:2]) - ✅ Follows dials package conventions
Optional Arguments
default
Specify default value:
default = "specific_value" # Explicit default
default = NULL # Use first value in `values` (default)Behavior:
- If
default = NULL(the default), first value invaluesis used - If explicit default provided, must be in
values
Examples:
# Explicit default
aggregation <- function(values = values_aggregation) {
dials::new_qual_param(
type = "character",
values = values,
default = "mean", # Explicit
label = c(aggregation = "Aggregation Method")
)
}
values_aggregation <- c("mean", "median", "min", "max")
# Implicit default (first value)
aggregation <- function(values = values_aggregation) {
dials::new_qual_param(
type = "character",
values = values,
# default = "mean" implicitly
label = c(aggregation = "Aggregation Method")
)
}
values_aggregation <- c("mean", "median", "min", "max")When to use explicit default:
- Default is not the first value alphabetically
- Want to emphasize recommended option
- Documentation clarity
When to use implicit default:
- First value in
valuesis the desired default - Simpler code
- Most common pattern in dials
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(activation = "Activation Function")
label = c(optimizer = "Optimization Algorithm")
label = c(weight_func = "Distance Weighting Function")
label = c(aggregation = "Aggregation Method")finalize
Rarely used for qualitative parameters:
finalize = NULL # Almost always NULL for categoricalQualitative parameters typically don’t need finalization because options don’t depend on data characteristics. The rare exception might be a parameter where available options depend on data properties, but this is uncommon.
Creating Companion values_* Vectors
Pattern and Convention
Strong recommendation: Always create a values_* vector alongside your parameter function.
#' Activation function
#'
#' The activation function for neural networks.
#'
#' @param values A character vector of possible activation functions.
#'
#' @details
#' This parameter defines the activation function between layers.
#'
#' @examples
#' values_activation
#' activation()
#' activation(values = c("relu", "sigmoid"))
#'
#' @export
activation <- function(values = values_activation) {
dials::new_qual_param(
type = "character",
values = values,
label = c(activation = "Activation Function")
)
}
#' @rdname activation
#' @export
values_activation <- c(
"relu", "sigmoid", "tanh", "softmax",
"elu", "selu", "softplus", "softsign"
)Why Use values_* Vectors?
For users:
# See available options
values_activation
#> [1] "relu" "sigmoid" "tanh" "softmax"
#> [5] "elu" "selu" "softplus" "softsign"
# Use default (all options)
activation()
# Subset to specific options
activation(values = c("relu", "tanh"))
# Use first few options
activation(values = values_activation[1:4])For package maintainers:
- Single source of truth for options
- Easy to update available values
- Clear documentation
- Consistent with dials conventions
Naming Convention
- Vector name:
values_[parameter_name] - If parameter is
activation(), vector isvalues_activation - If parameter is
weight_func(), vector isvalues_weight_func - Export both with
@exporttag - Link with
@rdnamefor shared documentation
Common Patterns
Pattern 1: Character Parameter with Values Vector
Most common pattern for categorical parameters:
# Extension pattern
optimizer <- function(values = values_optimizer) {
dials::new_qual_param(
type = "character",
values = values,
label = c(optimizer = "Optimization Algorithm")
)
}
#' @rdname optimizer
#' @export
values_optimizer <- c("adam", "sgd", "rmsprop", "adagrad")
# Source pattern
optimizer <- function(values = values_optimizer) {
new_qual_param(
type = "character",
values = values,
label = c(optimizer = "Optimization Algorithm")
)
}
#' @rdname optimizer
#' @export
values_optimizer <- c("adam", "sgd", "rmsprop", "adagrad")Pattern 2: Character Parameter with Explicit Default
When default is not first value:
# Extension pattern
aggregation <- function(values = values_aggregation) {
dials::new_qual_param(
type = "character",
values = values,
default = "none", # Explicit default
label = c(aggregation = "Aggregation Method")
)
}
#' @rdname aggregation
#' @export
values_aggregation <- c("none", "min", "max", "mean", "sum")Pattern 3: Logical Parameter
Rare, but occasionally useful:
# Extension pattern
use_weights <- function(values = c(TRUE, FALSE)) {
dials::new_qual_param(
type = "logical",
values = values,
label = c(use_weights = "Use Case Weights")
)
}
# Source pattern
use_weights <- function(values = c(TRUE, FALSE)) {
new_qual_param(
type = "logical",
values = values,
label = c(use_weights = "Use Case Weights")
)
}Note: Usually better to use character with two values instead:
weighting <- function(values = c("weighted", "unweighted")) {
dials::new_qual_param(
type = "character",
values = values,
label = c(weighting = "Weighting Method")
)
}Complete Examples
Example 1: Basic Character Parameter
Distance weighting function:
# Extension pattern
weight_func <- function(values = values_weight_func) {
dials::new_qual_param(
type = "character",
values = values,
label = c(weight_func = "Distance Weighting Function")
)
}
#' @rdname weight_func
#' @export
values_weight_func <- c(
"rectangular", "triangular", "epanechnikov",
"biweight", "triweight", "cosine",
"gaussian", "rank"
)
# Usage
weight_func()
#> Distance Weighting Function (qualitative)
#> 8 possible values include:
#> 'rectangular', 'triangular', 'epanechnikov', 'biweight', 'triweight' and 2 more
# See all options
values_weight_func
#> [1] "rectangular" "triangular" "epanechnikov" "biweight"
#> [5] "triweight" "cosine" "gaussian" "rank"
# Use subset
weight_func(values = c("rectangular", "gaussian", "rank"))
# Sample values
set.seed(123)
dials::value_sample(weight_func(), n = 3)
#> [1] "biweight" "gaussian" "triangular"Example 2: Character Parameter with Explicit Default
Aggregation method with “none” as default:
# Extension pattern
aggregation <- function(values = values_aggregation) {
dials::new_qual_param(
type = "character",
values = values,
default = "none",
label = c(aggregation = "Aggregation Method")
)
}
#' @rdname aggregation
#' @export
values_aggregation <- c("none", "min", "max", "mean", "sum")
# Usage
aggregation()
#> Aggregation Method (qualitative)
#> 5 possible values include:
#> 'none', 'min', 'max', 'mean' and 'sum'
# Generate grid with all values
grid <- dials::grid_regular(aggregation())
grid
#> # A tibble: 5 × 1
#> aggregation
#> <chr>
#> 1 none
#> 2 min
#> 3 max
#> 4 mean
#> 5 sum
# Generate random grid (sampling with replacement)
set.seed(456)
grid <- dials::grid_random(aggregation(), size = 10)
grid
#> # A tibble: 10 × 1
#> aggregation
#> <chr>
#> 1 max
#> 2 none
#> 3 sum
#> 4 mean
#> # ... with 6 more rowsExample 3: Optimization Algorithm
Common ML parameter:
# Extension pattern
optimizer <- function(values = values_optimizer) {
dials::new_qual_param(
type = "character",
values = values,
label = c(optimizer = "Optimization Algorithm")
)
}
#' @rdname optimizer
#' @export
values_optimizer <- c("adam", "sgd", "rmsprop", "adagrad")
# Usage
optimizer()
#> Optimization Algorithm (qualitative)
#> 4 possible values include:
#> 'adam', 'sgd', 'rmsprop' and 'adagrad'
# Custom subset
optimizer(values = c("adam", "sgd"))
#> Optimization Algorithm (qualitative)
#> 2 possible values include:
#> 'adam' and 'sgd'
# Use in grid
params <- dials::parameters(
learn_rate = dials::learn_rate(),
optimizer = optimizer()
)
grid <- dials::grid_regular(params, levels = c(3, 4))
grid
#> # A tibble: 12 × 2
#> learn_rate optimizer
#> <dbl> <chr>
#> 1 0.0000001 adam
#> 2 0.0000001 sgd
#> 3 0.0000001 rmsprop
#> 4 0.0000001 adagrad
#> 5 0.001 adam
#> # ... with 7 more rowsExtension vs Source Patterns
Extension Development
Always use dials:: prefix:
my_param <- function(values = values_my_param) {
dials::new_qual_param(
type = "character",
values = values,
label = c(my_param = "My Parameter")
)
}
#' @rdname my_param
#' @export
values_my_param <- c("option1", "option2", "option3")
# Use dials:: for grid functions
dials::grid_regular(my_param())
dials::value_sample(my_param(), n = 3)See Extension Development Guide for complete guide.
Source Development
No dials:: prefix needed:
my_param <- function(values = values_my_param) {
new_qual_param(
type = "character",
values = values,
label = c(my_param = "My Parameter")
)
}
#' @rdname my_param
#' @export
values_my_param <- c("option1", "option2", "option3")
# Direct function calls
grid_regular(my_param())
value_sample(my_param(), n = 3)See Source Development Guide for complete guide.
Testing Considerations
Essential Tests
All qualitative parameters should test:
- Parameter creation: Valid object structure
- Custom values: Accepts user-provided options
- Type enforcement: Correct value types
- Grid integration: Works with
grid_regular(),grid_random() - Value utilities:
value_sample()works - Default value: Correct default (explicit or implicit)
- Values validation: Rejects empty or invalid values
Example Test Suite
# tests/testthat/test-my-parameter.R
test_that("my_parameter creates valid parameter", {
param <- my_parameter()
expect_s3_class(param, "qual_param")
expect_equal(param$type, "character")
expect_equal(param$values, values_my_parameter)
})
test_that("my_parameter accepts custom values", {
custom_values <- c("option1", "option2")
param <- my_parameter(values = custom_values)
expect_equal(param$values, custom_values)
})
test_that("my_parameter has correct default", {
param <- my_parameter()
# If explicit default set
expect_equal(param$default, "option1")
# If implicit (first value)
expect_equal(param$default, values_my_parameter[1])
})
test_that("my_parameter works with grid_regular", {
param <- my_parameter()
grid <- dials::grid_regular(param)
expect_equal(nrow(grid), length(values_my_parameter))
expect_true(all(grid$my_parameter %in% values_my_parameter))
})
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 %in% values_my_parameter))
})
test_that("my_parameter works with value_sample", {
set.seed(456)
param <- my_parameter()
samples <- dials::value_sample(param, n = 5)
expect_length(samples, 5)
expect_true(all(samples %in% values_my_parameter))
})
test_that("my_parameter rejects invalid values", {
expect_error(my_parameter(values = character(0))) # Empty
expect_error(my_parameter(values = NULL)) # NULL
expect_error(my_parameter(values = c(1, 2, 3))) # Wrong type
})
test_that("values_my_parameter is exported and correct", {
expect_true("values_my_parameter" %in% ls("package:mypackage"))
expect_type(values_my_parameter, "character")
expect_true(length(values_my_parameter) > 0)
})For extension development, see Testing Requirements.
For source development, see Testing Patterns (Source).
Next Steps
Implementation Guides
- Extension development: Extension Development Guide
- Source development: Source Development Guide
Last Updated: 2026-03-31