-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathmar_model.py
1666 lines (1421 loc) · 67.3 KB
/
mar_model.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
"""
Markov Autoregressive Models
Author: Chad Fulton
License: BSD
References
----------
Hamilton, James D. 1989.
"A New Approach to the Economic Analysis of
Nonstationary Time Series and the Business Cycle."
Econometrica 57 (2) (March 1): 357-384.
Hamilton, James D. 1994.
Time Series Analysis.
Princeton, N.J.: Princeton University Press.
Kim, Chang-Jin, and Charles R. Nelson. 1999.
"State-Space Models with Regime Switching:
Classical and Gibbs-Sampling Approaches with Applications".
MIT Press Books. The MIT Press.
Notes
-----
Roadmap:
- Correctly estimate covariance matrix
- Add expected regime duration
- Add results class
- Add plotting capabilities (e.g. for showing probabilities of each regime at
time t - can do a line plot for M=2, otherwise an area plot)
- Add support for model specification testing (the usual nuisence approach)
- Add support for MS-VAR
- Add support for state-space models
- Add support for the EM algorithm
TODO: Modify tvtp_transition_vectors_left and right to only calculate a single
time if tvtp_order == 1, and then just repeat it nobs+1 times.
Also, refactor to not call it many times for each loglike call.
The MAR model has four types of parameters:
- transition probabilities
- AR parameters
- standard deviation parameters
- mean parameters
The standard case is the assumption of fixed transition probabilities. In this
case, there are nstates * (nstates - 1) parameters to be estimated, and
nstates^2 parameters used in the model. See below for more details.
If the transition probabilities are allowed to change over time, it is called a
Time-Varying Transition Probabilites (TVTP) Markov-Switching Model. In this
case, an additional exogenous matrix must be supplied that are assumed to
determine the transition probabilities at each point in time. The number of
parameters to be estimated is (k+1)(nstates) * (k+1)(nstates-1), and the number
of parameters used in the model is (k+1)^2 * nstates^2.
The AR, standard deviation, and mean parameters may be allowed to differ
across states, or may be restricted to be the same.
If the AR parameters are allowed to differ, there are `order`*`nstates`
parameters to be estimated and used in the model. If they are not, then there
are `order` parameters.
If the standard deviation (or the mean) parameter is allowed to differ,
there are `nstates` standard deviation (or mean) parameters to estimate and use
in the model, otherwise there is only 1.
Parameters are used in two ways:
(1) Optimization: the optimization routine requires a flat array of parameters
where each parameter can range over (-Inf, Inf), and it adjusts each
parameter while finding the values that optimize the objective function,
here the log likelihood. Thus if there are M states with regime
homoskedasticity, there must be only a single standard deviation parameter
in the array passed to the optimizer. If there are M states with regime
heteroskedasticity, there must be M standard deviation parameters in the
array.
These are the parameters passed to the MAR.loglike() method.
(2) Initializing the filter: the parameters selected by the optimizer at each
iteration are then used to calculate the inputs to the filtering routines
(i.e. joint_probabilities and marginal_conditional_densities). For this,
they need to be (a) transformed to their actual ranges (e.g. probabilities
to lie within [0,1]) and (b) expanded to the full state range. In the
regime homoskedasticity example above, the single standard deviation
parameter must be expanded so that there is one parameter per regime. In
this case, each regime's standard deviation parameter would be identical.
These are the parameters passed to the MAR.filter() and
MAR.initialize_filter() methods.
To achieve this, several helper methods are employed:
- MAR.expand_params()
- Takes an array of parameters from the optimizer, and returns an expanded
array of parameters suitable for use in the model.
- (If not TVTP) Expands the probability vector into a transition vector
- Expands restrictions (e.g. if parameters are restricted to not change, it
expands the single set of `order` parameters to `nstates`*`order`
parameters).
- Always returns `nparams` parameters.
- MAR.contract_params()
- Takes an array of parameters suitable for use in the model, and returns a
contracted array of parameters to be passed to the optimizer.
- MAR.fuse_params()
- Takes each set of parameters separately and fuses them into a single array
(used to maintain consistent parameter ordering in e.g. the optimization
setting).
- MAR.separate_params()
- Takes an array of parameters and separates it into the component parts.
- MAR.transform_params()
- Takes an array of parameters (either the contracted or expanded set of
parameters) that are unconstrained (as would be used in the optimizer) and
transforms them to a constrained form suitable for use in the model (e.g.
transforms probabilities from (-Inf, Inf) to [0,1])
- MAR.untransform_params()
- Takes an array of parameters (either the contracted or expanded set of
parameters) that are constrained (as would be used in the model) and
reverses the transformation to make them be unconstrained (e.g. transforms
probabilities from [0,1] to (-Inf, Inf))
The flow of parameters through the model looks like:
(1) MAR.fit() is called, optionally with the start_params argument.
(2) MAR.loglike() is called by the optimizer, and is passed the contracted,
untransformed (i.e. unconstrained) params.
(3) The parameters are
a. Transformed (i.e. constrained to lie within the actual parameter spaces)
b. Expanded
(4) MAR.initialize_filter() is called with the expanded, transformed
parameters.
The default functionality
To allow arbitrary specification of regime-switching for the parameters,
Internally, the transition matrix is constructed to be left stochastic, and
the transition vector is created from it by stacking its columns.
Notes about the (internally used, left stochastic) transition matrix:
The nstates x nstates Markov chain transition matrix.
The [i,j]th element of the matrix corresponds to the
probability of moving to the i-th state given that you are
in the j-th state. This means that it is the columns that
sum to one, aka that the matrix is left stochastic.
It looks like:
| p11 p12 ... p1M |
| p21 . . |
| . . . |
| pM1 ... pMM |
Here the element pM1 is the probability of moving from the first state
to the M-th state. This representation of the matrix:
- is consistent with usual row / column indexing, but
- inconveniently represents the "from" state as the second index and the
"to" state as the first index.
Kim and Nelson (1999) represent this same matrix (on p.70) as:
| p11 p21 ... pM1 |
| p12 . . |
| . . . |
| p1M ... pMM |
This is the same, identical, matrix, just with a different indexing
convention. Here, p1M is the probability of moving from the first state to
the M-th state. This representation of the matrix is:
- inconsitent with usual row / column indexing, but
- conveniently represents the "from" and "to" states as the, respectively,
first and second indices
Constructing the internally used transition vector (from column stacking)
is easily achieved via:
P.reshape((1, P.size), order='F')
or
P.reshape(-1, order='F')
or
P.ravel(order='F')
or
P.T.ravel()
etc.
Two convenience functions to assist with transition matrices are:
- MAR.transition_vector() accepts a transition matrix and converts it to a
transition vector. The default options create the vector via column
stacking. (Note: this function also may accept a probabilities vector,
which is then converted to a transition vector - see below for details)
- MAR.transition_matrix() accepts a transition vector and converts it to a
transition matrix. The default options create the matrix by setting the
first M elements of the vector to the first column of the matrix, etc.
Notes about the (internally used, constructed via column stacking) transition
vector:
The full (column stacked, M^2 length) transition vector is of the form:
[ p11, p21, ..., pM1, p12, p22, ..., pM2, ..., p1M, p2M, ..., pMM ]
This is the version that used internally, which means that it is:
- Returned by mar_c.hamilton_filter() and MAR.hamilton_filter()
- Accepted as an argument by MAR.smooth()
However, it is not the version that is accepted to externally facing
methods, because it contains redundant information. Each column of the
(left stochastic) transition matrix has M entries, but e.g. the M-th entry
could be left out, and instead calculated as the sum of the first M-1
entries. This is significant because the M-th (or whichever arbitrary
entry is left out) is constrained, and so is not estimated separately from
the other M-1 entries. Thus the optimizer will only optimize over M * (M-1)
probability values.
Some normalization must be specified, and the convention here is that the
last row of the transition matrix will be left off. This means that from
the full transition vector above, each M-th element must be left off (this
corresponds to eliminating the last row of the transition matrix before
creating the vector by stacking the columns). It is of the form:
[ p11, p21, ..., p(M-1)1, p12, p22, ..., p(M-1)2, ..., p1M, p2M, ..., p(M-1)M ]
and the last elements are calculated as:
PM* = 1 - p1* - p2* - ... - p(M-1)*
To distinguish between these two, the following terminology will be used:
- `transition_vector` refers to the full transition vector
- `probabilities` refers to the version without each M-th value
There are convenience functions to assist in moving between these two
representations:
- probabilities() accepts a transition vector and returns the
corresponding probabilities vector by removing each M-th value
- transition_vector() accepts a probabilities vector and returns the
corresponding transition vector by calculating and adding the M-th values
(this is its behavior if its first argument has ndim=1. If the first
argument has ndim=2, then it is assumed to be converting a transition
matrix to a transition vector by column stacking)
"""
from __future__ import division
from functools import reduce
import numpy as np
import pandas as pd
import statsmodels.tsa.base.tsa_model as tsbase
import statsmodels.base.model as base
from statsmodels.base import data
from statsmodels.tsa.tsatools import add_constant, lagmat
from statsmodels.regression.linear_model import OLS, OLSResults
from statsmodels.tools.numdiff import approx_fprime
from statsmodels.tools.decorators import (cache_readonly, cache_writable,
resettable_cache)
import statsmodels.base.wrapper as wrap
from scipy import stats
from mar_c import (hamilton_filter, tvtp_transition_vectors_left,
tvtp_transition_vectors_right,
marginal_conditional_densities)
import resource
class MAR(tsbase.TimeSeriesModel):
"""
"An autoregressive model of order k with first-order , M-state
Markov-switching mean and variance"
Parameters
----------
endog : array-like
The endogenous variable. Assumed not to be in deviation-from-mean form.
order : integer
The order of the autoregressive parameters.
nstates : integer
The number of states in the Markov chain.
switch_ar : boolean, optiona
Whether or not AR parameters are allowed to switch with regimes.
switch_var : boolean, optional
Whether or not the variances are allowed to vary across regimes.
(Regime-specific Heteroskedasticity)
switch_means : boolean, optional
Whether or not the means are allowed to vary across regimes.
tvtp_data : array-like, optional
A vector or matrix of exogenous or lagged variables to use in
calculating time varying transition probabilities (TVTP). TVTP is only
used if this variable is provided.
References
----------
Kim, Chang-Jin, and Charles R. Nelson. 1999.
"State-Space Models with Regime Switching:
Classical and Gibbs-Sampling Approaches with Applications".
MIT Press Books. The MIT Press.
Notes
-----
States are zero-indexed.
"""
def __init__(self, endog, order, nstates,
switch_ar=False, switch_var=False, switch_mean=True,
tvtp_exog=None,
dates=None, freq=None, missing='none'):
# "Immutable" properties
self.nobs_initial = order
self.nobs = endog.shape[0] - order
self.order = order
self.nstates = nstates
# Determine switching parameters
# Transition probabilities
if tvtp_exog is None:
self.tvtp_exog = np.ones((self.nobs + self.nobs_initial + 1, 1))
else:
self.tvtp_exog = add_constant(tvtp_exog)
self.tvtp_order = self.tvtp_exog.shape[1]
if not self.tvtp_exog.shape[0] == self.nobs + self.nobs_initial + 1:
raise ValueError('Length of exogenous data determining the time'
' varying transition probabilities must have'
' length equal to %d: the number of observations'
' plus one. Got length %d.' %
(self.nobs + self.nobs_initial + 1,
self.tvtp_exog.shape[0]))
self.nparams_prob = (
self.nstates * (self.nstates - 1) * self.tvtp_order
)
# AR parameters
if switch_ar == True:
self.nparams_ar = self.nstates*self.order
self.switch_ar = True
self.switch_method_ar = 'all'
elif switch_ar == False:
self.nparams_ar = self.order
self.switch_ar = False
self.switch_method_ar = 'none'
elif isinstance(switch_ar, (list, np.ndarray)):
self.nparams_ar = 0
self.switch_ar = np.asarray(switch_ar)
if not self.switch_ar.shape[0] == nstates:
raise ValueError('Fixed switching definitions for AR'
' parameters must be an array specifying a'
' fixed value for each state. Expected length'
' %d, got length %d.' %
(nstates, self.switch_ar.shape[0]))
self.switch_method_ar = 'fixed'
elif isinstance(switch_ar, tuple) and callable(switch_ar[1]):
self.nparams_ar, self.switch_ar = switch_ar
self.switch_method_ar = 'custom'
else:
raise ValueError('Custom switching definitions for AR'
' parameters must be an array of fixed values or'
' must be a tuple with the number of parameters'
' to estimate as the first value and a callback'
' as the second value.')
# Variance parameters
if switch_var == True:
self.nparams_var = self.nstates
self.switch_var = True
self.switch_method_var = 'all'
elif switch_var == False:
self.nparams_var = 1
self.switch_var = False
self.switch_method_var = 'none'
elif isinstance(switch_var, (list, np.ndarray)):
self.nparams_var = 0
self.switch_var = np.asarray(switch_var)
if not self.switch_var.shape[0] == nstates:
raise ValueError('Fixed switching definitions for variance'
' parameters must be an array specifying a'
' fixed value for each state. Expected length'
' %d, got length %d.' %
(nstates, self.switch_var.shape[0]))
self.switch_method_var = 'fixed'
elif isinstance(switch_var, tuple) and callable(switch_var[1]):
self.nparams_var, self.switch_var = switch_var
self.switch_method_var = 'custom'
else:
raise ValueError('Custom switching definitions for variance'
' parameters must be an array of fixed values or'
' must be a tuple with the number of parameters'
' to estimate as the first value and a callback'
' as the second value.')
# Mean parameters
if switch_mean == True:
self.nparams_mean = self.nstates
self.switch_mean = True
self.switch_method_mean = 'all'
elif switch_mean == False:
self.nparams_mean = 1
self.switch_mean = False
self.switch_method_mean = 'none'
elif isinstance(switch_mean, (list, np.ndarray)):
self.nparams_mean = 0
self.switch_mean = np.asarray(switch_mean)
if not self.switch_mean.shape[0] == nstates:
raise ValueError('Fixed switching definitions for mean'
' parameters must be an array specifying a'
' fixed value for each state. Expected length'
' %d, got length %d.' %
(nstates, self.switch_mean.shape[0]))
self.switch_method_mean = 'fixed'
elif isinstance(switch_mean, tuple) and callable(switch_mean[1]):
self.nparams_mean, self.switch_mean = switch_mean
self.switch_method_mean = 'custom'
else:
raise ValueError('Custom switching definitions for mean'
' parameters must be an array of fixed values or'
' must be a tuple with the number of parameters'
' to estimate as the first value and a callback'
' as the second value.')
# The number of parameters used by the optimizer
self.nparams = (
self.nparams_prob +
self.nparams_ar +
self.nparams_var +
self.nparams_mean
)
# The number of parameters used by the model
# (not quite right for nparams_prob, in case of TVTP)
self.nparams_prob_full = self.nparams_prob
self.nparams_ar_full = self.order * self.nstates
self.nparams_var_full = self.nstates
self.nparams_mean_full = self.nstates
self.nparams_full = (
self.nparams_prob_full +
self.nparams_ar_full +
self.nparams_var_full +
self.nparams_mean_full
)
# If we got custom (callable) switch functions, test them
test_args = self.separate_params(np.ones((self.nparams,)))
if self.switch_method_ar == 'custom':
test_ar = len(self.switch_ar(*test_args))
if not test_ar == self.nparams_ar_full:
raise ValueError('Invalid custom switching function for AR'
' parameters. Must return a vector of length'
' %d. Got a parameter of length %d.' %
(self.nparams_ar_full, test_ar))
if self.switch_method_var == 'custom':
test_var = len(self.switch_var(*test_args))
if not test_var == self.nparams_var_full:
raise ValueError('Invalid custom switching function for'
' variance parameters. Must return a vector'
' of length %d. Got a parameter of length'
' %d.' % (self.nparams_ar_full, test_var))
if self.switch_method_mean == 'custom':
test_mean = len(self.switch_mean(*test_args))
if not test_mean == self.nparams_mean_full:
raise ValueError('Invalid custom switching function for mean'
' parameters. Must return a vector of length'
' %d. Got a parameter of length %d.' %
(self.nparams_mean_full, test_mean))
# Make a copy of original datasets
orig_endog = endog
# Create datasets / complete initialization
endog = orig_endog[self.nobs_initial:]
# Handle exogenous data
if order > 0:
orig_exog = lagmat(orig_endog, order)
exog = orig_exog[self.nobs_initial:]
else:
orig_exog = None
exog = None
super(MAR, self).__init__(endog, exog, missing=missing)
# Overwrite originals
self.data.orig_endog = orig_endog
self.data.orig_exog = orig_exog
# Cache
if exog is not None:
self.augmented = np.c_[endog, exog]
else:
self.augmented = endog.values[:, np.newaxis]
def expand_params(self, params):
params = np.asarray(params)
# Make sure they're not already expanded
if params.shape == (self.nparams_full,):
return params
elif params.shape != (self.nparams,):
raise ValueError('Unexpected parameter vector shape. Expected %s,'
' got %s.' % ((self.nparams,), params.shape))
transitions, ar_params, stddevs, means = self.separate_params(params)
# Transition probabilities
# (these are expanded later, due to possibility of TVTP)
# AR parameters
if self.switch_method_ar == 'all':
pass
elif self.switch_method_ar == 'none':
ar_params = np.tile(ar_params, self.nstates)
elif self.switch_method_ar == 'fixed':
ar_params = self.switch_ar
else:
ar_params = self.switch_ar(transitions, ar_params, stddevs, means)
# Variance parameters
if self.switch_method_var == 'all':
pass
elif self.switch_method_var == 'none':
stddevs = np.tile(stddevs, self.nstates)
elif self.switch_method_var == 'fixed':
stddevs = self.switch_var
else:
stddevs = self.switch_var(transitions, ar_params, stddevs, means)
# Mean parameters
if self.switch_method_mean == 'all':
pass
elif self.switch_method_mean == 'none':
means = np.tile(means, self.nstates)
elif self.switch_method_mean == 'fixed':
means = self.switch_mean
else:
means = self.switch_mean(transitions, ar_params, stddevs, means)
return self.fuse_params(transitions, ar_params, stddevs, means)
def contract_params(self, params):
raise NotImplementedError
def fuse_params(self, transitions, ar_params, stddevs, means):
"""
Combines the component parameters into a single array.
Parameters
----------
transitions : array-like
A vector of transition probabilities
ar_params : array-like
The AR parameters
stddevs : array-like
The standard deviations for each state
means : array-like
The means for each state
Returns
-------
params : array-like
An array of parameters
"""
return np.r_[transitions, ar_params, stddevs, means]
def separate_params(self, params):
"""
Separates a single array of parameters into the component pieces.
Parameters
----------
params : array-like
An array of parameters
Returns
-------
transitions : array-like
A vector of transition probabilities
ar_params : array-like
The AR parameters
stddevs : array-like
The standard deviations for each state
means : array-like
The means for each state
"""
params = np.asarray(params)
# Separate the parameters
if params.shape == (self.nparams,):
nparams = np.cumsum((self.nparams_prob, self.nparams_ar,
self.nparams_var, self.nparams_mean))
elif params.shape == (self.nparams_full,):
nparams = np.cumsum((self.nparams_prob_full, self.nparams_ar_full,
self.nparams_var_full, self.nparams_mean_full))
else:
raise ValueError('Invalid number of parameters. Expected %s or %s,'
' got %s.' % ((self.nparams,),
(self.nparams_full,), params.shape))
transitions = params[:nparams[0]]
ar_params = params[nparams[0]:nparams[1]]
stddevs = params[nparams[1]:nparams[2]]
means = params[nparams[2]:]
return transitions, ar_params, stddevs, means
def transform_params(self, params, method='logit'):
"""
Transforms a set of unconstrained parameters to a set of contrained
parameters.
Optimization methods (e.g scipy.optimize) work on sets of unconstrained
parameters, but the model requires e.g. that probability values lie in
the range [0, 1]. This function takes the unconstrained parameters from
the optimizer and transforms them into parameters usable in the model
(e.g to evaluate the likelihood).
Parameters
----------
params : array-like
An array of unconstrained parameters
method : {'logit', 'abs'}, optional
The method used to transform parameters on the entire real line to
parameters in the range (0,1).
Returns
-------
params : an array of constrained parameters
"""
transitions, ar_params, stddevs, means = self.separate_params(params)
# Standard deviations: transform to always be positive
stddevs = np.exp(-stddevs)
return self.fuse_params(transitions, ar_params, stddevs, means)
def untransform_params(self, params, method='logit'):
"""
Transforms a set of constrained parameters to a set of uncontrained
parameters.
Optimization methods (e.g scipy.optimize) work on sets of unconstrained
parameters, but the model requires e.g. that probability values lie in
the range [0, 1]. This function takes the constrained parameters used
in the model and transforms them into parameters usable by the
optimizer (e.g to take step sizes, etc.).
Parameters
----------
params : array-like
An array of constrained parameters
method : {'logit', 'abs'}, optional
The method used to transform parameters on the entire real line to
parameters in the range (0,1).
Returns
-------
params : an array of unconstrained parameters
"""
transitions, ar_params, stddevs, means = self.separate_params(params)
stddevs = -np.log(stddevs)
return self.fuse_params(transitions, ar_params, stddevs, means)
def transform_jacobian(self, params):
"""
Evaluates the jacobian of the transformation function.
Used to calculate standard errors via the delta method (the method of
propagation of errors).
Parameters
----------
params : array-like
An array of parameters
Returns
-------
jacobian : array-like
The jacobian matrix of the transformation function, evaluated at
the given parameters.
"""
transitions, ar_params, stddevs, means = self.separate_params(params)
# If not TVTP, then we want to return the estimated probabilities
# themselves, and not the unconstrainted parameters
# (If TVTP, then we just want to return the unconstrained parameters)
if self.tvtp_order == 1:
transitions = (
np.exp(transitions) / (1 + np.exp(transitions))**2
)
# Standard deviation parameters:
stddevs = -np.exp(-stddevs)
vector = np.r_[
transitions, [1]*len(ar_params), stddevs, [1]*len(means)
]
return np.diag(vector)
def loglike(self, params):
"""
Calculate the log likelihood.
Parameters
----------
params : array-like
An array of unconstrained, contracted parameters
Returns
-------
loglike : float
The log likelihood of the model evaluated at the given parameters.
Notes
-----
Uses unconstrained parameters because it is meant to be called via
the optimization routine, which uses unconstrained parameters.
"""
params = self.transform_params(params)
params = self.expand_params(params)
(joint_probabilities,
marginal_conditional_densities) = self.initialize_filter(params)
transitions, _, _, _ = self.separate_params(params)
transition_vectors = self.tvtp_transition_vectors(transitions, 'right')
transition_vectors = transition_vectors[self.nobs_initial:]
marginal_densities, _, _ = hamilton_filter(
self.nobs, self.nstates, self.order,
transition_vectors, joint_probabilities,
marginal_conditional_densities
)
return np.sum(np.log(marginal_densities))
def tvtp_transition_vectors(self, transitions, matrix_type='left'):
"""
Create a vector of time varying transition probability vectors
Each transition vector is the vectorized version of the transition
matrix.
Parameters
----------
transitions : array-like
A vector of transition parameters, with length
self.nstates * (self.nstates - 1) * self.tvtp_order
matrix_type : {'left', 'right'}, optional
The method by which the corresponding transition matrix would be
constructed from the returned transition vector.
- If 'left', the transition matrix would be constructed to be left
stochastic.
- If 'right', the transition matrix would be constructed to be
right stochastic.
See MAR.transition_matrix() or the module docstring for details.
Returns
-------
transition_vector : array
An (nobs+1) x (nstates*nstates) matrix (i.e. an nobs+1 vector of
nstates*nstates transition vectors).
"""
transitions = transitions.reshape(
self.nstates*(self.nstates-1), self.tvtp_order
)
if matrix_type == 'left':
fn = tvtp_transition_vectors_left
elif matrix_type == 'right':
fn = tvtp_transition_vectors_right
else:
raise ValueError("Invalid matrix type method. Must be one of"
" {'left', 'right'}, corresponding to a left"
" stochastic or right stochastic transition"
" matrix. Got %s." % matrix_type)
transition_vectors = fn(
self.nobs + self.nobs_initial, self.nstates, self.tvtp_order,
transitions, np.asarray(self.tvtp_exog, order='C')
)
return transition_vectors
def probability_vector(self, transitions, matrix_type='left'):
"""
Create a probability vector
The probability vector is the vectorized version of the transition
matrix, excluding its last row.
Parameters
----------
transitions : array-like
A vector of transition values for the probability vector. It can be
either:
- a transition vector, if it has 1-dimension
- a transition matrix, if it has 2-dimensions
See the module docstring for more information about the difference.
matrix_type : {'left', 'right'}, optional
The method by which the corresponding transition matrix would be
constructed from the returned probability vector.
- If 'left', the transition matrix would be constructed to be left
stochastic.
- If 'right', the transition matrix would be constructed to be
right stochastic.
See MAR.transition_matrix() or the module docstring for details.
Returns
-------
probability_vector : array
A 1-dimensional probability vector
Notes
-----
See module docstring for details on the distinction between the terms
`transitions`, `probability_vector`, `transition_vector`, and
`transition_matrix`.
"""
# Figure out which type of stochastic matrix we have
if matrix_type == 'left':
order = 'F'
elif matrix_type == 'right':
order = 'C'
else:
raise ValueError("Invalid matrix type method. Must be one of"
" {'left', 'right'}, corresponding to a left"
" stochastic or right stochastic transition"
" matrix. Got %s." % matrix_type)
# Handle transition vector (convert to a transition matrix first)
if transitions.ndim == 1:
transitions = self.transition_matrix(array, order)
if not transitions.ndim == 2:
raise ValueError('Invalid input array. Must be 1-dimensional (a'
' transition vector) or 2-dimensional (a'
' transition matrix. Got %d dimensions.' %
transitions.ndim)
# Transition matrix to probabilities vector
return transitions[:-1,:].ravel(order=order)
def transition_matrix(self, transitions, matrix_type='left'):
"""
Create a transition matrix from a vector of probability values.
Parameters
----------
transitions : array-like
A vector of probability values for the transition matrix. It can be
either:
- a transition vector, if it has length self.nstates^2)
- a probabilities vector, if it has length
self.nstates*(self.nstates - 1)
See the module docstring for more information about the difference.
matrix_type : {'left', 'right'}, optional
The method by which the transition matrix is constructed.
- If 'left', the transition matrix is constructed to be left
stochastic by converting each set of `self.nstates` values in the
transition vector into columns of the transition matrix. This
corresponds to creating the matrix by unstacking the vector into
columns, and the operation is equivalent to reshaping the vector
into the matrix using Fortran ordering.
- If 'right', the transition matrix is constructed to be right
stochastic by converting each set of `self.nstates` values in the
transition vector into rows of the transition matrix. This
corresponds to creating the matrix by unstacking the vector into
rows, and the operation is equivalent to reshaping the vector
into the matrix using C ordering.
Returns
-------
transition_matrix : array
A 2-dimensional transition matrix
Notes
-----
See module docstring for details on the distinction between the terms
`transitions`, `probability_vector`, `transition_vector`, and
`transition_matrix`.
"""
transitions = np.asarray(transitions)
# Figure out which type of stochastic matrix we have
if matrix_type == 'left':
order = 'F'
elif matrix_type == 'right':
order = 'C'
else:
raise ValueError("Invalid matrix type method. Must be one of"
" {'left', 'right'}, corresponding to a left"
" stochastic or right stochastic transition"
" matrix. Got %s." % matrix_type)
# If we already have a transition matrix
if transitions.ndim == 2:
transition_matrix = transitions
elif transitions.ndim == 1:
# Handle a probabilities vector by converting it to a transition
# vector first
if transitions.shape[0] == self.nstates*(self.nstates-1):
transitions = self.transition_vector(transitions, matrix_type)
if not transitions.shape[0] == self.nstates**2:
raise ValueError('Invalid vector of probability values. Must'
' have length %d if it is a transition vector'
' or length %d if it is a probabilities vector'
' (see module docstring for details). Got '
' length %d.' %
(self.nstates**2,
self.nstates*(self.nstates-1),
transitions.shape[0]))
transition_matrix = transitions.reshape(
(self.nstates, self.nstates),
order=order
)
else:
raise ValueError('Invalid input array. Must be 1-dimensional (a'
' probability or transition vector) or '
' 2-dimensional (a transition matrix. Got %d'
' dimensions.' % transitions.ndim)
# Transition vector to transition matrix
return transition_matrix
def transition_vector(self, transitions, matrix_type='left'):
"""
Create a transition vector
The transition vector is the vectorized version of the transition
matrix.
Parameters
----------
transitions : array-like
A vector of transition values for the transition vector. It can be
either:
- a probability vector, if it has 1-dimension
- a transition matrix, if it has 2-dimensions
See the module docstring for more information about the difference.
matrix_type : {'left', 'right'}, optional
The method by which the corresponding transition matrix would be
constructed from the returned transition vector.
- If 'left', the transition matrix would be constructed to be left
stochastic.
- If 'right', the transition matrix would be constructed to be
right stochastic.
See MAR.transition_matrix() or the module docstring for details.
Returns
-------
transition_vector : array
A 1-dimensional transition vector
Notes
-----
See module docstring for details on the distinction between the terms
`transitions`, `probability_vector`, `transition_vector`, and
`transition_matrix`.
"""
transitions = np.asarray(transitions)
if matrix_type == 'left':
order = 'F'
elif matrix_type == 'right':
order = 'C'
else:
raise ValueError("Invalid matrix type method. Must be one of"
" {'left', 'right'}, corresponding to a left"
" stochastic or right stochastic transition"
" matrix. Got %s." % matrix_type)
# If we already have a transition vector
if transitions.ndim == 1 and transitions.size == self.nstates**2:
transition_vector = transitions
# Probabilities vector -> transition vector
elif transitions.ndim == 1:
# Get a transition matrix, but missing the last row
transition_matrix = transitions.reshape(
(self.nstates-1, self.nstates),
order=order
)
# Calculate and append the last row
transitions = np.c_[
transition_matrix.T, 1-transition_matrix.sum(0)
].T
# Vectorize
transition_vector = transitions.ravel(order=order)
# Transition matrix -> transition vector
elif transitions.ndim == 2:
transition_vector = transitions.ravel(order=order)
else:
raise ValueError('Invalid input array. Must be 1-dimensional (a'
' probability vector) or 2-dimensional (a'
' transition matrix. Got %d dimensions.' %
transitions.ndim)
return transition_vector
def unconditional_probabilities(self, transitions):
"""
Calculate the unconditional probabilities ("ergodic probabilities")
from a (left stochastic) transition matrix.
Parameters
----------