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
|
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveDataTypeable, DeriveGeneric #-}
-- |
-- Module : Statistics.Distribution.Lognormal
-- Copyright : (c) 2020 Ximin Luo
-- License : BSD3
--
-- Maintainer : infinity0@pwned.gg
-- Stability : experimental
-- Portability : portable
--
-- The Weibull distribution. This is a continuous probability
-- distribution that describes the occurrence of a single event whose
-- probability changes over time, controlled by the shape parameter.
module Statistics.Distribution.Weibull
(
WeibullDistribution
-- * Constructors
, weibullDistr
, weibullDistrErr
, weibullStandard
, weibullDistrApproxMeanStddevErr
) where
import Control.Applicative
import Data.Aeson (FromJSON(..), ToJSON, Value(..), (.:))
import Data.Binary (Binary(..))
import Data.Data (Data, Typeable)
import GHC.Generics (Generic)
import Numeric.MathFunctions.Constants (m_eulerMascheroni)
import Numeric.SpecFunctions (expm1, log1p, logGamma)
import qualified Data.Vector.Generic as G
import qualified Statistics.Distribution as D
import qualified Statistics.Sample as S
import Statistics.Internal
-- | The Weibull distribution.
data WeibullDistribution = WD {
wdShape :: {-# UNPACK #-} !Double
, wdLambda :: {-# UNPACK #-} !Double
} deriving (Eq, Typeable, Data, Generic)
instance Show WeibullDistribution where
showsPrec i (WD k l) = defaultShow2 "weibullDistr" k l i
instance Read WeibullDistribution where
readPrec = defaultReadPrecM2 "weibullDistr" $
(either (const Nothing) Just .) . weibullDistrErr
instance ToJSON WeibullDistribution
instance FromJSON WeibullDistribution where
parseJSON (Object v) = do
k <- v .: "wdShape"
l <- v .: "wdLambda"
either fail return $ weibullDistrErr k l
parseJSON _ = empty
instance Binary WeibullDistribution where
put (WD k l) = put k >> put l
get = do
k <- get
l <- get
either fail return $ weibullDistrErr k l
instance D.Distribution WeibullDistribution where
cumulative = cumulative
complCumulative = complCumulative
instance D.ContDistr WeibullDistribution where
logDensity = logDensity
quantile = quantile
complQuantile = complQuantile
instance D.MaybeMean WeibullDistribution where
maybeMean = Just . D.mean
instance D.Mean WeibullDistribution where
mean (WD k l) = l * exp (logGamma (1 + 1 / k))
instance D.MaybeVariance WeibullDistribution where
maybeStdDev = Just . D.stdDev
maybeVariance = Just . D.variance
instance D.Variance WeibullDistribution where
variance (WD k l) = l * l * (exp (logGamma (1 + 2 * invk)) - q * q)
where
invk = 1 / k
q = exp (logGamma (1 + invk))
instance D.Entropy WeibullDistribution where
entropy (WD k l) = m_eulerMascheroni * (1 - 1 / k) + log (l / k) + 1
instance D.MaybeEntropy WeibullDistribution where
maybeEntropy = Just . D.entropy
instance D.ContGen WeibullDistribution where
genContVar d = D.genContinuous d
-- | Standard Weibull distribution with scale factor (lambda) 1.
weibullStandard :: Double -> WeibullDistribution
weibullStandard k = weibullDistr k 1.0
-- | Create Weibull distribution from parameters.
--
-- If the shape (first) parameter is @1.0@, the distribution is equivalent to a
-- 'Statistics.Distribution.Exponential.ExponentialDistribution' with parameter
-- @1 / lambda@ the scale (second) parameter.
weibullDistr
:: Double -- ^ Shape
-> Double -- ^ Lambda (scale)
-> WeibullDistribution
weibullDistr k l = either error id $ weibullDistrErr k l
-- | Create Weibull distribution from parameters.
--
-- If the shape (first) parameter is @1.0@, the distribution is equivalent to a
-- 'Statistics.Distribution.Exponential.ExponentialDistribution' with parameter
-- @1 / lambda@ the scale (second) parameter.
weibullDistrErr
:: Double -- ^ Shape
-> Double -- ^ Lambda (scale)
-> Either String WeibullDistribution
weibullDistrErr k l | k <= 0 = Left $ errMsg k l
| l <= 0 = Left $ errMsg k l
| otherwise = Right $ WD k l
errMsg :: Double -> Double -> String
errMsg k l =
"Statistics.Distribution.Weibull.weibullDistr: both shape and lambda must be positive. Got shape "
++ show k
++ " and lambda "
++ show l
-- | Create Weibull distribution from mean and standard deviation.
--
-- The algorithm is from "Methods for Estimating Wind Speed Frequency
-- Distributions", C. G. Justus, W. R. Hargreaves, A. Mikhail, D. Graber, 1977.
-- Given the identity:
--
-- \[
-- (\frac{\sigma}{\mu})^2 = \frac{\Gamma(1+2/k)}{\Gamma(1+1/k)^2} - 1
-- \]
--
-- \(k\) can be approximated by
--
-- \[
-- k \approx (\frac{\sigma}{\mu})^{-1.086}
-- \]
--
-- \(\lambda\) is then calculated straightforwardly via the identity
--
-- \[
-- \lambda = \frac{\mu}{\Gamma(1+1/k)}
-- \]
--
-- Numerically speaking, the approximation for \(k\) is accurate only within a
-- certain range. We arbitrarily pick the range \(0.033 \le \frac{\sigma}{\mu} \le 1.45\)
-- where it is good to ~6%, and will refuse to create a distribution outside of
-- this range. The paper does not cover these details but it is straightforward
-- to check them numerically.
weibullDistrApproxMeanStddevErr
:: Double -- ^ Mean
-> Double -- ^ Stddev
-> Either String WeibullDistribution
weibullDistrApproxMeanStddevErr m s = if r > 1.45 || r < 0.033
then Left msg
else weibullDistrErr k l
where r = s / m
k = (s / m) ** (-1.086)
l = m / exp (logGamma (1 + 1/k))
msg = "Statistics.Distribution.Weibull.weibullDistr: stddev-mean ratio "
++ "outside approximation accuracy range [0.033, 1.45]. Got "
++ "stddev " ++ show s ++ " and mean " ++ show m
-- | Uses an approximation based on the mean and standard deviation in
-- 'weibullDistrEstMeanStddevErr', with standard deviation estimated
-- using maximum likelihood method (unbiased estimation).
--
-- Returns @Nothing@ if sample contains less than one element or
-- variance is zero (all elements are equal), or if the estimated mean
-- and standard-deviation lies outside the range for which the
-- approximation is accurate.
instance D.FromSample WeibullDistribution Double where
fromSample xs
| G.length xs <= 1 = Nothing
| v == 0 = Nothing
| otherwise = either (const Nothing) Just $
weibullDistrApproxMeanStddevErr m (sqrt v)
where
(m,v) = S.meanVarianceUnb xs
logDensity :: WeibullDistribution -> Double -> Double
logDensity (WD k l) x
| x < 0 = 0
| otherwise = log k + (k - 1) * log x - k * log l - (x / l) ** k
cumulative :: WeibullDistribution -> Double -> Double
cumulative (WD k l) x | x < 0 = 0
| otherwise = -expm1 (-(x / l) ** k)
complCumulative :: WeibullDistribution -> Double -> Double
complCumulative (WD k l) x | x < 0 = 1
| otherwise = exp (-(x / l) ** k)
quantile :: WeibullDistribution -> Double -> Double
quantile (WD k l) p
| p == 0 = 0
| p == 1 = inf
| p > 0 && p < 1 = l * (-log1p (-p)) ** (1 / k)
| otherwise =
error $ "Statistics.Distribution.Weibull.quantile: p must be in [0,1] range. Got: " ++ show p
where inf = 1 / 0
complQuantile :: WeibullDistribution -> Double -> Double
complQuantile (WD k l) q
| q == 0 = inf
| q == 1 = 0
| q > 0 && q < 1 = l * (-log q) ** (1 / k)
| otherwise =
error $ "Statistics.Distribution.Weibull.complQuantile: q must be in [0,1] range. Got: " ++ show q
where inf = 1 / 0
|