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
"""
@@ -106,27 +117,30 @@ def __init__(self):
106
117
pass
107
118
108
119
def latent_dim (self ) -> int :
109
- raise NotImplemented ()
120
+ raise NotImplementedError
110
121
111
122
def output_dim (self ) -> int :
112
- raise NotImplemented ()
123
+ raise NotImplementedError
124
+
125
+ def time_features (self ) -> List [TimeFeature ]:
126
+ raise NotImplementedError
113
127
114
- def emission_coeff (self , seasonal_indicators : Tensor ):
115
- raise NotImplemented ()
128
+ def emission_coeff (self , features : Tensor ) -> Tensor :
129
+ raise NotImplementedError
116
130
117
- def transition_coeff (self , seasonal_indicators : Tensor ):
118
- raise NotImplemented ()
131
+ def transition_coeff (self , features : Tensor ) -> Tensor :
132
+ raise NotImplementedError
119
133
120
- def innovation_coeff (self , seasonal_indicators : Tensor ):
121
- raise NotImplemented ()
134
+ def innovation_coeff (self , features : Tensor ) -> Tensor :
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)
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
164
# get the right shape: (batch_size, seq_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 .slice_axis (axis = - 1 , begin = 0 , end = 1 ).squeeze (axis = - 1 ),
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)
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
182
# get the right shape: (batch_size, seq_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 .slice_axis (axis = - 1 , begin = 0 , end = 1 ).squeeze (axis = - 1 ),
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)
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,10 +201,13 @@ 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)
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 ))
@@ -205,11 +217,7 @@ def transition_coeff(
205
217
206
218
# get the right shape: (batch_size, seq_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 .slice_axis (axis = - 1 , begin = 0 , end = 1 ).squeeze (axis = - 1 ),
213
221
axes = [2 , 3 ],
214
222
sizes = [self .latent_dim (), self .latent_dim ()],
215
223
)
@@ -223,26 +231,28 @@ 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
+ # feature = F.modulo(feature - 1, self.latent_dim)
255
+ return F .one_hot (feature , depth = self .latent_dim ()).squeeze (axis = 2 )
246
256
247
257
248
258
class CompositeISSM (ISSM ):
@@ -269,6 +279,12 @@ def latent_dim(self) -> int:
269
279
def output_dim (self ) -> int :
270
280
return self .nonseasonal_issm .output_dim ()
271
281
282
+ def time_features (self ) -> List [TimeFeature ]:
283
+ ans = self .nonseasonal_issm .time_features ()
284
+ for issm in self .seasonal_issms :
285
+ ans .extend (issm .time_features ())
286
+ return ans
287
+
272
288
@classmethod
273
289
def get_from_freq (cls , freq : str , add_trend : bool = DEFAULT_ADD_TREND ):
274
290
offset = to_offset (freq )
@@ -277,71 +293,63 @@ def get_from_freq(cls, freq: str, add_trend: bool = DEFAULT_ADD_TREND):
277
293
278
294
if offset .name == "M" :
279
295
seasonal_issms = [
280
- SeasonalityISSM (num_seasons = 12 ) # month-of-year seasonality
296
+ SeasonalityISSM ( # month-of-year seasonality
297
+ num_seasons = 12 , time_feature = MonthOfYear (normalized = False )
298
+ )
281
299
]
282
300
elif offset .name == "W-SUN" :
283
301
seasonal_issms = [
284
- SeasonalityISSM (num_seasons = 53 ) # week-of-year seasonality
302
+ SeasonalityISSM ( # week-of-year seasonality
303
+ num_seasons = 53 , time_feature = WeekOfYear (normalized = False )
304
+ )
285
305
]
286
306
elif offset .name == "D" :
287
307
seasonal_issms = [
288
- SeasonalityISSM (num_seasons = 7 )
289
- ] # day-of-week seasonality
308
+ SeasonalityISSM ( # day-of-week seasonality
309
+ num_seasons = 7 , time_feature = DayOfWeek (normalized = False )
310
+ )
311
+ ]
290
312
elif offset .name == "B" : # TODO: check this case
291
313
seasonal_issms = [
292
- SeasonalityISSM (num_seasons = 7 )
293
- ] # day-of-week seasonality
314
+ SeasonalityISSM ( # day-of-week seasonality
315
+ num_seasons = 7 , time_feature = DayOfWeek (normalized = False )
316
+ )
317
+ ]
294
318
elif offset .name == "H" :
295
319
seasonal_issms = [
296
- SeasonalityISSM (num_seasons = 24 ), # hour-of-day seasonality
297
- SeasonalityISSM (num_seasons = 7 ), # day-of-week seasonality
320
+ SeasonalityISSM ( # hour-of-day seasonality
321
+ num_seasons = 24 , time_feature = HourOfDay (normalized = False )
322
+ ),
323
+ SeasonalityISSM ( # day-of-week seasonality
324
+ num_seasons = 7 , time_feature = DayOfWeek (normalized = False )
325
+ ),
298
326
]
299
327
elif offset .name == "T" :
300
328
seasonal_issms = [
301
- SeasonalityISSM (num_seasons = 60 ), # minute-of-hour seasonality
302
- SeasonalityISSM (num_seasons = 24 ), # hour-of-day seasonality
329
+ SeasonalityISSM ( # minute-of-hour seasonality
330
+ num_seasons = 60 , time_feature = MinuteOfHour (normalized = False )
331
+ ),
332
+ SeasonalityISSM ( # hour-of-day seasonality
333
+ num_seasons = 24 , time_features = HourOfDay (normalized = False )
334
+ ),
303
335
]
304
336
else :
305
337
RuntimeError (f"Unsupported frequency { offset .name } " )
306
338
307
339
return cls (seasonal_issms = seasonal_issms , add_trend = add_trend )
308
340
309
- @classmethod
310
- def seasonal_features (cls , freq : str ) -> List [TimeFeature ]:
311
- offset = to_offset (freq )
312
- if offset .name == "M" :
313
- return [MonthOfYear (normalized = False )]
314
- elif offset .name == "W-SUN" :
315
- return [WeekOfYear (normalized = False )]
316
- elif offset .name == "D" :
317
- return [DayOfWeek (normalized = False )]
318
- elif offset .name == "B" : # TODO: check this case
319
- return [DayOfWeek (normalized = False )]
320
- elif offset .name == "H" :
321
- return [HourOfDay (normalized = False ), DayOfWeek (normalized = False )]
322
- elif offset .name == "T" :
323
- return [
324
- MinuteOfHour (normalized = False ),
325
- HourOfDay (normalized = False ),
326
- ]
327
- else :
328
- RuntimeError (f"Unsupported frequency { offset .name } " )
329
-
330
- return []
331
-
332
341
def get_issm_coeff (
333
- self , seasonal_indicators : Tensor # (batch_size, time_length)
342
+ self , features : Tensor # (batch_size, time_length)
334
343
) -> Tuple [Tensor , Tensor , Tensor ]:
335
- F = getF (seasonal_indicators )
344
+ F = getF (features )
336
345
emission_coeff_ls , transition_coeff_ls , innovation_coeff_ls = zip (
337
- self .nonseasonal_issm .get_issm_coeff (seasonal_indicators ),
338
346
* [
339
347
issm .get_issm_coeff (
340
- seasonal_indicators .slice_axis (
341
- axis = - 1 , begin = ix , end = ix + 1
342
- )
348
+ features .slice_axis (axis = - 1 , begin = ix , end = ix + 1 )
349
+ )
350
+ for ix , issm in enumerate (
351
+ [self .nonseasonal_issm ] + self .seasonal_issms
343
352
)
344
- for ix , issm in enumerate (self .seasonal_issms )
345
353
],
346
354
)
347
355
0 commit comments