Nominal conditional synthesis (experimental)

The SynthesizerPlus copula uses a single latent coordinate per categorical variable. This is natural for ordinal responses. For unordered categories the copula places them on that coordinate in an order estimated from the data (by their association with the other columns), not in the order of their labels, but one coordinate cannot represent every pattern of association a nominal variable can have. Since version 0.4.0, nominal_target instead fits a conditional multinomial logistic model of one unordered response. The other columns are generated using the usual fitted copula, then the response is drawn from predicted category probabilities. The model is

\[P(Y = k \mid x) = \frac{\exp\{\eta_k(x)\}}{\sum_l \exp\{\eta_l(x)\}}, \qquad \eta_k(x) = \beta_{k0} + \sum_j f_{kj}(x_j),\]

with \(\eta\) of the first category fixed at zero. For each numeric predictor with at least five distinct values, \(f_{kj}\) is a natural cubic spline with nominal_spline_df degrees of freedom (default 3, knots at quantiles of the training values), so the category probabilities can depend nonlinearly and non-monotonically on it; other predictors enter linearly or as category dummies, with a missingness indicator for numeric predictors. The model is fitted by nnet::multinom() with weight decay nominal_decay. The purely linear logit is fitted as well, and the model with the smaller AIC is used; the comparison is stored in fit$pooled$nominal$selection and the choice in fit$pooled$nominal$spline_df. Splines are thus used when the extra parameters are supported by the data; with (nearly) separated classes, as in iris, the linear logit is better determined and is kept. Setting nominal_spline_df = 0 fits only the linear logit.

library(SynthesizerPlus)
set.seed(204)
input <- data.frame(
  age = rnorm(500, 35, 10),
  income = rgamma(500, shape = 4, scale = 1000)
)
logits <- cbind(0, (input$age - 35)/15, log(input$income/4000))
p <- exp(logits - apply(logits, 1L, max))
p <- p/rowSums(p)
classes <- c("bus", "car", "rail")
input$transport <- factor(classes[apply(p, 1, function(q)
  sample.int(3, 1, prob = q))])

fit <- fit_synthesizer(input, nominal_target = "transport", seed = 11)
synthetic <- generate(fit, n = 500, seed = 12)
prop.table(table(synthetic$transport))
#> 
#>   bus   car  rail 
#> 0.344 0.346 0.310
fit$pooled$nominal$selection   # AIC of the spline and linear models
#>   spline_df converged      aic
#> 1         3      TRUE 1034.474
#> 2         0      TRUE 1033.022

Nominal category labels have no ordinal interpretation. Reordering or renaming those labels represents the same statistical outcome categories; the multinomial model is approximately equivariant to such relabelling (up to numerical optimisation and Monte Carlo variability). Reproducibility of the factor-level metadata is preserved by the generated table.

At least 40 observed categorical responses and two to twenty distinct categories are required. Sparse strata fall back to the conventional copula model, including the nominal target; fit_synthesizer() warns when this happens and lists the strata in fit$strata$conditional_fallback. The multinomial model has additive spline effects of the numeric predictors and no interactions, so class boundaries that depend on interactions between predictors remain a limitation. Fitted model objects include information about the training data and should not be publicly released as if privacy-safe.