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.
@@ -7,6 +7,9 @@ __pycache__
7
7
  .cache/*
8
8
  **/.ipynb_checkpoints
9
9
 
10
+ # Env variables with direnv
11
+ .envrc
12
+
10
13
  # OS generated files
11
14
  .DS_Store*
12
15
 
@@ -30,3 +33,4 @@ build/*
30
33
  dist/*
31
34
  sdist/*
32
35
  docs/_build
36
+ app.html
@@ -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
1
+ Metadata-Version: 2.3
2
2
  Name: simdec
3
- Version: 1.0.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
- Requires-Python: >=3.9
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
- Requires-Dist: typer[all]
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: simdec[doc,test]; extra == 'dev'
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 significance indices,
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
- Decomposition: A “Must-Have” Inclusion for Many Disciplines. _INFORMS
81
- Transactions on Education, 22_(3), 147-159. DOI:10.1287/ited.2019.0240.
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 significance indices,
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
- Decomposition: A “Must-Have” Inclusion for Many Disciplines. _INFORMS
39
- Transactions on Education, 22_(3), 147-159. DOI:10.1287/ited.2019.0240.
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.0.0"
7
+ version = "1.2.0dev0"
8
8
  description = "Sensitivity analysis using simulation decomposition"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
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
+ )
@@ -6,11 +6,11 @@ import pandas as pd
6
6
  from scipy import stats
7
7
 
8
8
 
9
- __all__ = ["significance"]
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 significance indices.
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 SignificanceResult:
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 significance(
40
+ def sensitivity_indices(
41
41
  inputs: pd.DataFrame | np.ndarray, output: pd.DataFrame | np.ndarray
42
- ) -> SignificanceResult:
43
- """Significance indices.
42
+ ) -> SensitivityAnalysisResult:
43
+ """Sensitivity indices.
44
44
 
45
- The significance express how much variability of the output is
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 : SignificanceResult
57
+ res : SensitivityAnalysisResult
58
58
  An object with attributes:
59
59
 
60
60
  si : ndarray of shape (n_factors, 1)
61
- Significance index, combined effect of each input.
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 `significance` function:
92
+ We can now pass our inputs and outputs to the `sensitivity_indices` function:
93
93
 
94
- >>> res = sd.significance(inputs=inputs, output=output)
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 SignificanceResult(si, foe, soe)
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,6 +0,0 @@
1
- """SimDec main namespace."""
2
- from simdec.decomposition import *
3
- from simdec.significance import *
4
- from simdec.visualization import *
5
-
6
- __all__ = ["significance", "decomposition", "visualization", "tableau", "palette"]
@@ -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