Notes on the core loop of the ABM

Author

David O’Sullivan

Published

July 2, 2024

Date Changes
2024-07-28 Correction in equation summarising full calculation of profit. Added note to consider the nudge calculation’s possible bias favouring changes after bad years.
2024-07-05 Corrected relative change from a percentage to a proportion.
2024-07-05 Corrected relative change from a percentage to a proportion.
2024-07-04 Added detail on calculation of relative change noting that profit can’t be handled this way because it is \pm and relative change is meaningless for such variables.
2024-07-03 Added bar chart of farm income/costs estimates.
2024-07-02 Initial post.

These notes are intended to make it easier to discuss with colleagues if we are approaching the core decision loop in the agent-based model (ABM) for ‘Moving the Middle’ (MtM) in a coherent and reasonable way.

Baseline income equation for farms

The baseline profit Z of each hectare of a farm is given by

Z=Y-X

where Y is gross income, and X is total costs. These are in turn given by

\begin{array}{rcl} Y &=& pq \\ X &=& x+te \end{array}

where

  • p is the price obtained per unit,
  • q is the quantity produced per hectare,
  • x is the cost per hectare to operate,
  • e is the greenhouse gas emissions per hectare, and
  • t is the tax on each unit of emissions.

Each of these parameters may depend on the farm type F (one of Crops, Dairy, Forestry, or Sheep/Beef) and the land use capability (LUC), L, such that the full calculation of profit for each hectare of a farm is Z_{FL} = p_Fq_{FL} - x_{FL} - te_{FL} and total farm profit is obtaining by summing this quantity across all hectares of the farm (which may be of varying landuse capability L).

Parameter values

The parameter values are stored in a number of CSV files which are read in by the model at start up.

Prices

p_F and the greenhouse gas tax (i.e. carbon price), t, are in the prices.csv file:

Code
prices <- read.csv("data/market/prices.csv", 
                   header = T, row.names = 1) |> 
  slice(1:2) # only rows 1 & 2 relevant
prices |> 
  kable() |> 
  kable_styling("striped", full_width = F)
SNB Dairy Forest Crop
Price_Commodity 5 7.5 157 0.5
Price_GhG 25 25.0 25 25.0

Yields

Yields by farm type and landuse capability, q_{FL}, are in the commodity-yields.csv file:

Code
yields <- read.csv("data/market/commodity-yields.csv", 
                   header = T, row.names = 1)
yields |> 
  kable() |> 
  kable_styling("striped", full_width = F) |>
  scroll_box(height = "250px")
SNB Dairy Forest Crop
LUC1_Mean 768.505660 1503.0000 30.0 9667.000
LUC1_SD 153.701132 300.6000 6.0 1933.400
LUC2_Mean 581.247332 1283.0000 29.0 9183.650
LUC2_SD 116.249466 256.6000 5.8 1836.730
LUC3_Mean 372.371287 1174.3000 28.0 8724.468
LUC3_SD 74.474257 234.8600 5.6 1744.893
LUC4_Mean 332.178378 923.3900 27.0 8288.244
LUC4_SD 66.435676 184.6780 5.4 1657.649
LUC5_Mean 259.480336 877.7308 26.0 7873.832
LUC5_SD 51.896067 175.5462 5.2 1574.766
LUC6_Mean 207.386971 845.7812 25.0 7480.140
LUC6_SD 41.477394 169.1562 5.0 1496.028
LUC7_Mean 193.188428 700.0000 24.0 7106.133
LUC7_SD 38.637686 140.0000 4.8 1421.227
LUC8_Mean 49.945208 661.0000 23.0 6750.827
LUC8_SD 9.989042 132.2000 4.6 1350.165

Costs

Base costs per ha. by farm type and landuse capability, x_{FL}, are in the input-costs.csv file:

Code
costs <- read.csv("data/market/input-costs.csv", 
                   header = T, row.names = 1)
costs |> 
  kable() |> 
  kable_styling("striped", full_width = F) |>
  scroll_box(height = "250px")
SNB Dairy Forest Crop
LUC1_Mean 3500 9500 4000 3000
LUC1_SD 700 1900 800 600
LUC2_Mean 2650 8050 4000 2850
LUC2_SD 530 1610 800 570
LUC3_Mean 1625 7350 3850 2675
LUC3_SD 325 1470 770 535
LUC4_Mean 1450 5500 3700 2500
LUC4_SD 290 1100 740 500
LUC5_Mean 1110 5200 3550 2400
LUC5_SD 222 1040 710 480
LUC6_Mean 875 5000 3500 2200
LUC6_SD 175 1000 700 440
LUC7_Mean 810 4000 3375 2100
LUC7_SD 162 800 675 420
LUC8_Mean 125 4000 3250 2000
LUC8_SD 25 800 650 400

Emissions

Emissions per ha. by farm type and landuse capability, e_{FL}, are in the ghg-emissions.csv file:

Code
emissions <- read.csv("data/market/ghg-emissions.csv", 
                      header = T, row.names = 1)
emissions |> 
  kable() |> 
  kable_styling("striped", full_width = F) |>
  scroll_box(height = "250px")
SNB Dairy Forest Crop
LUC1_Mean 4.000 11.00 -15.000000 1.2000000
LUC1_SD 1.200 3.30 4.500000 0.3600000
LUC2_Mean 3.750 10.50 -14.250000 1.1400000
LUC2_SD 1.125 3.15 4.275000 0.3420000
LUC3_Mean 3.500 10.00 -13.537500 1.0830000
LUC3_SD 1.050 3.00 4.061250 0.3249000
LUC4_Mean 3.250 9.50 -12.860625 1.0288500
LUC4_SD 0.975 2.85 3.858188 0.3086550
LUC5_Mean 3.000 9.00 -12.217594 0.9774075
LUC5_SD 0.900 2.70 3.665278 0.2932223
LUC6_Mean 2.750 8.50 -11.606714 0.9285371
LUC6_SD 0.825 2.55 3.482014 0.2785611
LUC7_Mean 2.500 8.00 -11.026378 0.8821103
LUC7_SD 0.750 2.40 3.307914 0.2646331
LUC8_Mean 2.250 7.50 -10.475059 0.8380048
LUC8_SD 0.675 2.25 3.142518 0.2514014

 

Note that all of yields, base costs, and emissions are stored as a mean and a standard deviation, and that individual hectare sites will see these numbers vary every model time period by drawing from a normal distribution.

We can use these data to show the profit, gross income, and total costs for each farm type across different landuse capability classes. We’ll throw away the standard deviations for simplicity and consider only mean outcomes.

Code
prices <- prices |> 
  t() |> 
  as.data.frame() |> 
  tibble::rownames_to_column("farm_type") |>
  rename(price = Price_Commodity, carbon_tax = Price_GhG) |>
  slice(rep(row_number(), 8)) |>
  mutate(LUC = rep(1:8, each = 4))

yields <- yields |>
  slice(seq(1, 15, 2)) |> 
  t() |> 
  as.data.frame() |> 
  tibble::rownames_to_column("farm_type") |>
  pivot_longer(-1) |>
  mutate(LUC = rep(1:8, 4), yield = round(value, 1)) |>
  select(farm_type, LUC, yield)

costs <- costs |>
  slice(seq(1, 15, 2)) |> 
  t() |> 
  as.data.frame() |> 
  tibble::rownames_to_column("farm_type") |>
  pivot_longer(-1) |>
  mutate(LUC = rep(1:8, 4), costs = round(value, 1)) |>
  select(farm_type, LUC, costs)

emissions <- emissions |> 
  slice(seq(1, 15, 2)) |> 
  t() |> 
  as.data.frame() |> 
  tibble::rownames_to_column("farm_type") |>
  pivot_longer(-1) |>
  mutate(LUC = rep(1:8, 4), emissions = round(value, 1)) |>
  select(farm_type, LUC, emissions)

combined_data <- prices |>
  left_join(yields) |>
  left_join(costs) |>
  left_join(emissions) |>
  mutate(gross_income = price * yield, # LUC = as.factor(LUC),
         total_costs = costs + carbon_tax * emissions, base_costs = costs,
         profit = gross_income - total_costs) |>
  select(farm_type, LUC, profit, price, yield, gross_income, base_costs,
         emissions, carbon_tax, total_costs) |>
  rename(Profit = profit, `Gross income` = gross_income, `Total costs` = total_costs)

combined_data |> 
  kable() |> 
  kable_styling("striped", full_width = F) |>
  scroll_box(height = "250px")
farm_type LUC Profit price yield Gross income base_costs emissions carbon_tax Total costs
SNB 1 242.50 5.0 768.5 3842.50 3500 4.0 25 3600.0
Dairy 1 1497.50 7.5 1503.0 11272.50 9500 11.0 25 9775.0
Forest 1 1085.00 157.0 30.0 4710.00 4000 -15.0 25 3625.0
Crop 1 1803.50 0.5 9667.0 4833.50 3000 1.2 25 3030.0
SNB 2 161.00 5.0 581.2 2906.00 2650 3.8 25 2745.0
Dairy 2 1310.00 7.5 1283.0 9622.50 8050 10.5 25 8312.5
Forest 2 908.00 157.0 29.0 4553.00 4000 -14.2 25 3645.0
Crop 2 1714.30 0.5 9183.6 4591.80 2850 1.1 25 2877.5
SNB 3 149.50 5.0 372.4 1862.00 1625 3.5 25 1712.5
Dairy 3 1207.25 7.5 1174.3 8807.25 7350 10.0 25 7600.0
Forest 3 883.50 157.0 28.0 4396.00 3850 -13.5 25 3512.5
Crop 3 1659.75 0.5 8724.5 4362.25 2675 1.1 25 2702.5
SNB 4 131.00 5.0 332.2 1661.00 1450 3.2 25 1530.0
Dairy 4 1188.00 7.5 923.4 6925.50 5500 9.5 25 5737.5
Forest 4 861.50 157.0 27.0 4239.00 3700 -12.9 25 3377.5
Crop 4 1619.10 0.5 8288.2 4144.10 2500 1.0 25 2525.0
SNB 5 112.50 5.0 259.5 1297.50 1110 3.0 25 1185.0
Dairy 5 1157.75 7.5 877.7 6582.75 5200 9.0 25 5425.0
Forest 5 837.00 157.0 26.0 4082.00 3550 -12.2 25 3245.0
Crop 5 1511.90 0.5 7873.8 3936.90 2400 1.0 25 2425.0
SNB 6 92.00 5.0 207.4 1037.00 875 2.8 25 945.0
Dairy 6 1131.00 7.5 845.8 6343.50 5000 8.5 25 5212.5
Forest 6 715.00 157.0 25.0 3925.00 3500 -11.6 25 3210.0
Crop 6 1517.55 0.5 7480.1 3740.05 2200 0.9 25 2222.5
SNB 7 93.50 5.0 193.2 966.00 810 2.5 25 872.5
Dairy 7 1050.00 7.5 700.0 5250.00 4000 8.0 25 4200.0
Forest 7 668.00 157.0 24.0 3768.00 3375 -11.0 25 3100.0
Crop 7 1430.55 0.5 7106.1 3553.05 2100 0.9 25 2122.5
SNB 8 69.50 5.0 49.9 249.50 125 2.2 25 180.0
Dairy 8 770.00 7.5 661.0 4957.50 4000 7.5 25 4187.5
Forest 8 623.50 157.0 23.0 3611.00 3250 -10.5 25 2987.5
Crop 8 1355.40 0.5 6750.8 3375.40 2000 0.8 25 2020.0

Or visually…

Below are shown the income, costs, and net revenue per hectare for each farm type across the eight landuse capability classes. These are crude estimates dating back to the ARLUNZ model1 and will be updated in due course.

Code
plot_data <- combined_data |> 
  pivot_longer(cols = c("Gross income", "Profit", "Total costs"))

ggplot(plot_data |> filter(name %in% c("Gross income", "Total costs"))) +
  geom_col(aes(x = as.factor(LUC), y = value, group = name, fill = name),
           position = "dodge") + 
  scale_fill_manual(values = c("#999999", "#ff6666"), name = "Income vs. Costs") +
  geom_point(data = plot_data |> filter(name == "Profit"), 
            aes(x = LUC, y = value, shape = name), size = 7) +
  scale_shape_manual(values = "\u2014", name = "") + # \u2014 is an em-dash
  # The below more correct, but doesn't extend the lines to the full width of the plot
  # geom_step(data = plot_data |> filter(name == "Profit"), 
  #           aes(x = LUC, y = value, group = farm_type, colour = name), 
  #           direction = "mid", lwd = 0.65) +
  # scale_colour_manual(values = c("black"), name = "") +
  xlab("Landuse Capability (LUC)") +
  ylab("Profit, $ per ha.") +
  facet_wrap(~ farm_type) +
  theme_minimal()

The effect of interventions

A range of possible interventions I=\{i_1,i_2,\ldots i_n\} may be undertaken by farmers, such as

  • Establish a wetland
  • Riparian planting
  • Clean races
  • Develop a farm plan
  • Join the emissions trading scheme
  • etc.

The currently adopted interventions on a particular farm we denote A which will be a subset of the available interventions I. Not all interventions will make sense on each farm, so it might be more accurate to think of there being a number of intervention sets I_F and on a particular farm of type F, the adopted interventions A are some subset of I_F, with the potential new interventions to be considered in each time interval given by I_F\setminus A, that is the possible interventions on this type of farm not yet adopted.

For each intervention i_i there are associated impacts, in terms of added costs \Delta x_{iFL}, and changes in yields \Delta q_{iFL} and emissions \Delta e_{iFL}, where the F and L subscripts indicate the dependence of these impacts on farm type F and landuse capability L. The impacts of various interventions are stored in the intervention-impacts.csv file:

Code
interventions <- read.csv("data/market/intervention-impacts.csv", 
                          header = T)
interventions |> 
  kable() |> 
  kable_styling("striped", full_width = F) |>
  scroll_box(height = "250px")
Intervention Impact SNB Dairy Forest Crop
Build_Wetland costs 25.00 68.00 NA 34.00
Build_Wetland yields -0.20 -0.02 NA -0.10
Build_Wetland emissions -0.10 -0.05 NA -0.05
Riparian_Planting costs 26.00 71.00 NA 11.00
Riparian_Planting yields -0.20 -0.02 NA -0.01
Riparian_Planting emissions -0.10 -0.03 NA -0.04
Clean_Races costs NA 75.00 NA NA
Clean_Races yields NA 0.00 NA NA
Clean_Races emissions NA -0.01 NA NA
Farm_Plan costs 6.00 34.00 NA NA
Farm_Plan yields -0.05 -0.01 NA NA
Farm_Plan emissions -0.06 -0.05 NA NA
Join_ETS costs 1.00 25.00 50.0 100.00
Join_ETS yields -0.25 -0.25 0.1 -0.10
Join_ETS emissions -0.50 -0.50 0.1 -0.25

 

The adopted interventions on a particular farm, change its yield to q = q_{FL}\prod_{i\in A} \left( 1+\Delta q_{iFL} \right) its total costs to x = x_{FL} + \sum_{i\in A} \Delta x_{iFL} and its emissions to e = e_{FL}\prod_{i\in A} \left(1+\Delta e_{iFL}\right) Together these give us an overall profit for each hectare of landuse capability L on a farm of type F that has adopted a set of interventions A { Z_A = \underbrace{ p_Fq_{FL}\prod_{i\in A} \left( 1+\Delta q_{iFL} \right)}_{\mathrm{gross\thinspace income}\,Y} - \underbrace{ \underbrace{\left( x_{FL} + \sum_{i\in A}\Delta x_{iFL} \right)}_{\mathrm{base\thinspace costs}} - \underbrace{te_{FL}\prod_{i\in A} \left(1+\Delta e_{iFL} \right)}_{\mathrm{carbon\thinspace costs}} }_{\mathrm{total\thinspace costs}\,X} } and the overall profit of any farm is given by summing this result across its constituent hectares. It is convenient to think of the profit as being the difference in the gross income given by the first term in the above equation, and total costs given by the second and third terms.

Considering interventions

The current profit of a farm is determined as in the previous section. Call the current profit, gross income, and total cost components \tilde{Y}_0 and \tilde{X}_0, respectively.

Then, for each intervention under consideration, i.e., each intervention i_i\in I_F\setminus A, the new expected gross income, and total costs that would result \bar{Y}_i and \bar{X}_i are determined using the same equation, but with A_i=A\cup\{i_i\} augmented to include the new intervention, and no variance in outcomes taken into consideration, i.e., using only the mean yield, costs, and emissions settings (the \tilde{Y}_0 and \tilde{X}_0 values will reflect that variance in the most recent year’s results).

This gives us relative change in each component, which for a particular component we define as \Delta x = \left\{ \begin{array}{rl} \left. 2\left(|x_1 - x_0|\right) \middle/ \left( |x_1| + |x_0| \right) \right. & \text{if $x_1>x_0$} \\ \left. -2\left(|x_1 - x_0|\right) \middle/ \left( |x_1| + |x_0| \right) \right. & \text{if $x_1<x_0$} \\ 0 & \text{if $x_0=x_1=0$} \end{array}\right.

NOTE: this formulation works if both quantities are of the same sign (both positive, or both negative) but produces meaningless results if the before and after quantities are of different sign, and so the approach outlined here cannot be extended to farm profits, which can be positive or negative.

Values for \Delta Y_i and \Delta X_i are calculated in this way and used to determine adjusted relative probabilities of adoption of each possible intervention. If we denote the baseline probability of adoption of each intervention on a particular farm type by P_{iF} then we adjust this probability using the \Delta values according to { P'_{iF}= \left. 1 \middle/ \left(1+e^{-a\left[ \ln\frac{P_{iF}}{1-P_{iF}} + \Delta Y_i - \Delta X_i \right]} \right) \right. }

This puts the adoption probability of any intervention on a sigmoid curve, given by P = 1/(1 + e^{-ax}) where x is some ‘pressure for change’ and a is a rate of change in the probability in response to changes in that pressure. We haven’t determined an appropriate setting for a, but intuitively it is a kind of measure of the ‘elasticity’ of farmer responses to changes in the combination of pressures driving their decision making. At low a large pushes are needed to make much difference to the probability of a particular intervention; at high a smaller pushes (perhaps even nudges!) might suffice. In general an adoption probability will be at its most elastic when it is around 0.5 with substantial pressure required to make a low probability event more likely, or to make an already probable outcome near certain.

Code
sigmoid <- function(x, a = 1) {
  1 / (1 + exp(-a * x))
}

df <- expand_grid(`Pressure to change` = -50:50 / 10, 
                  a = c(0.25, 0.5, 1, 2, 5)) |>
  mutate(Probability = sigmoid(`Pressure to change`, a))

ggplot(df) + 
  geom_line(aes(x = `Pressure to change`, y = Probability, group = a)) +
  annotate("label", x = c(4.25, 3.9, 3, 2, 1), 
           y = c(0.74, 0.87, 0.95, 0.975, 0.99),
           label = paste0("a = ", unique(df$a)))

A positive calculated change in either net revenue \Delta R_i, or income \Delta Y_i, pushes the farmer’s probability of adoption of intervention i higher (to the right), and any increase in total costs \Delta X_i pushes it lower (to the left). We start with initial probabilities of adoption in the farmer-threshold-matrix.csv file:

Code
probabilities <- read.csv("data/market/farmer-threshold-matrix.csv", 
                          header = T, row.names = 1)
probabilities |> 
  kable() |> 
  kable_styling("striped", full_width = F)
SNB Dairy Forest Crop
Build_Wetland 0.7 0.75 0.3 0.5
Riparian_Planting 0.7 0.75 0.2 0.4
Clean_Races 0.2 0.70 0.0 0.0
Farm_Plan 0.7 0.85 0.4 0.6
Join_ETS 0.2 0.20 0.9 0.4

The exact slope and configuration of the sigmoid function relative to these probabilities (which are themselves arbitrary at present) remains to be determined, and may be tuneable to yield plausible outcomes. The general principle is that nudges increase the presure on farmers to change and will move them to the right on these curves, but that increases to costs will act against such pressures.

NOTE: We have to consider if this approach is sensible given that it may mean that farmers are less likely to adopt interventions after a good year than they after a bad year…

Adopting an intervention

All possible interventions are considered and assigned a probability P'_{iF} on the above basis, giving a set of values P=\{P_{iF}\}. These are treated as ‘weights’ on a random draw of a single intervention. More favoured interventions are more likely to be drawn.

If a particular intervention is clearly preferable, say P=\{0.01,0.01,0.5\} then it will almost certainly be chosen for further consideration. If all options are similar in their impacts, say P=\{0.2,0.15,0.25\} then any of them stands a good chance of being picked.

The intervention picked in this way is then adopted with probability P'_{iF}. This means that P=\{0.1,0.1,0.1\} might put forward any intervention for consideration but that chosen intervention is still unlikely to be adopted. On the other hand P=\{0.5,0.5,0.5\} will give the selected intervention a 50% chance of adoption when it is considered.

Once an intervention is adopted, it will no longer be available in the next decision round and its impacts will be factored into calculation of the new Y_0 and X_0 income and costs, serving as a new baseline for the determination of \Delta Y_i and \Delta X_i scores.

A note on how model calculations are performed

In the model, calculations are performed using matrices.

All three of yields, costs, and emissions, are stored as a pair of matrices

\begin{array}{rcl} \overline{\mathbf{M}} & = & \left[\overline{\circ}_{LF}\right]\\ \widetilde{\mathbf{M}} & = & \left[\widetilde{\circ}_{LF}\right] \end{array}

for the mean and standard deviations respectively, where \circ stands in for one of q, x, or e. Row entries record the values associate with a given landuse capability level, and column entries the values assoicated with a given farm type.

Prices are stored as a prices vector, \mathbf{p}=\left[p_i\right], and the carbon tax as a scalar value, t, since it is fixed tax irrespective of farm type.

Ignoring for now the effect of adopted interventions, this gives us a matrix form of the income calculation for a farm

Y = \mathbf{1}\left[\mathbf{L}\odot N\left(\overline{\mathbf{Q}},\frac{\widetilde{\mathbf{Q}}}{\sqrt{n_{LF}}}\right)\right]\mathbf{p}

where \mathbf{L} is a landuse capability-landuse profile matrix for the farm with row and column entries n_{LF} giving the numbers of patches in each landuse capability and landuse category on that farm, and \odot denotes element-wise multiplication of two matrices. A similar matrix equation is applicable to the costs for a farm.

The most important thing to note here is the reduction in the standard deviation of the normal distribution from which yields q are drawn by the factor \sqrt{n_{LF}}. This allows only one random number draw for each landuse capability-landuse combination on a farm, rather than for every patch on the farm individually, and allows more rapid calculation of farms income, costs, and emissions. The penalty is that all patches in the model of the same landuse capability-landuse combination produce have the same mean yield, input cost, emissions every model tick, and that every landuse capability-landuse combination on each farm sees the same deviation from the mean applied each tick.

Calculation of additional costs resulting from adoption of various interventions can also be formulated as a matrix calculation, using a row-matrix of adopted interventions where 1 denotes adoption and 0 non-adoption of each intervention.

A more involved matrix formulation is required for the impact of adoption of interventions on emissions and yields because these are combined multiplicatively. The model code is the best guide to the details in this case. The code has been written to ensure that the equations detailed in the section on the effect of interventions has been followed.

Footnotes

  1. See: Morgan FJ and AJ Daigneault. 2015. Estimating Impacts of Climate Change Policy on Land Use: An Agent-Based Modelling Approach. PLOS ONE 10(5): e0127317.↩︎