Grass, forbs, and Shrubs. Rangelands cover more than 54% of the world’s surface, performing key roles in carbon storage, and providing 70% of the world’s livestock feed source. (Brown & Thorpe, 2008). Rangelands are among the most threatened and forsaken ecosystems in the USA, with less than 10% of climate goals stirring towards their protection and over 2.1 million acres lost just in 2019 (ILRI, 2021). Among rangelands, the American Southwest has unique challenges of augmenting wildfire occurrence and increasing aridity due to climate change (Seager et al., 2007). Efficacy in the management, among other conservation tactics should be used to preserve the desert rangelands of the southwest USA. Among these efforts is Grass-Cast. This tool can predict rangelands’ biomass using 30 years of satellite data to prevent overgrazing and plan actions for subsequent seasons. Assessment of the accuracy of the prediction is focused on the Great plains in the USA (Hartman et al., 2020), but further information for the efficacy in the southwest is still needed.
The following project uses data available in Grass-Cast Explorer. I will compare weekly data of predicted ANPP (above-ground net primary productivity) and NDVI (Normalized difference vegetation index) from each forecast during the growing season and compare it to the observable ANPP. Then, we will choose an appropriate statistical model to assess the predictability of the tool, with satellite data and on-ground data. We hope that this project will support the team’s work behind Grass Cast by giving them a fresh perspective on the tool forecast capabilities for the southwest. We will also hope that ranchers and government agencies find in Grass Cast a mature tool that will help them plan the use and conservation of rangelands across the southwest.
The forecast analysis of Grass-Cast is based on the paper by Hartman et al, and is strongly correlated with the the weather forecast and history of NOAA(National Oceanic and Atmospheric Administration), mostly the precipitation (Figure 1). This input is add in to the DayCent model, and ecosystem-level biochemical model that simulates various ecological process, the main value extracted from this model is evapotranspiration (AET). Later, the model takes information of NDVI (Normalized Difference Vegetation Index) from the MODIS sensor in the Terra satellite, NDVI measures the greenness or the the state of the vegetation, where higher levels of NDVI means healthier vegetation. Finally, three ANPP (Above ground net primary production) scenarios are computed (average, above average and bellow average) measuring the biomass in pounds per acre.
Figure1
The forecast data is from the Grass-Cast platform can be accessed online (National Drought Mitigation Center, 2022).
Weather data is from NOAA climate data online platform, taking precipitation data from New Mexico and Arizona from November 2019 to December 2021 in millimeters (mm) (NOAA, 2022).
For the southwest, there are two growing seasons Spring (April-May) and Summer(August-September), the spring is mostly correlated with the winter rains (November-March) and the summer is correlated with the monsoon season (June-September), (Smith et al., 2019).
In this experiment we are trying to understand how well the forecast correlates to the final observation of Grass Cast and also see the effects of rainfall in the final forecast. To accomplish this objective I will take coefficient of determination (R2) between the percent difference in ANPP from each forecast during the growing season compared to the percent difference in ANPP for the final forecast. The result will be a line graft of a correlation from 8 forecast of 2020 and the 12 forecast of 2021 in the three different scenarios to the final observation of the year.
Beside this, I will interpolate rainfall data from NOAA from each growing season to explain the possible reasons for the data to change correlations during time.
##Charge the libraries
#Data Manipulation
library(tidyverse) #pipes and more
library(janitor) #cleans col numbers and more
library(data.table) #for splits
#Graphics Tools
library(ggplot2) #draws the graphs
library(cowplot) #plots side by side
#Spatial Tools
library(maps) #database of USA maps
library(sp)
library(sf)
library(raster)
library(rgeos)
###Nearest neighbour
library(dismo)
library(gstat)#upload the data
GC <- read.csv(file = "data/az_nm_2000_2020.csv", head = TRUE, sep=",")
gfg_data <- read.csv(file = "data/grass_cast_20-21.csv", head = TRUE, sep=",")
#Filters Year and select columns for ANPP and slips the forecast in lists
ANPP_FORECAST <- subset(gfg_data, select=c("gridID","Year","Forecast","NPP_predict_below",
"NPP_predict_avg","NPP_predict_above","Order"))
ANPP_FORECAST_2020 <- subset(ANPP_FORECAST, Year == '2020')
ANPP_FORECAST_2020 <- split(ANPP_FORECAST_2020,ANPP_FORECAST_2020$Forecast)
ANPP_FORECAST_2021 <- subset(ANPP_FORECAST, Year == '2021')
ANPP_FORECAST_2021 <- split(ANPP_FORECAST_2021,ANPP_FORECAST_2021$Forecast)
#this functions takes the latest data and compares to the every other forecast date and see how well is the forecast predicting
fc <- function(n, x, y) {
a <- cor(y[[n]]$NPP_predict_above, y[[x]]$NPP_predict_above)
b <- cor(y[[n]]$NPP_predict_avg, y[[x]]$NPP_predict_avg)
c <- cor(y[[n]]$NPP_predict_below, y[[x]]$NPP_predict_below)
ANPP.r <- matrix(c(a,b,c),ncol = 3,byrow = TRUE)
colnames(ANPP.r) <- c('Abv','Avg','Blw')
ANPP.r <- as.data.frame(ANPP.r)
#chages row names to date
row.names(ANPP.r) <- as.Date(names(y[x]), format = "%m/%d/%Y")
return(ANPP.r)
}
##Creates an empty list for the for loop
r.list2020 <- list()
r.list2021 <- list()
##For loop to have all the rsquer in the list
#the final observation was on place 8 for 2020 and place 10 for 2021
for(i in 1:8) {
r.list2020[[i]] <- fc(8, i,ANPP_FORECAST_2020)
}
for(i in 1:10) {
r.list2021[[i]] <- fc(10, i,ANPP_FORECAST_2021)
}
#Changes from list to DF
All_cor_2020 <- bind_rows(r.list2020)
All_cor_2021 <- bind_rows(r.list2021)
#add column with the row names
All_cor_2020$Forecast <- row.names(All_cor_2020)
All_cor_2021$Forecast <- row.names(All_cor_2021)
#pivot longer and change column names
All_cor_2020s <- All_cor_2020 %>% pivot_longer(cols = c(Abv,Avg,Blw), names_to = "Time",values_to = "R_squre")
All_cor_2021s <- All_cor_2021 %>% pivot_longer(cols = c(Abv,Avg,Blw), names_to = "Time",values_to = "R_squre")
colnames(All_cor_2020s) <- c('Date','Level','Rsqure')
colnames(All_cor_2021s) <- c('Date','Level','Rsqure')
#Creates the line graph
Cor_2020 <- ggplot(data = All_cor_2020s, aes(x=Date)) +
geom_line(aes(y=Rsqure, group=Level, colour =Level), size=0.9) +
ylim(0,1) +
labs (x = "Date of Forecast", y= "R squered of linear model",
title = "Correlation Coeffcient of ANPP Observations and Forecast 2020")+
theme_minimal() +
theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1))
Cor_2021 <- ggplot(data = All_cor_2021s, aes(x=Date)) +
geom_line(aes(y=Rsqure, group=Level, colour =Level), size=0.9) +
ylim(0,1) +
labs (x = "Date of Forecast", y= "R squered of linear model",
title = "Correlation Coeffcient of ANPP Observations and Forecast 2021")+
theme_minimal() +
theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1))## Call features from state and county
states <- map_data("state")
county <- map_data("county")
#Creates Data frames and filters states
state_df <- states %>% filter(str_detect(region, c("arizona","new mexico")))
county_df <- county %>% filter(str_detect(region, c("arizona","new mexico")))
#changes state maps to simple forms to make it eay
states_sf <- sf::st_as_sf(maps::map("state", plot = FALSE, fill = TRUE))
county_sf <- sf::st_as_sf(maps::map("county", plot = FALSE, fill = TRUE))
#select only AZ and NM Partial matching in the row ID
south_west_s1 <- states_sf %>% filter(str_detect(ID, c("arizona")))
south_west_s2 <- states_sf %>% filter(str_detect(ID, c("new mexico")))
south_west_s <- rbind(south_west_s1, south_west_s2)
south_west_nm <- county_sf %>% filter(str_detect(ID,"new mexico"))
south_west_az <- county_sf %>% filter(str_detect(ID,"arizona"))
south_west_c <- rbind(south_west_nm, south_west_az)
######
###Create Centroids of counties to use for labels
coordinates(county_df) <- c("long", "lat")
# Get centroids
ctrs <- lapply(unique(county_df$group),
function(x) gCentroid(SpatialPoints(county_df[county_df$group==x,])))
ctrsout <- setNames( ctrs , unique(county_df$group ) )
##Create data frame
centroid_df <- do.call(rbind, lapply(ctrsout, data.frame, stringsAsFactors=FALSE))
#finds and adds unique values
uniq_logical <- !duplicated(county_df$group)
centroid_df$county <- county_df$subregion[uniq_logical]
#change names of collums
names(centroid_df) <- c("lon", "lat", 'county')
centroid_sf <- st_as_sf(centroid_df, coords = c("lon", "lat")) %>%
st_set_crs(st_crs(south_west_s))
#########
#Prep data for maps
#We use 2020 year to get the lat and long
ANPP_GEOM <- subset(GC, year == '2020', select=c("gridID","lon","lat"))
#Filters Year and select columns for ANPP and drops NA values
ANPP_ALL <- subset(gfg_data, select=c("gridID", "Forecast","NPP_predict_below","NPP_predict_avg","NPP_predict_above")) %>%
drop_na()
#Slips the forecast file in to lists
ANPP_ALLFORECAST <- split(ANPP_ALL, ANPP_ALL$Forecast)
##Choose Date for Forecast to draw
#in every line drops NA values, transform df to sf object and changes the coordinates system to mach others
#First forecast from 2020
ANPP_5152020 <- merge(ANPP_GEOM, ANPP_ALLFORECAST[[2]], by = "gridID", all = TRUE) %>% drop_na() %>% st_as_sf(coords = c("lon", "lat")) %>% st_set_crs(st_crs(south_west_s))
#Last Observation from 2020
ANPP_9012020 <- merge(ANPP_GEOM, ANPP_ALLFORECAST[[18]], by = "gridID", all = TRUE) %>% drop_na() %>% st_as_sf(coords = c("lon", "lat")) %>% st_set_crs(st_crs(south_west_s))
#First forecast from 2021
ANPP_4142021 <- merge(ANPP_GEOM, ANPP_ALLFORECAST[[1]], by = "gridID", all = TRUE) %>% drop_na() %>% st_as_sf(coords = c("lon", "lat")) %>% st_set_crs(st_crs(south_west_s))
#Last Observation from 2021
ANPP_8242021 <- merge(ANPP_GEOM, ANPP_ALLFORECAST[[15]], by = "gridID", all = TRUE) %>% drop_na() %>% st_as_sf(coords = c("lon", "lat")) %>% st_set_crs(st_crs(south_west_s))
######
#Draws maps for 2020
ANPP_515202_plotAbo <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_5152020, mapping = aes(col=NPP_predict_above)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2020",
subtitle = "05/15/2020 Above Average ", col="ANNP \n (pounds/acre) \n")
ANPP_515202_plotAvg <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_5152020, mapping = aes(col=NPP_predict_avg)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2020",
subtitle = "05/15/2020 Average ", col="ANNP \n (pounds/acre) \n")
ANPP_515202_plotBel <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_5152020, mapping = aes(col=NPP_predict_below)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2020",
subtitle = "05/15/2020 Below average ", col="ANNP \n (pounds/acre) \n")
ANPP_9012020_plotfinal <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_9012020, mapping = aes(col=NPP_predict_below)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP last observation for 2020",
subtitle = "09/12/2020", col="ANNP \n (pounds/acre) \n")
######
#Draws maps for 2021
ANPP_4142021_plotAbo <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_4142021, mapping = aes(col=NPP_predict_above)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2021",
subtitle = "04/14/2021 Above Average ", col="ANNP \n (pounds/acre) \n")
ANPP_4142021_plotAvg <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_4142021, mapping = aes(col=NPP_predict_avg)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2021",
subtitle = "04/14/2021 Average ", col="ANNP \n (pounds/acre) \n")
ANPP_4142021_plotBel <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_4142021, mapping = aes(col=NPP_predict_below)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP first forecast for 2021",
subtitle = "04/14/2021 Below average ", col="ANNP \n (pounds/acre) \n")
ANPP_8242021_plotfinal <- ggplot() +
geom_sf() +
geom_sf(data = ANPP_8242021, mapping = aes(col=NPP_predict_below)) +
geom_sf(data = south_west_s, fill = NA, color=alpha("#003366",0.4), size=2.0) +
geom_sf(data = south_west_c, fill= NA, color=alpha("#3399FF",0.4), size=1.0) +
geom_sf_label(centroid_sf, mapping = aes(geometry=geometry, label = county), size=2,fontface = "bold") +
scale_colour_viridis_c(aesthetics = "col", option = "cividis", direction = -1, limits=c(0, 800)) +
theme_void() +
labs(title="ANPP last observation for 2021",
subtitle = "08/24/2020", col="ANNP \n (pounds/acre) \n")In the first observations of 2020, we see good estimations for the last season in all the scenarios but, finally the last observation turns out to be quite yellow. This explains the low correlation we see at the begging of correlation graph.
The opposite happend in 2021, the estimations where bad in the three scenarios, while the last one was pretty good. The grey points show missing data. Even more, the correlation for this year where good but they fluctuated more than in 2021. Because of this, I had the idea of showing the rains and breakdown what happens and why the first observation where so different from the first forecast, specially for the 2020 data.
#Call the data
rain_sum <- read.csv(file = "data/rain_sum.csv", head = TRUE, sep=",")
#there are some packed that only work with this CRS
TA <- CRS("+proj=aea +lat_1=34 +lat_2=40.5 +lat_0=0 +lon_0=-120 +x_0=0 +y_0=-4000000 +datum=WGS84 +units=m")
#Change sf to sp and transfor the coordintate
south_west_s_sp <- sf::as_Spatial(south_west_s) %>% sp::spTransform(TA)
#filter the data - November 2019 to March 2020
rain_4spring_2019 <- rain_sum %>% filter(year == 2019, month %in% c(11,12))
rain_4spring_2020 <- rain_sum %>% filter(year == 2020, month %in% c(1,2,3))
rain_4spring2020 <- rbind(rain_4spring_2020,rain_4spring_2019) %>% sf::st_as_sf(coords = c("longitude", "latitude")) %>% st_set_crs(st_crs(south_west_s)) %>% sf::as_Spatial() %>% sp::spTransform(TA)
#filter the data - November 2020 to March 2021
rain_4spring_2020s <- rain_sum %>% filter(year == 2020, month %in% c(11,12))
rain_4spring_2021 <- rain_sum %>% filter(year == 2021, month %in% c(1,2,3))
rain_4spring2021 <- rbind(rain_4spring_2020s,rain_4spring_2021) %>% sf::st_as_sf(coords = c("longitude", "latitude")) %>% st_set_crs(st_crs(south_west_s)) %>% sf::as_Spatial() %>% sp::spTransform(TA)
#filter the data - June to September 2020
rain_4summer2020 <- rain_sum %>% filter(year == 2020, month %in% c(6,7,8,9)) %>% sf::st_as_sf(coords = c("longitude", "latitude")) %>% st_set_crs(st_crs(south_west_s)) %>% sf::as_Spatial() %>% sp::spTransform(TA)
#filter the data - June to September 2020
rain_4summer2021 <- rain_sum %>% filter(year == 2021, month %in% c(6,7,8,9)) %>% sf::st_as_sf(coords = c("longitude", "latitude")) %>% st_set_crs(st_crs(south_west_s)) %>% sf::as_Spatial() %>% sp::spTransform(TA)
Inter <- function(x,y,my){
#interpolation1
ra <- voronoi(x) #data to be interpolated
sw <- aggregate(y) #boundries
ra_sw <- intersect(ra, sw) #SpatialPolygons
##Raster the result
r <- raster(y, res=10000)
raster_ra_sw <- rasterize(ra_sw, r, field='prcp')
#this is the formula for the interpolation
gs <- gstat(formula=prcp~1, data = x, locations=sw, nmax=30 ,set=list(idp = 0))
nn <- interpolate(r, gs)
nnmsk <- mask(nn, ra_sw)
#For drawing in GGPLOt
nnmsk_pts <- rasterToPoints(nnmsk, spatial = TRUE)
# Then to a 'conventional' dataframe
nnmsk_df <- data.frame(nnmsk_pts)
#Change it to a St data
nnmsk_sf <- st_as_sf(nnmsk_df, coords = c("x", "y"))
ggplot() +
geom_sf(data = nnmsk_sf , aes(col=var1.pred)) +
ggtitle(paste("Precipitation from",my)) +
theme_void() +
scale_color_viridis_c(aesthetics = "col", option="D", direction = -1,
name = "Rainfall \n (mm)", limits=c(0, 180))
}Once we see the rain, we can have an idea of why the correlations where low at the beginning of 2020 but later they become more stable than 2021. The winter precipitation for 2020 was good, and the models for the 05/15 where very promising.
## [inverse distance weighted interpolation]
Once the precipitation forecast where included in beginning of June and they showed very low precipitation for the monsoon season, the forecast for that day showed high correlation since, that is because the precipitation for the summer of 2020 end it up with very low rainfall across the southwest and this scenario was stable during all the forecast season after.
FALSE [inverse distance weighted interpolation]
The forecast for 2021 showed low ANNP at the begging of the season, because the winter rains that feed the spring growth where low. The correlation stared greater than 2020 probably because the final observation was not as different.
## [inverse distance weighted interpolation]
Still we saw a more variable correlation for this season, probably because the monsoon of 2021 was the greatest in history and the model has hard time to adjust to extremes. By the end of the summer, the rains where above normal and the correlation took a long time to adjust to the observation. The above scenario keep the higher correlation across all forecast in 2021.
## [inverse distance weighted interpolation]
There is a lot to understand about Forecast and Productivity for dry lands, as in every model, the extremes are hard to predict, the 2020 and 2021 seasons are excellent study cases to understand how well this models work unexpected situations. The rains for winter are easier to predict than the monsoon season (Smith et al., 2019), and the model gets more accurate as the monsoon season forecast improves or shows below normal precipitation from the start.
Brown, J. R., & Thorpe, J. (2008). Climate Change and Rangelands: Responding Rationally to Uncertainty. 4.
Hartman, M. D., Parton, W. J., Derner, J. D., Schulte, D. K., Smith, W. K., Peck, D. E., Day, K. A., Del Grosso, S. J., Lutz, S., Fuchs, B. A., Chen, M., & Gao, W. (2020). Seasonal grassland productivity forecast for the U.S. Great Plains using Grass‐Cast. Ecosphere, 11(11). https://doi.org/10.1002/ecs2.3280
ILRI. (2021). Rangelands ATLAS. International Livestock Research Institute. https://www.rangelandsdata.org/atlas/sites/default/files/2021-05/Rangelands%20Atlas.pdf
National Drought Mitigation Center. 2022. [Data Set] available online: https://grasscast.unl.edu/Archive.aspx
NOAA. (2021). GHCN (Global Historical Climatology Network) [Data Set] available online: https://www.ncdc.noaa.gov/cdo-web/
Seager, R., Ting, M., Held, I., Kushnir, Y., Lu, J., Vecchi, G., Huang, H.-P., Harnik, N., Leetmaa, A., Lau, N.-C., Li, C., Velez, J., & Naik, N. (2007). Model Projections of an Imminent Transition to a More Arid Climate in Southwestern North America. Science, 316(5828), 1181–1184. https://doi.org/10.1126/science.1139601
Smith, W. K., Dannenberg, M. P., & Yang, D. (2019). Remote sensing of dryland ecosystem structure and function_ Progress, challenges, and opportunities. Remote Sensing of Environment, 23.