simdec 1.0.0__tar.gz → 1.2.0.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simdec-1.0.0 → simdec-1.2.0.dev0}/.gitignore +4 -0
- {simdec-1.0.0 → simdec-1.2.0.dev0}/CONTRIBUTING.md +36 -0
- {simdec-1.0.0 → simdec-1.2.0.dev0}/PKG-INFO +28 -19
- {simdec-1.0.0 → simdec-1.2.0.dev0}/README.md +13 -12
- {simdec-1.0.0 → simdec-1.2.0.dev0}/pyproject.toml +16 -9
- simdec-1.2.0.dev0/src/simdec/__init__.py +13 -0
- simdec-1.2.0.dev0/src/simdec/auth.py +17 -0
- simdec-1.2.0.dev0/src/simdec/decomposition.py +190 -0
- simdec-1.0.0/src/simdec/significance.py → simdec-1.2.0.dev0/src/simdec/sensitivity_indices.py +16 -17
- simdec-1.2.0.dev0/src/simdec/visualization.py +263 -0
- simdec-1.0.0/Makefile +0 -25
- simdec-1.0.0/app.py +0 -96
- simdec-1.0.0/src/simdec/__init__.py +0 -6
- simdec-1.0.0/src/simdec/decomposition.py +0 -113
- simdec-1.0.0/src/simdec/visualization.py +0 -168
- simdec-1.0.0/src/simdec/workflow.py +0 -57
- {simdec-1.0.0 → simdec-1.2.0.dev0}/CODE_OF_CONDUCT.md +0 -0
- {simdec-1.0.0 → simdec-1.2.0.dev0}/LICENSE.txt +0 -0
|
@@ -161,3 +161,39 @@ a release:
|
|
|
161
161
|
5. Check that release has been deployed to PyPI
|
|
162
162
|
6. Check documentation is built and deployed to readthedocs
|
|
163
163
|
7. Check that auto-generated PR is auto-merged on the conda-forge feedstock repo
|
|
164
|
+
|
|
165
|
+
## Dashboard
|
|
166
|
+
A live dashboard is available at:
|
|
167
|
+
|
|
168
|
+
https://simdec.io
|
|
169
|
+
|
|
170
|
+
The DNS records are available on CPanel and the rest is hosted on Google Cloud
|
|
171
|
+
Platform.
|
|
172
|
+
|
|
173
|
+
Developing locally requires the installation of GCP CLI and Docker engine.
|
|
174
|
+
|
|
175
|
+
A few helper commands are provided in the Makefile.
|
|
176
|
+
|
|
177
|
+
### Local use
|
|
178
|
+
|
|
179
|
+
The dashboard can be run locally using:
|
|
180
|
+
|
|
181
|
+
make serve-dev
|
|
182
|
+
|
|
183
|
+
If you want to test OAuth, you need to export the following env variables:
|
|
184
|
+
|
|
185
|
+
export PANEL_OAUTH_REDIRECT_URI=http://localhost:5006/app
|
|
186
|
+
export PANEL_OAUTH_KEY=[VALUE IN GCP Secret Manager]
|
|
187
|
+
export PANEL_OAUTH_SECRET=[VALUE IN GCP Secret Manager]
|
|
188
|
+
export PANEL_OAUTH_ENCRYPTION=[VALUE IN GCP Secret Manager]
|
|
189
|
+
|
|
190
|
+
Use the CLI tool `direnv` for convenience. Then you can serve with
|
|
191
|
+
OAuth support:
|
|
192
|
+
|
|
193
|
+
make serve-oauth
|
|
194
|
+
|
|
195
|
+
### Deployment
|
|
196
|
+
|
|
197
|
+
New version can either be deployed from the CI with `cloudbuild` or locally:
|
|
198
|
+
|
|
199
|
+
make production
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: simdec
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.2.0.dev0
|
|
4
4
|
Summary: Sensitivity analysis using simulation decomposition
|
|
5
5
|
Project-URL: homepage, https://www.simdec.fi/
|
|
6
6
|
Project-URL: documentation, https://simdec.readthedocs.io
|
|
@@ -16,20 +16,28 @@ Classifier: License :: OSI Approved :: BSD License
|
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
17
17
|
Classifier: Programming Language :: Python
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
-
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
23
|
Requires-Dist: numpy
|
|
24
24
|
Requires-Dist: pandas
|
|
25
|
-
Requires-Dist: panel
|
|
26
25
|
Requires-Dist: salib
|
|
27
26
|
Requires-Dist: seaborn
|
|
28
|
-
|
|
27
|
+
Provides-Extra: dashboard
|
|
28
|
+
Requires-Dist: cryptography; extra == 'dashboard'
|
|
29
|
+
Requires-Dist: panel>=1.3.8; extra == 'dashboard'
|
|
29
30
|
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: accessible-pygments; extra == 'dev'
|
|
32
|
+
Requires-Dist: cryptography; extra == 'dev'
|
|
30
33
|
Requires-Dist: hatch; extra == 'dev'
|
|
34
|
+
Requires-Dist: numpydoc; extra == 'dev'
|
|
35
|
+
Requires-Dist: panel>=1.3.8; extra == 'dev'
|
|
31
36
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
32
|
-
Requires-Dist:
|
|
37
|
+
Requires-Dist: pydata-sphinx-theme; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
40
|
+
Requires-Dist: sphinx; extra == 'dev'
|
|
33
41
|
Provides-Extra: doc
|
|
34
42
|
Requires-Dist: accessible-pygments; extra == 'doc'
|
|
35
43
|
Requires-Dist: numpydoc; extra == 'doc'
|
|
@@ -56,7 +64,7 @@ When public
|
|
|
56
64
|
analysis method, which is based on Monte Carlo simulation. SimDec consists of
|
|
57
65
|
three major parts:
|
|
58
66
|
|
|
59
|
-
1. computing
|
|
67
|
+
1. computing sensitivity indices,
|
|
60
68
|
2. creating multi-variable scenarios and mapping the output values to them, and
|
|
61
69
|
3. visualizing the scenarios on the output distribution by color-coding its segments.
|
|
62
70
|
|
|
@@ -64,7 +72,15 @@ SimDec reveals the nature of causalities and interaction effects in the model.
|
|
|
64
72
|
See our [publications](https://www.simdec.fi/publications) and join our
|
|
65
73
|
[discord community](https://discord.gg/54SFcNsZS4).
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
## Python API
|
|
76
|
+
The library is distributed on PyPi and can be installed with:
|
|
77
|
+
|
|
78
|
+
pip install simdec
|
|
79
|
+
|
|
80
|
+
## Dashboard
|
|
81
|
+
A live dashboard is available at:
|
|
82
|
+
|
|
83
|
+
https://simdec.io
|
|
68
84
|
|
|
69
85
|
## Citations
|
|
70
86
|
|
|
@@ -76,13 +92,6 @@ Foundation, and Finnish Foundation for Economic Education.
|
|
|
76
92
|
If you use SimDec in your research we would appreciate a citation to the
|
|
77
93
|
following publications:
|
|
78
94
|
|
|
79
|
-
- Kozlova, M., & Yeomans, J. S. (2022). Monte Carlo Enhancement via Simulation
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (forthcoming).
|
|
83
|
-
Uncovering Heterogeneous Effects in Computational Models for Sustainable
|
|
84
|
-
Decision-making. _Environmental Modelling & Software_.
|
|
85
|
-
- Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (forthcoming).
|
|
86
|
-
SimDec algorithm. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis
|
|
87
|
-
for Business, Technology, and Policymaking Made Easy with Simulation
|
|
88
|
-
Decomposition_. Routledge.
|
|
95
|
+
- Kozlova, M., & Yeomans, J. S. (2022). Monte Carlo Enhancement via Simulation Decomposition: A “Must-Have” Inclusion for Many Disciplines. _INFORMS Transactions on Education, 22_(3), 147-159. [Available here](https://pubsonline.informs.org/doi/10.1287/ited.2019.0240).
|
|
96
|
+
- Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (2024). Uncovering Heterogeneous Effects in Computational Models for Sustainable Decision-making. _Environmental Modelling & Software_, 171, 105898. [https://doi.org/10.1016/j.envsoft.2023.105898](https://doi.org/10.1016/j.envsoft.2023.105898)
|
|
97
|
+
- Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (forthcoming). SimDec algorithm and guidelines for its usage and interpretation. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis for Business, Technology, and Policymaking. Made Easy with Simulation Decomposition_. Routledge.
|
|
@@ -14,7 +14,7 @@ When public
|
|
|
14
14
|
analysis method, which is based on Monte Carlo simulation. SimDec consists of
|
|
15
15
|
three major parts:
|
|
16
16
|
|
|
17
|
-
1. computing
|
|
17
|
+
1. computing sensitivity indices,
|
|
18
18
|
2. creating multi-variable scenarios and mapping the output values to them, and
|
|
19
19
|
3. visualizing the scenarios on the output distribution by color-coding its segments.
|
|
20
20
|
|
|
@@ -22,7 +22,15 @@ SimDec reveals the nature of causalities and interaction effects in the model.
|
|
|
22
22
|
See our [publications](https://www.simdec.fi/publications) and join our
|
|
23
23
|
[discord community](https://discord.gg/54SFcNsZS4).
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
## Python API
|
|
26
|
+
The library is distributed on PyPi and can be installed with:
|
|
27
|
+
|
|
28
|
+
pip install simdec
|
|
29
|
+
|
|
30
|
+
## Dashboard
|
|
31
|
+
A live dashboard is available at:
|
|
32
|
+
|
|
33
|
+
https://simdec.io
|
|
26
34
|
|
|
27
35
|
## Citations
|
|
28
36
|
|
|
@@ -34,13 +42,6 @@ Foundation, and Finnish Foundation for Economic Education.
|
|
|
34
42
|
If you use SimDec in your research we would appreciate a citation to the
|
|
35
43
|
following publications:
|
|
36
44
|
|
|
37
|
-
- Kozlova, M., & Yeomans, J. S. (2022). Monte Carlo Enhancement via Simulation
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (forthcoming).
|
|
41
|
-
Uncovering Heterogeneous Effects in Computational Models for Sustainable
|
|
42
|
-
Decision-making. _Environmental Modelling & Software_.
|
|
43
|
-
- Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (forthcoming).
|
|
44
|
-
SimDec algorithm. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis
|
|
45
|
-
for Business, Technology, and Policymaking Made Easy with Simulation
|
|
46
|
-
Decomposition_. Routledge.
|
|
45
|
+
- Kozlova, M., & Yeomans, J. S. (2022). Monte Carlo Enhancement via Simulation Decomposition: A “Must-Have” Inclusion for Many Disciplines. _INFORMS Transactions on Education, 22_(3), 147-159. [Available here](https://pubsonline.informs.org/doi/10.1287/ited.2019.0240).
|
|
46
|
+
- Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (2024). Uncovering Heterogeneous Effects in Computational Models for Sustainable Decision-making. _Environmental Modelling & Software_, 171, 105898. [https://doi.org/10.1016/j.envsoft.2023.105898](https://doi.org/10.1016/j.envsoft.2023.105898)
|
|
47
|
+
- Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (forthcoming). SimDec algorithm and guidelines for its usage and interpretation. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis for Business, Technology, and Policymaking. Made Easy with Simulation Decomposition_. Routledge.
|
|
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simdec"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0dev0"
|
|
8
8
|
description = "Sensitivity analysis using simulation decomposition"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = "BSD-3-Clause"
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Pamphile Roy" },
|
|
@@ -23,9 +23,9 @@ classifiers = [
|
|
|
23
23
|
"Operating System :: OS Independent",
|
|
24
24
|
"Programming Language :: Python",
|
|
25
25
|
"Programming Language :: Python :: 3",
|
|
26
|
-
"Programming Language :: Python :: 3.9",
|
|
27
26
|
"Programming Language :: Python :: 3.10",
|
|
28
27
|
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
dependencies = [
|
|
@@ -33,11 +33,14 @@ dependencies = [
|
|
|
33
33
|
"pandas",
|
|
34
34
|
"SALib",
|
|
35
35
|
"seaborn",
|
|
36
|
-
"typer[all]",
|
|
37
|
-
"panel",
|
|
38
36
|
]
|
|
39
37
|
|
|
40
38
|
[project.optional-dependencies]
|
|
39
|
+
dashboard = [
|
|
40
|
+
"panel>=1.3.8",
|
|
41
|
+
"cryptography",
|
|
42
|
+
]
|
|
43
|
+
|
|
41
44
|
test = [
|
|
42
45
|
"pytest",
|
|
43
46
|
"pytest-cov",
|
|
@@ -51,29 +54,33 @@ doc = [
|
|
|
51
54
|
]
|
|
52
55
|
|
|
53
56
|
dev = [
|
|
54
|
-
"simdec[doc,test]",
|
|
57
|
+
"simdec[doc,test,dashboard]",
|
|
55
58
|
"pre-commit",
|
|
56
59
|
"hatch",
|
|
57
60
|
]
|
|
58
61
|
|
|
59
|
-
[project.scripts]
|
|
60
|
-
simdec = "simdec.workflow:app"
|
|
61
|
-
|
|
62
62
|
[project.urls]
|
|
63
63
|
homepage = "https://www.simdec.fi/"
|
|
64
64
|
documentation = "https://simdec.readthedocs.io"
|
|
65
65
|
source = "https://github.com/Simulation-Decomposition/simdec-python"
|
|
66
66
|
|
|
67
67
|
[tool.hatch]
|
|
68
|
+
build.targets.wheel.packages = ["src/simdec"]
|
|
68
69
|
build.targets.sdist.exclude = [
|
|
69
70
|
".github",
|
|
70
71
|
"docs",
|
|
72
|
+
"panel",
|
|
71
73
|
"tests",
|
|
72
74
|
"*.rst",
|
|
73
75
|
"*.yml",
|
|
74
76
|
".*",
|
|
77
|
+
"Makefile",
|
|
78
|
+
"Dockerfile",
|
|
75
79
|
]
|
|
76
80
|
|
|
81
|
+
[project.entry-points."panel.auth"]
|
|
82
|
+
custom_google = "simdec.auth:CustomGoogleLoginHandler"
|
|
83
|
+
|
|
77
84
|
[tool.pytest.ini_options]
|
|
78
85
|
addopts = "--durations 10"
|
|
79
86
|
testpaths = [
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""SimDec main namespace."""
|
|
2
|
+
from simdec.decomposition import *
|
|
3
|
+
from simdec.sensitivity_indices import *
|
|
4
|
+
from simdec.visualization import *
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"sensitivity_indices",
|
|
8
|
+
"states_expansion",
|
|
9
|
+
"decomposition",
|
|
10
|
+
"visualization",
|
|
11
|
+
"tableau",
|
|
12
|
+
"palette",
|
|
13
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from panel.auth import GoogleLoginHandler
|
|
2
|
+
from panel.io.resources import CDN_DIST
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CustomGoogleLoginHandler(GoogleLoginHandler):
|
|
6
|
+
def _simple_get(self):
|
|
7
|
+
html = self._login_template.render(errormessage="", PANEL_CDN=CDN_DIST)
|
|
8
|
+
self.write(html)
|
|
9
|
+
|
|
10
|
+
async def get(self):
|
|
11
|
+
if "login" in self.request.uri and "state" not in self.request.uri:
|
|
12
|
+
self._simple_get()
|
|
13
|
+
else:
|
|
14
|
+
await super().get()
|
|
15
|
+
|
|
16
|
+
async def post(self):
|
|
17
|
+
await super().get()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from hashlib import blake2b
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from scipy import stats
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = ["decomposition", "states_expansion"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def states_expansion(states: list[int], inputs: pd.DataFrame) -> list[list[str]]:
|
|
16
|
+
"""Expand states list to fully represent all scenarios."""
|
|
17
|
+
inputs = pd.DataFrame(inputs)
|
|
18
|
+
expanded_states = []
|
|
19
|
+
for state in states:
|
|
20
|
+
if isinstance(state, int):
|
|
21
|
+
if state == 2:
|
|
22
|
+
expanded_states.append(["low", "high"])
|
|
23
|
+
elif state == 3:
|
|
24
|
+
expanded_states.append(["low", "medium", "high"])
|
|
25
|
+
else:
|
|
26
|
+
expanded_states.append(state)
|
|
27
|
+
|
|
28
|
+
# categorical for a given variable
|
|
29
|
+
cat_cols = inputs.select_dtypes(exclude=["number"])
|
|
30
|
+
cat_cols_idx = []
|
|
31
|
+
states_cats_ = []
|
|
32
|
+
for cat_col in cat_cols:
|
|
33
|
+
_, cats = pd.factorize(inputs[cat_col])
|
|
34
|
+
cat_cols_idx.append(inputs.columns.get_loc(cat_col))
|
|
35
|
+
states_cats_.append(cats)
|
|
36
|
+
|
|
37
|
+
for i, states_cat_ in zip(cat_cols_idx, states_cats_):
|
|
38
|
+
n_unique = np.unique(inputs.iloc[:, i]).size
|
|
39
|
+
expanded_states[i] = list(states_cat_) if n_unique < 5 else expanded_states[i]
|
|
40
|
+
|
|
41
|
+
return expanded_states
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DecompositionResult:
|
|
46
|
+
var_names: list[str]
|
|
47
|
+
statistic: np.ndarray
|
|
48
|
+
bins: pd.DataFrame
|
|
49
|
+
states: list[int]
|
|
50
|
+
bin_edges: np.ndarray
|
|
51
|
+
|
|
52
|
+
def __reduce__(self):
|
|
53
|
+
h = blake2b(key=b"result hashing", digest_size=20)
|
|
54
|
+
|
|
55
|
+
h.update(str(self.var_names).encode())
|
|
56
|
+
h.update(str(self.statistic).encode())
|
|
57
|
+
h.update(str(self.bins).encode())
|
|
58
|
+
h.update(str(self.states).encode())
|
|
59
|
+
h.update(str(self.bin_edges).encode())
|
|
60
|
+
|
|
61
|
+
return [h.hexdigest()]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def decomposition(
|
|
65
|
+
inputs: pd.DataFrame,
|
|
66
|
+
output: pd.DataFrame,
|
|
67
|
+
*,
|
|
68
|
+
sensitivity_indices: np.ndarray,
|
|
69
|
+
dec_limit: float = 1,
|
|
70
|
+
auto_ordering: bool = True,
|
|
71
|
+
states: list[int] | None = None,
|
|
72
|
+
statistic: Literal["mean", "median"] | None = "mean",
|
|
73
|
+
) -> DecompositionResult:
|
|
74
|
+
"""SimDec decomposition.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
inputs : DataFrame of shape (n_runs, n_factors)
|
|
79
|
+
Input variables.
|
|
80
|
+
output : DataFrame of shape (n_runs, 1)
|
|
81
|
+
Target variable.
|
|
82
|
+
sensitivity_indices : ndarray of shape (n_factors, 1)
|
|
83
|
+
Sensitivity indices, combined effect of each input.
|
|
84
|
+
dec_limit : float
|
|
85
|
+
Explained variance ratio to filter the number input variables.
|
|
86
|
+
auto_ordering : bool
|
|
87
|
+
Automatically order input columns based on the relative sensitivity_indices
|
|
88
|
+
or use the provided order.
|
|
89
|
+
states : list of int, optional
|
|
90
|
+
List of possible states for the considered parameter.
|
|
91
|
+
statistic : {"mean", "median"}, optional
|
|
92
|
+
Statistic to compute in each bin.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
res : DecompositionResult
|
|
97
|
+
An object with attributes:
|
|
98
|
+
|
|
99
|
+
var_names : list of string (n_factors, 1)
|
|
100
|
+
Variable names.
|
|
101
|
+
statistic : ndarray of shape (n_factors, 1)
|
|
102
|
+
Statistic in each bin.
|
|
103
|
+
bins : DataFrame
|
|
104
|
+
Multidimensional bins.
|
|
105
|
+
states : list of int
|
|
106
|
+
List of possible states for the considered parameter.
|
|
107
|
+
|
|
108
|
+
"""
|
|
109
|
+
var_names = inputs.columns
|
|
110
|
+
|
|
111
|
+
cat_cols = inputs.select_dtypes(exclude=["number"])
|
|
112
|
+
cat_cols_idx = []
|
|
113
|
+
for cat_col in cat_cols:
|
|
114
|
+
codes, cat_states_ = pd.factorize(inputs[cat_col])
|
|
115
|
+
inputs[cat_col] = codes
|
|
116
|
+
cat_cols_idx.append(inputs.columns.get_loc(cat_col))
|
|
117
|
+
|
|
118
|
+
inputs = inputs.to_numpy()
|
|
119
|
+
output = output.to_numpy()
|
|
120
|
+
|
|
121
|
+
# 1. variables for decomposition
|
|
122
|
+
var_order = np.argsort(sensitivity_indices)[::-1]
|
|
123
|
+
|
|
124
|
+
# only keep the explained variance corresponding to `dec_limit`
|
|
125
|
+
sensitivity_indices = sensitivity_indices[var_order]
|
|
126
|
+
|
|
127
|
+
if auto_ordering:
|
|
128
|
+
n_var_dec = np.where(np.cumsum(sensitivity_indices) < dec_limit)[0].size
|
|
129
|
+
n_var_dec = max(1, n_var_dec) # keep at least one variable
|
|
130
|
+
n_var_dec = min(5, n_var_dec) # use at most 5 variables
|
|
131
|
+
else:
|
|
132
|
+
n_var_dec = inputs.shape[1]
|
|
133
|
+
|
|
134
|
+
# 2. states formation
|
|
135
|
+
if states is None:
|
|
136
|
+
states = 3 if n_var_dec < 3 else 2
|
|
137
|
+
states = [states] * n_var_dec
|
|
138
|
+
|
|
139
|
+
# categorical for a given variable
|
|
140
|
+
for i in cat_cols_idx:
|
|
141
|
+
n_unique = np.unique(inputs[:, i]).size
|
|
142
|
+
states[i] = n_unique if n_unique < 5 else states[i]
|
|
143
|
+
|
|
144
|
+
if auto_ordering:
|
|
145
|
+
var_names = var_names[var_order[:n_var_dec]].tolist()
|
|
146
|
+
inputs = inputs[:, var_order[:n_var_dec]]
|
|
147
|
+
|
|
148
|
+
# 3. decomposition
|
|
149
|
+
bins = []
|
|
150
|
+
|
|
151
|
+
statistic_methods = {
|
|
152
|
+
"mean": np.mean,
|
|
153
|
+
"median": np.median,
|
|
154
|
+
}
|
|
155
|
+
try:
|
|
156
|
+
statistic_method = statistic_methods[statistic]
|
|
157
|
+
except IndexError:
|
|
158
|
+
msg = f"'statistic' must be one of {statistic_methods.values()}"
|
|
159
|
+
raise ValueError(msg)
|
|
160
|
+
|
|
161
|
+
def statistic_(inputs):
|
|
162
|
+
"""Custom function to keep track of the content of bins."""
|
|
163
|
+
bins.append(inputs)
|
|
164
|
+
return statistic_method(inputs)
|
|
165
|
+
|
|
166
|
+
res = stats.binned_statistic_dd(
|
|
167
|
+
inputs, values=output, statistic=statistic_, bins=states
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
bins = pd.DataFrame(bins[1:]).T
|
|
171
|
+
|
|
172
|
+
if len(bins.columns) != np.prod(states):
|
|
173
|
+
# mismatch with the number of states vs bins
|
|
174
|
+
# when it happens, we have NaNs in the statistic
|
|
175
|
+
# we can add empty columns with NaNs on these positions as bins
|
|
176
|
+
# then are not present for these states
|
|
177
|
+
nan_idx = np.argwhere(np.isnan(res.statistic).flatten()).flatten()
|
|
178
|
+
|
|
179
|
+
for idx in nan_idx:
|
|
180
|
+
bins = np.insert(bins, idx, np.nan, axis=1)
|
|
181
|
+
|
|
182
|
+
bins = pd.DataFrame(bins)
|
|
183
|
+
|
|
184
|
+
return DecompositionResult(
|
|
185
|
+
var_names=var_names,
|
|
186
|
+
statistic=res.statistic,
|
|
187
|
+
bins=bins,
|
|
188
|
+
states=states,
|
|
189
|
+
bin_edges=res.bin_edges,
|
|
190
|
+
)
|
simdec-1.0.0/src/simdec/significance.py → simdec-1.2.0.dev0/src/simdec/sensitivity_indices.py
RENAMED
|
@@ -6,11 +6,11 @@ import pandas as pd
|
|
|
6
6
|
from scipy import stats
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
__all__ = ["
|
|
9
|
+
__all__ = ["sensitivity_indices"]
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def number_of_bins(n_runs: int, n_factors: int) -> tuple[int, int]:
|
|
13
|
-
"""Optimal number of bins for first & second-order
|
|
13
|
+
"""Optimal number of bins for first & second-order sensitivity_indices indices.
|
|
14
14
|
|
|
15
15
|
Linear approximation of experimental results from (Marzban & Lahmer, 2016).
|
|
16
16
|
"""
|
|
@@ -31,18 +31,18 @@ def _weighted_var(x: np.ndarray, weights: np.ndarray) -> np.ndarray:
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@dataclass
|
|
34
|
-
class
|
|
34
|
+
class SensitivityAnalysisResult:
|
|
35
35
|
si: np.ndarray
|
|
36
36
|
first_order: np.ndarray
|
|
37
37
|
second_order: np.ndarray
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def
|
|
40
|
+
def sensitivity_indices(
|
|
41
41
|
inputs: pd.DataFrame | np.ndarray, output: pd.DataFrame | np.ndarray
|
|
42
|
-
) ->
|
|
43
|
-
"""
|
|
42
|
+
) -> SensitivityAnalysisResult:
|
|
43
|
+
"""Sensitivity indices.
|
|
44
44
|
|
|
45
|
-
The
|
|
45
|
+
The sensitivity_indices express how much variability of the output is
|
|
46
46
|
explained by the inputs.
|
|
47
47
|
|
|
48
48
|
Parameters
|
|
@@ -54,11 +54,11 @@ def significance(
|
|
|
54
54
|
|
|
55
55
|
Returns
|
|
56
56
|
-------
|
|
57
|
-
res :
|
|
57
|
+
res : SensitivityAnalysisResult
|
|
58
58
|
An object with attributes:
|
|
59
59
|
|
|
60
60
|
si : ndarray of shape (n_factors, 1)
|
|
61
|
-
|
|
61
|
+
Sensitivity indices, combined effect of each input.
|
|
62
62
|
foe : ndarray of shape (n_factors, 1)
|
|
63
63
|
First-order effects (also called 'main' or 'individual').
|
|
64
64
|
soe : ndarray of shape (n_factors, 1)
|
|
@@ -89,19 +89,18 @@ def significance(
|
|
|
89
89
|
... )
|
|
90
90
|
>>> output = f_ishigami(inputs.T)
|
|
91
91
|
|
|
92
|
-
We can now pass our inputs and outputs to the `
|
|
92
|
+
We can now pass our inputs and outputs to the `sensitivity_indices` function:
|
|
93
93
|
|
|
94
|
-
>>> res = sd.
|
|
94
|
+
>>> res = sd.sensitivity_indices(inputs=inputs, output=output)
|
|
95
95
|
>>> res.si
|
|
96
96
|
array([0.43157591, 0.44241433, 0.11767249])
|
|
97
97
|
|
|
98
98
|
"""
|
|
99
|
-
cat_columns = inputs.select_dtypes(["category", "O"]).columns
|
|
100
|
-
inputs[cat_columns] = inputs[cat_columns].apply(
|
|
101
|
-
lambda x: x.astype("category").cat.codes
|
|
102
|
-
)
|
|
103
|
-
|
|
104
99
|
if isinstance(inputs, pd.DataFrame):
|
|
100
|
+
cat_columns = inputs.select_dtypes(["category", "O"]).columns
|
|
101
|
+
inputs[cat_columns] = inputs[cat_columns].apply(
|
|
102
|
+
lambda x: x.astype("category").cat.codes
|
|
103
|
+
)
|
|
105
104
|
inputs = inputs.to_numpy()
|
|
106
105
|
if isinstance(output, pd.DataFrame):
|
|
107
106
|
output = output.to_numpy()
|
|
@@ -169,4 +168,4 @@ def significance(
|
|
|
169
168
|
soe = np.where(soe == 0, soe.T, soe)
|
|
170
169
|
si[i] = foe[i] + soe[:, i].sum() / 2
|
|
171
170
|
|
|
172
|
-
return
|
|
171
|
+
return SensitivityAnalysisResult(si, foe, soe)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import itertools
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import colorsys
|
|
6
|
+
import matplotlib as mpl
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
import numpy as np
|
|
9
|
+
import seaborn as sns
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pandas.io.formats.style import Styler
|
|
12
|
+
|
|
13
|
+
__all__ = ["visualization", "tableau", "palette"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SEQUENTIAL_PALETTES = [
|
|
17
|
+
"#DC267F",
|
|
18
|
+
"#E8EA2F",
|
|
19
|
+
"#26DCD1",
|
|
20
|
+
"#C552E4",
|
|
21
|
+
"#3F45D0",
|
|
22
|
+
"Oranges",
|
|
23
|
+
"Purples",
|
|
24
|
+
"Reds",
|
|
25
|
+
"Blues",
|
|
26
|
+
"Greens",
|
|
27
|
+
"YlOrBr",
|
|
28
|
+
"YlOrRd",
|
|
29
|
+
"OrRd",
|
|
30
|
+
"PuRd",
|
|
31
|
+
"RdPu",
|
|
32
|
+
"BuPu",
|
|
33
|
+
"GnBu",
|
|
34
|
+
"PuBu",
|
|
35
|
+
"YlGnBu",
|
|
36
|
+
"PuBuGn",
|
|
37
|
+
"BuGn",
|
|
38
|
+
"YlGn",
|
|
39
|
+
"Greys",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@functools.cache
|
|
44
|
+
def sequential_cmaps():
|
|
45
|
+
cmaps = []
|
|
46
|
+
for cmap in SEQUENTIAL_PALETTES:
|
|
47
|
+
try:
|
|
48
|
+
cmap_ = mpl.colormaps[cmap]
|
|
49
|
+
except KeyError:
|
|
50
|
+
color = mpl.colors.hex2color(cmap)
|
|
51
|
+
cmap_ = single_color_to_colormap(color)
|
|
52
|
+
cmaps.append(cmap_)
|
|
53
|
+
return cmaps
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def single_color_to_colormap(
|
|
57
|
+
rgba_color: list[float] | str, *, factor: float = 0.5
|
|
58
|
+
) -> mpl.colors.LinearSegmentedColormap:
|
|
59
|
+
"""Create a linear colormap using a single color."""
|
|
60
|
+
if isinstance(rgba_color, str):
|
|
61
|
+
rgba_color = mpl.colors.hex2color(rgba_color)
|
|
62
|
+
# discard alpha channel
|
|
63
|
+
if len(rgba_color) == 4:
|
|
64
|
+
*rgb_color, alpha = rgba_color
|
|
65
|
+
else:
|
|
66
|
+
alpha = 1.0
|
|
67
|
+
rgb_color = rgba_color
|
|
68
|
+
rgba_color = list(rgba_color) + [1]
|
|
69
|
+
|
|
70
|
+
# lighten and darken from factor around single color
|
|
71
|
+
hls_color = colorsys.rgb_to_hls(*rgb_color)
|
|
72
|
+
|
|
73
|
+
lightness = hls_color[1]
|
|
74
|
+
lightened_hls_color = (hls_color[0], lightness * (1 + factor), hls_color[2])
|
|
75
|
+
lightened_rgb_color = list(colorsys.hls_to_rgb(*lightened_hls_color))
|
|
76
|
+
|
|
77
|
+
darkened_hls_color = (hls_color[0], lightness * (1 - factor), hls_color[2])
|
|
78
|
+
darkened_rgb_color = list(colorsys.hls_to_rgb(*darkened_hls_color))
|
|
79
|
+
|
|
80
|
+
lightened_rgba_color = lightened_rgb_color + [alpha]
|
|
81
|
+
darkened_rgba_color = darkened_rgb_color + [alpha]
|
|
82
|
+
|
|
83
|
+
# convert to CMAP
|
|
84
|
+
cmap = mpl.colors.LinearSegmentedColormap.from_list(
|
|
85
|
+
"CustomSingleColor",
|
|
86
|
+
[lightened_rgba_color, rgba_color, darkened_rgba_color],
|
|
87
|
+
N=3,
|
|
88
|
+
)
|
|
89
|
+
return cmap
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def palette(
|
|
93
|
+
states: list[int], cmaps: list[mpl.colors.LinearSegmentedColormap] = None
|
|
94
|
+
) -> list[list[float]]:
|
|
95
|
+
"""Colour palette.
|
|
96
|
+
|
|
97
|
+
The product of the states gives the number of scenarios. For each
|
|
98
|
+
scenario, a colour is set.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
states : list of int
|
|
103
|
+
List of possible states for the considered parameter.
|
|
104
|
+
cmaps : list of LinearSegmentedColormap
|
|
105
|
+
List of colormaps. Must have the same number of colormaps as the number
|
|
106
|
+
of first level of states.
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
palette : list of float of size (n, 4)
|
|
110
|
+
List of colors corresponding to scenarios. RGBA formatted.
|
|
111
|
+
"""
|
|
112
|
+
n_cmaps = states[0]
|
|
113
|
+
if cmaps is None:
|
|
114
|
+
cmaps = sequential_cmaps()[:n_cmaps]
|
|
115
|
+
else:
|
|
116
|
+
cmaps = cmaps[:n_cmaps]
|
|
117
|
+
if len(cmaps) != n_cmaps:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Must have the same number of cmaps ({len(cmaps)}) as the "
|
|
120
|
+
f"number of first states ({n_cmaps})"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
colors = []
|
|
124
|
+
# one palette per first level state, could use more palette when there are
|
|
125
|
+
# many levels
|
|
126
|
+
n_shades = int(np.prod(states[1:]))
|
|
127
|
+
for i in range(n_cmaps):
|
|
128
|
+
cmap = cmaps[i].resampled(n_shades)
|
|
129
|
+
colors.append(cmap(np.linspace(0, 1, n_shades)))
|
|
130
|
+
|
|
131
|
+
return np.concatenate(colors).tolist()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def visualization(
|
|
135
|
+
*,
|
|
136
|
+
bins: pd.DataFrame,
|
|
137
|
+
palette: list[list[float]],
|
|
138
|
+
n_bins: str | int = "auto",
|
|
139
|
+
kind: Literal["histogram", "boxplot"] = "histogram",
|
|
140
|
+
ax=None,
|
|
141
|
+
) -> plt.Axes:
|
|
142
|
+
"""Histogram plot of scenarios.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
bins : DataFrame
|
|
147
|
+
Multidimensional bins.
|
|
148
|
+
palette : list of int of size (n, 4)
|
|
149
|
+
List of colours corresponding to scenarios.
|
|
150
|
+
n_bins : str or int
|
|
151
|
+
Number of bins or method from `np.histogram_bin_edges`.
|
|
152
|
+
kind: {"histogram", "boxplot"}
|
|
153
|
+
Histogram or Box Plot.
|
|
154
|
+
ax : Axes, optional
|
|
155
|
+
Matplotlib axis.
|
|
156
|
+
|
|
157
|
+
Returns
|
|
158
|
+
-------
|
|
159
|
+
ax : Axes
|
|
160
|
+
Matplotlib axis.
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
# needed to get the correct stacking order
|
|
164
|
+
bins.columns = pd.RangeIndex(start=len(bins.columns), stop=0, step=-1)
|
|
165
|
+
|
|
166
|
+
if kind == "histogram":
|
|
167
|
+
ax = sns.histplot(
|
|
168
|
+
bins,
|
|
169
|
+
multiple="stack",
|
|
170
|
+
stat="probability",
|
|
171
|
+
palette=palette,
|
|
172
|
+
common_bins=True,
|
|
173
|
+
common_norm=True,
|
|
174
|
+
bins=n_bins,
|
|
175
|
+
legend=False,
|
|
176
|
+
ax=ax,
|
|
177
|
+
)
|
|
178
|
+
elif kind == "boxplot":
|
|
179
|
+
ax = sns.boxplot(
|
|
180
|
+
bins,
|
|
181
|
+
palette=palette,
|
|
182
|
+
orient="h",
|
|
183
|
+
order=list(bins.columns)[::-1],
|
|
184
|
+
ax=ax,
|
|
185
|
+
)
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError("'kind' can only be 'histogram' or 'boxplot'")
|
|
188
|
+
return ax
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def tableau(
|
|
192
|
+
*,
|
|
193
|
+
var_names: list[str],
|
|
194
|
+
statistic: np.ndarray,
|
|
195
|
+
states: list[int | list[str]],
|
|
196
|
+
bins: pd.DataFrame,
|
|
197
|
+
palette: np.ndarray,
|
|
198
|
+
) -> tuple[pd.DataFrame, Styler]:
|
|
199
|
+
"""Generate a table of statistics for all scenarios.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
var_names : list of str
|
|
204
|
+
Variables name.
|
|
205
|
+
states : list of int or list of str
|
|
206
|
+
For each variable, number of states. Can either be a scalar or a list.
|
|
207
|
+
|
|
208
|
+
``states=[2, 2]`` or ``states=[['a', 'b'], ['low', 'high']]``
|
|
209
|
+
bins : DataFrame
|
|
210
|
+
Multidimensional bins.
|
|
211
|
+
palette : list of int of size (n, 4)
|
|
212
|
+
Ordered list of colours corresponding to each state.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
table : DataFrame
|
|
217
|
+
Summary table of statistics for the scenarios.
|
|
218
|
+
styler : Styler
|
|
219
|
+
Object to style the table with colours and formatting.
|
|
220
|
+
"""
|
|
221
|
+
table = bins.describe(percentiles=[0.5]).T
|
|
222
|
+
|
|
223
|
+
# get the index out to use a state id/colour
|
|
224
|
+
table = table.reset_index()
|
|
225
|
+
table.rename(columns={"index": "colour"}, inplace=True)
|
|
226
|
+
|
|
227
|
+
# Default states for 2 or 3
|
|
228
|
+
for i, state in enumerate(states):
|
|
229
|
+
if isinstance(state, int):
|
|
230
|
+
states: list
|
|
231
|
+
if state == 2:
|
|
232
|
+
states[i] = ["low", "high"]
|
|
233
|
+
elif state == 3:
|
|
234
|
+
states[i] = ["low", "medium", "high"]
|
|
235
|
+
|
|
236
|
+
# get the list of states
|
|
237
|
+
gen_states = [range(x) if isinstance(x, int) else x for x in states]
|
|
238
|
+
states_ = np.asarray(list(itertools.product(*gen_states)))
|
|
239
|
+
for i, var_name in enumerate(var_names):
|
|
240
|
+
table.insert(loc=i + 1, column=var_name, value=states_[:, i])
|
|
241
|
+
|
|
242
|
+
# groupby on the variable names
|
|
243
|
+
table.set_index(list(var_names), inplace=True)
|
|
244
|
+
|
|
245
|
+
proba = table["count"] / sum(table["count"])
|
|
246
|
+
proba = np.asarray(proba)
|
|
247
|
+
table["probability"] = proba
|
|
248
|
+
table["mean"] = statistic.flatten()
|
|
249
|
+
|
|
250
|
+
# only select/ordering interesting columns
|
|
251
|
+
table = table[["colour", "std", "min", "mean", "max", "probability"]]
|
|
252
|
+
|
|
253
|
+
table.insert(loc=0, column="N°", value=np.arange(1, stop=len(table) + 1)[::-1])
|
|
254
|
+
|
|
255
|
+
# style the colour background with palette
|
|
256
|
+
cmap = mpl.colors.ListedColormap(palette)
|
|
257
|
+
styler = table.style
|
|
258
|
+
styler.format(precision=2)
|
|
259
|
+
styler.background_gradient(subset=["colour"], cmap=cmap)
|
|
260
|
+
styler.format(lambda x: "", subset=["colour"])
|
|
261
|
+
|
|
262
|
+
styler.set_table_styles([{"selector": "th", "props": [("text-align", "center")]}])
|
|
263
|
+
return table, styler
|
simdec-1.0.0/Makefile
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
.PHONY: help prepare doc test serve
|
|
2
|
-
.DEFAULT_GOAL := help
|
|
3
|
-
SHELL:=/bin/bash
|
|
4
|
-
|
|
5
|
-
# Add help text after each target name starting with '\#\#'
|
|
6
|
-
help: ## show this help
|
|
7
|
-
@echo -e "Help for this makefile\n"
|
|
8
|
-
@echo "Possible commands are:"
|
|
9
|
-
@grep -h "##" $(MAKEFILE_LIST) | grep -v grep | sed -e 's/\(.*\):.*##\(.*\)/ \1: \2/'
|
|
10
|
-
|
|
11
|
-
prepare: ## Install dependencies and pre-commit hook
|
|
12
|
-
pip install -e ".[dev]"
|
|
13
|
-
pre-commit install
|
|
14
|
-
|
|
15
|
-
doc: ## Build Sphinx documentation
|
|
16
|
-
sphinx-build -b html docs docs/html
|
|
17
|
-
|
|
18
|
-
test: ## Run tests with coverage
|
|
19
|
-
pytest --cov simdec --cov-report term-missing
|
|
20
|
-
|
|
21
|
-
serve-dev: ## Serve Panel dashboard - Dev mode
|
|
22
|
-
panel serve app.py --show --autoreload
|
|
23
|
-
|
|
24
|
-
serve: ## Serve Panel dashboard - Prod mode
|
|
25
|
-
panel serve app.py
|
simdec-1.0.0/app.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import io
|
|
2
|
-
|
|
3
|
-
import matplotlib.pyplot as plt
|
|
4
|
-
import pandas as pd
|
|
5
|
-
import panel as pn
|
|
6
|
-
|
|
7
|
-
import simdec as sd
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# panel app
|
|
11
|
-
pn.extension(template="material")
|
|
12
|
-
pn.config.throttled = True
|
|
13
|
-
|
|
14
|
-
text_fname = pn.widgets.FileInput(sizing_mode="stretch_width")
|
|
15
|
-
slider_dec_limit = pn.widgets.FloatSlider(
|
|
16
|
-
value=1.0, step=0.05, name="Explained variance ratio"
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pn.cache
|
|
21
|
-
def load_data(text_fname):
|
|
22
|
-
if text_fname is None:
|
|
23
|
-
text_fname = "tests/data/stress.csv"
|
|
24
|
-
else:
|
|
25
|
-
text_fname = io.BytesIO(text_fname)
|
|
26
|
-
|
|
27
|
-
data = pd.read_csv(text_fname)
|
|
28
|
-
output_name, *v_names = list(data.columns)
|
|
29
|
-
inputs, output = data[v_names], data[output_name]
|
|
30
|
-
|
|
31
|
-
si = sd.significance(inputs=inputs, output=output).si
|
|
32
|
-
|
|
33
|
-
return si, inputs, output
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def decomposition(dec_limit, data):
|
|
37
|
-
si, inputs, output = data
|
|
38
|
-
return sd.decomposition(
|
|
39
|
-
inputs=inputs, output=output, significance=si, dec_limit=dec_limit
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def palette(res):
|
|
44
|
-
return sd.palette(res.states)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def figure(res, palette):
|
|
48
|
-
fig, ax = plt.subplots()
|
|
49
|
-
_ = sd.visualization(bins=res.bins, palette=palette, states=res.states, ax=ax)
|
|
50
|
-
return fig
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def tableau(res, palette):
|
|
54
|
-
# use a notebook to see the styling
|
|
55
|
-
_, styler = sd.tableau(
|
|
56
|
-
statistic=res.statistic,
|
|
57
|
-
var_names=res.var_names,
|
|
58
|
-
states=res.states,
|
|
59
|
-
bins=res.bins,
|
|
60
|
-
palette=palette,
|
|
61
|
-
)
|
|
62
|
-
return styler
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
interactive_data = pn.bind(load_data, text_fname)
|
|
66
|
-
interactive_decomposition = pn.bind(decomposition, slider_dec_limit, interactive_data)
|
|
67
|
-
interactive_palette = pn.bind(palette, interactive_decomposition)
|
|
68
|
-
interactive_figure = pn.bind(figure, interactive_decomposition, interactive_palette)
|
|
69
|
-
interactive_tableau = pn.bind(tableau, interactive_decomposition, interactive_palette)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
top_description = """
|
|
73
|
-
# Parameters
|
|
74
|
-
|
|
75
|
-
Select a CSV file:
|
|
76
|
-
- comma delimited and with point decimal separator;
|
|
77
|
-
- first column is the output of the model;
|
|
78
|
-
- rest of the columns are the inputs.
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
|
-
params_description = """
|
|
82
|
-
The following parameters can be adjusted:
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
pn_params = pn.layout.WidgetBox(
|
|
86
|
-
top_description,
|
|
87
|
-
text_fname,
|
|
88
|
-
params_description,
|
|
89
|
-
slider_dec_limit,
|
|
90
|
-
max_width=350,
|
|
91
|
-
sizing_mode="stretch_width",
|
|
92
|
-
).servable(area="sidebar")
|
|
93
|
-
|
|
94
|
-
pn_app = pn.Column(pn.Row(interactive_figure, interactive_tableau)).servable(
|
|
95
|
-
title="Simulation Decomposition Dashboard"
|
|
96
|
-
)
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import Literal
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from scipy import stats
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
__all__ = ["decomposition"]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class DecompositionResult:
|
|
16
|
-
var_names: list[str]
|
|
17
|
-
statistic: np.ndarray
|
|
18
|
-
bins: pd.DataFrame
|
|
19
|
-
states: list[int]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def decomposition(
|
|
23
|
-
inputs: pd.DataFrame,
|
|
24
|
-
output: pd.DataFrame,
|
|
25
|
-
*,
|
|
26
|
-
significance: np.ndarray,
|
|
27
|
-
dec_limit: float = 1,
|
|
28
|
-
states: list[int] | None = None,
|
|
29
|
-
statistic: Literal["mean", "median"] | None = "mean",
|
|
30
|
-
) -> DecompositionResult:
|
|
31
|
-
"""SimDec decomposition.
|
|
32
|
-
|
|
33
|
-
Parameters
|
|
34
|
-
----------
|
|
35
|
-
inputs : DataFrame of shape (n_runs, n_factors)
|
|
36
|
-
Input variables.
|
|
37
|
-
output : DataFrame of shape (n_runs, 1)
|
|
38
|
-
Target variable.
|
|
39
|
-
significance : ndarray of shape (n_factors, 1)
|
|
40
|
-
Significance index, combined effect of each input.
|
|
41
|
-
dec_limit : float
|
|
42
|
-
Explained variance ratio to filter the number input variables.
|
|
43
|
-
states : list of int, optional
|
|
44
|
-
List of possible states for the considered parameter.
|
|
45
|
-
statistic : {"mean", "median"}, optional
|
|
46
|
-
Statistic to compute in each bin.
|
|
47
|
-
|
|
48
|
-
Returns
|
|
49
|
-
-------
|
|
50
|
-
res : DecompositionResult
|
|
51
|
-
An object with attributes:
|
|
52
|
-
|
|
53
|
-
var_names : list of string (n_factors, 1)
|
|
54
|
-
Variable names.
|
|
55
|
-
statistic : ndarray of shape (n_factors, 1)
|
|
56
|
-
Statistic in each bin.
|
|
57
|
-
bins : DataFrame
|
|
58
|
-
Multidimensional bins.
|
|
59
|
-
states : list of int
|
|
60
|
-
List of possible states for the considered parameter.
|
|
61
|
-
|
|
62
|
-
"""
|
|
63
|
-
var_names = inputs.columns
|
|
64
|
-
inputs = inputs.to_numpy()
|
|
65
|
-
output = output.to_numpy()
|
|
66
|
-
|
|
67
|
-
# 1. variables for decomposition
|
|
68
|
-
var_order = np.argsort(significance)[::-1]
|
|
69
|
-
|
|
70
|
-
# only keep the explained variance corresponding to `dec_limit`
|
|
71
|
-
significance = significance[var_order]
|
|
72
|
-
n_var_dec = np.where(np.cumsum(significance) < dec_limit)[0].size
|
|
73
|
-
n_var_dec = max(1, n_var_dec) # keep at least one variable
|
|
74
|
-
n_var_dec = min(5, n_var_dec) # use at most 5 variables
|
|
75
|
-
|
|
76
|
-
var_names = var_names[var_order[:n_var_dec]].tolist()
|
|
77
|
-
inputs = inputs[:, var_order[:n_var_dec]]
|
|
78
|
-
|
|
79
|
-
# 2. states formation
|
|
80
|
-
if states is None:
|
|
81
|
-
states = 3 if n_var_dec < 3 else 2
|
|
82
|
-
states = [states] * n_var_dec
|
|
83
|
-
|
|
84
|
-
# categorical for a given variable
|
|
85
|
-
for i in range(n_var_dec):
|
|
86
|
-
n_unique = np.unique(inputs[:, i]).size
|
|
87
|
-
states[i] = n_unique if n_unique < 5 else states[i]
|
|
88
|
-
|
|
89
|
-
# 3. decomposition
|
|
90
|
-
bins = []
|
|
91
|
-
|
|
92
|
-
statistic_methods = {
|
|
93
|
-
"mean": np.mean,
|
|
94
|
-
"median": np.median,
|
|
95
|
-
}
|
|
96
|
-
try:
|
|
97
|
-
statistic_method = statistic_methods[statistic]
|
|
98
|
-
except IndexError:
|
|
99
|
-
msg = f"'statistic' must be one of {statistic_methods.values()}"
|
|
100
|
-
raise ValueError(msg)
|
|
101
|
-
|
|
102
|
-
def statistic_(inputs):
|
|
103
|
-
"""Custom function to keep track of the content of bins."""
|
|
104
|
-
bins.append(inputs)
|
|
105
|
-
return statistic_method(inputs)
|
|
106
|
-
|
|
107
|
-
res = stats.binned_statistic_dd(
|
|
108
|
-
inputs, values=output, statistic=statistic_, bins=states
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
bins = pd.DataFrame(bins[1:]).T
|
|
112
|
-
|
|
113
|
-
return DecompositionResult(var_names, res.statistic, bins, states)
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import itertools
|
|
2
|
-
|
|
3
|
-
import matplotlib as mpl
|
|
4
|
-
import matplotlib.pyplot as plt
|
|
5
|
-
import numpy as np
|
|
6
|
-
import seaborn as sns
|
|
7
|
-
import pandas as pd
|
|
8
|
-
from pandas.io.formats.style import Styler
|
|
9
|
-
|
|
10
|
-
__all__ = ["visualization", "tableau", "palette"]
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
sequential_palettes = [
|
|
14
|
-
"Purples",
|
|
15
|
-
"Blues",
|
|
16
|
-
"Greens",
|
|
17
|
-
"Oranges",
|
|
18
|
-
"Reds",
|
|
19
|
-
"YlOrBr",
|
|
20
|
-
"YlOrRd",
|
|
21
|
-
"OrRd",
|
|
22
|
-
"PuRd",
|
|
23
|
-
"RdPu",
|
|
24
|
-
"BuPu",
|
|
25
|
-
"GnBu",
|
|
26
|
-
"PuBu",
|
|
27
|
-
"YlGnBu",
|
|
28
|
-
"PuBuGn",
|
|
29
|
-
"BuGn",
|
|
30
|
-
"YlGn",
|
|
31
|
-
"Greys",
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def palette(states: list[int]) -> list[list[float]]:
|
|
36
|
-
"""Colour palette.
|
|
37
|
-
|
|
38
|
-
The product of the states gives the number of scenarios. For each
|
|
39
|
-
scenario, a colour is set.
|
|
40
|
-
|
|
41
|
-
Parameters
|
|
42
|
-
----------
|
|
43
|
-
states : list of int
|
|
44
|
-
List of possible states for the considered parameter.
|
|
45
|
-
|
|
46
|
-
Returns
|
|
47
|
-
-------
|
|
48
|
-
palette : list of int of size (n, 4)
|
|
49
|
-
List of colours corresponding to scenarios.
|
|
50
|
-
"""
|
|
51
|
-
colors = []
|
|
52
|
-
# one palette per first level state, could use more palette when there are
|
|
53
|
-
# many levels
|
|
54
|
-
n_shades = int(np.prod(states[1:]))
|
|
55
|
-
for i in range(states[0]):
|
|
56
|
-
palette_ = sequential_palettes[i]
|
|
57
|
-
cmap = mpl.colormaps[palette_].resampled(n_shades + 1)
|
|
58
|
-
colors.append(cmap(range(1, n_shades + 1)))
|
|
59
|
-
|
|
60
|
-
return np.concatenate(colors).tolist()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def visualization(
|
|
64
|
-
*, bins: pd.DataFrame, states: list[int], palette: list[list[float]], ax=None
|
|
65
|
-
) -> plt.Axes:
|
|
66
|
-
"""Histogram plot of scenarios.
|
|
67
|
-
|
|
68
|
-
Parameters
|
|
69
|
-
----------
|
|
70
|
-
bins : DataFrame
|
|
71
|
-
Multidimensional bins.
|
|
72
|
-
states : list of int
|
|
73
|
-
List of possible states for the considered parameter.
|
|
74
|
-
palette : list of int of size (n, 4)
|
|
75
|
-
List of colours corresponding to scenarios.
|
|
76
|
-
ax : Axes, optional
|
|
77
|
-
Matplotlib axis.
|
|
78
|
-
|
|
79
|
-
Returns
|
|
80
|
-
-------
|
|
81
|
-
ax : Axes
|
|
82
|
-
Matplotlib axis.
|
|
83
|
-
|
|
84
|
-
"""
|
|
85
|
-
# needed to get the correct stacking order
|
|
86
|
-
bins.columns = pd.RangeIndex(start=np.prod(states), stop=0, step=-1)
|
|
87
|
-
|
|
88
|
-
ax = sns.histplot(
|
|
89
|
-
bins,
|
|
90
|
-
multiple="stack",
|
|
91
|
-
stat="probability",
|
|
92
|
-
palette=palette,
|
|
93
|
-
common_bins=True,
|
|
94
|
-
common_norm=True,
|
|
95
|
-
legend=False,
|
|
96
|
-
ax=ax,
|
|
97
|
-
)
|
|
98
|
-
return ax
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def tableau(
|
|
102
|
-
*,
|
|
103
|
-
var_names: list[str],
|
|
104
|
-
statistic: np.ndarray,
|
|
105
|
-
states: list[int | str],
|
|
106
|
-
bins: pd.DataFrame,
|
|
107
|
-
palette: np.ndarray,
|
|
108
|
-
) -> tuple[pd.DataFrame, Styler]:
|
|
109
|
-
"""Generate a table of statistics for all scenarios.
|
|
110
|
-
|
|
111
|
-
Parameters
|
|
112
|
-
----------
|
|
113
|
-
var_names : list of str
|
|
114
|
-
Variables name.
|
|
115
|
-
states : list of int or str
|
|
116
|
-
For each variable, number of states. Can either be a scalar or a list.
|
|
117
|
-
|
|
118
|
-
``states=[2, 2]`` or ``states=[['a', 'b'], ['low', 'high']]``
|
|
119
|
-
bins : DataFrame
|
|
120
|
-
Multidimensional bins.
|
|
121
|
-
palette : list of int of size (n, 4)
|
|
122
|
-
Ordered list of colours corresponding to each state.
|
|
123
|
-
|
|
124
|
-
Returns
|
|
125
|
-
-------
|
|
126
|
-
table : DataFrame
|
|
127
|
-
Summary table of statistics for the scenarios.
|
|
128
|
-
styler : Styler
|
|
129
|
-
Object to style the table with colours and formatting.
|
|
130
|
-
"""
|
|
131
|
-
table = bins.describe(percentiles=[0.5]).T
|
|
132
|
-
|
|
133
|
-
# get the index out to use a state id/colour
|
|
134
|
-
table = table.reset_index()
|
|
135
|
-
table.rename(columns={"index": "colour"}, inplace=True)
|
|
136
|
-
|
|
137
|
-
# get the list of states
|
|
138
|
-
gen_states = [range(x) if isinstance(x, int) else x for x in states]
|
|
139
|
-
states_ = np.asarray(list(itertools.product(*gen_states)))
|
|
140
|
-
for i, var_name in enumerate(var_names):
|
|
141
|
-
table.insert(loc=i + 1, column=var_name, value=states_[:, i])
|
|
142
|
-
|
|
143
|
-
# groupby on the variable names
|
|
144
|
-
table = (
|
|
145
|
-
table.groupby(var_names, group_keys=True, sort=False)
|
|
146
|
-
.apply(lambda x: x)
|
|
147
|
-
.droplevel(-1)
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
proba = table["count"] / sum(table["count"])
|
|
151
|
-
proba = np.asarray(proba)
|
|
152
|
-
table["probability"] = proba
|
|
153
|
-
|
|
154
|
-
table["weighted mean"] = statistic.flatten()
|
|
155
|
-
|
|
156
|
-
# only select/ordering interesting columns
|
|
157
|
-
table = table[
|
|
158
|
-
["colour", "std", "min", "weighted mean", "50%", "max", "probability"]
|
|
159
|
-
]
|
|
160
|
-
|
|
161
|
-
# style the colour background with palette
|
|
162
|
-
cmap = mpl.colors.ListedColormap(palette)
|
|
163
|
-
styler = table.style
|
|
164
|
-
styler.format(precision=3)
|
|
165
|
-
styler.background_gradient(subset=["colour"], cmap=cmap)
|
|
166
|
-
styler.format(lambda x: "", subset=["colour"])
|
|
167
|
-
|
|
168
|
-
return table, styler
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from typing_extensions import Annotated
|
|
3
|
-
|
|
4
|
-
import matplotlib.pyplot as plt
|
|
5
|
-
import pandas as pd
|
|
6
|
-
import panel as pn
|
|
7
|
-
import typer
|
|
8
|
-
|
|
9
|
-
import simdec as sd
|
|
10
|
-
|
|
11
|
-
app = typer.Typer()
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@app.command()
|
|
15
|
-
def main(
|
|
16
|
-
data: Annotated[
|
|
17
|
-
Path,
|
|
18
|
-
typer.Option(
|
|
19
|
-
exists=True,
|
|
20
|
-
file_okay=True,
|
|
21
|
-
dir_okay=False,
|
|
22
|
-
writable=False,
|
|
23
|
-
readable=True,
|
|
24
|
-
resolve_path=True,
|
|
25
|
-
),
|
|
26
|
-
]
|
|
27
|
-
):
|
|
28
|
-
data = pd.read_csv(data)
|
|
29
|
-
output_name, *v_names = list(data.columns)
|
|
30
|
-
inputs, output = data[v_names], data[output_name]
|
|
31
|
-
|
|
32
|
-
res = sd.significance(inputs=inputs, output=output)
|
|
33
|
-
si = res.si
|
|
34
|
-
|
|
35
|
-
res = sd.decomposition(inputs=inputs, output=output, significance=si)
|
|
36
|
-
|
|
37
|
-
fig, ax = plt.subplots()
|
|
38
|
-
palette = sd.palette(states=res.states)
|
|
39
|
-
ax, palette = sd.visualization(
|
|
40
|
-
bins=res.bins, palette=palette, states=res.states, ax=ax
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
# use a notebook to see the styling
|
|
44
|
-
table, styler = sd.tableau(
|
|
45
|
-
statistic=res.statistic,
|
|
46
|
-
var_names=res.var_names,
|
|
47
|
-
states=res.states,
|
|
48
|
-
bins=res.bins,
|
|
49
|
-
palette=palette,
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
# panel app
|
|
53
|
-
pn.extension(template="material")
|
|
54
|
-
pn_fig = pn.pane.Matplotlib(fig, dpi=144)
|
|
55
|
-
pn_table = pn.pane.DataFrame(styler)
|
|
56
|
-
pn_app = pn.Column(pn_fig, pn_table)
|
|
57
|
-
pn_app.save("app.html")
|
|
File without changes
|
|
File without changes
|