Skip to content

Commit c7e4157

Browse files
authored
feat: Implement the Gamma Distribution. (#161)
This change implements the gamma distribution, now that it's possible to have a more precise calculation of the regularised lower incomplete gamma function (called `normalised_lower_incomplete_gamma_function`).
1 parent 8d9ff45 commit c7e4157

2 files changed

Lines changed: 225 additions & 0 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
module RubyStatistics
4+
module Distribution
5+
class Gamma
6+
attr_reader :shape, :scale, :rate
7+
8+
def initialize(shape:, scale: nil)
9+
@shape = shape
10+
@scale = scale
11+
12+
# If the scale is nil, it means we want the distribution to behave with a rate parameter
13+
# instead of a scale parameter
14+
@rate = if scale.nil?
15+
1.0 / shape
16+
else
17+
nil
18+
end
19+
end
20+
21+
def as_rate?
22+
scale.nil?
23+
end
24+
25+
def mean
26+
if as_rate?
27+
self.shape / self.rate
28+
else
29+
self.shape * self.scale
30+
end
31+
end
32+
33+
def mode
34+
return 0.0 if self.shape < 1.0
35+
36+
if as_rate?
37+
(self.shape - 1.0) / self.rate
38+
else
39+
(self.shape - 1.0) * self.scale
40+
end
41+
end
42+
43+
def variance
44+
if as_rate?
45+
self.shape / (self.rate ** 2.0)
46+
else
47+
self.shape * (self.scale ** 2.0)
48+
end
49+
end
50+
51+
def skewness
52+
2.0 / Math.sqrt(self.shape)
53+
end
54+
55+
def density_function(x)
56+
euler = if as_rate?
57+
Math.exp(- self.rate * x)
58+
else
59+
Math.exp(-x / self.scale.to_r)
60+
end
61+
62+
left = if as_rate?
63+
(self.rate ** self.shape).to_r / Math.gamma(self.shape).to_r
64+
else
65+
1r / (Math.gamma(self.shape).to_r * (self.scale ** self.shape).to_r)
66+
end
67+
68+
left * (x ** (self.shape - 1)) * euler
69+
end
70+
71+
def cumulative_function(x)
72+
upper = if as_rate?
73+
self.rate * x.to_r
74+
else
75+
x / self.scale.to_r
76+
end
77+
78+
# left = 1.0 / Math.gamma(self.shape)
79+
# right = Math.lower_incomplete_gamma_function(self.shape, upper)
80+
# left * right
81+
Math.normalised_lower_incomplete_gamma_function(self.shape, upper)
82+
end
83+
end
84+
end
85+
end
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RubyStatistics::Distribution::Gamma do
6+
let(:shape_p) { 4 }
7+
let(:scale_p) { 2 }
8+
let(:rate_p) { 1.0 / shape_p }
9+
let(:rate_gamma) { described_class.new(shape: shape_p) }
10+
let(:scale_gamma) { described_class.new(shape: shape_p, scale: scale_p) }
11+
12+
before do
13+
# so we can check that the rate/scale attr has been called
14+
allow(rate_gamma).to receive(:rate).and_call_original
15+
allow(scale_gamma).to receive(:scale).and_call_original
16+
end
17+
18+
describe '#as_rate?' do
19+
it 'is true when no scale parameter is defined' do
20+
expect(described_class.new(shape: 1)).to be_as_rate
21+
end
22+
23+
it 'is false when a scale parameter is defined' do
24+
expect(described_class.new(shape: 1, scale: 1)).to_not be_as_rate
25+
end
26+
end
27+
28+
describe '#mean' do
29+
context 'when the scale parameter is defined' do
30+
it 'returns the expected value using the scale parameter' do
31+
expect(scale_gamma.mean).to eq shape_p * scale_p
32+
expect(scale_gamma).to have_received(:scale).at_least(:once)
33+
end
34+
end
35+
36+
context 'when the rate parameter is used' do
37+
it 'returns the expected value using the rate parameter' do
38+
expect(rate_gamma.mean).to eq shape_p / rate_p
39+
expect(rate_gamma).to have_received(:rate).once
40+
end
41+
end
42+
end
43+
44+
describe '#mode' do
45+
it 'returns zero if the shape is zero' do
46+
expect(described_class.new(shape: 0).mode).to be_zero
47+
end
48+
49+
it 'returns zero if the shape is negative' do
50+
expect(described_class.new(shape: -1).mode).to be_zero
51+
end
52+
53+
context 'when the scale parameter is defined' do
54+
it 'returns the expected calculations using the scale parameter' do
55+
expect(scale_gamma.mode).to eq (shape_p - 1.0) * scale_p
56+
expect(scale_gamma).to have_received(:scale).at_least(:once)
57+
end
58+
end
59+
60+
context 'when the rate parameter is used' do
61+
it 'returns the expected calculations using the rate parameter' do
62+
expect(rate_gamma.mode).to eq (shape_p - 1.0) / rate_p
63+
expect(rate_gamma).to have_received(:rate).at_least(:once)
64+
end
65+
end
66+
end
67+
68+
describe '#variance' do
69+
context 'when the scale parameter is defined' do
70+
it 'returns the expected calculations using the scale parameter' do
71+
expect(scale_gamma.variance).to eq shape_p * (scale_p ** 2.0)
72+
expect(scale_gamma).to have_received(:scale).at_least(:once)
73+
end
74+
end
75+
76+
context 'when the rate parameter is used' do
77+
it 'returns the expected calculations using the rate parameter' do
78+
expect(rate_gamma.variance).to eq shape_p / (rate_p ** 2.0)
79+
expect(rate_gamma).to have_received(:rate).at_least(:once)
80+
end
81+
end
82+
end
83+
84+
describe '#skewness' do
85+
it 'returns the expected calculation for both types of gamma definitions' do
86+
skewness = 2.0 / Math.sqrt(shape_p)
87+
88+
expect(rate_gamma.skewness).to eq(skewness)
89+
expect(scale_gamma.skewness).to eq(skewness)
90+
end
91+
end
92+
93+
describe '#density_function' do
94+
context 'when the scale parameter is defined' do
95+
it 'returns the excepted calculations' do
96+
# Values extracted using dgamma(x, shape = shape_p, scale = scale_p) in R 4.4.1
97+
values = [0.006318028, 0.03065662, 0.06275536, 0.09022352, 0.1068815]
98+
99+
values.each_with_index do |value, index|
100+
expect(scale_gamma.density_function(index + 1)).to be_within(0.0000001).of(value)
101+
end
102+
end
103+
end
104+
105+
context 'when the rate parameter is used' do
106+
it 'returns the expected calculations' do
107+
# Values extracted using dgamma(x, shape = shape_p, rate = rate_p) in R 4.4.1
108+
values = [0.0005070318, 0.003159014, 0.008303318, 0.01532831, 0.02331582]
109+
110+
values.each_with_index do |value, index|
111+
expect(rate_gamma.density_function(index + 1)).to be_within(0.0000001).of(value)
112+
end
113+
end
114+
end
115+
end
116+
117+
describe '#cumulative_function' do
118+
context 'when the scale parameter is defined' do
119+
it 'returns the expected calculations' do
120+
# Values extracted using pgamma(x, shape = shape_p, scale = scale_p) in R 4.4.1
121+
values = [0.001751623, 0.01898816, 0.06564245, 0.1428765, 0.2424239]
122+
123+
values.each_with_index do |value, index|
124+
expect(scale_gamma.cumulative_function(index + 1)).to be_within(0.0000001).of(value)
125+
end
126+
end
127+
end
128+
129+
context 'when the rate parameter is used' do
130+
it 'returns the expected calculations' do
131+
# Values extracted using pgamma(x, shape = shape_p, rate = rate_p) in R 4.4.1
132+
values = [0.0001333697, 0.001751623, 0.007292167, 0.01898816, 0.03826905]
133+
134+
values.each_with_index do |value, index|
135+
expect(rate_gamma.cumulative_function(index + 1)).to be_within(0.0000001).of(value)
136+
end
137+
end
138+
end
139+
end
140+
end

0 commit comments

Comments
 (0)