EvalFnOnAgentDist_AggVars_Case1 and EvalFnOnAgentDist_AllStats_Case1 bug

I found that EvalFnOnAgentDist_AggVars_Case1 and EvalFnOnAgentDist_AllStats_Case1 give different results for the aggregate variables, i.e. field “Mean”. Please see minimum working example at the end of this post that reproduces the issue.

Suppose I want to calculate average income. I define

FnsToEvaluate.income = @(aprime,a,z,P) f_income(aprime,a,z,P);

Then I compute policy functions and stationary distributions.
Then I call the functions to compute averages and other moments.

AggVars=EvalFnOnAgentDist_AggVars_Case1(StationaryDist, Policy, FnsToEvaluate, Params, FnsToEvaluateParamNames, n_d, n_a, n_z, d_grid, a_grid, z_grid, simoptions);

AllStats=EvalFnOnAgentDist_AllStats_Case1(StationaryDist,Policy,FnsToEvaluate,Params,[],n_d,n_a,n_z,d_grid,a_grid,z_grid,simoptions);

The moment average income should be equal to

AggVars.income.Mean = 0.13
AllStats.income.Mean = NaN

The reason for this discrepancy is that the function for income, when evaluated on the grid, contains at least one NaN value. Now, this is not a problem in EvalFnOnAgentDist_AggVars_Case1 because this function computes the average by excluding NaN values, as it should be:

temp=Values.*StationaryDistVec;
AggVars(ff)=sum(temp(~isnan(temp)));

However, the function EvalFnOnAgentDist_AllStats_Case1 does not implement this check and therefore returns NaN as output for the mean of the variable. Indeed, in the subfunction stats from weighted grid we have:

% Eliminate all the zero-weights from these (trivial increase in runtime, but makes it easier to spot when there is no variance)
temp=logical(Weights==0);
Weights=Weights(~temp);
Values=Values(~temp);
% code omitted
WeightedSortedValues=SortedValues.*SortedWeights;
AllStats.Mean=sum(WeightedSortedValues);

I think this should be fixed by simply replacing the last line with

valid = ~isnan(WeightedSortedValues);
AllStats.Mean=sum(WeightedSortedValues(valid));

or something similar.

To provide some more context on why this happens in the code I am working on:
In the model I am working on this turned out to be very important. In some cases the income of entrepreneurs is negative because of a fixed operating cost. This gives NaN at some point. For some reason, these points do receive a positive mass in equilibrium, so they are not eliminated by StatsFromWeightedGrid.

There could be something wrong in terms of economics in my model of course. However, I think the functions EvalFnOnAgentDist_AggVars_Case1 and EvalFnOnAgentDist_AllStats_Case1 should return the same result for shared statistics (like the mean) so this is why I dared to claim there is a bug :slight_smile:

Minimum working example that shows the bug:

% Example based on Aiyagari (1994).
clear,clc,close all
addpath(genpath('C:\Users\aledi\Documents\GitHub\VFIToolkit-matlab'))
% These codes set up and solve the Aiyagari (1994) model for a given
% parametrization. After solving the model they then show how some of the
% vfitoolkit commands to easily calculate things like the Gini coefficient
% for income, and how to plot the distribution of asset holdings.
%
% VFI Toolkit automatically detects hardware (GPU? Number of CPUs?) and
% sets defaults accordingly. It will run without a GPU, but slowly. It is
% indended for use with GPU.

%% Set some basic variables

% VFI Toolkit thinks of there as being:
% k: an endogenous state variable (assets)
% z: an exogenous state variable (exogenous labor supply)

% Size of the grids
n_k=2^9;%2^9;
n_z=11; %21;

% Parameters
Params.beta=0.96; %Model period is one-sixth of a year
Params.alpha=0.36;
Params.delta=0.08;
Params.mu=3;
Params.sigma=0.2;
Params.rho=0.6;

%% Set up the exogenous shock process
% Create markov process for the exogenous labour productivity, l.
Tauchen_q=3; % Footnote 33 of Aiyagari(1993WP, pg 25) implicitly says that he uses q=3
[z_grid,pi_z]=discretizeAR1_Tauchen(0,Params.rho,sqrt((1-Params.rho^2)*Params.sigma^2),n_z,Tauchen_q);
% Note: sigma is standard deviations of s, input needs to be standard deviation of the innovations
% Because s is AR(1), the variance of the innovations is (1-rho^2)*sigma^2
% Note: Nowadays you would be better using Farmer-Toda than Tauchen, but leaving it here to match the original

[z_mean,z_variance,z_corr,~]=MarkovChainMoments(z_grid,pi_z);
z_grid=exp(z_grid);
% Get some info on the markov process
[Expectation_l,~,~,~]=MarkovChainMoments(z_grid,pi_z); %Since l is exogenous, this will be it's eqm value 
% Note: Aiyagari (1994) actually then normalizes l by dividing it by Expectation_l (so that the resulting process has expectation equal to 1)
z_grid=z_grid./Expectation_l;
[Expectation_l,~,~,~]=MarkovChainMoments(z_grid,pi_z);
% If you look at Expectation_l you will see it is now equal to 1
Params.Expectation_l=Expectation_l;

%% Grids

% In the absence of idiosyncratic risk, the steady state equilibrium is given by
r_ss=1/Params.beta-1;
K_ss=((r_ss+Params.delta)/Params.alpha)^(1/(Params.alpha-1)); %The steady state capital in the absence of aggregate uncertainty.

% Set grid for asset holdings
k_grid=10*K_ss*(linspace(0,1,n_k).^3)'; % linspace ^3 puts more points near zero, where the curvature of value and policy functions is higher and where model spends more time

% Bring model into the notational conventions used by the toolkit
d_grid=0; %There is no d variable
a_grid=k_grid;
% z_grid
% pi_z;

n_d=0;
n_a=n_k;
% n_z

% Create functions to be evaluated
FnsToEvaluate.K = @(aprime,a,z) a; %We just want the aggregate assets (which is this periods state)

% Now define the functions for the General Equilibrium conditions
    % Should be written as LHS of general eqm eqn minus RHS, so that the closer the value given by the function is to 
    % zero, the closer the general eqm condition is to holding.
GeneralEqmEqns.CapitalMarket = @(r,K,alpha,delta,Expectation_l) r-(alpha*(K^(alpha-1))*(Expectation_l^(1-alpha))-delta); %The requirement that the interest rate corresponds to the agg capital level
% Inputs can be any parameter, price, or aggregate of the FnsToEvaluate

fprintf('Grid sizes are: %i points for assets, and %i points for exogenous shock \n', n_a,n_z)

%%
DiscountFactorParamNames={'beta'};

ReturnFn=@(aprime, a, z, alpha,delta,mu,r) Aiyagari1994_ReturnFn(aprime, a, z,alpha,delta,mu,r);
% The first inputs must be: next period endogenous state, endogenous state, exogenous state. Followed by any parameters

%%

% Use the toolkit to find the equilibrium price index
GEPriceParamNames={'r'};
% Set initial value for interest rates (Aiyagari proves that with idiosyncratic
% uncertainty, the eqm interest rate is limited above by it's steady state value
% without idiosyncratic uncertainty, that is that r<r_ss).
Params.r=0.038;


%%
% Solve for the stationary general equilbirium
vfoptions=struct(); % Use default options for solving the value function (and policy fn)
vfoptions.howardsgreedy=0;
simoptions=struct(); % Use default options for solving for stationary distribution
heteroagentoptions.verbose=1; % verbose means that you want it to give you feedback on what is going on

%fprintf('Calculating price vector corresponding to the stationary general eqm \n')
%[p_eqm,~,GeneralEqmCondn]=HeteroAgentStationaryEqm_Case1(n_d, n_a, n_z, 0, pi_z, d_grid, a_grid, z_grid, ReturnFn, FnsToEvaluate, GeneralEqmEqns, Params, DiscountFactorParamNames, [], [], [], GEPriceParamNames,heteroagentoptions, simoptions, vfoptions);

%p_eqm % The equilibrium values of the GE prices
p_eqm.r = Params.r;

%% Now that we have the GE, let's calculate a bunch of related objects
%Params.r=p_eqm.r; % Put the equilibrium interest rate into Params so we can use it to calculate things based on equilibrium parameters

% Equilibrium wage
Params.w=(1-Params.alpha)*((Params.r+Params.delta)/Params.alpha)^(Params.alpha/(Params.alpha-1));

fprintf('Calculating various equilibrium objects \n')
[V,Policy]=ValueFnIter_Case1(n_d,n_a,n_z,d_grid,a_grid,z_grid, pi_z, ReturnFn, Params, DiscountFactorParamNames, [], vfoptions);
% V is value function
% Policy is policy function (but as an index of k_grid, not the actual values)

% PolicyValues=PolicyInd2Val_Case1(Policy,n_d,n_a,n_s,d_grid,a_grid,vfoptions); % This will give you the policy in terms of values rather than index
StationaryDist=StationaryDist_Case1(Policy,n_d,n_a,n_z,pi_z, simoptions);

FnsToEvaluate.income = @(aprime,a,z,r,w) r*a+w*z;

FnsToEvaluate.income = @(aprime,a,z,r,w) f_income(aprime,a,z,r,w);

ValuesOnGrid=EvalFnOnAgentDist_ValuesOnGrid_Case1(Policy,FnsToEvaluate,Params,[],n_d,n_a,n_z,d_grid,a_grid,z_grid,simoptions,[],StationaryDist);

incomeOnGrid = gather(ValuesOnGrid.income);
disp('Are there NaN values in IncomeOnGrid? 1=yes,0=no')
any(isnan(incomeOnGrid),"all")
sum(isnan(incomeOnGrid(:)))/numel(incomeOnGrid)

simoptions.whichstats = zeros(7,1);
simoptions.whichstats(1) = 1;

AggVars=EvalFnOnAgentDist_AggVars_Case1(StationaryDist, Policy, FnsToEvaluate,Params, [],n_d, n_a, n_z, d_grid, a_grid,z_grid,simoptions);
AllStats=EvalFnOnAgentDist_AllStats_Case1(StationaryDist, Policy, FnsToEvaluate,Params, [],n_d, n_a, n_z, d_grid, a_grid,z_grid,simoptions);

% AggVars contains the aggregate values of the 'FnsToEvaluate' (in this model aggregates are equal to the mean expectation over the agent distribution)
% Currently the only FnsToEvaluate is assets, so we get aggregate capital stock
AggVars.income.Mean
AllStats.income.Mean

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function income = f_income(aprime,a,z,r,w)

income_temp = r*a+w*z;

if z>1.6
    income = NaN;
else
    income = income_temp;
end


end

function F=Aiyagari1994_ReturnFn(aprime, a, z,alpha,delta,mu,r)
% The return function is essentially the combination of the utility
% function and the constraints.

F=-Inf;
w=(1-alpha)*((r+delta)/alpha)^(alpha/(alpha-1));
c=w*z+(1+r)*a-aprime; % Budget Constraint
% c=w l_t+(1+r)a_t-a_{t+1}
if c>0
    if mu==1
        F=log(c);
    else
        F=(c^(1-mu) -1)/(1-mu);
    end
end

end


Unrelated to the bug: if I run the VFI in the Aiyagari code with vfoptions.howardsgreedy=0;, it runs 5 times faster.
Same number of grid points and same parameters as in the vfi toolkit examples. May be worth it to revisit the default options :slight_smile: