Evaluating Ensemble Streamflow Forecasts#
Overview#
This notebook introduces using TEEHR to evaluate ensemble forecasts. Note that ensemble analysis in TEEHR is currently ongoing, and additional metrics and analyses are in development.
In this exercise we will use a pre-existing evaluation stored in TEEHR’s AWS S3 evaluation bucket consisting of two USGS gage locations with Hydrologic Ensemble Forecasting System (HEFS) forecasts from CNRFC.
Read evaluation data from S3#
from teehr.loading.s3.clone_from_s3 import list_s3_evaluations
list_s3_evaluations()["url"].values
array(['s3a://ciroh-rti-public-data/teehr-data-warehouse/v0_4_evaluations/e0_2_location_example',
's3a://ciroh-rti-public-data/teehr-data-warehouse/v0_4_evaluations/e1_camels_daily_streamflow',
's3a://ciroh-rti-public-data/teehr-data-warehouse/v0_4_evaluations/e2_camels_hourly_streamflow',
's3a://ciroh-rti-public-data/teehr-data-warehouse/v0_4_evaluations/e3_usgs_hourly_streamflow',
's3a://ciroh-rti-public-data/teehr-data-warehouse/v0_4_evaluations/e4_hefs_ensemble_example'],
dtype=object)
Explore the evaluation data#
In this example we have two USGS gage locations corresponding to CNRFC forecast points.
locations_gdf = ev.locations.to_geopandas()
locations_gdf.teehr.locations_map()
The secondary location IDs correspond to the CNRFC forecast point IDs.
ev.location_crosswalks.to_pandas()
primary_location_id | secondary_location_id | |
---|---|---|
0 | usgs-11421000 | MRYC1 |
1 | usgs-11402000 | SCBC1 |
The primary timeseries are the USGS gage observations.
pt_df = ev. primary_timeseries.to_pandas()
pt_df.head()
reference_time | value_time | value | unit_name | location_id | configuration_name | variable_name | |
---|---|---|---|---|---|---|---|
0 | NaT | 2024-04-01 12:00:00 | 711.0 | ft^3/s | usgs-11402000 | usgs_observations | streamflow_hourly_inst |
1 | NaT | 2024-04-01 13:00:00 | 701.0 | ft^3/s | usgs-11402000 | usgs_observations | streamflow_hourly_inst |
2 | NaT | 2024-04-01 14:00:00 | 696.0 | ft^3/s | usgs-11402000 | usgs_observations | streamflow_hourly_inst |
3 | NaT | 2024-04-01 15:00:00 | 691.0 | ft^3/s | usgs-11402000 | usgs_observations | streamflow_hourly_inst |
4 | NaT | 2024-04-01 16:00:00 | 686.0 | ft^3/s | usgs-11402000 | usgs_observations | streamflow_hourly_inst |
pt_df.teehr.timeseries_plot()
The secondary timeseries are the CNRFC HEFS ensemble forecasts corresponding to each location.
st_df = ev.secondary_timeseries.to_pandas()
st_df.head()
reference_time | value_time | value | unit_name | location_id | member | configuration_name | variable_name | |
---|---|---|---|---|---|---|---|---|
0 | 2024-04-01 12:00:00 | 2024-04-01 12:00:00 | 739.859985 | ft^3/s | SCBC1 | 1 | CNRFC_HEFS | streamflow_hourly_inst |
1 | 2024-04-01 12:00:00 | 2024-04-01 12:00:00 | 739.859985 | ft^3/s | SCBC1 | 2 | CNRFC_HEFS | streamflow_hourly_inst |
2 | 2024-04-01 12:00:00 | 2024-04-01 12:00:00 | 739.859985 | ft^3/s | SCBC1 | 3 | CNRFC_HEFS | streamflow_hourly_inst |
3 | 2024-04-01 12:00:00 | 2024-04-01 12:00:00 | 739.859985 | ft^3/s | SCBC1 | 4 | CNRFC_HEFS | streamflow_hourly_inst |
4 | 2024-04-01 12:00:00 | 2024-04-01 12:00:00 | 739.859985 | ft^3/s | SCBC1 | 5 | CNRFC_HEFS | streamflow_hourly_inst |
HEFS is probabilistic and each forecast consists of several ensemble traces or members. In TEEHR,each trace is represented by the member
column in the timeseries table schema.
In this case each forecast contains 42 members.
st_df.member.unique().size
42
Let’s visualize the data. We’ll use some helper functions to plot the forecasts against the USGS gage data. Note that visualization functionality within TEEHR is in-development.
import pandas as pd
import hvplot.pandas # noqa
def plot_teehr_timeseries(
df: pd.DataFrame,
x_column: str = "value_time",
y_column: str = "value",
title: str = None
):
columns = df.columns
variable_columns = [col for col in columns if df[col].nunique() > 1]
variable_columns.remove(x_column)
variable_columns.remove(y_column)
figure = df.hvplot(x="value_time", y="value", by=variable_columns)
return figure.options(
width=700,
height=400,
show_grid=True,
title=title
)
# Get the HEFS forecast for location SCBC1.
st_df = ev.secondary_timeseries.query("location_id == 'SCBC1'").to_pandas()
# Create a line plot of the corresponding USGS observed flow data.
usgs_plot = pt_df[pt_df.location_id == "usgs-11402000"].hvplot(x="value_time", y="value").options(line_width=3, line_dash="dashed", color="black")
# Create a line plot of the HEFS forecast data with the USGS observed flow data.
(plot_teehr_timeseries(st_df, title="SCBC1 HEFS Ensemble vs. USGS") * usgs_plot).options(show_legend=False)
# Get the HEFS forecast for location MRYC1.
st_df = ev.secondary_timeseries.query("location_id == 'MRYC1'").to_pandas()
# Create a line plot of the corresponding USGS observed flow data.
usgs_plot = pt_df[pt_df.location_id == "usgs-11421000"].hvplot(x="value_time", y="value").options(line_width=3, line_dash="dashed", color="black")
# Create a line plot of the HEFS forecast data with the USGS observed flow data.
(plot_teehr_timeseries(st_df, title="MRYC1 HEFS Ensemble vs. USGS") * usgs_plot).options(show_legend=False)
Calculating the Continuous Ranked Probability Score (CRPS)#
The continuous ranked probability score (CRPS) is a common metric used to evaluate the performance of ensemble forecasts. TEEHR implements CRPS through the scoringrules library.
We can define a CRPS metric object and customize it’s parameters in the same way we defined metrics in previous examples.
crps = teehr.ProbabilisticMetrics.CRPS()
# Specify the estimator to use.
# "pwm" - Probability Weighted Moment form (default)
# "nrg" - Energy form
# "fair" - Fair version of energy form
crps.estimator = "pwm"
# Specify the backend to use.
# "numba" - Use the numba library for calculations (default)
# "numpy" - Use the numpy library for calculations
crps.backend = "numba"
crps.output_field_name = "crps_ensemble"
ev.metrics.query(
include_metrics=[crps],
group_by=[
"primary_location_id",
"reference_time",
"configuration_name"
],
order_by=["primary_location_id"],
).to_pandas()
primary_location_id | reference_time | configuration_name | crps_ensemble | |
---|---|---|---|---|
0 | usgs-11402000 | 2024-04-01 12:00:00 | CNRFC_HEFS | [28.859985, 28.409973, 28.210022, 18.72637, 9.... |
1 | usgs-11421000 | 2024-04-01 12:00:00 | CNRFC_HEFS | [2.7600098, 2.840088, 4.129883, 5.7700195, 32.... |
By default, CRPS returns an array of scores corresponding to each value time. We can summarize these values by defining a function (typically mean) in the metric object.
import numpy as np
crps.summary_func = np.mean
crps.output_field_name = "mean_crps_ensemble"
ev.metrics.query(
include_metrics=[crps],
group_by=[
"primary_location_id",
"reference_time",
"configuration_name"
],
order_by=["primary_location_id"],
).to_pandas()
primary_location_id | reference_time | configuration_name | mean_crps_ensemble | |
---|---|---|---|---|
0 | usgs-11402000 | 2024-04-01 12:00:00 | CNRFC_HEFS | 55.764488 |
1 | usgs-11421000 | 2024-04-01 12:00:00 | CNRFC_HEFS | 412.486298 |
Summary#
Currently only the CRPS is implemented in TEEHR however additonal metrics and functionality is ongoing including ensemble timeseries visualization, skill score calculation, Brier Score, and more.
ev.spark.stop()