-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathConditioning.jl
More file actions
337 lines (300 loc) · 15.4 KB
/
Conditioning.jl
File metadata and controls
337 lines (300 loc) · 15.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
###############################################################################
##### Conditioning framework.
##### User-facing function: `condition(), rosenblatt(), inverse_rosenblatt()`
#####
##### When implementing new models, you can overwrite:
##### - `DistortionFromCop(C::Copula{d}, js::NTuple{p,Int}, uⱼₛ::NTuple{p,Float64}, i::Int) where {d, p}`
##### - `ConditionalCopula(C::Copula{d}, js::NTuple{p,Int}, uⱼₛ::NTuple{p,Float64}) where {d, p}`
###############################################################################
# A few utilities :
function _assemble(D, is, js, uᵢₛ, uⱼₛ)
Tᵢ = eltype(typeof(uᵢₛ)); Tⱼ = eltype(typeof(uⱼₛ)); T = promote_type(Tᵢ, Tⱼ)
w = fill(one(T), D)
@inbounds for (k,i) in pairs(is); w[i] = uᵢₛ[k]; end
@inbounds for (k,j) in pairs(js); w[j] = uⱼₛ[k]; end
return w
end
function _swap(u, i, uᵢ)
T = promote_type(eltype(u), typeof(uᵢ))
v = similar(u, T)
@inbounds for k in eachindex(u)
v[k] = u[k]
end
v[i] = uᵢ
return v
end
_der(f, u, i::Int) = ForwardDiff.derivative(uᵢ -> f(_swap(u, i, uᵢ)), u[i])
_der(f, u, is::NTuple{1,Int}) = _der(f, u, is[1])
_der(f, u, is::NTuple{N,Int}) where {N} = _der(u′ -> _der(f, u′, (is[end],)), u, is[1:end-1])
_partial_cdf(C, is, js, uᵢₛ, uⱼₛ) = _der(u -> Distributions.cdf(C, u), _assemble(length(C), is, js, uᵢₛ, uⱼₛ), js)
_process_tuples(::Val{D}, js::NTuple{p, Int64}, ujs::NTuple{p, Float64}) where {D,p} = (js, ujs)
_process_tuples(::Val{D}, j::Int64, uj::Real) where {D} = ((j,), (uj,))
function _process_tuples(::Val{D}, js, ujs) where D
p, p2 = length(js), length(ujs)
@assert 0 < p < D "js=$(js) must be a non-empty proper subset of 1:D of length at most D-1 (D = $D)"
@assert p == p2 && all(0 .<= ujs .<= 1) "uⱼₛ must be in [0,1] and match js length"
jst = Tuple(collect(Int, js))
@assert all(in(1:D), jst)
ujst = Tuple(collect(float.(ujs)))
return (jst, ujst)
end
"""
Distortion <: Distributions.ContinuousUnivariateDistribution
Abstract super-type for objects describing the (uniform-scale) conditional marginal
transformation U_i | U_J = u_J of a copula.
Subtypes implement cdf/quantile on [0,1]. They are not full arbitrary distributions;
they model how a uniform variable is distorted by conditioning. They can be applied
as a function to a base marginal distribution to obtain the conditional marginal on
the original scale: if `D::Distortion` and `X::UnivariateDistribution`, then `D(X)`
is the distribution of `X_i | U_J = u_J`.
"""
abstract type Distortion<:Distributions.ContinuousUnivariateDistribution end
(D::Distortion)(::Distributions.Uniform) = D
(D::Distortion)(X::Distributions.UnivariateDistribution) = DistortedDist(D, X)
Distributions.minimum(::Distortion) = 0.0
Distributions.maximum(::Distortion) = 1.0
function Distributions.quantile(d::Distortion, α::Real)
T = typeof(float(α))
ϵ = eps(T)
α < ϵ && return zero(T)
α > 1 - 2ϵ && return one(T)
lα = log(α)
f(u) = Distributions.logcdf(d, u) - lα
return Roots.find_zero(f, (ϵ, 1 - 2ϵ), Roots.Bisection(); xtol = sqrt(eps(T)))
end
# You have to implement one of these two:
Distributions.logcdf(d::Distortion, t::Real) = log(Distributions.cdf(d, t))
Distributions.cdf(d::Distortion, t::Real) = exp(Distributions.logcdf(d, t))
"""
DistortionFromCop{TC,p,T} <: Distortion
Generic, uniform-scale conditional marginal transformation for a copula.
This is the default fallback (based on mixed partial derivatives computed via
automatic differentiation) used when a faster specialized `Distortion` is not
available for a given copula family.
Parameters
- `TC`: copula type
- `p`: length of the conditioned index set J (static)
- `T`: element type for the conditioned values u_J
Construction
- `DistortionFromCop(C::Copula, js::NTuple{p,Int}, ujs::NTuple{p,<:Real}, i::Int)`
builds the distortion for the conditional marginal of index `i` given `U_js = ujs`.
Notes
- A convenience method `DistortionFromCop(C, j::Int, uj::Real, i::Int)` exists for
the common `p = 1` case.
"""
struct DistortionFromCop{TC,p}<:Distortion
C::TC
i::Int
js::NTuple{p,Int}
uⱼₛ::NTuple{p,Float64}
den::Float64
function DistortionFromCop(C::Copula{D}, js, uⱼₛ, i) where {D}
jst, uⱼₛt = _process_tuples(Val{D}(), js, uⱼₛ)
p = length(jst)
if p==1
den = Distributions.pdf(subsetdims(C, jst), uⱼₛt[1])
else
den = Distributions.pdf(subsetdims(C, jst), collect(uⱼₛt))
end
return new{typeof(C), p}(C, i, jst, uⱼₛt, den)
end
end
Distributions.cdf(d::DistortionFromCop, u::Real) = _partial_cdf(d.C, (d.i,), d.js, (u,), d.uⱼₛ) / d.den
"""
DistortedDist{Disto,Distrib} <: Distributions.UnivariateDistribution
Push-forward of a base marginal by a `Distortion`.
"""
struct DistortedDist{Disto, Distrib}<:Distributions.ContinuousUnivariateDistribution
D::Disto
X::Distrib
function DistortedDist(D::Distortion, X::Distributions.UnivariateDistribution)
return new{typeof(D), typeof(X)}(D, X)
end
end
Distributions.cdf(D::DistortedDist, t::Real) = Distributions.cdf(D.D, Distributions.cdf(D.X, t))
Distributions.quantile(D::DistortedDist, α::Real) = Distributions.quantile(D.X, Distributions.quantile(D.D, α))
"""
ConditionalCopula{d} <: Copula{d}
Copula of the conditioned random vector U_I | U_J = u_J.
"""
struct ConditionalCopula{d, D, p, TDs}<:Copula{d}
C::Copula{D}
js::NTuple{p, Int}
is::NTuple{d, Int}
uⱼₛ::NTuple{p, Float64}
den::Float64
distortions::TDs
function ConditionalCopula(C::Copula{D}, js, uⱼₛ) where {D}
jst, uⱼₛt = _process_tuples(Val{D}(), js, uⱼₛ)
ist = Tuple(setdiff(1:D, jst))
p = length(jst)
d = D - p
distos = Tuple(DistortionFromCop(C, jst, uⱼₛt, i) for i in ist)
if p==1
den = Distributions.pdf(subsetdims(C, jst), uⱼₛt[1])
else
den = Distributions.pdf(subsetdims(C, jst), collect(uⱼₛt))
end
return new{d, D, p, typeof(distos)}(C, jst, ist, uⱼₛt, den, distos)
end
end
function _cdf(CC::ConditionalCopula{d,D,p,T}, v::AbstractVector{<:Real}) where {d,D,p,T}
return _partial_cdf(CC.C, CC.is, CC.js, Distributions.quantile.(CC.distortions, v), CC.uⱼₛ) / CC.den
end
# Sampling: sequential inverse-CDF using conditional distortions
function Distributions._rand!(rng::Distributions.AbstractRNG, CC::ConditionalCopula{d, D, p}, x::AbstractVector{T}) where {T<:Real, d, D, p}
# We want a sample from the COPULA of the conditional model. Let U be a
# draw from the conditional joint H_{I|J}(· | u_J). The corresponding
# copula coordinates are V_k = F_{i_k|J}(U_k | u_J) = cdf(distortions[k], U_k).
# Sample U sequentially by conditioning on J ∪ previously sampled I.
J = [j for j in CC.js]
ujs = [u for u in CC.uⱼₛ]
for k in 1:d
iₖ = CC.is[k]
uₖ = rand(rng, DistortionFromCop(CC.C, Tuple(J), Tuple(ujs), iₖ))
xₖ = Distributions.cdf(CC.distortions[k], uₖ)
x[k] = xₖ
push!(J, iₖ)
push!(ujs, uₖ)
end
return x
end
###########################################################################
##### condition() function
###########################################################################
"""
condition(C::Copula{D}, js, u_js)
condition(X::SklarDist, js, x_js)
Construct conditional distributions with respect to a copula, either on the
uniform scale (when passing a `Copula`) or on the original data scale (when
passing a `SklarDist`).
Arguments
- `C::Copula{D}`: D-variate copula
- `X::SklarDist`: joint distribution with copula `X.C` and marginals `X.m`
- `js`: indices of conditioned coordinates (tuple, NTuple, or vector)
- `u_js`: values in [0,1] for `U_js` (when conditioning a copula)
- `x_js`: values on original scale for `X_js` (when conditioning a SklarDist)
- `j, u_j, x_j`: 1D convenience overloads for the common p = 1 case
Returns
- If the number of remaining coordinates `d = D - length(js)` is 1:
- `condition(C, js, u_js)` returns a `Distortion` on [0,1] describing
`U_i | U_js = u_js`.
- `condition(X, js, x_js)` returns an unconditional univariate distribution
for `X_i | X_js = x_js`, computed as the push-forward `D(X.m[i])` where
`D = condition(C, js, u_js)` and `u_js = cdf.(X.m[js], x_js)`.
- If `d > 1`:
- `condition(C, js, u_js)` returns the conditional joint distribution on
the uniform scale as a `SklarDist(ConditionalCopula, distortions)`.
- `condition(X, js, x_js)` returns the conditional joint distribution on the
original scale as a `SklarDist` with copula `ConditionalCopula(C, js, u_js)` and
appropriately distorted marginals `D_k(X.m[i_k])`.
Notes
- For best performance, pass `js` and `u_js` as NTuple to keep `p = length(js)`
known at compile time. The specialized method `condition(::Copula{2}, j, u_j)`
exploits this for the common `D = 2, d = 1` case.
- Specializations are provided for many copula families (Independent, Gaussian, t,
Archimedean, several bivariate families). Others fall back to an automatic
differentiation based construction.
- This function returns the conditional joint distribution `H_{I|J}(· | u_J)`.
The “conditional copula” is `ConditionalCopula(C, js, u_js)`, i.e., the copula
of that conditional distribution.
"""
condition(C::Copula{D}, j, xⱼ) where D = condition(C, _process_tuples(Val{D}(), j, xⱼ)...)
function condition(C::Copula{D}, js::NTuple{p, Int}, uⱼₛ::NTuple{p, Float64}) where {D, p}
margins = Tuple(DistortionFromCop(C, js, uⱼₛ, i) for i in setdiff(1:D, js))
p==D-1 && return margins[1]
return SklarDist(ConditionalCopula(C, js, uⱼₛ), margins)
end
condition(C::SklarDist{<:Copula{D}}, j, xⱼ) where D = condition(C, _process_tuples(Val{D}(), j, xⱼ)...)
function condition(X::SklarDist{<:Copula{D}, Tpl}, js::NTuple{p, Int}, xⱼₛ::NTuple{p, Float64}) where {D, Tpl, p}
uⱼₛ = Tuple(Distributions.cdf(X.m[j], xⱼ) for (j,xⱼ) in zip(js, xⱼₛ))
margins = Tuple(DistortionFromCop(X.C, js, uⱼₛ, i)(X.m[i]) for i in setdiff(1:D, js))
p==D-1 && return margins[1]
return SklarDist(ConditionalCopula(X.C, js, uⱼₛ), margins)
end
###########################################################################
##### Methods for conditioning subsetcopulas.
###########################################################################
function DistortionFromCop(S::SubsetCopula, js::NTuple{p,Int}, uⱼₛ::NTuple{p,Float64}, i::Int) where {p}
ibase = S.dims[i]
jsbase = ntuple(k -> S.dims[js[k]], p)
return DistortionFromCop(S.C, jsbase, uⱼₛ, ibase)
end
function ConditionalCopula(S::SubsetCopula{d,CT}, js, uⱼₛ) where {d,CT}
Jbase = Tuple(S.dims[j] for j in js)
CC_base = ConditionalCopula(S.C, Jbase, uⱼₛ)
D = length(S.C); I = Tuple(setdiff(1:D, Jbase))
dims_remain = Tuple(i for i in S.dims if !(i in Jbase))
posmap = Dict(i => p for (p,i) in enumerate(I))
dims_positions = Tuple(posmap[i] for i in dims_remain)
return (length(dims_positions) == length(I)) ? CC_base : SubsetCopula(CC_base, dims_positions)
end
###########################################################################
##### Generic Rosenblatt and inverse Rosenblatt via conditioning
###########################################################################
"""
rosenblatt(C::Copula, u)
Computes the rosenblatt transform associated to the copula C on the vector u. Formally, assuming that U ∼ C, the result should be uniformely distributed on the unit hypercube. The importance of this transofrmation comes from its bijectivity: `inverse_rosenblatt(C, rand(d))` is equivalent to `rand(C)`. The interface proposes faster versions for matrix inputs `u`.
Generic Rosenblatt transform using conditional distortions:
S₁ = U₁, S_k = H_{k|1:(k-1)}(U_k | U₁:U_{k-1}).
Specialized families may provide faster overrides.
* [rosenblatt1952](@cite) Rosenblatt, M. (1952). Remarks on a multivariate transformation. Annals of Mathematical Statistics, 23(3), 470-472.
* [joe2014](@cite) Joe, H. (2014). Dependence Modeling with Copulas. CRC Press. (Section 2.10)
* [mcneil2009](@cite) McNeil, A. J., & Nešlehová, J. (2009). Multivariate Archimedean copulas, d-monotone functions and ℓ 1-norm symmetric distributions.
"""
rosenblatt(C::Copula{d}, u::AbstractVector{<:Real}) where {d} = rosenblatt(C, reshape(u, (d, 1)))[:]
function rosenblatt(C::Copula{d}, u::AbstractMatrix{<:Real}) where {d}
size(u, 1) == d || throw(ArgumentError("Dimension mismatch between copula and input matrix"))
v = similar(u)
@inbounds for j in axes(u, 2)
# First coordinate is unchanged
v[1, j] = clamp(float(u[1, j]), 0.0, 1.0)
for k in 2:d
js = ntuple(i -> i, k - 1)
ujs = ntuple(i -> float(u[i, j]), k - 1) # condition on original u's
Dk = DistortionFromCop(C, js, ujs, k)
v[k, j] = Distributions.cdf(Dk, clamp(float(u[k, j]), 0.0, 1.0))
end
end
return v
end
function rosenblatt(D::SklarDist, u::AbstractMatrix{<:Real})
v = similar(u)
for (i,Mᵢ) in enumerate(D.m)
v[i,:] .= Distributions.cdf.(Mᵢ, u[i,:])
end
return rosenblatt(D.C, v)
end
"""
inverse_rosenblatt(C::Copula, u)
Computes the inverse rosenblatt transform associated to the copula C on the vector u. Formally, assuming that U ∼ Π, the independence copula, the result should be distributed as C. Also look at `rosenblatt(C, u)` for the inverse transformation. The interface proposes faster versions for matrix inputs `u`.
Generic inverse Rosenblatt using conditional distortions:
U₁ = S₁, U_k = H_{k|1:(k-1)}^{-1}(S_k | U₁:U_{k-1}).
Specialized families may provide faster overrides.
References:
* [rosenblatt1952](@cite) Rosenblatt, M. (1952). Remarks on a multivariate transformation. Annals of Mathematical Statistics, 23(3), 470-472.
* [joe2014](@cite) Joe, H. (2014). Dependence Modeling with Copulas. CRC Press. (Section 2.10)
* [mcneil2009](@cite) McNeil, A. J., & Nešlehová, J. (2009). Multivariate Archimedean copulas, d-monotone functions and ℓ 1-norm symmetric distributions.
"""
inverse_rosenblatt(C::Copula{d}, u::AbstractVector{<:Real}) where {d} = inverse_rosenblatt(C, reshape(u, (d, 1)))[:]
function inverse_rosenblatt(C::Copula{d}, s::AbstractMatrix{<:Real}) where {d}
size(s, 1) == d || throw(ArgumentError("Dimension mismatch between copula and input matrix"))
v = similar(s)
@inbounds for j in axes(s, 2)
v[1, j] = clamp(float(s[1, j]), 0.0, 1.0)
for k in 2:d
js = ntuple(i -> i, k - 1)
ujs = ntuple(i -> float(v[i, j]), k - 1) # use already reconstructed U's
Dk = DistortionFromCop(C, js, ujs, k)
v[k, j] = Distributions.quantile(Dk, clamp(float(s[k, j]), 0.0, 1.0))
end
end
return v
end
function inverse_rosenblatt(D::SklarDist, u::AbstractMatrix{<:Real})
v = inverse_rosenblatt(D.C,u)
for (i,Mᵢ) in enumerate(D.m)
v[i,:] .= Distributions.quantile.(Mᵢ, v[i,:])
end
return v
end