Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/lrnv/Copulas.jl
Browse files Browse the repository at this point in the history
  • Loading branch information
lrnv committed Dec 11, 2023
2 parents 5b3014a + f717d71 commit 8f6153c
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 48 deletions.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
MvNormalCDF = "37188c8d-bc69-4638-b057-733e744175ec"
PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
QuadGK = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
Expand All @@ -30,6 +31,7 @@ InteractiveUtils = "1.6"
LinearAlgebra = "1.6"
LogExpFunctions = "0.3"
MvNormalCDF = "0.2, 0.3"
PrecompileTools = "1"
QuadGK = "2"
Random = "1.6"
Roots = "1, 2"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
`Copulas.jl` brings most standard [copula](https://en.wikipedia.org/wiki/Copula_(probability_theory)) features into native Julia: random number generation, pdf and cdf, fitting, copula-based multivariate distributions through Sklar's theorem, etc. Since copulas are distribution functions, we fully comply with the [`Distributions.jl`](https://github.com/JuliaStats/Distributions.jl) API. This complience allows interoperability with other packages based on this API such as, e.g., [`Turing.jl`](https://github.com/TuringLang/Turing.jl).

Usually, people that use and work with copulas turn to R, because of the amazing `R` package [`copula`](https://cran.r-project.org/web/packages/copula/copula.pdf).
While it is still well maintained and regularly updated, the `R` poackage `copula` is a mixture of obscure, heavily optimized `C` code and more standard `R` cde, which makes it a complicated code base for readability, extensibility, reliability and maintenance.
While it is still well maintained and regularly updated, the `R` package `copula` is a mixture of obscure, heavily optimized `C` code and more standard `R` code, which makes it a complicated code base for readability, extensibility, reliability and maintenance.

This is an attempt to provide a very light, fast, reliable and maintainable copula implementation in native Julia. Among others, one of the notable benefits of such a native implementatioon is the floating point type agnosticity, i.e. compatibility with `BigFloat`, [`DoubleFloats`](https://github.com/JuliaMath/DoubleFloats.jl), [`MultiFloats`](https://github.com/dzhang314/MultiFloats.jl) and other kind of numbers.

Expand All @@ -36,7 +36,7 @@ The package revolves around two main types:
- `Copula`, an abstract mother type for all the copulas in the package
- `SklarDist`, a distribution type that allows construction of a multivariate distribution by specifying the copula and the marginals through [Sklar's theorem](https://en.wikipedia.org/wiki/Copula_(probability_theory)#Sklar's_theorem).

**Warning: This is fairly untested and experimental work and the API might change without notice.**
**Warning: This is fairly experimental work and our API might change without notice.**

## Instalation

Expand Down
6 changes: 3 additions & 3 deletions src/ArchimedeanCopula.jl
Original file line number Diff line number Diff line change
Expand Up @@ -192,18 +192,18 @@ function Distributions._rand!(rng::Distributions.AbstractRNG, ::ArchimedeanCopul
x[1] = rand(rng)
x[2] = 1-x[1]
end
function Distributions._rand!(rng::Distributions.AbstractRNG, C::ArchimedeanCopula{d,IndependentGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
function Distributions._rand!(rng::Distributions.AbstractRNG, ::ArchimedeanCopula{d,IndependentGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
Random.rand!(rng,A)
return A
end
function Distributions._rand!(rng::Distributions.AbstractRNG, C::ArchimedeanCopula{d,MGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
function Distributions._rand!(rng::Distributions.AbstractRNG, ::ArchimedeanCopula{d,MGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
A[1,:] .= rand(rng,size(A,2))
for i in 2:size(A,1)
A[i,:] .= A[1,:]
end
return A
end
function Distributions._rand!(rng::Distributions.AbstractRNG, C::ArchimedeanCopula{d,WGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
function Distributions._rand!(rng::Distributions.AbstractRNG, ::ArchimedeanCopula{d,WGenerator}, A::DenseMatrix{T}) where {T<:Real, d}
@assert size(A,1) == 2
A[1,:] .= rand(rng,size(A,2))
A[2,:] .= 1 .- A[1,:]
Expand Down
7 changes: 4 additions & 3 deletions src/Copula.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ Base.length(::Copula{d}) where d = d
# Base.eltype
# τ, τ⁻¹
# Base.eltype
function Distributions.cdf(C::Copula{d},u::AbstractVector) where d
length(u) != length(C) && throw(ArgumentError("Dimension mismatch between copula and input vector"))
function Distributions.cdf(C::Copula{d},u::VT) where {d,VT<:AbstractVector}
length(u) != d && throw(ArgumentError("Dimension mismatch between copula and input vector"))
return _cdf(C,u)
end
function Distributions.cdf(C::Copula{d},A::AbstractMatrix) where d
return [Distributions.cdf(C,u) for u in eachcol(A)]
size(A,1) != d && throw(ArgumentError("Dimension mismatch between copula and input vector"))
return [_cdf(C,u) for u in eachcol(A)]
end
function _cdf(C::CT,u) where {CT<:Copula}
f(x) = Distributions.pdf(C,x)
Expand Down
63 changes: 53 additions & 10 deletions src/Copulas.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ module Copulas
include("MiscellaneousCopulas/EmpiricalCopula.jl")
include("MiscellaneousCopulas/FGMCopula.jl")
include("MiscellaneousCopulas/RafteryCopula.jl")
export MCopula,
WCopula,
SurvivalCopula,
export SurvivalCopula,
PlackettCopula,
EmpiricalCopula,
FGMCopula,
Expand All @@ -57,7 +55,6 @@ module Copulas

include("Generator/WilliamsonGenerator.jl")
export WilliamsonGenerator, i𝒲

include("Generator/ZeroVariateGenerator/IndependentGenerator.jl")
include("Generator/ZeroVariateGenerator/MGenerator.jl")
include("Generator/ZeroVariateGenerator/WGenerator.jl")
Expand All @@ -72,15 +69,61 @@ module Copulas
# Archimedean copulas
include("ArchimedeanCopula.jl")
export ArchimedeanCopula,
IndependentCopula,
IndependentCopula,
MCopula,
WCopula,
AMHCopula,
ClaytonCopula,
JoeCopula,
GumbelCopula,
FrankCopula,
AMHCopula,
GumbelBarnettCopula,
GumbelCopula,
InvGaussianCopula,
MCopula,
WCopula
JoeCopula



using PrecompileTools
@setup_workload begin
# Putting some things in `@setup_workload` instead of `@compile_workload` can reduce the size of the
# precompile file and potentially make loading faster.
@compile_workload begin
for C in (
IndependentCopula(3),
AMHCopula(3,0.6),
AMHCopula(4,-0.3),
ClaytonCopula(2,-0.7),
ClaytonCopula(3,-0.1),
ClaytonCopula(4,7),
FrankCopula(2,-5),
FrankCopula(3,12),
JoeCopula(3,7),
GumbelCopula(4,7),
GumbelBarnettCopula(3,0.7),
InvGaussianCopula(4,0.05),
InvGaussianCopula(3,8),
GaussianCopula([1 0.5; 0.5 1]),
TCopula(4, [1 0.5; 0.5 1]),
FGMCopula(2,1),
MCopula(4),
ArchimedeanCopula(2,Copulas.i𝒲(Distributions.LogNormal(),2)),
PlackettCopula(2.0),
EmpiricalCopula(randn(2,100),pseudo_values=false),
SurvivalCopula(ClaytonCopula(2,-0.7),(1,2)),
# WCopula(2), ################ <<<<<<<<<-------------- Does not work and I cannot explain why !
# RafteryCopula(2, 0.2), ################ <<<<<<<<<<------------- BUGGY
# RafteryCopula(3, 0.5), ################ <<<<<<<<<<------------- BUGGY
# We should probably add others to speed up again.
)
u1 = rand(C)
u = rand(C,2)
if applicable(Distributions.pdf,C,u1) && !(typeof(C)<:EmpiricalCopula)
Distributions.pdf(C,u1)
Distributions.pdf(C,u)
end
Distributions.cdf(C,u1)
Distributions.cdf(C,u)
end
end
end

end
7 changes: 3 additions & 4 deletions src/Generator/UnivariateGenerator/GumbelBarnettGenerator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ struct GumbelBarnettGenerator{T} <: UnivariateGenerator
end
max_monotony(G::GumbelBarnettGenerator) = Inf
ϕ( G::GumbelBarnettGenerator, t) = exp((1-exp(t))/G.θ)
ϕ⁻¹(G::GumbelBarnettGenerator, t) = log(1-G.θ*log(t))
ϕ⁻¹(G::GumbelBarnettGenerator, t) = log1p(-G.θ*log(t))
# ϕ⁽¹⁾(G::GumbelBarnettGenerator, t) = # First derivative of ϕ
# ϕ⁽ᵏ⁾(G::GumbelBarnettGenerator, k, t) = # kth derivative of ϕ

function τ(G::GumbelBarnettGenerator)
# Use a numerical integration method to obtain tau
result, _ = QuadGK.quadgk(x -> -((x-G.θ*x*log(x))*log(1-G.θ*log(x))/G.θ), 0, 1)

return 1+4*result
r, _ = QuadGK.quadgk(x -> (1-G.θ*log(x)) * log1p(-G.θ*log(x)) * x, 0, 1)
return 1-4*r/G.θ
end
function τ⁻¹(::Type{T}, tau) where T<:GumbelBarnettGenerator
if tau == 0
Expand Down
3 changes: 3 additions & 0 deletions src/MiscellaneousCopulas/EmpiricalCopula.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ end
function _cdf(C::EmpiricalCopula{d,MT},u) where {d,MT}
return StatsBase.mean(all(C.u .<= u,dims=1)) # might not be very efficient implementation.
end
function Distributions._logpdf(C::EmpiricalCopula{d,MT}, u) where {d,MT}
any(C.u .== u) ? -log(size(C.u,2)) : -Inf
end
function Distributions._rand!(rng::Distributions.AbstractRNG, C::EmpiricalCopula{d,MT}, x::AbstractVector{T}) where {d,MT,T<:Real}
x .= C.u[:,Distributions.rand(rng,axes(C.u,2),1)[1]]
end
Expand Down
2 changes: 1 addition & 1 deletion src/MiscellaneousCopulas/FGMCopula.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ end
function _cdf(fgm::FGMCopula, u::Vector{T}) where {T}
return prod(u) * (1 + _reduce_over_combinations(fgm, 1 .-u, prod))
end
function Distributions._logpdf(fgm::FGMCopula, u::Vector{T}) where {T}
function Distributions._logpdf(fgm::FGMCopula, u)
return log1p(_reduce_over_combinations(fgm, 1 .-2u, prod))
end
function Distributions._rand!(rng::Distributions.AbstractRNG, fgm::FGMCopula{d,Tθ}, x::AbstractVector{T}) where {d,Tθ, T <: Real}
Expand Down
35 changes: 16 additions & 19 deletions test/RafteryTest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
@test_throws ArgumentError RafteryCopula(2, 2.6)
end

@testset "RafteryCopula CDF" begin

@testitem "RafteryCopula CDF" begin
using StableRNGs
using Distributions
rng = StableRNG(123)
for d in [2, 3, 4]
F = RafteryCopula(d, 0.5)

Expand All @@ -17,28 +19,23 @@ end
cdf_value = cdf(F, u)
@test cdf_value >= 0 && cdf_value <= 1
end
examples = [
([0.2, 0.5], [1.199432, 1e-5], 0.8),
([0.3, 0.8], [0.2817, 1e-5], 0.5),
([0.1, 0.2, 0.3], [0.08325884, 1e-5], 0.5),
([0.4, 0.8, 0.2], [0.120415, 1e-5], 0.1),
]

for (u, expected, θ) in examples
copula = RafteryCopula(length(u), θ)
@test cdf(copula, u) expected[1] atol=expected[2]
end

@test cdf(RafteryCopula(2, 0.8), [0.2, 0.5]) 0.199432 atol=1e-5
@test cdf(RafteryCopula(2, 0.5), [0.3, 0.8]) 0.2817 atol=1e-5
@test_broken cdf(RafteryCopula(3, 0.5), [0.1, 0.2, 0.3]) 0.08325884 atol=1e-5
@test_broken cdf(RafteryCopula(3, 0.1), [0.4, 0.8, 0.2]) 0.120415 atol=1e-5
end

@testset "RafteryCopula PDF" begin

@testitem "RafteryCopula PDF" begin
using StableRNGs
using Distributions
rng = StableRNG(123)
for d in [2, 3, 4]
F = RafteryCopula(d, 0.5)

# Test PDF with some random values
u = rand(d)
pdf_value = pdf(F, u)
@test pdf_value >= 0
u = rand(rng,d)
@test_broken 0 <= pdf(F, u) <= 1
end
examples = [
([0.2, 0.5], [0.114055555, 1e-4], 0.8),
Expand All @@ -49,7 +46,7 @@ end

for (u, expected, θ) in examples
copula = RafteryCopula(length(u), θ)
@test pdf(copula, u) expected[1] atol=expected[2]
@test_broken pdf(copula, u) expected[1] atol=expected[2]
end
end

Expand Down
12 changes: 6 additions & 6 deletions test/margins_uniformity.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
n = 1000
U = Uniform(0,1)
for C in cops

@show C
d = length(C)
CT = typeof(C)
rng = StableRNG(123)
Expand All @@ -119,16 +119,16 @@

# On the cdf:
u = ones(d)
for val in [0,1,rand(10)...]
for val in [0,1,rand(rng,10)...]
u[i] = val
if typeof(C)<:TCopula
@test_broken cdf(C,u) val
else
@test cdf(C,u) val
@test cdf(C,u) val atol=1e-5
end
end
# extra check for zeros:
u = rand(d)
u = rand(rng,d)
u[i] = 0
if typeof(C)<:TCopula
@test_broken cdf(C,u) val
Expand All @@ -154,7 +154,7 @@
# # also check that pdf values are indeed derivatives of the cdf values:
# begin
# for _ in 1:10
# u = rand(d)
# u = rand(rng,d)
# @test isapprox(get_numerical_pdf(C,u),pdf(C,u),atol=1e-5)
# end
# end
Expand Down Expand Up @@ -200,4 +200,4 @@
# @test true

end
end
end

0 comments on commit 8f6153c

Please sign in to comment.