In this blog, I will continue to showcase the functionality of the exploratory modelling workbench. In the previous blog, I have given a general introduction to the workbench, and showed how the Direct Policy Search example that comes with Rhodium can be adapted for use with the workbench. In this blog post, I will showcase how the workbench can be used for open exploration.

## first a short background

In exploratory modeling, we are interested in understanding how regions in the uncertainty space and/or the decision space map to the whole outcome space, or partitions thereof. There are two general approaches for investigating this mapping. The first one is through systematic sampling of the uncertainty or decision space. This is sometimes also known as open exploration. The second one is to search through the space in a directed manner using some type of optimization approach. This is sometimes also known as directed search.

The workbench support both open exploration and directed search. Both can be applied to investigate the mapping of the uncertainty space and/or the decision space to the outcome space. In most applications, search is used for finding promising mappings from the decision space to the outcome space, while exploration is used to stress test these mappings under a whole range of possible resolutions to the various uncertainties. This need not be the case however. Optimization can be used to discover the worst possible scenario, while sampling can be used to get insight into the sensitivity of outcomes to the various decision levers.

## open exploration

To showcase the open exploration functionality, let’s start with a basic example using the DPS lake problem also used in the previous blog post. We are going to simultaneously sample over uncertainties and decision levers. We are going to generate 1000 scenarios and 5 policies, and see how they jointly affect the outcomes. A *scenario* is understood as a point in the uncertainty space, while a *policy* is a point in the decision space. The combination of a scenario and a policy is called *experiment*. The uncertainty space is spanned by uncertainties, while the decision space is spanned by levers. Both uncertainties and levers are instances of *RealParameter* (a continuous range), *IntegerParameter* (a range of integers), or *CategoricalParameter* (an unorder set of things). By default, the workbench will use Latin Hypercube sampling for generating both the scenarios and the policies. Each policy will be always evaluated over all scenarios (i.e. a full factorial over scenarios and policies).

from ema_workbench import (RealParameter, ScalarOutcome, Constant, ReplicatorModel) model = ReplicatorModel('lakeproblem', function=lake_model) model.replications = 150 #specify uncertainties model.uncertainties = [RealParameter('b', 0.1, 0.45), RealParameter('q', 2.0, 4.5), RealParameter('mean', 0.01, 0.05), RealParameter('stdev', 0.001, 0.005), RealParameter('delta', 0.93, 0.99)] # set levers model.levers = [RealParameter("c1", -2, 2), RealParameter("c2", -2, 2), RealParameter("r1", 0, 2), RealParameter("r2", 0, 2), RealParameter("w1", 0, 1)] def process_p(values): values = np.asarray(values) values = np.mean(values, axis=0) return np.max(values) #specify outcomes model.outcomes = [ScalarOutcome('max_P', kind=ScalarOutcome.MINIMIZE, function=process_p), ScalarOutcome('utility', kind=ScalarOutcome.MAXIMIZE, function=np.mean), ScalarOutcome('inertia', kind=ScalarOutcome.MINIMIZE, function=np.mean), ScalarOutcome('reliability', kind=ScalarOutcome.MAXIMIZE, function=np.mean)] # override some of the defaults of the model model.constants = [Constant('alpha', 0.41), Constant('steps', 100)]

Next, we can perform experiments with this model.

from ema_workbench import (MultiprocessingEvaluator, ema_logging, perform_experiments) ema_logging.log_to_stderr(ema_logging.INFO) with MultiprocessingEvaluator(model) as evaluator: results = evaluator.perform_experiments(scenarios=1000, policies=5)

### Visual analysis

Having generated these results, the next step is to analyze them and see what we can learn from the results. The workbench comes with a variety of techniques for this analysis. A simple first step is to make a few quick visualizations of the results. The workbench has convenience functions for this, but it also possible to create your own visualizations using the scientific Python stack.

from ema_workbench.analysis import pairs_plotting fig, axes = pairs_plotting.pairs_scatter(results, group_by='policy', legend=False) plt.show()

Writing your own visualizations requires a more in-depth understanding of how the results from the workbench are structured. `perform_experiments`

returns a tuple. The first item is a numpy structured array where each row is a single experiment. The second item contains the outcomes, structured in a dict with the name of the outcome as key and a numpy array as value. Experiments and outcomes are aligned based on index.

import seaborn as sns experiments, outcomes = results df = pd.DataFrame.from_dict(outcomes) df = df.assign(policy=experiments['policy']) # rename the policies using numbers df['policy'] = df['policy'].map({p:i for i, p in enumerate(set(experiments['policy']))}) # use seaborn to plot the dataframe grid = sns.pairplot(df, hue='policy', vars=outcomes.keys()) ax = plt.gca() plt.show()

Often, it is convenient to separate the process of performing the experiments from the analysis. To make this possible, the workbench offers convenience functions for storing results to disc and loading them from disc. The workbench will store the results in a tarbal with .csv files and separate metadata files. This is a convenient format that has proven sufficient over the years.

from ema_workbench import save_results save_results(results, '1000 scenarios 5 policies.tar.gz') from ema_workbench import load_results results = load_results('1000 scenarios 5 policies.tar.gz')

## advanced analysis

In addition to visual analysis, the workbench comes with a variety of techniques to perform a more in-depth analysis of the results. In addition, other analyses can simply be performed by utilizing the scientific python stack. The workbench comes with

- Scenario Discovery, a model driven approach to scenario development.
- Dimensional stacking, a quick visual approach drawing on feature scoring to enable scenario discovery. This approach has received limited attention in the literature (Suzuki et al., 2015). The implementation in the workbench replaces the rule mining approach with a feature scoring approach.
- Feature Scoring, a poor man’s alternative to global sensitivity analysis
- Regional sensitivity analysis

### Scenario Discovery

A detailed discussion on scenario discovery can be found in an earlier blogpost. For completeness, I provide a code snippet here. Compared to the previous blog post, there is one small change. The library mpld3 is currently not being maintained and broken on Python 3.5 and higher. To still utilize the interactive exploration of the trade offs within the notebook, use the interactive back-end as shown below.

from ema_workbench.analysis import prim experiments, outcomes = results x = experiments y = outcomes['max_P'] <0.8 prim_alg = prim.Prim(x, y, threshold=0.8) box1 = prim_alg.find_box()

%matplotlib notebook box1.show_tradeoff() plt.show()

%matplotlib inline # we go back to default not interactive box1.inspect(43) box1.inspect(43, style='graph') plt.show()

### dimensional stacking

Dimensional stacking was suggested as a more visual approach to scenario discovery. It involves two steps: identifying the most important uncertainties that affect system behavior, and creating a pivot table using the most influential uncertainties. Creating the pivot table involves binning the uncertainties. More details can be found in Suzuki et al. (2015) or by looking through the code in the workbench. Compared to the original paper, I use feature scoring for determining the most influential uncertainties. The code is set up in a modular way so other approaches to global sensitivity analysis can easily be used as well if so desired.

from ema_workbench.analysis import dimensional_stacking x = experiments y = outcomes['max_P'] <0.8 dimensional_stacking.create_pivot_plot(x,y, 2, nbins=3) plt.show()

We can see from this visual that if B is low, while Q is high, we have a high concentration of cases where pollution stays below 0.8. The mean and delta have some limited additional influence. By playing around with an alternative number of bins, or different number of layers, patterns can be coarsened or refined.

### regional sensitivity analysis

A third approach for supporting scenario discovery is to perform a regional sensitivity analysis. The workbench implements a visual approach based on plotting the empirical CDF given a classification vector. Please look at section 3.4 in Pianosi et al (2016) for more details.

from ema_workbench.analysis import regional_sa from numpy.lib import recfunctions as rf x = rf.drop_fields(experiments, 'model', asrecarray=True) y = outcomes['max_P'] < 0.8 regional_sa.plot_cdfs(x,y) plt.show()

### feature scoring

Feature scoring is a family of techniques often used in machine learning to identify the most relevant features to include in a model. This is similar to one of the use cases for global sensitivity analysis, namely factor prioritisation. In some of the work ongoing in Delft, we are comparing feature scoring with Sobol and Morris and the results are quite positive. The main advantage of feature scoring techniques is that they impose virtually no constraints on the experimental design, while they can handle real valued, integer valued, and categorical valued parameters. The workbench supports multiple techniques, the most useful of which generally is extra trees (Geurts et al. 2006).

For this example, we run feature scoring for each outcome of interest. We can also run it for a specific outcome if desired. Similarly, we can choose if we want to run in regression mode or classification mode. The later is applicable if the outcome is a categorical variable and the results should be interpreted similar to regional sensitivity analysis results. For more details, see the documentation.

from ema_workbench.analysis import feature_scoring x = experiments y = outcomes fs = feature_scoring.get_feature_scores_all(x, y) sns.heatmap(fs, cmap='viridis', annot=True) plt.show()

From the results, we see that max_P is primarily influenced by b, while utility is driven by delta, for inertia and reliability the situation is a little bit less clear cut.

### linear regression

In addition to the prepackaged analyses that come with the workbench, it is also easy to rig up something quickly using the ever expanding scientific Python stack. Below is a quick example of performing a basic regression analysis on the results.

experiments, outcomes = results for key, value in outcomes.items(): params = model.uncertainties #+ model.levers[:] fig, axes = plt.subplots(ncols=len(params), sharey=True) y = value for i, param in enumerate(params): ax = axes[i] ax.set_xlabel(param.name) pearson = sp.stats.pearsonr(experiments[param.name], y) ax.annotate("r: {:6.3f}".format(pearson[0]), xy=(0.15, 0.85), xycoords='axes fraction',fontsize=13) x = experiments[param.name] sns.regplot(x, y, ax=ax, ci=None, color='k', scatter_kws={'alpha':0.2, 's':8, 'color':'gray'}) ax.set_xlim(param.lower_bound, param.upper_bound) axes[0].set_ylabel(key) plt.show()

## More advanced sampling techniques

The workbench can also be used for more advanced sampling techniques. To achieve this, it relies on SALib. On the workbench side, the only change is to specify the sampler we want to use. Next, we can use SALib directly to perform the analysis. To help with this, the workbench provides a convenience function for generating the problem dict which SALib provides. The example below focusses on performing SOBOL on the uncertainties, but we could do the exact same thing with the levers instead. The only changes required would be to set `lever_sampling`

instead of `uncertainty_sampling`

, and get the SALib problem dict based on the levers.

from SALib.analyze import sobol from ema_workbench.em_framework.salib_samplers import get_SALib_problem with MultiprocessingEvaluator(model) as evaluator: sa_results = evaluator.perform_experiments(scenarios=1000, uncertainty_sampling='sobol') experiments, outcomes = sa_results problem = get_SALib_problem(model.uncertainties) Si = sobol.analyze(problem, outcomes['max_P'], calc_second_order=True, print_to_console=False) Si_filter = {k:Si[k] for k in ['ST','ST_conf','S1','S1_conf']} Si_df = pd.DataFrame(Si_filter, index=problem['names'])