library(distr6)
set.seed(42)
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")

DistributionWrapper Class

Wrappers may the hardest class to extend in distr6, simply due to the fact that there are no set rules for what can and can't go in a wrapper. Every implemented wrapper inherits from the DistributionWrapper class which in turn inherits from Distribution (see the uml diagram). The DistributionWrapper class introduces its own constructor, the wrappedModels method and overloads the setParameterValue method. We will briefly discuss why these three methods are important to all implemented methods.

The Constructor

The DistributionWrapper constructor takes one named argument and has ... for named arguments to passed to the Distribution constructor. The named argument is distlist, a list of distributions to wrap. Internally the constructor combines all ParameterSet from every distribution into one ParameterSet that the user can see and query.

For example, say we use the MixtureDistribution wrapper on two Binomial distributions, see how the parameter names are automatically updated

M <- MixtureDistribution$new(list(Binomial$new(),Binomial$new()))
M$parameters()

setParameterValue

As the constructor may change the names of parameters, the setParameterValue is overloaded to ensure the correct ParameterSet from the corresponding distribution is updated. So given the example above, if a user wants to update the first Binomial distribution

M$setParameterValue(Binom1__prob = 0.2)

And internally the parameter name is split at the underscore and updates the parameter of the first Binomial distribution, notice now that both the external representation of the combined ParameterSet is updated as well as the one in the internal model

M$parameters()
M$wrappedModels("Binom1")$parameters()

wrappedModels

We saw above the use of wrappedModels, this is a simple accessor found in all implemented wrappers that ensure the underlying models can be accessed, which is usually required for the new methods in wrappers, which we will see more below.

Creating a Wrapper

Now we have a background of the DistributionWrapper class we can discuss actually creating a wrapper. As discussed in the wrappers tutorial, there are multiple types of wrappers, each will be implemented slightly differently. Therefore we're just going to look at one quick example from each example and point you to the source code for further examples.

TruncatedDistribution

An implemented wrapper should consist of two parts: the wrapper definition and the wrapper constructor. In the case of the TruncatedDistribution, a slightly abridged version looks like

TruncatedDistribution <- R6::R6Class("TruncatedDistribution", inherit = DistributionWrapper, lock_objects = FALSE)

TruncatedDistribution$set("public","initialize",function(distribution, lower = NULL,
                                                         upper = NULL){

  pdf <- function(x1,...) {
    self$wrappedModels()[[1]]$pdf(x1) / (self$wrappedModels()[[1]]$cdf(self$sup()) - self$wrappedModels()[[1]]$cdf(self$inf()))
  }

  cdf <- function(x1,...){
    num = self$wrappedModels()[[1]]$cdf(x1) - self$wrappedModels()[[1]]$cdf(self$inf())
    den = self$wrappedModels()[[1]]$cdf(self$sup()) - self$wrappedModels()[[1]]$cdf(self$inf())
    return(num/den)
  }

  name = paste("Truncated",distribution$name)
  short_name = paste0("Truncated",distribution$short_name)

  distlist = list(distribution)
  names(distlist) = distribution$short_name

  description = paste0(distribution$description, " Truncated between ",lower," and ",upper,".")

  super$initialize(distlist = distlist, pdf = pdf, cdf = cdf,
                   name = name, short_name = short_name, support = support,
                   type = distribution$type(),
                   description = description)
})

All wrappers should pass the following to the parent-class constructor:

  1. Name - New name after wrapping
  2. short_name - New short_name after wrapping
  3. distlist - The named list of distributions (even if just one)
  4. description - Optional, updated description after wrapping

And then any other arguments that may be changed by wrapping, remember that any arguments passed to the DistributionWrapper constructor are in turn passed to the Distribution constructor, hence we can think of implemented wrappers as custom distributions. Read the custom distribution tutorial for more information about the arguments passed to the Distribution constructor.

Notice also that the new pdf and cdf methods reference the original model using the wrappedModels method. This will generally be the case with all wrappers.

MixtureDistribution

The MixtureDistribution wrapper is slightly different in that it takes a composition of multiple distributions and it adds a private variable

MixtureDistribution <- R6::R6Class("MixtureDistribution", inherit = DistributionWrapper, lock_objects = FALSE)
MixtureDistribution$set("public","initialize",function(distlist, weights = NULL){

  distlist = makeUniqueDistributions(distlist)
  distnames = names(distlist)

  private$.weights <- weights

  pdf <- function(x1,...) {
    if(length(x1)==1)
      return(as.numeric(sum(sapply(self$wrappedModels(), function(y) y$pdf(x1)) * private$.weights)))
    else
      return(as.numeric(rowSums(sapply(self$wrappedModels(), function(y) y$pdf(x1)) %*% diag(private$.weights))))
  }

  name = paste("Mixture of",paste(distnames, collapse = "_"))
  short_name = paste0("Mix_",paste(distnames, collapse = "_"))
  type = do.call(setunion, lapply(distlist, type))
  support = do.call(setunion, lapply(distlist, type))

  super$initialize(distlist = distlist, pdf = pdf, cdf = cdf, rand = rand, name = name,
                   short_name = short_name, description = description, type = type,
                   support = support, valueSupport = "mixture")
})
MixtureDistribution$set("private",".weights",numeric(0))

Again this is a shortened version of the code. Note the following in the above

  1. As multiple distributions are wrapped we first call makeUniqueDistributions, a helper function that ensures the IDs of the distributions are unique. This automatically clones all distributions passed to it to prevent the R6 copying problem (see here).
  2. We add a private variable .weights which is accessed in the pdf/cdf. In general we allow additional private variables and methods to be added to wrappers, but not public ones.
  3. The type and support are updated to account for multiple distributions.
  4. All implemented wrappers inherit from DistributionWrapper and again lock_objects = FALSE

Summary

  1. Implementing wrappers is similar to implementing a SDistribution or Kernel but generally more flexible
  2. We can add private methods/variables to a wrapper, but if possible, not public methods/variables
  3. Wrappers automatically add methods for accessing internally models and correctly getting/setting parameters

Extension Guidelines



RaphaelS1/distr6 documentation built on Feb. 24, 2024, 9:14 p.m.