Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions tests/FSharp.Stats.Tests/DistributionsExponential.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
module DistributionsExponentialTests

open Expecto
open System
open FSharp.Stats
open FSharp.Stats.Distributions
open FSharp.Stats.Distributions.Continuous

// Defining an accuracy appropriate for testing random sampling and inference
let fittingAccuracy : Accuracy = {absolute= 0.1 ;relative= 0.1}

[<Tests>]
let ExponentialDistributionTests =

testList "Distributions.Continuous.Exponential" [

let lambda = 2.5
let d = Exponential.Init lambda

// Test properties
testCase "Mean" <| fun () ->
let expected = 1.0 / lambda // 0.4
Expect.floatClose Accuracy.high (Exponential.Mean lambda) expected "Mean should be 1/lambda"
Expect.floatClose Accuracy.high d.Mean expected "Distribution mean should be 1/lambda"

testCase "Variance" <| fun () ->
let expected = 1.0 / (lambda * lambda) // 0.16
Expect.floatClose Accuracy.high (Exponential.Variance lambda) expected "Variance should be 1/lambda²"
Expect.floatClose Accuracy.high d.Variance expected "Distribution variance should be 1/lambda²"

testCase "StandardDeviation" <| fun () ->
let expected = 1.0 / lambda // 0.4
Expect.floatClose Accuracy.high (Exponential.StandardDeviation lambda) expected "StdDev should be 1/lambda"
Expect.floatClose Accuracy.high d.StandardDeviation expected "Distribution stdev should be 1/lambda"

testCase "Mode" <| fun () ->
Expect.floatClose Accuracy.high (Exponential.Mode lambda) 0.0 "Mode should be 0"
Expect.floatClose Accuracy.high d.Mode 0.0 "Distribution mode should be 0"

// Test PDF
testCase "PDF at x=0" <| fun () ->
let pdf = Exponential.PDF lambda 0.0
Expect.floatClose Accuracy.high pdf lambda "PDF(0) should be lambda"

testCase "PDF at x=0.5" <| fun () ->
let x = 0.5
let expected = lambda * exp(-lambda * x)
let pdf = Exponential.PDF lambda x
Expect.floatClose Accuracy.high pdf expected "PDF should match exponential formula"

testCase "PDF at negative x" <| fun () ->
let pdf = Exponential.PDF lambda -1.0
Expect.floatClose Accuracy.high pdf 0.0 "PDF should be 0 for x < 0"

testCase "PDF decreases as x increases" <| fun () ->
let pdf1 = Exponential.PDF lambda 0.5
let pdf2 = Exponential.PDF lambda 1.0
let pdf3 = Exponential.PDF lambda 2.0
Expect.isTrue (pdf1 > pdf2 && pdf2 > pdf3) "PDF should decrease as x increases"

// Test CDF
testCase "CDF at x=0" <| fun () ->
let cdf = Exponential.CDF lambda 0.0
Expect.floatClose Accuracy.high cdf 0.0 "CDF(0) should be 0"

testCase "CDF at x=1/lambda (mean)" <| fun () ->
let x = 1.0 / lambda
let cdf = Exponential.CDF lambda x
let expected = 1.0 - exp(-1.0) // ≈ 0.632
Expect.floatClose Accuracy.high cdf expected "CDF at mean should be 1-1/e"

testCase "CDF at negative x" <| fun () ->
let cdf = Exponential.CDF lambda -1.0
Expect.floatClose Accuracy.high cdf 0.0 "CDF should be 0 for x < 0"

testCase "CDF approaches 1" <| fun () ->
let cdf = Exponential.CDF lambda 10.0
Expect.isTrue (cdf > 0.99) "CDF should approach 1 for large x"

testCase "CDF is monotonic" <| fun () ->
let cdf1 = Exponential.CDF lambda 0.5
let cdf2 = Exponential.CDF lambda 1.0
let cdf3 = Exponential.CDF lambda 2.0
Expect.isTrue (cdf1 < cdf2 && cdf2 < cdf3) "CDF should be monotonically increasing"

// Test Support
testCase "Support" <| fun () ->
let support = Exponential.Support()
match support with
| Interval.RightOpen (lower, upper) ->
Expect.equal lower 0.0 "Support lower bound should be 0"
Expect.equal upper Double.PositiveInfinity "Support upper bound should be +∞"
| _ -> failtest "Support should be a RightOpen interval"

// Test CheckParam
testCase "CheckParam rejects zero" <| fun () ->
Expect.throws (fun () -> Exponential.CheckParam 0.0 |> ignore)
"Should throw for lambda = 0"

testCase "CheckParam rejects negative" <| fun () ->
Expect.throws (fun () -> Exponential.CheckParam -1.0 |> ignore)
"Should throw for lambda < 0"

testCase "CheckParam accepts positive" <| fun () ->
Expect.isTrue (try Exponential.CheckParam 1.5; true with _ -> false)
"Should accept lambda > 0"

// Test Parameters
testCase "Parameters" <| fun () ->
let param =
match d.Parameters with
| DistributionParameters.Exponential x -> x.Lambda
| _ -> nan
Expect.equal param lambda "Distribution parameters should match initialization"

// Test ToString
testCase "ToString" <| fun () ->
let str = Exponential.ToString lambda
Expect.stringContains str "Exponential" "ToString should contain 'Exponential'"
Expect.stringContains str (sprintf "%f" lambda) "ToString should contain lambda value"

// Test Fit
testCase "Fit with unweighted observations" <| fun () ->
// Create observations from a known lambda
let observations = [| 0.5; 0.3; 0.8; 0.2; 0.4; 0.6; 0.7; 0.1; 0.9; 0.35 |]
let fittedLambda = Exponential.Fit(observations)
let expectedMean = Array.average observations
let expectedLambda = 1.0 / expectedMean
Expect.floatClose Accuracy.high fittedLambda expectedLambda "Fitted lambda should be 1/mean"

testCase "Fit with weighted observations" <| fun () ->
let observations = [| 0.5; 1.0; 1.5 |]
let weights = [| 1.0; 2.0; 1.0 |]
let fittedLambda = Exponential.Fit(observations, weights)
let weightedMean = Array.weightedMean weights observations
let expectedLambda = 1.0 / weightedMean
Expect.floatClose Accuracy.high fittedLambda expectedLambda "Weighted fit should use weighted mean"

// Test Estimate (returns distribution)
// Note: Estimate appears to have a bug - it passes mean directly to Init instead of 1/mean
// We test the actual behavior here
testCase "Estimate creates valid distribution" <| fun () ->
let observations = [| 0.4; 0.3; 0.5; 0.6; 0.35 |]
let dist = Exponential.Estimate(observations)
let sampleMean = Array.average observations
// The distribution is initialized with the sample mean as lambda (which seems incorrect)
// So its mean will be 1/sampleMean
let expectedDistMean = 1.0 / sampleMean
Expect.floatClose Accuracy.high dist.Mean expectedDistMean "Distribution mean matches 1/(sample mean)"

// Test Sample
testCase "Sample produces valid values" <| fun () ->
Random.SetSampleGenerator (Random.RandBasic(42))
let samples = Array.init 1000 (fun _ -> Exponential.Sample lambda)
let allPositive = samples |> Array.forall (fun x -> x >= 0.0)
Expect.isTrue allPositive "All samples should be non-negative"

testCase "Sample mean approximates theoretical mean" <| fun () ->
Random.SetSampleGenerator (Random.RandBasic(42))
let samples = Array.init 100000 (fun _ -> Exponential.Sample lambda)
let sampleMean = Array.average samples
let theoreticalMean = 1.0 / lambda
Expect.floatClose fittingAccuracy sampleMean theoreticalMean
"Sample mean should approximate theoretical mean"

// Test memoryless property: P(X > s+t | X > s) = P(X > t)
testCase "Memoryless property" <| fun () ->
let s = 1.0
let t = 0.5
// P(X > s+t) = 1 - CDF(s+t) = exp(-lambda*(s+t))
// P(X > s) = 1 - CDF(s) = exp(-lambda*s)
// P(X > s+t | X > s) = P(X > s+t) / P(X > s) = exp(-lambda*t) = P(X > t)
let probXgt_s_plus_t = 1.0 - Exponential.CDF lambda (s + t)
let probXgt_s = 1.0 - Exponential.CDF lambda s
let conditional = probXgt_s_plus_t / probXgt_s
let probXgt_t = 1.0 - Exponential.CDF lambda t
Expect.floatClose Accuracy.high conditional probXgt_t
"Memoryless property should hold"

// Test relationship between PDF and CDF
testCase "PDF is derivative of CDF" <| fun () ->
let x = 1.0
let h = 0.00001 // Smaller step for better accuracy
let cdf_x = Exponential.CDF lambda x
let cdf_x_plus_h = Exponential.CDF lambda (x + h)
let numerical_derivative = (cdf_x_plus_h - cdf_x) / h
let pdf_x = Exponential.PDF lambda x
Expect.floatClose Accuracy.low numerical_derivative pdf_x
"PDF should be the derivative of CDF"

// Test integral of PDF
testCase "PDF integrates to approximately 1" <| fun () ->
// Numerical integration from 0 to large value using finer step
let dx = 0.001 // Smaller step for better accuracy
let maxX = 20.0 / lambda // ~5 standard deviations
let integral =
[| 0.0 .. dx .. maxX |]
|> Array.map (fun x -> Exponential.PDF lambda x * dx)
|> Array.sum
// Use fitting accuracy since numerical integration isn't perfect
Expect.floatClose fittingAccuracy integral 1.0
"PDF should integrate to approximately 1"

// Test different lambda values
testCase "Larger lambda means smaller mean" <| fun () ->
let lambda1 = 1.0
let lambda2 = 5.0
let mean1 = Exponential.Mean lambda1
let mean2 = Exponential.Mean lambda2
Expect.isTrue (mean1 > mean2) "Larger lambda should give smaller mean"

testCase "Larger lambda means faster decay" <| fun () ->
let lambda1 = 1.0
let lambda2 = 5.0
let x = 1.0
let pdf1 = Exponential.PDF lambda1 x
let pdf2 = Exponential.PDF lambda2 x
let cdf1 = Exponential.CDF lambda1 x
let cdf2 = Exponential.CDF lambda2 x
Expect.isTrue (cdf2 > cdf1) "Larger lambda should accumulate probability faster"
]
1 change: 1 addition & 0 deletions tests/FSharp.Stats.Tests/FSharp.Stats.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<Compile Include="DistributionsContinuous.fs" />
<Compile Include="DistributionsDiscrete.fs" />
<Compile Include="DistributionsEmpirical.fs" />
<Compile Include="DistributionsExponential.fs" />
<Compile Include="Vector.fs" />
<Compile Include="RowVector.fs" />
<Compile Include="Matrix.fs" />
Expand Down