Skip to content

Commit e372030

Browse files
committed
standardised t distribution
1 parent 962f084 commit e372030

File tree

8 files changed

+184
-83
lines changed

8 files changed

+184
-83
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ exstatic-*.tar
2424

2525
# Temporary files, for example, from tests.
2626
/tmp/
27+
28+
# Shared binary artifacts (compiled NIFs).
29+
priv/native/*.so

lib/exstatic/distribution.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Exstatic.Distribution do
22
@type t() :: struct()
33
@type error() :: {:error, atom()}
44

5-
@callback mean(distribution :: t()) :: float() | :undefined
5+
@callback mean(distribution :: t()) :: float()
66
@callback variance(distribution :: t()) :: float() | :undefined | :infinity
77
@callback std_dev(distribution :: t()) :: float()
88
@callback entropy(distribution :: t()) :: float()

lib/exstatic/distribution/t.ex

Lines changed: 108 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,133 @@
1-
defmodule Exstatic.Distribution.T do
1+
defmodule Exstatic.Distribution.StandardizedT do
22
@moduledoc """
3-
Student's t-distribution implementation.
3+
The standardized Student's t-distribution, used in statistical hypothesis testing.
4+
5+
This implementation ensures that:
6+
- The mean is always `0.0`.
7+
- The variance exists for `df > 1` (it is infinite for `1 < df ≤ 2`).
8+
- The distribution is well-defined only for `df > 1`.
9+
10+
## Examples
11+
12+
iex> alias Exstatic.Distribution.StandardizedT
13+
iex> {:ok, t} = StandardizedT.new(5.0)
14+
iex> StandardizedT.mean(t)
15+
0.0
16+
iex> pdf = StandardizedT.pdf(t, 0.0)
17+
iex> TestHelper.assert_in_delta(pdf, 0.37960669, 1.0e-6)
18+
true
19+
iex> result = StandardizedT.cdf(t, 0.0)
20+
iex> TestHelper.assert_in_delta(result, 0.5)
21+
true
422
"""
523

624
@behaviour Exstatic.Distribution
725
@behaviour Exstatic.Continuous
826
@behaviour Exstatic.ContinuousCDF
927

10-
defstruct [:mean, :std_dev, :df]
28+
defstruct [:df]
1129

12-
@type t :: %__MODULE__{
13-
mean: float(),
14-
std_dev: float(),
15-
df: float()
16-
}
30+
@type t :: %__MODULE__{df: float()}
1731

18-
def new(mean, std_dev, df) when is_number(mean) and is_number(std_dev) and is_number(df) do
19-
cond do
20-
std_dev <= 0 -> {:error, :invalid_std_dev}
21-
df <= 0 -> {:error, :invalid_df}
22-
true -> {:ok, %__MODULE__{mean: mean, std_dev: std_dev, df: df}}
23-
end
24-
end
32+
@doc """
33+
Creates a new standardized Student's t-distribution with the given degrees of freedom.
2534
26-
@impl Exstatic.Distribution
27-
def mean(%__MODULE__{mean: mean, df: df}) do
28-
if df > 1, do: mean, else: :undefined
35+
## Parameters
36+
- `df` - The degrees of freedom (`df > 1` required).
37+
38+
## Examples
39+
40+
iex> alias Exstatic.Distribution.StandardizedT
41+
iex> StandardizedT.new(5.0)
42+
{:ok, %StandardizedT{df: 5.0}}
43+
44+
iex> StandardizedT.new(1.0)
45+
{:error, :invalid_df}
46+
47+
iex> StandardizedT.new(-5.0)
48+
{:error, :invalid_df}
49+
"""
50+
def new(df) when is_number(df) and df > 1 do
51+
{:ok, %__MODULE__{df: df}}
2952
end
3053

54+
def new(_df), do: {:error, :invalid_df}
55+
56+
@doc """
57+
Returns the mean of the t-distribution.
58+
59+
The mean is always `0.0` for standardized t-distributions since `df > 1`.
60+
61+
## Examples
62+
63+
iex> {:ok, t} = StandardizedT.new(5.0)
64+
iex> StandardizedT.mean(t)
65+
0.0
66+
"""
3167
@impl Exstatic.Distribution
32-
def std_dev(%__MODULE__{std_dev: std_dev}), do: std_dev
68+
def mean(_t), do: 0.0
69+
70+
@doc """
71+
Returns the variance of the t-distribution.
3372
73+
- If `1 < df ≤ 2`, the variance is `:infinity`.
74+
- Otherwise, the variance is computed using `Exstatic.Native.standardized_t_variance/1`.
75+
76+
## Examples
77+
78+
iex> {:ok, t} = StandardizedT.new(5.0)
79+
iex> TestHelper.assert_in_delta(StandardizedT.variance(t), 5.0 / (5.0 - 2.0), 1.0e-10)
80+
true
81+
82+
iex> {:ok, t} = StandardizedT.new(1.5)
83+
iex> StandardizedT.variance(t)
84+
:infinity
85+
"""
3486
@impl Exstatic.Distribution
35-
@spec variance(t) :: float() | :infinity | :undefined
36-
def variance(%__MODULE__{std_dev: std_dev, df: df}) do
37-
cond do
38-
df <= 1.0 -> :undefined
39-
df > 1.0 and df <= 2.0 -> :infinity
40-
true -> Exstatic.Native.t_variance(std_dev, df)
41-
end
87+
@spec variance(t) :: float() | :infinity
88+
def variance(%__MODULE__{df: df}) do
89+
if df <= 2.0, do: :infinity, else: Exstatic.Native.standardized_t_variance(df)
4290
end
4391

92+
@doc """
93+
Computes the probability density function (PDF) at `x`.
94+
95+
## Examples
96+
97+
iex> {:ok, t} = StandardizedT.new(5.0)
98+
iex> TestHelper.assert_in_delta(StandardizedT.pdf(t, 0.0), 0.37960669, 1.0e-6)
99+
true
100+
"""
44101
@impl Exstatic.Continuous
45102
def pdf(%__MODULE__{} = dist, x) when is_number(x) do
46-
Exstatic.Native.t_pdf(dist.mean, dist.std_dev, dist.df, x)
103+
Exstatic.Native.standardized_t_pdf(dist.df, x)
47104
end
48105

106+
@doc """
107+
Computes the cumulative distribution function (CDF) at `x`.
108+
109+
## Examples
110+
111+
iex> {:ok, t} = StandardizedT.new(5.0)
112+
iex> TestHelper.assert_in_delta(StandardizedT.cdf(t, 0.0), 0.5, 1.0e-10)
113+
true
114+
"""
49115
@impl Exstatic.ContinuousCDF
50116
def cdf(%__MODULE__{} = dist, x) when is_number(x) do
51-
Exstatic.Native.t_cdf(dist.mean, dist.std_dev, dist.df, x)
117+
Exstatic.Native.standardized_t_cdf(dist.df, x)
118+
end
119+
120+
@doc """
121+
Computes the survival function (SF) at `x`, which is `1 - CDF(x)`.
122+
123+
## Examples
124+
125+
iex> {:ok, t} = StandardizedT.new(5.0)
126+
iex> TestHelper.assert_in_delta(StandardizedT.sf(t, 0.0), 0.5, 1.0e-10)
127+
true
128+
"""
129+
@impl Exstatic.ContinuousCDF
130+
def sf(%__MODULE__{} = dist, x) when is_number(x) do
131+
Exstatic.Native.standardized_t_sf(dist.df, x)
52132
end
53133
end

lib/exstatic/native.ex

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,16 @@ defmodule Exstatic.Native do
2323
@spec normal_variance(float()) :: float()
2424
def normal_variance(_std_dev), do: :erlang.nif_error(:nif_not_loaded)
2525

26-
@spec t_pdf(float(), float(), float(), float()) :: float()
27-
def t_pdf(_mean, _std_dev, _df, _x), do: :erlang.nif_error(:nif_not_loaded)
26+
@spec standardized_t_pdf(float(), float()) :: float()
27+
def standardized_t_pdf(_df, _x), do: :erlang.nif_error(:nif_not_loaded)
2828

29-
@spec t_cdf(float(), float(), float(), float()) :: float()
30-
def t_cdf(_mean, _std_dev, _df, _x), do: :erlang.nif_error(:nif_not_loaded)
29+
@spec standardized_t_cdf(float(), float()) :: float()
30+
def standardized_t_cdf(_df, _x), do: :erlang.nif_error(:nif_not_loaded)
3131

32-
@spec t_variance(float(), float()) :: {:ok, float()} | {:error, String.t()}
33-
def t_variance(_std_dev, _df), do: :erlang.nif_error(:nif_not_loaded)
32+
@spec standardized_t_sf(float(), float()) :: float()
33+
def standardized_t_sf(_df, _x), do: :erlang.nif_error(:nif_not_loaded)
34+
35+
@spec standardized_t_variance(float()) :: {:ok, float()} | {:error, String.t()}
36+
def standardized_t_variance(_df), do: :erlang.nif_error(:nif_not_loaded)
3437
end
38+

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
%{
22
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
3-
"hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"},
3+
"hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"},
44
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
55
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
66
"mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"},

native/exstatic/src/lib.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,26 +52,44 @@ fn normal_inverse_cdf(mean: f64, std_dev: f64, p: f64) -> NifResult<f64> {
5252
}
5353

5454
#[rustler::nif]
55-
fn t_pdf(mean: f64, std_dev: f64, df: f64, x: f64) -> NifResult<f64> {
56-
let t = StudentsT::new(mean, std_dev, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
55+
fn standardized_t_pdf(df: f64, x: f64) -> NifResult<f64> {
56+
if df <= 1.0 {
57+
return Err(Error::Term(Box::new("Degrees of freedom must be greater than 1")));
58+
}
59+
60+
let t = StudentsT::new(0.0, 1.0, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
5761
Ok(t.pdf(x))
5862
}
5963

6064
#[rustler::nif]
61-
fn t_cdf(mean: f64, std_dev: f64, df: f64, x: f64) -> NifResult<f64> {
62-
let t = StudentsT::new(mean, std_dev, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
65+
fn standardized_t_cdf(df: f64, x: f64) -> NifResult<f64> {
66+
if df <= 1.0 {
67+
return Err(Error::Term(Box::new("Degrees of freedom must be greater than 1")));
68+
}
69+
70+
let t = StudentsT::new(0.0, 1.0, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
6371
Ok(t.cdf(x))
6472
}
6573

6674
#[rustler::nif]
67-
fn t_variance(std_dev: f64, df: f64) -> NifResult<f64> {
75+
fn standardized_t_sf(df: f64, x: f64) -> NifResult<f64> {
76+
if df <= 1.0 {
77+
return Err(Error::Term(Box::new("Degrees of freedom must be greater than 1")));
78+
}
79+
80+
let t = StudentsT::new(0.0, 1.0, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
81+
Ok(1.0 - t.cdf(x))
82+
}
83+
84+
#[rustler::nif]
85+
fn standardized_t_variance(df: f64) -> NifResult<f64> {
6886
if df <= 1.0 {
6987
return Err(Error::Term(Box::new("Variance is undefined for df ≤ 1")));
7088
} else if df > 1.0 && df <= 2.0 {
7189
return Err(Error::Term(Box::new("Variance is infinite for 1 < df ≤ 2")));
7290
}
7391

74-
let t = StudentsT::new(0.0, std_dev, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
92+
let t = StudentsT::new(0.0, 1.0, df).map_err(|e| Error::Term(Box::new(e.to_string())))?;
7593
t.variance().ok_or_else(|| Error::Term(Box::new("Failed to calculate variance")))
7694
}
7795

priv/native/libexstatic.so

-432 KB
Binary file not shown.
Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,71 @@
1-
defmodule Exstatic.Distribution.TTest do
1+
defmodule Exstatic.Distribution.StandardizedTTest do
22
use ExUnit.Case, async: true
33

4-
alias Exstatic.Distribution.T
4+
alias Exstatic.Distribution.StandardizedT
55

6-
doctest Exstatic.Distribution.T
6+
doctest Exstatic.Distribution.StandardizedT
77

8-
describe "new/3" do
9-
test "creates a valid t-distribution" do
10-
assert {:ok, _t} = T.new(0.0, 1.0, 5.0)
11-
end
12-
13-
test "returns error for invalid std_dev" do
14-
assert {:error, :invalid_std_dev} = T.new(0.0, 0.0, 5.0)
8+
describe "new/1" do
9+
test "creates a valid standardized t-distribution" do
10+
assert {:ok, _t} = StandardizedT.new(5.0)
1511
end
1612

1713
test "returns error for invalid degrees of freedom" do
18-
assert {:error, :invalid_df} = T.new(0.0, 1.0, 0.0)
19-
assert {:error, :invalid_df} = T.new(0.0, 1.0, -5.0)
14+
assert {:error, :invalid_df} = StandardizedT.new(1.0)
15+
assert {:error, :invalid_df} = StandardizedT.new(0.0)
16+
assert {:error, :invalid_df} = StandardizedT.new(-5.0)
2017
end
2118
end
2219

2320
describe "mean/1" do
24-
test "returns the mean when df > 1" do
25-
{:ok, t} = T.new(5.0, 1.0, 3.0)
26-
assert T.mean(t) == 5.0
27-
end
21+
test "returns 0.0 for any valid standardized t-distribution" do
22+
{:ok, t} = StandardizedT.new(3.0)
23+
assert StandardizedT.mean(t) == 0.0
2824

29-
test "returns :undefined when df = 1" do
30-
{:ok, t} = T.new(5.0, 1.0, 1.0)
31-
assert T.mean(t) == :undefined
32-
end
33-
34-
test "returns :undefined when df < 1" do
35-
{:ok, t} = T.new(5.0, 1.0, 0.5)
36-
assert T.mean(t) == :undefined
25+
{:ok, t} = StandardizedT.new(10.0)
26+
assert StandardizedT.mean(t) == 0.0
3727
end
3828
end
3929

4030
describe "variance/1" do
4131
test "returns a finite variance when df > 2" do
42-
{:ok, t} = T.new(0.0, 2.0, 5.0)
43-
expected_variance = 5.0 / (5.0 - 2.0) * (2.0 * 2.0)
44-
assert TestHelper.assert_in_delta(T.variance(t), expected_variance)
32+
{:ok, t} = StandardizedT.new(5.0)
33+
expected_variance = 5.0 / (5.0 - 2.0)
34+
assert TestHelper.assert_in_delta(StandardizedT.variance(t), expected_variance)
4535
end
4636

4737
test "returns :infinity when 1 < df ≤ 2" do
48-
{:ok, t} = T.new(0.0, 1.0, 1.5)
49-
assert T.variance(t) == :infinity
50-
end
51-
52-
test "returns :undefined when df ≤ 1" do
53-
{:ok, t} = T.new(0.0, 1.0, 1.0)
54-
assert T.variance(t) == :undefined
38+
{:ok, t} = StandardizedT.new(1.5)
39+
assert StandardizedT.variance(t) == :infinity
5540
end
5641
end
5742

5843
describe "pdf/2" do
5944
test "computes valid PDF values" do
60-
{:ok, t} = T.new(0.0, 1.0, 5.0)
61-
assert TestHelper.assert_in_delta(T.pdf(t, 0.0), 0.37960669, 1.0e-6)
62-
assert TestHelper.assert_in_delta(T.pdf(t, 1.0), 0.219679797, 1.0e-6)
45+
{:ok, t} = StandardizedT.new(5.0)
46+
assert TestHelper.assert_in_delta(StandardizedT.pdf(t, 0.0), 0.37960669, 1.0e-6)
47+
assert TestHelper.assert_in_delta(StandardizedT.pdf(t, 1.0), 0.219679797, 1.0e-6)
6348
end
6449
end
6550

6651
describe "cdf/2" do
6752
test "computes valid CDF values" do
68-
{:ok, t} = T.new(0.0, 1.0, 5.0)
53+
{:ok, t} = StandardizedT.new(5.0)
54+
55+
assert TestHelper.assert_in_delta(StandardizedT.cdf(t, 0.0), 0.5, 1.0e-9)
56+
assert TestHelper.assert_in_delta(StandardizedT.cdf(t, -100.0), 0.0, 1.0e-9)
57+
assert TestHelper.assert_in_delta(StandardizedT.cdf(t, 100.0), 1.0, 1.0e-9)
58+
end
59+
end
60+
61+
describe "sf/2" do
62+
test "computes valid SF values" do
63+
{:ok, t} = StandardizedT.new(5.0)
6964

70-
assert TestHelper.assert_in_delta(T.cdf(t, 0.0), 0.5, 1.0e-9)
71-
assert TestHelper.assert_in_delta(T.cdf(t, -100.0), 0.0, 1.0e-9)
72-
assert TestHelper.assert_in_delta(T.cdf(t, 100.0), 1.0, 1.0e-9)
65+
assert TestHelper.assert_in_delta(StandardizedT.sf(t, 0.0), 0.5, 1.0e-9)
66+
assert TestHelper.assert_in_delta(StandardizedT.sf(t, -100.0), 1.0, 1.0e-9)
67+
assert TestHelper.assert_in_delta(StandardizedT.sf(t, 100.0), 0.0, 1.0e-9)
7368
end
7469
end
7570
end
71+

0 commit comments

Comments
 (0)