Skip to main content

Understanding MPSGE: Interpreting solutions

Mitch Phillipson July 18, 2025


The discussion during the MPSGE monthly this week got me thinking about an interesting example to demonstrate what the solutions from MPSGE mean. I thought it would be useful to share this example here, as it is a common question that comes up when working with MPSGE.

Model Setup

The model is a simple two sector, two good, closed economy with some intermediate inputs. The model diagram is given below.

The model has two sectors, X and Y, with the W sector acting as a composite to represent the Hicksian welfare index. I should note that W is not a “real” sector, it is in the model to simplify the demand structure.

If you are unfamiliar with the tree structure above, it’s a more specific method to represent a SAM. A standard SAM lacks information about taxes and the nesting structure. While you can represent the same information in a standard SAM, the matrix becomes large and sparse which makes it difficult to interpret. The tree structure allows us to represent the same information in a more compact form.

Let’s break down the X sector to ensure we understand the structure. The large X in the middle of the tree represents the sector, anything above is an output and below is an input, you may have surmised as much from the arrows. Directly above X we see t=0, this is saying the elasticity of transformation on the outputs is 0. Of course, there is only one output PX with a quantity of 170, making the elasticity of transformation irrelevant.

The input side is more interesting. The top nest is Cobb-Douglas, we can see this with the s=1. The PX input has a quantity of 20. The RATRA\Rightarrow T denotes a tax on the input with a rate of T and going to RA. We also have a nested CES structure called va with an elasticity of .5.

Finally, we have one consumer, RA. For consumers, the outputs represent endowments and the inputs represent final demands.

We will take PL to be the numeraire.

Model Balance

This model is not balanced. I just made up the numbers as I went. What does it mean to be balanced and how can we tell this is unbalanced?

The W sector is balanced, the sum of the inputs, 100+100100+100, is the same as the sum of the outputs, 200200. The X sector is not balanced, the outputs sum to 170 and the inputs sum to 100. This is called the zero profit condition, the sum of the inputs must equal the sum of the outputs. This can be tricker to determine visually if there are taxes, we may do an example in the future to demonstrate this.

Related to the zero profit condition is the income balance condition. This refers to consumers, the sum of the endowments must equal the sum of the final demands. Again, this is more complicated with taxes, but in this case the endowment is 200 and the final demand is 200, so it is balanced.

Finally, there is the market clearance condition. Every commodity must sum to zero, with inputs being negative and outputs being positive. Consider PX, this has a total output of 170 and a total input of 20+50+100=170, which means for PX the market is balanced. Sometimes we get lucky with numbers. PY is not balanced, the outputs sum to 125 and the inputs sum to 165.

Implementing in Julia

Here is a link to my Julia Code. The file model.jl contains the MPSGE model, a function to get the new equilibrium quantities, and a function to report the results. The model is written so that we can easily update the start values of the variables, this will be important.

The file main.jl contains the code to run the models and generate the results. The rest of this document is discussing the results of this file. I written comments in the code so you should be able to follow along.

I think it’ll be useful to discuss the new_quantity function. Find the definition of this function in model.jl, you’ll see there are three version:

new_quantity(M::MPSGEModel, X::Symbol, P::Symbol, n::Symbol)
new_quantity(X::Sector, P::Commodity, n::Symbol)
new_quantity(RA::Consumer, P::Commodity, n::Symbol)

Julia uses multiple dispatch, which means that the function can have different implementations based on the types of the arguments. The first version is a convenience so that we can use symbols to represent the objects in the model. This function calls new_quantity(M[X], M[P], n). Julia type-checks the first argument and then calls the appropriate version of the function.

At the MPSGE Monthly, I discussed how the new_quantity function works for sectors. To summarize,

compensated_demand(X, P, n)*X*P

represents the total value of commodity P in sector X under nest n.

For consumers, the function is similar, but using the endowments and demands instead of the inputs and outputs.

What are we doing?

It’s finally time to talk about what we are actually doing. We are going to create an unbalanced model M and find the equilibrium. We use this solution to create two new models, L and N. The model L will have quantities extracted from M using the new_quantity function. The model N has the same quantities as M, but the starting values will be set to the values from M.

Both L and N will be balanced models, but in slightly different ways. L will be balanced at the standard benchmark solution, which means you could add up the values and verify balance. You can verify this by inspecting the X production block, production(L[:X]). Add up the inputs and it will be the same as the outputs. N will appear balanced when solving with cumulative_iteration_limit=0, but the prices will not be unital. This is expected, we are using the same quantities as M, but the prices are not set to 1.

At this point you should read some code and understand how M, L, and N are created and solved. Remember, Julia is interactive. If there is a portion that you don’t understand, try to rerun it, break it down, extract small examples, and play with it. You probably won’t break anything. In particular, look at new_inputs and new_outputs, this is kind of key.

Below are the equilibrium solutions before any applied taxes, taken from the pre_tax dataframe.

var M N L
PY 0.589443 0.589443 1.0
PK 1.1916 1.1916 1.0
PL 1.0 1.0 1.0
PX 0.387055 0.387055 1.0
PW 0.488249 0.488249 1.0
RA 219.16 219.16 219.16
W 2.24434 2.24434 1.0
X 4.22812 4.22812 1.0
Y 4.23592 4.23592 1.0

This should not be surprising. M and N have the same values because we set them to be the same and L is balanced at the benchmark solution, so all prices are set to 1.

More interesting is what happens when we set the value of the tax rate to be 20% and solve the models again.

var M N L
PY 0.608383 0.608383 1.03213
PK 1.18811 1.18811 0.997069
PL 1.0 1.0 1.0
PX 0.412881 0.412881 1.06672
PW 0.510632 0.510632 1.04584
RA 227.957 227.957 227.957
W 2.23211 2.23211 0.994547
X 3.90919 3.90919 0.92457
Y 4.12817 4.12817 0.974563

It appears that L has a different solution than M and N. Remember, these are all the same model with the same tax, so the solutions should be consistent.

So what is going on? Everything in MPSGE is relative. In this case, the solutions are relative to the initial equilibrium solution. For L this is very easy to interpret because the benchmark values are 1 so the price of PY shows a 3.2% increase (again, relative to the numeraire, PL). That same 3.2% increase can be seen in N as well, take the value of PY in after the counterfactual and divide by the value of PY in the benchmark, 0.608383/0.589443 = 1.03213.

The dataframe comparison computes these values for each variable. And you can see that all the values are now equal.

var M N L
PY 1.03213 1.03213 1.03213
PK 0.997069 0.997069 0.997069
PL 1.0 1.0 1.0
PX 1.06672 1.06672 1.06672
PW 1.04584 1.04584 1.04584
RA 1.04014 1.04014 1.04014
W 0.994547 0.994547 0.994547
X 0.92457 0.92457 0.92457
Y 0.974563 0.974563 0.974563

Conclusion

My intention is to show that MPSGE values are relative and it is very important to understand the context of the model when interpreting the results. I also wanted to demonstrate how flexible the MPSGE.jl syntax is for both creating models, but also for extracting information.