from os.path import join, abspath, basename, splitext
from glob import glob
from datetime import datetime, date, timedelta
from zipfile import ZipFile
import warnings
ECOSTRESS Workshop Fall 2022
Gregory Halverson Jet Propulsion Laboratory, California Institute of Technology
This research was carried out at the Jet Propulsion Laboratory, California Institute of Technology, and was sponsored by ECOSTRESS and the National Aeronautics and Space Administration (80NM0018D0004).
© 2022. All rights reserved.
Importing Libraries
These are some built-in Python functions we need for this notebook, including functions for handling filenames and dates.
We’re using the rioxarray
package for loading raster data from GeoTIFF files, and we’re importing it as rxr
. We’re using the numpy
library to handle arrays, and we’re importing it as np
. We’re using the rasterstats
package for zonal statistics.
!mamba install -q -y rasterstats
import rioxarray as rxr
import numpy as np
from rasterstats import zonal_stats
We’re using the geopandas
library to load vector data from GeoJSON files, and we’re importing it as gpd
. We’re using the shapely
library to handle vector data and the pyproj
library to handle projections.
import geopandas as gpd
from shapely.geometry import Point, box
from shapely.ops import transform
from pyproj import Transformer
We’re using the pandas
library to handle tables, and we’re importing it as pd
.
import pandas as pd
We’re using the seaborn
library to produce our graphs, and we’re importing it as sns
. We’re using the hvplot
library to produce our maps. We’re using the matplotlib
library to handle plotting figures, and we’re importing it as plt
.
import seaborn as sns
import hvplot.xarray
import hvplot.pandas
import matplotlib.pyplot as plt
Defining Constants
These constants define the dimensions of our figures. Feel free to adjust these to fit your display.
= 1080
FIG_WIDTH_PX = 720
FIG_HEIGHT_PX = 16
FIG_WIDTH_IN = 9
FIG_HEIGHT_IN = 0.7
FIG_ALPHA = "ESRI"
BASEMAP = "whitegrid"
SEABORN_STYLE sns.set_style(SEABORN_STYLE)
This is the location of the example ECOSTRESS Collection 2 product files.
= "/home/jovyan/shared/2022-fall-ecostress-workshop/Carpinteria_ECOSTRESS_Collection_2"
DATA_DIRECTORY print(f"data directory: {DATA_DIRECTORY}")
Loading an ECOSTRESS Collection 2 Granule
First, let’s trying opening a data layer from a product file.
= join(DATA_DIRECTORY, "ECOv002_L2T_LSTE_12139_005_11SKU_20200826T191453_0700_01.zip")
filename print(f"example L2T LSTE file: {filename}")
The granule ID for this granule can be parsed from the filename by dropping the .zip
extension.
= splitext(basename(filename))[0]
granule_ID print(f"granule ID: {granule_ID}")
This product bundle, stored in zip format, contains a number of files, including raster data layers in GeoTIFF format as .tif
files, and GeoJPEG browse images as .jpeg
files. The GeoTIFF files can be loaded into GIS software, such as QGIS and ArcGIS. The GeoJPEG files can be loaded into Google Earth.
with ZipFile(filename) as zip_file:
for internal_file in zip_file.filelist:
print(internal_file.filename)
The ECOSTRESS Collection 2 tiled products include metadata in JSON format as a .json
text file.
with ZipFile(filename) as zip_file:
= zip_file.read(f"{granule_ID}/{granule_ID}.json").decode()
metadata
print(metadata)
To open the temperature layer of this file, we’ll form a Universion Resource Identifier (URI) with the pattern: zip://{filename}!/{granule_ID}/{granule_ID}_{variable}.tif
= f"zip://{abspath(filename)}!/{granule_ID}/{granule_ID}_LST.tif"
URI print(f"URI: {URI}")
We’re using rioxarray
to open the surface temperature layer from the L2T_LSTE
product on the 11SKU tile covering the Carpinteria Salt Marsh. We’re passing the URI pointing to the GeoTIFF file contained within this zip file. If you unzip this product bundle or download the GeoTIFF file on its own, you can pass the filename of the GeoTIFF file directly into rioxarray
.
= rxr.open_rasterio(URI).squeeze('band', drop=True)
ST_K_raster ST_K_raster
This xarray.DataArray
object contains both an array of image values and spatial metadata. The rioxarray
package extends xarray
with a .rio
attribute containing the metadata. Here we’re examining the coordinate reference system (CRS) of this image in the rioxarray
metadata.
= ST_K_raster.rio.crs
CRS CRS.to_wkt()
This image is projected in UTM zone 11 north. Distances in this projection system are in meters. All tiled ECOSTRESS products are projected in UTM, but tiles are projected into different UTM zones, depending on where they are in the world.
We can also check the spatial resolution of the grid cells in this image in the .rio
metadata.
= ST_K_raster.rio.resolution()
cell_width, cell_height print(f"cell width: {cell_width} cell height: {cell_height}")
This image is projected with 70m square pixels, as are all tiled ECOSTRESS products.
To know the observation time for this granule, we’re parsing the timestamp from the filename. This timestamp is given as Coordinated Universal Time (UTC).
= datetime.strptime(basename(filename).split("_")[-3], "%Y%m%dT%H%M%S")
datetime_UTC print(f"date/time UTC: {datetime_UTC:%Y-%m-%d %H:%M:%S}")
We want to know the centroid coordinate of this tile so that we can adjust the UTC time given to solar apparent time. We’re calculating the centroid as the average of x coordinate values and average of y coordinate values.
= Point(np.nanmean(ST_K_raster.x), np.nanmean(ST_K_raster.y))
centroid_UTM print(f"centroid UTM: {centroid_UTM}")
This centroid coordinate is in meters. We want to convert these projected x and y values in meters to geographic latitude and longitude in degress.
= transform(Transformer.from_crs(CRS, "EPSG:4326", always_xy=True).transform, centroid_UTM)
centroid_latlon print(f"centroid lat/lon: {centroid_latlon.wkt}")
We’re shifting the UTC time to local time according to longitude.
= datetime_UTC + timedelta(hours=(np.radians(centroid_latlon.x) / np.pi * 12))
datetime_solar print(f"date/time solar: {datetime_solar:%Y-%m-%d %H:%M:%S}")
The hvplot
package extends xarray
to allow us to plot maps. We’re reprojecting the raster geographic projection EPSG 4326 to overlay on the basemap with a latitude and longitude graticule. We’re using the jet
color scheme to render temperature with a rainbow of colors with red meaning hot and blue meaning cool. We’re setting the alpha to make the raster semi-transparent on top of the basemap. We’re filtering out values lower than the 2% percentile and higher than the 98% percentile to make the variation in the image more visible.
= "jet"
ST_CMAP
= ST_K_raster.rio.reproject("EPSG:4326").hvplot.image(
ST_K_map =True,
geo=ST_CMAP,
cmap=BASEMAP,
tiles=FIG_ALPHA,
alpha=FIG_WIDTH_PX,
width=FIG_HEIGHT_PX,
height=(ST_K_raster.quantile(0.02), ST_K_raster.quantile(0.98)),
clim=f"{granule_ID} Surface Temperature (Kelvin)"
title
)
= ST_K_map.options(xlabel="Longitude", ylabel="Latitude")
ST_K_map ST_K_map
The temperatures in the L2T_LSTE
product are given in Kelvin. To convert them to Celsius, we subtract 273.15.
= ST_K_raster.copy()
ST_C_raster = ST_K_raster.data - 273.15
ST_C_raster.data
= ST_C_raster.rio.reproject("EPSG:4326").hvplot.image(
ST_C_map =True,
geo=ST_CMAP,
cmap=BASEMAP,
tiles=FIG_ALPHA,
alpha=FIG_WIDTH_PX,
width=FIG_HEIGHT_PX,
height=(ST_C_raster.quantile(0.02), ST_C_raster.quantile(0.98)),
clim=f"{granule_ID} Surface Temperature (Celsius)"
title
)
= ST_C_map.options(xlabel="Longitude", ylabel="Latitude")
ST_C_map ST_C_map
Including Vector Data in Analysis
We want to analyze the ECOSTRESS images using the Carpinteria Salt Marsh Habitat Polygons provided by the USGS. This dataset is included here in GeoJSON format, which we’ll load using the geopandas
package.
= gpd.read_file("landcover.geojson")
landcover_latlon landcover_latlon
To align this vector dataset with the raster datasets, we need to project it to the UTM projection used for the rasters.
= landcover_latlon.to_crs(ST_C_raster.rio.crs)
landcover_UTM landcover_UTM
This vector dataset contains polygons classifying the surface of the Carpinteria Salt Marsh into channel, salt flat, upland, pan, and marsh.
= {
landcover_colors "channel": "blue",
"marsh": "yellow",
"pan": "green",
"salt flat": "white",
"upland": "brown"
}
= landcover_latlon.to_crs("EPSG:4326").hvplot.polygons(
landcover_map =True,
geo=landcover_UTM["type"].apply(lambda type: landcover_colors[type]),
color=BASEMAP,
tiles=FIG_ALPHA,
alpha=FIG_WIDTH_PX,
width=FIG_HEIGHT_PX,
height="Carpinteria Salt Marsh Habitat Polygons"
title
)
= landcover_map.options(xlabel="Longitude", ylabel="Latitude")
landcover_map landcover_map
To compare the raster image to the vector dataset, we want to subset the raster to the bounds of the vector dataset. We’re getting the bounds of our study area in meters from the convex hull of our land-cover polygons with a 100 meter buffer.
= landcover_UTM.unary_union.convex_hull.buffer(100).bounds
xmin, ymin, xmax, ymax xmin, ymin, xmax, ymax
We’re using this bounding box obtained from the extent of the vector dataset to clip the extent of the raster dataset. Now we can look at the ECOSTRESS surface temperature over the Carpinteria Salt Marsh.
= ST_C_raster.rio.clip([box(xmin, ymin, xmax, ymax)])
ST_C_subset
= ST_C_subset.rio.reproject("EPSG:4326").hvplot.image(
ST_C_subset_map =True,
geo=ST_CMAP,
cmap=BASEMAP,
tiles=FIG_ALPHA,
alpha=FIG_WIDTH_PX,
width=FIG_HEIGHT_PX,
height=(ST_C_subset.quantile(0.02), ST_C_subset.quantile(0.98)),
clim= "Carpinteria Salt Marsh Surface Temperature (Celsius)"
title
)
= ST_C_subset_map.options(xlabel="Longitude", ylabel="Latitude")
ST_C_subset_map ST_C_subset_map
We’re comparing the raster dataset to the polygon dataset by calculating zonal statistics with the rasterstats
package.
= pd.DataFrame(zonal_stats(
landcover_stats
landcover_UTM,
ST_C_subset.data,=ST_C_subset.rio.transform(),
affine=np.nan,
nodata=["min", "median", "max"]
stats
))
"type"] = landcover_UTM["type"]
landcover_stats["variable"] = "ST_C"
landcover_stats["datetime_solar"] = datetime_solar
landcover_stats[= landcover_stats[["type", "variable", "datetime_solar", "min", "median", "max"]]
landcover_stats landcover_stats
Loading a Time-Series of ECOSTRESS Files in Bulk
We’re using the built-in glob
function to search for L2T_LSTE
filenames in our downloaded collection of ECOSTRESS files.
= sorted(glob(join(DATA_DIRECTORY, "*_L2T_LSTE_*.zip")))
L2T_LSTE_filenames L2T_LSTE_filenames
Let’s run our zonal statistics analysis on each of the ECOSTRESS granules we have available, and produce a table of these statistics.
= pd.DataFrame({}, columns=["datetime_solar", "type", "ST"])
salt_marsh_ST
for i, filename in enumerate(L2T_LSTE_filenames):
= splitext(basename(filename))[0]
granule_ID print(f"({i+1}/{len(L2T_LSTE_filenames)}) {granule_ID}")
= f"zip://{filename}!/{granule_ID}/{granule_ID}_LST.tif"
URI = rxr.open_rasterio(URI).squeeze('band', drop=True)
ST_K_raster = ST_K_raster.copy()
ST_C_raster = ST_K_raster.data - 273.15
ST_C_raster.data = datetime.strptime(basename(filename).split("_")[-3], "%Y%m%dT%H%M%S")
datetime_UTC = Point(np.nanmean(ST_K_raster.x), np.nanmean(ST_K_raster.y))
centroid_UTM = transform(Transformer.from_crs(CRS, "EPSG:4326", always_xy=True).transform, centroid_UTM)
centroid_latlon = datetime_UTC + timedelta(hours=(np.radians(centroid_latlon.x) / np.pi * 12))
datetime_solar = ST_C_raster.rio.clip([box(xmin, ymin, xmax, ymax)])
ST_C_subset
= pd.DataFrame(zonal_stats(
landcover_stats
landcover_UTM,
ST_C_subset.data,=ST_C_subset.rio.transform(),
affine=np.nan,
nodata=["median"]
stats
))
"type"] = landcover_UTM["type"]
landcover_stats["datetime_solar"] = datetime_solar
landcover_stats["ST"] = landcover_stats["median"].apply(lambda value: np.nan if value is None else value)
landcover_stats[= landcover_stats[["datetime_solar", "type", "ST"]]
landcover_stats = pd.concat([salt_marsh_ST, landcover_stats])
salt_marsh_ST
= salt_marsh_ST.dropna()
salt_marsh_ST salt_marsh_ST
Visualizing Distributions with Boxplots
Let’s examine the distribution of ECOSTRESS surface temperatures seen in the different land-cover types within the Carpinteria Salt Marsh. To visualize these distributions, we’re plotting them as boxplots. We’re using the seaborn
library to generate these boxplots.
We tend to see cooler temperatures where water settles in the channel and warmer temperatures in the upland and salt flat areas, which are above the water and lack vegetation. Moderate temperatures are seen in the vegetated marsh and pan areas in between.
with warnings.catch_warnings():
"ignore")
warnings.simplefilter(= salt_marsh_ST.groupby("type").median().reset_index()[["type", "ST"]].sort_values("ST")
salt_marsh_ST_overall_median
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax
sns.boxplot(=salt_marsh_ST,
data="type",
x="ST",
y=salt_marsh_ST_overall_median.type,
order=landcover_colors,
palette=ax
ax
)
set(xlabel="Land Cover Type", ylabel="Median Surface Temperature (Celsius)")
ax.= range(int(np.nanmin(salt_marsh_ST.ST)), int(np.nanmax(salt_marsh_ST.ST)) + 2)
yticks
ax.set_yticks(yticks)= [f"{tick}°C" for tick in yticks]
yticklabels
ax.set_yticklabels(yticklabels)"Carpinteria Salt Marsh Median ECOSTRESS Surface Temperature Boxplots by Land Cover")
plt.title(
plt.show()
plt.close(fig)
print(salt_marsh_ST_overall_median)
Seasonal Agreggate Time-Series
Let’s examine a time-series of ECOSTRESS surface temperature. To make this time-series smooth, we’ll aggregate our zonal statistics by season.
= salt_marsh_ST.copy()
salt_marsh_ST_seasonal "season"] = salt_marsh_ST_seasonal.apply(
salt_marsh_ST_seasonal[lambda row: date(int(row.datetime_solar.year), int(((row.datetime_solar.month - 1) // 3) * 3 + 1), 1), axis=1)
= salt_marsh_ST_seasonal[["season", "type", "ST"]]
salt_marsh_ST_seasonal = salt_marsh_ST_seasonal.dropna().groupby(
salt_marsh_ST_seasonal =["season", "type"]).median().reset_index()
by salt_marsh_ST_seasonal
We’re using the seaborn
package again to visualize this timeseries as a line-plot. In this line-plot, we can see that temperatures in the Carptineria Salt Marsh tend to drop in the Winter and rise in the Summer. Channel and marsh temperatures tend to be the coolest for much of the year, and upland and salt flat temperatures tend to be the warmest for much of the year.
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax
sns.lineplot(=salt_marsh_ST_seasonal,
data="season",
x="ST",
y="type",
hue=ax
ax
)
set(xlabel="Season", ylabel="Seasonal Median Surface Temperature (Celsius)")
ax.= sorted(np.unique(salt_marsh_ST_seasonal.season))
xticks
ax.set_xticks(xticks)= {1: "Winter", 4: "Spring", 7: "Summer", 10: "Fall"}
seasons f"{seasons[xtick.month]}\n{xtick.year}" for xtick in xticks])
ax.set_xticklabels([= range(int(min(salt_marsh_ST_seasonal.ST)), int(max(salt_marsh_ST_seasonal.ST)) + 1)
yticks
ax.set_yticks(yticks)f"{ytick:0.0f}°C" for ytick in yticks])
ax.set_yticklabels([
="Marsh Land Type")
plt.legend(title"Carpinteria Salt Marsh Seasonal Median ECOSTRESS Surface Temperature Timeline by Land Cover")
plt.title(
plt.show() plt.close(fig)
ECOSTRESS Evapotranspiration
Now let’s repeat our time-series analysis using the ECOSTRESS evapotranspiration product. ECOSTRESS Collection 2 includes the JPL Evapotranspiration Ensemble (JET) product, which runs several models to create a balanced ET estimate. We’re searching through our set of downloaded ECOSTRESS files for L3T_JET
granules using glob
.
= sorted(glob(join(DATA_DIRECTORY, "*_L3T_JET_*.zip")))
L3T_JET_filenames L3T_JET_filenames
We’re running our zonal statistics time-series analysis again, but now loading daily evapotranspiration (ET) in millimeters per day from the L3T JET product.
= pd.DataFrame({}, columns=["datetime_solar", "type", "ET"])
salt_marsh_ET
for i, filename in enumerate(L3T_JET_filenames):
= splitext(basename(filename))[0]
granule_ID print(f"({i+1}/{len(L3T_JET_filenames)}) {granule_ID}")
= f"zip://{filename}!/{granule_ID}/{granule_ID}_ETdaily.tif"
URI = rxr.open_rasterio(URI).squeeze('band', drop=True)
ET_raster = datetime.strptime(
datetime_UTC "_")[-3], "%Y%m%dT%H%M%S")
basename(filename).split(= Point(np.nanmean(ST_K_raster.x), np.nanmean(ST_K_raster.y))
centroid_UTM = transform(Transformer.from_crs(
centroid_latlon "EPSG:4326", always_xy=True).transform, centroid_UTM)
CRS, = datetime_UTC + \
datetime_solar =(np.radians(centroid_latlon.x) / np.pi * 12))
timedelta(hours= ET_raster.rio.clip([box(xmin, ymin, xmax, ymax)])
ET_subset
= pd.DataFrame(zonal_stats(
landcover_stats
landcover_UTM,
ET_subset.data,=ET_subset.rio.transform(),
affine=np.nan,
nodata=["median"]
stats
))
"type"] = landcover_UTM["type"]
landcover_stats["datetime_solar"] = datetime_solar
landcover_stats["ET"] = landcover_stats["median"].apply(lambda value: np.nan if value is None or value == 0 else value)
landcover_stats[= landcover_stats[["datetime_solar", "type", "ET"]]
landcover_stats = pd.concat([salt_marsh_ET, landcover_stats])
salt_marsh_ET
= salt_marsh_ET.dropna()
salt_marsh_ET salt_marsh_ET
Now let’s produce seaborn
box-plots again to visualize the distribution of ECOSTRESS evapotranspiration. The non-vegetated upland and salt flat areas tend to be the driest, while the channel and marsh areas tend to be the wettest.
with warnings.catch_warnings():
"ignore")
warnings.simplefilter(= salt_marsh_ET.groupby("type").median().reset_index()[["type", "ET"]].sort_values("ET")
salt_marsh_ET_overall_median
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax
sns.boxplot(=salt_marsh_ET,
data="type",
x="ET",
y=salt_marsh_ET_overall_median.type,
order=landcover_colors,
palette=ax
ax
)
set(xlabel="Land Cover Type", ylabel="Median Daily Evapotranspiration (mm)")
ax.= np.arange(np.round(np.nanmin(salt_marsh_ET.ET)), np.round(np.nanmax(salt_marsh_ET.ET)) + 0.2, 0.1)
yticks
ax.set_yticks(yticks)= [f"{tick:.1f} mm" for tick in yticks]
yticklabels
ax.set_yticklabels(yticklabels)"Carpinteria Salt Marsh Median ECOSTRESS Daily Evapotranspiration Boxplots by Land Cover")
plt.title(
plt.show()
plt.close(fig)print(salt_marsh_ET_overall_median)
Let’s create another seasonal aggregate, now for ECOSTRESS evapotranspiration.
= salt_marsh_ET.copy()
salt_marsh_ET_seasonal "season"] = salt_marsh_ET_seasonal.apply(
salt_marsh_ET_seasonal[lambda row: date(int(row.datetime_solar.year), int(((row.datetime_solar.month - 1) // 3) * 3 + 1), 1), axis=1)
= salt_marsh_ET_seasonal[["season", "type", "ET"]]
salt_marsh_ET_seasonal = salt_marsh_ET_seasonal.dropna().groupby(
salt_marsh_ET_seasonal =["season", "type"]).median().reset_index()
by salt_marsh_ET_seasonal
We’re using a seaborn
line-plot again to visualize the seasonal aggregate of ECOSTRESS evapotranspiration by land-cover type. The channel and pan areas become dry in the heat of the Summer but then become wet again in the Autumn.
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax
sns.lineplot(=salt_marsh_ET_seasonal,
data="season",
x="ET",
y="type",
hue=ax
ax
)
set(xlabel="Season", ylabel="Seasonal Median Daily Evapotranspiration (mm)")
ax.= sorted(np.unique(salt_marsh_ET_seasonal.season))
xticks
ax.set_xticks(xticks)= {1: "Winter", 4: "Spring", 7: "Summer", 10: "Fall"}
seasons f"{seasons[xtick.month]}\n{xtick.year}" for xtick in xticks])
ax.set_xticklabels([= np.arange(np.round(np.nanmin(salt_marsh_ET_seasonal.ET), 1), np.round(np.nanmax(salt_marsh_ET_seasonal.ET), 1) + 0.1, 0.1)
yticks
ax.set_yticks(yticks)= [f"{tick:.1f} mm" for tick in yticks]
yticklabels
ax.set_yticklabels(yticklabels)="Marsh Land Type")
plt.legend(title"Carpinteria Salt Marsh Seasonal Median ECOSTRESS Evapotranspiration Timeline by Land Cover")
plt.title(
plt.show() plt.close(fig)
Relationship Between Surface Temperature and Evapotranspiration
Now let’s compare the seasonal time-series of surface temperature and evapotranspiration. First we’re merging the surface temperature and evapotranspiration tables we’re produced into a single table using pandas
.
= pd.merge(salt_marsh_ST_seasonal, salt_marsh_ET_seasonal, how="inner").dropna().groupby(["season", "type"]).median().reset_index()
salt_marsh_seasonal salt_marsh_seasonal
Let’s compare the seasonal timelines of ECOSTRESS surface temperature and evapotranspiration with one last seaborn
line-plot. We’re plotting these lines together with a shared x axis for the season and separate y axes to compare temperature in Celsius to evapotranspiration in millimeters per day. We see a consistent and complementary seasonal cycle between these two variables, with cool temperatures and high evapotranspiration in the Winter and warm temperatures and low evapotranspiration in the Summer.
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax set(xlabel="Season", ylabel="Seasonal Median Surface Temperature (Celsius)")
ax.True)
ax.grid(= np.arange(np.round(np.nanmin(salt_marsh_seasonal.ST)), np.round(np.nanmax(salt_marsh_seasonal.ST)))
yticks = sorted(np.unique(salt_marsh_seasonal.season))
xticks
ax.set_xticks(xticks)= {1: "Winter", 4: "Spring", 7: "Summer", 10: "Fall"}
seasons f"{seasons[xtick.month]}\n{xtick.year}" for xtick in xticks])
ax.set_xticklabels([
ax.set_yticks(yticks)f"{int(tick)} °C" for tick in yticks])
ax.set_yticklabels([
sns.lineplot(=salt_marsh_seasonal,
data="season",
x="ST",
y="red",
color=ax,
ax="Surface Temperature"
label
)
="upper left")
plt.legend(loc= ax.twinx()
ax2 set(ylabel="Seasonal Median Daily Evapotranspiration (mm)")
ax2.False)
ax2.grid(= np.arange(np.round(np.nanmin(salt_marsh_seasonal.ET), 1), np.round(np.nanmax(salt_marsh_seasonal.ET), 1), 0.1)
yticks
ax2.set_yticks(yticks)f"{tick:0.1f} mm" for tick in yticks])
ax2.set_yticklabels([
sns.lineplot(=salt_marsh_seasonal,
data="season",
x="ET",
y="blue",
color=ax2,
ax="Evapotranspiration"
label
)
="upper right")
plt.legend(loc"Carpinteria Salt Marsh Seasonal Median ECOSTRESS Surface Temperature & Evapotranspiration Timeline")
plt.title(
plt.show() plt.close(fig)
Finally, let’s visualize this inverse relationship between surface temperature and evapotranspiration with a scatter-plot, again using seaborn
. The surface temperature is the x-axis and the evapotranspiration is the y-axis. In this scatter of points, we see a roughly linear relationship from cool and wet points in the upper left to warm and dry points in the lower right. We’re using regplot
to plot this trend-line.
= plt.subplots(figsize=(FIG_WIDTH_IN, FIG_HEIGHT_IN))
fig, ax = np.arange(np.round(np.nanmin(salt_marsh_seasonal.ST)), np.round(np.nanmax(salt_marsh_seasonal.ST)) + 1)
xticks
ax.set_xticks(xticks)f"{int(tick)} °C" for tick in xticks])
ax.set_xticklabels([= np.arange(np.round(np.nanmin(salt_marsh_seasonal.ET), 1), np.round(np.nanmax(salt_marsh_seasonal.ET), 1) + 0.1, 0.1)
yticks
ax.set_yticks(yticks)f"{tick:0.1f} mm" for tick in yticks])
ax.set_yticklabels([
sns.scatterplot(=salt_marsh_seasonal,
data="ST",
x="ET",
y="type",
hue=landcover_colors,
palette="black",
edgecolor=1,
linewidth=ax
ax
)
sns.regplot(=salt_marsh_seasonal,
data="ST",
x="ET",
y=False,
scatter="gray",
color=ax
ax
)
set(xlabel="Seasonal Median Surface Temperature (Celsius)", ylabel="Seasonal Median Daily Evapotranspiration (mm)")
ax.="Marsh Land Type")
plt.legend(title"Carpinteria Salt Marsh Seasonal Surface Temperature and Evapotranspiration Scatterplot")
plt.title(
plt.show() plt.close(fig)
To quantify this inverse relationship, first we’ll calculate the correlation between these two variables using numpy
. We see an inverse relationship between seasonally aggregated ECOSTRESS surface temperature and evapotranspiration in the Carpinteria Salt Marsh with a correlation coefficient of -0.49.
round(np.corrcoef(x=salt_marsh_seasonal.ST, y=salt_marsh_seasonal.ET)[0][1], 2) np.