Herding Data: The Udderly Fascinating Trends in New Zealand Livestock

code
maps
animation
Author

Isaac Bain

Published

July 23, 2024

1 Introduction

In New Zealand, discussing livestock numbers is practically a national pastime, whether you’re interested in the agri-economy, environmental health, or the ever-popular sheep-to-people ratio.1

In this post, I will present visualisations aimed at enhancing our understanding of the number and distribution of various livestock over time and across different regions of New Zealand.

2 Data sources

StatsNZ publishes livestock numbers as part of their Agricultural Production Survey (APS) derived data series. It’s important to note that within this single dataset, there are varying levels of spatial resolution and time series:

  1. Total livestock numbers across NZ (1971-2019)
  2. Regional breakdown (1990-2019)
  3. APS hex grid breakdown (1994-2017)
Show the code
library(tidyverse)
library(sf)
library(mapgl)
library(koordinatr)
library(zoo)
library(gganimate)
library(scales)
library(isaacr)
library(pals)
library(magick)
library(billboarder)

options(scipen = 999)

# Load the data
aps17 <- st_read("data/livestock-numbers-grid-aps-2017/livestock-numbers-grid-aps-2017.shp")
aps94 <- st_read("data/livestock-numbers-grid-aps-1994/livestock-numbers-grid-aps-1994.shp")

aps17$sheepdens[aps17$grid_id == 724] <- 0
aps17$sheepdens[aps17$grid_id == 742] <- 0

livestock_numbers_raw <- get_table_as_tibble(Sys.getenv("mfe_api_key"), "mfe", "105406") |> select(-gml_id)

# Read in the shapefile of regions
regions_sf <- st_read("data/statsnz-regional-council-2023-clipped-generalised-SHP/regional-council-2023-clipped-generalised.shp") |> 
  mutate(REGC2023_2 = str_replace(REGC2023_2, " Region$", "")) |> 
  st_simplify(dTolerance = 1000, preserveTopology = FALSE)

4 Decline in Sheep Numbers

Sheep numbers in New Zealand have experienced precipitous decline.

  • Since their peak in 1982 at just over 70 million, sheep numbers have been declining almost every year, reaching a record low of 26.8 million in 2019.
Show the code
# Load and prepare your data
sheep_numbers <- livestock_numbers_raw |>
  filter(geography_name == "New Zealand") |>
  filter(animal == "Sheep") |>
  group_by(animal) |>
  mutate(count = na.spline(count, na.rm = FALSE)) # Interpolate missing values via cubic spline interpolation

# Create the plot
p2 <- ggplot(sheep_numbers, aes(x = year, y = count, colour = animal, group = animal)) +
  geom_line() +
  geom_point(size = 2) +
  geom_text(aes(label = animal), vjust = -0.5, hjust = 0, size = 4) +
  labs(title = "Total sheep numbers for New Zealand",
       x = "",
       y = "Number of sheep") +
  theme_minimal() +
  scale_colour_brewer(palette = "Set1") +
  expand_limits(x = c(1971, 2019 + 6), y = 0) + # Expand limits on x-axis
  scale_y_continuous(breaks = seq(0, max(sheep_numbers$count, na.rm = TRUE), by = 1e7),
                     labels = scales::label_number(scale = 1e-6, suffix = "M")) + # Format y-axis labels
  theme(legend.position = "none") +
  transition_reveal(year)

# Render the animation
animate(p2, nframes = 100, fps = 10, end_pause = 20)

5 Comparing Livestock Populations

A graph of total livestock numbers highlights the sheer dominance of sheep compared to other livestock types. However, this dominance can obscure the trends in other animals, making it difficult to discern their patterns and changes over time.

Show the code
# Load and prepare your data
livestock_numbers <- livestock_numbers_raw  |> 
  filter(geography_name == "New Zealand") |>
  group_by(animal) |>
  mutate(count = na.spline(count, na.rm = FALSE)) |> # Interpolate missing values via cubic spline interpolation
  filter(count > 0) # Filter out animals with zero counts

# Create the plot
p3 <- ggplot(livestock_numbers, aes(x = year, y = count, colour = animal, group = animal)) +
  geom_line() +
  geom_point(size = 2) +
  geom_text(aes(label = animal), vjust = -0.5, hjust = 0, size = 4) +
  labs(title = "Total livestock numbers for New Zealand",
       x = "",
       y = "Number of animals") +
  theme_minimal() +
  scale_colour_brewer(palette = "Set1") +
  expand_limits(x = c(1971, 2019 + 6), y = 0) + # Expand limits on x-axis
  scale_y_continuous(breaks = seq(0, max(livestock_numbers$count, na.rm = TRUE), by = 1e7),
                     labels = scales::label_number(scale = 1e-6, suffix = "M")) + # Format y-axis labels
  theme(legend.position = "none") +
  transition_reveal(year)

# Render the animation
animate(p3, nframes = 100, fps = 10, end_pause = 20)

6 Stock Unit Equivalents Explained

The previous graph doesn’t effectively communicate the varying metabolic demand of different animals, nor the changes in metabolic demand over time. These demands have been increasing due to rising animal size and productivity.

Table 1: Stock unit equivalent values assumed for sheep, beef, dairy and deer livestock classes between 1980 and 2017. From Snelder et al (2021)
Stock type 1980 1985 2000 2017
Sheep 0.95 0.95 1.15 1.35
Beef 5 5.3 6 6.9
Dairy 5 5.5 6.8 8
Deer 1.6 1.6 2 2.3


Converting to stock unit equivalents, as done by Snelder et al. (2021),2 allows for more meaningful comparisons. I’ve used their methods of conversion for consistency. In short, the count of animals is multiplied by the stock unit equivalent (Table 1) for that animal type and nearest year.

  • Total stock units peaked in 2012 at 121.9 million stock unit equivalents (SU).
  • Total stock units in New Zealand have remained relatively high over time, with a modest increase from 106 million SUs in 1980 to 115 million SUs in 2019.
Show the code
# Apply the function to the dataset
total_stock_units <- livestock_numbers_raw |> 
  filter(geography_name == "New Zealand") |>
  filter(animal != "Total cattle") |> 
  group_by(animal) |> 
  mutate(count = na.spline(count, na.rm = FALSE)) |> # Interpolate missing values via cubic spline interpolation
  ungroup() |> 
  rowwise() |>
  mutate(stock_unit_equivalent = count * get_stock_unit(animal, year)) |>
  ungroup() |> 
  group_by(year) |> 
  summarise(total_stock_unit_equivalent = sum(stock_unit_equivalent, na.rm = TRUE))

# Create the plot
p4 <- ggplot(total_stock_units, aes(x = year, y = total_stock_unit_equivalent)) +
  geom_line() +
  geom_point(size = 2) +
  geom_text(label = "Total stock units", vjust = -0.5, hjust = 0, size = 4) +
  labs(title = "Total stock unit equivalents for New Zealand",
       x = "",
       y = "Number of stock units") +
  theme_minimal() +
  expand_limits(x = c(1971, 2019 + 6), y = 0) + # Expand limits on x-axis
  scale_y_continuous(breaks = seq(0, max(total_stock_units$total_stock_unit_equivalent, na.rm = TRUE), by = 5e7),
                     labels = scales::label_number(scale = 1e-6, suffix = "M")) + # Format y-axis labels
  theme(legend.position = "none") +
  transition_reveal(year)

# Render the animation
animate(p4, nframes = 100, fps = 10, end_pause = 20)

9 2D Livestock Density Comparison

Now you get a bonus for reading this far! The talented Kyle Walker has been developing a new R package called mapgl, which brings the power of Mapbox GL JS and MapLibre GL JS to R users. This package introduces exciting new features that surpass traditional mapping packages like Leaflet.

The first map is a simple 2D representation of the livestock hex grids, allowing you to slide the slider back and forth to compare sheep density between the years 1994 and 2017.

Show the code
my_column <- "sheepdens"
my_range <- round(range(aps17$sheepdens), 0)

mapboxgl() |>
  fit_bounds(aps94, animate = FALSE) |>
  add_fill_layer(id = "APS 1994",
                 source = aps94,
                 fill_color = interpolate(
                   column = my_column,
                   values = c(0, 420), 
                   stops = c("#F4F4F3", "#A40D23"),
                   na_color = "lightgrey",
                 ),
                 fill_opacity = 0.8) |> 
  add_legend("Sheep density (count/ha) <br>1994 / 2017",
    values = my_range,
    colors = c("#F4F4F3", "#A40D23")
  )-> map1

mapboxgl() |> 
  fit_bounds(aps17, animate = FALSE) |>
  add_fill_layer(id = "APS 2017",
                 source = aps17,
                 fill_color = interpolate(
                   column = my_column,
                   values = c(0, 420), 
                   stops = c("#F4F4F3", "#A40D23"),
                   na_color = "lightgrey",
                 ),
                 fill_opacity = 0.8) -> map2

# very hacky way to include the map in the html. See issue at: https://github.com/walkerke/mapgl/issues/3
htmlwidgets::saveWidget(compare(map1, map2), "www/map1.html")

10 3D Livestock Density Visualisation

The next plot is something truly special! The hex grids are rendered in 3D, with both their height and colour proportional to the sheep density. You can slide the slider back and forth as before, but you can also hold down the control button while clicking to tilt and pan the 3D view.

In 3D, the dramatic reduction in sheep numbers is striking, and you can clearly see where the losses have predominantly occurred in the South Island.

Show the code
my_column <- "sheepdens"
my_range <- round(range(aps17$sheepdens), 0)

maplibre(
  style = maptiler_style('basic'),
  center = c(170.5, -43.5),
  zoom = 5,
  pitch = 60,
  bearing = 0
) |>
  add_fill_extrusion_layer(
    id = "3d",
    source = aps94,
    fill_extrusion_color = interpolate(
      column = my_column,
      values = my_range,
      stops = c("#F4F4F3", "#A40D23")
    ),
    fill_extrusion_height = interpolate(
      column = my_column,
      values = my_range, # Data values for interpolation
      stops = c(0, 160000) # Corresponding heights for those values
    )
  ) |>
  add_legend("Sheep density (count/ha) <br>1994 / 2017",
    values = my_range,
    colors = c("#F4F4F3", "#A40D23")
  ) -> map3

maplibre(
  style = maptiler_style('basic'),
  center = c(170.5, -43.5),
  zoom = 5,
  pitch = 60,
  bearing = 0
) |>
  add_fill_extrusion_layer(
    id = "3d",
    source = aps17,
    fill_extrusion_color = interpolate(
      column = my_column,
      values = my_range,
      stops = c('#F4F4F3', '#A40D23')
    ),
    fill_extrusion_height = interpolate(
      column = my_column,
      values = my_range,  # Data values for interpolation
      stops = c(0, 160000)  # Corresponding heights for those values
    )
  ) -> map4

#compare(map3, map4)

# very hacky way to include the map in the html. See issue at: https://github.com/walkerke/mapgl/issues/3
htmlwidgets::saveWidget(compare(map3, map4), "www/map2.html")

11 Conclusions

That wraps it up. This post explored livestock numbers in New Zealand, highlighting trends and regional variations for cattle and sheep populations. By using stock unit equivalents, we gained deeper insights into these changes.

We also showcased innovative visualisation techniques, from animations to interactive 3D comparisons, to illustrate these trends effectively.

I hope this has provided valuable insights into the livestock dataset and inspired new ways to visualise agricultural data. Thank you for reading, and stay tuned for more insights and visualisations.

Footnotes

  1. FYI it’s currently 4.6 sheep per person! See RNZ for more details.↩︎

  2. Snelder TH, Fraser C, Larned ST, Monaghan R, De Malmanche S, Whitehead AL. Attribution of river water-quality trends to agricultural land use and climate variability in New Zealand. Marine and Freshwater Research. 2021;73(1):1-19. doi:10.1071/mf21086↩︎