13
13
14
14
from typing import List , Tuple
15
15
16
+ import numpy as np
17
+ import pandas as pd
16
18
from pandas .tseries .frequencies import to_offset
17
19
18
20
from gluonts .core .component import validated
@@ -89,14 +91,23 @@ def _make_2_block_diagonal(F, left: Tensor, right: Tensor) -> Tensor:
89
91
return _block_diagonal
90
92
91
93
94
+ class ZeroFeature (TimeFeature ):
95
+ """
96
+ A feature that is identically zero.
97
+ """
98
+
99
+ def __call__ (self , index : pd .DatetimeIndex ) -> np .ndarray :
100
+ return np .zeros (index .values .shape )
101
+
102
+
92
103
class ISSM :
93
104
r"""
94
105
An abstract class for providing the basic structure of Innovation State Space Model (ISSM).
95
106
96
107
The structure of ISSM is given by
97
108
98
109
* dimension of the latent state
99
- * transition and emission coefficents of the transition model
110
+ * transition and innovation coefficents of the transition model
100
111
* emission coefficient of the observation model
101
112
102
113
"""
@@ -111,22 +122,25 @@ def latent_dim(self) -> int:
111
122
def output_dim (self ) -> int :
112
123
raise NotImplementedError
113
124
114
- def emission_coeff (self , seasonal_indicators : Tensor ) :
125
+ def time_features (self ) -> List [ TimeFeature ] :
115
126
raise NotImplementedError
116
127
117
- def transition_coeff (self , seasonal_indicators : Tensor ):
128
+ def emission_coeff (self , features : Tensor ) -> Tensor :
118
129
raise NotImplementedError
119
130
120
- def innovation_coeff (self , seasonal_indicators : Tensor ):
131
+ def transition_coeff (self , features : Tensor ) -> Tensor :
132
+ raise NotImplementedError
133
+
134
+ def innovation_coeff (self , features : Tensor ) -> Tensor :
121
135
raise NotImplementedError
122
136
123
137
def get_issm_coeff (
124
- self , seasonal_indicators : Tensor
138
+ self , features : Tensor
125
139
) -> Tuple [Tensor , Tensor , Tensor ]:
126
140
return (
127
- self .emission_coeff (seasonal_indicators ),
128
- self .transition_coeff (seasonal_indicators ),
129
- self .innovation_coeff (seasonal_indicators ),
141
+ self .emission_coeff (features ),
142
+ self .transition_coeff (features ),
143
+ self .innovation_coeff (features ),
130
144
)
131
145
132
146
@@ -137,52 +151,47 @@ def latent_dim(self) -> int:
137
151
def output_dim (self ) -> int :
138
152
return 1
139
153
154
+ def time_features (self ) -> List [TimeFeature ]:
155
+ return [ZeroFeature ()]
156
+
140
157
def emission_coeff (
141
- self , seasonal_indicators : Tensor # (batch_size, time_length)
158
+ self , feature : Tensor # (batch_size, time_length, 1 )
142
159
) -> Tensor :
143
- F = getF (seasonal_indicators )
160
+ F = getF (feature )
144
161
145
162
_emission_coeff = F .ones (shape = (1 , 1 , 1 , self .latent_dim ()))
146
163
147
- # get the right shape: (batch_size, seq_length , obs_dim, latent_dim)
164
+ # get the right shape: (batch_size, time_length , obs_dim, latent_dim)
148
165
zeros = _broadcast_param (
149
- F .zeros_like (
150
- seasonal_indicators .slice_axis (
151
- axis = - 1 , begin = 0 , end = 1
152
- ).squeeze (axis = - 1 )
153
- ),
166
+ feature .squeeze (axis = 2 ),
154
167
axes = [2 , 3 ],
155
168
sizes = [1 , self .latent_dim ()],
156
169
)
157
170
158
171
return _emission_coeff .broadcast_like (zeros )
159
172
160
173
def transition_coeff (
161
- self , seasonal_indicators : Tensor # (batch_size, time_length)
174
+ self , feature : Tensor # (batch_size, time_length, 1 )
162
175
) -> Tensor :
163
- F = getF (seasonal_indicators )
176
+ F = getF (feature )
164
177
165
178
_transition_coeff = (
166
179
F .eye (self .latent_dim ()).expand_dims (axis = 0 ).expand_dims (axis = 0 )
167
180
)
168
181
169
- # get the right shape: (batch_size, seq_length , latent_dim, latent_dim)
182
+ # get the right shape: (batch_size, time_length , latent_dim, latent_dim)
170
183
zeros = _broadcast_param (
171
- F .zeros_like (
172
- seasonal_indicators .slice_axis (
173
- axis = - 1 , begin = 0 , end = 1
174
- ).squeeze (axis = - 1 )
175
- ),
184
+ feature .squeeze (axis = 2 ),
176
185
axes = [2 , 3 ],
177
186
sizes = [self .latent_dim (), self .latent_dim ()],
178
187
)
179
188
180
189
return _transition_coeff .broadcast_like (zeros )
181
190
182
191
def innovation_coeff (
183
- self , seasonal_indicators : Tensor # (batch_size, time_length)
192
+ self , feature : Tensor # (batch_size, time_length, 1 )
184
193
) -> Tensor :
185
- return self .emission_coeff (seasonal_indicators ).squeeze (axis = 2 )
194
+ return self .emission_coeff (feature ).squeeze (axis = 2 )
186
195
187
196
188
197
class LevelTrendISSM (LevelISSM ):
@@ -192,24 +201,23 @@ def latent_dim(self) -> int:
192
201
def output_dim (self ) -> int :
193
202
return 1
194
203
204
+ def time_features (self ) -> List [TimeFeature ]:
205
+ return [ZeroFeature ()]
206
+
195
207
def transition_coeff (
196
- self , seasonal_indicators : Tensor # (batch_size, time_length)
208
+ self , feature : Tensor # (batch_size, time_length, 1 )
197
209
) -> Tensor :
198
- F = getF (seasonal_indicators )
210
+ F = getF (feature )
199
211
200
212
_transition_coeff = (
201
213
(F .diag (F .ones (shape = (2 ,)), k = 0 ) + F .diag (F .ones (shape = (1 ,)), k = 1 ))
202
214
.expand_dims (axis = 0 )
203
215
.expand_dims (axis = 0 )
204
216
)
205
217
206
- # get the right shape: (batch_size, seq_length , latent_dim, latent_dim)
218
+ # get the right shape: (batch_size, time_length , latent_dim, latent_dim)
207
219
zeros = _broadcast_param (
208
- F .zeros_like (
209
- seasonal_indicators .slice_axis (
210
- axis = - 1 , begin = 0 , end = 1
211
- ).squeeze (axis = - 1 )
212
- ),
220
+ feature .squeeze (axis = 2 ),
213
221
axes = [2 , 3 ],
214
222
sizes = [self .latent_dim (), self .latent_dim ()],
215
223
)
@@ -223,26 +231,47 @@ class SeasonalityISSM(LevelISSM):
223
231
"""
224
232
225
233
@validated ()
226
- def __init__ (self , num_seasons : int ) -> None :
234
+ def __init__ (self , num_seasons : int , time_feature : TimeFeature ) -> None :
227
235
super (SeasonalityISSM , self ).__init__ ()
228
236
self .num_seasons = num_seasons
237
+ self .time_feature = time_feature
229
238
230
239
def latent_dim (self ) -> int :
231
240
return self .num_seasons
232
241
233
242
def output_dim (self ) -> int :
234
243
return 1
235
244
236
- def emission_coeff (self , seasonal_indicators : Tensor ) -> Tensor :
237
- F = getF (seasonal_indicators )
238
- return F .one_hot (seasonal_indicators , depth = self .latent_dim ())
245
+ def time_features (self ) -> List [TimeFeature ]:
246
+ return [self .time_feature ]
239
247
240
- def innovation_coeff (self , seasonal_indicators : Tensor ) -> Tensor :
241
- F = getF (seasonal_indicators )
242
- # seasonal_indicators = F.modulo(seasonal_indicators - 1, self.latent_dim)
243
- return F .one_hot (seasonal_indicators , depth = self .latent_dim ()).squeeze (
244
- axis = 2
245
- )
248
+ def emission_coeff (self , feature : Tensor ) -> Tensor :
249
+ F = getF (feature )
250
+ return F .one_hot (feature , depth = self .latent_dim ())
251
+
252
+ def innovation_coeff (self , feature : Tensor ) -> Tensor :
253
+ F = getF (feature )
254
+ return F .one_hot (feature , depth = self .latent_dim ()).squeeze (axis = 2 )
255
+
256
+
257
+ def MonthOfYearSeasonalISSM ():
258
+ return SeasonalityISSM (num_seasons = 12 , time_feature = MonthOfYearIndex ())
259
+
260
+
261
+ def WeekOfYearSeasonalISSM ():
262
+ return SeasonalityISSM (num_seasons = 53 , time_feature = WeekOfYearIndex ())
263
+
264
+
265
+ def DayOfWeekSeasonalISSM ():
266
+ return SeasonalityISSM (num_seasons = 7 , time_feature = DayOfWeekIndex ())
267
+
268
+
269
+ def HourOfDaySeasonalISSM ():
270
+ return SeasonalityISSM (num_seasons = 24 , time_feature = HourOfDayIndex ())
271
+
272
+
273
+ def MinuteOfHourSeasonalISSM ():
274
+ return SeasonalityISSM (num_seasons = 60 , time_feature = MinuteOfHourIndex ())
246
275
247
276
248
277
class CompositeISSM (ISSM ):
@@ -269,79 +298,53 @@ def latent_dim(self) -> int:
269
298
def output_dim (self ) -> int :
270
299
return self .nonseasonal_issm .output_dim ()
271
300
301
+ def time_features (self ) -> List [TimeFeature ]:
302
+ ans = self .nonseasonal_issm .time_features ()
303
+ for issm in self .seasonal_issms :
304
+ ans .extend (issm .time_features ())
305
+ return ans
306
+
272
307
@classmethod
273
308
def get_from_freq (cls , freq : str , add_trend : bool = DEFAULT_ADD_TREND ):
274
309
offset = to_offset (freq )
275
310
276
311
seasonal_issms : List [SeasonalityISSM ] = []
277
312
278
313
if offset .name == "M" :
279
- seasonal_issms = [
280
- SeasonalityISSM (num_seasons = 12 ) # month-of-year seasonality
281
- ]
314
+ seasonal_issms = [MonthOfYearSeasonalISSM ()]
282
315
elif offset .name == "W-SUN" :
283
- seasonal_issms = [
284
- SeasonalityISSM (num_seasons = 53 ) # week-of-year seasonality
285
- ]
316
+ seasonal_issms = [WeekOfYearSeasonalISSM ()]
286
317
elif offset .name == "D" :
287
- seasonal_issms = [
288
- SeasonalityISSM (num_seasons = 7 )
289
- ] # day-of-week seasonality
318
+ seasonal_issms = [DayOfWeekSeasonalISSM ()]
290
319
elif offset .name == "B" : # TODO: check this case
291
- seasonal_issms = [
292
- SeasonalityISSM (num_seasons = 7 )
293
- ] # day-of-week seasonality
320
+ seasonal_issms = [DayOfWeekSeasonalISSM ()]
294
321
elif offset .name == "H" :
295
322
seasonal_issms = [
296
- SeasonalityISSM ( num_seasons = 24 ), # hour-of-day seasonality
297
- SeasonalityISSM ( num_seasons = 7 ), # day-of-week seasonality
323
+ HourOfDaySeasonalISSM (),
324
+ DayOfWeekSeasonalISSM (),
298
325
]
299
326
elif offset .name == "T" :
300
327
seasonal_issms = [
301
- SeasonalityISSM ( num_seasons = 60 ), # minute-of-hour seasonality
302
- SeasonalityISSM ( num_seasons = 24 ), # hour-of-day seasonality
328
+ MinuteOfHourSeasonalISSM (),
329
+ HourOfDaySeasonalISSM (),
303
330
]
304
331
else :
305
332
RuntimeError (f"Unsupported frequency { offset .name } " )
306
333
307
334
return cls (seasonal_issms = seasonal_issms , add_trend = add_trend )
308
335
309
- @classmethod
310
- def seasonal_features (cls , freq : str ) -> List [TimeFeature ]:
311
- offset = to_offset (freq )
312
- if offset .name == "M" :
313
- return [MonthOfYearIndex ()]
314
- elif offset .name == "W-SUN" :
315
- return [WeekOfYearIndex ()]
316
- elif offset .name == "D" :
317
- return [DayOfWeekIndex ()]
318
- elif offset .name == "B" : # TODO: check this case
319
- return [DayOfWeekIndex ()]
320
- elif offset .name == "H" :
321
- return [HourOfDayIndex (), DayOfWeekIndex ()]
322
- elif offset .name == "T" :
323
- return [
324
- MinuteOfHourIndex (),
325
- HourOfDayIndex (),
326
- ]
327
- else :
328
- RuntimeError (f"Unsupported frequency { offset .name } " )
329
-
330
- return []
331
-
332
336
def get_issm_coeff (
333
- self , seasonal_indicators : Tensor # (batch_size, time_length)
337
+ self , features : Tensor # (batch_size, time_length, num_features )
334
338
) -> Tuple [Tensor , Tensor , Tensor ]:
335
- F = getF (seasonal_indicators )
339
+ F = getF (features )
336
340
emission_coeff_ls , transition_coeff_ls , innovation_coeff_ls = zip (
337
- self .nonseasonal_issm .get_issm_coeff (seasonal_indicators ),
338
341
* [
339
342
issm .get_issm_coeff (
340
- seasonal_indicators .slice_axis (
341
- axis = - 1 , begin = ix , end = ix + 1
342
- )
343
+ features .slice_axis (axis = - 1 , begin = ix , end = ix + 1 )
344
+ )
345
+ for ix , issm in enumerate (
346
+ [self .nonseasonal_issm ] + self .seasonal_issms
343
347
)
344
- for ix , issm in enumerate (self .seasonal_issms )
345
348
],
346
349
)
347
350
0 commit comments