Sensitivity analysis#
modelly.sensitivity says which parameters drive which outputs: screen many parameters cheaply,
then split an output’s variance between the few that matter. Every method takes a model (run
through the experiment executors), a plain function of the parameters, or an
emulator, and a ScenarioSpace whose ranges and choices are the inputs.
Function |
Measures |
Runs |
|---|---|---|
|
the output’s range as each parameter moves alone |
|
|
elementary effects: |
|
|
first-order |
|
|
how much each parameter changes the output’s distribution |
|
|
a Gaussian-process stand-in for an expensive model |
|
Runs of a model go to a Parquet store, so models need the experiments extra
(pip install "modelly-abm[experiments]"); functions and emulators do not.
A worked example: which epidemic parameters matter#
The SIR model of the calibration guide
(epidemic.py) has four parameters. Which ones decide how high the epidemic peaks?
from epidemic import Epidemic
from modelly.experiments import Range, ScenarioSpace
from modelly.sensitivity import morris, sobol
space = ScenarioSpace(
beta=(0.1, 0.8),
gamma=(0.05, 0.3),
initial=(1, 30),
population=Range(500, 2000, integer=True),
)
common = {
"output": "infected",
"statistic": "max",
"steps": 60,
"replicates": 2,
"seed": 1,
}
screen = morris(Epidemic, space, trajectories=20, **common)
print(screen.ranking().round(3))
mu mu_star sigma
gamma -464.850 465.075 408.749
beta 433.800 462.000 370.234
population 408.075 408.975 400.663
initial 85.575 94.575 203.890
output="infected" records the number infected every step and statistic="max" keeps each
run’s peak; the two replicates are averaged. mu_star, the mean absolute effect of moving a
parameter across a step of its grid, ranks the parameters; a large sigma next to it means
the effect depends on where the other parameters are (interactions, non-linearity). The peak
is a head count, so it scales with the population; the size of the first outbreak matters much
less. Recovery and infection rates matter most, in opposite directions (mu is negative for
gamma).
With the population and the first cases fixed, Sobol indices split the peak’s variance between the two rates:
narrowed = ScenarioSpace(
beta=(0.1, 0.8), gamma=(0.05, 0.3), initial=10, population=1000
)
indices = sobol(Epidemic, narrowed, samples=256, **common)
print(indices.ranking().round(3))
indices.plot().savefig("sobol.png")
S1 ST S1_low S1_high ST_low ST_high
beta 0.544 0.576 0.469 0.612 0.495 0.687
gamma 0.423 0.458 0.325 0.514 0.387 0.540
beta alone explains about half the variance, gamma alone about 40 %; ST - S1, the share
that comes through interactions, is small for both. The intervals come from 200 bootstrap
resamples; more samples narrow them.
One at a time#
from modelly.sensitivity import oat
result = oat(Epidemic, space, levels=5, **common)
result.table # low, high, effect per parameter
result.sweeps # every evaluated point: parameter, position, value, output
Each parameter takes levels evenly spaced values (every value of a choice) while the others
stay at the middle of their ranges. It is the cheapest look and blind to interactions.
Morris elementary effects#
morris(model, space, output, trajectories=10, levels=4, ...) draws trajectories random
paths on a grid of levels values per parameter, each moving one parameter at a time by
levels / (2 * (levels - 1)) of its range. The elementary effect of a move is the change in the
output divided by the step; mu is their mean, mu_star the mean of their absolute values and
sigma their standard deviation. plot() draws sigma against mu_star.
Sobol indices#
sobol(model, space, output, samples=1024, bootstrap=200, confidence=0.95, estimator="jansen")
uses Saltelli sampling: two matrices A and B from one scrambled Sobol sequence
(scipy.stats.qmc, power-of-two samples), and for every parameter the matrix A with that
column from B.
|
|
|
|---|---|---|
|
|
|
|
|
|
The test suite checks both on the Ishigami function and the Sobol G function, whose indices are known exactly: every analytical value lies inside its bootstrap interval.
PAWN#
pawn(model, space, output, samples=500, slices=10, summary="median") evaluates one Latin
hypercube, cuts each parameter’s range into slices equal-count slices and compares the
output’s distribution in each slice with the unconditional one by the Kolmogorov-Smirnov
statistic. The table gives the median, mean and max over the slices; 0 means the
parameter does not change the output’s distribution. Unlike variance-based indices, PAWN does
not assume the variance describes the output well, which matters for skewed or multi-modal
outputs (extinction or outbreak, boom or bust).
Emulators for expensive models#
from modelly.sensitivity import emulate
surrogate = emulate(Epidemic, narrowed, samples=80, **common)
surrogate.r2 # share of variance explained on held-out runs: 0.993 here
sobol(surrogate, narrowed, samples=4096) # no further model runs
emulate runs the model at a Latin hypercube of samples points and fits a Gaussian process
(the basic numpy and scipy one of modelly.calibrate.gp) to the output. The emulator then
stands in for the model in every method. Check r2 before trusting it: a low value means the
process does not capture the output, and more samples or a narrower space are needed.
Results#
Every function returns a result with:
table: one row per parameter;ranking(): the table sorted by the main measure (effect,mu_star,STor the PAWN summary), most influential first;to_dataframe(): the table with the parameter as a column, ready for CSV or Parquet;plot(): a Matplotlib figure;info: the number of evaluations and the settings.
Settings, seeds and executors#
seed, steps, replicates, statistic ("last", "max", "min", "mean", "sum" or a
function of the series), store, executor, max_parallel and options (the executor’s) are
accepted by every function. The designs and the bootstrap are drawn from the seed, and every
run’s seed comes from it as in an experiment, so the indices are the same on every executor and
with any number of workers.
From the command line#
[experiment]
model = "epidemic.py:Epidemic" # a Model class, or module:function of the parameters
steps = 60
replicates = 2
seed = 1
outputs = ["infected"] # the first output is analysed
[space]
beta = { min = 0.1, max = 0.8 }
gamma = { min = 0.05, max = 0.3 }
initial = { min = 1, max = 30 }
population = 1000
[sensitivity]
method = "morris" # oat, morris, sobol, pawn
statistic = "max"
trajectories = 20 # the method's settings
[run]
executor = "processes"
modelly sensitivity study.toml
modelly sensitivity study.toml --method pawn --executor threads
modelly sensitivity study.toml --output ranking.csv
It prints the ranking. Exit status: 0 when the analysis finished, 1 when a model run failed,
2 for a bad command line or spec. python -m modelly.sensitivity is the same command.