-
Notifications
You must be signed in to change notification settings - Fork 415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add mean et al. for truncated log normal #1874
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Moments of the truncated log-normal can be computed directly from the moment generating | ||
# function of the truncated normal: | ||
# Let Y ~ LogNormal(μ, σ) truncated to (a, b). Then log(Y) ~ Normal(μ, σ) truncated | ||
# to (log(a), log(b)), and E[Y^n] = E[(e^log(Y))^n] = E[e^(nlog(Y))] = mgf(log(Y), n). | ||
|
||
# Given `truncate(LogNormal(μ, σ), a, b)`, return `truncate(Normal(μ, σ), log(a), log(b))` | ||
function _truncnorm(d::Truncated{<:LogNormal}) | ||
μ, σ = params(d.untruncated) | ||
T = float(partype(d)) | ||
a = d.lower === nothing || d.lower <= 0 ? nothing : log(T(d.lower)) | ||
b = d.upper === nothing || isinf(d.upper) ? nothing : log(T(d.upper)) | ||
return truncated(Normal{T}(T(μ), T(σ)), a, b) | ||
end | ||
|
||
mean(d::Truncated{<:LogNormal}) = mgf(_truncnorm(d), 1) | ||
|
||
function var(d::Truncated{<:LogNormal}) | ||
tn = _truncnorm(d) | ||
# Ensure the variance doesn't end up negative, which can occur due to numerical issues | ||
return max(mgf(tn, 2) - mgf(tn, 1)^2, 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Repeated evaluation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I had the same thought and wasn't sure what to do about it so I just... didn't address it, haha |
||
end | ||
|
||
function skewness(d::Truncated{<:LogNormal}) | ||
tn = _truncnorm(d) | ||
m1 = mgf(tn, 1) | ||
m2 = mgf(tn, 2) | ||
m3 = mgf(tn, 3) | ||
sqm1 = m1^2 | ||
v = m2 - sqm1 | ||
return (m3 + m1 * (-3 * m2 + 2 * sqm1)) / (v * sqrt(v)) | ||
end | ||
|
||
function kurtosis(d::Truncated{<:LogNormal}) | ||
tn = _truncnorm(d) | ||
m1 = mgf(tn, 1) | ||
m2 = mgf(tn, 2) | ||
m3 = mgf(tn, 3) | ||
m4 = mgf(tn, 4) | ||
v = m2 - m1^2 | ||
return @horner(m1, m4, -4m3, 6m2, 0, -3) / v^2 - 3 | ||
end | ||
|
||
# TODO: The entropy can be written "directly" as well, according to Mathematica, but | ||
# the expression for it fills me with regret. There are some recognizable components, | ||
# so a sufficiently motivated person could try to manually simplify it into something | ||
# comprehensible. For reference, you can obtain the entropy with Mathematica like so: | ||
# | ||
# d = TruncatedDistribution[{a, b}, LogNormalDistribution[m, s]]; | ||
# Expectation[-LogLikelihood[d, {x}], Distributed[x, d], | ||
# Assumptions -> Element[x | m | s | a | b, Reals] && s > 0 && 0 < a < x < b] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
using Distributions, Test | ||
using Distributions: expectation | ||
|
||
naive_moment(d, n, μ, σ²) = (σ = sqrt(σ²); expectation(x -> ((x - μ) / σ)^n, d)) | ||
|
||
@testset "Truncated log normal" begin | ||
@testset "truncated(LogNormal{$T}(0, 1), ℯ⁻², ℯ²)" for T in (Float32, Float64, BigFloat) | ||
d = truncated(LogNormal{T}(zero(T), one(T)), exp(T(-2)), exp(T(2))) | ||
tn = truncated(Normal{BigFloat}(big(0.0), big(1.0)), -2, 2) | ||
bigmean = mgf(tn, 1) | ||
bigvar = mgf(tn, 2) - bigmean^2 | ||
@test @inferred(mean(d)) ≈ bigmean | ||
@test @inferred(var(d)) ≈ bigvar | ||
@test @inferred(median(d)) ≈ one(T) | ||
@test @inferred(skewness(d)) ≈ naive_moment(d, 3, bigmean, bigvar) | ||
@test @inferred(kurtosis(d)) ≈ naive_moment(d, 4, bigmean, bigvar) - big(3) | ||
@test mean(d) isa T | ||
end | ||
@testset "Bound with no effect" begin | ||
# Uses the example distribution from issue #709, though what's tested here is | ||
# mostly unrelated to that issue (aside from `mean` not erroring). | ||
# The specified left truncation at 0 has no effect for `LogNormal` | ||
d1 = truncated(LogNormal(1, 5), 0, 1e5) | ||
@test mean(d1) ≈ 0 atol=eps() | ||
v1 = var(d1) | ||
@test v1 ≈ 0 atol=eps() | ||
# Without a `max(_, 0)`, this would be within machine precision of 0 (as above) but | ||
# numerically negative, which could cause downstream issues that assume a nonnegative | ||
# variance | ||
@test v1 >= 0 | ||
# Compare results with not specifying a lower bound at all | ||
d2 = truncated(LogNormal(1, 5); upper=1e5) | ||
@test mean(d1) == mean(d2) | ||
@test var(d1) == var(d2) | ||
|
||
# Truncated outside of support where taking a log would error | ||
d3 = truncated(LogNormal(); lower=-1) | ||
@test mean(d3) == mean(d3.untruncated) | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests on Julia 1.3 pass locally when changing this to
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That isn't functionally equivalent though; IIRC, I was relying on
a
and/orb
beingnothing
in those cases so thattruncated
would handle it a particular way.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But arguably this optimization (
d.lower === nothing
ord.upper === nothing
) should already have happened, either by a user or internally, when constructing thed = truncated(LogNormal(...))
? Maybe one shouldn't expect that unoptimized inputs lead to optimized algorithms. AFAICT the optimization oftruncated(Normal(...), ...)
is also only exploited in the case where this returns aNormal
(a = b = nothing
); the code forTruncated{<:Normal}
does not seem to use the fact that a bound might benothing
. In theNormal
case, arguably theLogNormal
shouldn't be truncated in the first place.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm currently sick and can only barely comprehend that message but if you'd prefer to go with your suggested change then feel free to apply it, I trust your judgement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did we have this conversation a few months ago? My brain similarly can't comprehend whether this is the same discussion: #1874 (comment)
I should probably just log off and go sleep