The previous tutorial introduced decorators in distr6. These are used to add numeric and more complex methods to the distribution interface. Wrappers are the next big distr6 feature and serve a similar purpose.
Just like decorators, wrappers are a design pattern commonly used in object-oriented programming. For people interested in these principles, we are specifically using the ‘Adapter’ pattern from Design Patterns (Gamma et al. 1994). Instead of simply adding methods, as decorators do, wrappers also slightly alter the interface of the object and usually result in a slightly different object being created. For example,
class(Normal$new())
#> [1] "Normal" "SDistribution" "Distribution" "R6"
class(truncate(Normal$new()))
#> [1] "TruncatedDistribution" "DistributionWrapper" "Distribution"
#> [4] "R6"
See how the Normal distribution becomes an object of class
TruncatedDistribution
which in itself is of class
DistributionWrapper
. This tells the machine to expect a
slightly different interface but it still recognises the original Normal
distribution that is being wrapped.
All wrappers share a common method wrappedModels()
. More
often than not this is used internally but it’s still useful if you want
to remind yourself of the original wrapped model.
You can also specify the name of the internal model to access it,
A slightly quicker approach is to use the description
variable which is updated each time a distribution is wrapped
truncate(Normal$new(),-1,1)$description
#> [1] "Normal Probability Distribution. Truncated between -1 and 1."
huberize(truncate(Normal$new(),-3,3),-2,2)$description
#> [1] "Normal Probability Distribution. Truncated between -3 and 3. Huberized between -2 and 2."
Or finally just look at the new distribution name
There are currently 8 wrappers in distr6, these can be listed with
listWrappers()
. They can be sectioned into two groups:
composite wrappers and transformer wrappers. Composite wrappers create
composite distributions, i.e. distributions that use internal functions
to combine multiple distributions. Transformation wrappers perform some
form of transformation on the distribution, e.g. scaling the
distribution to a given mean or variance. We will look at three of these
in detail and hopefully understanding the others will follow
intuitively.
Note: The Convolution and Scale wrappers are still in an experimental lifecycle, meaning that we are aware they may be sub-optimal or bugs may be present.
This is perhaps the simplest wrapper in distr6. It can be constructed in one of two ways:
N <- Normal$new()
TruncatedDistribution$new(N, lower = -2, upper = 2)
#> TruncNorm(Norm__mean = 0, Norm__var = 1, trunc__lower = -2, trunc__upper = 2)
truncate(N, lower = -2, upper = 2)
#> TruncNorm(Norm__mean = 0, Norm__var = 1, trunc__lower = -2, trunc__upper = 2)
In both cases, the result has to be saved to truncate the distribution as these are not methods in the Normal distribution class, i.e. to use the truncated normal distribution:
# This is a good example of when piping is clean and effective
library(magrittr)
tn <- Normal$new() %>% truncate(-2,2)
class(tn)
#> [1] "TruncatedDistribution" "DistributionWrapper" "Distribution"
#> [4] "R6"
In the example above we are truncating the Normal distribution between the limits -2 and 2, this updates the distribution support accordingly and re-distributes the weights of the pdf and cdf
tn$cdf(-3)
#> [1] 0
tn$cdf(1)
#> [1] 0.8576164
A product distribution is a composite distribution in which two or
more independent distributions are combined into a single joint
distribution. Details about the maths behind this can be found in
?ProductDistribution
. For now we use this as an example of
the composite distributions in distr6. Say we want the product
distribution of the binomial and normal distributions,
N <- Normal$new(mean = 2, var = 1)
B <- Binomial$new(size = 5, prob = 0.2)
P <- ProductDistribution$new(list(N, B))
print(P)
#> Norm X Binom
P$description
#> [1] "Product of: Norm, Binom"
class(P)
#> [1] "ProductDistribution" "VectorDistribution" "DistributionWrapper"
#> [4] "Distribution" "R6"
Now we can treat P
like any other multivariate
distribution
A mixture distribution is a weighted sum of component distributions. By default the weights are assumed to follow the uniform distribution, i.e. the probability of each distribution is identical, but this can be edited. Below we take the mixture of Binomial, Normal and Exponential distributions.
N <- Normal$new()
B <- Binomial$new()
E <- Exponential$new()
M <- MixtureDistribution$new(list(N, B, E), weights = c(0.1,0.5,0.4))
print(M)
#> Norm wX Binom wX Exp
M$description
#> [1] "Mixture of: Norm, Binom, Exp"
class(M)
#> [1] "MixtureDistribution" "VectorDistribution" "DistributionWrapper"
#> [4] "Distribution" "R6"
M$cdf(1:3)
#> [1] 0.3423538 0.4709346 0.5658877
There are plenty other wrappers to explore, including vector distributions that combine distributions into one vector and array distributions that are a type of product distribution for k-dimensional distributions of a specific type. The tools learnt from this tutorial are all applicable to every other wrapper and we hope you find the rest intuitive to use. In the next and final tutorial we conclude with running through how to construct your own custom distribution.