Skip to content

Commit 4ca57b1

Browse files
authored
Add more discrete distributions (#25)
* distributions: discrete: Negative binomial. Add negative binomial distribution and unit tests. * distribution: discrete: Geometric distribution. This change adds the geometric distribution to the list of discrete functions. * distribution: discrete: Bernoulli distribution. * distribution: discrete: LogSeries distribution. * gem: Bump to version 2.0.5. * distribution: beta: Add uncovered case where alpha + beta == 0.
1 parent c131d90 commit 4ca57b1

10 files changed

Lines changed: 620 additions & 2 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module Statistics
2+
module Distribution
3+
class Bernoulli
4+
def self.density_function(n, p)
5+
return if n != 0 && n != 1 # The support of the distribution is n = {0, 1}.
6+
7+
case n
8+
when 0 then 1.0 - p
9+
when 1 then p
10+
end
11+
end
12+
13+
def self.cumulative_function(n, p)
14+
return if n != 0 && n != 1 # The support of the distribution is n = {0, 1}.
15+
16+
case n
17+
when 0 then 1.0 - p
18+
when 1 then 1.0
19+
end
20+
end
21+
22+
def self.variance(p)
23+
p * (1.0 - p)
24+
end
25+
26+
def self.skewness(p)
27+
(1.0 - 2.0*p).to_f / Math.sqrt(p * (1.0 - p))
28+
end
29+
30+
def self.kurtosis(p)
31+
(6.0 * (p ** 2) - (6 * p) + 1) / (p * (1.0 - p))
32+
end
33+
end
34+
end
35+
end
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Statistics
2+
module Distribution
3+
class Geometric
4+
attr_accessor :probability_of_success, :always_success_allowed
5+
6+
def initialize(p, always_success: false)
7+
self.probability_of_success = p.to_f
8+
self.always_success_allowed = always_success
9+
end
10+
11+
def density_function(k)
12+
k = k.to_i
13+
14+
if always_success_allowed
15+
return if k < 0
16+
17+
((1.0 - probability_of_success) ** k) * probability_of_success
18+
else
19+
return if k <= 0
20+
21+
((1.0 - probability_of_success) ** (k - 1.0)) * probability_of_success
22+
end
23+
end
24+
25+
def cumulative_function(k)
26+
k = k.to_i
27+
28+
if always_success_allowed
29+
return if k < 0
30+
31+
1.0 - ((1.0 - probability_of_success) ** (k + 1.0))
32+
else
33+
return if k <= 0
34+
35+
1.0 - ((1.0 - probability_of_success) ** k)
36+
end
37+
end
38+
39+
def mean
40+
if always_success_allowed
41+
(1.0 - probability_of_success) / probability_of_success
42+
else
43+
1.0 / probability_of_success
44+
end
45+
end
46+
47+
def median
48+
if always_success_allowed
49+
(-1.0 / Math.log2(1.0 - probability_of_success)).ceil - 1.0
50+
else
51+
(-1.0 / Math.log2(1.0 - probability_of_success)).ceil
52+
end
53+
end
54+
55+
def mode
56+
if always_success_allowed
57+
0.0
58+
else
59+
1.0
60+
end
61+
end
62+
63+
def variance
64+
(1.0 - probability_of_success) / (probability_of_success ** 2)
65+
end
66+
67+
def skewness
68+
(2.0 - probability_of_success) / Math.sqrt(1.0 - probability_of_success)
69+
end
70+
71+
def kurtosis
72+
6.0 + ((probability_of_success ** 2) / (1.0 - probability_of_success))
73+
end
74+
end
75+
end
76+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module Statistics
2+
module Distribution
3+
class LogSeries
4+
def self.density_function(k, p)
5+
return if k <= 0
6+
k = k.to_i
7+
8+
left = (-1.0 / Math.log(1.0 - p))
9+
right = (p ** k).to_f
10+
11+
left * right / k
12+
end
13+
14+
def self.cumulative_function(k, p)
15+
return if k <= 0
16+
17+
# Sadly, the incomplete beta function is converging
18+
# too fast to zero and breaking the calculation on logs.
19+
# So, we default to the basic definition of the CDF which is
20+
# the integral (-Inf, K) of the PDF, with P(X <= x) which can
21+
# be solved as a summation of all PDFs from 1 to K. Note that the summation approach
22+
# only applies to discrete distributions.
23+
#
24+
# right = Math.incomplete_beta_function(p, (k + 1).floor, 0) / Math.log(1.0 - p)
25+
# 1.0 + right
26+
27+
result = 0.0
28+
1.upto(k) do |number|
29+
result += self.density_function(number, p)
30+
end
31+
32+
result
33+
end
34+
35+
def self.mode
36+
1.0
37+
end
38+
39+
def self.mean(p)
40+
(-1.0 / Math.log(1.0 - p)) * (p / (1.0 - p))
41+
end
42+
43+
def self.variance(p)
44+
up = p + Math.log(1.0 - p)
45+
down = ((1.0 - p) ** 2) * (Math.log(1.0 - p) ** 2)
46+
47+
(-1.0 * p) * (up / down.to_f)
48+
end
49+
end
50+
end
51+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
module Statistics
2+
module Distribution
3+
class NegativeBinomial
4+
attr_accessor :number_of_failures, :probability_per_trial
5+
6+
def initialize(r, p)
7+
self.number_of_failures = r.to_i
8+
self.probability_per_trial = p
9+
end
10+
11+
def probability_mass_function(k)
12+
return if number_of_failures < 0 || k < 0 || k > number_of_failures
13+
14+
left = Math.combination(k + number_of_failures - 1, k)
15+
right = ((1 - probability_per_trial) ** number_of_failures) * (probability_per_trial ** k)
16+
17+
left * right
18+
end
19+
20+
def cumulative_function(k)
21+
return if k < 0 || k > number_of_failures
22+
k = k.to_i
23+
24+
1.0 - Math.incomplete_beta_function(probability_per_trial, k + 1, number_of_failures)
25+
end
26+
27+
def mean
28+
(probability_per_trial * number_of_failures)/(1 - probability_per_trial).to_f
29+
end
30+
31+
def variance
32+
(probability_per_trial * number_of_failures)/((1 - probability_per_trial) ** 2).to_f
33+
end
34+
35+
def skewness
36+
(1 + probability_per_trial).to_f / Math.sqrt(probability_per_trial * number_of_failures)
37+
end
38+
39+
def mode
40+
if number_of_failures > 1
41+
up = probability_per_trial * (number_of_failures - 1)
42+
down = (1 - probability_per_trial).to_f
43+
44+
(up/down).floor
45+
elsif number_of_failures <= 1
46+
0.0
47+
end
48+
end
49+
end
50+
end
51+
end

lib/statistics/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Statistics
2-
VERSION = "2.0.4"
2+
VERSION = "2.0.5"
33
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
require 'spec_helper'
2+
3+
describe Statistics::Distribution::Bernoulli do
4+
describe '.density_function' do
5+
it 'is not defined when the outcome is different from zero or one' do
6+
expect(described_class.density_function(rand(2..10), rand)).to be_nil
7+
expect(described_class.density_function(rand(-5..-1), rand)).to be_nil
8+
end
9+
10+
it 'returns the expected value when the outcome is zero' do
11+
p = rand
12+
expect(described_class.density_function(0, p)).to eq (1.0 - p)
13+
end
14+
15+
it 'returns the expected value when the outcome is one' do
16+
p = rand
17+
expect(described_class.density_function(1, p)).to eq p
18+
end
19+
end
20+
21+
describe '.cumulative_function' do
22+
it 'is not defined when the outcome is different from zero or one' do
23+
expect(described_class.cumulative_function(rand(2..10), rand)).to be_nil
24+
expect(described_class.density_function(rand(-5..-1), rand)).to be_nil
25+
end
26+
27+
it 'returns the expected value when the outcome is zero' do
28+
p = rand
29+
expect(described_class.cumulative_function(0, p)).to eq (1.0 - p)
30+
end
31+
32+
it 'returns the expected value when the outcome is one' do
33+
expect(described_class.cumulative_function(1, rand)).to eq 1.0
34+
end
35+
end
36+
37+
describe '.variance' do
38+
it 'returns the expected value for the bernoulli distribution' do
39+
p = rand
40+
41+
expect(described_class.variance(p)).to eq p * (1.0 - p)
42+
end
43+
end
44+
45+
describe '.skewness' do
46+
it 'returns the expected value for the bernoulli distribution' do
47+
p = rand
48+
expected_value = (1.0 - 2.0*p).to_f / Math.sqrt(p * (1.0 - p))
49+
50+
expect(described_class.skewness(p)).to eq expected_value
51+
end
52+
end
53+
54+
describe '.kurtosis' do
55+
it 'returns the expected value for the bernoulli distribution' do
56+
p = rand
57+
expected_value = (6.0 * (p ** 2) - (6 * p) + 1) / (p * (1.0 - p))
58+
59+
expect(described_class.kurtosis(p)).to eq expected_value
60+
end
61+
end
62+
end

spec/statistics/distribution/beta_spec.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,21 @@
6262
expect(described_class.new(alpha, beta).mean).to be_nil
6363
end
6464

65+
it 'returns nil if the sum of alpha and beta is zero' do
66+
alpha = -1
67+
beta = 1
68+
69+
expect(described_class.new(alpha, beta).mean).to be_nil
70+
end
71+
6572
it 'calculates the expected mean for the beta distribution' do
6673
alpha = rand(-5..5)
6774
beta = rand(-5..5)
6875

69-
alpha = 1 if alpha + beta == 0 # To avoid NaN results.
76+
if alpha + beta == 0 # To avoid NaN results.
77+
alpha = 1
78+
beta = 1
79+
end
7080

7181
expect(described_class.new(alpha, beta).mean).to eq alpha.to_f/(alpha.to_f + beta.to_f)
7282
end

0 commit comments

Comments
 (0)