prophet-rb 0.2.1 → 0.2.5

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: 6469e3c9b0f7d026678845025284f37f7c72f875e19a5f245d9bbc745d348a38
4
- data.tar.gz: f3be6a4488cf07a9b2b36f2ce8dd20e9c42aa4297b36b7c56d0ac16bbd6a28f2
3
+ metadata.gz: ec194b65cb4bfd061bac7868cad435fea98df148fc7e129d8562c55ed3c91fcc
4
+ data.tar.gz: 45b9357d04a6c01a8d4ad6ce7888065b946e39cbfa743b650a41d5b12cf9989e
5
5
  SHA512:
6
- metadata.gz: 6476dc943d7a23bf74e84a8fec345009b4587e1e514e9b8e22b32fdd02e44a0f60870eb918b5cb097cd47f1da3e9ad8481098f6800736eadc31fffd7db266f75
7
- data.tar.gz: e831ae81325d98e12a3b53bf3e68f87fdc953ad8cba1f22daca5cd375c822a9d39e5f192a4c40eeaf5c62c852955fce3b300680538988a7449069fc3de25fefb
6
+ metadata.gz: cd67e017dac94994c960a42b59fb55f2c33560d9233bb2764581cef3154bc03bd75ba29283f8e51788fcde356f682e9fe487a5506ccb82ed10ab373b11774989
7
+ data.tar.gz: a07444bc2d9d6a29b6b09303b5d4cef60709a4a4be0a31d2a4526f79366696875b72a36923747b673f05b686c857a78be803f2475fbbc89ffb3dac4a1c856c07
data/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ## 0.2.5 (2021-07-28)
2
+
3
+ - Added `anomalies` method
4
+
5
+ ## 0.2.4 (2021-04-02)
6
+
7
+ - Added support for flat growth
8
+
9
+ ## 0.2.3 (2020-10-14)
10
+
11
+ - Added support for times to `forecast` method
12
+
13
+ ## 0.2.2 (2020-07-26)
14
+
15
+ - Fixed error with constant series
16
+ - Fixed error with no changepoints
17
+
1
18
  ## 0.2.1 (2020-07-15)
2
19
 
3
20
  - Added `forecast` method
data/LICENSE.txt CHANGED
@@ -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,7 +10,7 @@ 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)
13
+ [![Build Status](https://github.com/ankane/prophet/workflows/build/badge.svg?branch=master)](https://github.com/ankane/prophet/actions)
14
14
 
15
15
  ## Installation
16
16
 
@@ -48,6 +48,12 @@ series = User.group_by_day(:created_at).count
48
48
  Prophet.forecast(series)
49
49
  ```
50
50
 
51
+ Detect anomalies in a time series
52
+
53
+ ```ruby
54
+ Prophet.anomalies(series)
55
+ ```
56
+
51
57
  ## Advanced API
52
58
 
53
59
  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.
@@ -118,7 +124,7 @@ Plot the forecast
118
124
  m.plot(forecast).savefig("forecast.png")
119
125
  ```
120
126
 
121
- ![Forecast](https://blazer.dokkuapp.com/assets/prophet/forecast-a9d43195b8ad23703eda7bb8b52b8a758efb4699e2313f32d7bbdfaa2f4275f6.png)
127
+ ![Forecast](https://blazer.dokkuapp.com/assets/prophet/forecast-77cf453fda67d1b462c6c22aee3a02572203b71c4517fedecc1f438cd374a876.png)
122
128
 
123
129
  Plot components
124
130
 
@@ -126,7 +132,7 @@ Plot components
126
132
  m.plot_components(forecast).savefig("components.png")
127
133
  ```
128
134
 
129
- ![Components](https://blazer.dokkuapp.com/assets/prophet/components-b9e31bfcf77e57bbd503c0bcff5e5544e66085b90709b06dd96c5f622a87d84f.png)
135
+ ![Components](https://blazer.dokkuapp.com/assets/prophet/components-2cdd260e23bc89824ecca25f6bfe394deb5821d60b7e0e551469c90d204acd67.png)
130
136
 
131
137
  ## Saturating Forecasts
132
138
 
data/lib/prophet.rb CHANGED
@@ -21,19 +21,22 @@ module Prophet
21
21
  Forecaster.new(**kwargs)
22
22
  end
23
23
 
24
- # to add time support in future, see
25
- # https://github.com/ankane/prophet/commit/06e3562835cbcf06b8431f3a91fe2618d4703eb7
26
24
  def self.forecast(series, count: 10)
27
25
  raise ArgumentError, "Series must have at least 10 data points" if series.size < 10
28
26
 
27
+ # check type to determine output format
28
+ # check for before converting to time
29
29
  keys = series.keys
30
- bad_key = keys.find { |k| !k.is_a?(Date) }
31
- raise ArgumentError, "Expected Date, got #{bad_key.class.name}" if bad_key
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)
32
34
 
33
- week = keys.map { |k| k.wday }.uniq.size == 1
34
- month = keys.all? { |k| k.day == 1 }
35
- quarter = month && keys.all? { |k| k.month % 3 == 1 }
36
- year = quarter && keys.all? { |k| k.month == 1 }
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 }
37
40
 
38
41
  freq =
39
42
  if year
@@ -44,10 +47,20 @@ module Prophet
44
47
  "MS"
45
48
  elsif week
46
49
  "W"
47
- else
50
+ elsif day
48
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"
49
61
  end
50
62
 
63
+ # use series, not times, so dates are handled correctly
51
64
  df = Rover::DataFrame.new({"ds" => series.keys, "y" => series.values})
52
65
 
53
66
  m = Prophet.new
@@ -56,6 +69,28 @@ module Prophet
56
69
 
57
70
  future = m.make_future_dataframe(periods: count, include_history: false, freq: freq)
58
71
  forecast = m.predict(future)
59
- forecast[["ds", "yhat"]].to_a.map { |v| [v["ds"].to_date, v["yhat"]] }.to_h
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
86
+
87
+ def self.anomalies(series)
88
+ df = Rover::DataFrame.new(series.map { |k, v| {"ds" => k, "y" => v} })
89
+ m = Prophet.new(interval_width: 0.99)
90
+ m.logger.level = ::Logger::FATAL # no logging
91
+ m.fit(df)
92
+ forecast = m.predict(df)
93
+ # filter df["ds"] to ensure dates/times in same format as input
94
+ df["ds"][(df["y"] < forecast["yhat_lower"]) | (df["y"] > forecast["yhat_upper"])].to_a
60
95
  end
61
96
  end
@@ -75,8 +75,8 @@ module Prophet
75
75
  end
76
76
 
77
77
  def validate_inputs
78
- if !["linear", "logistic"].include?(@growth)
79
- raise ArgumentError, "Parameter \"growth\" should be \"linear\" or \"logistic\"."
78
+ if !["linear", "logistic", "flat"].include?(@growth)
79
+ raise ArgumentError, "Parameter \"growth\" should be \"linear\", \"logistic\", or \"flat\"."
80
80
  end
81
81
  if @changepoint_range < 0 || @changepoint_range > 1
82
82
  raise ArgumentError, "Parameter \"changepoint_range\" must be in [0, 1]"
@@ -602,6 +602,12 @@ module Prophet
602
602
  [k, m]
603
603
  end
604
604
 
605
+ def flat_growth_init(df)
606
+ k = 0
607
+ m = df["y_scaled"].mean
608
+ [k, m]
609
+ end
610
+
605
611
  def fit(df, **kwargs)
606
612
  raise Error, "Prophet object can only be fit once" if @history
607
613
 
@@ -624,6 +630,8 @@ module Prophet
624
630
 
625
631
  set_changepoints
626
632
 
633
+ trend_indicator = {"linear" => 0, "logistic" => 1, "flat" => 2}
634
+
627
635
  dat = {
628
636
  "T" => history.shape[0],
629
637
  "K" => seasonal_features.shape[1],
@@ -634,7 +642,7 @@ module Prophet
634
642
  "X" => seasonal_features,
635
643
  "sigmas" => prior_scales,
636
644
  "tau" => @changepoint_prior_scale,
637
- "trend_indicator" => @growth == "logistic" ? 1 : 0,
645
+ "trend_indicator" => trend_indicator[@growth],
638
646
  "s_a" => component_cols["additive_terms"],
639
647
  "s_m" => component_cols["multiplicative_terms"]
640
648
  }
@@ -642,6 +650,9 @@ module Prophet
642
650
  if @growth == "linear"
643
651
  dat["cap"] = Numo::DFloat.zeros(@history.shape[0])
644
652
  kinit = linear_growth_init(history)
653
+ elsif @growth == "flat"
654
+ dat["cap"] = Numo::DFloat.zeros(@history.shape[0])
655
+ kinit = flat_growth_init(history)
645
656
  else
646
657
  dat["cap"] = history["cap_scaled"]
647
658
  kinit = logistic_growth_init(history)
@@ -655,12 +666,12 @@ module Prophet
655
666
  "sigma_obs" => 1
656
667
  }
657
668
 
658
- if history["y"].min == history["y"].max && @growth == "linear"
669
+ if history["y"].min == history["y"].max && (@growth == "linear" || @growth == "flat")
659
670
  # Nothing to fit.
660
671
  @params = stan_init
661
672
  @params["sigma_obs"] = 1e-9
662
- @params.each do |par|
663
- @params[par] = Numo::NArray.asarray(@params[par])
673
+ @params.each do |par, _|
674
+ @params[par] = Numo::NArray.asarray([@params[par]])
664
675
  end
665
676
  elsif @mcmc_samples > 0
666
677
  @params = @stan_backend.sampling(stan_init, dat, @mcmc_samples, **kwargs)
@@ -671,8 +682,10 @@ module Prophet
671
682
  # If no changepoints were requested, replace delta with 0s
672
683
  if @changepoints.size == 0
673
684
  # Fold delta into the base rate k
674
- @params["k"] = @params["k"] + @params["delta"].reshape(-1)
675
- @params["delta"] = Numo::DFloat.zeros(@params["delta"].shape).reshape(-1, 1)
685
+ # Numo doesn't support -1 with reshape
686
+ negative_one = @params["delta"].shape.inject(&:*)
687
+ @params["k"] = @params["k"] + @params["delta"].reshape(negative_one)
688
+ @params["delta"] = Numo::DFloat.zeros(@params["delta"].shape).reshape(negative_one, 1)
676
689
  end
677
690
 
678
691
  self
@@ -739,6 +752,11 @@ module Prophet
739
752
  cap.to_numo / (1 + Numo::NMath.exp(-k_t * (t - m_t)))
740
753
  end
741
754
 
755
+ def flat_trend(t, m)
756
+ m_t = m * t.new_ones
757
+ m_t
758
+ end
759
+
742
760
  def predict_trend(df)
743
761
  k = @params["k"].mean(nan: true)
744
762
  m = @params["m"].mean(nan: true)
@@ -747,9 +765,11 @@ module Prophet
747
765
  t = Numo::NArray.asarray(df["t"].to_a)
748
766
  if @growth == "linear"
749
767
  trend = piecewise_linear(t, deltas, k, m, @changepoints_t)
750
- else
768
+ elsif @growth == "logistic"
751
769
  cap = df["cap_scaled"]
752
770
  trend = piecewise_logistic(t, cap, deltas, k, m, @changepoints_t)
771
+ elsif @growth == "flat"
772
+ trend = flat_trend(t, m)
753
773
  end
754
774
 
755
775
  trend * @y_scale + Numo::NArray.asarray(df["floor"].to_a)
@@ -885,9 +905,11 @@ module Prophet
885
905
 
886
906
  if @growth == "linear"
887
907
  trend = piecewise_linear(t, deltas, k, m, changepoint_ts)
888
- else
908
+ elsif @growth == "logistic"
889
909
  cap = df["cap_scaled"]
890
910
  trend = piecewise_logistic(t, cap, deltas, k, m, changepoint_ts)
911
+ elsif @growth == "flat"
912
+ trend = flat_trend(t, m)
891
913
  end
892
914
 
893
915
  trend * @y_scale + Numo::NArray.asarray(df["floor"].to_a)
@@ -899,6 +921,9 @@ module Prophet
899
921
  # TODO add more freq
900
922
  # https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases
901
923
  case freq
924
+ when /\A\d+S\z/
925
+ secs = freq.to_i
926
+ dates = (periods + 1).times.map { |i| last_date + i * secs }
902
927
  when "H"
903
928
  hour = 3600
904
929
  dates = (periods + 1).times.map { |i| last_date + i * hour }
@@ -1,3 +1,3 @@
1
1
  module Prophet
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.5"
3
3
  end
@@ -73,6 +73,15 @@ functions {
73
73
  ) {
74
74
  return (k + A * delta) .* t + (m + A * (-t_change .* delta));
75
75
  }
76
+
77
+ // Flat trend function
78
+
79
+ vector flat_trend(
80
+ real m,
81
+ int T
82
+ ) {
83
+ return rep_vector(m, T);
84
+ }
76
85
  }
77
86
 
78
87
  data {
@@ -86,7 +95,7 @@ data {
86
95
  matrix[T,K] X; // Regressors
87
96
  vector[K] sigmas; // Scale on seasonality prior
88
97
  real<lower=0> tau; // Scale on changepoints prior
89
- int trend_indicator; // 0 for linear, 1 for logistic
98
+ int trend_indicator; // 0 for linear, 1 for logistic, 2 for flat
90
99
  vector[K] s_a; // Indicator of additive features
91
100
  vector[K] s_m; // Indicator of multiplicative features
92
101
  }
@@ -104,6 +113,17 @@ parameters {
104
113
  vector[K] beta; // Regressor coefficients
105
114
  }
106
115
 
116
+ transformed parameters {
117
+ vector[T] trend;
118
+ if (trend_indicator == 0) {
119
+ trend = linear_trend(k, m, delta, t, A, t_change);
120
+ } else if (trend_indicator == 1) {
121
+ trend = logistic_trend(k, m, delta, t, cap, A, t_change, S);
122
+ } else if (trend_indicator == 2) {
123
+ trend = flat_trend(m, T);
124
+ }
125
+ }
126
+
107
127
  model {
108
128
  //priors
109
129
  k ~ normal(0, 5);
@@ -113,19 +133,10 @@ model {
113
133
  beta ~ normal(0, sigmas);
114
134
 
115
135
  // Likelihood
116
- if (trend_indicator == 0) {
117
- y ~ normal(
118
- linear_trend(k, m, delta, t, A, t_change)
119
- .* (1 + X * (beta .* s_m))
120
- + X * (beta .* s_a),
121
- sigma_obs
122
- );
123
- } else if (trend_indicator == 1) {
124
- y ~ normal(
125
- logistic_trend(k, m, delta, t, cap, A, t_change, S)
126
- .* (1 + X * (beta .* s_m))
127
- + X * (beta .* s_a),
128
- sigma_obs
129
- );
130
- }
136
+ y ~ normal(
137
+ trend
138
+ .* (1 + X * (beta .* s_m))
139
+ + X * (beta .* s_a),
140
+ sigma_obs
141
+ );
131
142
  }
@@ -47,7 +47,7 @@ functions {
47
47
  }
48
48
  return gamma;
49
49
  }
50
-
50
+
51
51
  real[] logistic_trend(
52
52
  real k,
53
53
  real m,
@@ -94,6 +94,17 @@ functions {
94
94
  }
95
95
  return Y;
96
96
  }
97
+
98
+ // Flat trend function
99
+
100
+ real[] flat_trend(
101
+ real m,
102
+ int T
103
+ ) {
104
+ return rep_array(m, T);
105
+ }
106
+
107
+
97
108
  }
98
109
 
99
110
  data {
@@ -107,7 +118,7 @@ data {
107
118
  real X[T,K]; // Regressors
108
119
  vector[K] sigmas; // Scale on seasonality prior
109
120
  real<lower=0> tau; // Scale on changepoints prior
110
- int trend_indicator; // 0 for linear, 1 for logistic
121
+ int trend_indicator; // 0 for linear, 1 for logistic, 2 for flat
111
122
  real s_a[K]; // Indicator of additive features
112
123
  real s_m[K]; // Indicator of multiplicative features
113
124
  }
@@ -135,6 +146,8 @@ transformed parameters {
135
146
  trend = linear_trend(k, m, delta, t, A, t_change, S, T);
136
147
  } else if (trend_indicator == 1) {
137
148
  trend = logistic_trend(k, m, delta, t, cap, A, t_change, S, T);
149
+ } else if (trend_indicator == 2){
150
+ trend = flat_trend(m, T);
138
151
  }
139
152
 
140
153
  for (i in 1:K) {
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.2.1
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-15 00:00:00.000000000 Z
11
+ date: 2021-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdstan
@@ -52,78 +52,8 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: bundler
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: rake
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: minitest
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '5'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
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'
111
- - !ruby/object:Gem::Dependency
112
- name: matplotlib
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- description:
126
- email: andrew@chartkick.com
55
+ description:
56
+ email: andrew@ankane.org
127
57
  executables: []
128
58
  extensions:
129
59
  - ext/prophet/extconf.rb
@@ -148,7 +78,7 @@ homepage: https://github.com/ankane/prophet
148
78
  licenses:
149
79
  - MIT
150
80
  metadata: {}
151
- post_install_message:
81
+ post_install_message:
152
82
  rdoc_options: []
153
83
  require_paths:
154
84
  - lib
@@ -163,8 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
163
93
  - !ruby/object:Gem::Version
164
94
  version: '0'
165
95
  requirements: []
166
- rubygems_version: 3.1.2
167
- signing_key:
96
+ rubygems_version: 3.2.22
97
+ signing_key:
168
98
  specification_version: 4
169
99
  summary: Time series forecasting for Ruby
170
100
  test_files: []