File: normal.rb

package info (click to toggle)
ruby-statistics 2.1.1-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid, trixie
  • size: 224 kB
  • sloc: ruby: 989; sh: 4; makefile: 4
file content (139 lines) | stat: -rw-r--r-- 4,205 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
module Statistics
  module Distribution
    class Normal
      attr_accessor :mean, :standard_deviation, :variance
      alias_method :mode, :mean

      def initialize(avg, std)
        self.mean = avg.to_f
        self.standard_deviation = std.to_f
        self.variance = std.to_f**2
      end

      def cumulative_function(value)
        (1/2.0) * (1.0 + Math.erf((value - mean)/(standard_deviation * Math.sqrt(2.0))))
      end

      def density_function(value)
        return 0 if standard_deviation <= 0

        up_right = (value - mean)**2.0
        down_right = 2.0 * variance
        right = Math.exp(-(up_right/down_right))
        left_down = Math.sqrt(2.0 * Math::PI * variance)
        left_up = 1.0

        (left_up/(left_down) * right)
      end

      ## Marsaglia polar method implementation for random gaussian (normal) number generation.
      # References:
      # https://en.wikipedia.org/wiki/Marsaglia_polar_method
      # https://math.stackexchange.com/questions/69245/transform-uniform-distribution-to-normal-distribution-using-lindeberg-l%C3%A9vy-clt
      # https://www.projectrhea.org/rhea/index.php/The_principles_for_how_to_generate_random_samples_from_a_Gaussian_distribution

      def random(elements: 1, seed: Random.new_seed)
        results = []

        # Setup seed
        srand(seed)

        # Number of random numbers to be generated.
        elements.times do
          x, y, r = 0.0, 0.0, 0.0

          # Find an (x, y) point in the x^2 + y^2 < 1 circumference.
          loop do
            x = 2.0 * rand - 1.0
            y = 2.0 * rand - 1.0

            r = (x ** 2) + (y ** 2)

            break unless r >= 1.0 || r == 0
          end

          # Project the random point to the required random distance
          r = Math.sqrt(-2.0 * Math.log(r) / r)

          # Transform the random distance to a gaussian value and append it to the results array
          results << mean + x * r * standard_deviation
        end

        if elements == 1
          results.first
        else
          results
        end
      end
    end

    class StandardNormal < Normal
      def initialize
        super(0, 1) # Mean = 0, Std = 1
      end

      def density_function(value)
        pow = (value**2)/2.0
        euler = Math.exp(-pow)

        euler/Math.sqrt(2 * Math::PI)
      end
    end

    # Inverse Standard Normal distribution:
    # References:
    # https://en.wikipedia.org/wiki/Inverse_distribution
    # http://www.source-code.biz/snippets/vbasic/9.htm
    class InverseStandardNormal < StandardNormal
      A1 = -39.6968302866538
      A2 = 220.946098424521
      A3 = -275.928510446969
      A4 = 138.357751867269
      A5 = -30.6647980661472
      A6 = 2.50662827745924
      B1 = -54.4760987982241
      B2 = 161.585836858041
      B3 = -155.698979859887
      B4 = 66.8013118877197
      B5 = -13.2806815528857
      C1 = -7.78489400243029E-03
      C2 = -0.322396458041136
      C3 = -2.40075827716184
      C4 = -2.54973253934373
      C5 = 4.37466414146497
      C6 = 2.93816398269878
      D1 = 7.78469570904146E-03
      D2 = 0.32246712907004
      D3 = 2.445134137143
      D4 = 3.75440866190742
      P_LOW = 0.02425
      P_HIGH = 1 - P_LOW

      def density_function(_)
        raise NotImplementedError
      end

      def random(elements: 1, seed: Random.new_seed)
        raise NotImplementedError
      end

      def cumulative_function(value)
        return if value < 0.0 || value > 1.0
        return -1.0 * Float::INFINITY if value.zero?
        return Float::INFINITY if value == 1.0

        if value < P_LOW
          q = Math.sqrt((Math.log(value) * -2.0))
          (((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6) / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1.0)
        elsif value <= P_HIGH
          q = value - 0.5
          r = q ** 2
          (((((A1 * r + A2) * r + A3) * r + A4) * r + A5) * r + A6) * q / (((((B1 * r + B2) * r + B3) * r + B4) * r + B5) * r + 1.0)
        else
          q = Math.sqrt((Math.log(1 - value) * -2.0))
          - (((((C1 * q + C2) * q + C3) * q + C4) * q + C5) * q + C6) / ((((D1 * q + D2) * q + D3) * q + D4) * q + 1)
        end
      end
    end
  end
end