mc_forecast 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 387e653e97a687f2ab7618e2612c38fae72ddb5bc87485b44998005cab548d3d
4
- data.tar.gz: 161aca02d0cb2ded0c143908ee9d4747078bd73888974d026b08d8039ccb117a
3
+ metadata.gz: 699d3557e6c13728ecad8b44aa145dc54f2aa6afdf2d007b9ad52429423687be
4
+ data.tar.gz: 3472b10896989b22c91cfdc5213f2b1600dd46057c3d9390b9403a1f048a77b4
5
5
  SHA512:
6
- metadata.gz: f075afc3d8b5c2c234c50fc2b58ea66da44a6bb6cfbc3058982c6a131aa374576b1ec7c953bb1b21f81c0cddba46233be67c8fddc09887049f060db37f903848
7
- data.tar.gz: c0e17a7551d1ba892b7d372e1a1424eda9b8ee3231b68420e6a03e773cde672d6fad125529f99e72d7a1c810aed6b30b6a6ac4e12ebbfa9adfb06a96b8174454
6
+ metadata.gz: 9e9a931a1a95e631313addb817a29296250ce7085240695508c783e0e114c1280b69b1634f3b019d2936fcc99719d9ab69f9cac2382e85c3e36068f541196c0d
7
+ data.tar.gz: ea015bbd39ee328435c08ee99004d08b8ac37e12c41d466fd6520d10935fb91829efc14518c6576ab68dec09e74eeeec3de9907898ab3ecb126c77e180a2036e
@@ -2,7 +2,7 @@ require "deep_dup"
2
2
 
3
3
  module McForecast
4
4
  class Simulation
5
- def run(init_state: nil, trials: 1_000, steps: 1, quantiles: [0.025, 0.975])
5
+ def run(init_state: nil, trials: 1_000, steps: 1, quantiles: [0.025, 0.16, 0.84, 0.975], ranges: [])
6
6
  events = {}
7
7
  (0..trials - 1).each do |trial|
8
8
  state = DeepDup.deep_dup(init_state)
@@ -17,40 +17,77 @@ module McForecast
17
17
  end
18
18
  end
19
19
  end
20
- analyze(events, quantiles)
20
+ analyze(events, quantiles, ranges)
21
21
  end
22
22
 
23
23
  private
24
24
 
25
25
  # Return an analysis of the events, containing:
26
26
  # { event_name:
27
- # { mean: [...], # per step
27
+ # {
28
+ # ranges: {
29
+ # 0..12: { mean: ..., quantiles: { 0.025: ..., 0.975: ... }},
30
+ # 13..24: { mean: ..., quantiles: { 0.025: ..., 0.975: ... }},
31
+ # ...
32
+ # },
33
+ # sum: { mean: ..., quantiles: { 0.025: ..., 0.975: ... }},
34
+ # mean: [...], # per step
28
35
  # quantiles:
29
36
  # { 0.025: [...],
30
37
  # 0.975: [...]
31
38
  # }}}
32
- def analyze(events, quantiles)
39
+ def analyze(events, quantiles, ranges)
33
40
  events.transform_values do |steps| # array(steps) of arrays(trials)
34
- a = if quantiles.any?
35
- # only need to sort if we request answers on any quantiles
36
- steps.map do |trials|
37
- # could avoid sorting with some creativity, but probably fine for our data lengths so far
38
- trials.sort.values_at(*quantile_indices(trials.length, quantiles))
39
- end.transpose # a[step][]
40
- else
41
- []
42
- end
43
-
44
41
  {
45
- mean: steps.map { |trials| Rational(trials.sum || 0, trials.length) },
46
- quantiles: quantiles.zip(a).to_h
42
+ ranges: analyze_ranges(steps, quantiles, ranges),
43
+ sum: sum(steps, quantiles),
44
+ # besides the total sum we may want to have a sum for multiples of our base period
45
+ # (or week/month/quarter/year but that gets a bit complicated)
46
+ mean: steps.map { |trials| (trials.sum || 0).to_f / trials.length },
47
+ quantiles: quantiles.zip(step_quantiles(quantiles, steps)).to_h
47
48
  }
48
49
  end
49
50
  end
50
51
 
51
- def quantile_indices(n_trials, quantiles)
52
+ def analyze_ranges(steps, quantiles, ranges) # rubocop:disable Metrics/AbcSize
53
+ ranges.each_with_object({}) do |range, results|
54
+ range_sums = steps.slice(range).transpose.map(&:sum)
55
+
56
+ results[range] = {
57
+ mean: range_sums.sum.to_f / range_sums.length,
58
+ quantiles: quantiles.zip(
59
+ quantiles.map do |q|
60
+ range_sums.sort[(q * (range_sums.length - 1)).round.clamp(0, range_sums.length - 1)]
61
+ end
62
+ ).to_h
63
+ }
64
+ end
65
+ end
66
+
67
+ def sum(steps, quantiles)
68
+ sums = steps.transpose.map(&:sum) # gives a sum of this event, per trial
69
+ {
70
+ mean: (sums.sum || 0).to_f / steps[0].length,
71
+ # sort all of the sums, and take the elements closest to the chosen quantiles, and then make a nice hash
72
+ quantiles: quantiles.zip(sums.sort.values_at(*quantile_indices(steps[0].length, quantiles))).to_h
73
+ }
74
+ end
75
+
76
+ def step_quantiles(quantiles, steps)
77
+ if quantiles.any?
78
+ # only need to sort if we request answers on any quantiles
79
+ steps.map do |trials|
80
+ # could avoid sorting with some creativity, but probably fine for our data lengths so far
81
+ trials.sort.values_at(*quantile_indices(trials.length, quantiles))
82
+ end.transpose # a[step][]
83
+ else
84
+ []
85
+ end
86
+ end
87
+
88
+ def quantile_indices(count, quantiles)
52
89
  quantiles.map do |q|
53
- (q * (n_trials - 1)).round.to_i.clamp(0, n_trials - 1)
90
+ (q * (count - 1)).round.to_i.clamp(0, count - 1)
54
91
  end
55
92
  end
56
93
  end
@@ -1,3 +1,3 @@
1
1
  module McForecast
2
- VERSION = "0.1.1".freeze
2
+ VERSION = "0.2.1".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mc_forecast
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daan van Vugt
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-08 00:00:00.000000000 Z
11
+ date: 2024-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_dup