Skip to content

Commit b75ace9

Browse files
Merge pull request #32 from ConnectedSystems/ensemble-node
Add support for weighted ensembles
2 parents 5396c11 + d177b0b commit b75ace9

File tree

13 files changed

+379
-114
lines changed

13 files changed

+379
-114
lines changed

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
<img align="center" src="docs/src/assets/logo.svg" alt="Streamfall.jl" fill="currentColor"/>
33
<p>A graph-based streamflow modelling system written in Julialang.</p>
44

5-
[![DOI](https://zenodo.org/badge/345341654.svg)](https://zenodo.org/badge/latestdoi/345341654)
5+
[![DOI](https://zenodo.org/badge/345341654.svg)](https://zenodo.org/badge/latestdoi/345341654) [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://connectedsystems.github.io/Streamfall.jl/dev)
6+
67
</div>
78

89
Streamfall leverages the Julia language and ecosystem to provide:
@@ -23,10 +24,25 @@ The IHACRES rainfall-runoff model was previously implemented with [ihacres_nim](
2324

2425
[Graphs](https://github.com/JuliaGraphs/Graphs.jl) and [MetaGraphs](https://github.com/JuliaGraphs/MetaGraphs.jl) are used underneath for network traversal/analysis.
2526

26-
Development version of the documentation can be found here: [![](https://img.shields.io/badge/docs-dev-blue.svg)](https://connectedsystems.github.io/Streamfall.jl/dev).
27-
2827
> [NOTE] Streamfall is currently in its early stages and under active development. Although it is fairly usable for small networks and assessing/analyzing single sub-catchments, things may change drastically and unexpectedly.
2928
29+
## Installation
30+
31+
Streamfall is now registered! The latest release version can be installed with:
32+
33+
```julia
34+
] add Streamfall
35+
```
36+
37+
or the latest development version from GitHub with `dev` () or `add`:
38+
39+
```julia
40+
# Editable install
41+
] dev Streamfall#main
42+
43+
] add https://github.com/ConnectedSystems/Streamfall.jl#main
44+
```
45+
3046
## Development
3147

3248
Local development should follow the usual process of git cloning the repository.

docs/make.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ makedocs(sitename="Streamfall Documentation",
2727
"examples/simple_showcase.md",
2828
"examples/model_comparison.md",
2929
"examples/simple_multisystem.md",
30+
],
31+
"Ensemble modeling" => [
32+
"examples/weighted_ensembles.md"
3033
]
3134
],
3235
"API" => [
107 KB
Loading
213 KB
Loading
154 KB
Loading
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
Streamfall supports ensemble modeling using individual instances of rainfall-runoff models
2+
as the ensemble constituents. The default ensemble is a normalized weighted sum.
3+
4+
The usual setup process is shown here, detailed in previous sections of this guide.
5+
6+
```julia
7+
using YAML, DataFrames, CSV, Plots
8+
using Statistics
9+
using Streamfall
10+
11+
12+
climate = Climate("../test/data/campaspe/climate/climate.csv", "_rain", "_evap")
13+
14+
# Historic flows and dam level data
15+
obs_data = CSV.read(
16+
"../test/data/cotter/climate/CAMELS-AUS_410730.csv",
17+
DataFrame;
18+
comment="#"
19+
)
20+
21+
Qo = extract_flow(obs_data, "410730")
22+
climate = extract_climate(obs_data)
23+
```
24+
25+
Specific node representations can then be created, each representing the same sub-catchment.
26+
A stream network is not considered for this demonstration.
27+
28+
```julia
29+
30+
# Create one instance each of IHACRES_CMD and GR4J
31+
ihacres_node = create_node(IHACRESBilinearNode, "410730", 129.2)
32+
gr4j_node = create_node(GR4JNode, "410730", 129.2)
33+
34+
# Create a weighted ensemble with equal weights
35+
# The default behavior is to combine component predictions with a normalized weighted sum.
36+
ensemble = create_node(WeightedEnsembleNode, [ihacres_node, gr4j_node], [0.5, 0.5])
37+
```
38+
39+
In previous examples, the `calibrate!()` method was used to calibrate nodes to available
40+
data. For WeightedEnsembleNodes, `calibrate!()` optimizes just the weights to allow for
41+
pre-calibrated instances to be provided for use in an ensemble. This may be useful if
42+
it is desired for the component models to be calibrated according to different criteria and
43+
objective functions.
44+
45+
A separate `calibrate_instances!()` method is available to calibrate individual component
46+
models (and the weights used).
47+
48+
```julia
49+
# The default behaviour for WeightedEnsembleNodes are to calibrate just the weights.
50+
# res, opt = calibrate!(ensemble, climate, Qo, (obs, sim) -> 1.0 .- Streamfall.NmKGE(obs, sim); MaxTime=180)
51+
52+
# Here, the component models are uncalibrated so we calibrate these and the weights.
53+
res, opt = calibrate_instances!(ensemble, climate, Qo, (obs, sim) -> 1.0 .- Streamfall.NmKGE(obs, sim); MaxTime=180)
54+
```
55+
56+
Running the calibrated models directly, the amount of improvement to model performance can
57+
be assessed. Here, a 1-year burn in period is used.
58+
59+
60+
```julia
61+
run_node!(ihacres_node, climate)
62+
run_node!(gr4j_node, climate)
63+
run_node!(ensemble, climate)
64+
65+
burn_in = 365
66+
burn_dates = timesteps(climate)[burn_in:end]
67+
burn_obs = Qo[burn_in:end, "410730"]
68+
69+
ihacres_qp = quickplot(burn_obs, ihacres_node.outflow[burn_in:end], climate, "IHACRES", true)
70+
gr4j_qp = quickplot(burn_obs, gr4j_node.outflow[burn_in:end], climate, "GR4J", true)
71+
ensemble_qp = quickplot(burn_obs, ensemble.outflow[burn_in:end], climate, "Weighted Ensemble", true)
72+
73+
plot(ihacres_qp, gr4j_qp, ensemble_qp; layout=(3, 1), size=(800, 1200))
74+
```
75+
76+
Below a small improvement to model performance based on the modified Kling-Gupta Efficiency
77+
score can be seen. Comparing the Q-Q plots, IHACRES had a tendency to underestimate low
78+
flows and high flows, whereas GR4J had a tendency to overestimate low flows.
79+
80+
The weighted ensemble combined characteristics of both, with a tendency to overestimate
81+
low flows as with GR4J.
82+
83+
![](../assets/ensemble_model_comparison_quickplots.png)
84+
85+
Comparing the temporal cross section:
86+
87+
```julia
88+
ihacres_xs = temporal_cross_section(burn_dates, burn_obs, ihacres_node.outflow[burn_in:end]; title="IHACRES")
89+
gr4j_xs = temporal_cross_section(burn_dates, burn_obs, gr4j_node.outflow[burn_in:end]; title="GR4J")
90+
ensemble_xs = temporal_cross_section(burn_dates, burn_obs, ensemble.outflow[burn_in:end]; title="Weighted Ensemble (IHACRES-GR4J)")
91+
92+
plot(ihacres_xs, gr4j_xs, ensemble_xs; layout=(3, 1), size=(800, 1200))
93+
```
94+
95+
A reduction in the median error can be seen with extreme errors reduced somewhat (according to
96+
the 95% CI).
97+
98+
![](../assets/ensemble_xsection.png)
99+
100+
The median error can then be applied to modelled streamflow (on a month-day basis) as a
101+
form of bias correction.
102+
103+
```julia
104+
q_star = Streamfall.apply_temporal_correction(ensemble, climate, Qo[:, "410730"])
105+
106+
bc_ensemble_qp = quickplot(burn_obs, q_star[burn_in:end], climate, "Bias Corrected Ensemble")
107+
108+
bias_corrected_xs = temporal_cross_section(
109+
burn_dates,
110+
burn_obs,
111+
q_star[burn_in:end];
112+
title="Bias Corrected Ensemble"
113+
)
114+
115+
plot(bc_ensemble_qp, bias_corrected_xs; layout=(2,1), size=(800, 800))
116+
```
117+
118+
While the median error has increased, its variance has reduced significantly. At the same
119+
time, performance at the 75 and 95% CI remain steady relative to the original weighted
120+
ensemble results.
121+
122+
![](../assets/ensemble_bias_corrected.png)

src/Analysis/uncertainty.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ function cross_section(tr::TemporalCrossSection)
5555
boxframe[i, 8:9] .= mean(vi), std(vi)
5656
end
5757

58-
cols = [:abs_min, :lower_95, :lower_75, :median,
58+
cols = [:abs_min, :lower_95, :lower_75, :median,
5959
:upper_75, :upper_95, :abs_max, :mean, :std]
6060

6161
x_section = DataFrame(boxframe, cols)
@@ -68,9 +68,9 @@ end
6868

6969

7070
"""
71-
offsets(tr::TemporalCrossSection, period::Function=monthday)
71+
offsets(tr::TemporalCrossSection)
7272
73-
Determine (median) offsets, assuming mean error-based temporal cross-sectional analysis
73+
Determine (median) offsets, assuming mean error-based temporal cross-sectional analysis
7474
was performed. Only supports daily time series.
7575
"""
7676
function offsets(tr::TemporalCrossSection)
@@ -84,11 +84,11 @@ function offsets(tr::TemporalCrossSection)
8484
mds = monthday.(dates)
8585
for (idx, (d, md)) in enumerate(zip(doy, mds))
8686
if md == (2, 29) || d == 366
87-
offset_vals[idx] = offset[d-1] * -1.0
87+
offset_vals[idx] = -offset[d-1]
8888
continue
8989
end
9090

91-
offset_vals[idx] = offset[d] * -1.0
91+
offset_vals[idx] = -offset[d]
9292
end
9393

9494
return offset_vals

src/Nodes/EnsembleNode.jl

Lines changed: 0 additions & 101 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Parameters
2+
using ModelParameters
3+
using Statistics
4+
5+
6+
abstract type EnsembleNode <: NetworkNode end
7+
8+
include("WeightedEnsembleNode.jl")
9+
# include("StateEnsembleNode.jl")
10+

0 commit comments

Comments
 (0)