Skip to contents

design_spec() defines how analysis data should be turned into model-ready inputs. It records the model formula, optional row roles, and design-matrix options. It does not inspect data, expand ., encode factors, remove missing rows, or fit a statistical model.

Use the returned object with design_fit() to learn a reproducible design schema from training data. Use it with design_cv(), design_nested_cv(), design_bootstrap(), or design_resample() to build resampling plans that use the same design rules.

Usage

design_spec(
  formula,
  weights = NULL,
  offset = NULL,
  case_id = NULL,
  exclude = NULL,
  encoding = c("one_hot", "dummy"),
  intercept = FALSE,
  na_action = "na.pass",
  novel_levels = c("na", "fail")
)

Arguments

formula

(formula)
One-sided or two-sided model formula. Use a two-sided formula when the design has a response. Use a one-sided formula for predictor-only designs. . is resolved later, when the specification is fitted to data.

weights

(formula or NULL: NULL)
Optional one-sided formula for analysis weights. The right-hand side must evaluate to one numeric vector with one value per row.

offset

(formula or NULL: NULL)
Optional one-sided formula for an offset. The right-hand side must evaluate to one numeric vector with one value per row. This argument must contain a single top-level expression. Use offset() terms in formula or create one offset column if multiple components are needed.

case_id

(formula or NULL: NULL)
Optional one-sided formula for row or subject identifiers. The right-hand side must evaluate to one atomic vector with one value per row.

exclude

(formula or NULL: NULL)
Optional one-sided formula listing variables to remove from . expansion. The right-hand side may contain only bare variable names separated by +. Explicit formula terms are not removed.

encoding

(Scalar character: "one_hot")
Categorical predictor encoding used when the fitted design matrix is built. Must be one of "one_hot" or "dummy".

intercept

(Scalar logical: FALSE)
Whether to include a numeric "(Intercept)" column in fitted design matrices.

na_action

(Scalar character: "na.pass")
Missing-value policy for evaluated predictors and responses. Must be one of "na.pass", "na.omit", "na.fail", or "na.exclude".

novel_levels

(Scalar character: "na")
Handling for categorical levels in new data that were not observed during fitting. Must be one of "na" or "fail".

Value

A list of class "design_spec". It stores the unevaluated design contract and contains these elements:

  • formula

  • weights, offset, case_id, exclude

  • encoding, intercept, na_action, novel_levels

The returned object does not contain a design matrix, response vector, factor schema, retained-row index, or fitted model.

Details

Workflow

A typical workflow has three steps. First, call design_spec() to declare the statistical design. Second, call design_fit() on the analysis data to evaluate formulas, expand ., learn factor levels, and build the training design matrix. Third, use design_data() for training inputs and design_newdata() to encode validation or prediction data with the same columns and factor levels.

Keeping these steps separate makes the analysis reproducible. The specification can be reused across cross-validation folds, bootstrap samples, and final model fitting without silently changing the predictor schema.

Roles

The weights, offset, and case_id arguments declare columns or expressions with special roles. Each role may be NULL or a one-sided formula. Role formulas may not use .. Source variables used by these roles are removed from . expansion automatically and may not also be named as explicit predictors.

Response source variables may not also be used as predictors, weights, offsets, or case IDs. This prevents outcome leakage during training, assessment, and resampling. If an overlap is intentional, create a separate response column before calling design_spec().

exclude may be NULL or a one-sided formula whose right-hand side contains only bare variable names separated by +. The exclusion set is all.vars(exclude). Variables in exclude are removed from . expansion only. Explicitly named predictor terms are kept even when they also appear in exclude.

Right-hand-side offset() terms inside formula are collected as offset source variables. Variables referenced inside those calls are removed from . expansion and may not appear as explicit predictors. If formula contains offset() terms and the offset argument is also supplied, the evaluated components are summed into one numeric offset vector.

Encoding

encoding = "dummy" uses treatment contrasts. encoding = "one_hot" creates one indicator column per categorical level. intercept controls only whether fitted design matrices contain a numeric "(Intercept)" column. It is separate from any intercept option supplied to a downstream model fitter. If the formula explicitly suppresses the intercept with -1 or +0, intercept = TRUE errors.

With encoding = "dummy", categorical predictors are treatment-coded. The reference level is represented by zeros in the treatment columns. This is the usual choice for ordinary linear and generalized linear model workflows that include an intercept.

With encoding = "one_hot", categorical predictors use one column per level. The default intercept = FALSE gives a design matrix similar to glmnet::makeX() factor encoding. This is useful when a downstream fitter supplies its own intercept or when full level indicators are desired.

Some choices can produce rank-deficient design matrices. For example, encoding = "one_hot" with intercept = TRUE is rank deficient when categorical predictors are present. Additive formulas with multiple one-hot categorical predictors can also be rank deficient when intercept = FALSE. Choose encoding = "dummy" for reference-level contrasts. Choose encoding = "one_hot" for full level indicators.

Missing values

na_action is one of "na.pass", "na.omit", "na.exclude", or "na.fail". It is applied when design_fit(), design_newdata(), and design-aware resampling functions evaluate predictors and responses. "na.pass" keeps rows with missing predictor or response values. "na.omit" and "na.exclude" keep only complete rows. "na.fail" errors if any evaluated predictor or response value is missing.

Weights and offsets are validated more strictly. On every retained row, weights must be non-missing, finite, and non-negative. On every retained row, offsets must be non-missing and finite. Case IDs are metadata and do not participate in na_action.

Novel levels

novel_levels controls categorical values in new data that were not observed when the design schema was fitted. "na" converts those values to NA before encoding. "fail" errors and reports the affected variables and values.

Examples

#----------------------------------------------------------------------------
# Declare a design, fit its schema, and model the analysis data
#----------------------------------------------------------------------------
library(bkmodel)

set.seed(1L)
analysis <- data.frame(
  patient_id = sprintf("P%02d", 1:12),
  outcome = rnorm(12),
  age = round(rnorm(12, mean = 60, sd = 8)),
  treatment = factor(rep(c("control", "active"), 6)),
  stage = factor(rep(c("I", "II", "III"), each = 4)),
  exposure = runif(12, min = 0.5, max = 2),
  inverse_prob_weight = runif(12, min = 0.8, max = 1.2),
  chart_score = rnorm(12)
)

spec <- design_spec(
  outcome ~ .,
  weights = ~ inverse_prob_weight,
  offset = ~ log(exposure),
  case_id = ~ patient_id,
  exclude = ~ chart_score,
  encoding = "dummy",
  intercept = TRUE,
  na_action = "na.omit",
  novel_levels = "fail"
)

fit <- design_fit(spec, analysis)
inputs <- design_data(fit)

model <- stats::lm.wfit(
  x = inputs$x,
  y = inputs$y,
  w = inputs$weights,
  offset = inputs$offset
)
model$coefficients
#>      (Intercept)              age treatmentcontrol          stageII 
#>      0.156073030     -0.009539819     -0.017562419      0.928695544 
#>         stageIII 
#>      0.717146802 

#----------------------------------------------------------------------------
# Apply the same columns and factor levels to validation data
#----------------------------------------------------------------------------
validation <- data.frame(
  patient_id = sprintf("V%02d", 1:3),
  age = c(55, 67, 61),
  treatment = factor(c("active", "control", "active")),
  stage = factor(c("II", "I", "III")),
  exposure = c(1.1, 0.9, 1.5)
)

validation_inputs <- design_newdata(
  fit,
  validation,
  outcome = "ignore",
  weights = "ignore"
)

linear_predictor <- drop(validation_inputs$x %*% model$coefficients)
linear_predictor <- linear_predictor + validation_inputs$offset
linear_predictor
#>          1          2          3 
#>  0.6553887 -0.6060177  0.6967560