macroshock 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (21) hide show
  1. {macroshock-0.2.0/src/macroshock.egg-info → macroshock-0.2.2}/PKG-INFO +15 -15
  2. {macroshock-0.2.0 → macroshock-0.2.2}/README.md +14 -14
  3. {macroshock-0.2.0 → macroshock-0.2.2}/pyproject.toml +1 -1
  4. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/bootstrap.py +12 -12
  5. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/data.py +5 -4
  6. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/fc.py +1 -1
  7. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/fevd.py +5 -6
  8. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/identification.py +81 -35
  9. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/irf.py +5 -5
  10. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/macroshock.py +75 -200
  11. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/plotting.py +47 -53
  12. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/var.py +5 -4
  13. {macroshock-0.2.0 → macroshock-0.2.2/src/macroshock.egg-info}/PKG-INFO +15 -15
  14. {macroshock-0.2.0 → macroshock-0.2.2}/LICENSE +0 -0
  15. {macroshock-0.2.0 → macroshock-0.2.2}/setup.cfg +0 -0
  16. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/__init__.py +0 -0
  17. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock/stats.py +0 -0
  18. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock.egg-info/SOURCES.txt +0 -0
  19. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock.egg-info/dependency_links.txt +0 -0
  20. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock.egg-info/requires.txt +0 -0
  21. {macroshock-0.2.0 → macroshock-0.2.2}/src/macroshock.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: macroshock
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Powerful time series econometrics for Structural Vector Autoregressions (SVAR)
5
5
  Author-email: Patricio Arias <patoariaslopez2@gmail.com>
6
6
  License: MIT License
@@ -41,13 +41,13 @@ Dynamic: license-file
41
41
 
42
42
  ## Main Features
43
43
 
44
- * **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
44
+ * - **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
45
45
 
46
46
  - Constant, trend and quadratic trend.
47
47
  - Exogenous variables.
48
48
  - Seasonal dummies (quarterly or monthly).
49
49
  - Linear restrictions on the coefficients (`restrictions='zeros'` or `'custom zeros'`).
50
- * **Four structural identification methods**:
50
+ * - **Four structural identification methods**:
51
51
 
52
52
  |Method|Description|
53
53
  |-|-|
@@ -56,12 +56,12 @@ Dynamic: license-file
56
56
  |`signs`|Sign restrictions (orthogonal rotation sampling with acceptance/rejection)|
57
57
  |`IV`|Instrumental variable identification (proxy-SVAR)|
58
58
 
59
- * **Impulse-response functions (IRF)** with confidence bands.
60
- * **Forecast error variance decomposition (FEVD)**.
61
- * **Conditional forecasts** with confidence bands.
62
- * **Bootstrap** (normal or *wild* resampling) for inference.
63
- * **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
64
- * **Stability test** of the system (characteristic polynomial roots).
59
+ * - **Impulse-response functions (IRF)** with confidence bands.
60
+ * - **Forecast error variance decomposition (FEVD)**.
61
+ * - **Conditional forecasts** with confidence bands.
62
+ * - **Bootstrap** (normal or *wild* resampling) for inference.
63
+ * - **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
64
+ * - **Stability test** of the system (characteristic polynomial roots).
65
65
 
66
66
  ## Installation
67
67
 
@@ -96,7 +96,7 @@ model = SVAR(
96
96
  horizon=8, # forecast horizon
97
97
  past=12, # past periods to show in the forecast
98
98
  alpha=32, # confidence interval level (32 -> 68% central band)
99
- reps\_default=1000, # bootstrap replications
99
+ reps=1000, # bootstrap replications
100
100
  )
101
101
 
102
102
  # Runs the complete pipeline: Y/X construction, VAR estimation,
@@ -116,13 +116,13 @@ model.S() # identifies the structural system (matrix B)
116
116
  model.summary() # prints the estimation summary
117
117
 
118
118
  # Impulse response to shock 1, with "pair" bands (low/high percentile)
119
- ir = model.ImpulseResponse(shock=1, show=True, bands="pair")
119
+ ir = model.ImpulseResponse(1, show=True, bands="pair")
120
120
 
121
121
  # Forecast with "many" bands (multiple stacked confidence levels)
122
122
  fc = model.Forecast(show=True, bands="many")
123
123
 
124
- # Forecast error variance decomposition for shock 1
125
- vd = model.VarianceDecomp(shock=1, show=True)
124
+ # Forecast error variance decomposition for variable 1
125
+ vd = model.VarianceDecomp(1, show=True)
126
126
  ```
127
127
 
128
128
  Each of these methods returns a dictionary with the point estimate and confidence bands at different levels (`'point'`, `'low'`, `'high'`, `2.5`, `5`, `95`, `97.5`, `0.5`, `99.5`), indexed by variable name.
@@ -143,7 +143,7 @@ By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a b
143
143
  model = SVAR(data=df, variables=variables, lags=4, method="long")
144
144
  ```
145
145
 
146
- By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
146
+ By default it uses the Cholesky decomposition. If `matrix\_long` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
147
147
 
148
148
  ### 3\. Sign restrictions
149
149
 
@@ -157,7 +157,7 @@ model = SVAR(
157
157
  method="signs",
158
158
  matrix\_signs=matrix\_signs,
159
159
  steps\_signs=4, # horizons over which signs are checked
160
- reps\_default=1000, # number of accepted B matrices
160
+ reps=1000, # number of accepted B matrices
161
161
  )
162
162
  ```
163
163
 
@@ -4,13 +4,13 @@
4
4
 
5
5
  ## Main Features
6
6
 
7
- * **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
7
+ * - **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
8
8
 
9
9
  - Constant, trend and quadratic trend.
10
10
  - Exogenous variables.
11
11
  - Seasonal dummies (quarterly or monthly).
12
12
  - Linear restrictions on the coefficients (`restrictions='zeros'` or `'custom zeros'`).
13
- * **Four structural identification methods**:
13
+ * - **Four structural identification methods**:
14
14
 
15
15
  |Method|Description|
16
16
  |-|-|
@@ -19,12 +19,12 @@
19
19
  |`signs`|Sign restrictions (orthogonal rotation sampling with acceptance/rejection)|
20
20
  |`IV`|Instrumental variable identification (proxy-SVAR)|
21
21
 
22
- * **Impulse-response functions (IRF)** with confidence bands.
23
- * **Forecast error variance decomposition (FEVD)**.
24
- * **Conditional forecasts** with confidence bands.
25
- * **Bootstrap** (normal or *wild* resampling) for inference.
26
- * **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
27
- * **Stability test** of the system (characteristic polynomial roots).
22
+ * - **Impulse-response functions (IRF)** with confidence bands.
23
+ * - **Forecast error variance decomposition (FEVD)**.
24
+ * - **Conditional forecasts** with confidence bands.
25
+ * - **Bootstrap** (normal or *wild* resampling) for inference.
26
+ * - **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
27
+ * - **Stability test** of the system (characteristic polynomial roots).
28
28
 
29
29
  ## Installation
30
30
 
@@ -59,7 +59,7 @@ model = SVAR(
59
59
  horizon=8, # forecast horizon
60
60
  past=12, # past periods to show in the forecast
61
61
  alpha=32, # confidence interval level (32 -> 68% central band)
62
- reps\_default=1000, # bootstrap replications
62
+ reps=1000, # bootstrap replications
63
63
  )
64
64
 
65
65
  # Runs the complete pipeline: Y/X construction, VAR estimation,
@@ -79,13 +79,13 @@ model.S() # identifies the structural system (matrix B)
79
79
  model.summary() # prints the estimation summary
80
80
 
81
81
  # Impulse response to shock 1, with "pair" bands (low/high percentile)
82
- ir = model.ImpulseResponse(shock=1, show=True, bands="pair")
82
+ ir = model.ImpulseResponse(1, show=True, bands="pair")
83
83
 
84
84
  # Forecast with "many" bands (multiple stacked confidence levels)
85
85
  fc = model.Forecast(show=True, bands="many")
86
86
 
87
- # Forecast error variance decomposition for shock 1
88
- vd = model.VarianceDecomp(shock=1, show=True)
87
+ # Forecast error variance decomposition for variable 1
88
+ vd = model.VarianceDecomp(1, show=True)
89
89
  ```
90
90
 
91
91
  Each of these methods returns a dictionary with the point estimate and confidence bands at different levels (`'point'`, `'low'`, `'high'`, `2.5`, `5`, `95`, `97.5`, `0.5`, `99.5`), indexed by variable name.
@@ -106,7 +106,7 @@ By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a b
106
106
  model = SVAR(data=df, variables=variables, lags=4, method="long")
107
107
  ```
108
108
 
109
- By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
109
+ By default it uses the Cholesky decomposition. If `matrix\_long` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
110
110
 
111
111
  ### 3\. Sign restrictions
112
112
 
@@ -120,7 +120,7 @@ model = SVAR(
120
120
  method="signs",
121
121
  matrix\_signs=matrix\_signs,
122
122
  steps\_signs=4, # horizons over which signs are checked
123
- reps\_default=1000, # number of accepted B matrices
123
+ reps=1000, # number of accepted B matrices
124
124
  )
125
125
  ```
126
126
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "macroshock"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  requires-python = ">=3.10"
9
9
  dependencies = [
10
10
  "numpy>=1.24.0",
@@ -10,7 +10,7 @@ from .fc import compute_forecast
10
10
  from .fevd import forecast_error_variance_decomposition
11
11
 
12
12
 
13
- def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 0):
13
+ def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 100, impact: float = 1.0, steps: int = 30, horizon: int = 5, past: int = 24):
14
14
  """
15
15
  Replica la lógica original de SVAR.Bootstrap, pero fuera de la clase.
16
16
 
@@ -30,19 +30,19 @@ def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 0):
30
30
  - IR: (reps, steps)
31
31
  - FC: (reps, past + horizon)
32
32
  """
33
+ if model.method == 'signs':
34
+ raise ValueError("No bootstrap in sign restriction SVAR")
33
35
 
34
- if reps == 0:
35
- reps = model.reps_default
36
36
 
37
37
  # BEGIN BOOTSTRAP
38
38
  b = 0
39
39
 
40
40
  if typ == "IR":
41
- draws = {var: np.zeros((reps, model.steps)) for var in model.variables}
41
+ draws = {var: np.zeros((reps, steps)) for var in model.variables}
42
42
  elif typ == "FC":
43
- draws = {var: np.zeros((reps, model.past + model.horizon)) for var in model.variables}
43
+ draws = {var: np.zeros((reps, past + horizon)) for var in model.variables}
44
44
  elif typ == "VD":
45
- draws = {var: np.zeros((reps, model.steps)) for var in model.variables}
45
+ draws = {var: np.zeros((reps, steps)) for var in model.variables}
46
46
  else:
47
47
  raise ValueError("typ must be 'IR', 'FC' or 'VD'.")
48
48
 
@@ -157,11 +157,11 @@ def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 0):
157
157
  [np.ones((model.n_obs, 1)), trend, trend**2, Xsim]
158
158
  )
159
159
 
160
- if model.EXOGdata is not None:
161
- if len(model.EXOG) == model.n_obs:
162
- Xsim_exog = model.EXOGdata
160
+ if model.data_exogenous is not None:
161
+ if len(model.variables_exogenous) == model.n_obs:
162
+ Xsim_exog = model.data_exogenous
163
163
  else:
164
- Xsim_exog = model.EXOGdata[-model.n_obs :]
164
+ Xsim_exog = model.data_exogenous[-model.n_obs :]
165
165
  if len(Xsim_exog) != model.n_obs:
166
166
  print("Exogenous variables in different domain")
167
167
  Xsim = np.hstack([Xsim, Xsim_exog])
@@ -242,7 +242,7 @@ def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 0):
242
242
 
243
243
  # ---------- almacenar draws ----------
244
244
  if typ == "IR":
245
- ir_b = impulse_response(Bsim, Fsim, shock, steps=model.steps, impact=model.impact)
245
+ ir_b = impulse_response(Bsim, Fsim, shock, steps=steps, impact=impact)
246
246
 
247
247
  if np.any(Bsim != 0):
248
248
  if model.method in ("short", "long", "IV"):
@@ -260,7 +260,7 @@ def bootstrap_svar(model, typ: str, shock: int = 1, reps: int = 0):
260
260
 
261
261
  if typ == "VD":
262
262
  vd_dict = forecast_error_variance_decomposition(
263
- Bsim, Fsim, steps=model.steps
263
+ Bsim, Fsim, steps=steps
264
264
  )
265
265
  if np.any(Bsim != 0):
266
266
  if model.method in ("short", "long", "IV"):
@@ -32,11 +32,11 @@ def YX(model) -> YXR:
32
32
 
33
33
  model.n_obs, _ = X.shape
34
34
 
35
- if model.EXOGdata is not None:
35
+ if model.data_exogenous is not None:
36
36
  if model.n_obsex==model.n_obs:
37
- X_exog=model.EXOGdata
37
+ X_exog=model.data_exogenous
38
38
  else:
39
- X_exog=model.EXOGdata[-model.n_obs:]
39
+ X_exog=model.data_exogenous[-model.n_obs:]
40
40
  if len(X_exog)!=model.n_obs:
41
41
  print('Exogenous variables in different domain')
42
42
  X=np.hstack([X,X_exog])
@@ -107,4 +107,5 @@ def YX(model) -> YXR:
107
107
 
108
108
  model.R=np.delete(RRR, drop, axis=1)
109
109
 
110
- return YXR(Y=model.Y, X=model.X, R=model.R)
110
+ return YXR(Y=model.Y, X=model.X, R=model.R)
111
+
@@ -38,7 +38,7 @@ def compute_forecast(modelo, F):
38
38
  if modelo.const == 1:
39
39
  FYs += beta_const
40
40
  elif modelo.const == 2:
41
- FYs += beta_const + ((i + modelo.lags) * beta_trend)
41
+ FYs += beta_const + (i * beta_trend)
42
42
  elif modelo.const == 3:
43
43
  FYs += beta_const + (i * beta_trend) + ((i**2) * beta_sqrd)
44
44
 
@@ -34,16 +34,15 @@ def forecast_error_variance_decomposition(
34
34
 
35
35
  n_vars = B.shape[0]
36
36
 
37
- # 1. Pre-calculate Orthogonalized Impulse Responses for all horizons
37
+ # 1. Pre-calculate Orthogonalized Impulse Responses for all horizons.
38
+ # Accumulate F^h incrementally (F^h = F^(h-1) @ F) instead of calling
39
+ # np.linalg.matrix_power(F, h) from scratch at every horizon.
38
40
  theta = np.zeros((steps, n_vars, n_vars))
41
+ F_power = np.eye(F.shape[0]) # F^0
39
42
  for h in range(steps):
40
- if h == 0:
41
- F_power = np.eye(F.shape[0])
42
- else:
43
- F_power = np.linalg.matrix_power(F, h)
44
-
45
43
  Psi_h = F_power[:n_vars, :n_vars]
46
44
  theta[h] = Psi_h @ B
45
+ F_power = F_power @ F # advance to F^(h+1) for the next iteration
47
46
 
48
47
  # 2. Calculate Variance Decomposition per variable
49
48
  VD = {}
@@ -250,7 +250,7 @@ def identify_signs(
250
250
  FEVDL[k1 + 1].append(vd_check)
251
251
 
252
252
  m_accepted += 1
253
- if m_accepted in list(range(100, reps + 1, 100)):
253
+ if m_accepted in list(range(0+reps//10, reps + 1, reps//10)):
254
254
  print(f"{m_accepted}/{reps}")
255
255
  else:
256
256
  n_rejected += 1
@@ -265,37 +265,75 @@ def identify_signs(
265
265
  "=============================="
266
266
  )
267
267
  B_zero = np.zeros((n_vars, n_vars))
268
+
269
+ # Same key set as the successful path below (ir_* and vd_*,
270
+ # zero-filled with the right shape) so SVAR.S() can always
271
+ # unpack 'extra' the same way, whether or not sampling aborted.
272
+ ir_stats = _zero_stat_dicts(steps, n_vars, extra_percentiles=True)
273
+ vd_stats = _zero_stat_dicts(steps, n_vars, extra_percentiles=False)
274
+
275
+ extra = {
276
+ "IRL": IRL,
277
+ "MB": MB,
278
+ "ir_median": ir_stats["median"],
279
+ "ir_mean": ir_stats["mean"],
280
+ "ir_high": ir_stats["high"],
281
+ "ir_low": ir_stats["low"],
282
+ "ir_00": ir_stats["00"],
283
+ "ir_99": ir_stats["99"],
284
+ "ir_02": ir_stats["02"],
285
+ "ir_97": ir_stats["97"],
286
+ "ir_05": ir_stats["05"],
287
+ "ir_95": ir_stats["95"],
288
+ "ir_10": ir_stats["10"],
289
+ "ir_90": ir_stats["90"],
290
+ "ir_16": ir_stats["16"],
291
+ "ir_84": ir_stats["84"],
292
+ "ir_25": ir_stats["25"],
293
+ "ir_75": ir_stats["75"],
294
+ "FEVDL": FEVDL,
295
+ "vd_median": vd_stats["median"],
296
+ "vd_mean": vd_stats["mean"],
297
+ "vd_high": vd_stats["high"],
298
+ "vd_low": vd_stats["low"],
299
+ "vd_00": vd_stats["00"],
300
+ "vd_99": vd_stats["99"],
301
+ "vd_02": vd_stats["02"],
302
+ "vd_97": vd_stats["97"],
303
+ "vd_05": vd_stats["05"],
304
+ "vd_95": vd_stats["95"],
305
+ "alpha": alpha,
306
+ "steps_signs": steps_signs,
307
+ }
308
+
268
309
  return StructuralResults(
269
310
  B=B_zero,
270
311
  method="signs",
271
312
  n_rejected=n_rejected,
272
313
  m_accepted=m_accepted,
273
314
  corr=0,
274
- extra={"IRL": IRL, "FEVDL": FEVDL, "MB": MB},
315
+ extra=extra,
275
316
  )
276
317
 
277
- # Build IR bands across accepted draws
278
- ir_median = {}
279
- ir_mean = {}
280
- ir_high = {}
281
- ir_low = {}
282
- ir_00 = {}
283
- ir_99 = {}
284
- ir_02 = {}
285
- ir_97 = {}
286
- ir_05 = {}
287
- ir_95 = {}
288
- ir_10 = {}
289
- ir_90 = {}
290
- ir_16 = {}
291
- ir_84 = {}
292
- ir_25 = {}
293
- ir_75 = {}
318
+ # Build IR bands across accepted draws, starting from a zero baseline so
319
+ # that a shock with zero accepted draws (edge case) degrades to zeros
320
+ # instead of crashing np.stack([]) with a ValueError.
321
+ ir_stats = _zero_stat_dicts(steps, n_vars, extra_percentiles=True)
322
+ ir_median, ir_mean = ir_stats["median"], ir_stats["mean"]
323
+ ir_high, ir_low = ir_stats["high"], ir_stats["low"]
324
+ ir_00, ir_99 = ir_stats["00"], ir_stats["99"]
325
+ ir_02, ir_97 = ir_stats["02"], ir_stats["97"]
326
+ ir_05, ir_95 = ir_stats["05"], ir_stats["95"]
327
+ ir_10, ir_90 = ir_stats["10"], ir_stats["90"]
328
+ ir_16, ir_84 = ir_stats["16"], ir_stats["84"]
329
+ ir_25, ir_75 = ir_stats["25"], ir_stats["75"]
294
330
 
295
331
  alpha_low = alpha / 2.0
296
332
  alpha_high = 100.0 - alpha_low
297
333
 
298
334
  for key, lst in IRL.items():
335
+ if len(lst) == 0:
336
+ continue # leave the zero baseline for this shock
299
337
  stack = np.stack(lst, axis=0) # (n_accept, steps, n_vars)
300
338
  ir_median[key] = np.median(stack, axis=0)
301
339
  ir_mean[key] = np.mean(stack, axis=0)
@@ -318,20 +356,16 @@ def identify_signs(
318
356
  # above: 'high' uses the lower percentile and 'low' the upper one,
319
357
  # since that is the swap SVAR.ImpulseResponse() already compensates
320
358
  # for on read -- SVAR.VarianceDecomp() will do the same for FEVD).
321
- vd_median = {}
322
- vd_mean = {}
323
- vd_high = {}
324
- vd_low = {}
325
- vd_00 = {}
326
- vd_99 = {}
327
- vd_02 = {}
328
- vd_97 = {}
329
- vd_05 = {}
330
- vd_95 = {}
359
+ vd_stats = _zero_stat_dicts(steps, n_vars, extra_percentiles=False)
360
+ vd_median, vd_mean = vd_stats["median"], vd_stats["mean"]
361
+ vd_high, vd_low = vd_stats["high"], vd_stats["low"]
362
+ vd_00, vd_99 = vd_stats["00"], vd_stats["99"]
363
+ vd_02, vd_97 = vd_stats["02"], vd_stats["97"]
364
+ vd_05, vd_95 = vd_stats["05"], vd_stats["95"]
331
365
 
332
366
  for key, lst in FEVDL.items():
333
367
  if len(lst) == 0:
334
- continue
368
+ continue # leave the zero baseline for this shock
335
369
  stack = np.stack(lst, axis=0) # (n_accept, steps, n_vars)
336
370
  vd_median[key] = np.median(stack, axis=0)
337
371
  vd_mean[key] = np.mean(stack, axis=0)
@@ -345,8 +379,8 @@ def identify_signs(
345
379
  vd_95[key] = np.percentile(stack, 95.0, axis=0)
346
380
 
347
381
  B_median = np.median(np.stack(MB, axis=0), axis=0)
348
- B_mean = np.mean(np.stack(MB, axis=0), axis=0)
349
-
382
+ #B_mean = np.mean(np.stack(MB, axis=0), axis=0)
383
+
350
384
  extra = {
351
385
  "IRL": IRL,
352
386
  "MB": MB,
@@ -382,7 +416,7 @@ def identify_signs(
382
416
  }
383
417
 
384
418
  return StructuralResults(
385
- B=B_mean,
419
+ B=B_median,
386
420
  method="signs",
387
421
  n_rejected=n_rejected,
388
422
  m_accepted=m_accepted,
@@ -467,7 +501,7 @@ def identify_iv(
467
501
  C = np.linalg.cholesky(sigma_u)
468
502
  q = solve_triangular(C, B[:, j], lower=True)
469
503
  v = np.linalg.norm(q)
470
- B[:, j] = B[:, j] * v
504
+ B[:, j] = B[:, j] / v
471
505
 
472
506
  extra = {
473
507
  "z": z_list,
@@ -482,4 +516,16 @@ def identify_iv(
482
516
  m_accepted=None,
483
517
  corr=corr_list,
484
518
  extra=extra,
485
- )
519
+ )
520
+
521
+ def _zero_stat_dicts(steps: int, n_vars: int, extra_percentiles: bool = False) -> dict:
522
+
523
+ def _zero_per_shock():
524
+ return {k: np.zeros((steps, n_vars)) for k in range(1, n_vars + 1)}
525
+
526
+ names = ["median", "mean", "high", "low", "00", "99", "02", "97", "05", "95"]
527
+ if extra_percentiles:
528
+ names += ["10", "90", "16", "84", "25", "75"]
529
+
530
+ return {name: _zero_per_shock() for name in names}
531
+
@@ -27,14 +27,14 @@ def impulse_response(
27
27
  impulse = np.zeros((n_vars, 1))
28
28
  impulse[shock - 1, 0] = impact
29
29
 
30
+ # Accumulate F^h incrementally (F^h = F^(h-1) @ F) instead of calling
31
+ # np.linalg.matrix_power(F, h) from scratch at every horizon.
32
+ F_power = np.eye(n_vars * lags) # F^0
30
33
  for h in range(steps):
31
- if h == 0:
32
- F_power = np.eye(n_vars * lags)
33
- else:
34
- F_power = np.linalg.matrix_power(F, h)
35
34
  Psi_h = F_power[:n_vars, :n_vars]
36
35
  response = Psi_h @ B @ impulse
37
36
  ir[h, :] = response[:, 0]
37
+ F_power = F_power @ F # advance to F^(h+1) for the next iteration
38
38
 
39
39
  return ir
40
40
 
@@ -101,4 +101,4 @@ def compute_bands_from_draws(
101
101
  stats["q99_5"][var] = np.percentile(arr, 99.5, axis=0)
102
102
 
103
103
 
104
- return stats
104
+ return stats
@@ -13,7 +13,6 @@ from .identification import (
13
13
  identify_iv,
14
14
  )
15
15
  from .bootstrap import bootstrap_svar
16
-
17
16
  from .fc import compute_forecast
18
17
  from .irf import compute_bands_from_draws
19
18
  from .fevd import forecast_error_variance_decomposition
@@ -28,30 +27,30 @@ class SVAR:
28
27
  def __init__(self,
29
28
  data,
30
29
  variables,
31
- lags,
32
- transformation=None,
33
- const=1,
34
- rescaling=None,
35
- restrictions=None,
36
- R=None,
37
- method='short',
38
- matrix_long=None,
39
- matrix_short=None,
40
- matrix_signs=None,
41
- iv=None,
42
- shocks=None,
43
- EXOGdata=None,
44
- EXOG=None,
45
- DUM=None,
46
- impact=1,
47
- steps=60,
48
- steps_signs=1,
49
- resampling=1,
50
- alpha=32,
51
- reps_default=1000,
52
- titles=None,
53
- horizon=6,
54
- past=12
30
+ lags: int,
31
+ transformation = None,
32
+ const: int = 1,
33
+ rescaling = None,
34
+ restrictions = None,
35
+ R = None,
36
+ method: str = 'short',
37
+ matrix_long = None,
38
+ matrix_short = None,
39
+ matrix_signs = None,
40
+ iv = None,
41
+ shocks = None,
42
+ data_exogenous = None,
43
+ variables_exogenous = None,
44
+ DUM = None,
45
+ impact: float = 1.0,
46
+ steps: int = 60,
47
+ steps_signs: int = 1,
48
+ resampling: int = 1,
49
+ alpha: float = 32,
50
+ reps:int = 1000,
51
+ titles = None,
52
+ horizon: int = 6,
53
+ past: int = 12
55
54
  ):
56
55
 
57
56
 
@@ -72,15 +71,15 @@ class SVAR:
72
71
  self.matrix_signs=matrix_signs
73
72
  self.iv=iv
74
73
  self.shocks=shocks
75
- self.EXOGdata=EXOGdata
76
- self.EXOG=EXOG
74
+ self.data_exogenous=data_exogenous
75
+ self.variables_exogenous=variables_exogenous
77
76
  self.DUM=DUM
78
77
  self.impact=impact
79
78
  self.steps=steps
80
79
  self.steps_signs=steps_signs
81
80
  self.resampling=resampling
82
81
  self.alpha=alpha
83
- self.reps_default=reps_default
82
+ self.reps=reps
84
83
  self.titles=titles
85
84
  self.horizon=horizon
86
85
  self.past=past
@@ -96,17 +95,17 @@ class SVAR:
96
95
  if self.method=='IV':
97
96
  self.iv=self.data[self.iv].values
98
97
  self.data=self.data[self.variables].values
99
- if EXOGdata is not None:
100
- if isinstance(EXOGdata, pd.DataFrame):
101
- self.EXOGdata=self.EXOGdata[self.EXOG].values
102
- self.n_obsex,self.n_ex=self.EXOGdata.shape
98
+ if data_exogenous is not None:
99
+ if isinstance(data_exogenous, pd.DataFrame):
100
+ self.data_exogenous=self.data_exogenous[self.variables_exogenous].values
101
+ self.n_obsex,self.n_ex=self.data_exogenous.shape
103
102
  else:
104
103
  self.n_ex=0
105
- if self.EXOG is None:
106
- self.EXOG=['Exog'+f"{i+1}" for i in range(self.n_ex)]
104
+ if self.variables_exogenous is None:
105
+ self.variables_exogenous=['Exog'+f"{i+1}" for i in range(self.n_ex)]
107
106
  for i in range(len(self.transformation)):
108
107
  if self.transformation[i]=="diff":
109
- self.data[1:,i]=np.diff(self.data[:, i])
108
+ self.data[1:,i]=np.diff(self.data[:, i])*self.rescaling[i]
110
109
  elif self.transformation[i]=="logdiff":
111
110
  self.data[1:,i]=np.diff(np.log(self.data[:,i]))*self.rescaling[i]
112
111
  elif self.transformation[i]=="log":
@@ -151,6 +150,7 @@ class SVAR:
151
150
  self.n_obs = res.n_obs
152
151
  self.n_vars = res.n_vars
153
152
  self.n_cols = res.n_cols
153
+ self.n_params = res.n_params
154
154
 
155
155
  # Build companion matrix F exactly as before
156
156
  top = self.beta[:, self.const : self.const + (self.lags * self.n_vars)]
@@ -187,7 +187,7 @@ class SVAR:
187
187
  matrix_signs=self.matrix_signs,
188
188
  steps_signs=self.steps_signs,
189
189
  steps=self.steps,
190
- reps=self.reps_default,
190
+ reps=self.reps,
191
191
  alpha=self.alpha,
192
192
  )
193
193
  self.B = struct.B
@@ -256,7 +256,7 @@ class SVAR:
256
256
  for j in range(self.n_vars):
257
257
  lagged.append("Lag"+f"{i+1}"+"."+self.variables[j])
258
258
 
259
- summary0="Summary\n=====================================\nModel: SVARX\nMethod: OLS\nDate/Time: "+datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")+"\n-------------------------------------\nNo. of Equations: "+f"{self.n_vars:>18.2f}"+"\nNo. of Observations: "+f"{self.n_obs:>15.2f}"+"\nLog Likelihood: "+f"{self.ll:>20.6f}"+"\nAIC: "+f"{self.aic:>31.6f}"+"\nBIC: "+f"{self.bic:>31.6f}"+"\nHQIC: "+f"{self.hqic:>30.6f}"+"\nFPE: "+f"{self.fpe:>31.6f}"+"\nDet: "+f"{self.det:>31.6f}"+"\n-------------------------------------\n"
259
+ summary0="\nSummary\n=====================================\nModel: SVARX\nMethod: OLS\nDate/Time: "+datetime.now().strftime("%Y-%m-%d %H:%M:%S %Z")+"\n-------------------------------------\nNo. of Equations: "+f"{self.n_vars:>18.2f}"+"\nNo. of Observations: "+f"{self.n_obs:>15.2f}"+"\nFree Parameters: "+f"{self.n_params:>19.2f}"+"\nLog Likelihood: "+f"{self.ll:>20.6f}"+"\nAIC: "+f"{self.aic:>31.6f}"+"\nBIC: "+f"{self.bic:>31.6f}"+"\nHQIC: "+f"{self.hqic:>30.6f}"+"\nFPE: "+f"{self.fpe:>31.6f}"+"\nDet: "+f"{self.det:>31.6f}"+"\n-------------------------------------\n"
260
260
  for i in range(self.n_vars):
261
261
  summary1="Results for equation "+self.variables[i]+"\n=====================================================================\n coefficient std. error t-stat p-value\n---------------------------------------------------------------------\n"
262
262
  if self.const==1:
@@ -274,7 +274,7 @@ class SVAR:
274
274
  summary2 = (lagged[j-self.const]+f"{self.beta[i,j]:>{26-len(lagged[j-self.const])}.6f}"f"{self.beta_std[i,j]:>16.6f}"f"{self.tstat[i,j]:>12.3f}"f"{self.pvalue[i,j]:>12.3f}\n")
275
275
  summary1=summary1+summary2
276
276
  elif j in range(self.const+(self.lags*self.n_vars),self.const+(self.lags*self.n_vars)+self.n_ex):
277
- summary2 = (self.EXOG[j-self.const-(self.lags*self.n_vars)]+f"{self.beta[i,j]:>{26-len(self.EXOG[j-self.const-(self.lags*self.n_vars)])}.6f}"f"{self.beta_std[i,j]:>16.6f}"f"{self.tstat[i,j]:>12.3f}"f"{self.pvalue[i,j]:>12.3f}\n")
277
+ summary2 = (self.variables_exogenous[j-self.const-(self.lags*self.n_vars)]+f"{self.beta[i,j]:>{26-len(self.variables_exogenous[j-self.const-(self.lags*self.n_vars)])}.6f}"f"{self.beta_std[i,j]:>16.6f}"f"{self.tstat[i,j]:>12.3f}"f"{self.pvalue[i,j]:>12.3f}\n")
278
278
  summary1=summary1+summary2
279
279
  elif j in range(self.const+(self.lags*self.n_vars)+self.n_ex,self.const+(self.lags*self.n_vars)+self.n_ex+self.n_dum):
280
280
  summary2 = (self.dummies[j-self.const-(self.lags*self.n_vars)-self.n_ex]+f"{self.beta[i,j]:>{26-len(self.dummies[j-self.const-(self.lags*self.n_vars)-self.n_ex])}.6f}"f"{self.beta_std[i,j]:>16.6f}"f"{self.tstat[i,j]:>12.3f}"f"{self.pvalue[i,j]:>12.3f}\n")
@@ -315,29 +315,27 @@ class SVAR:
315
315
  #--------------------------------------------------------------------------
316
316
 
317
317
  # impulse response calculation and plotting function
318
- def ImpulseResponse(self, shock, show=True, bands='pair', steps=None, impact=None):
318
+ def ImpulseResponse(self, shock: int, show: bool = True, bands: str = 'pair', reps: int = None, steps: int = None, impact: float = None):
319
+ if reps is None:
320
+ reps = self.reps
319
321
  if steps is None:
320
322
  steps = self.steps
321
- else:
322
- self.steps = steps
323
323
  if impact is None:
324
324
  impact = self.impact
325
- else:
326
- self.impact = impact
327
325
  """
328
326
  Calcula IRFs (punto y bandas) y, opcionalmente, grafica.
329
327
  Mantiene la API y el output de la versión original.
330
328
  """
331
- print("\nshock: " + str(shock) + "\n")
332
-
333
329
  # tipo de bandas
334
330
  self.bands = bands
335
331
 
336
332
  # Métodos distintos a 'signs': bootstrap estándar
337
333
  if self.method != 'signs':
334
+
335
+ print("\nCalculating responses to shock " + str(shock) + ":\n")
338
336
 
339
337
  # Bootstrap de IRFs para este shock
340
- self.drawsIR=bootstrap_svar(self, typ='IR', shock=shock)
338
+ self.drawsIR=bootstrap_svar(self, typ='IR', shock=shock, reps=reps, steps=steps)
341
339
 
342
340
  # Calcular medias y cuantiles a partir de self.drawsIR
343
341
  stats = compute_bands_from_draws(self.drawsIR, self.alpha)
@@ -356,6 +354,9 @@ class SVAR:
356
354
 
357
355
  # Método 'signs': ya tenemos el conjunto de identificaciones
358
356
  else:
357
+
358
+ print("\nResponses to shock " + str(shock) + "\n")
359
+
359
360
  # self.ir_median, self.ir_mean, etc. vienen de identify_signs (identification.py),
360
361
  # indexados por número de shock.
361
362
 
@@ -426,84 +427,15 @@ class SVAR:
426
427
  }
427
428
 
428
429
  return output
429
-
430
-
431
- print("\nshock: "+str(shock)+"\n")
432
- # type of bands
433
- self.bands=bands
434
-
435
- # for signs IMF are already estimated from the set identification process, thus this bootstrap is only for the other methods
436
- if self.method!='signs':
437
-
438
- self.Bootstrap('IR',shock=shock)
439
-
440
- # alpha levels
441
- lower_q = self.alpha / 2
442
- upper_q = 100 - self.alpha / 2
443
- #--------------------------------------------------------------
444
-
445
- # storage for IRF
446
- self.ir_point = {}
447
- self.ir_mean = {}
448
- self.ir_low = {}
449
- self.ir_high = {}
450
- self.ir_95 = {}
451
- self.ir_05 = {}
452
- self.ir_00 = {}
453
- self.ir_99 = {}
454
- self.ir_02 = {}
455
- self.ir_97 = {}
456
-
457
- for var in self.variables:
458
- x=np.zeros((self.reps_default,self.steps))
459
- for j in range(self.reps_default):
460
- x[j,:]=self.drawsIR[var][j]
461
- self.drawsIR[var]=x
462
-
463
- for var in self.variables:
464
- self.ir_point[var] = np.mean(self.drawsIR[var], axis=0)
465
- self.ir_mean[var] = np.mean(self.drawsIR[var], axis=0)
466
- self.ir_low[var] = np.percentile(self.drawsIR[var], lower_q, axis=0)
467
- self.ir_high[var] = np.percentile(self.drawsIR[var], upper_q, axis=0)
468
- self.ir_95[var] = np.percentile(self.drawsIR[var], 95, axis=0)
469
- self.ir_05[var] = np.percentile(self.drawsIR[var], 5, axis=0)
470
- self.ir_00[var] = np.percentile(self.drawsIR[var], 0.5, axis=0)
471
- self.ir_99[var] = np.percentile(self.drawsIR[var], 99.5, axis=0)
472
- self.ir_02[var] = np.percentile(self.drawsIR[var], 2.5, axis=0)
473
- self.ir_97[var] = np.percentile(self.drawsIR[var], 97.5, axis=0)
474
- #--------------------------------------------------------------
475
-
476
- # if method is signs we already have the identification set for B matrices thus no bootstrap
477
- elif self.method=='signs':
478
-
479
- self.ir_point = self.ir_median[shock]
480
- self.ir_low = self.ir_low[shock]
481
- self.ir_high = self.ir_high[shock]
482
- self.ir_02 = self.ir_02[shock]
483
- self.ir_97 = self.ir_97[shock]
484
- self.ir_05 = self.ir_05[shock]
485
- self.ir_95 = self.ir_95[shock]
486
- self.ir_00 = self.ir_00[shock]
487
- self.ir_99 = self.ir_99[shock]
488
- self.ir_mean = self.ir_mean[shock]
489
- #--------------------------------------------------------------
490
-
491
- # show the plots if indicated
492
- if show==True:
493
- plot_irf_svar(shock)
494
-
495
- #--------------------------------------------------------------
496
-
497
- # output set up
498
- if self.method!='signs':
499
- output={'point':self.ir_mean, 'low':self.ir_low, 'high':self.ir_high, 2.5:self.ir_02, 5:self.ir_05, 0.5:self.ir_00, 99.5:self.ir_99,97.5:self.ir_97,95:self.ir_95}
500
- else:
501
- output={'point':self.ir_point, 'low':self.ir_low, 'high':self.ir_high, 2.5:self.ir_02, 5:self.ir_05, 0.5:self.ir_00, 99.5:self.ir_99,97.5:self.ir_97,95:self.ir_95}
502
-
503
- return output
504
430
 
505
431
  # forecast calculation and plotting (THIS FUNCTION MIMICS THE IMPULSE RESPONSE ONE FOR BOOTSTAP AND BANDS LOGIC)
506
- def Forecast(self, show=True, bands='many'):
432
+ def Forecast(self, show: bool = True, bands: str = 'many', horizon: int = None, past: int = None, reps: int = None):
433
+ if reps is None:
434
+ reps = self.reps
435
+ if horizon is None:
436
+ horizon = self.horizon
437
+ if past is None:
438
+ past = self.past
507
439
  """
508
440
  Calcula pronósticos (punto y bandas) y, opcionalmente, grafica.
509
441
  Mantiene la API y estructura de salida de la función original.
@@ -511,13 +443,13 @@ class SVAR:
511
443
  # Para métodos distintos de 'signs', hacemos bootstrap
512
444
  if self.method != 'signs':
513
445
 
514
- print("Forecast:\n")
446
+ print("\nCalculating forecast:\n")
515
447
 
516
448
  # Pronóstico determinista (no lo usabas mucho, pero lo mantenemos)
517
449
  self.fc_point = compute_forecast(self, self.F)
518
450
 
519
451
  # Bootstrap de pronósticos
520
- self.drawsFC=bootstrap_svar(self, typ='FC')
452
+ self.drawsFC=bootstrap_svar(self, typ='FC', horizon=horizon, past=past, reps=reps)
521
453
 
522
454
  # Calcular bandas a partir de self.drawsFC
523
455
  stats = compute_bands_from_draws(self.drawsFC, self.alpha)
@@ -535,7 +467,7 @@ class SVAR:
535
467
  # Para 'signs', no había bootstrap en tu código: solo trayectoria determinista
536
468
  elif self.method == 'signs':
537
469
 
538
- print('No bands Forecast:\n')
470
+ print('\nNo bands Forecast\n')
539
471
 
540
472
  fc_det = compute_forecast(self, self.F) # (past + horizon, n_vars)
541
473
 
@@ -582,92 +514,33 @@ class SVAR:
582
514
  }
583
515
 
584
516
  return output
585
-
586
-
587
- if self.method!='signs':
588
-
589
- print("\nreps_default:\n")
590
-
591
- self.fc_point = self.FC(self.F)
592
-
593
- self.Bootstrap('FC')
594
-
595
- lower_q = self.alpha / 2
596
- upper_q = 100 - self.alpha / 2
597
-
598
- self.fc_mean = {}
599
- self.fc_low = {}
600
- self.fc_high = {}
601
- self.fc_95 = {}
602
- self.fc_50 = {}
603
- self.fc_05 = {}
604
- self.fc_02 = {}
605
- self.fc_97 = {}
606
- self.fc_00 = {}
607
- self.fc_99 = {}
608
-
609
- for var in self.variables:
610
- x=np.zeros((self.reps_default,self.past+self.horizon))
611
- for j in range(self.reps_default):
612
- x[j,:]=self.drawsFC[var][j]
613
- self.drawsFC[var]=x
614
-
615
- for var in self.variables:
616
- self.fc_mean[var] = np.mean(self.drawsFC[var], axis=0)
617
- self.fc_low[var] = np.percentile(self.drawsFC[var], lower_q, axis=0)
618
- self.fc_high[var] = np.percentile(self.drawsFC[var], upper_q, axis=0)
619
- self.fc_95[var] = np.percentile(self.drawsFC[var], 95, axis=0)
620
- self.fc_99[var] = np.percentile(self.drawsFC[var], 99.5, axis=0)
621
- self.fc_00[var] = np.percentile(self.drawsFC[var], 0.5, axis=0)
622
- self.fc_05[var] = np.percentile(self.drawsFC[var], 5, axis=0)
623
- self.fc_02[var] = np.percentile(self.drawsFC[var], 2.5, axis=0)
624
- self.fc_97[var] = np.percentile(self.drawsFC[var], 97.5, axis=0)
625
-
626
- elif self.method=='signs':
627
-
628
- print('no bands Forecast:')
629
- self.fc_mean = {self.variables[i]:self.FC(self.F)[:,i] for i in range(self.n_vars)}
630
- self.fc_low = self.fc_mean
631
- self.fc_high = self.fc_mean
632
- self.fc_95 = self.fc_mean
633
- self.fc_99 = self.fc_mean
634
- self.fc_00 = self.fc_mean
635
- self.fc_05 = self.fc_mean
636
- self.fc_02 = self.fc_mean
637
- self.fc_97 = self.fc_mean
638
-
639
- if show==True:
640
-
641
- self.bands=bands
642
-
643
- plot_forecast_svar(self)
644
-
645
- output={'point':self.fc_mean, 'low':self.fc_low, 'high':self.fc_high, 2.5:self.fc_02, 5:self.fc_05,97.5:self.fc_97,95:self.fc_95,0.5:self.fc_00,99.5:self.fc_99}
646
-
647
- return output
648
517
 
649
518
 
650
- def VarianceDecomp(self, shock: int = 1, show: bool = True, bands: str = 'pair', steps: int = None):
519
+ def VarianceDecomp(self, shock: int, show: bool = True, bands: str = 'pair', steps: int = None, reps: int = None):
651
520
  """
652
521
  Calcula la Descomposición de Varianza (punto y bandas) y, opcionalmente, grafica.
653
522
  Mantiene la API y estructura de salida de ImpulseResponse y Forecast.
654
523
  """
655
524
  if steps is None:
656
525
  steps = self.steps
657
- else:
658
- self.steps = steps
659
-
660
- print("Calculating Variance Decomposition:\n")
526
+ if reps is None:
527
+ reps = self.reps
661
528
 
662
529
  self.bands = bands
663
530
 
664
531
  if self.method != 'signs':
532
+
533
+ print(f"\nCalculating FEVD for {self.variables[shock-1]}:\n")
534
+
665
535
  self.vd_det = forecast_error_variance_decomposition(self.B, self.F, steps=self.steps)
666
536
 
667
- self.drawsVD = bootstrap_svar(self, typ='VD', shock=shock)
668
-
537
+ self.drawsVD = bootstrap_svar(self, typ='VD', shock=shock, reps=reps, steps=steps)
538
+
539
+ #self.vd_point
540
+
669
541
  stats = compute_bands_from_draws(self.drawsVD, self.alpha)
670
-
542
+
543
+ self.vd_point = stats["point"]
671
544
  self.vd_mean = stats["mean"]
672
545
  self.vd_low = stats["low"]
673
546
  self.vd_high = stats["high"]
@@ -679,6 +552,8 @@ class SVAR:
679
552
  self.vd_99 = stats["q99_5"]
680
553
 
681
554
  elif self.method == 'signs':
555
+
556
+ print(f"\nFEVD for {self.variables[shock-1]}\n")
682
557
 
683
558
  # Deterministic FEVD for the median B matrix (self.B is already
684
559
  # the median of the accepted set for 'signs'). This is what
@@ -727,11 +602,11 @@ class SVAR:
727
602
  if show is True:
728
603
  # Uses self.vd_det in both branches -- plot_variance_decomp_svar()
729
604
  # doesn't need to know about self.method at all.
730
- plot_variance_decomp_svar(self)
605
+ plot_variance_decomp_svar(self, shock)
731
606
 
732
607
  if self.method != 'signs':
733
608
  output = {
734
- 'point': self.vd_mean,
609
+ 'point': self.vd_point,
735
610
  'low': self.vd_low,
736
611
  'high': self.vd_high,
737
612
  2.5: self.vd_02,
@@ -801,8 +676,8 @@ class SVAR:
801
676
  self.Forecast(bands=FCbands)
802
677
 
803
678
  if showVD==True:
804
- self.VarianceDecomp()
805
-
679
+ for k in range(self.n_vars):
680
+ self.VarianceDecomp(k+1,show=show,bands=IRbands)
806
681
 
807
682
  class SVEC:
808
683
 
@@ -1,4 +1,3 @@
1
- # plotting.py
2
1
  from __future__ import annotations
3
2
 
4
3
  import numpy as np
@@ -231,73 +230,68 @@ def plot_forecast_svar(model):
231
230
 
232
231
  plt.tight_layout(rect=[0, 0, 1, 0.95])
233
232
 
234
- def plot_variance_decomp_svar(model):
233
+ def plot_variance_decomp_svar(model, shock: int):
235
234
  """
236
- Grafica la Descomposición de Varianza (FEVD) para un modelo SVAR.
237
- Crea un gráfico de áreas apiladas (stackplot) por cada variable, donde
238
- las áreas suman 100% y muestran la proporción de la varianza explicada.
235
+ Grafica la Descomposición de Varianza (FEVD) para una sola variable de
236
+ un modelo SVAR: un stackplot con la contribución de cada shock
237
+ estructural a la varianza del error de pronóstico de esa variable, en
238
+ cada horizonte.
239
+
240
+ Parameters
241
+ ----------
242
+ model : objeto SVAR
243
+ Debe tener .variables, .shocks, .steps y .vd_det poblados (p.ej.
244
+ tras llamar model.VarianceDecomp()).
245
+ shock : int
246
+ Índice 1-based de la variable a graficar. Coincide directamente con
247
+ las llaves de model.vd_det (indexado 1..n_vars por variable).
239
248
  """
240
- n_vars = len(model.variables)
241
-
242
- # Adjust layout dynamically based on the number of variables
243
- cols = 1
244
- rows = n_vars
245
-
246
- fig, axes = plt.subplots(rows, cols, figsize=(8, 2+ 2 * rows), squeeze=False)
247
- axes = axes.flatten()
248
-
249
+ var_name = model.variables[shock - 1]
250
+
249
251
  horizon = np.arange(model.steps)
250
252
 
251
253
  # Check for assigned shocks
252
254
  if model.shocks is None:
253
255
  model.shocks = model.variables
254
-
256
+
257
+ fig, ax = plt.subplots(figsize=(7, 4))
258
+
255
259
  fig.suptitle(
256
- "Forecast Error Variance Decomposition",
257
- fontsize=16,
260
+ f"Forecast Error Variance Decomposition: {var_name}",
261
+ fontsize=13,
258
262
  fontname="Times New Roman",
259
263
  color="black"
260
264
  )
261
-
265
+
262
266
  # Generate a distinct color palette for the structural shocks
263
267
  colors = plt.cm.tab10(np.linspace(0, 1, len(model.shocks)))
264
-
265
- for i, var in enumerate(model.variables):
266
- ax = axes[i]
267
-
268
- # Extract the deterministic FEVD matrix for variable i+1
269
- # model.vd_det[i+1] is shape (steps, n_shocks)
270
- # We multiply by 100 and transpose (.T) so stackplot can read shape (n_shocks, steps)
271
- y_data = (model.vd_det[i + 1] * 100).T
272
-
273
- # Generate the continuous stacked area plot
274
- ax.stackplot(
275
- horizon,
276
- y_data,
277
- labels=model.shocks if i == 0 else (), # Only attach labels to the first subplot
278
- colors=colors,
279
- alpha=0.9 # Adds a slight transparency
280
- )
281
-
282
- ax.set_title(f"{var}", fontsize=12, fontweight='bold')
283
- ax.set_ylim([0, 100])
284
- ax.set_xlim([0, model.steps - 1]) # Flush limits for a continuous line
285
- ax.set_xlabel("Horizonte")
286
- ax.set_ylabel("Varianza Explicada (%)")
287
- ax.grid(True, axis='y', alpha=0.3)
288
-
289
- # Add a single, centralized legend for the entire figure
290
- fig.legend(
291
- loc='lower center',
292
- ncol=len(model.shocks),
293
- bbox_to_anchor=(0.5, -0.05),
268
+
269
+ # Extract the deterministic FEVD matrix for the target variable.
270
+ # model.vd_det[shock] is shape (steps, n_shocks); multiply by 100 and
271
+ # transpose (.T) so stackplot can read shape (n_shocks, steps).
272
+ y_data = (model.vd_det[shock] * 100).T
273
+
274
+ ax.stackplot(
275
+ horizon,
276
+ y_data,
277
+ labels=model.shocks,
278
+ colors=colors,
279
+ alpha=0.9
280
+ )
281
+
282
+ ax.set_ylim([0, 100])
283
+ ax.set_xlim([0, model.steps - 1])
284
+ ax.set_xlabel("Horizonte")
285
+ ax.set_ylabel("Varianza Explicada (%)")
286
+ ax.grid(True, axis='y', alpha=0.3)
287
+
288
+ ax.legend(
289
+ loc='lower center',
290
+ ncol=len(model.shocks),
291
+ bbox_to_anchor=(0.5, -0.45),
294
292
  frameon=False,
295
293
  fontsize=11
296
294
  )
297
295
 
298
- # Hide any empty subplots if n_vars doesn't perfectly fill the grid
299
- for k in range(i + 1, len(axes)):
300
- axes[k].set_visible(False)
301
-
302
- fig.tight_layout(rect=[0, 0.05, 1, 0.95])
296
+ fig.tight_layout()
303
297
  plt.show()
@@ -26,10 +26,10 @@ class VARResults:
26
26
  beta_std: np.ndarray
27
27
  tstat: np.ndarray
28
28
  pvalue: np.ndarray
29
- F: np.ndarray
30
29
  n_obs: int
31
30
  n_vars: int
32
31
  n_cols: int
32
+ n_params: int
33
33
 
34
34
  def estimate_var(Y: np.ndarray, X: np.ndarray, R: np.ndarray) -> VARResults:
35
35
  # 1. Basic Setup
@@ -98,10 +98,11 @@ def estimate_var(Y: np.ndarray, X: np.ndarray, R: np.ndarray) -> VARResults:
98
98
  aic = 2.0 * n_params - 2.0 * ll
99
99
  bic = n_params * np.log(n_obs) - 2.0 * ll
100
100
  hqic = 2.0 * n_params * np.log(np.log(n_obs)) - 2.0 * ll
101
-
101
+ fpe = np.linalg.det(sigma_u) * ((n_obs + n_cols) / (n_obs - n_cols)) ** n_vars
102
+
102
103
  return VARResults(
103
104
  beta=beta, sigma_u=sigma_u, resid=resid, Y_hat=Y_hat,
104
105
  ll=ll, aic=aic, bic=bic, hqic=hqic, det=np.linalg.det(sigma_u),
105
- fpe=0.0, beta_std=beta_std, tstat=tstat, pvalue=pvalue,
106
- F=None, n_obs=n_obs, n_vars=n_vars, n_cols=n_cols
106
+ fpe=fpe, beta_std=beta_std, tstat=tstat, pvalue=pvalue,
107
+ n_obs=n_obs, n_vars=n_vars, n_cols=n_cols, n_params=n_params
107
108
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: macroshock
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Powerful time series econometrics for Structural Vector Autoregressions (SVAR)
5
5
  Author-email: Patricio Arias <patoariaslopez2@gmail.com>
6
6
  License: MIT License
@@ -41,13 +41,13 @@ Dynamic: license-file
41
41
 
42
42
  ## Main Features
43
43
 
44
- * **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
44
+ * - **Restricted VAR estimation** (Lütkepohl-style GLS), with support for:
45
45
 
46
46
  - Constant, trend and quadratic trend.
47
47
  - Exogenous variables.
48
48
  - Seasonal dummies (quarterly or monthly).
49
49
  - Linear restrictions on the coefficients (`restrictions='zeros'` or `'custom zeros'`).
50
- * **Four structural identification methods**:
50
+ * - **Four structural identification methods**:
51
51
 
52
52
  |Method|Description|
53
53
  |-|-|
@@ -56,12 +56,12 @@ Dynamic: license-file
56
56
  |`signs`|Sign restrictions (orthogonal rotation sampling with acceptance/rejection)|
57
57
  |`IV`|Instrumental variable identification (proxy-SVAR)|
58
58
 
59
- * **Impulse-response functions (IRF)** with confidence bands.
60
- * **Forecast error variance decomposition (FEVD)**.
61
- * **Conditional forecasts** with confidence bands.
62
- * **Bootstrap** (normal or *wild* resampling) for inference.
63
- * **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
64
- * **Stability test** of the system (characteristic polynomial roots).
59
+ * - **Impulse-response functions (IRF)** with confidence bands.
60
+ * - **Forecast error variance decomposition (FEVD)**.
61
+ * - **Conditional forecasts** with confidence bands.
62
+ * - **Bootstrap** (normal or *wild* resampling) for inference.
63
+ * - **Built-in plots** for IRF, forecasts and FEVD, publication-ready.
64
+ * - **Stability test** of the system (characteristic polynomial roots).
65
65
 
66
66
  ## Installation
67
67
 
@@ -96,7 +96,7 @@ model = SVAR(
96
96
  horizon=8, # forecast horizon
97
97
  past=12, # past periods to show in the forecast
98
98
  alpha=32, # confidence interval level (32 -> 68% central band)
99
- reps\_default=1000, # bootstrap replications
99
+ reps=1000, # bootstrap replications
100
100
  )
101
101
 
102
102
  # Runs the complete pipeline: Y/X construction, VAR estimation,
@@ -116,13 +116,13 @@ model.S() # identifies the structural system (matrix B)
116
116
  model.summary() # prints the estimation summary
117
117
 
118
118
  # Impulse response to shock 1, with "pair" bands (low/high percentile)
119
- ir = model.ImpulseResponse(shock=1, show=True, bands="pair")
119
+ ir = model.ImpulseResponse(1, show=True, bands="pair")
120
120
 
121
121
  # Forecast with "many" bands (multiple stacked confidence levels)
122
122
  fc = model.Forecast(show=True, bands="many")
123
123
 
124
- # Forecast error variance decomposition for shock 1
125
- vd = model.VarianceDecomp(shock=1, show=True)
124
+ # Forecast error variance decomposition for variable 1
125
+ vd = model.VarianceDecomp(1, show=True)
126
126
  ```
127
127
 
128
128
  Each of these methods returns a dictionary with the point estimate and confidence bands at different levels (`'point'`, `'low'`, `'high'`, `2.5`, `5`, `95`, `97.5`, `0.5`, `99.5`), indexed by variable name.
@@ -143,7 +143,7 @@ By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a b
143
143
  model = SVAR(data=df, variables=variables, lags=4, method="long")
144
144
  ```
145
145
 
146
- By default it uses the Cholesky decomposition. If `matrix\_short` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
146
+ By default it uses the Cholesky decomposition. If `matrix\_long` is passed (a binary matrix of 1s and 0s), the `B` matrix satisfying the indicated zero restrictions is solved numerically.
147
147
 
148
148
  ### 3\. Sign restrictions
149
149
 
@@ -157,7 +157,7 @@ model = SVAR(
157
157
  method="signs",
158
158
  matrix\_signs=matrix\_signs,
159
159
  steps\_signs=4, # horizons over which signs are checked
160
- reps\_default=1000, # number of accepted B matrices
160
+ reps=1000, # number of accepted B matrices
161
161
  )
162
162
  ```
163
163
 
File without changes
File without changes