Skip to contents

vine() separates marginal modeling from copula modeling. Each fitted margin provides a density or probability mass function, a distribution function, and a quantile function. rvinecopulib uses those functions to move between the data scale and the copula scale.

This vignette covers the default nonparametric margins, optional parametric selection, custom families, discrete and zero-inflated variables, and the protocol that other packages can implement.

Default nonparametric margins

The default is a kde1d fit for every variable.

x <- data.frame(
  first = rnorm(60),
  second = rgamma(60, shape = 2)
)
fit_kde <- vine(
  x,
  copula_controls = list(family_set = "indep")
)
summary(fit_kde)$margins
#> # A data.frame: 2 x 8 
#>  margin   name family type xmin xmax npars loglik
#>       1  first  kde1d    c -Inf  Inf   3.5    -73
#>       2 second  kde1d    c -Inf  Inf   5.0    -88

KDE controls belong to the family specification. This keeps vine() free of method-specific options:

fit_bounded <- vine(
  transform(x, second = pmin(second, 10)),
  margins_controls = list(
    family_set = list(
      kde1d_family(mult = 1.5),
      kde1d_family(xmin = 0, xmax = 10, deg = 1)
    )
  ),
  copula_controls = list(family_set = "indep")
)

Observation weights

The weights argument to vine() is used for both the margins and copula. kde1d supports weights directly. Every margin-family fitter receives x, weights, and type; it must use the weights or reject them explicitly:

weighted_normal <- margin_family(
  fit = function(x, weights, type) {
    location <- weighted.mean(x, weights)
    scale <- sqrt(weighted.mean((x - location)^2, weights))
    margin_dist(
      d = function(y) dnorm(y, location, scale),
      p = function(y) pnorm(y, location, scale),
      q = function(p) qnorm(p, location, scale),
      family = "weighted-normal",
      type = type,
      npars = 2,
      loglik = sum(weights * dnorm(x, location, scale, log = TRUE))
    )
  },
  family_name = "weighted-normal"
)
fit_weighted <- vine(x, margins_controls = list(family_set = weighted_normal),
                     weights = runif(nrow(x)))

When no weights are supplied, weights is numeric(). The built-in univariateML family rejects non-empty weights. A failed candidate does not stop selection if another candidate succeeds, but the failure is reported.

On non-Windows systems, margins are fitted in forked processes when several cores are requested. Stochastic custom fitters may then depend on the number of processes; set margins_controls = list(cores = 1) when results must remain invariant to the core count. Margin fitting is always serial on Windows.

Parametric selection with univariateML

The suggested univariateML package supplies named parametric families. A character vector is a common candidate set for every variable; rvinecopulib fits every compatible candidate and performs the selection itself.

fit_parametric <- vine(
  x,
  margins_controls = list(
    family_set = c("norm", "cauchy", "gamma"),
    selcrit = "bic"
  ),
  copula_controls = list(family_set = "indep")
)
#> Loading required namespace: intervals
#> Warning: margin selection for variable 1 reported problems (gamma failed: x not
#> in the support of the data).
summary(fit_parametric)$margins
#> # A data.frame: 2 x 8 
#>  margin   name family type xmin xmax npars loglik
#>       1  first   norm    c -Inf  Inf     2    -74
#>       2 second  gamma    c    0  Inf     2    -89

Available criteria are "loglik", "aic", and "bic". The aliases "parametric" and "par" expand to all univariateML families, while "all" also includes "kde1d". These names require univariateML; custom families and the default KDE fit do not.

Use a list to specify candidates separately by variable:

fit_mixed <- vine(
  data.frame(amount = rexp(100), count = rpois(100, 3)),
  var_types = c("c", "d"),
  margins_controls = list(
    family_set = list(
      amount = c("exp", "gamma", "weibull"),
      count = c("pois", "nbinom")
    )
  )
)

A named list must contain every variable name exactly once. An unnamed list is matched by position.

Custom fitted families

margin_family() wraps a fitting function with the canonical x, weights, and type interface. It returns a fitted object implementing the fitted-margin protocol. margin_dist() is the simplest way to construct such an object.

normal_family <- margin_family(
  fit = function(x, weights, type) {
    location <- mean(x)
    scale <- sqrt(mean((x - location)^2))
    margin_dist(
      d = function(y) dnorm(y, location, scale),
      p = function(y) pnorm(y, location, scale),
      q = function(p) qnorm(p, location, scale),
      family = "custom_normal",
      type = type,
      npars = 2,
      loglik = sum(dnorm(x, location, scale, log = TRUE))
    )
  },
  family_name = "custom_normal",
  types = "c"
)

fit_custom <- vine(
  x,
  margins_controls = list(family_set = normal_family),
  copula_controls = list(family_set = "indep")
)
summary(fit_custom)$margins
#> # A data.frame: 2 x 8 
#>  margin   name        family type xmin xmax npars loglik
#>       1  first custom_normal    c -Inf  Inf     2    -74
#>       2 second custom_normal    c -Inf  Inf     2    -99

The fitting callback can capture additional settings in its environment or receive them through the fit_args argument to margin_family(). When several candidates compete, each fitted object’s margin_info() result needs a finite loglik entry; AIC and BIC additionally require a finite npars entry. If one candidate fails, selection continues with the remaining fits; if all fail, vine() reports the collected errors. A sole candidate without a finite parameter count is retained with a warning, but model AIC and BIC are then unavailable.

To combine named and custom candidates separately by variable, use nested lists:

margins_controls <- list(
  family_set = list(
    list("norm", normal_family),
    list("gamma", "weibull")
  ),
  selcrit = "aic"
)

The fitted-margin protocol

Packages can integrate their own fitted classes by implementing four S3 methods. The dmargin(), pmargin(), and qmargin() generics dispatch on their second argument, margin; margin_info() provides model metadata.

dmargin.my_margin <- function(x, margin) { ... }
pmargin.my_margin <- function(x, margin) { ... }
qmargin.my_margin <- function(p, margin) { ... }
margin_info.my_margin <- function(object) {
  list(
    family_name = "my-family",
    type = "c",
    support = c(-Inf, Inf),
    npars = object$npars,
    loglik = object$loglik
  )
}

The metadata methods are part of the protocol rather than attributes that the core code interprets. See ?margin_protocol for the complete return-value and left-limit contract.

A package can likewise implement the margin-family protocol directly with fit_margin() and margin_info() methods. The core selection path treats such families like any other implementation.

Integer-valued discrete variables

All discrete margins use integer support. Declare an ordinary numeric column with var_types = "d", or use an ordered column. Ordered factors are fitted internally using the integer codes 0, 1, ...; simulations restore the original ordered levels.

ordered_data <- data.frame(
  rating = ordered(
    sample(c("low", "middle", "high"), 80, replace = TRUE),
    levels = c("low", "middle", "high")
  ),
  value = rnorm(80)
)
fit_ordered <- vine(
  ordered_data,
  copula_controls = list(family_set = "indep")
)
str(rvine(4, fit_ordered))
#> 'data.frame':    4 obs. of  2 variables:
#>  $ rating: Ord.factor w/ 3 levels "low"<"middle"<..: 3 3 1 3
#>  $ value : num  1.298 -0.3304 -0.0999 0.3728

For integer-supported margins, rvinecopulib computes the left-limit CDF as F(x - 1).

Continuous variables with an atom at zero

zero_inflated() marks a numeric vector as continuous away from zero with an atom at zero. The marker survives data-frame storage and subsetting. The same type can be declared explicitly with var_types = "zi".

zero_data <- data.frame(
  claim = zero_inflated(c(rep(0, 20), rexp(60))),
  score = rnorm(80)
)
inherits(zero_data$claim, "zero_inflated")
#> [1] TRUE

A custom zero-inflated family declares types = "zi"; its fitted margin returns type = "zi" from margin_info(). At zero, dmargin() must return the atom probability. rvinecopulib then computes the left limit as F(0) - f(0); away from zero the left limit equals the ordinary CDF.

Fixed margins and persistence

stats_margin() adapts fixed stats distributions, including norm, lnorm, gamma, and weibull, to the fitted-margin protocol. Legacy list(distr = ...) specifications remain accepted by vine_dist(). These margins retain their distribution parameter counts because the supplied parameters may have been estimated before constructing the vine distribution.

fixed_model <- vine_dist(
  margins = list(
    stats_margin("norm", mean = 0, sd = 1),
    stats_margin("lnorm", meanlog = 0, sdlog = 0.5)
  ),
  pair_copulas = list(list(bicop_dist())),
  structure = dvine_structure(1:2)
)
rvine(3, fixed_model)
#>            [,1]      [,2]
#> [1,] -0.4671167 1.8678497
#> [2,]  1.5498242 1.4452552
#> [3,]  0.3289217 0.5503876

Fitted margins, including callback-based margin_dist() objects, are stored in the vine model. Standard R serialization therefore preserves the complete model:

path <- tempfile(fileext = ".rds")
saveRDS(fit_custom, path)
restored <- readRDS(path)
unlink(path)
all.equal(dvine(x, restored), dvine(x, fit_custom))
#> [1] TRUE