[1]:
import emat
from emat.util.loggers import timing_log
emat.versions()
emat 0.5.2, plotly 4.14.3

Meta-Model Creation

To demostrate the creation of a meta-model, we will use the Road Test example model included with TMIP-EMAT. We will first create and run a design of experiments, to have some experimental data to define the meta-model.

[2]:
import emat.examples
scope, db, model = emat.examples.road_test()
design = model.design_experiments(design_name='lhs')
results = model.run_experiments(design)

We can then create a meta-model automatically from these experiments.

[3]:
from emat.model import create_metamodel

with timing_log("create metamodel"):
    mm = create_metamodel(scope, results, suppress_converge_warnings=True)
<TIME BEGINS> create metamodel
< TIME ENDS > create metamodel <18.05s>

If you are using the default meta-model regressor, as we are doing here, you can directly access a cross-validation method that uses the experimental data to evaluate the quality of the regression model. The cross_val_scores provides a measure of how well the meta-model predicts the experimental outcomes, similar to an R^2 measure on a linear regression model.

[4]:
with timing_log("crossvalidate metamodel"):
    display(mm.cross_val_scores())
<TIME BEGINS> crossvalidate metamodel
Cross Validation Score
no_build_travel_time 0.9946
build_travel_time 0.9846
time_savings 0.9242
value_of_time_savings 0.8345
net_benefits 0.6746
cost_of_capacity_expansion 0.8952
present_cost_expansion 0.9445
< TIME ENDS > crossvalidate metamodel <14.22s>

We can apply the meta-model directly on a new design of experiments, and use the contrast_experiments visualization tool to review how well the meta-model is replicating the underlying model’s results.

[5]:
design2 = mm.design_experiments(design_name='lhs_meta', n_samples=10_000)
[6]:
with timing_log("apply metamodel"):
    results2 = mm.run_experiments(design2)
<TIME BEGINS> apply metamodel
< TIME ENDS > apply metamodel <0.18s>
[7]:
results2.info()
<class 'emat.experiment.experimental_design.ExperimentalDesign'>
Int64Index: 10000 entries, 0 to 9999
Data columns (total 20 columns):
 #   Column                      Non-Null Count  Dtype
---  ------                      --------------  -----
 0   alpha                       10000 non-null  float64
 1   amortization_period         10000 non-null  int64
 2   beta                        10000 non-null  float64
 3   debt_type                   10000 non-null  category
 4   expand_capacity             10000 non-null  float64
 5   input_flow                  10000 non-null  int64
 6   interest_rate               10000 non-null  float64
 7   interest_rate_lock          10000 non-null  bool
 8   unit_cost_expansion         10000 non-null  float64
 9   value_of_time               10000 non-null  float64
 10  yield_curve                 10000 non-null  float64
 11  free_flow_time              10000 non-null  int64
 12  initial_capacity            10000 non-null  int64
 13  no_build_travel_time        10000 non-null  float64
 14  build_travel_time           10000 non-null  float64
 15  time_savings                10000 non-null  float64
 16  value_of_time_savings       10000 non-null  float64
 17  net_benefits                10000 non-null  float64
 18  cost_of_capacity_expansion  10000 non-null  float64
 19  present_cost_expansion      10000 non-null  float64
dtypes: bool(1), category(1), float64(14), int64(4)
memory usage: 1.5 MB
[8]:
from emat.analysis import contrast_experiments
contrast_experiments(mm.scope, results2, results)

No Build Time

Build Time

Time Savings

Value Time Save

Net Benefits

Cost of Expand

Present Cost

Design Sizing

An important advantage of using a Latin hypercube design of experiments, as is the default in TMIP-EMAT, is that the required number of experimental runs is not directly dependent on the dimensionality of the input variables and, importantly, does not grow exponentially with the number of dimensions. With a factorial or grid-based design, the number of experiments required does expand exponentially with the number of input dimensions.

Practical experience across multiple domains has led to a “rule of thumb” that good results for prediction (i.e., for the creation of a meta-model) can usually be obtained from 10 experimental data points per input variable dimension (Loeppky et al., 2009). This can still lead to a sizable number of experiments that need to be undertaken using a core model, if there are a large number of uncertainties and policy levers to consider. When defining an exploratory scope, which defines the number and definition of uncertainties and policy levers, analysts will want to carefully consider the tradeoffs between the number of experiments to be run, the resources (i.e. runtime) needed for each core model experiment, and the detail needed to adequately describe and model both uncertainties and policy levers in the context of the overall policy goals being considered.

If the underlying core model is generally well behaved, and has reasonable and proportional performance measure responses to changes in input values, then it may be possible to successfully develop a good-fitting meta-model with fewer than 10 experiments per input dimension. Unfortunately, for all but the most basic models it is quite difficult to ascertain a priori whether the core model will in fact be well behaved, and general “rules of thumb” are not available for this situation. Ultimately, if the dimensionality of the inputs cannot be reduced but a project cannot afford to undertake 10 model experiments per dimension, the best course is to specifically develop a design of experiments sized to the available resource budget. This can be done using the n_samples arguments of the design_experiments function, which allows for creating a design of any particular size. It is preferable to intentionally create this smaller design than to simply run a subset of a larger Latin Hypercube design, as smaller design will provide better coverage across the uncertainty space.

Whether a design of experiments is sized with 10 experiments per input variable, or fewer, it remains important to conduct a (cross-)validation for all meta-modeled performance measures, to ensure that meta-models are performing well. The 10x approach is a “rule of thumb” and not a guarantee that enough experimental data will be generated to develop a high quality meta-model in any given circumstance. Indeed, the guidance from Loeppky et al. is not simply that 10x is enough, but rather that either (a) 10x will be sufficient or (b) the number of experiments that will actually be sufficient is at least an order of magnitude larger.

Performance measures that are poorly behaved or depend on complex interactions across multiple inputs can give poor results even with a larger number of experiments. For example, in the Road Test meta-model shown above, the “net benefits” performance measure exhibits a heteroskedastic response to “expand amount” and also rare high-value outliers that are dependent on at least 3 different inputs aligning (input flow, value of time, and expand amount). In combination, these conditions make it difficult to properly meta-model this performance measure, at least when using the automatic meta-modeling tools provided by TMIP-EMAT. The low cross-validation score reported for this performance measure reflects this, and serves as a warning to analysts that the meta-model may be unreliable for this performance measure.

Partial Metamodels

It may be desirable in some cases to construct a partial metamodel, covering only a subset of the performance measures. This is likely to be particularly desirable if some of the performance measures are poorly fit by the meta-model as noted above, or if a large number of performance measures are included in the scope, but only a few are of particular interest for a given analysis. The time required for generating and using meta-models is linear in the number of performance measures included, so if you have 100 performance measures but you are only presently interested in 5, your meta-model can be created much faster if you only include the 5 performance measures. It will also run much faster, but the run time for metamodels is so small anyhow, it’s likely you won’t notice.

To create a partial meta-model for a curated set of performance measures, you can use the include_measures argument of the create_metamodel command.

[9]:
with timing_log("create limited metamodel"):
    mm2 = create_metamodel(
        scope, results,
        include_measures=['time_savings', 'present_cost_expansion'],
        suppress_converge_warnings=True,
    )

with timing_log("crossvalidate limited metamodel"):
    display(mm2.cross_val_scores())

with timing_log("apply limited metamodel"):
    results2_limited = mm2.run_experiments(design2)

results2_limited.info()
<TIME BEGINS> create limited metamodel
< TIME ENDS > create limited metamodel <4.30s>
<TIME BEGINS> crossvalidate limited metamodel
Cross Validation Score
time_savings 0.8847
present_cost_expansion 0.8987
< TIME ENDS > crossvalidate limited metamodel <5.17s>
<TIME BEGINS> apply limited metamodel
< TIME ENDS > apply limited metamodel <0.07s>
<class 'emat.experiment.experimental_design.ExperimentalDesign'>
Int64Index: 10000 entries, 0 to 9999
Data columns (total 15 columns):
 #   Column                  Non-Null Count  Dtype
---  ------                  --------------  -----
 0   alpha                   10000 non-null  float64
 1   amortization_period     10000 non-null  int64
 2   beta                    10000 non-null  float64
 3   debt_type               10000 non-null  category
 4   expand_capacity         10000 non-null  float64
 5   input_flow              10000 non-null  int64
 6   interest_rate           10000 non-null  float64
 7   interest_rate_lock      10000 non-null  bool
 8   unit_cost_expansion     10000 non-null  float64
 9   value_of_time           10000 non-null  float64
 10  yield_curve             10000 non-null  float64
 11  free_flow_time          10000 non-null  int64
 12  initial_capacity        10000 non-null  int64
 13  time_savings            10000 non-null  float64
 14  present_cost_expansion  10000 non-null  float64
dtypes: bool(1), category(1), float64(9), int64(4)
memory usage: 1.1 MB

There is also an exclude_measures argument for the create_metamodel command, which will retain all of the scoped performance measures except the enumerated list. This can be handy for dropping a few measures that are not working well, either because the data is bad in some way or if the measure isn’t well fitted using the metamodel.

[10]:
with timing_log("create limited metamodel"):
    mm3 = create_metamodel(
        scope, results,
        exclude_measures=['net_benefits'],
        suppress_converge_warnings=True,
    )

with timing_log("crossvalidate limited metamodel"):
    display(mm3.cross_val_scores())

with timing_log("apply limited metamodel"):
    results3_limited = mm3.run_experiments(design2)

results3_limited.info()
<TIME BEGINS> create limited metamodel
< TIME ENDS > create limited metamodel <14.58s>
<TIME BEGINS> crossvalidate limited metamodel
Cross Validation Score
no_build_travel_time 0.9917
build_travel_time 0.9774
time_savings 0.9113
value_of_time_savings 0.8366
cost_of_capacity_expansion 0.9110
present_cost_expansion 0.9415
< TIME ENDS > crossvalidate limited metamodel <12.07s>
<TIME BEGINS> apply limited metamodel
< TIME ENDS > apply limited metamodel <0.14s>
<class 'emat.experiment.experimental_design.ExperimentalDesign'>
Int64Index: 10000 entries, 0 to 9999
Data columns (total 19 columns):
 #   Column                      Non-Null Count  Dtype
---  ------                      --------------  -----
 0   alpha                       10000 non-null  float64
 1   amortization_period         10000 non-null  int64
 2   beta                        10000 non-null  float64
 3   debt_type                   10000 non-null  category
 4   expand_capacity             10000 non-null  float64
 5   input_flow                  10000 non-null  int64
 6   interest_rate               10000 non-null  float64
 7   interest_rate_lock          10000 non-null  bool
 8   unit_cost_expansion         10000 non-null  float64
 9   value_of_time               10000 non-null  float64
 10  yield_curve                 10000 non-null  float64
 11  free_flow_time              10000 non-null  int64
 12  initial_capacity            10000 non-null  int64
 13  no_build_travel_time        10000 non-null  float64
 14  build_travel_time           10000 non-null  float64
 15  time_savings                10000 non-null  float64
 16  value_of_time_savings       10000 non-null  float64
 17  cost_of_capacity_expansion  10000 non-null  float64
 18  present_cost_expansion      10000 non-null  float64
dtypes: bool(1), category(1), float64(13), int64(4)
memory usage: 1.4 MB