-
Notifications
You must be signed in to change notification settings - Fork 12
/
BVAR_NIG.py
3045 lines (2617 loc) · 155 KB
/
BVAR_NIG.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""
Created on Tue Jun 20 13:30:37 2017
@author: Jeremias Knoblauch ([email protected])
Description: Implements Bayesian Linear Autoregression with the NIG model
(i.e., spatial locations have iid errors with common variance)
"""
import numpy as np
from scipy import special
from scipy import linalg
from scipy import stats
import scipy
from probability_model import ProbabilityModel
from nearestPD import NPD
class BVARNIG(ProbabilityModel):
"""The Bayesian Vector Autoregression model using past observations as
regressors in a specified neighbourhood. E.g., if the 4-neighbourhood is
selected with lag length 1, then the mean of y_{t,i} is modelled as linear
combination of observations y_{t-1, j} \in nb(i). Around the boundary,
the neighbourhoods are 0-padded.
###****************************************************************###
### MODEL PRIORS ###
###****************************************************************###
Inputs always needed. They correspond to priors in the following model:
Y ~ N(X*beta, sigma2 * I),
beta ~ N(beta_0, sigma2 * V_0),
sigma2 ~ IG(a,b)
prior_a: float >0:
a parameter of the Inverse Gamma, a>0
prior_b: float >0:
b parameter of the Inverse Gamma, b>0
prior_mean_beta: 1D-numpy array of size k, k = num regressors:
corresponds to beta_0, i.e. the mean prior of coefficients.
Takes precedence over prior_mean_scale if both specified.
prior_var_beta: 2D-numpy array of size kxk, k = num regressors:
corresponds to V_0, i.e. the covariance prior of coefs
Takes precedence over prior_var_scale if both specified.
prior_mean_scale: float:
If prior_mean_beta is None, prior_mean_scale supplied, the number
of regressors k is calculated automatically and
beta_0 = prior_mean_scale * np.ones(k)
prior_var_scale: float >0:
If prior_var_beta is None, prior_var_scale supplied, the number
of regressors k is calculated automatically and
beta_0 = prior_var_scale * np.identity(k)
###****************************************************************###
### REGULAR GRID + STRONG PARAM BINDING ###
###****************************************************************###
Inputs needed when assuming regular grid with strong parameter binding:
nbh_sequence, restriction_sequence, padding
nbh_sequence: array with integer entries, only needed if data on
regular grid, with strong coupling between effects:
0, 4, 8 -> specify the sequence ofVAR-nbhs.
corresponds to strong parameter coupling on regular
grid with no neighbourhood (0), 4-neighbourhood (4),
and 8-neighbourhood (8). I.e. all locations are on
a grid defining their nbhs, and share params.
(See restriction_sequence for param sharing)
restriction_sequence: array with integer entries, only needed if data
on regular grid, with strong coupling between effects:
0, 4, 8 -> specify the restriction of nbh_sequence on regular
spatial grid with parameter coupling.
Regardless of 0,4,8, we always couple across all
LOCATIONS! I.e., params the same across the grid.
However, we can vary how much we couple params within
each location's nbh: Not at all, i.e. one parameter
for each nbh location relative to each location (0),
the 4 inner and the 4 outer (4), and in the case of
a 8-nbh, all 8 together (8). See Fig. 2 in the paper
for illustration of 4-nbh (red), 8 nbh (red + blue),
0 nbh (orange).
NOTE: The parameter bindings for the intercepts are
again specified via intercept_grouping (see below).
They are NOT strongly coupled unless the argument
is not specified or is supplied as None.
padding: string:
ONLY needed if we specify nbh_sequence and restriction_sequence,
implying that we are on a regular grid. Then, we need to pad the
outside of the grid using one of the below options:
'overall_mean' -> compute mean across space and fill in
'row_col_mean' -> compute row and col means and fill in
'zero' -> insert zeros (bias estimation towards 0)
'leave-out' -> don't pad at all, and estimate only using
locations with full neighbourhood
###****************************************************************###
### GENERAL NBHS + ANY PARAM BINDING ###
###****************************************************************###
Inputs needed when assuming general nbh structures with arbitrary
parameter bindings:
intercept_grouping, general_nbh_sequence,
general_nbh_restriction_sequence , general_nbh_coupling
intercept_grouping: GxS1xS2 numpy array of ones or zeros grouping the
locations into G groups so that each group shares the intercept.
Notice that summing over the G-dimension, we would get an S1xS2
array of only ones. I.e., each location has to be in one of the G
groups. Extreme cases: G=1 with a single slice of ones => all
locations have one shared intercept. G=S1*S2 with each of the G
slicescontaining exactly a single 1-entry and only zeros otherwise
=> each location has individual intercept.
general_nbh_sequence: list of list of lists:
Gives an nparray of nparrays of nparrays of
coordinates/identifiers, i.e. an object like
[[[2,3,4],[5,6],[7]], [[5,6],[8],[9,10]], ...].
Here, [2,3,4] would be the 'closest' nbh to the
point with spatial coordinate 0, [5,6] the second-
closest, [7] the third-closest. how far away from
the closest nbh you consider the data is implied
by the general_nbh_restriction_sequence that
will give you the indices of the nbhs to be
considered for each lag length.
In the notation of the PAPER, this gives you the nbh. system as
[[N_1(1), N_2(1), N_3(1)], [N_1(2), N_2(2), N_2(3)], ...], i.e.
list entry s belongs to location with index s and contains n neigh-
bourhoods N_1(s), ... N_n(s) s.t. the indices describe spatial
closeness, with smaller indices indicating that we are closer to s.
Note that we assume n to be the same for all locations. If there
is a case where you assume that some locations s have less nbhs
than others, simply add some empty nbhs, i.e. N_i(s) = [].
general_nbh_restriction_sequence: list of list:
Gives you a list of lists of indices, i.e.
[[0,1,2,3], [0,1,2], [0],[]], where it must hold that
later entries are strict subsets of previous ones
s.t. the largest value at position l is at most as
big as the largest value at position l-1. Also, if
k is the largest value at position l, it must hold
that all k' s.t. 0<=k'<=k must be in that list entry
NOTE: If you want to have only auto-regressive
terms at some nbh (I.e., only its own past influen-
ces the present), then simply add an empty list [].
In the notation of the PAPER, this is the function p(.) assigning
temporal meaning to the neighbourhood structure. p is given in
list form so that the l-th entry of p gives all indices that
are going to be used at lag length l. I.e., assuming p(l) = 3
(using p's definition from the paper), it will hold that the
respective entry in general_nbh_restriction_sequence is going to
be [0,1,2]. More generally, for p(l) = k, [0,1,...,k-1].
general_nbh_coupling: string:
["no coupling", "weak coupling",
"strong coupling"], tells you how neighbourhoods
are tied together. "no coupling" means that each
linear effect of s' \in N_i(s) is modelled sepa-
rately. "weak coupling" means that the linear
effect for all s' \in N_i(s) are modelled together,
and "strong coupling" means that the linear effects
are also modelled together across space, i.e.
s' \in N_i(s) and g \in N_i(k) have the same effect
(but s' in N_j(s) and g in N_i(k) do not)
NOTE: no coupling is not implemented, because you
can obtain the same effect by weak coupling and
treating each station as its own nbh.
In the PAPER, notes on this are right after SSBVAR definition.
"weak coupling" is the standard modelling framework that assumes
that for all locations in a given nbh, we have a single linear
effect. "strong coupling" means that in addition, we have the same
linear neighbourhood effect for each location.
###****************************************************************###
### HYPERPARAMETER LEARNING ###
###****************************************************************###
Inputs needed when doing hyperparameter learning:
hyperparameter_optimization [don't use auto_prior_update!]
hyperparameter_optimization (ProbabilityModel level): string or None:
-> [True, False, None, "caron", "online", "turner"]
by default, this is True, which amounts to updating
the gradients but not performing on-line/caron's
hyperpar. learning. If False or None, the gradients
are not updated. "caron" and "online" both employ
the on-line hyperparameter learning proposed by
Caron, Doucet, Gottardo (2012). If you don't want
this, but only want to do Turner's routine, you
have to do so via an enclosing HyperparameterOptimization
object. For this, put hyperparameter_optimization
to True (default) or "turner".
I.e., "turner", "True" mean that gradients are updated recursively,
but not used (unless an enclosing HyperparameterOptimization
object uses them), "caron" and "online" mean that we perform
gradient descent updates as in the PAPER. "False" and None mean
that we don't update the gradients. (barely any computational
benefit in so doing)
auto_prior_update: boolean.
Basically, DON'T set to True. It updates the priors by setting them
to the posterior expectations at time t. For instance, the beta_0
prior at time t will be set to
sum_r{ beta_rt[r,:] * P(r|y_1:t) }.
###****************************************************************###
### EXOGENEOUS/ADDITIONAL PREDICTORS ###
###****************************************************************###
NOT IMPLEMENTED!!!!
Inputs needed when using additional variables:
exo_selection, nbh_sequence_exo
NOTE: Intercepts, EXO, and ENDO vars can always ge grouped by the
following simple procedure: Suppose you have two groups G1, G2.
Let's assume you assume the same model in G1, G2 but with diff-
erent parameterizations. Lets say the params you want are
a(G1), a(G2), b(G1), b(G2). Then you can just estimate all four
coefs jointly by having G1 have a column of zeros for the var
corresponding to a(G2), b(G2) and vice versa.
NOTE: At some point, it may be good to replace the strings indicating
our neighbourhood structures using integers instead, since
string-computations are more expensive than integer-computations
exo_selection:
0,1,2,.. -> gives you a selection vector of length
num_exo_vars allowing you to select which exos
you want to regress on Y. The integers are
the row index in vector [exo1, exo2, ...] of
regressors available at each location.
nbh_sequence_exo: #not in the input
0,4,8 -> gives you the nbh of the lagged exos that are
regressors for your problem. Starts at time t
(rather than t-1, as for endo sequence)
###****************************************************************###
### OTHER INPUTS ###
###****************************************************************###
None of these inputs are needed, they provide additional functionality
non_spd_alerts: boolean:
Gives an alert whenever the covariance matrix was not semi-positive
definite and needed to be converted into an spd-matrix by forcing
it via 'nearestPD' or adding a disturbance.
NOTE: If you experience this a lot, try to rescale your data, i.e.
normalize it on-line or do something along the same lines.
"""
"""~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
""" OBJECT INITIALIZATION FUNCTIONS """
"""~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
def __init__(self,
prior_a,
prior_b,
S1,
S2,
prior_mean_beta=None,
prior_var_beta=None,
prior_mean_scale=0,
prior_var_scale=100,
nbh_sequence=None,
restriction_sequence = None,
intercept_grouping = None,
general_nbh_sequence = None,
general_nbh_restriction_sequence = None,
exo_selection = None,
padding = 'overall_mean',
#deprecated argument, should go
auto_prior_update=False,
hyperparameter_optimization = "online",
general_nbh_coupling = "strong coupling",
non_spd_alerts =False
):
"""STEP 1: Store priors"""
self.a, self.b = prior_a, prior_b
"""if beta_0 or beta's covariance matrix are specified, that takes
precedence over a supplied scaling of a vector/matrix of ones"""
if not prior_mean_beta is None:
self.prior_mean_beta = prior_mean_beta.flatten()
else:
self.prior_mean_beta= prior_mean_beta
self.prior_var_beta= prior_var_beta
"""STEP 2: Store execution parameters"""
self.auto_prior_update = auto_prior_update #Don't use
if (hyperparameter_optimization is not None or
hyperparameter_optimization is not False):
self.a_old = prior_a + 0.0000001 #Used for gradient computation
self.b_old = prior_b+ 0.0000001 #Used for gradient computation
self.gradient_old = 0.0 #Used for gradient computation
self.a_list, self.b_list = [],[]
self.hyperparameter_optimization = hyperparameter_optimization
self.non_spd_alerts = non_spd_alerts #if cov mat not spd and forced
#to be, this alerts you.
"""STEP 3: Get informations about the model we set up"""
self.has_lags = True #needed inside detector
self.generalized_bayes_rld = "kullback_leibler" #changed from inside detector init
self.alpha_rld_learning = False
self.alpha_rld = None #changed from inside detector init
self.S1, self.S2 = S1, S2
"""STEP 3.1: If we are on a regular grid with strong param binding"""
self.restriction_sequence = restriction_sequence
self.nbh_sequence = nbh_sequence
self.padding = padding
"""STEP 3.2: If we are on general neighbourhood structures"""
self.general_nbh_sequence = general_nbh_sequence
self.general_nbh_restriction_sequence = general_nbh_restriction_sequence
self.general_nbh_coupling = general_nbh_coupling
self.intercept_grouping = intercept_grouping
"""STEP 3.3: Check if we use regular grid + strong param binding or
the more general framework"""
if ((not self.restriction_sequence is None) and
(not self.nbh_sequence is None) and
(not self.padding is None)):
self.regular_grid = True
elif ((not self.general_nbh_sequence is None) and
(not self.general_nbh_restriction_sequence is None) and
(not self.general_nbh_coupling is None)):
self.regular_grid = False
elif (( self.restriction_sequence is None) and
( self.nbh_sequence is None) and
( self.general_nbh_sequence is None) and
( self.general_nbh_restriction_sequence is None)):
#In this case, we have only constant terms
self.regular_grid = False
self.has_lags = False
self.lag_length = 0 #unclear if it is arrived at automatically
self.general_nbh_coupling = None
else:
"""Neither specification is complete, so end the execution here"""
raise SystemExit("Your neighbourhood specifications " +
"are incomplete: At least one of " +
"restriction_sequence, nbh_sequence, padding is None; " +
"or at least one of " +
"general_nbh_sequence, general_nbh_restriction_sequence ,"+
" general_nbh_coupling is None")
"""STEP 3.4: If we have any exogeneous/additional variables"""
if exo_selection is None or exo_selection == []:
self.exo_bool = False
exo_selection = []
self.exo_selection = []
else:
self.exo_bool = True
self.exo_selection = exo_selection
"""STEP 4: Convert the neighbourhood into a sequence of strings
for the endogeneous variables"""
"""STEP 4.1: Get the codes for the intercept design"""
self.get_intercept_codes()
"""STEP 4.2: Get endogeneous regressor codes (self.endo_vars), lag
length (self.lag_length), and information about empty nbhs
(self.empty_nbhs, self.sum_empty_nbhs_per_lag)"""
#DEBUG: Not needed under constant fct. Simply set self.endo_var=[].
# do this inside fct.
self.get_endo_vars()
"""STEP 4.3: Get exogeneous regressor codes (self.exo_vars)"""
self.exo_vars = [self.intercept_codes + exo_selection]
"""STEP 4.4: Get all regressor codes"""
self.all_vars = list(self.exo_vars) + list(self.endo_vars)
self.all_vars = sum(self.all_vars, [])
"""STEP 5: Define quantities relating to the regressors:
the sequences of variables, the counts of variables,
the lag-structure, extraction list for updating"""
"""STEP 5.1: Get the number of each type of variable"""
self.num_exo_regressors = len(sum(self.exo_vars, []))
self.num_endo_regressors = len(sum(self.endo_vars, []))
self.num_regressors = (self.num_endo_regressors +
self.num_exo_regressors)
"""STEP 5.2: Get the lag structure such that lag_counts stores the
#exo_vars at position 0,and stores at position l the count
{#exo_vars + sum(#endo_vars: lag <= l) inside
self.lag_counts"""
#DEBUG: For constant function, this should be only the first line of
# the function
self.get_lag_counts()
"""STEP 6: Get the extraction vector and the insertion position. Note
that the result will be a list of form [1,1,1,0,0,1,1,1,1,0,0,0], which
means that the first 3 endogeneous variables will be kept, the next
two will be discarded, the next 4 will be kept, and the next 3 disc."""
"""STEP 6.1: Store in each entry l the number of endogeneous regressors
for lag l"""
#For constant fct, this should just return an empty list (if se set lag_length = 0)
endo_regressors_per_lag = self.get_endo_regressors_per_lag()
"""STEP 6.2: You can now get a list that tells you for given X_t-1
which columns need copying to X_t. You never copy exogeneous variables.
Also, the first lag for X_t will be new, so one can copy at most
lag_length -1 neighbourhoods from X_t-1 to X_t. Store this list as
self.extraction_list, and the position where you start extracting
as self.insertion_position with the function below"""
#DEBUG: This should still work and return an empty extraction list as
# well as an insertion position = p
self.get_extraction_list(endo_regressors_per_lag)
"""STEP 7: create the objects we need to trace through time"""
self.XX, self.YX, self.model_log_evidence = None, None, -np.inf
"""NOTE: The quantities below will be re-initialized in the
initialization function, but have to be instantated here due to how
the enclosing Detector object calls model_and_run_length_distr"""
self.retained_run_lengths = np.array([0,0])
self.joint_log_probabilities = 1
#DEBUG: Should not really be here (but insted in initialization)
self.log_alpha_derivatives_joint_probabilities = None #np.ones(3)
self.log_alpha_derivatives_joint_probabilities_sign = None #np.ones(3)
"""STEP 8: Rectify prior_beta_mean and prior_beta_var if needed.
Give a warning about this, too!"""
"""STEP 8.1: prior mean beta is not supplied or does not correspond
to the right dimensions: Check if a scale is
supplied. If not, automatically set the scale to 0.0, ensuring
that beta_0 = 0."""
if (self.prior_mean_beta is None or
self.num_regressors != np.size(self.prior_mean_beta)):
if prior_mean_scale is None:
prior_mean_scale = 0.0
self.prior_mean_beta = (prior_mean_scale*
np.ones(self.num_regressors))
"""STEP 8.2: prior var beta is not supplied or does not correspond
to the right dimensions: Check if a scale is
supplied. If not, automatically set the scale to 100.0, ensuring
that V_0 = 100*I."""
if (self.prior_var_beta is None or
self.num_regressors != prior_var_beta.shape[0] or
self.num_regressors != prior_var_beta.shape[1]):
if prior_var_scale is None:
prior_var_scale = 100.0
self.prior_var_beta = (prior_var_scale*
np.identity(self.num_regressors))
def get_intercept_codes(self):
"""Only called in __init__: Gets the intercept regressor codes"""
if (self.intercept_grouping is None or
self.intercept_grouping == np.array([])):
self.intercept_codes = ["intercept"]
else:
self.num_intercept_groups = self.intercept_grouping.shape[0]
self.intercept_codes = []
for g in range(0, self.num_intercept_groups):
self.intercept_codes.append(("intercept_group_" + str(g)))
def get_endo_vars(self):
"""Only called in __init__: Gets self.endo_vars, self.lag_length,
self.empty_nbhs, self.sum_empty_nbhs_per_lag in different ways,
depending on how your nbh structure is set up."""
endo_vars = []
""""STEP A: If you are on regular grid with strong parameter binding"""
if self.regular_grid:
self.lag_length = np.size(self.nbh_sequence)
for lag in range(0,int(self.lag_length)):
restriction = self.restriction_sequence[lag]
nbh = self.nbh_sequence[lag]
if restriction == 0:
if nbh == 0:
endo_vars.append(["center"])
elif nbh == 4:
endo_vars.append([ "center","top", "left", "right",
"bottom"])
elif nbh == 8:
endo_vars.append(["center",
"top", "left", "right", "bottom",
"topleft", "topright","bottomleft", "bottomright"])
elif restriction == 4:
if nbh == 0:
endo_vars.append(["center"])
print("Warning: Restriction sequence")
print("contained 4, nbh sequence a 1-nbh")
print("at the same position.\n")
elif nbh == 4:
endo_vars.append(["center", "4_inner_nbh_res"])
elif nbh == 8:
endo_vars.append(["center", "4_outer_nbh_res",
"4_inner_nbh_res"])
elif restriction == 8:
if nbh == 0:
endo_vars.append(["center"])
print("Warning: Restriction sequence")
print("contained 8, nbh sequence a 1-nbh")
print("at the same position.\n")
elif nbh == 4:
endo_vars.append(["center", "4_inner_nbh_res"])
print("Warning: Restriction sequence")
print("contained 8, nbh sequence a 4-nbh")
print("at the same position.\n")
elif nbh == 8:
endo_vars.append(["center", "8_nbh_res"])
print("Warning: Restriction = 8, which is not fully implemented")
elif self.general_nbh_coupling == "weak coupling":
"""STEP B: If we use the general nbh sequence formulation with
weak coupling (i.e. nbh-specific, but not across space).
Recall that the structure is as follows:
general_nbh_sequence = [[[4,5,6],[7,8],[9]], [[2,3,4],[5],[7]],...]
general_nbh_restriction_sequence = [[0,1,2],[0,1,2],[0,1],[2]].
Here, lag_length = 4, general_nbh_restriction_sequence[lag] = g(l),
where g(l) gives you the index of the nbh generating the regressors
at lag length l for s, i.e. N_p(l)(s)
We want to get strings of form
general_nbh_<lag>_<nbh_index>_<loc>,
where <lag> gives you the index in general_nbh_restriction_seq that
you need, say <lag> = 0, i.e. we care about [0,1,2]. Given this
index list, <nbh_index> then tells us which of the indices (and
thus neighbourhoods) we care about, i.e. nbh_index = 0 would mean
we care about [0,1,2][0] = [0]. Lastly, the <loc> tells us which
index on the lattice we care about, allowing us to retrieve
general_nbh_sequence[<loc>][general_nbh_restriction_seq[<lag>][<nbh_index>]]
as the indices of the nbh with <nbh_index> corresponding to
<loc>'s neighbourhood at lag <lag>+1
"""
self.lag_length = int(len(self.general_nbh_restriction_sequence))
self.empty_nbhs = [] #helps us to sort out the extraction list later
self.sum_empty_nbhs_per_lag = np.zeros(self.lag_length)
"""loop I: Go over all lag lengths, since the nbhs and their
restrictions will differ between lag lengths"""
for lag in range(0, int(self.lag_length)):
new_endo_vars_entry = []
"""Loop II: over all locations to fill self.endo_vars with the
correct endogeneous variables for each location and lag"""
for location in range(0, self.S1*self.S2):
#DEBUG: This marks the center for each location separately
# make sure that this does not cause problems for how
# we find the lag (e.g., by counting # of "center"s)
new_endo_vars_entry.append("general_nbh_" +
str(lag) + "_" + "center" + "_" +
str(location))
self.empty_nbhs.append(False)
relevant_nbh_indices = self.general_nbh_restriction_sequence[lag]
"""Loop III: Over all relevant nbh indices for this
location at the current lag. This makes sure that our
endo var codes are specific to lag, location, and the
neighbour whose values are used."""
for nbh_index in relevant_nbh_indices:
"""Only add the nbh if it is non-empty. If it is
empty, nbh_index will have boolean value False."""
if nbh_index:
"""Again, we only want to create the regressor code
if the list is non-empty. If it is empty, we
instead note so inside self.empty_nbhs and
self.sum_empty_nbhs_per_lag in the 'else' cond."""
if self.general_nbh_sequence[location][nbh_index]:
new_endo_vars_entry.append("general_nbh_" +
str(lag) + "_" + str(nbh_index) + "_" +
str(location))
self.empty_nbhs.append(False)
else:
"""Mark which neighbourhoods were left out because
they were empty. Needed for extraction_list and
lag_counts"""
self.empty_nbhs.append(True)
self.sum_empty_nbhs_per_lag[lag] += 1
"""Inside Loop II: For this location and lag, add the
required endogeneous variables into the collection of all
of them"""
endo_vars.append(new_endo_vars_entry)
new_endo_vars_entry = []
elif self.general_nbh_coupling == "strong coupling":
"""STEP C: In this case, we have the same input as for weak
coupling, but a different interpretation. In particular, we couple
the effects over different spatial locations. Accordingly, we save
general_nbh_<lag>_<nbh_index> only.
Then, in the extractors, we loop over <loc> to retrieve the
regressors in a single column as
regressor(<lag>, <nbh_index>)[<loc>] = sum over all measurements
at time t - <lag> for nbh given by
gen_nbh_seq[<loc>][gen_nbh_res_seq[<lag>][<nbh]].
"""
self.lag_length = int(len(self.general_nbh_restriction_sequence))
"""Loop I: Over the lags"""
for lag in range(0, int(self.lag_length)):
new_endo_vars_entry = ["general_nbh_" + str(lag) + "_center"]
relevant_nbh_indices = self.general_nbh_restriction_sequence[lag]
"""Loop II: Over the relevant nbhs. Notice that unlike for the
weak coupling, we only have 2 (rather than 3) loops, as the
locations do not require a separate loop for strong coupling"""
for nbh_index in relevant_nbh_indices:
new_endo_vars_entry.append("general_nbh_" +
str(lag) + "_" + str(nbh_index))
endo_vars.append(new_endo_vars_entry)
elif (self.general_nbh_coupling is None) and (not self.regular_grid):
"""In this case, we only fit constants!|"""
endo_vars = []
self.lag_length = 0
"""Last step: Declare endo_vars as the new attribute of the object"""
self.endo_vars = endo_vars
def get_lag_counts(self):
"""Only called in __init__: Gets self.lag_counts"""
self.lag_counts = [self.num_exo_regressors]
last_count = self.num_exo_regressors
if self.regular_grid:
"""STEP 1.A: If 0/4/8 nbhs used: Can be done via endo vars"""
for entry in self.endo_vars:
self.lag_counts.append(last_count + len(entry) + 1)
last_count = last_count + len(entry) + 1 #update
elif self.general_nbh_coupling == "strong coupling":
"""STEP 1.B: Similar to weak coupling, except you don't need to
multiply by the numbers of locations"""
for lag in range(0, self.lag_length):
self.lag_counts.append(last_count + (
(len(self.general_nbh_restriction_sequence[lag]) + 1)))
last_count = last_count + (
(len(self.general_nbh_restriction_sequence[lag]) + 1))
elif self.general_nbh_coupling == "weak coupling":
"""STEP 1.C: If general nbhs, we need more care"""
"""each gen_res corresponds to a lag and gives you a set at
position l, e.g. [0,1,2] at position 0, telling you that at the
first lag, the neighbourhoods used are 0,1,2. Thus, at the first
lag, each location has 3 regressors corresponding to the first
three nbhs for that location in general_nbh_sequence PLUS the
autoregressive term, which is always incorporated but not repre-
sented in any regressor code.
I.e., we need [len([0,1,2]) + 1]*S1*S2 to get the #endogeneous
variables at lag 1. Generally, we thus need
[len(gen_nbh_res_seq[l]) + 1]*S1*S2"""
for lag in range(0, self.lag_length):
self.lag_counts.append(last_count + (
(len(self.general_nbh_restriction_sequence[lag]) + 1)
*self.S1*self.S2) - self.sum_empty_nbhs_per_lag[lag])
last_count = last_count + ( - self.sum_empty_nbhs_per_lag[lag] +
(len(self.general_nbh_restriction_sequence[lag]) + 1)
*self.S1*self.S2)
elif (not self.regular_grid) and self.general_nbh_coupling is None:
"""STEP 1.D: We only fit a constant, so self.lag_counts remains
unchanged. self.lag_counts will be None"""
def get_endo_regressors_per_lag(self):
"""Returns as output the endogeneous regressors per lag"""
if self.regular_grid:
"""STEP 1A: If we have the 4-nbh structure"""
endo_regressors_per_lag = []
for l in range(0, self.lag_length):
res = self.restriction_sequence[l]
nbh = self.nbh_sequence[l]
if res == 0:
endo_regressors_per_lag.append(int(nbh) + 1)
elif res == 4:
endo_regressors_per_lag.append(int(nbh*0.25) + 1)
elif self.general_nbh_coupling is not None:
"""STEP 1B: If we have a general nbh structure, we get
endo_regressors_per_lag differently. In particular, just look at
the self.endo_vars object."""
endo_regressors_per_lag = []
for l in range(0, self.lag_length):
endo_regressors_per_lag.append(int(len(self.endo_vars[l])))
else:
"""STEP 1C: If we only fit a constant"""
endo_regressors_per_lag = []
"""STEP 2: Return the result"""
return endo_regressors_per_lag
def get_extraction_list(self, endo_regressors_per_lag):
"""Gets self.extraction_list and self.insertion position"""
""""STEP 1: Make sure we don't want to copy exogeneous regressors"""
self.extraction_list = [False]*(self.num_exo_regressors)
if self.regular_grid:
"""STEP 1A: IF we have 0/4/8 nbhs """
for i in range(0,self.lag_length-1):
self.extraction_list = (self.extraction_list
+ [True]*endo_regressors_per_lag[i+1]
+ [False]*int(endo_regressors_per_lag[i] -
endo_regressors_per_lag[i+1]))
"""STEP 2A: The last lag of X_t-1 will 'slide out' of sight, so it
definitely is not needed for X_t anymore."""
self.extraction_list += ([False]*
endo_regressors_per_lag[self.lag_length-1])
elif self.general_nbh_coupling == "weak coupling":
"""STEP 1B: IF we have general nbhs"""
per_location = []
for lag in range(0, self.lag_length-1):
num_retained = (1 + len(np.intersect1d(
self.general_nbh_restriction_sequence[lag],
self.general_nbh_restriction_sequence[lag+1])))
num_discarded = ( -num_retained + 1 +
len(self.general_nbh_restriction_sequence[lag]))
per_location += ([True]* num_retained +
[False] * num_discarded)
"""STEP 2B: The last lag of X_t-1 will 'slide out' of sight, so it
definitely is not needed for X_t anymore."""
total_num_last_lag = 1+ len(
self.general_nbh_restriction_sequence[self.lag_length-1])
per_location += ([False]* total_num_last_lag)
"""STEP 3B: Use that we have the same structure all across the
lattice, and simply multiply each entry of 'per_location' by the
number of lattice elements"""
self.extraction_list += sum(
[self.S1*self.S2*[e] for e in per_location],[])
self.extraction_list[self.num_exo_regressors:] = np.array(
self.extraction_list)[np.where(np.array(
self.empty_nbhs) == False)].tolist()
elif self.general_nbh_coupling == "strong coupling":
"""STEP 1C: IF we have general nbhs"""
per_location = []
for lag in range(0, self.lag_length-1):
num_retained = (1 + len(np.intersect1d(
self.general_nbh_restriction_sequence[lag],
self.general_nbh_restriction_sequence[lag+1])))
num_discarded = ( -num_retained + 1 +
len(self.general_nbh_restriction_sequence[lag]))
per_location += ([True]* num_retained +
[False] * num_discarded)
"""STEP 2C: The last lag of X_t-1 will 'slide out' of sight, so it
definitely is not needed for X_t anymore."""
total_num_last_lag = 1+ len(
self.general_nbh_restriction_sequence[self.lag_length-1])
per_location += ([False]* total_num_last_lag)
"""STEP 3C: Use that we have the same structure all across the
lattice, and simply multiply each entry of 'per_location' by the
number of lattice elements"""
self.extraction_list += per_location
elif self.general_nbh_coupling is None and not self.regular_grid:
"""We have constant function and don't need to change anything"""
"""STEP 4: In order to copy entries of X_t-1 to X_t, you need to know
the position of X_t at which you should insert. (This does
only affect the endogeneous part of the regressors)"""
self.insertion_position = - sum(self.extraction_list)
def reinstantiate(self, a = None, b = None):
"""Return a new BVARNIG-model that contains all the same attributes as
this BVARNIG model. In some sense, it is an 'emptied' version of the
same model. Used inside HyperparameterOptimization, if BVARNIGs
Detector is run for hyperparameter optimization"""
"""STEP 1: Get all the characteristics of this model"""
prior_a, prior_b, S1, S2 = self.a, self.b, self.S1, self.S2
prior_mean_beta,prior_var_beta=self.prior_mean_beta,self.prior_var_beta
nbh_sequence = self.nbh_sequence
restriction_sequence = self.restriction_sequence
intercept_grouping = self.intercept_grouping
general_nbh_sequence = self.general_nbh_sequence
general_nbh_restriction_sequence = self.general_nbh_restriction_sequence
nbh_sequence_exo = self.nbh_sequence_exo
exo_selection = self.exo_selection
padding = self.padding
auto_prior_update = self.auto_prior_update
hyperparameter_optimization = self.hyperparameter_optimization
general_nbh_coupling = self.general_nbh_coupling
non_spd_alerts = self.non_spd_alerts
"""STEP 2: Check whether you have a new prior already"""
if a is None:
a = prior_a
if b is None:
b = prior_b
"""STEP 2: Use the characteristics to clone the model"""
clone_model = BVARNIG(prior_a = a, prior_b = b, S1=S1, S2=S2,
prior_mean_beta=prior_mean_beta,
prior_var_beta =prior_var_beta,
prior_mean_scale=None, prior_var_scale=None,
nbh_sequence=nbh_sequence,
restriction_sequence=restriction_sequence,
intercept_grouping=intercept_grouping,
general_nbh_sequence=general_nbh_sequence,
general_nbh_restriction_sequence=general_nbh_restriction_sequence,
nbh_sequence_exo=nbh_sequence_exo, exo_selection=exo_selection,
padding=padding, auto_prior_update=auto_prior_update,
hyperparameter_optimization=hyperparameter_optimization,
general_nbh_coupling=general_nbh_coupling,
non_spd_alerts=non_spd_alerts)
"""STEP 3: Return the cloned model"""
return clone_model
"""~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
""" FIRST OBSERVATION INITIALIZATION FUNCTIONS """
"""~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"""
#NOTE: We need to pass X_endo with one more entry into this function,
# namely for y_2!
def initialization(self, X_endo, X_exo, Y_2, X_exo_2, cp_model, model_prior,
padding_columns_computeXX = None, padding_column_get_x_new = None):
"""Initialize the model (i.e. t=1) with some inputs from the
containing Detector object. The padding_column arguments are only
needed for the demo Csurf object. This is different from object
instantiation/creation, as it processes the very first (collection of)
observation(s), thus creating the objects and quantities we will trace
through time.
NOTE I: The exo_selection list is applied inside detector, so X_exo
will already contain everything relevant
NOTE II: The tag #QR ADAPTION means that the lines following the tag
could/can be adapted to QR-updating (rather than Woodbury)
X_endo: S1xS2x(L+1) numpy array, float:
is the S1xS2x(L+1) array of the last L observations before t
as well as the observation at t at position L.
Y_2: S1xS2 np array, float:
will be endogeneous regressors at time t+1, which means Y_t.
X_exo: S1xS2xnum_exo np array, float:
will contain exogeneous variables at time t (NOT IMPLEMENTED)
X_exo_2: S1xS2xnum_exo np array, float:
will contain exogeneous variables at time t+1 (NOT IMPLEMENTED)
cp_model: CpModel object:
gives the hazard function inside an object
model_prior: float:
Passes the prior of the Detector object into the model object
padding_columns_computeXX, padding_column_get_x_new:
deprecated, leave None.
"""
print("Initializing BVAR object")
"""STEP 1: Take the data-stream that was partitioned appropriately
inside the Detector object and reshape/rename it for further processing
Y1 = Y_t, Y2 = Y_{t+1}, X1_endo = Y_1:t-1, with t = L-1."""
Y1 = X_endo[-1,:].flatten()
Y2 = Y_2.flatten()
if self.has_lags:
X1_endo = X_endo[:self.lag_length,:].reshape(self.lag_length,
self.S1, self.S2)
else:
X1_endo = None
"""In case there are no exogeneous variables in this model, take
the relevant precautions."""
if self.exo_bool:
#RESHAPE will not corr. to real dims of exo vars
X1_exo = (X_exo[-1,:,:].reshape(
self.num_exo_regressors, self.S1, self.S2))
else:
X1_exo = None
"""STEP 2: Format the quantities we wish to trace through time (i.e.
typically sufficient statistics), and correctly compute them using
neighbourhood structure"""
"""STEP 2.1: Quantities for time point t, i.e. dimension does not
depend on how many run-lengths we retain.
Quantities will hold:
XX
Y_1:t-1'Y_1:t-1, i.e. the cross-product of regressors at time t.
XY
Y_1:t-1'Y_t, i.e. the cross-product of regressors and obs. at t
X_t
Y_1:t-1, i.e. regressors at time t
X_tp1
Y_2:t, i.e. regressors at time t+1 ('t plus (p) 1')
YY
Y_t'Y_t, i.e. observation cross-product
"""
self.XX = np.zeros(shape=(self.num_regressors,self.num_regressors))
self.XY = np.zeros(self.num_regressors)
self.X_t = np.zeros(shape=(self.S1*self.S2, self.num_regressors))
self.X_tp1 = np.zeros(shape=(self.S1*self.S2, self.num_regressors))
self.YY = np.inner(Y1, Y1)
"""STEP 2.2: Cross-product quantities for time point t and run-length
r, i.e. dimension does depend on how many run-lengths we retain. Unlike
quantities only stored for the current time, the quantities below
incorporate the prior beliefs.
Quantities will hold;
XX_rt
At time t, r-th entry holds the cross-product of all regressors
corresponding to run-length r_t, i.e. you sum over the last r_t
cross-products XX. Additionally, XX_rt also holds the prior
belief inside, so
XX_rt[r,:,:] = prior_var_beta^-1 + sum_{i = t-r}^t XX(i)
XY_rt
At time t, r-th entry holds the cross-product of all regressors
and observationscorresponding to run-length r_t, i.e. you sum
over the last r_t cross-products XY. Additionally, XY_rt also holds
the prior belief inside, so
XY_rt[r,:] = prior_var_beta^-1 * prior_beta + sum_{i = t-r}^t XY(i)
YY_rt
As the other two, but with YY, and no prior belief occurs, so
YY_rt[r] = sum_{i = t-r}^t YY(i)
Q_rt, R_rt
Unuseable in current version, would hold the QR-decomposition of
inverse of XX_rt
"""
self.XX_rt = np.zeros(shape=(2,self.num_regressors, self.num_regressors)) #2 for r=-1 and r=0
self.XY_rt = np.zeros(shape=(2,self.num_regressors)) #2 for r=-1 and r=0
self.YY_rt = np.zeros(2)
#QR ADAPTION
self.Q_rt = np.zeros(shape=(2,self.num_regressors, self.num_regressors))
self.R_rt = np.zeros(shape=(2,self.num_regressors, self.num_regressors))
"""STEP 2.3: Inverse-related quantities for time point t and run-length
r, i.e. dimension again depends on how many run-lengths on retains.
These are direct functionals of the cross-produts stored above, but
computed/updated in an efficient rather than brute-force way
Quantities will hold:
M_inv_1_rt
Inverse of XX_rt, updated via Woodbury formula at each time point,
but at a later time point than M_inv_2_rt. This means within a
certain time window inside an iteration, we have access to both,
XX_rt^-1 at t and XX_rt^-1 at time t-1, which is needed for
efficient updates.
M_inv_2_rt
Inverse or XX_rt, updated via Woodbury formula at each time point.
See above for the relative timing.
log_det_1_rt
log determinants of all entries in M_inv_1_rt, computed efficiently
log_det_2_rt
log dets of all entries in M_inv_2_rt, computed efficiently
"""
self.M_inv_1_rt = np.zeros(shape=(2,self.num_regressors,
self.num_regressors))
self.M_inv_2_rt = np.zeros(shape=(2,self.num_regressors,
self.num_regressors))
self.log_det_1_rt = np.zeros(2)
self.log_det_2_rt = np.zeros(2)
"""STEP 2.4: beta-coef related quantities for time point t and run-
length r, i.e. dimension depends on how many run-lengths one retains
Quantities will hold:
beta_rt
beta_rt[r,:] stores the coefficients beta corresponding to the
MAP-estimate at time t if one assumes run-length r
beta_XX_beta_rt
what it says: beta_rt[r,:] * XX_rt[r,:,:] * beta_rt[r,:] at pos r
each time point t.
"""
self.beta_XX_beta_rt = np.zeros(2)
self.beta_rt = np.zeros(shape=(2,self.num_regressors))
"""STEP 2.5: Retained run lengths, storing which run-lengths you retain
at time t. Careful with this, as retained_run_lengths[i] = j means that
the i-th longest run-length you retain is j"""
self.retained_run_lengths = np.array([0,0])
"""STEP 3: Compute prior- and data-dependent quantities: