Predicting Post-Pandemic Consumption Behaviors Given Potential Paths of Monetary Policy, Part 2 -- Cointegration, VAR/VECM and Forecast

In the last blog, we went through the economic intuitions behind this consumption model, conducted some preliminary analysis in search for structural breaks and checked the stationarity of the endogenous variables using ADF and KPSS tests. Since we have found that none of the variables can be considered stationary, we can therefore ask whether the variables form a cointegrated system with a given number of “common trends”. Intuitively, I would argue that there exists one cointegration equation among the three variables as in the long run, one’s consumption and net wealth should add up to the one’s income.

So let’s keep going down to the path, and see how we can test for cointegration and include it in a VECM.

Cointegration Test

Given that all variables are consistent with the unit root hypothesis and possibly cointegrated, we can use Johansen’s trace statistic to test for the existence of a common trend i.e., a long-run cointegration relationship among the three endogenous variables.

First, we should estimate the VAR in levels so as to find out the optimal lags to use for the cointegration test. You might want to do this in first differences depending on your preferences, but basically the objective here is to run a preliminary VAR without worrying about whether the variables are cointegrated, which is obviously an issue given that they all have unit roots.

VAR in Levels

I’m going to allow for 10 lags in the first instance just to get the process going, and these are the variables in the VAR as I include the dummy variables sb_1975_4, sb_2008_3 to allow for the possibility of a structural breaks.

The BIC and HQ statistics suggest two lags for the optimal number of lags, whereas the AIC and FPE statistics suggest three. We choose the VAR model with three lags to allow for the possibility.

1
train = train.iloc[1:]
1
2
3
model = VAR(endog=train[['ln_rc','ln_rdy','ln_rnw']], exog =train[['sb_1975_4','sb_2008_3','const']])
res = model.select_order(10)
res.summary()
VAR Order Selection (* highlights the minimums)
AIC BIC FPE HQIC
0 -17.58 -17.39 2.310e-08 -17.51
1 -27.99 -27.66 6.975e-13 -27.86
2 -28.19 -27.71* 5.696e-13 -28.00*
3 -28.20* -27.58 5.664e-13* -27.95
4 -28.17 -27.40 5.829e-13 -27.86
5 -28.13 -27.22 6.067e-13 -27.76
6 -28.12 -27.07 6.144e-13 -27.69
7 -28.07 -26.87 6.488e-13 -27.58
8 -28.02 -26.68 6.810e-13 -27.48
9 -28.02 -26.53 6.807e-13 -27.42
10 -27.97 -26.33 7.217e-13 -27.31

Johansen Cointegration Test

Then, we conduct the Johansen’s test with two lags. One has to ensure that the lag interval for the
differenced endogenous variables is 1 smaller than the number of lags used in VAR in levels by moving to the corresponding VECM form.

1
2
3
vec_rank = vecm.select_coint_rank(endog=train[['ln_rc','ln_rdy','ln_rnw']], det_order = 0, k_ar_diff = 2, method = 'trace', signif=0.05)
print(vec_rank.summary())
print('The rank to choose according to the Johansen test: '+ str(vec_rank.rank))
Johansen cointegration test using trace test statistic with 5% significance level
=====================================
r_0 r_1 test statistic critical value
-------------------------------------
  0   3          33.75          29.80
  1   3          11.44          15.49
-------------------------------------
The rank to choose according to the Johansen test: 1

The results suggest that we can reject the null hypothesis of no cointegration (or zero cointegrating vectors) using a 5% significance level. Moreover, we cannot reject the null hypotheses of at most 1 cointegrating vectors versus the alternative of 2. Therefore, we assume that there exists one (and only one) cointegrating vector.

VECM Estimation

Since there is a cointegration relationship existing, we should construct an ECM equation (e.g., an error correction model) that can explain the actual behavior of consumption during the sample period based on the long-run specification.

In developing the ECM model, we can include additional exogenous variables in the VECM to explain short-run dynamics of consumption. Besides the structural break around 1976 and the inequality gap, here are a bunch of other variables that we should consider. Remember the the determinants we discussed at the beginning?

  • Disposable Income (in the current period) (+)
  • Expectations (consumer confidence indexes, employment growth) (+)
  • Wealth (stock market performance, housing prices) (+)
  • Uncertainty (precautionary saving) (-)
  • Availability of Credit (+)
  • Real Interest Rate (?)
    • Substitution effect (-)
    • Income effect (+)

Explore Exogenous Variables

In developing the ECM model, we should include additional exogenous variables in the VECM to explain short-run dynamics of consumption.

Expectations

Here we use the unemployment rate and the consumer confidence index as exogenous variables. These variables are proxies for changes in the level of income uncertainty facing the household sector.

1
2
3
4
5
f, (ax1, ax2) = plt.subplots(1, 2,figsize=(14,4))
ax1.plot(train['unemp'])
ax2.plot(train['unemp'].diff().dropna())
ax1.set_title('unemp')
ax2.set_title('diff_unemp')
1
2
3
4
5
f, (ax1, ax2) = plt.subplots(1, 2,figsize=(14,4))
ax1.plot(train['consumer_confidence'])
ax2.plot(np.log(train['consumer_confidence']))
ax1.set_title('consumer_confidence')
ax2.set_title('ln_consumer_confidence')

Let’s then conduct the stationary tests for the change in the unemployment rate and the logarithm of the consumer confidence index, to check whether they are separately consistent with the stationarity assumption.

1
2
augmented_dickey_fuller_test(train['unemp'].diff().dropna())
kpss_test(train['unemp'].diff().dropna())
ADF Statistic: -5.032319
ADF p-value: 0.000019
stationary - null hypothesis of a unit root can be rejected at a 5% significant level
KPSS Statistic: 0.048204
KPSS p-value: 0.100000
stationary - null hypothesis of stationary cannot be rejected at a 5% significant level
1
2
augmented_dickey_fuller_test(np.log(train['consumer_confidence']))
kpss_test(np.log(train['consumer_confidence']))
ADF Statistic: -3.450606
ADF p-value: 0.009350
stationary - null hypothesis of a unit root can be rejected at a 5% significant level
KPSS Statistic: 0.134007
KPSS p-value: 0.100000
stationary - null hypothesis of stationary cannot be rejected at a 5% significant level

Availability of Credit

Evidently, the availability and cost of credit will also affect decisions of households to consume. So the behavior of monetary aggregates in terms of the availability of credit and the interest rates in terms of the cost of credit will matter for our projections for private consumption.

(Honestly, it would be ridiculous to ignore the crazy amount of money that the fed has been pumping into the economy since 2008 and its consequential impact. The hopeless belief in the Keynesian’s monetary policy.)

1
2
3
4
5
f, (ax1, ax2) = plt.subplots(1, 2,figsize=(14,4))
ax1.plot(train['m2'])
ax2.plot(np.log(train['m2']).diff())
ax1.set_title('real m2')
ax2.set_title('diff real m2')
1
2
3
4
5
f, (ax1, ax2) = plt.subplots(1, 2,figsize=(14,4))
ax1.plot(train['dff'])
ax2.plot(train['dff'].diff())
ax1.set_title('Federal Funds Effective Rate')
ax2.set_title('Diff Federal Funds Effective Rate')
1
2
augmented_dickey_fuller_test(np.log(train['m2']).diff().dropna())
kpss_test(np.log(train['m2']).diff().dropna())
ADF Statistic: -5.040178
ADF p-value: 0.000018
stationary - null hypothesis of a unit root can be rejected at a 5% significant level
KPSS Statistic: 0.172896
KPSS p-value: 0.100000
stationary - null hypothesis of stationary cannot be rejected at a 5% significant level

Let’s adding these new variables to the dataframe.

1
2
3
4
5
6
7
df['d_unemp'] = df['unemp'].diff()
df['ln_consumer_confidence'] = np.log(df['consumer_confidence'])
df['d_ln_m2'] = np.log(df['m2']).diff()
df['d_dff'] = df['dff'].diff()
df['spread_10y3m'] = df['yield_10y']-df['yield_3m']

train,test = df[1:cut], df[cut:]

Model Estimation

Based on all previous analysis, we can eventually run a VECM. Based on the long-run equation, we specify the form of the model with three endogenous variables log(rc), log(rdy) and log(rnw), and allow for five additional exogenous variables.

The lag intervals for the differenced endogenous variables are 2 given that we estimated the VAR with 3 lags in levels.

1
2
endog = ['ln_rc','ln_rdy','ln_rnw']
exog = ['sb_1975_4','sb_2008_3','d_unemp','ln_consumer_confidence', 'd_ln_m2', 'd_dff']
1
2
3
4
vec = vecm.VECM(endog = train[endog],
exog =train[exog],
k_ar_diff = 2, coint_rank = 1, deterministic = "co", missing= 'drop')
vecm_fit = vec.fit()

The coefficients in the long-run equation are both statistically significantly different from zero (see the first three tables below), and most importantly, consistent with our economic priors i.e., income and wealth have positive coefficients on consumption (see parameters for ln_rdy and ln_rnw in the first table).

For the exogenous variables, the negative coefficient on the structural breaks indicates that the increasing inequality may compress the consumption of households. Positive and significant coefficient on real m2 complies with our assumption that expanding monetary policy through quantitative ease may stimulate consumption in the short term.

Also, the coefficient on the ECM term is negative and significant with respect to real consumption, and significant and positive with respect to disposable income, both using a 5% level of significance, which are reasonable since both consumption and income are endogenous variables and dependent on each other, a rational household would react to disequilibrium in consumption relative to its long-run path though simultaneous adjustments - lower their spending (negative coefficient) and increase their income (positive coefficient).

From the “cointegration relations” table, we can verify our idea that in the long run, amount that one’s consumption and wealth accumulated would be equal to the income one earns.

1
vecm_fit.summary().as_latex
<bound method Summary.as_latex of <class 'statsmodels.iolib.summary.Summary'>
"""
Det. terms outside the coint. relation & lagged endog. parameters for equation ln_rc
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const         -0.6625      0.154     -4.310      0.000      -0.964      -0.361
exog1         -0.0027      0.001     -3.309      0.001      -0.004      -0.001
exog2         -0.0052      0.001     -4.940      0.000      -0.007      -0.003
exog3         -0.0069      0.001     -4.782      0.000      -0.010      -0.004
exog4          0.1386      0.032      4.279      0.000       0.075       0.202
exog5          0.1697      0.033      5.189      0.000       0.106       0.234
exog6          0.0006      0.000      1.463      0.144      -0.000       0.001
L1.ln_rc      -0.2099      0.068     -3.108      0.002      -0.342      -0.078
L1.ln_rdy      0.0742      0.043      1.739      0.082      -0.009       0.158
L1.ln_rnw      0.0485      0.022      2.248      0.025       0.006       0.091
L2.ln_rc      -0.0623      0.063     -0.986      0.324      -0.186       0.062
L2.ln_rdy      0.0077      0.042      0.184      0.854      -0.074       0.090
L2.ln_rnw      0.0039      0.022      0.176      0.860      -0.039       0.047
Det. terms outside the coint. relation & lagged endog. parameters for equation ln_rdy
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const         -0.2331      0.251     -0.928      0.353      -0.725       0.259
exog1         -0.0025      0.001     -1.879      0.060      -0.005       0.000
exog2         -0.0031      0.002     -1.777      0.076      -0.006       0.000
exog3         -0.0069      0.002     -2.937      0.003      -0.011      -0.002
exog4          0.0640      0.053      1.210      0.226      -0.040       0.168
exog5          0.1607      0.053      3.007      0.003       0.056       0.265
exog6         -0.0003      0.001     -0.445      0.656      -0.002       0.001
L1.ln_rc       0.1241      0.110      1.124      0.261      -0.092       0.340
L1.ln_rdy     -0.1826      0.070     -2.620      0.009      -0.319      -0.046
L1.ln_rnw      0.0519      0.035      1.469      0.142      -0.017       0.121
L2.ln_rc      -0.1472      0.103     -1.426      0.154      -0.350       0.055
L2.ln_rdy      0.0505      0.068      0.738      0.460      -0.084       0.184
L2.ln_rnw     -0.0868      0.036     -2.421      0.015      -0.157      -0.017
Det. terms outside the coint. relation & lagged endog. parameters for equation ln_rnw
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
const         -1.4968      0.494     -3.028      0.002      -2.466      -0.528
exog1          0.0003      0.003      0.097      0.923      -0.005       0.005
exog2         -0.0009      0.003     -0.260      0.795      -0.007       0.006
exog3         -0.0024      0.005     -0.514      0.607      -0.011       0.007
exog4          0.3093      0.104      2.968      0.003       0.105       0.513
exog5          0.1839      0.105      1.748      0.080      -0.022       0.390
exog6         -0.0021      0.001     -1.576      0.115      -0.005       0.001
L1.ln_rc       0.1244      0.217      0.573      0.567      -0.301       0.550
L1.ln_rdy     -0.2364      0.137     -1.723      0.085      -0.505       0.033
L1.ln_rnw      0.0856      0.069      1.232      0.218      -0.051       0.222
L2.ln_rc       0.2163      0.203      1.065      0.287      -0.182       0.615
L2.ln_rdy     -0.2192      0.135     -1.629      0.103      -0.483       0.044
L2.ln_rnw      0.0711      0.071      1.008      0.314      -0.067       0.209
               Loading coefficients (alpha) for equation ln_rc                
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ec1           -0.0605      0.020     -2.993      0.003      -0.100      -0.021
               Loading coefficients (alpha) for equation ln_rdy              
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ec1            0.0887      0.033      2.684      0.007       0.024       0.153
               Loading coefficients (alpha) for equation ln_rnw              
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
ec1           -0.1382      0.065     -2.126      0.033      -0.266      -0.011
          Cointegration relations for loading-coefficients-column 1          
==============================================================================
                 coef    std err          z      P>|z|      [0.025      0.975]
------------------------------------------------------------------------------
beta.1         1.0000          0          0      0.000       1.000       1.000
beta.2        -1.1175      0.073    -15.311      0.000      -1.261      -0.974
beta.3         0.0555      0.058      0.963      0.336      -0.057       0.168
==============================================================================
""">

VECM Forecast

Out-of-sample Forecast

As we are comfortable with the model estimated, let’s do some forecasting with the out-of-sample data and evaluate how the model works.

To forecast for time beyond the sample period pre-2017, we need to provide futures shocks post-2017 i.e., the exogenous variables, which includes the dramatic economic downturn during the Covid recession. Let’s plot them out and get a feel.

1
2
3
4
5
6
7
8
f, (ax1, ax2, ax3) = plt.subplots(1,3,figsize=(18,4))
ax1.plot(test['d_unemp'])
ax2.plot(test['d_ln_m2'])
ax3.plot(test['d_dff'])
ax1.set_title('Diff Unemployment Rate')
ax2.set_title('Diff Real M2')
ax3.set_title('Diff Fed Fund Rate')
f.autofmt_xdate()

When forecasting, I include the confidence intervals for the predictions as well. Remember, point predictions won’t help you much with gauging the uncertainty of forecasts.

1
2
3
4
5
6
7
8
9
mid, lower_1se, upper_1se = vecm_fit.predict(steps=len(test),alpha = 0.32, exog_fc =test[exog])
_, lower_2se, upper_2se = vecm_fit.predict(steps=len(test),alpha = 0.05, exog_fc =test[exog])

pred_mid = pd.DataFrame(np.exp(mid), columns=['pred_rc','pred_rdy','pred_rnw'], index = test.index)
pred_lower_1se = pd.DataFrame(np.exp(lower_1se), columns=['lower_1se_rc','lower_1se_rdy','lower_1se_rnw'], index = test.index)
pred_upper_1se = pd.DataFrame(np.exp(upper_1se), columns=['upper_1se_rc','upper_1se_rdy','upper_1se_rnw'], index = test.index)
pred_lower_2se = pd.DataFrame(np.exp(lower_2se), columns=['lower_2se_rc','lower_2se_rdy','lower_2se_rnw'], index = test.index)
pred_upper_2se = pd.DataFrame(np.exp(upper_2se), columns=['upper_2se_rc','upper_2se_rdy','upper_2se_rnw'], index = test.index)
ptest = pd.concat([test, pred_mid, pred_lower_1se, pred_upper_1se, pred_lower_2se, pred_upper_2se],axis = 1)

The saving rate (percentage points) can be derived from total nominal consumption and
nominal household disposable income using the following formula:

$saving\ rate=100*(rdy - (rc + (government\ transfers + interest\ payments/gdp\ deflator))) /rdy$

1
ptest['pred_saving_rate']=100*(ptest.pred_rdy-(ptest.pred_rc+(ptest.gov_transfers+ptest.interest_payments/ptest.c_deflator)))/ptest.pred_rdy

Now let’s see how the model predicts,

1
2
3
4
5
6
7
8
9
10
11
12
13
def plot_point_prediction(test):
fig, axes = plt.subplots(2, 2, figsize=(12,8))
count=0
for endog in ['rc','rdy','rnw','saving_rate']:
col=count %2
row=count //2
ax=axes[row][col]
ax.set_title(endog)
ax.plot(test[endog], color='Black',linewidth=1, label="Observed")
ax.plot(test['pred_'+endog],color='grey',linestyle='--',linewidth=1, label="Forecast")
ax.legend()
count+=1
fig.autofmt_xdate()
1
plot_point_prediction(ptest)

The results are not so bad. Actually pretty dope pre-pandemic. For a model that is trying to predict for consumption three years out, its predictive accuracy is out-of-expectation.

Though not surprisingly, the forecasts are over predicting actual real disposable income, real net wealth and therefore real consumption expenditures. This reflects the I(1) nature of the variables involved, the limited serial correlation in their innovation sequences, and the fact that all variables were trending upwards before the pandemic. Because of their I(1) nature, a dynamic forecast will effectively use the last known value (four years ago) of these variables to generate out-of-sample forecasts. Consequently, the forecasting model does not do good job of predicting the impact of covid19 on real consumption, and its predictions for the saving ratio appear to be systematically below the actual saving outcome.

1
2
3
4
5
6
7
8
9
10
11
12
13
def plot_forecast_with_confidence_intervals(df,test):
fig, axes = plt.subplots(3, 1, figsize=(10,10))
count=0
for endog in ['rc','rdy','rnw']:
col=count
ax=axes[col]
ax.set_title(endog)
ax.plot(df[endog], color='Black',linewidth=1.2,label="Observed")
ax.plot(test['pred_'+endog],'k--',linewidth=0.7,label="Forecast")
ax.fill_between(test['pred_'+endog].index,test['lower_1se_'+endog],test['upper_1se_'+endog],alpha=0.07,color="k")
ax.fill_between(test['pred_'+endog].index,test['lower_2se_'+endog],test['upper_2se_'+endog],alpha=0.07,color="k")
ax.legend(loc ='upper left')
count+=1

With that being said, the previous assessment considers only a single point prediction using the baseline model. A fairer assessment of the model can be made considering the confidential intervals for prediction, which suggest that the underlying model is useful for predicting the outcome of the consumption and net wealth.

1
plot_forecast_with_confidence_intervals(df.iloc[200:],ptest)

While there is obvious (downward) bias in the model’s predictions, it should be noted that this partly a reflection of the initial starting point of the forecast. If one begins the forecast after the pandemic, the resulting confidential intervals would be much closer to what observed. In this way, a static forecast which is one-period ahead forecasting would do a much better job. This is also how the model should be used in practice. As always, predicting future is difficult, trying to predict mutiple years out is meaningless.

Long-run Forecasts Based on Potential Paths of Monetary Policy

Now it has come to a very interesting point. Let’s fit a VECM with the complete data from 1962 to 2021Q2, and use it to forecast for 2021Q3 onwards using two simulated forward paths of the Fed’s monetary policy. Correct assumptions would help us to get more accurate estimates for the long-run consumption, disposal income and net wealth of the American households after the pandemic.

1
2
3
4
5
6
endog = ['ln_rc','ln_rdy','ln_rnw']
exog = ['sb_1975_4','sb_2008_3','d_unemp','ln_consumer_confidence', 'd_ln_m2','d_dff']
vec = vecm.VECM(endog = df[endog],
exog =df[exog],
k_ar_diff = 2, coint_rank = 1, deterministic = "co", missing= 'drop')
vecm_fit = vec.fit()

Let’s generate two paths for the forward monetary shocks. One path assumes that fed keeps its current pacing of assets purchasing i.e., no tapering, the other path assumes four interest rate hikes in 2022, 2023 and 2024 assuming the same pace as the period after the financial crisis from 2014Q4 to 2017Q3, with all the other exogenous variables being the same, specifically, current value for unemployment rate and the historical average for consumer confidence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
forecast_range = pd.date_range(start='04/01/2021', end ='01/01/2024',freq='QS' )
shocks = {}
shocks = pd.DataFrame(shocks, index = forecast_range, columns = exog)
shocks['sb_1975_4'] = 1
shocks['sb_2008_3'] = 1
shocks['d_unemp'] = 0
shocks['ln_consumer_confidence'] = df['ln_consumer_confidence'].mean()

no_hikes_shocks = shocks.copy()
no_hikes_shocks['d_ln_m2'] = df.iloc[-4:]['d_ln_m2'].mean()
no_hikes_shocks['d_dff'] = 0.0

four_hikes_shocks =shocks.copy()
four_hikes_shocks[['d_ln_m2','d_dff']] = df.loc['2014-10-01':'2017-07-01',['d_ln_m2','d_dff']].values

Here’s how the money aggregates is going to be like if assuming no tapering until 2024, which would be of course totally insane…

1
2
# change of log(real m2) and change of fed fund rate
no_hikes_shocks[['d_ln_m2','d_dff']]

d_ln_m2 d_dff
2021-04-01 0.02268 0.0
2021-07-01 0.02268 0.0
2021-10-01 0.02268 0.0
2022-01-01 0.02268 0.0
2022-04-01 0.02268 0.0
2022-07-01 0.02268 0.0
2022-10-01 0.02268 0.0
2023-01-01 0.02268 0.0
2023-04-01 0.02268 0.0
2023-07-01 0.02268 0.0
2023-10-01 0.02268 0.0
2024-01-01 0.02268 0.0

Here’s a more likely path assumes that the fed would stop expanding its balance sheet from the forth quarter of 2021 and hike once in 2022 and three times at the end 2023 and beginning of 2024. This path is quite aggressive.

(By the way, when central banks expend their balance sheets i.e., increase their net asset position, they literally buy assets or reduce their liability through money creation, which means writing checks to themselves.)

1
2
3
# hike schedule - historical simulation using 2014-2017 data
## change of log(real m2) and change of fed fund rate
four_hikes_shocks[['d_ln_m2','d_dff']]

d_ln_m2 d_dff
2021-04-01 0.015029 0.01
2021-07-01 0.025704 0.01
2021-10-01 0.003989 0.02
2022-01-01 0.007853 0.00
2022-04-01 0.013799 0.03
2022-07-01 0.022938 0.20
2022-10-01 0.009184 0.01
2023-01-01 0.011144 0.02
2023-04-01 0.009073 0.06
2023-07-01 0.008134 0.25
2023-10-01 0.010906 0.25
2024-01-01 0.005544 0.20
1
2
3
4
5
6
7
8
9
10
11
12
mid_no_hikes = vecm_fit.predict(steps=12,exog_fc =no_hikes_shocks[exog])
pred_mid_no_hikes = pd.DataFrame(np.exp(mid_no_hikes), columns=['pred_rc','pred_rdy','pred_rnw'], index = forecast_range)

mid, lower_1se, upper_1se = vecm_fit.predict(steps=12,alpha = 0.32, exog_fc =four_hikes_shocks[exog])
_, lower_2se, upper_2se = vecm_fit.predict(steps=12,alpha = 0.05, exog_fc =four_hikes_shocks[exog])

pred_mid = pd.DataFrame(np.exp(mid), columns=['pred_rc','pred_rdy','pred_rnw'], index = forecast_range)
pred_lower_1se = pd.DataFrame(np.exp(lower_1se), columns=['lower_1se_rc','lower_1se_rdy','lower_1se_rnw'], index = forecast_range)
pred_upper_1se = pd.DataFrame(np.exp(upper_1se), columns=['upper_1se_rc','upper_1se_rdy','upper_1se_rnw'], index = forecast_range)
pred_lower_2se = pd.DataFrame(np.exp(lower_2se), columns=['lower_2se_rc','lower_2se_rdy','lower_2se_rnw'], index = forecast_range)
pred_upper_2se = pd.DataFrame(np.exp(upper_2se), columns=['upper_2se_rc','upper_2se_rdy','upper_2se_rnw'], index = forecast_range)
pred = pd.concat([pred_mid, pred_lower_1se, pred_upper_1se, pred_lower_2se, pred_upper_2se],axis = 1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def plot_forecasts_given_different_paths(df,pred1,pred2):
fig, axes = plt.subplots(3, 1, figsize=(10,10))
count=0
for endog in ['rc','rdy','rnw']:
col=count
ax=axes[col]
ax.set_title(endog)
ax.plot(df[endog], color='Black',linewidth=1.2,label="Observed")
ax.plot(pred1['pred_'+endog],'--',color='tab:blue', linewidth=1.5,label="Forecast - no hikes")
ax.plot(pred2['pred_'+endog],'--',color='tab:red', linewidth=1.5,label="Forecast - four hikes")
ax.fill_between(pred2['pred_'+endog].index,pred2['lower_1se_'+endog],pred2['upper_1se_'+endog],alpha=0.07,color="tab:red")
ax.fill_between(pred2['pred_'+endog].index,pred2['lower_2se_'+endog],pred2['upper_2se_'+endog],alpha=0.07,color="tab:red")
ax.legend(loc ='upper left')
ax.axvline(x=datetime(2022, 7, 1), color='tab:red')
ax.axvline(x=datetime(2023, 7, 1), color='tab:red')
ax.axvline(x=datetime(2023, 10, 1), color='tab:red')
ax.axvline(x=datetime(2024, 1, 1), color='tab:red')
count+=1

In March 2020, in response to the economic impact of the pandemic, the Fed acted quickly having learned its lesson from the financial crisis and cut short-term interest rates to zero. It also restarted its large-scale asset purchases and since June 2020, the Fed has been buying 80 billion of Treasury securities and 40 billion of mortgage-backed securities each month, with its balance sheet swelling from 4.4 trillion to 8.6 trillion. The extreme monetary support combined with a huge fiscal stimulus package holds the U.S economy and shields American household incomes. Though plots won’t be shown here, the model forecasts a much lower rdy and rnw if not taking into account the rapid increase of money base. Therefore, in the last 20 months, we all see the quick recovery of consumption and a rapid growth of asset prices, which is also reflected in the steeper slope of the third graph. However, as the economy rebounded, it may no longer need such extreme measures of support and keeping them in place could do more harm than good. For example, low mortgage rates have fueled a boom in house prices, but the problems that now afflict the economy are mostly supply issues while demand, which the bond buys most directly affects. Therefore, the policy makers may start to hit the brakes. That’s also why we are seeing Fed officials talking about tapering the pace of its bond purchases and lately laying the groundwork for higher rates.

1. Growth rates return to normal

First it’s not surprising that real consumption, disposable income and household net wealth are projected to grow at milder rates comparing to those when the economy was bottoming out from the slump. As the stimulus aids wear off over time, one would expect the growth rates to normalize to pre-pandemic levels.

2. Growth differences taking expansionary and tight monetary policy

Secondly, assuming a tighter monetary policy to come, we project that the growth rate of real consumption throughout 2023 would slow down to 2.2% from 3.0%. The real income and net wealth of households would decrease from 3.9% and 9.1% to 2.4% and 6.5% respectively. At the end of 2023, all three measures would be lower than those come from the first path - that the Fed sticks to its current rate target and asset buying pace. In fact the amounts would be lowered by 1.8%, 3.3% and 4.8% respectively.

3. Foreseeable shallow decline in mid 2022

Thirdly, we also notice that in the middle of 2022, the private consumption and household income would show a temporary and shallow decline and then grow at an annual rate again coming close to the path observed just before the pandemic. Likely this is caused by the tightening of policy in 2022. So far, Powell has announced that the fed will begin tapering this month, the first step toward pulling back on the massive amount of help it had been providing markets and the economy. One the fiscal side, though public programs to support households and businesses have been the most powerful engine of recovery from the hit, governments of the major economies now are hitting the brakes. The money they’ ll pull out of their economies in 2022 amounts to 2.5 percentage points of the world’s GDP, five times bigger than anything that happened during the turn to austerity after the 2008 crisis, according to UBS estimates. The fiscal tightening - the withdrawal of pandemic spending will likely have more impact on the global economy next year.

1
plot_forecasts_given_different_paths(df.iloc[220:],pred_mid_no_hikes,pred)
A Practical Walkthrough of Using Alternative Data to Evaluate Opportunities in Recession-Resilient Niche Sectors Predicting Post-Pandemic Consumption Behaviors Given Potential Paths of Monetary Policy, Part 1 -- Economic Theories, Structural Breaks and Stationarity
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×