Interoperability with PartitionedDistributions.jl
PartitionedDistributions.jl provides a generic interface for extracting marginal and conditional distributions from multivariate distributions.
When both Copulas.jl and PartitionedDistributions.jl are loaded, an extension connects their APIs in both directions:
PartitionedDistributions.marginalandPartitionedDistributions.conditionalcan be used withCopulaandSklarDistobjects;Copulas.subsetdims,Copulas.condition,Copulas.rosenblatt, andCopulas.inverse_rosenblattcan be used with compatible vector-valued distributions implementing the public marginal and conditional interfaces.
This lets downstream code choose either interface without having to special-case copula-based models.
Setup
using Copulas
using Distributions
using PartitionedDistributionsUsing the PartitionedDistributions.jl API on copulas
Consider a three-dimensional Gaussian copula.
C = GaussianCopula{3}(0.35)
u = [0.2, 0.4, 0.7]PartitionedDistributions.marginal specifies the coordinates to keep. For copulas, it delegates to Copulas.subsetdims.
C13_pd = marginal(C, [1, 3])
C13_copulas = subsetdims(C, (1, 3))
(
partitioned = logpdf(C13_pd, u[[1, 3]]),
copulas = logpdf(C13_copulas, u[[1, 3]]),
)(partitioned = -0.17933297139487347, copulas = -0.17933297139487347)The order of the selected dimensions is preserved:
C31_pd = marginal(C, [3, 1])
C31_copulas = subsetdims(C, (3, 1))
(
partitioned = logpdf(C31_pd, u[[3, 1]]),
copulas = logpdf(C31_copulas, u[[3, 1]]),
)(partitioned = -0.17933297139487347, copulas = -0.17933297139487347)The two packages use complementary conventions for conditioning.
PartitionedDistributions.conditional(dist, x, keep) specifies the coordinates that remain random, whereas Copulas.condition(dist, observed, values) specifies the coordinates that are observed.
For example, keeping coordinates 1 and 3 means conditioning on coordinate 2:
C13_cond_pd = conditional(C, u, [1, 3])
C13_cond_copulas = condition(C, 2, u[2])
v = [0.3, 0.6]
(
partitioned = logpdf(C13_cond_pd, v),
copulas = logpdf(C13_cond_copulas, v),
)(partitioned = 0.10042179825478056, copulas = 0.10042179825478056)Likewise, keeping only the first coordinate is equivalent to conditioning on coordinates 2 and 3:
C1_cond_pd = conditional(C, u, 1)
C1_cond_copulas = condition(C, (2, 3), (u[2], u[3]))
x = 0.3
(
partitioned = logpdf(C1_cond_pd, x),
copulas = logpdf(C1_cond_copulas, x),
)(partitioned = 0.02160515110655159, copulas = 0.02160515110655159)Because PartitionedDistributions.jl defines its generic pointwise conditional log-density interface in terms of conditional, it also works directly with copulas:
pointwise_conditional_logpdfs(C, u)3-element Vector{Float64}:
-0.05366841420426027
0.11433797800726249
-0.16145837556733095Using the PartitionedDistributions.jl API on SklarDist
The same interface works on distributions built with Sklar's theorem.
S = SklarDist(
C,
(
Normal(0.0, 1.0),
LogNormal(0.1, 0.5),
Gamma(2.0, 1.0),
),
)
x = [0.2, 1.1, 2.0]Marginalization can again be expressed with either package:
S13_pd = marginal(S, [1, 3])
S13_copulas = subsetdims(S, (1, 3))
(
partitioned = logpdf(S13_pd, x[[1, 3]]),
copulas = logpdf(S13_copulas, x[[1, 3]]),
)(partitioned = -2.1682200820554445, copulas = -2.1682200820554445)And the conditioning conventions remain complementary:
S13_cond_pd = conditional(S, x, [1, 3])
S13_cond_copulas = condition(S, 2, x[2])
y = [0.1, 1.8]
(
partitioned = logpdf(S13_cond_pd, y),
copulas = logpdf(S13_cond_copulas, y),
)(partitioned = -1.9703856126953456, copulas = -1.9703856126953456)Using the Copulas.jl API on other distributions
The extension also works in the opposite direction.
For vector-valued distributions supported by PartitionedDistributions.jl, subsetdims delegates to PartitionedDistributions.marginal, while condition delegates to PartitionedDistributions.conditional. Because the latter requires a complete support point, the adapter fills retained coordinates with their marginal medians and verifies both the marginal placeholders and the assembled joint point before making the call. If a constrained joint support rejects that completion, condition reports that a complete point must instead be supplied directly to PartitionedDistributions.conditional.
For example, consider a multivariate normal distribution:
μ = [0.2, -0.3, 0.7]
Σ = [
1.0 0.3 0.1
0.3 1.2 0.25
0.1 0.25 0.8
]
D = MvNormal(μ, Σ)
x = [0.1, -0.4, 1.1]The Copulas.jl subsetting interface can now be used directly:
D13_copulas = subsetdims(D, (1, 3))
D13_pd = marginal(D, [1, 3])
(
copulas_mean = mean(D13_copulas),
partitioned_mean = mean(D13_pd),
)(copulas_mean = [0.2, 0.7], partitioned_mean = [0.2, 0.7])The same applies to conditioning. With the Copulas.jl convention, specifying coordinate 2 means that coordinate 2 is observed and coordinates 1 and 3 remain random:
D13_cond_copulas = condition(D, 2, x[2])
D13_cond_pd = conditional(D, x, [1, 3])
(
copulas_mean = mean(D13_cond_copulas),
partitioned_mean = mean(D13_cond_pd),
)(copulas_mean = [0.175, 0.6791666666666666], partitioned_mean = [0.175, 0.6791666666666666])Conditioning on several coordinates works in the same way:
D1_cond_copulas = condition(
D,
(2, 3),
(x[2], x[3]),
)
D1_cond_pd = conditional(D, x, 1)
(
copulas_mean = mean(D1_cond_copulas),
partitioned_mean = mean(D1_cond_pd),
)(copulas_mean = 0.19610027855153206, partitioned_mean = 0.19610027855153206)The marginal and conditional interface also supplies the successive laws needed by the Rosenblatt transform and its inverse:
u = rosenblatt(D, x)
x_reconstructed = inverse_rosenblatt(D, u)
(
uniforms = u,
reconstruction_error = maximum(abs, x_reconstructed .- x),
)(uniforms = [0.460172162722971, 0.4735133407437463, 0.6881550316809848], reconstruction_error = 5.551115123125783e-17)Choosing an interface
The two interfaces describe the same marginalization and conditioning operations but use different conditioning conventions:
| Operation | Copulas.jl | PartitionedDistributions.jl |
|---|---|---|
| Keep dimensions 1 and 3 | subsetdims(D, (1, 3)) | marginal(D, [1, 3]) |
| Observe dimension 2 | condition(D, 2, x[2]) | conditional(D, x, [1, 3]) |
| Observe dimensions 2 and 3 | condition(D, (2, 3), (x[2], x[3])) | conditional(D, x, 1) |
Loading both packages activates the interoperability extension, and compatible vector-valued families can use whichever convention best fits the application. Marginalization only requires the public marginal interface. Conditioning and Rosenblatt transforms additionally require the conditional interface and a valid complete support point assembled as described above.