Fiscal multipliers
Practicum · Session 06
Before starting
This practicum examines the fiscal-news application in Session 06. The question is how output responds to military-spending news, and how that response relates to the purchases associated with the news. We will work with the existing replication, without changing its lag order or sample.
The lecture notes give the model and covariance formulas. Read through the cumulative-IV section before beginning. Basic familiarity with MATLAB arrays and tables is enough; each command below explains the object it creates or checks.
Download the replication package linked above, or clone the course repository. Obtain the workbook following data/README.md inside the package. The raw workbook is not bundled. The importer checks its fingerprint, so a different vintage should not be renamed to make it pass. The instructions and benchmark results refer to MATLAB R2024a.
Set MATLAB’s Current Folder to the directory containing run_session.m. Run the following blocks in order, in the same MATLAB session. They inspect the returned estimates and do not edit the source data. The initial replication writes its own result files under outputs/06-local-projections/; rerunning replaces those generated files. If an assertion fails, stop and examine its message before continuing.
Run the replication
% Use the repository selected as MATLAB's Current Folder.
projectRoot = pwd;
% Check that this folder contains the course replication entrypoint.
assert(isfile(fullfile(projectRoot, 'run_session.m')));
% Fit Session 06 and retain every model, sample, and covariance in memory.
result = run_session(6);
% Run estimator and data-dependent tests; an unsuccessful test raises an error.
testResults = run_tests();
% Show the exact workbook fingerprint recorded in this run.
disp(result.inputSHA256);The result structure contains more than the plotted response. In result.linear, rows correspond to horizons and columns to outcomes: column 1 is output and column 2 is government purchases. MATLAB arrays start at one, but the economic horizon starts at zero. Thus {1, 1} is the output regression at impact, not one quarter ahead. The code below finds horizons by their stored values rather than relying on that offset.
The preferred run gives 500 observations at impact and 480 at horizon twenty. A run that silently replaces missing initial news with zero would not have the same sample. The separate compatibility audit deliberately reproduces that classroom convention; it is not the specification used here.
Check the estimation calendar
First inspect the exported coefficient table. Keep only the news term in the linear output LP, so that each displayed row represents one regression.
% Retain the long-form table already produced by the replication.
coefficients = result.tables.coefficients;
% Select the linear output response, excluding controls and other models.
isOutputLP = strcmp(coefficients.model, 'linear_lp') & ...
strcmp(coefficients.outcome, 'output');
% Select the named news coefficient rather than a numerical column position.
isNews = strcmp(coefficients.term, 'news');
% Examine impact, one year ahead, and five years ahead.
isCheckpoint = ismember(coefficients.horizon, [0, 4, 20]);
% Keep one coefficient row for each of those three regressions.
sampleCheck = coefficients(isOutputLP & isNews & isCheckpoint, :);
% Display origin dates, observation counts, and maximum included HAC lags.
disp(sampleCheck(:, {'horizon', 'n', 'first_quarter', ...
'last_quarter', 'hac_lag'}));
% Compare the three counts with the inclusive-quarter calculation in Exercise 1.
assert(isequal(sampleCheck.n, [500; 496; 480]));
% Confirm the classroom bandwidth rule L = h + 1 in the stored results.
assert(isequal(sampleCheck.hac_lag, sampleCheck.horizon + 1));The output should read as follows:
| Horizon | Observations | First origin | Last origin | HAC lag |
|---|---|---|---|---|
| 0 | 500 | 1891 Q1 | 2015 Q4 | 1 |
| 4 | 496 | 1891 Q1 | 2014 Q4 | 5 |
| 20 | 480 | 1891 Q1 | 2010 Q4 | 21 |
For horizon four, the last origin is 2014 Q4 because the outcome is observed in 2015 Q4. The last_quarter field records the origin, not the outcome date. The window 1889 Q4-2015 Q4 contains 505 quarters, but four lags and the missing first news observation prevent its first five quarters from being usable origins. At horizon four, four further origins are lost at the end. Hence 505-5-4=496.
Now inspect one actual design row. The stored period code is 4\times\text{year}+\text{quarter}-1, so subtracting one means one calendar quarter, not one row of the workbook.
% Work at a horizon of four quarters throughout the remaining examples.
horizon = 4;
% Find the array row associated with the economic horizon h = 4.
horizonIndex = find(result.config.horizons == horizon, 1);
% Read the output LP fit at that horizon, including its exact origin dates.
outputFit = result.linear{horizonIndex, 1};
% Take the first retained origin, 1891 Q1, for a concrete timing check.
origin = outputFit.periods(1);
% Find that calendar date in the transformed data, not in the trimmed fit.
[originFound, originRow] = ismember(origin, result.data.period);
% Refuse to inspect a row unless the origin actually exists in the input.
assert(originFound);
% List the four lag dates, current date, and horizon-four outcome date.
requiredDates = [origin - 4:origin - 1, origin, origin + horizon]';
% Convert integer quarter codes into readable labels, preserving their order.
quarterLabels = string(floor(requiredDates / 4)) + "Q" + ...
string(mod(requiredDates, 4) + 1);
% Identify the role of each listed date in this one regression observation.
role = ["Lag 4"; "Lag 3"; "Lag 2"; "Lag 1"; "Origin"; "Outcome"];
% Show the timing map before looking at numerical regressors.
disp(table(role, quarterLabels));
% Label all 14 design columns using the names stored beside the matrix.
designRow = array2table(result.design.linear(originRow, :), ...
'VariableNames', cellstr(result.design.linearNames));
% Inspect the intercept, news, and first lag block of this observation.
disp(designRow(:, 1:5));The lag dates are 1890 Q1 through 1890 Q4; the outcome is 1892 Q1. The design itself orders lag blocks from the most recent to the oldest: L1_news, L1_output, L1_spending, then the corresponding second lags, and so on. The first five columns therefore contain the constant, current news, and the three values from 1890 Q4. They must not contain the future 1892 Q1 outcome. This is a check on the regression’s information set, not just its matrix dimensions.
Read the multiplier table
The multiplier table keeps the cumulative-IV estimate separate from the ratio of summed LP responses. It also reports two IV standard errors so the inference correction remains visible.
% Read the horizon-level multiplier table generated from the fitted objects.
multipliers = result.tables.multipliers;
% Select the cumulative horizon that ends four quarters after the news.
atFour = multipliers(multipliers.horizon == horizon, :);
% Display the multiplier, the inference comparison, and first-stage diagnostics.
disp(atFour);
% Obtain the IV model on exactly the sample used for this table row.
ivFit = result.iv{horizonIndex};
% Apply the stored critical value to the unrounded structural standard error.
interval = ivFit.beta(1) + [-1, 1] * ...
result.config.criticalValue * ivFit.se(1);
% Display the pointwise lower and upper bounds, in output per unit of purchases.
disp(interval);The checked horizon-four values are approximately 0.680128 for the IV multiplier and 0.097806 for its structural HAC standard error. The fitted-regression standard error is approximately 0.168318. These are different covariance calculations for the same fitted coefficient, not two competing point estimates. Using the unrounded numbers gives a pointwise interval of approximately [0.4884,0.8718].
The first-stage HAC Wald statistic per restriction is approximately 12.201, and partial R^2 is approximately 0.1034. The latter says how much the excluded news instrument reduces the first-stage residual sum of squares, relative to using the controls alone. Neither number establishes exclusion or makes the interval weak-instrument robust. In particular, the reported Wald measure is not a Kleibergen–Paap statistic.
The ratio of summed LP responses is approximately 0.679914, slightly different from the cumulative-IV estimate. At each horizon the cumulative IV regression uses one common sample for its sums. The separate LP responses being added in the ratio come from different horizon samples. With the same controls, the same instrument, and an identical sample for every component, the just-identified algebra gives equality of these two calculations. The horizon-specific samples here do not impose that condition. See the lecture’s multiplier section for the distinction.
Reconstruct the structural residual
At horizon four, each cumulative outcome contains five quarters. We can reconstruct the actual regressors on the saved IV sample, then verify which residual belongs in the covariance. This does not estimate a new model; it checks the objects returned by the existing fit.
% Match every retained IV origin to its row in the transformed data.
[ivOriginsFound, ivRows] = ismember(ivFit.periods, result.data.period);
% Require a genuine date match for every origin in the fitted model.
assert(all(ivOriginsFound));
% Form all dates t through t+4 for each origin; rows are origins, columns leads.
cumulativeDates = ivFit.periods + (0:horizon);
% Look up each required date by its calendar code, not by its row offset.
[datesFound, cumulativeRows] = ismember(cumulativeDates, result.data.period);
% The complete IV sample must contain every quarter in each cumulative window.
assert(all(datesFound, 'all'));
% Select normalized output as a vector for the calendar-indexed lookup.
outputSeries = result.data.outcomes(:, 1);
% Select normalized government purchases in the same input-date order.
spendingSeries = result.data.outcomes(:, 2);
% Sum the five matched output levels separately for each origin.
cumulativeOutput = sum(outputSeries(cumulativeRows), 2);
% Sum purchases over exactly the same dates, without omitting missing values.
cumulativeSpending = sum(spendingSeries(cumulativeRows), 2);
% Recreate X with actual cumulative purchases first and the included controls.
structuralX = [cumulativeSpending, result.design.controls(ivRows, :)];
% Form the disturbance estimate in Y = X beta + e, using actual regressors.
structuralResidual = cumulativeOutput - structuralX * ivFit.beta;
% Compare every reconstructed residual with the estimator's saved residual.
assert(norm(structuralResidual - ivFit.residual, Inf) < 1e-10);
% Form the different residual from regression on first-stage fitted regressors.
fittedResidual = cumulativeOutput - ivFit.projectedX * ivFit.beta;
% Calculate the algebraic difference attributable to the projection of X.
projectionRemainder = (structuralX - ivFit.projectedX) * ivFit.beta;
% Check r - e = (X - projectedX) beta to numerical precision.
assert(norm(fittedResidual - structuralResidual - ...
projectionRemainder, Inf) < 1e-10);
% Check that the difference is not merely roundoff in this empirical regression.
assert(norm(fittedResidual - structuralResidual, Inf) > 1e-8);The first identity checks the sample alignment and the definition of the structural disturbance together. The second checks why the fitted-regressor residual is different. The 2SLS estimate uses the projection of purchases on the instruments; its structural equation still contains actual purchases.
In matlab/+tsma/+inference/iv_hac.m, the score for inference is the projected regressor multiplied by structuralResidual. Replacing the latter with fittedResidual changes the HAC meat. The bread, coefficient, and sample are unchanged. The corrected standard error need not always be smaller: at impact, this same table reports a structural error of about 0.5663 against a fitted-regression error of about 0.3560. The complete derivation is in worked Exercise 3.
Compare the initial states
The state LP estimates the high- and low-unemployment blocks jointly. Its state is determined by unemployment in the quarter before the news. We can verify that timing for our chosen origin and then form the high-minus-low coefficient contrast.
% Locate unemployment exactly one quarter before the chosen origin.
[lagFound, lagRow] = ismember(origin - 1, result.data.period);
% A missing lag would make this origin unsuitable for the state specification.
assert(lagFound);
% Apply the stored threshold to the observed pre-shock unemployment rate.
expectedHigh = double(result.data.unemployment(lagRow) >= ...
result.config.threshold);
% Confirm that the saved design uses this lagged classification.
assert(result.design.high(originRow) == expectedHigh);
% Retrieve the joint state LP for output at horizon four.
stateFit = result.state{horizonIndex, 1};
% Find the news coefficient in the high-unemployment block by its name.
highColumn = find(result.design.stateNames == "high_news", 1);
% Find the corresponding coefficient in the low-unemployment block.
lowColumn = find(result.design.stateNames == "low_news", 1);
% Start a column contrast with one entry per estimated state coefficient.
contrast = zeros(numel(stateFit.beta), 1);
% Add the high-state news coefficient to the desired difference.
contrast(highColumn) = 1;
% Subtract the low-state news coefficient, leaving all other weights at zero.
contrast(lowColumn) = -1;
% Compute the high-minus-low response difference, not either marginal response.
difference = contrast' * stateFit.beta;
% Use the full covariance, including its cross-state off-diagonal entries.
differenceVariance = contrast' * stateFit.covariance * contrast;
% Refuse a nonpositive variance before constructing a normal statistic.
assert(differenceVariance > 0);
% Express the uncertainty in the same units as the response difference.
differenceSE = sqrt(differenceVariance);
% Form a pointwise interval for the contrast under the maintained HAC assumptions.
differenceInterval = difference + [-1, 1] * ...
result.config.criticalValue * differenceSE;
% Report the contrast and both bounds without replacing the marginal estimates.
disp([difference, differenceSE, differenceInterval]);This calculation implements \widehat V_d=\widehat V_{HH}+\widehat V_{LL}-2\widehat V_{HL}. The cross-state term belongs in the formula even though each observation has only one initial state: HAC uses lagged score pairs, which can cross a state boundary. The numerical illustration in worked Exercise 5 shows why comparing two marginal intervals does not perform this test.
The contrast concerns output responses to news, not the state-specific cumulative-IV multipliers. Do not substitute result.stateIV without also changing the coefficient names and interpretation. Nor does conditioning on the initial state hold the future unemployment path fixed. At this single horizon the interval is pointwise; repeating the contrast at all horizons would raise a separate joint-inference question.
A replication note
An empirical result should travel with enough information to reconstruct its meaning. The following is a worked description of the checked horizon-four specification, not a claim that the historical instrument has been independently validated:
The cumulative-IV regression uses 496 origins from 1891 Q1 through 2014 Q4. For each origin it sums output and government purchases over five quarters, ending no later than 2015 Q4. Current military-spending news instruments cumulative purchases; the included controls are a constant and four lags each of news, output, and purchases. Preserving the missing initial news observation determines the first usable origin. The estimated multiplier is 0.6801, with a structural-residual Bartlett HAC standard error of 0.0978. The maximum autocovariance lag is five quarters, with no finite-sample multiplier. Its conventional 95 percent pointwise interval is approximately [0.4884, 0.8718]. The first-stage diagnostics describe relevance but do not establish exclusion or provide weak-instrument-robust coverage.
Keep the saved replication.mat and input fingerprint with the generated tables when documenting your run. A successful execution, a classroom comparison, and an identification argument answer different questions. The code checks the first two; the last still requires an economic argument about the news instrument.