prophet-rb 0.1.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8cdf7fd6b309bc24d83bb12e4ad87ba1ba36ba0c6abc803485f11a5718ab649
4
- data.tar.gz: d63c201452d57ef99349f3bf8b51249863cadbc555daef6a1f122d2ede571fdb
3
+ metadata.gz: 756e42a4e2c39e114610d2f41e2403d9b160e77da02c0483e43b4bf460dd828b
4
+ data.tar.gz: e7bce55f47410227131afcba9d2f90c9812dab093efd1c17465383b3a5e6dc73
5
5
  SHA512:
6
- metadata.gz: 81991c4edf0fa86d34e876ff8ebf2344491ae10dde95d736bbae3987592e7d419dc5167bf2b390eec19a88700152fff8c8f7eadb18f2d81fcfa928c9172c1ee8
7
- data.tar.gz: 17e146ec6c6f74c792ac6da8bcd1103ad34df8303c7c76961ef956fcdbd0b343f14ff6a97ec4774c4d813dcfd07f5c3a1404365f24f27a636450f2bed9e895b0
6
+ metadata.gz: ec014f32ff39abd49195d7e9a7f38b3b297bc7f6fc28da3d1be6fb19b6edbe816a681c4fbb35d5ea79fc2ced0aa948b0468cff9f6d9d8293590761711944b71b
7
+ data.tar.gz: 87ecc8706c73e7f1063c8d75cc5c687ebc3fddb92d89e60bb115d21c90a60fbb6172648a0d414baeb11f04572b37e06777d4100acb07b8559dea8e8a711741fb
@@ -1,3 +1,27 @@
1
+ ## 0.2.3 (2020-10-14)
2
+
3
+ - Added support for times to `forecast` method
4
+
5
+ ## 0.2.2 (2020-07-26)
6
+
7
+ - Fixed error with constant series
8
+ - Fixed error with no changepoints
9
+
10
+ ## 0.2.1 (2020-07-15)
11
+
12
+ - Added `forecast` method
13
+
14
+ ## 0.2.0 (2020-05-13)
15
+
16
+ - Switched from Daru to Rover
17
+
18
+ ## 0.1.1 (2020-04-10)
19
+
20
+ - Added `add_changepoints_to_plot`
21
+ - Fixed error with `changepoints` option
22
+ - Fixed error with `mcmc_samples` option
23
+ - Fixed error with additional regressors
24
+
1
25
  ## 0.1.0 (2020-04-09)
2
26
 
3
27
  - First release
@@ -1,7 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Andrew Kane
4
3
  Copyright (c) Facebook, Inc. and its affiliates.
4
+ Copyright (c) 2020 Andrew Kane
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining
7
7
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -10,6 +10,8 @@ Supports:
10
10
 
11
11
  And gracefully handles missing data
12
12
 
13
+ [![Build Status](https://travis-ci.org/ankane/prophet.svg?branch=master)](https://travis-ci.org/ankane/prophet) [![Build status](https://ci.appveyor.com/api/projects/status/8ahmsvvhum4ivnmv/branch/master?svg=true)](https://ci.appveyor.com/project/ankane/prophet/branch/master)
14
+
13
15
  ## Installation
14
16
 
15
17
  Add this line to your application’s Gemfile:
@@ -18,19 +20,47 @@ Add this line to your application’s Gemfile:
18
20
  gem 'prophet-rb'
19
21
  ```
20
22
 
21
- ## Documentation
23
+ ## Simple API
24
+
25
+ Get future predictions for a time series
26
+
27
+ ```ruby
28
+ series = {
29
+ Date.parse("2020-01-01") => 100,
30
+ Date.parse("2020-01-02") => 150,
31
+ Date.parse("2020-01-03") => 136,
32
+ # ...
33
+ }
34
+
35
+ Prophet.forecast(series)
36
+ ```
37
+
38
+ Specify the number of predictions to return
39
+
40
+ ```ruby
41
+ Prophet.forecast(series, count: 3)
42
+ ```
43
+
44
+ Works great with [Groupdate](https://github.com/ankane/groupdate)
45
+
46
+ ```ruby
47
+ series = User.group_by_day(:created_at).count
48
+ Prophet.forecast(series)
49
+ ```
50
+
51
+ ## Advanced API
22
52
 
23
- Check out the [Prophet documentation](https://facebook.github.io/prophet/docs/quick_start.html) for a great explanation of all of the features. The Ruby API follows the Python API and supports the same features.
53
+ Check out the [Prophet documentation](https://facebook.github.io/prophet/docs/quick_start.html) for a great explanation of all of the features. The advanced API follows the Python API and supports the same features. It uses [Rover](https://github.com/ankane/rover) for data frames.
24
54
 
25
- ## Quick Start
55
+ ## Advanced Quick Start
26
56
 
27
57
  [Explanation](https://facebook.github.io/prophet/docs/quick_start.html)
28
58
 
29
59
  Create a data frame with `ds` and `y` columns - here’s [an example](examples/example_wp_log_peyton_manning.csv) you can use
30
60
 
31
61
  ```ruby
32
- df = Daru::DataFrame.from_csv("example_wp_log_peyton_manning.csv")
33
- df.head(5)
62
+ df = Rover.read_csv("example_wp_log_peyton_manning.csv")
63
+ df.head
34
64
  ```
35
65
 
36
66
  ds | y
@@ -52,7 +82,7 @@ Make a data frame with a `ds` column for future predictions
52
82
 
53
83
  ```ruby
54
84
  future = m.make_future_dataframe(periods: 365)
55
- future.tail(5)
85
+ future.tail
56
86
  ```
57
87
 
58
88
  ds |
@@ -67,7 +97,7 @@ Make predictions
67
97
 
68
98
  ```ruby
69
99
  forecast = m.predict(future)
70
- forecast["ds", "yhat", "yhat_lower", "yhat_upper"].tail(5)
100
+ forecast[["ds", "yhat", "yhat_lower", "yhat_upper"]].tail
71
101
  ```
72
102
 
73
103
  ds | yhat | yhat_lower | yhat_upper
@@ -88,7 +118,7 @@ Plot the forecast
88
118
  m.plot(forecast).savefig("forecast.png")
89
119
  ```
90
120
 
91
- ![Forecast](https://blazer.dokkuapp.com/assets/prophet/forecast-a9d43195b8ad23703eda7bb8b52b8a758efb4699e2313f32d7bbdfaa2f4275f6.png)
121
+ ![Forecast](https://blazer.dokkuapp.com/assets/prophet/forecast-77cf453fda67d1b462c6c22aee3a02572203b71c4517fedecc1f438cd374a876.png)
92
122
 
93
123
  Plot components
94
124
 
@@ -96,7 +126,46 @@ Plot components
96
126
  m.plot_components(forecast).savefig("components.png")
97
127
  ```
98
128
 
99
- ![Components](https://blazer.dokkuapp.com/assets/prophet/components-b9e31bfcf77e57bbd503c0bcff5e5544e66085b90709b06dd96c5f622a87d84f.png)
129
+ ![Components](https://blazer.dokkuapp.com/assets/prophet/components-2cdd260e23bc89824ecca25f6bfe394deb5821d60b7e0e551469c90d204acd67.png)
130
+
131
+ ## Saturating Forecasts
132
+
133
+ [Explanation](https://facebook.github.io/prophet/docs/saturating_forecasts.html)
134
+
135
+ Forecast logistic growth instead of linear
136
+
137
+ ```ruby
138
+ df = Rover.read_csv("example_wp_log_R.csv")
139
+ df["cap"] = 8.5
140
+ m = Prophet.new(growth: "logistic")
141
+ m.fit(df)
142
+ future = m.make_future_dataframe(periods: 365)
143
+ future["cap"] = 8.5
144
+ forecast = m.predict(future)
145
+ ```
146
+
147
+ ## Trend Changepoints
148
+
149
+ [Explanation](https://facebook.github.io/prophet/docs/trend_changepoints.html)
150
+
151
+ Plot changepoints
152
+
153
+ ```ruby
154
+ fig = m.plot(forecast)
155
+ m.add_changepoints_to_plot(fig.gca, forecast)
156
+ ```
157
+
158
+ Adjust trend flexibility
159
+
160
+ ```ruby
161
+ m = Prophet.new(changepoint_prior_scale: 0.5)
162
+ ```
163
+
164
+ Specify the location of changepoints
165
+
166
+ ```ruby
167
+ m = Prophet.new(changepoints: ["2014-01-01"])
168
+ ```
100
169
 
101
170
  ## Holidays and Special Events
102
171
 
@@ -105,21 +174,21 @@ m.plot_components(forecast).savefig("components.png")
105
174
  Create a data frame with `holiday` and `ds` columns. Include all occurrences in your past data and future occurrences you’d like to forecast.
106
175
 
107
176
  ```ruby
108
- playoffs = Daru::DataFrame.new(
109
- "holiday" => ["playoff"] * 14,
177
+ playoffs = Rover::DataFrame.new(
178
+ "holiday" => "playoff",
110
179
  "ds" => ["2008-01-13", "2009-01-03", "2010-01-16",
111
180
  "2010-01-24", "2010-02-07", "2011-01-08",
112
181
  "2013-01-12", "2014-01-12", "2014-01-19",
113
182
  "2014-02-02", "2015-01-11", "2016-01-17",
114
183
  "2016-01-24", "2016-02-07"],
115
- "lower_window" => [0] * 14,
116
- "upper_window" => [1] * 14
184
+ "lower_window" => 0,
185
+ "upper_window" => 1
117
186
  )
118
- superbowls = Daru::DataFrame.new(
119
- "holiday" => ["superbowl"] * 3,
187
+ superbowls = Rover::DataFrame.new(
188
+ "holiday" => "superbowl",
120
189
  "ds" => ["2010-02-07", "2014-02-02", "2016-02-07"],
121
- "lower_window" => [0] * 3,
122
- "upper_window" => [1] * 3
190
+ "lower_window" => 0,
191
+ "upper_window" => 1
123
192
  )
124
193
  holidays = playoffs.concat(superbowls)
125
194
 
@@ -141,7 +210,25 @@ Specify custom seasonalities
141
210
  m = Prophet.new(weekly_seasonality: false)
142
211
  m.add_seasonality(name: "monthly", period: 30.5, fourier_order: 5)
143
212
  forecast = m.fit(df).predict(future)
144
- m.plot_components(forecast).savefig("components.png")
213
+ ```
214
+
215
+ Specify additional regressors
216
+
217
+ ```ruby
218
+ nfl_sunday = lambda do |ds|
219
+ date = ds.respond_to?(:to_date) ? ds.to_date : Date.parse(ds)
220
+ date.wday == 0 && (date.month > 8 || date.month < 2) ? 1 : 0
221
+ end
222
+
223
+ df["nfl_sunday"] = df["ds"].map(&nfl_sunday)
224
+
225
+ m = Prophet.new
226
+ m.add_regressor("nfl_sunday")
227
+ m.fit(df)
228
+
229
+ future["nfl_sunday"] = future["ds"].map(&nfl_sunday)
230
+
231
+ forecast = m.predict(future)
145
232
  ```
146
233
 
147
234
  ## Multiplicative Seasonality
@@ -149,13 +236,27 @@ m.plot_components(forecast).savefig("components.png")
149
236
  [Explanation](https://facebook.github.io/prophet/docs/multiplicative_seasonality.html)
150
237
 
151
238
  ```ruby
152
- df = Daru::DataFrame.from_csv("example_air_passengers.csv")
239
+ df = Rover.read_csv("example_air_passengers.csv")
153
240
  m = Prophet.new(seasonality_mode: "multiplicative")
154
241
  m.fit(df)
155
242
  future = m.make_future_dataframe(periods: 50, freq: "MS")
156
243
  forecast = m.predict(future)
157
244
  ```
158
245
 
246
+ ## Uncertainty Intervals
247
+
248
+ Specify the width of uncertainty intervals (80% by default)
249
+
250
+ ```ruby
251
+ Prophet.new(interval_width: 0.95)
252
+ ```
253
+
254
+ Get uncertainty in seasonality
255
+
256
+ ```ruby
257
+ Prophet.new(mcmc_samples: 300)
258
+ ```
259
+
159
260
  ## Non-Daily Data
160
261
 
161
262
  [Explanation](https://facebook.github.io/prophet/docs/non-daily_data.html)
@@ -163,17 +264,25 @@ forecast = m.predict(future)
163
264
  Sub-daily data
164
265
 
165
266
  ```ruby
166
- df = Daru::DataFrame.from_csv("example_yosemite_temps.csv")
267
+ df = Rover.read_csv("example_yosemite_temps.csv")
167
268
  m = Prophet.new(changepoint_prior_scale: 0.01).fit(df)
168
269
  future = m.make_future_dataframe(periods: 300, freq: "H")
169
- fcst = m.predict(future)
170
- m.plot(fcst).savefig("forecast.png")
270
+ forecast = m.predict(future)
171
271
  ```
172
272
 
173
273
  ## Resources
174
274
 
175
275
  - [Forecasting at Scale](https://peerj.com/preprints/3190.pdf)
176
276
 
277
+ ## Upgrading
278
+
279
+ ### 0.2.0
280
+
281
+ Prophet now uses [Rover](https://github.com/ankane/rover) instead of Daru. Two changes you may need to make are:
282
+
283
+ - `Rover.read_csv` instead of `Daru::DataFrame.from_csv`
284
+ - `df[["ds", "yhat"]]` instead of `df["ds", "yhat"]`
285
+
177
286
  ## Credits
178
287
 
179
288
  This library was ported from the [Prophet Python library](https://github.com/facebook/prophet) and is available under the same license.
@@ -1,6 +1,6 @@
1
1
  # dependencies
2
2
  require "cmdstan"
3
- require "daru"
3
+ require "rover"
4
4
  require "numo/narray"
5
5
 
6
6
  # stdlib
@@ -20,4 +20,67 @@ module Prophet
20
20
  def self.new(**kwargs)
21
21
  Forecaster.new(**kwargs)
22
22
  end
23
+
24
+ def self.forecast(series, count: 10)
25
+ raise ArgumentError, "Series must have at least 10 data points" if series.size < 10
26
+
27
+ # check type to determine output format
28
+ # check for before converting to time
29
+ keys = series.keys
30
+ dates = keys.all? { |k| k.is_a?(Date) }
31
+ time_zone = keys.first.time_zone if keys.first.respond_to?(:time_zone)
32
+ utc = keys.first.utc? if keys.first.respond_to?(:utc?)
33
+ times = keys.map(&:to_time)
34
+
35
+ day = times.all? { |t| t.hour == 0 && t.min == 0 && t.sec == 0 && t.nsec == 0 }
36
+ week = day && times.map { |k| k.wday }.uniq.size == 1
37
+ month = day && times.all? { |k| k.day == 1 }
38
+ quarter = month && times.all? { |k| k.month % 3 == 1 }
39
+ year = quarter && times.all? { |k| k.month == 1 }
40
+
41
+ freq =
42
+ if year
43
+ "YS"
44
+ elsif quarter
45
+ "QS"
46
+ elsif month
47
+ "MS"
48
+ elsif week
49
+ "W"
50
+ elsif day
51
+ "D"
52
+ else
53
+ diff = Rover::Vector.new(times).sort.diff.to_numo[1..-1]
54
+ min_diff = diff.min.to_i
55
+
56
+ # could be another common divisor
57
+ # but keep it simple for now
58
+ raise "Unknown frequency" unless (diff % min_diff).eq(0).all?
59
+
60
+ "#{min_diff}S"
61
+ end
62
+
63
+ # use series, not times, so dates are handled correctly
64
+ df = Rover::DataFrame.new({"ds" => series.keys, "y" => series.values})
65
+
66
+ m = Prophet.new
67
+ m.logger.level = ::Logger::FATAL # no logging
68
+ m.fit(df)
69
+
70
+ future = m.make_future_dataframe(periods: count, include_history: false, freq: freq)
71
+ forecast = m.predict(future)
72
+ result = forecast[["ds", "yhat"]].to_a
73
+
74
+ # use the same format as input
75
+ if dates
76
+ result.each { |v| v["ds"] = v["ds"].to_date }
77
+ elsif time_zone
78
+ result.each { |v| v["ds"] = v["ds"].in_time_zone(time_zone) }
79
+ elsif utc
80
+ result.each { |v| v["ds"] = v["ds"].utc }
81
+ else
82
+ result.each { |v| v["ds"] = v["ds"].localtime }
83
+ end
84
+ result.map { |v| [v["ds"], v["yhat"]] }.to_h
85
+ end
23
86
  end
@@ -82,12 +82,12 @@ module Prophet
82
82
  raise ArgumentError, "Parameter \"changepoint_range\" must be in [0, 1]"
83
83
  end
84
84
  if @holidays
85
- if !@holidays.is_a?(Daru::DataFrame) && @holidays.vectors.include?("ds") && @holidays.vectors.include?("holiday")
85
+ if !@holidays.is_a?(Rover::DataFrame) && @holidays.include?("ds") && @holidays.include?("holiday")
86
86
  raise ArgumentError, "holidays must be a DataFrame with \"ds\" and \"holiday\" columns."
87
87
  end
88
88
  @holidays["ds"] = to_datetime(@holidays["ds"])
89
- has_lower = @holidays.vectors.include?("lower_window")
90
- has_upper = @holidays.vectors.include?("upper_window")
89
+ has_lower = @holidays.include?("lower_window")
90
+ has_upper = @holidays.include?("upper_window")
91
91
  if has_lower ^ has_upper # xor
92
92
  raise ArgumentError, "Holidays must have both lower_window and upper_window, or neither"
93
93
  end
@@ -141,7 +141,7 @@ module Prophet
141
141
  end
142
142
 
143
143
  def setup_dataframe(df, initialize_scales: false)
144
- if df.vectors.include?("y")
144
+ if df.include?("y")
145
145
  df["y"] = df["y"].map(&:to_f)
146
146
  raise ArgumentError "Found infinity in column y." unless df["y"].all?(&:finite?)
147
147
  end
@@ -152,18 +152,18 @@ module Prophet
152
152
  raise ArgumentError, "Found NaN in column ds." if df["ds"].any?(&:nil?)
153
153
 
154
154
  @extra_regressors.each_key do |name|
155
- if !df.vectors.include?(name)
155
+ if !df.include?(name)
156
156
  raise ArgumentError, "Regressor #{name.inspect} missing from dataframe"
157
157
  end
158
158
  df[name] = df[name].map(&:to_f)
159
- if df[name].any?(&:nil)
159
+ if df[name].any?(&:nil?)
160
160
  raise ArgumentError, "Found NaN in column #{name.inspect}"
161
161
  end
162
162
  end
163
163
  @seasonalities.values.each do |props|
164
164
  condition_name = props[:condition_name]
165
165
  if condition_name
166
- if !df.vectors.include?(condition_name)
166
+ if !df.include?(condition_name)
167
167
  raise ArgumentError, "Condition #{condition_name.inspect} missing from dataframe"
168
168
  end
169
169
  if df.where(!df[condition_name].in([true, false])).any?
@@ -172,36 +172,33 @@ module Prophet
172
172
  end
173
173
  end
174
174
 
175
- if df.index.name == "ds"
176
- df.index.name = nil
177
- end
178
- df = df.sort(["ds"])
175
+ df = df.sort_by { |r| r["ds"] }
179
176
 
180
177
  initialize_scales(initialize_scales, df)
181
178
 
182
- if @logistic_floor && !df.vectors.include?("floor")
179
+ if @logistic_floor && !df.include?("floor")
183
180
  raise ArgumentError, "Expected column \"floor\"."
184
181
  else
185
182
  df["floor"] = 0
186
183
  end
187
184
 
188
185
  if @growth == "logistic"
189
- unless df.vectors.include?("cap")
186
+ unless df.include?("cap")
190
187
  raise ArgumentError, "Capacities must be supplied for logistic growth in column \"cap\""
191
188
  end
192
- if df.where(df["cap"] <= df["floor"]).size > 0
189
+ if df[df["cap"] <= df["floor"]].size > 0
193
190
  raise ArgumentError, "cap must be greater than floor (which defaults to 0)."
194
191
  end
195
- df["cap_scaled"] = (df["cap"] - df["floor"]) / @y_scale
192
+ df["cap_scaled"] = (df["cap"] - df["floor"]) / @y_scale.to_f
196
193
  end
197
194
 
198
195
  df["t"] = (df["ds"] - @start) / @t_scale.to_f
199
- if df.vectors.include?("y")
200
- df["y_scaled"] = (df["y"] - df["floor"]) / @y_scale
196
+ if df.include?("y")
197
+ df["y_scaled"] = (df["y"] - df["floor"]) / @y_scale.to_f
201
198
  end
202
199
 
203
200
  @extra_regressors.each do |name, props|
204
- df[name] = ((df[name] - props["mu"]) / props["std"])
201
+ df[name] = (df[name] - props[:mu]) / props[:std].to_f
205
202
  end
206
203
 
207
204
  df
@@ -218,32 +215,40 @@ module Prophet
218
215
  end
219
216
 
220
217
  def set_changepoints
221
- hist_size = (@history.shape[0] * @changepoint_range).floor
218
+ if @changepoints
219
+ if @changepoints.size > 0
220
+ too_low = @changepoints.min < @history["ds"].min
221
+ too_high = @changepoints.max > @history["ds"].max
222
+ if too_low || too_high
223
+ raise ArgumentError, "Changepoints must fall within training data."
224
+ end
225
+ end
226
+ else
227
+ hist_size = (@history.shape[0] * @changepoint_range).floor
222
228
 
223
- if @n_changepoints + 1 > hist_size
224
- @n_changepoints = hist_size - 1
225
- logger.info "n_changepoints greater than number of observations. Using #{@n_changepoints}"
226
- end
229
+ if @n_changepoints + 1 > hist_size
230
+ @n_changepoints = hist_size - 1
231
+ logger.info "n_changepoints greater than number of observations. Using #{@n_changepoints}"
232
+ end
227
233
 
228
- if @n_changepoints > 0
229
- step = (hist_size - 1) / @n_changepoints.to_f
230
- cp_indexes = (@n_changepoints + 1).times.map { |i| (i * step).round }
231
- @changepoints = @history["ds"][*cp_indexes][1..-1]
232
- else
233
- @changepoints = []
234
+ if @n_changepoints > 0
235
+ step = (hist_size - 1) / @n_changepoints.to_f
236
+ cp_indexes = (@n_changepoints + 1).times.map { |i| (i * step).round }
237
+ @changepoints = Rover::Vector.new(@history["ds"].to_a.values_at(*cp_indexes)).tail(-1)
238
+ else
239
+ @changepoints = []
240
+ end
234
241
  end
235
242
 
236
243
  if @changepoints.size > 0
237
- @changepoints_t = Numo::NArray.asarray(((@changepoints - @start) / @t_scale.to_f).to_a).sort
244
+ @changepoints_t = (@changepoints.map(&:to_i).sort.to_numo.cast_to(Numo::DFloat) - @start.to_i) / @t_scale.to_f
238
245
  else
239
246
  @changepoints_t = Numo::NArray.asarray([0])
240
247
  end
241
248
  end
242
249
 
243
250
  def fourier_series(dates, period, series_order)
244
- start = Time.utc(1970).to_i
245
- # uses to_datetime first so we get UTC
246
- t = Numo::DFloat.asarray(dates.map { |v| v.to_i - start }) / (3600 * 24.0)
251
+ t = dates.map(&:to_i).to_numo / (3600 * 24.0)
247
252
 
248
253
  # no need for column_stack
249
254
  series_order.times.flat_map do |i|
@@ -255,11 +260,11 @@ module Prophet
255
260
 
256
261
  def make_seasonality_features(dates, period, series_order, prefix)
257
262
  features = fourier_series(dates, period, series_order)
258
- Daru::DataFrame.new(features.map.with_index { |v, i| ["#{prefix}_delim_#{i + 1}", v] }.to_h)
263
+ Rover::DataFrame.new(features.map.with_index { |v, i| ["#{prefix}_delim_#{i + 1}", v] }.to_h)
259
264
  end
260
265
 
261
266
  def construct_holiday_dataframe(dates)
262
- all_holidays = Daru::DataFrame.new
267
+ all_holidays = Rover::DataFrame.new
263
268
  if @holidays
264
269
  all_holidays = @holidays.dup
265
270
  end
@@ -271,12 +276,12 @@ module Prophet
271
276
  # Drop future holidays not previously seen in training data
272
277
  if @train_holiday_names
273
278
  # Remove holiday names didn't show up in fit
274
- all_holidays = all_holidays.where(all_holidays["holiday"].in(@train_holiday_names))
279
+ all_holidays = all_holidays[all_holidays["holiday"].in?(@train_holiday_names)]
275
280
 
276
281
  # Add holiday names in fit but not in predict with ds as NA
277
- holidays_to_add = Daru::DataFrame.new(
278
- "holiday" => @train_holiday_names.where(!@train_holiday_names.in(all_holidays["holiday"]))
279
- )
282
+ holidays_to_add = Rover::DataFrame.new({
283
+ "holiday" => @train_holiday_names[!@train_holiday_names.in?(all_holidays["holiday"])]
284
+ })
280
285
  all_holidays = all_holidays.concat(holidays_to_add)
281
286
  end
282
287
 
@@ -310,7 +315,7 @@ module Prophet
310
315
 
311
316
  lw.upto(uw).each do |offset|
312
317
  occurrence = dt ? dt + offset : nil
313
- loc = occurrence ? row_index.index(occurrence) : nil
318
+ loc = occurrence ? row_index.to_a.index(occurrence) : nil
314
319
  key = "#{row["holiday"]}_delim_#{offset >= 0 ? "+" : "-"}#{offset.abs}"
315
320
  if loc
316
321
  expanded_holidays[key][loc] = 1.0
@@ -319,14 +324,14 @@ module Prophet
319
324
  end
320
325
  end
321
326
  end
322
- holiday_features = Daru::DataFrame.new(expanded_holidays)
323
- # # Make sure column order is consistent
324
- holiday_features = holiday_features[*holiday_features.vectors.sort]
325
- prior_scale_list = holiday_features.vectors.map { |h| prior_scales[h.split("_delim_")[0]] }
327
+ holiday_features = Rover::DataFrame.new(expanded_holidays)
328
+ # Make sure column order is consistent
329
+ holiday_features = holiday_features[holiday_features.vector_names.sort]
330
+ prior_scale_list = holiday_features.vector_names.map { |h| prior_scales[h.split("_delim_")[0]] }
326
331
  holiday_names = prior_scales.keys
327
332
  # Store holiday names used in fit
328
- if !@train_holiday_names
329
- @train_holiday_names = Daru::Vector.new(holiday_names)
333
+ if @train_holiday_names.nil?
334
+ @train_holiday_names = Rover::Vector.new(holiday_names)
330
335
  end
331
336
  [holiday_features, prior_scale_list, holiday_names]
332
337
  end
@@ -424,16 +429,16 @@ module Prophet
424
429
  modes[@seasonality_mode].concat(holiday_names)
425
430
  end
426
431
 
427
- # # Additional regressors
432
+ # Additional regressors
428
433
  @extra_regressors.each do |name, props|
429
- seasonal_features << df[name].to_df
434
+ seasonal_features << Rover::DataFrame.new({name => df[name]})
430
435
  prior_scales << props[:prior_scale]
431
436
  modes[props[:mode]] << name
432
437
  end
433
438
 
434
- # # Dummy to prevent empty X
439
+ # Dummy to prevent empty X
435
440
  if seasonal_features.size == 0
436
- seasonal_features << Daru::DataFrame.new("zeros" => [0] * df.shape[0])
441
+ seasonal_features << Rover::DataFrame.new({"zeros" => [0] * df.shape[0]})
437
442
  prior_scales << 1.0
438
443
  end
439
444
 
@@ -445,16 +450,16 @@ module Prophet
445
450
  end
446
451
 
447
452
  def regressor_column_matrix(seasonal_features, modes)
448
- components = Daru::DataFrame.new(
453
+ components = Rover::DataFrame.new(
449
454
  "col" => seasonal_features.shape[1].times.to_a,
450
- "component" => seasonal_features.vectors.map { |x| x.split("_delim_")[0] }
455
+ "component" => seasonal_features.vector_names.map { |x| x.split("_delim_")[0] }
451
456
  )
452
457
 
453
- # # Add total for holidays
458
+ # Add total for holidays
454
459
  if @train_holiday_names
455
460
  components = add_group_component(components, "holidays", @train_holiday_names.uniq)
456
461
  end
457
- # # Add totals additive and multiplicative components, and regressors
462
+ # Add totals additive and multiplicative components, and regressors
458
463
  ["additive", "multiplicative"].each do |mode|
459
464
  components = add_group_component(components, mode + "_terms", modes[mode])
460
465
  regressors_by_mode = @extra_regressors.select { |r, props| props[:mode] == mode }
@@ -465,20 +470,15 @@ module Prophet
465
470
  modes[mode] << mode + "_terms"
466
471
  modes[mode] << "extra_regressors_" + mode
467
472
  end
468
- # # After all of the additive/multiplicative groups have been added,
473
+ # After all of the additive/multiplicative groups have been added,
469
474
  modes[@seasonality_mode] << "holidays"
470
- # # Convert to a binary matrix
471
- component_cols = Daru::DataFrame.crosstab_by_assignation(
472
- components["col"], components["component"], [1] * components.size
473
- )
474
- component_cols.each_vector do |v|
475
- v.map! { |vi| vi.nil? ? 0 : vi }
476
- end
477
- component_cols.rename_vectors(:_id => "col")
475
+ # Convert to a binary matrix
476
+ component_cols = components["col"].crosstab(components["component"])
477
+ component_cols["col"] = component_cols.delete("_")
478
478
 
479
479
  # Add columns for additive and multiplicative terms, if missing
480
480
  ["additive_terms", "multiplicative_terms"].each do |name|
481
- component_cols[name] = 0 unless component_cols.vectors.include?(name)
481
+ component_cols[name] = 0 unless component_cols.include?(name)
482
482
  end
483
483
 
484
484
  # TODO validation
@@ -487,10 +487,10 @@ module Prophet
487
487
  end
488
488
 
489
489
  def add_group_component(components, name, group)
490
- new_comp = components.where(components["component"].in(group)).dup
490
+ new_comp = components[components["component"].in?(group)].dup
491
491
  group_cols = new_comp["col"].uniq
492
492
  if group_cols.size > 0
493
- new_comp = Daru::DataFrame.new("col" => group_cols, "component" => [name] * group_cols.size)
493
+ new_comp = Rover::DataFrame.new({"col" => group_cols, "component" => name})
494
494
  components = components.concat(new_comp)
495
495
  end
496
496
  components
@@ -566,8 +566,8 @@ module Prophet
566
566
  end
567
567
 
568
568
  def linear_growth_init(df)
569
- i0 = df["ds"].index.min
570
- i1 = df["ds"].index.max
569
+ i0 = 0
570
+ i1 = df.size - 1
571
571
  t = df["t"][i1] - df["t"][i0]
572
572
  k = (df["y_scaled"][i1] - df["y_scaled"][i0]) / t
573
573
  m = df["y_scaled"][i0] - k * df["t"][i0]
@@ -575,8 +575,8 @@ module Prophet
575
575
  end
576
576
 
577
577
  def logistic_growth_init(df)
578
- i0 = df["ds"].index.min
579
- i1 = df["ds"].index.max
578
+ i0 = 0
579
+ i1 = df.size - 1
580
580
  t = df["t"][i1] - df["t"][i0]
581
581
 
582
582
  # Force valid values, in case y > cap or y < 0
@@ -605,8 +605,13 @@ module Prophet
605
605
  def fit(df, **kwargs)
606
606
  raise Error, "Prophet object can only be fit once" if @history
607
607
 
608
- history = df.where(!df["y"].in([nil, Float::NAN]))
609
- raise Error, "Data has less than 2 non-nil rows" if history.shape[0] < 2
608
+ if defined?(Daru::DataFrame) && df.is_a?(Daru::DataFrame)
609
+ df = Rover::DataFrame.new(df.to_h)
610
+ end
611
+ raise ArgumentError, "Must be a data frame" unless df.is_a?(Rover::DataFrame)
612
+
613
+ history = df[!df["y"].missing]
614
+ raise Error, "Data has less than 2 non-nil rows" if history.size < 2
610
615
 
611
616
  @history_dates = to_datetime(df["ds"]).sort
612
617
  history = setup_dataframe(history, initialize_scales: true)
@@ -654,8 +659,8 @@ module Prophet
654
659
  # Nothing to fit.
655
660
  @params = stan_init
656
661
  @params["sigma_obs"] = 1e-9
657
- @params.each do |par|
658
- @params[par] = Numo::NArray.asarray(@params[par])
662
+ @params.each do |par, _|
663
+ @params[par] = Numo::NArray.asarray([@params[par]])
659
664
  end
660
665
  elsif @mcmc_samples > 0
661
666
  @params = @stan_backend.sampling(stan_init, dat, @mcmc_samples, **kwargs)
@@ -666,8 +671,10 @@ module Prophet
666
671
  # If no changepoints were requested, replace delta with 0s
667
672
  if @changepoints.size == 0
668
673
  # Fold delta into the base rate k
669
- @params["k"] = @params["k"] + @params["delta"].reshape(-1)
670
- @params["delta"] = Numo::DFloat.zeros(@params["delta"].shape).reshape(-1, 1)
674
+ # Numo doesn't support -1 with reshape
675
+ negative_one = @params["delta"].shape.inject(&:*)
676
+ @params["k"] = @params["k"] + @params["delta"].reshape(negative_one)
677
+ @params["delta"] = Numo::DFloat.zeros(@params["delta"].shape).reshape(negative_one, 1)
671
678
  end
672
679
 
673
680
  self
@@ -693,10 +700,10 @@ module Prophet
693
700
 
694
701
  # Drop columns except ds, cap, floor, and trend
695
702
  cols = ["ds", "trend"]
696
- cols << "cap" if df.vectors.include?("cap")
703
+ cols << "cap" if df.include?("cap")
697
704
  cols << "floor" if @logistic_floor
698
705
  # Add in forecast components
699
- df2 = df_concat_axis_one([df[*cols], intervals, seasonal_components])
706
+ df2 = df_concat_axis_one([df[cols], intervals, seasonal_components])
700
707
  df2["yhat"] = df2["trend"] * (df2["multiplicative_terms"] + 1) + df2["additive_terms"]
701
708
  df2
702
709
  end
@@ -731,8 +738,7 @@ module Prophet
731
738
  k_t[indx] += deltas[s]
732
739
  m_t[indx] += gammas[s]
733
740
  end
734
- # need df_values to prevent memory from blowing up
735
- df_values(cap) / (1 + Numo::NMath.exp(-k_t * (t - m_t)))
741
+ cap.to_numo / (1 + Numo::NMath.exp(-k_t * (t - m_t)))
736
742
  end
737
743
 
738
744
  def predict_trend(df)
@@ -758,10 +764,10 @@ module Prophet
758
764
  upper_p = 100 * (1.0 + @interval_width) / 2
759
765
  end
760
766
 
761
- x = df_values(seasonal_features)
767
+ x = seasonal_features.to_numo
762
768
  data = {}
763
- component_cols.vectors.each do |component|
764
- beta_c = @params["beta"] * Numo::NArray.asarray(component_cols[component].to_a)
769
+ component_cols.vector_names.each do |component|
770
+ beta_c = @params["beta"] * component_cols[component].to_numo
765
771
 
766
772
  comp = x.dot(beta_c.transpose)
767
773
  if @component_modes["additive"].include?(component)
@@ -769,11 +775,11 @@ module Prophet
769
775
  end
770
776
  data[component] = comp.mean(axis: 1, nan: true)
771
777
  if @uncertainty_samples
772
- data[component + "_lower"] = percentile(comp, lower_p, axis: 1)
773
- data[component + "_upper"] = percentile(comp, upper_p, axis: 1)
778
+ data[component + "_lower"] = comp.percentile(lower_p, axis: 1)
779
+ data[component + "_upper"] = comp.percentile(upper_p, axis: 1)
774
780
  end
775
781
  end
776
- Daru::DataFrame.new(data)
782
+ Rover::DataFrame.new(data)
777
783
  end
778
784
 
779
785
  def sample_posterior_predictive(df)
@@ -784,9 +790,9 @@ module Prophet
784
790
  seasonal_features, _, component_cols, _ = make_all_seasonality_features(df)
785
791
 
786
792
  # convert to Numo for performance
787
- seasonal_features = df_values(seasonal_features)
788
- additive_terms = df_values(component_cols["additive_terms"])
789
- multiplicative_terms = df_values(component_cols["multiplicative_terms"])
793
+ seasonal_features = seasonal_features.to_numo
794
+ additive_terms = component_cols["additive_terms"].to_numo
795
+ multiplicative_terms = component_cols["multiplicative_terms"].to_numo
790
796
 
791
797
  sim_values = {"yhat" => [], "trend" => []}
792
798
  n_iterations.times do |i|
@@ -823,11 +829,11 @@ module Prophet
823
829
 
824
830
  series = {}
825
831
  ["yhat", "trend"].each do |key|
826
- series["#{key}_lower"] = percentile(sim_values[key], lower_p, axis: 1)
827
- series["#{key}_upper"] = percentile(sim_values[key], upper_p, axis: 1)
832
+ series["#{key}_lower"] = sim_values[key].percentile(lower_p, axis: 1)
833
+ series["#{key}_upper"] = sim_values[key].percentile(upper_p, axis: 1)
828
834
  end
829
835
 
830
- Daru::DataFrame.new(series)
836
+ Rover::DataFrame.new(series)
831
837
  end
832
838
 
833
839
  def sample_model(df, seasonal_features, iteration, s_a, s_m)
@@ -848,8 +854,8 @@ module Prophet
848
854
  end
849
855
 
850
856
  def sample_predictive_trend(df, iteration)
851
- k = @params["k"][iteration, true]
852
- m = @params["m"][iteration, true]
857
+ k = @params["k"][iteration]
858
+ m = @params["m"][iteration]
853
859
  deltas = @params["delta"][iteration, true]
854
860
 
855
861
  t = Numo::NArray.asarray(df["t"].to_a)
@@ -889,82 +895,81 @@ module Prophet
889
895
  trend * @y_scale + Numo::NArray.asarray(df["floor"].to_a)
890
896
  end
891
897
 
892
- def percentile(a, percentile, axis:)
893
- raise Error, "Axis must be 1" if axis != 1
894
-
895
- sorted = a.sort(axis: axis)
896
- x = percentile / 100.0 * (sorted.shape[axis] - 1)
897
- r = x % 1
898
- i = x.floor
899
- # this should use axis, but we only need axis: 1
900
- if i == sorted.shape[axis] - 1
901
- sorted[true, -1]
902
- else
903
- sorted[true, i] + r * (sorted[true, i + 1] - sorted[true, i])
904
- end
905
- end
906
-
907
898
  def make_future_dataframe(periods:, freq: "D", include_history: true)
908
899
  raise Error, "Model has not been fit" unless @history_dates
909
900
  last_date = @history_dates.max
901
+ # TODO add more freq
902
+ # https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases
910
903
  case freq
904
+ when /\A\d+S\z/
905
+ secs = freq.to_i
906
+ dates = (periods + 1).times.map { |i| last_date + i * secs }
907
+ when "H"
908
+ hour = 3600
909
+ dates = (periods + 1).times.map { |i| last_date + i * hour }
911
910
  when "D"
912
911
  # days have constant length with UTC (no DST or leap seconds)
913
- dates = (periods + 1).times.map { |i| last_date + i * 86400 }
914
- when "H"
915
- dates = (periods + 1).times.map { |i| last_date + i * 3600 }
912
+ day = 24 * 3600
913
+ dates = (periods + 1).times.map { |i| last_date + i * day }
914
+ when "W"
915
+ week = 7 * 24 * 3600
916
+ dates = (periods + 1).times.map { |i| last_date + i * week }
916
917
  when "MS"
917
918
  dates = [last_date]
919
+ # TODO reset day from last date, but keep time
918
920
  periods.times do
919
921
  dates << dates.last.to_datetime.next_month.to_time.utc
920
922
  end
923
+ when "QS"
924
+ dates = [last_date]
925
+ # TODO reset day and month from last date, but keep time
926
+ periods.times do
927
+ dates << dates.last.to_datetime.next_month.next_month.next_month.to_time.utc
928
+ end
929
+ when "YS"
930
+ dates = [last_date]
931
+ # TODO reset day and month from last date, but keep time
932
+ periods.times do
933
+ dates << dates.last.to_datetime.next_year.to_time.utc
934
+ end
921
935
  else
922
936
  raise ArgumentError, "Unknown freq: #{freq}"
923
937
  end
924
938
  dates.select! { |d| d > last_date }
925
939
  dates = dates.last(periods)
926
- dates = @history_dates + dates if include_history
927
- Daru::DataFrame.new("ds" => dates)
940
+ dates = @history_dates.to_numo.concatenate(Numo::NArray.cast(dates)) if include_history
941
+ Rover::DataFrame.new({"ds" => dates})
928
942
  end
929
943
 
930
944
  private
931
945
 
932
- # Time is prefer over DateTime Ruby
946
+ # Time is preferred over DateTime in Ruby docs
933
947
  # use UTC to be consistent with Python
934
948
  # and so days have equal length (no DST)
935
949
  def to_datetime(vec)
936
950
  return if vec.nil?
937
- vec.map do |v|
938
- case v
939
- when Time
940
- v.utc
941
- when Date
942
- v.to_datetime.to_time.utc
943
- else
944
- DateTime.parse(v.to_s).to_time.utc
951
+ vec =
952
+ vec.map do |v|
953
+ case v
954
+ when Time
955
+ v.utc
956
+ when Date
957
+ v.to_datetime.to_time.utc
958
+ else
959
+ DateTime.parse(v.to_s).to_time.utc
960
+ end
945
961
  end
946
- end
962
+ Rover::Vector.new(vec)
947
963
  end
948
964
 
949
965
  # okay to do in-place
950
966
  def df_concat_axis_one(dfs)
951
967
  dfs[1..-1].each do |df|
952
- df.each_vector_with_index do |v, k|
953
- dfs[0][k] = v
954
- end
968
+ dfs[0].merge!(df)
955
969
  end
956
970
  dfs[0]
957
971
  end
958
972
 
959
- def df_values(df)
960
- if df.is_a?(Daru::Vector)
961
- Numo::NArray.asarray(df.to_a)
962
- else
963
- # TODO make more performant
964
- Numo::NArray.asarray(df.to_matrix.to_a)
965
- end
966
- end
967
-
968
973
  # https://en.wikipedia.org/wiki/Poisson_distribution#Generating_Poisson-distributed_random_variables
969
974
  def poisson(lam)
970
975
  l = Math.exp(-lam)
@@ -979,7 +984,7 @@ module Prophet
979
984
 
980
985
  # https://en.wikipedia.org/wiki/Laplace_distribution#Generating_values_from_the_Laplace_distribution
981
986
  def laplace(loc, scale, size)
982
- u = Numo::DFloat.new(size).rand - 0.5
987
+ u = Numo::DFloat.new(size).rand(-0.5, 0.5)
983
988
  loc - scale * u.sign * Numo::NMath.log(1 - 2 * u.abs)
984
989
  end
985
990
  end
@@ -6,7 +6,7 @@ module Prophet
6
6
  end
7
7
 
8
8
  def make_holidays_df(year_list, country)
9
- holidays_df.where(holidays_df["country"].eq(country) & holidays_df["year"].in(year_list))["ds", "holiday"]
9
+ holidays_df[(holidays_df["country"] == country) & (holidays_df["year"].in?(year_list))][["ds", "holiday"]]
10
10
  end
11
11
 
12
12
  # TODO marshal on installation
@@ -20,7 +20,7 @@ module Prophet
20
20
  holidays["country"] << row["country"]
21
21
  holidays["year"] << row["year"]
22
22
  end
23
- Daru::DataFrame.new(holidays)
23
+ Rover::DataFrame.new(holidays)
24
24
  end
25
25
  end
26
26
  end
@@ -8,16 +8,16 @@ module Prophet
8
8
  fig = ax.get_figure
9
9
  end
10
10
  fcst_t = to_pydatetime(fcst["ds"])
11
- ax.plot(to_pydatetime(@history["ds"]), @history["y"].map(&:to_f), "k.")
12
- ax.plot(fcst_t, fcst["yhat"].map(&:to_f), ls: "-", c: "#0072B2")
13
- if fcst.vectors.include?("cap") && plot_cap
14
- ax.plot(fcst_t, fcst["cap"].map(&:to_f), ls: "--", c: "k")
11
+ ax.plot(to_pydatetime(@history["ds"]), @history["y"].to_a, "k.")
12
+ ax.plot(fcst_t, fcst["yhat"].to_a, ls: "-", c: "#0072B2")
13
+ if fcst.include?("cap") && plot_cap
14
+ ax.plot(fcst_t, fcst["cap"].to_a, ls: "--", c: "k")
15
15
  end
16
- if @logistic_floor && fcst.vectors.include?("floor") && plot_cap
17
- ax.plot(fcst_t, fcst["floor"].map(&:to_f), ls: "--", c: "k")
16
+ if @logistic_floor && fcst.include?("floor") && plot_cap
17
+ ax.plot(fcst_t, fcst["floor"].to_a, ls: "--", c: "k")
18
18
  end
19
19
  if uncertainty && @uncertainty_samples
20
- ax.fill_between(fcst_t, fcst["yhat_lower"].map(&:to_f), fcst["yhat_upper"].map(&:to_f), color: "#0072B2", alpha: 0.2)
20
+ ax.fill_between(fcst_t, fcst["yhat_lower"].to_a, fcst["yhat_upper"].to_a, color: "#0072B2", alpha: 0.2)
21
21
  end
22
22
  # Specify formatting to workaround matplotlib issue #12925
23
23
  locator = dates.AutoDateLocator.new(interval_multiples: false)
@@ -33,25 +33,25 @@ module Prophet
33
33
 
34
34
  def plot_components(fcst, uncertainty: true, plot_cap: true, weekly_start: 0, yearly_start: 0, figsize: nil)
35
35
  components = ["trend"]
36
- if @train_holiday_names && fcst.vectors.include?("holidays")
36
+ if @train_holiday_names && fcst.include?("holidays")
37
37
  components << "holidays"
38
38
  end
39
39
  # Plot weekly seasonality, if present
40
- if @seasonalities["weekly"] && fcst.vectors.include?("weekly")
40
+ if @seasonalities["weekly"] && fcst.include?("weekly")
41
41
  components << "weekly"
42
42
  end
43
43
  # Yearly if present
44
- if @seasonalities["yearly"] && fcst.vectors.include?("yearly")
44
+ if @seasonalities["yearly"] && fcst.include?("yearly")
45
45
  components << "yearly"
46
46
  end
47
47
  # Other seasonalities
48
- components.concat(@seasonalities.keys.select { |name| fcst.vectors.include?(name) && !["weekly", "yearly"].include?(name) }.sort)
48
+ components.concat(@seasonalities.keys.select { |name| fcst.include?(name) && !["weekly", "yearly"].include?(name) }.sort)
49
49
  regressors = {"additive" => false, "multiplicative" => false}
50
50
  @extra_regressors.each do |name, props|
51
51
  regressors[props[:mode]] = true
52
52
  end
53
53
  ["additive", "multiplicative"].each do |mode|
54
- if regressors[mode] && fcst.vectors.include?("extra_regressors_#{mode}")
54
+ if regressors[mode] && fcst.include?("extra_regressors_#{mode}")
55
55
  components << "extra_regressors_#{mode}"
56
56
  end
57
57
  end
@@ -93,6 +93,24 @@ module Prophet
93
93
  fig
94
94
  end
95
95
 
96
+ # in Python, this is a separate method
97
+ def add_changepoints_to_plot(ax, fcst, threshold: 0.01, cp_color: "r", cp_linestyle: "--", trend: true)
98
+ artists = []
99
+ if trend
100
+ artists << ax.plot(to_pydatetime(fcst["ds"]), fcst["trend"].to_a, c: cp_color)
101
+ end
102
+ signif_changepoints =
103
+ if @changepoints.size > 0
104
+ (@params["delta"].mean(axis: 0, nan: true).abs >= threshold).mask(@changepoints.to_numo)
105
+ else
106
+ []
107
+ end
108
+ to_pydatetime(signif_changepoints).each do |cp|
109
+ artists << ax.axvline(x: cp, c: cp_color, ls: cp_linestyle)
110
+ end
111
+ artists
112
+ end
113
+
96
114
  private
97
115
 
98
116
  def plot_forecast_component(fcst, name, ax: nil, uncertainty: true, plot_cap: false, figsize: [10, 6])
@@ -102,15 +120,15 @@ module Prophet
102
120
  ax = fig.add_subplot(111)
103
121
  end
104
122
  fcst_t = to_pydatetime(fcst["ds"])
105
- artists += ax.plot(fcst_t, fcst[name].map(&:to_f), ls: "-", c: "#0072B2")
106
- if fcst.vectors.include?("cap") && plot_cap
107
- artists += ax.plot(fcst_t, fcst["cap"].map(&:to_f), ls: "--", c: "k")
123
+ artists += ax.plot(fcst_t, fcst[name].to_a, ls: "-", c: "#0072B2")
124
+ if fcst.include?("cap") && plot_cap
125
+ artists += ax.plot(fcst_t, fcst["cap"].to_a, ls: "--", c: "k")
108
126
  end
109
- if @logistic_floor && fcst.vectors.include?("floor") && plot_cap
110
- ax.plot(fcst_t, fcst["floor"].map(&:to_f), ls: "--", c: "k")
127
+ if @logistic_floor && fcst.include?("floor") && plot_cap
128
+ ax.plot(fcst_t, fcst["floor"].to_a, ls: "--", c: "k")
111
129
  end
112
130
  if uncertainty && @uncertainty_samples
113
- artists += [ax.fill_between(fcst_t, fcst[name + "_lower"].map(&:to_f), fcst[name + "_upper"].map(&:to_f), color: "#0072B2", alpha: 0.2)]
131
+ artists += [ax.fill_between(fcst_t, fcst[name + "_lower"].to_a, fcst[name + "_upper"].to_a, color: "#0072B2", alpha: 0.2)]
114
132
  end
115
133
  # Specify formatting to workaround matplotlib issue #12925
116
134
  locator = dates.AutoDateLocator.new(interval_multiples: false)
@@ -127,17 +145,17 @@ module Prophet
127
145
  end
128
146
 
129
147
  def seasonality_plot_df(ds)
130
- df_dict = {"ds" => ds, "cap" => [1.0] * ds.size, "floor" => [0.0] * ds.size}
131
- @extra_regressors.each do |name|
132
- df_dict[name] = [0.0] * ds.size
148
+ df_dict = {"ds" => ds, "cap" => 1.0, "floor" => 0.0}
149
+ @extra_regressors.each_key do |name|
150
+ df_dict[name] = 0.0
133
151
  end
134
152
  # Activate all conditional seasonality columns
135
153
  @seasonalities.values.each do |props|
136
154
  if props[:condition_name]
137
- df_dict[props[:condition_name]] = [true] * ds.size
155
+ df_dict[props[:condition_name]] = true
138
156
  end
139
157
  end
140
- df = Daru::DataFrame.new(df_dict)
158
+ df = Rover::DataFrame.new(df_dict)
141
159
  df = setup_dataframe(df)
142
160
  df
143
161
  end
@@ -154,9 +172,9 @@ module Prophet
154
172
  df_w = seasonality_plot_df(days)
155
173
  seas = predict_seasonal_components(df_w)
156
174
  days = days.map { |v| v.strftime("%A") }
157
- artists += ax.plot(days.size.times.to_a, seas[name].map(&:to_f), ls: "-", c: "#0072B2")
175
+ artists += ax.plot(days.size.times.to_a, seas[name].to_a, ls: "-", c: "#0072B2")
158
176
  if uncertainty && @uncertainty_samples
159
- artists += [ax.fill_between(days.size.times.to_a, seas[name + "_lower"].map(&:to_f), seas[name + "_upper"].map(&:to_f), color: "#0072B2", alpha: 0.2)]
177
+ artists += [ax.fill_between(days.size.times.to_a, seas[name + "_lower"].to_a, seas[name + "_upper"].to_a, color: "#0072B2", alpha: 0.2)]
160
178
  end
161
179
  ax.grid(true, which: "major", c: "gray", ls: "-", lw: 1, alpha: 0.2)
162
180
  ax.set_xticks(days.size.times.to_a)
@@ -180,9 +198,9 @@ module Prophet
180
198
  days = 365.times.map { |i| start + i + yearly_start }
181
199
  df_y = seasonality_plot_df(days)
182
200
  seas = predict_seasonal_components(df_y)
183
- artists += ax.plot(to_pydatetime(df_y["ds"]), seas[name].map(&:to_f), ls: "-", c: "#0072B2")
201
+ artists += ax.plot(to_pydatetime(df_y["ds"]), seas[name].to_a, ls: "-", c: "#0072B2")
184
202
  if uncertainty && @uncertainty_samples
185
- artists += [ax.fill_between(to_pydatetime(df_y["ds"]), seas[name + "_lower"].map(&:to_f), seas[name + "_upper"].map(&:to_f), color: "#0072B2", alpha: 0.2)]
203
+ artists += [ax.fill_between(to_pydatetime(df_y["ds"]), seas[name + "_lower"].to_a, seas[name + "_upper"].to_a, color: "#0072B2", alpha: 0.2)]
186
204
  end
187
205
  ax.grid(true, which: "major", c: "gray", ls: "-", lw: 1, alpha: 0.2)
188
206
  months = dates.MonthLocator.new((1..12).to_a, bymonthday: 1, interval: 2)
@@ -213,9 +231,9 @@ module Prophet
213
231
  days = plot_points.times.map { |i| Time.at(start + i * step).utc }
214
232
  df_y = seasonality_plot_df(days)
215
233
  seas = predict_seasonal_components(df_y)
216
- artists += ax.plot(to_pydatetime(df_y["ds"]), seas[name].map(&:to_f), ls: "-", c: "#0072B2")
234
+ artists += ax.plot(to_pydatetime(df_y["ds"]), seas[name].to_a, ls: "-", c: "#0072B2")
217
235
  if uncertainty && @uncertainty_samples
218
- artists += [ax.fill_between(to_pydatetime(df_y["ds"]), seas[name + "_lower"].map(&:to_f), seas[name + "_upper"].map(&:to_f), color: "#0072B2", alpha: 0.2)]
236
+ artists += [ax.fill_between(to_pydatetime(df_y["ds"]), seas[name + "_lower"].to_a, seas[name + "_upper"].to_a, color: "#0072B2", alpha: 0.2)]
219
237
  end
220
238
  ax.grid(true, which: "major", c: "gray", ls: "-", lw: 1, alpha: 0.2)
221
239
  step = (finish - start) / (7 - 1).to_f
@@ -263,7 +281,7 @@ module Prophet
263
281
 
264
282
  def to_pydatetime(v)
265
283
  datetime = PyCall.import_module("datetime")
266
- v.map { |v| datetime.datetime.utcfromtimestamp(v.to_i) }
284
+ v.map { |v| datetime.datetime.utcfromtimestamp(v.to_i) }.to_a
267
285
  end
268
286
  end
269
287
  end
@@ -127,7 +127,7 @@ module Prophet
127
127
  stan_data["t_change"] = stan_data["t_change"].to_a
128
128
  stan_data["s_a"] = stan_data["s_a"].to_a
129
129
  stan_data["s_m"] = stan_data["s_m"].to_a
130
- stan_data["X"] = stan_data["X"].to_matrix.to_a
130
+ stan_data["X"] = stan_data["X"].to_numo.to_a
131
131
  stan_init["delta"] = stan_init["delta"].to_a
132
132
  stan_init["beta"] = stan_init["beta"].to_a
133
133
  [stan_init, stan_data]
@@ -1,3 +1,3 @@
1
1
  module Prophet
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prophet-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-09 00:00:00.000000000 Z
11
+ date: 2020-10-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdstan
@@ -16,30 +16,30 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.1.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.1.2
27
27
  - !ruby/object:Gem::Dependency
28
- name: daru
28
+ name: numo-narray
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
33
+ version: 0.9.1.7
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 0.9.1.7
41
41
  - !ruby/object:Gem::Dependency
42
- name: numo-narray
42
+ name: rover-df
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: daru
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: matplotlib
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -109,7 +123,21 @@ dependencies:
109
123
  - !ruby/object:Gem::Version
110
124
  version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
- name: ruby-prof
126
+ name: activesupport
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: tzinfo-data
113
141
  requirement: !ruby/object:Gem::Requirement
114
142
  requirements:
115
143
  - - ">="