module Factory.Math.Probability(
Distribution(..),
ContinuousDistribution(..),
DiscreteDistribution(..),
maxPreciseInteger,
boxMullerTransform,
generateStandardizedNormalDistribution,
generateContinuousPopulation,
generateDiscretePopulation
) where
import qualified Control.Arrow
import Control.Arrow((***), (&&&))
import qualified Factory.Data.Interval as Data.Interval
import qualified Factory.Math.Power as Math.Power
import qualified System.Random
import qualified ToolShed.Data.List
import qualified ToolShed.Data.Pair
import qualified ToolShed.SelfValidate
maxPreciseInteger :: RealFloat a => a -> Integer
maxPreciseInteger = (2 ^) . floatDigits
minPositiveFloat :: RealFloat a => a -> a
minPositiveFloat = encodeFloat 1 . uncurry () . (fst . floatRange &&& floatDigits)
data ContinuousDistribution parameter
= ExponentialDistribution parameter
| LogNormalDistribution parameter parameter
| NormalDistribution parameter parameter
| UniformDistribution (Data.Interval.Interval parameter)
deriving (Eq, Read, Show)
instance (Floating parameter, Ord parameter, Show parameter) => ToolShed.SelfValidate.SelfValidator (ContinuousDistribution parameter) where
getErrors probabilityDistribution = ToolShed.SelfValidate.extractErrors $ case probabilityDistribution of
ExponentialDistribution lambda -> [(lambda <= 0, "'lambda' must exceed zero; " ++ show probabilityDistribution ++ ".")]
LogNormalDistribution location scale2 -> let
maxParameter = log . fromInteger $ maxPreciseInteger (undefined :: Double)
in [
(scale2 <= 0, "'scale' must exceed zero; " ++ show probabilityDistribution ++ "."),
(location > maxParameter || scale2 > maxParameter, "loss of precision will result from either 'location' or 'scale^2' exceeding '" ++ show maxParameter ++ "'; " ++ show probabilityDistribution ++ ".")
]
NormalDistribution _ variance -> [(variance <= 0, "variance must exceed zero; " ++ show probabilityDistribution ++ ".")]
UniformDistribution interval -> [(Data.Interval.isReversed interval, "reversed interval='" ++ show probabilityDistribution ++ "'.")]
data DiscreteDistribution parameter
= PoissonDistribution parameter
| ShiftedGeometricDistribution parameter
deriving (Eq, Read, Show)
instance (Num parameter, Ord parameter, Show parameter) => ToolShed.SelfValidate.SelfValidator (DiscreteDistribution parameter) where
getErrors probabilityDistribution = ToolShed.SelfValidate.extractErrors $ case probabilityDistribution of
PoissonDistribution lambda -> [(lambda <= 0, "'lambda' must exceed zero; " ++ show probabilityDistribution ++ ".")]
ShiftedGeometricDistribution probability -> [(any ($ probability) [(<= 0), (> 1)], "probability must be in the semi-closed unit-interval (0, 1]; " ++ show probabilityDistribution ++ ".")]
class Distribution probabilityDistribution where
generatePopulation
:: (Fractional sample, System.Random.RandomGen randomGen)
=> probabilityDistribution
-> randomGen
-> [sample]
getMean :: Fractional mean => probabilityDistribution -> mean
getStandardDeviation :: Floating standardDeviation => probabilityDistribution -> standardDeviation
getStandardDeviation = sqrt . getVariance
getVariance :: Floating variance => probabilityDistribution -> variance
getVariance = Math.Power.square . getStandardDeviation
instance (RealFloat parameter, Show parameter, System.Random.Random parameter) => Distribution (ContinuousDistribution parameter) where
generatePopulation probabilityDistribution = map realToFrac . generateContinuousPopulation probabilityDistribution
getMean (ExponentialDistribution lambda) = realToFrac $ recip lambda
getMean (LogNormalDistribution location scale2) = realToFrac . exp . (+ location) $ scale2 / 2
getMean (NormalDistribution mean _) = realToFrac mean
getMean (UniformDistribution (minParameter, maxParameter)) = realToFrac $ (minParameter + maxParameter) / 2
getVariance (ExponentialDistribution lambda) = realToFrac . recip $ Math.Power.square lambda
getVariance (LogNormalDistribution location scale2) = realToFrac $ (exp scale2 1) * exp (2 * location + scale2)
getVariance (NormalDistribution _ variance) = realToFrac variance
getVariance (UniformDistribution (minParameter, maxParameter)) = realToFrac $ Math.Power.square (maxParameter minParameter) / 12
instance (RealFloat parameter, Show parameter, System.Random.Random parameter) => Distribution (DiscreteDistribution parameter) where
generatePopulation probabilityDistribution = map fromInteger . generateDiscretePopulation probabilityDistribution
getMean (PoissonDistribution lambda) = realToFrac lambda
getMean (ShiftedGeometricDistribution probability) = realToFrac $ recip probability
getVariance (PoissonDistribution lambda) = realToFrac lambda
getVariance (ShiftedGeometricDistribution probability) = realToFrac $ (1 probability) / Math.Power.square probability
boxMullerTransform :: (
Floating f,
Ord f,
Show f
)
=> (f, f)
-> (f, f)
boxMullerTransform cartesian
| not . uncurry (&&) $ ToolShed.Data.Pair.mirror inSemiClosedUnitInterval cartesian = error $ "Factory.Math.Probability.boxMullerTransform:\tspecified Cartesian coordinates, must be within semi-closed unit-interval (0, 1]; " ++ show cartesian
| otherwise = polarToCartesianTransform $ (sqrt . negate . (* 2) . log *** (* 2) . (* pi)) cartesian
where
inSemiClosedUnitInterval :: (Num n, Ord n) => n -> Bool
inSemiClosedUnitInterval = uncurry (&&) . ((> 0) &&& (<= 1))
polarToCartesianTransform :: Floating f => (f, f) -> (f, f)
polarToCartesianTransform = uncurry (*) . Control.Arrow.second cos &&& uncurry (*) . Control.Arrow.second sin
generateStandardizedNormalDistribution :: (
RealFloat f,
Show f,
System.Random.Random f,
System.Random.RandomGen randomGen
) => randomGen -> [f]
generateStandardizedNormalDistribution = ToolShed.Data.List.linearise . uncurry (zipWith $ curry boxMullerTransform) . ToolShed.Data.Pair.mirror (
System.Random.randomRs (minPositiveFloat undefined, 1)
) . System.Random.split
reProfile :: (Distribution distribution, Floating n) => distribution -> [n] -> [n]
reProfile distribution = map ((+ getMean distribution) . (* getStandardDeviation distribution))
generateContinuousPopulation :: (
RealFloat f,
Show f,
System.Random.Random f,
System.Random.RandomGen randomGen
)
=> ContinuousDistribution f
-> randomGen
-> [f]
generateContinuousPopulation probabilityDistribution randomGen
| not $ ToolShed.SelfValidate.isValid probabilityDistribution = error $ "Factory.Math.Probability.generateContinuousPopulation:\t" ++ ToolShed.SelfValidate.getFirstError probabilityDistribution
| otherwise = (
case probabilityDistribution of
ExponentialDistribution lambda -> let
quantile = (/ lambda) . negate . log . (1 )
in map quantile . System.Random.randomRs (0, 1)
LogNormalDistribution location scale2 -> map (
exp . (+ location) . (* sqrt scale2)
) . generateStandardizedNormalDistribution
NormalDistribution _ _ -> reProfile probabilityDistribution . generateStandardizedNormalDistribution
UniformDistribution interval -> System.Random.randomRs interval
) randomGen
generatePoissonDistribution :: (
Integral sample,
RealFloat lambda,
Show lambda,
System.Random.Random lambda,
System.Random.RandomGen randomGen
)
=> lambda
-> randomGen
-> [sample]
generatePoissonDistribution lambda
| lambda <= 0 = error $ "Factory.Math.Probability.generatePoissonDistribution:\tlambda must exceed zero " ++ show lambda
| lambda > (
negate . log $ minPositiveFloat lambda
) = filter (>= 0) . map round . (reProfile (PoissonDistribution lambda) :: [Double] -> [Double]) . generateStandardizedNormalDistribution
| otherwise = generator
where
generator = uncurry (:) . (
fst . head . dropWhile (
(> exp (negate lambda)) . snd
) . scanl (
\accumulator random -> succ *** (* random) $ accumulator
) (negate 1, 1) . System.Random.randomRs (0, 1) *** generator
) . System.Random.split
generateDiscretePopulation :: (
Integral sample,
Ord parameter,
RealFloat parameter,
Show parameter,
System.Random.Random parameter,
System.Random.RandomGen randomGen
)
=> DiscreteDistribution parameter
-> randomGen
-> [sample]
generateDiscretePopulation probabilityDistribution randomGen
| not $ ToolShed.SelfValidate.isValid probabilityDistribution = error $ "Factory.Math.Probability.generateDiscretePopulation:\t" ++ ToolShed.SelfValidate.getFirstError probabilityDistribution
| otherwise = (
case probabilityDistribution of
PoissonDistribution lambda -> generatePoissonDistribution lambda
ShiftedGeometricDistribution probability
| probability == 1 -> const $ repeat 1
| otherwise -> map ceiling . (\x -> x :: [Rational]) . generatePopulation (ExponentialDistribution . negate $ log (1 probability))
) randomGen