tseffects 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tseffects/__init__.py +120 -0
- tseffects/_checks.py +445 -0
- tseffects/_delta.py +258 -0
- tseffects/_util.py +239 -0
- tseffects/_vcov.py +112 -0
- tseffects/baselines.py +182 -0
- tseffects/calculators.py +469 -0
- tseffects/data/approval.csv +141 -0
- tseffects/data/toy_ts_interaction_data.csv +51 -0
- tseffects/datasets.py +58 -0
- tseffects/gdrf.py +666 -0
- tseffects/interact.py +401 -0
- tseffects/plots.py +399 -0
- tseffects/results.py +87 -0
- tseffects-0.4.1.dist-info/METADATA +454 -0
- tseffects-0.4.1.dist-info/RECORD +19 -0
- tseffects-0.4.1.dist-info/WHEEL +5 -0
- tseffects-0.4.1.dist-info/licenses/LICENSE +338 -0
- tseffects-0.4.1.dist-info/top_level.txt +1 -0
tseffects/__init__.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
r"""Dynamic effects from single-equation time series models, with interactions.
|
|
2
|
+
|
|
3
|
+
Autoregressive distributed lag (ADL) models and their reparameterized
|
|
4
|
+
equivalent, the Generalized Error-Correction Model (GECM), are easy to
|
|
5
|
+
estimate. What is harder is getting a rich set of dynamic inferences back out
|
|
6
|
+
of them: the coefficients alone do not tell you how a shock to x moves y three
|
|
7
|
+
periods later, nor what happens when the shock is permanent rather than
|
|
8
|
+
transitory, nor what happens when the model was estimated in differences but
|
|
9
|
+
you want the answer in levels.
|
|
10
|
+
|
|
11
|
+
This package recovers those inferences. Everything is built from the Impulse
|
|
12
|
+
Response Function and the Step Response Function, generalized into one family
|
|
13
|
+
-- the General Dynamic Response Function -- indexed by the order of the shock
|
|
14
|
+
history :math:`h` and the orders of differencing :math:`d_x` and :math:`d_y`:
|
|
15
|
+
|
|
16
|
+
.. math::
|
|
17
|
+
|
|
18
|
+
\mathrm{GDRF}_{(s,\,h,\,d_y,\,d_x)}
|
|
19
|
+
= \sum_{j=0}^{s} w_j(h, d_x, d_y)\, \xi_{s-j},
|
|
20
|
+
|
|
21
|
+
where :math:`\xi_s` is the period-:math:`s` impulse response and the weights
|
|
22
|
+
are generalized binomial coefficients. Arbitrary shock histories are handled
|
|
23
|
+
too, since any history decomposes into a sum of dated impulses.
|
|
24
|
+
|
|
25
|
+
Every quantity is produced as an algebraic *formula* in the model's coefficient
|
|
26
|
+
names first, and only then evaluated by the delta method. That is what lets one
|
|
27
|
+
standard-error machinery cover impulse responses, cumulated responses, long-run
|
|
28
|
+
multipliers, fitted trajectories and conditional effects alike -- and it means
|
|
29
|
+
you can read the formula behind any number the package reports.
|
|
30
|
+
|
|
31
|
+
Getting started
|
|
32
|
+
---------------
|
|
33
|
+
>>> import statsmodels.formula.api as smf
|
|
34
|
+
>>> from tseffects import load_toy_data, gdrf_adl
|
|
35
|
+
>>> data = load_toy_data()
|
|
36
|
+
>>> fit = smf.ols("y ~ l_1_y + x + l_1_x", data=data).fit()
|
|
37
|
+
>>> out = gdrf_adl(fit,
|
|
38
|
+
... x_vrbl={"x": 0, "l_1_x": 1},
|
|
39
|
+
... y_vrbl={"l_1_y": 1},
|
|
40
|
+
... shock_history="pulse", s_limit=10)
|
|
41
|
+
>>> out.estimates.head(3).round(4)
|
|
42
|
+
Period GDRF SE Lower Upper
|
|
43
|
+
0 0 0.6384 0.2608 0.1273 1.1496
|
|
44
|
+
1 1 0.3918 0.3184 -0.2322 1.0158
|
|
45
|
+
2 2 0.2987 0.2475 -0.1864 0.7839
|
|
46
|
+
|
|
47
|
+
``out.plot()`` draws it, and ``out.formulae`` gives the algebra behind each row.
|
|
48
|
+
|
|
49
|
+
This is a Python port of the R package **tseffects** by Soren Jordan, Garrett N.
|
|
50
|
+
Vande Kamp and Reshi Rajan. The method is documented in Warner, Vande Kamp and
|
|
51
|
+
Jordan, "Conditional Relationships in Dynamic Models", *Political Science
|
|
52
|
+
Research and Methods* (2026),
|
|
53
|
+
`doi:10.1017/psrm.2026.10087 <https://doi.org/10.1017/psrm.2026.10087>`_, and in
|
|
54
|
+
Vande Kamp and Jordan, "The Necessity of Moving Averages in Dynamic Linear
|
|
55
|
+
Regression Models", *American Journal of Political Science* 69(1): 176-193
|
|
56
|
+
(2025).
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from ._delta import BatchDelta, DeltaFn, DeltaResult, delta_method
|
|
60
|
+
from ._vcov import VCOV_TYPES, vcov_hc
|
|
61
|
+
from .baselines import yhat_calculator
|
|
62
|
+
from .calculators import (
|
|
63
|
+
Formulae,
|
|
64
|
+
convolution_calculator,
|
|
65
|
+
gecm_to_adl,
|
|
66
|
+
general_calculator,
|
|
67
|
+
pulse_calculator,
|
|
68
|
+
shock_weights,
|
|
69
|
+
)
|
|
70
|
+
from .datasets import load_approval, load_toy_data
|
|
71
|
+
from .gdrf import adl_effects, gdrf_adl, gdrf_gecm, gecm_effects
|
|
72
|
+
from .interact import combine_interaction_terms, interact_adl
|
|
73
|
+
from .plots import (
|
|
74
|
+
OKABE_ITO,
|
|
75
|
+
apply_theme,
|
|
76
|
+
plot_effects,
|
|
77
|
+
plot_gdrf,
|
|
78
|
+
plot_interact_heatmap,
|
|
79
|
+
plot_interact_lines,
|
|
80
|
+
)
|
|
81
|
+
from .results import EffectResult
|
|
82
|
+
|
|
83
|
+
__version__ = "0.4.1"
|
|
84
|
+
|
|
85
|
+
__all__ = [
|
|
86
|
+
# Main entry points
|
|
87
|
+
"gdrf_adl",
|
|
88
|
+
"adl_effects",
|
|
89
|
+
"gdrf_gecm",
|
|
90
|
+
"gecm_effects",
|
|
91
|
+
"interact_adl",
|
|
92
|
+
# Symbolic building blocks
|
|
93
|
+
"pulse_calculator",
|
|
94
|
+
"general_calculator",
|
|
95
|
+
"convolution_calculator",
|
|
96
|
+
"gecm_to_adl",
|
|
97
|
+
"yhat_calculator",
|
|
98
|
+
"combine_interaction_terms",
|
|
99
|
+
"shock_weights",
|
|
100
|
+
"Formulae",
|
|
101
|
+
# Inference
|
|
102
|
+
"delta_method",
|
|
103
|
+
"DeltaFn",
|
|
104
|
+
"BatchDelta",
|
|
105
|
+
"DeltaResult",
|
|
106
|
+
"vcov_hc",
|
|
107
|
+
"VCOV_TYPES",
|
|
108
|
+
# Results and plots
|
|
109
|
+
"EffectResult",
|
|
110
|
+
"plot_effects",
|
|
111
|
+
"plot_gdrf",
|
|
112
|
+
"plot_interact_lines",
|
|
113
|
+
"plot_interact_heatmap",
|
|
114
|
+
"apply_theme",
|
|
115
|
+
"OKABE_ITO",
|
|
116
|
+
# Data
|
|
117
|
+
"load_toy_data",
|
|
118
|
+
"load_approval",
|
|
119
|
+
"__version__",
|
|
120
|
+
]
|
tseffects/_checks.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Argument validation.
|
|
2
|
+
|
|
3
|
+
These mirror ``GDRF.dummy.checks``, ``adl.dummy.checks`` and
|
|
4
|
+
``gecm.dummy.checks`` in the R package, including which situations warn and
|
|
5
|
+
which stop, so a specification that R rejects is rejected here with the same
|
|
6
|
+
reasoning.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from ._vcov import VCOV_TYPES
|
|
16
|
+
|
|
17
|
+
EFFECT_TYPES = ("discrete", "fitted")
|
|
18
|
+
INFERENCES = ("levels", "differences")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _effect_label(kind: str) -> str:
|
|
22
|
+
return "dynamic conditional effect" if kind == "interact" else "GDRF"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_effect_type(effect_type: str) -> None:
|
|
26
|
+
if effect_type not in EFFECT_TYPES:
|
|
27
|
+
raise ValueError(
|
|
28
|
+
"Invalid effect_type. effect_type must be one of 'discrete' or 'fitted'"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def check_se_type(se_type: str) -> None:
|
|
33
|
+
if se_type not in VCOV_TYPES:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Invalid se_type {se_type!r}. se_type must be one of: "
|
|
36
|
+
+ ", ".join(VCOV_TYPES)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_integer(value, name: str) -> int:
|
|
41
|
+
if value is None:
|
|
42
|
+
raise ValueError(f"{name} must be specified")
|
|
43
|
+
if isinstance(value, bool) or not isinstance(value, (int, np.integer, float)):
|
|
44
|
+
raise ValueError(f"{name} must be an integer")
|
|
45
|
+
if float(value) % 1 != 0:
|
|
46
|
+
raise ValueError(f"{name} must be an integer")
|
|
47
|
+
return int(value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_gdrf_arguments(
|
|
51
|
+
effect_type: str,
|
|
52
|
+
prediction_values,
|
|
53
|
+
baseline_y,
|
|
54
|
+
baseline_y_se,
|
|
55
|
+
shock_size,
|
|
56
|
+
d_y: int,
|
|
57
|
+
inferences_y: str,
|
|
58
|
+
kind: str = "GDRF",
|
|
59
|
+
) -> None:
|
|
60
|
+
"""The shared baseline/shock-size checks for anything producing a GDRF."""
|
|
61
|
+
label = _effect_label(kind)
|
|
62
|
+
check_effect_type(effect_type)
|
|
63
|
+
|
|
64
|
+
if isinstance(shock_size, bool) or not isinstance(
|
|
65
|
+
shock_size, (int, float, np.integer, np.floating)
|
|
66
|
+
):
|
|
67
|
+
raise ValueError("shock_size must be numeric")
|
|
68
|
+
if np.ndim(shock_size) != 0:
|
|
69
|
+
raise ValueError(
|
|
70
|
+
"shock_size should be a single value by which to scale the entire "
|
|
71
|
+
"shock_history"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if effect_type == "discrete":
|
|
75
|
+
if prediction_values is not None:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Do not supply prediction_values when effect_type = 'discrete'. "
|
|
78
|
+
f"The {label} is the discrete effect, which takes no predicted values"
|
|
79
|
+
)
|
|
80
|
+
if baseline_y is not None and baseline_y != 0:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Do not supply baseline_y when effect_type = 'discrete'. The "
|
|
83
|
+
f"{label} is the discrete effect, which is always compared to 0"
|
|
84
|
+
)
|
|
85
|
+
if baseline_y_se is not None and baseline_y_se != 0:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Do not supply baseline_y_se when effect_type = 'discrete'. The "
|
|
88
|
+
f"{label} is the discrete effect, which has its own standard errors"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
# Everything below is effect_type == "fitted".
|
|
93
|
+
if prediction_values is None and baseline_y is None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"You must provide either a baseline_y value or prediction_values (to "
|
|
96
|
+
"calculate a steady state). If unsure, set baseline_y = 0 (no assumed "
|
|
97
|
+
"starting value for y)"
|
|
98
|
+
)
|
|
99
|
+
if prediction_values is not None and not isinstance(prediction_values, dict):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"prediction_values must be a mapping of {variable: value}. Be sure to "
|
|
102
|
+
"set any differenced variables to 0"
|
|
103
|
+
)
|
|
104
|
+
if prediction_values is not None and baseline_y_se not in (0, 0.0):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"If using prediction_values, leave baseline_y_se at 0 and let the model "
|
|
107
|
+
"supply model-based uncertainty around the steady state"
|
|
108
|
+
)
|
|
109
|
+
if baseline_y is not None:
|
|
110
|
+
if isinstance(baseline_y, bool) or not isinstance(
|
|
111
|
+
baseline_y, (int, float, np.integer, np.floating)
|
|
112
|
+
):
|
|
113
|
+
raise ValueError("If providing a baseline_y, it must be numeric")
|
|
114
|
+
if np.ndim(baseline_y) != 0:
|
|
115
|
+
raise ValueError("Only provide a single baseline_y value")
|
|
116
|
+
if baseline_y_se is not None:
|
|
117
|
+
if isinstance(baseline_y_se, bool) or not isinstance(
|
|
118
|
+
baseline_y_se, (int, float, np.integer, np.floating)
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("If providing a baseline_y_se, it must be numeric")
|
|
121
|
+
if np.ndim(baseline_y_se) != 0:
|
|
122
|
+
raise ValueError("Only provide a single baseline_y_se value")
|
|
123
|
+
|
|
124
|
+
if d_y == 0:
|
|
125
|
+
if baseline_y is not None:
|
|
126
|
+
if prediction_values is not None:
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"Both baseline_y and prediction_values supplied; baseline_y "
|
|
129
|
+
"takes precedence and prediction_values will be ignored",
|
|
130
|
+
stacklevel=3,
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
warnings.warn(
|
|
134
|
+
"If any differenced variables are included in the model, ensure "
|
|
135
|
+
"they are set to 0 in prediction_values for a meaningful "
|
|
136
|
+
"steady-state prediction. Multiple lags of the same variable "
|
|
137
|
+
"should also be set to the same value",
|
|
138
|
+
stacklevel=3,
|
|
139
|
+
)
|
|
140
|
+
elif d_y > 0:
|
|
141
|
+
if prediction_values is not None:
|
|
142
|
+
warnings.warn(
|
|
143
|
+
"prediction_values is ignored when d_y > 0: there is no "
|
|
144
|
+
"steady-state prediction for y",
|
|
145
|
+
stacklevel=3,
|
|
146
|
+
)
|
|
147
|
+
if inferences_y == "differences":
|
|
148
|
+
if baseline_y is not None and baseline_y != 0:
|
|
149
|
+
warnings.warn(
|
|
150
|
+
"Assuming anything other than baseline_y = 0 when "
|
|
151
|
+
"inferences_y = 'differences' suggests the model is unstable "
|
|
152
|
+
"(y is always changing). Using 0 as baseline_y",
|
|
153
|
+
stacklevel=3,
|
|
154
|
+
)
|
|
155
|
+
if shock_size == 1:
|
|
156
|
+
warnings.warn(
|
|
157
|
+
"effect_type = 'fitted' with inferences_y = 'differences' and "
|
|
158
|
+
"shock_size = 1 is identical to effect_type = 'discrete'; "
|
|
159
|
+
"returning formulae unchanged",
|
|
160
|
+
stacklevel=3,
|
|
161
|
+
)
|
|
162
|
+
elif inferences_y == "levels" and baseline_y is None:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"There is no baseline for y if effect_type = 'fitted' and d_y > 0. "
|
|
165
|
+
"Specify a baseline through baseline_y (with optional uncertainty "
|
|
166
|
+
"through baseline_y_se). If you just want to observe changes with "
|
|
167
|
+
"no meaningful baseline, use baseline_y = 0 and baseline_y_se = 0"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def check_adl_arguments(
|
|
172
|
+
model_info,
|
|
173
|
+
x_vrbl,
|
|
174
|
+
y_vrbl,
|
|
175
|
+
z_vrbl=None,
|
|
176
|
+
x_z_vrbl=None,
|
|
177
|
+
d_x=None,
|
|
178
|
+
d_y=None,
|
|
179
|
+
inferences_x=None,
|
|
180
|
+
inferences_y=None,
|
|
181
|
+
se_type: str = "const",
|
|
182
|
+
kind: str = "GDRF",
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Checks specific to an ADL specification."""
|
|
185
|
+
label = _effect_label(kind)
|
|
186
|
+
check_se_type(se_type)
|
|
187
|
+
|
|
188
|
+
if x_vrbl is None:
|
|
189
|
+
raise ValueError(
|
|
190
|
+
f"Variables in {label} terms (x and possibly y) must be specified "
|
|
191
|
+
"through x_vrbl and possibly y_vrbl"
|
|
192
|
+
)
|
|
193
|
+
if y_vrbl is None:
|
|
194
|
+
warnings.warn(
|
|
195
|
+
f"No y_vrbl in {label} terms implies a static or finite dynamics "
|
|
196
|
+
"model: are you sure you want this?",
|
|
197
|
+
stacklevel=3,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
model_info.require(x_vrbl, "x_vrbl")
|
|
201
|
+
model_info.require(y_vrbl, "y_vrbl")
|
|
202
|
+
|
|
203
|
+
if kind == "GDRF":
|
|
204
|
+
d_x = check_integer(d_x, "d_x")
|
|
205
|
+
d_y = check_integer(d_y, "d_y")
|
|
206
|
+
if inferences_x not in INFERENCES:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"Invalid inferences_x. The shock_history for x must be either "
|
|
209
|
+
"'levels' or 'differences'"
|
|
210
|
+
)
|
|
211
|
+
if inferences_y not in INFERENCES:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
"Invalid inferences_y. The response function for y must be either "
|
|
214
|
+
"'levels' or 'differences'"
|
|
215
|
+
)
|
|
216
|
+
if inferences_x == "differences" and d_x == 0:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
"Invalid inferences_x. The shock_history for x cannot be in a "
|
|
219
|
+
"higher order of differencing (d_x) than the original independent "
|
|
220
|
+
"variable"
|
|
221
|
+
)
|
|
222
|
+
if inferences_y == "differences" and d_y == 0:
|
|
223
|
+
raise ValueError(
|
|
224
|
+
"Invalid inferences_y. The response function for y cannot be in a "
|
|
225
|
+
"higher order of differencing (d_y) than the original dependent "
|
|
226
|
+
"variable"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if kind == "interact":
|
|
230
|
+
if z_vrbl is None or x_z_vrbl is None:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
"Variables in the interaction term (x, z and x:z) must be "
|
|
233
|
+
"specified through x_vrbl, z_vrbl and x_z_vrbl"
|
|
234
|
+
)
|
|
235
|
+
model_info.require(z_vrbl, "z_vrbl")
|
|
236
|
+
model_info.require(x_z_vrbl, "x_z_vrbl")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def check_gecm_arguments(
|
|
240
|
+
model_info,
|
|
241
|
+
x_vrbl,
|
|
242
|
+
y_vrbl,
|
|
243
|
+
x_d_vrbl,
|
|
244
|
+
y_d_vrbl,
|
|
245
|
+
x_vrbl_d_x,
|
|
246
|
+
y_vrbl_d_y,
|
|
247
|
+
x_d_vrbl_d_x,
|
|
248
|
+
y_d_vrbl_d_y,
|
|
249
|
+
inferences_x: str,
|
|
250
|
+
inferences_y: str,
|
|
251
|
+
se_type: str = "const",
|
|
252
|
+
kind: str = "GDRF",
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Checks specific to a GECM specification."""
|
|
255
|
+
label = _effect_label(kind)
|
|
256
|
+
check_se_type(se_type)
|
|
257
|
+
|
|
258
|
+
if x_vrbl is None or y_vrbl is None or x_d_vrbl is None:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Variables in {label} terms (x and y) and lagged differences must be "
|
|
261
|
+
"specified through x_vrbl, y_vrbl, x_d_vrbl and (possibly) y_d_vrbl "
|
|
262
|
+
"for a GECM"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
x_vrbl_d_x = check_integer(x_vrbl_d_x, "x_vrbl_d_x")
|
|
266
|
+
y_vrbl_d_y = check_integer(y_vrbl_d_y, "y_vrbl_d_y")
|
|
267
|
+
x_d_vrbl_d_x = check_integer(x_d_vrbl_d_x, "x_d_vrbl_d_x")
|
|
268
|
+
y_d_vrbl_d_y = check_integer(y_d_vrbl_d_y, "y_d_vrbl_d_y")
|
|
269
|
+
|
|
270
|
+
if x_d_vrbl_d_x - x_vrbl_d_x != 1:
|
|
271
|
+
raise ValueError(
|
|
272
|
+
"In a GECM, the variable in differences should be one order of "
|
|
273
|
+
"differencing from the variable in levels. Check the order of "
|
|
274
|
+
"differencing of x"
|
|
275
|
+
)
|
|
276
|
+
if y_d_vrbl is not None and y_d_vrbl_d_y - y_vrbl_d_y != 1:
|
|
277
|
+
raise ValueError(
|
|
278
|
+
"In a GECM, the variable in differences should be one order of "
|
|
279
|
+
"differencing from the variable in levels. Check the order of "
|
|
280
|
+
"differencing of y"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if len(x_vrbl) != 1 or len(y_vrbl) != 1:
|
|
284
|
+
raise ValueError(
|
|
285
|
+
"In a GECM, include only the first lag of the variable in levels "
|
|
286
|
+
"(x_vrbl and y_vrbl)"
|
|
287
|
+
)
|
|
288
|
+
if list(x_vrbl.values())[0] != 1 or list(y_vrbl.values())[0] != 1:
|
|
289
|
+
raise ValueError(
|
|
290
|
+
"In a GECM, include only the first lag of the variable in levels "
|
|
291
|
+
"(x_vrbl and y_vrbl)"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if inferences_x != "levels" or inferences_y != "levels":
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"In a GECM, inferences regarding the {label} of x on y are "
|
|
297
|
+
"automatically recovered in levels"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
model_info.require(x_vrbl, "x_vrbl")
|
|
301
|
+
model_info.require(y_vrbl, "y_vrbl")
|
|
302
|
+
model_info.require(x_d_vrbl, "x_d_vrbl")
|
|
303
|
+
model_info.require(y_d_vrbl, "y_d_vrbl")
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def resolve_shock_history(shock_history, s_limit: int, cumulative: bool) -> dict:
|
|
307
|
+
"""Validate the shock history and convert a word to its integer order.
|
|
308
|
+
|
|
309
|
+
``"pulse"`` / ``"impulse"`` is :math:`h = -1` and ``"step"`` is
|
|
310
|
+
:math:`h = 0`; an integer is taken as :math:`h` directly; a sequence of
|
|
311
|
+
length ``s_limit + 1`` is a custom history. When ``cumulative`` is set, a
|
|
312
|
+
custom history is cumulated and an integer order is incremented by one.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
dict
|
|
317
|
+
``h_order`` (used for the calculation), ``h_order_plot`` (the order the
|
|
318
|
+
user asked for, used for axis labels) and ``shock_history``.
|
|
319
|
+
"""
|
|
320
|
+
if shock_history is None:
|
|
321
|
+
raise ValueError("shock_history must be specified")
|
|
322
|
+
|
|
323
|
+
custom = None
|
|
324
|
+
h_order = None
|
|
325
|
+
|
|
326
|
+
if isinstance(shock_history, str):
|
|
327
|
+
if shock_history in ("pulse", "impulse"):
|
|
328
|
+
h_order = -1
|
|
329
|
+
elif shock_history == "step":
|
|
330
|
+
h_order = 0
|
|
331
|
+
else:
|
|
332
|
+
raise ValueError(
|
|
333
|
+
"Invalid shock_history. shock_history must be one of 'pulse', "
|
|
334
|
+
"'impulse' or 'step', a sequence for a custom shock history, or "
|
|
335
|
+
"an integer h representing the order of the GDRF"
|
|
336
|
+
)
|
|
337
|
+
elif isinstance(shock_history, bool):
|
|
338
|
+
raise ValueError("Invalid shock_history: booleans are not shock histories")
|
|
339
|
+
elif isinstance(shock_history, (int, np.integer)):
|
|
340
|
+
h_order = int(shock_history)
|
|
341
|
+
elif isinstance(shock_history, (float, np.floating)):
|
|
342
|
+
if float(shock_history) % 1 != 0:
|
|
343
|
+
raise ValueError(
|
|
344
|
+
"Invalid shock_history. A single-value shock_history is the "
|
|
345
|
+
"integer order h"
|
|
346
|
+
)
|
|
347
|
+
h_order = int(shock_history)
|
|
348
|
+
else:
|
|
349
|
+
custom = np.asarray(shock_history, dtype=float).ravel()
|
|
350
|
+
if custom.size == 1:
|
|
351
|
+
h_order, custom = int(custom[0]), None
|
|
352
|
+
elif custom.size != s_limit + 1:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Invalid shock_history. A custom shock history must be of length "
|
|
355
|
+
f"s_limit + 1 ({s_limit + 1}). The supplied shock history has "
|
|
356
|
+
f"length {custom.size}"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
h_order_plot = h_order
|
|
360
|
+
if cumulative:
|
|
361
|
+
if custom is not None:
|
|
362
|
+
custom = np.cumsum(custom)
|
|
363
|
+
else:
|
|
364
|
+
h_order = h_order + 1
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
"h_order": h_order,
|
|
368
|
+
"h_order_plot": h_order_plot,
|
|
369
|
+
"shock_history": custom if custom is not None else h_order_plot,
|
|
370
|
+
"custom": custom,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def adjust_inferences(inferences_y: str, inferences_x: str, d_y: int, d_x: int) -> dict:
|
|
375
|
+
"""Split the orders of differencing into calculation and labelling parts.
|
|
376
|
+
|
|
377
|
+
Asking for inferences in the *differences* of a variable means the
|
|
378
|
+
calculation needs no unwinding for it, but the plot label still has to say
|
|
379
|
+
the response is in differences -- so each order is carried in two places.
|
|
380
|
+
"""
|
|
381
|
+
if inferences_y == "differences":
|
|
382
|
+
calc_d_y, plot_d_y = 0, d_y
|
|
383
|
+
else:
|
|
384
|
+
calc_d_y, plot_d_y = d_y, 0
|
|
385
|
+
if inferences_x == "differences":
|
|
386
|
+
calc_d_x, plot_d_x = 0, d_x
|
|
387
|
+
else:
|
|
388
|
+
calc_d_x, plot_d_x = d_x, 0
|
|
389
|
+
return {
|
|
390
|
+
"calc_d_x": calc_d_x,
|
|
391
|
+
"calc_d_y": calc_d_y,
|
|
392
|
+
"plot_d_x": plot_d_x,
|
|
393
|
+
"plot_d_y": plot_d_y,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def lrm_exists(
|
|
398
|
+
custom_history,
|
|
399
|
+
h_order_plot,
|
|
400
|
+
cumulative: bool,
|
|
401
|
+
calc_d_x: int,
|
|
402
|
+
calc_d_y: int,
|
|
403
|
+
effect_type: str,
|
|
404
|
+
return_lrm: bool,
|
|
405
|
+
) -> bool:
|
|
406
|
+
r"""Whether a Long-Run Multiplier is well defined and should be reported.
|
|
407
|
+
|
|
408
|
+
The LRM exists when :math:`h - d_x + d_y = 0`: the effects then settle on a
|
|
409
|
+
finite non-zero value. If that expression is negative the effects decay to
|
|
410
|
+
zero, and if it is positive they diverge, so there is nothing to report
|
|
411
|
+
either way. Fitted values are excluded entirely, since they are
|
|
412
|
+
predictions rather than effects.
|
|
413
|
+
"""
|
|
414
|
+
if not return_lrm or effect_type == "fitted":
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
if custom_history is not None:
|
|
418
|
+
if not cumulative:
|
|
419
|
+
# A custom history without cumulation has no limiting value to read.
|
|
420
|
+
return False
|
|
421
|
+
warnings.warn(
|
|
422
|
+
"The LRM condition (h - d_x + d_y = 0) holds for integer h but is "
|
|
423
|
+
"not guaranteed to hold for a custom shock history. Interpret the "
|
|
424
|
+
"LRM with caution",
|
|
425
|
+
stacklevel=3,
|
|
426
|
+
)
|
|
427
|
+
return True
|
|
428
|
+
|
|
429
|
+
h = int(h_order_plot) + (1 if cumulative else 0)
|
|
430
|
+
condition = h - calc_d_x + calc_d_y
|
|
431
|
+
if condition == 0:
|
|
432
|
+
return True
|
|
433
|
+
if condition < 0:
|
|
434
|
+
warnings.warn(
|
|
435
|
+
"return_lrm = True but the effects decay to 0 as s -> infinity. No "
|
|
436
|
+
"LRM is returned",
|
|
437
|
+
stacklevel=3,
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
warnings.warn(
|
|
441
|
+
"return_lrm = True but the effects diverge to +/-infinity as "
|
|
442
|
+
"s -> infinity. No LRM is returned",
|
|
443
|
+
stacklevel=3,
|
|
444
|
+
)
|
|
445
|
+
return False
|