Using a split dataset, estimate parameters using optimisation on data subsets and evaluate their performance on all other subsets.

crossValidate(
  MODEL,
  periods,
  name.Model.str = paste(MODEL$sma, MODEL$routing),
  name.Cal.objfn = "unknown",
  name.Catchment = as.character(MODEL$call$DATA),
  fitBy,
  ...,
  trace = isTRUE(hydromad.getOption("trace")),
  parallel = hydromad.getOption("parallel")[["crossValidate"]]
)

Arguments

MODEL

an object of class hydromad.

periods

named list of start and end dates, passed to splitData

name.Model.str

Name to give to this model structure to allow a combined analysis

name.Cal.objfn

Name to give to this model identification process (e.g. name of objective function and/or optimisation algorithm), to allow a combined analysis

name.Catchment

Name to give to this catchment to allow a combined analysis

fitBy

function to estimate parameters of MODEL, e.g. fitByOptim, fitBySCE

...

Arguments passed to fitBy

trace

Whether to report messages.

parallel

name of method to use for parallelisation ("foreach" or "none"), or list giving settings for parallelisation. See hydromad_parallelisation.

Value

A runlist of n*n models evaluated in each of n periods with parameters estimated from each of n periods, of subclass crossvalidation

Parallelisation

crossValidate optionally allows the separate optimisations to be run concurrently with the parallel option method="foreach". This is usually only worthwhile for longer running optimisations such as fitBySCE rather than relatively fast methods such as fitByOptim.

The total runtime is limited by the runtime of the slowest of optimisations, e.g. the longest data subset, most complex objective function response surface, or slowest computer on which an optimisation is being run. Some of the workers may therefore be idle (potentially wasting money) even though others are still running.

The evaluation of parameters on validation data subsets is also optionally parallelised through the function update.runlist, by setting hydromad.options(parallel=list(update.runlist="clusterApply")). The advantage of this is likely to be minor, unless a large number of cross validation periods are used, due to the overhead involved and its relative speed compared to the optimisation. Note that this requires parallelisation to be setup on the worker, which is where the evaluation occurs.

If the parallelisation backend for foreach supports it, the cross-validations can be set to occur in the background using parallel=list(method="foreach",async=TRUE). In this case, the function returns immediately and the progress and results can be retrieved using functions provided by the parallelisation backend. This can be useful to submit a number of cross-validations for which the results are not immediately needed. As with a single cross-validation, mixing long and short running optimisations can make it difficult to fully utilise available workers.

In future, it may also be possible to parallelise each optimisation itself in addition to or instead of parallelising the optimisation of each data period.

Author

Joseph Guillaume

Examples


data(Cotter)
modx <- hydromad(Cotter,
  sma = "cwi", routing = "expuh",
  tau_s = c(2, 100), v_s = c(0, 1)
)
periods <- list(
  P1 = as.Date(c("1966-05-01", "1976-04-30")),
  P2 = as.Date(c("1976-05-1", "1986-04-30"))
)
## Estimate parameters using single fitByOptim run
## from single initial parameter set
runs <- crossValidate(modx, periods = periods, fitBy = fitByOptim, samples = 1)
summary(runs)
#>          timesteps missing   mean.P    mean.Q    runoff      rel.bias r.squared
#> P1_calP1      3553       0 2.936248 1.0010380 0.3409242  1.647550e-17 0.7838178
#> P2_calP1      3552       0 2.791132 0.7544418 0.2702996  2.220760e-01 0.6733349
#> P1_calP2      3553       0 2.936248 1.0010380 0.3409242 -1.916880e-01 0.7043730
#> P2_calP2      3552       0 2.791132 0.7544418 0.2702996 -2.930517e-09 0.7614177
#>          r.sq.sqrt  r.sq.log sim.period calib.period Model.str Cal.objfn
#> P1_calP1 0.7887613 0.7629356         P1           P1 cwi expuh   unknown
#> P2_calP1 0.7552158 0.7761833         P2           P1 cwi expuh   unknown
#> P1_calP2 0.7532489 0.7349019         P1           P2 cwi expuh   unknown
#> P2_calP2 0.8411148 0.8464667         P2           P2 cwi expuh   unknown
#>          Catchment
#> P1_calP1    Cotter
#> P2_calP1    Cotter
#> P1_calP2    Cotter
#> P2_calP2    Cotter
## Cross-validation statistics can then be analysed with other methods
paretoTimeAnalysis_areModelsDominated(summary(runs))
#>   calib.period Model.str Cal.objfn Catchment objective        P1        P2
#> 1           P1 cwi expuh   unknown    Cotter r.squared 0.7838178 0.6733349
#> 2           P2 cwi expuh   unknown    Cotter r.squared 0.7043730 0.7614177
#>   dominated
#> 1     FALSE
#> 2     FALSE
cast(
  melt(summary(runs), id.vars = c("calib.period", "sim.period")),
  calib.period ~ variable + sim.period
)
#>   calib.period timesteps_P1 timesteps_P2 missing_P1 missing_P2        mean.P_P1
#> 1           P1         3553         3552          0          0 2.93624824092316
#> 2           P2         3553         3552          0          0 2.93624824092316
#>          mean.P_P2        mean.Q_P1        mean.Q_P2         runoff_P1
#> 1 2.79113175675676 1.00103801325108 0.75444183767805 0.340924176402861
#> 2 2.79113175675676 1.00103801325108 0.75444183767805 0.340924176402861
#>           runoff_P2          rel.bias_P1           rel.bias_P2
#> 1 0.270299614431207 1.64755036639954e-17      0.22207598877803
#> 2 0.270299614431207   -0.191687990812397 -2.93051684043601e-09
#>        r.squared_P1      r.squared_P2      r.sq.sqrt_P1      r.sq.sqrt_P2
#> 1  0.78381782651507 0.673334854810548 0.788761332725465 0.755215815313714
#> 2 0.704372971173514 0.761417709020757 0.753248892472808 0.841114791953054
#>         r.sq.log_P1       r.sq.log_P2 Model.str_P1 Model.str_P2 Cal.objfn_P1
#> 1  0.76293564308086 0.776183292137543    cwi expuh    cwi expuh      unknown
#> 2 0.734901880638413 0.846466696367463    cwi expuh    cwi expuh      unknown
#>   Cal.objfn_P2 Catchment_P1 Catchment_P2
#> 1      unknown       Cotter       Cotter
#> 2      unknown       Cotter       Cotter
paretoTimeAnalysis(runs)
#> 
#> Cross-validation Pareto analysis
#> Which models cannot be rejected, due to dataset uncertainty/non-stationarity?
#> 
#> == Eliminating Pareto-dominated models ==
#>  (worse than another model in all periods)
#> 
#> How many models are dominated (and therefore eliminated)
#> Cotter.FALSE 
#>            2 
#> 
#> Which model structures are non-dominated in each catchment?
#> Proportion of model instances that are non-dominated
#>        cwi expuh
#> Cotter         1
#> 
#> Specify show.models=TRUE to show non-dominated and dominated models
#> Specify show.models=prefix to obtain csv of whether models are dominated
#> 
#> == Performance across all periods ==
#> What is the range of non-dominated performance (RNDP) across all periods?
#> Is it large - is changing datasets causing problems?
#>  Catchment value.min value.max     RNDP
#>     Cotter 0.6733349 0.7838178 0.110483
#> 
#> == Performance in each period ==
#> What is the RNDP in each period?
#> Is it low even though total RNDP is high? Why?
#>  Is there reason to believe the objective function is not comparable over time?
#>  Catchment sim.period value.min value.max       RNDP
#>     Cotter         P1 0.7043730 0.7838178 0.07944486
#>     Cotter         P2 0.6733349 0.7614177 0.08808285
#> 
#> == Worst non-dominated models in each period ==
#> Do any non-dominated models have unacceptable performance?
#> Which non-dominated model has the worst performance in each period? Why?
#>  Is it consistently the same dataset? Is there reason for that dataset to be problematic?
#>  Is it consistently the same model structure? Should another model structure have been selected?
#>  Is it consistently the same calibration objective function? Is it overfitting part of the record?
#> 
#>  Cotter 
#>  sim.period worst.performance calib.period Model.str Cal.objfn        P1
#>          P1         0.7043730           P2 cwi expuh   unknown 0.7043730
#>          P2         0.6733349           P1 cwi expuh   unknown 0.7838178
#>         P2
#>  0.7614177
#>  0.6733349
#> 
#> == Variation in inferred internal behaviour - Range of non-dominated parameters ==
#> Is the range of parameters of non-dominated models large for any single model structure?
#> Does the difference in performance correspond to different internal model behaviour?
#> 
#>  Catchment Model.str variable   value.mean    value.min    value.max  range
#>     Cotter cwi expuh       tw 70.079841318 59.078470550 81.081212085 22.003
#>     Cotter cwi expuh        f  2.465770963  2.189511638  2.742030287  0.553
#>     Cotter cwi expuh    scale  0.001281642  0.001277323  0.001285962  0.000
#>     Cotter cwi expuh        l  0.000000000  0.000000000  0.000000000  0.000
#>     Cotter cwi expuh        p  1.000000000  1.000000000  1.000000000  0.000
#>     Cotter cwi expuh    t_ref 20.000000000 20.000000000 20.000000000  0.000
#>     Cotter cwi expuh    tau_s 14.818779726 11.550794573 18.086764880  6.536
#>     Cotter cwi expuh      v_s  0.885253120  0.880118594  0.890387646  0.010
#>  range.as.%.of.mean
#>               31.40
#>               22.43
#>                0.00
#>                 NaN
#>                0.00
#>                0.00
#>               44.11
#>                1.13
#> 
#> == Performance with other statistics ==
#> Use the set of non-dominated models as an ensemble to predict a quantity of interest
#> Is the model unacceptable in any period? Is the uncertainty too large?
#>                variable sim.period       min       max      range
#>  q90.in.units.of.runoff         P1 1.8186625 2.2699434 0.45128086
#>  q90.in.units.of.runoff         P2 1.8974348 2.3766883 0.47925350
#>                r.sq.log         P1 0.7349019 0.7629356 0.02803376
#>                r.sq.log         P2 0.7761833 0.8464667 0.07028340