Skip to content

Commit df5d056

Browse files
authored
Slef pert domain fix (#77)
* Fixes for vuln calculation and deprecations. * Fixes for vuln calculation and deprecations. * Clarified confusion between model identity (UUID) versus model name for database operations. store uses UUID for replacement, load by name fetches the first match. Creating FairModel('New Name') generates a new UUID and thus a new, distinct model. * Added 'Secondary Loss Event Frequency' to the self._le_1_targets list in FairDataInput.__init__. This will ensure that any parameters supplied for SLEF (e.g., low, mode, high, constant, mean) are checked to be within [0, 1]. It also ensures that the generated values for SLEF are clipped to [0, 1].
1 parent 897b97a commit df5d056

File tree

9 files changed

+270
-258
lines changed

9 files changed

+270
-258
lines changed

pyfair/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""PyFair is an open source implementation of the FAIR methodology."""
22

3-
VERSION = '0.1-alpha.12'
3+
from ._version import __version__
44

55

66
from . import model

pyfair/_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = "0.1-alpha.12"

pyfair/model/model_calc.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ class FairCalculations(object):
1313
3) a multiplication function.
1414
1515
"""
16+
1617
def __init__(self):
1718
# Lookup table for functions (no leaf nodes required)
1819
self._function_dict = {
19-
'Risk' : self._calculate_multiplication,
20-
'Loss Event Frequency' : self._calculate_multiplication,
21-
'Threat Event Frequency': self._calculate_multiplication,
22-
'Vulnerability' : self._calculate_step_average,
23-
'Loss Magnitude' : self._calculate_addition,
24-
'Primary Loss' : self._calculate_multiplication,
25-
'Secondary Loss' : self._calculate_multiplication,
20+
"Risk": self._calculate_multiplication,
21+
"Loss Event Frequency": self._calculate_multiplication,
22+
"Threat Event Frequency": self._calculate_multiplication,
23+
"Vulnerability": self._calculate_step_average,
24+
"Loss Magnitude": self._calculate_addition,
25+
"Primary Loss": self._calculate_multiplication,
26+
"Secondary Loss": self._calculate_multiplication,
2627
}
2728

2829
def calculate(self, parent_name, child_1_data, child_2_data):
@@ -58,22 +59,11 @@ def calculate(self, parent_name, child_1_data, child_2_data):
5859
return calculated_result
5960

6061
def _calculate_step_average(self, child_1_data, child_2_data):
61-
"""Get bool series based on step function, then average for vuln"""
62+
"""Return per-simulation boolean (as float) for Vulnerability: 1.0 if TC > CS, else 0.0"""
6263
# Get Trues (1) where child_2 (TCap) is greater than child_1 (CS)
63-
# Otherwise False (0)
64-
bool_series = child_1_data < child_2_data
65-
# Treat those bools as 1 and 0 and get mean
66-
bool_scalar_average = bool_series.mean()
67-
# Create a long array of that mean
68-
vuln_data = np.full(
69-
len(bool_series),
70-
bool_scalar_average
71-
)
72-
# And put it in a series
73-
vuln = pd.Series(
74-
data=vuln_data,
75-
index=bool_series.index
76-
)
64+
bool_series = (child_1_data < child_2_data).astype(float)
65+
# Return the per-simulation result as a Series
66+
vuln = pd.Series(data=bool_series.values, index=bool_series.index)
7767
return vuln
7868

7969
def _calculate_addition(self, child_1_data, child_2_data):

pyfair/model/model_input.py

Lines changed: 55 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,33 @@ class FairDataInput(object):
2929
is stored when converting to JSON or another serialization format.
3030
3131
"""
32+
3233
def __init__(self):
3334
# These targets must be less than or equal to one
34-
self._le_1_targets = ['Probability of Action', 'Vulnerability', 'Control Strength', 'Threat Capability']
35-
self._le_1_keywords = ['constant', 'high', 'mode', 'low', 'mean']
35+
self._le_1_targets = [
36+
"Probability of Action",
37+
"Vulnerability",
38+
"Control Strength",
39+
"Threat Capability",
40+
"Secondary Loss Event Frequency",
41+
]
42+
self._le_1_keywords = ["constant", "high", "mode", "low", "mean"]
3643
# Parameter map associates parameters with functions
3744
self._parameter_map = {
38-
'constant': self._gen_constant,
39-
'high' : self._gen_pert,
40-
'mode' : self._gen_pert,
41-
'low' : self._gen_pert,
42-
'gamma' : self._gen_pert,
43-
'mean' : self._gen_normal,
44-
'stdev' : self._gen_normal,
45+
"constant": self._gen_constant,
46+
"high": self._gen_pert,
47+
"mode": self._gen_pert,
48+
"low": self._gen_pert,
49+
"gamma": self._gen_pert,
50+
"mean": self._gen_normal,
51+
"stdev": self._gen_normal,
4552
}
4653
# List of keywords with function keys
4754
self._required_keywords = {
48-
self._gen_constant: ['constant'],
49-
self._gen_pert : ['low', 'mode', 'high'],
50-
self._gen_normal : ['mean', 'stdev'],
51-
}
55+
self._gen_constant: ["constant"],
56+
self._gen_pert: ["low", "mode", "high"],
57+
self._gen_normal: ["mean", "stdev"],
58+
}
5259
# Storage of inputs
5360
self._supplied_values = {}
5461

@@ -59,7 +66,7 @@ def get_supplied_values(self):
5966
-------
6067
dict
6168
A dictionary of the values supplied to generate function. The
62-
keys for the dict will be the target node as a string (e.g.
69+
keys for the dict will be the target node as a string (e.g.
6370
'Loss Event Frequency') and the values will be a sub-dictionary
6471
of keyword arguments ({'low': 50, 'mode}: 51, 'high': 52}).
6572
@@ -80,7 +87,11 @@ def _check_le_1(self, target, **kwargs):
8087
pass
8188
# If not, raise error
8289
else:
83-
raise FairException('"{}" must have "{}" value between zero and one.'.format(target, key))
90+
raise FairException(
91+
'"{}" must have "{}" value between zero and one.'.format(
92+
target, key
93+
)
94+
)
8495

8596
def _check_parameters(self, target_function, **kwargs):
8697
"""Runs parameter checks
@@ -94,7 +105,7 @@ def _check_parameters(self, target_function, **kwargs):
94105
for keyword, value in kwargs.items():
95106
# Two conditions
96107
value_is_less_than_zero = value < 0
97-
keyword_is_relevant = keyword in ['mean', 'constant', 'low', 'mode', 'high']
108+
keyword_is_relevant = keyword in ["mean", "constant", "low", "mode", "high"]
98109
# Test conditions
99110
if keyword_is_relevant and value_is_less_than_zero:
100111
raise FairException('"{}" is less than zero.'.format(keyword))
@@ -104,7 +115,11 @@ def _check_parameters(self, target_function, **kwargs):
104115
if required_keyword in kwargs.keys():
105116
pass
106117
else:
107-
raise FairException('"{}" is missing "{}".'.format(str(target_function), required_keyword))
118+
raise FairException(
119+
'"{}" is missing "{}".'.format(
120+
str(target_function), required_keyword
121+
)
122+
)
108123

109124
def generate(self, target, count, **kwargs):
110125
"""Executes request, records parameters, and return random values
@@ -123,7 +138,7 @@ def generate(self, target, count, **kwargs):
123138
The number of random numbers generated (or alternatively, the
124139
length of the Series returned).
125140
**kwargs
126-
Keyword arguments with one of the following values: {`mean`,
141+
Keyword arguments with one of the following values: {`mean`,
127142
`stdev`, `low`, `mode`, `high`, `gamma`, or `constant`}.
128143
129144
Raises
@@ -146,8 +161,8 @@ def generate(self, target, count, **kwargs):
146161
result = self._generate_single(target, count, **kwargs)
147162
# Explicitly insert optional keywords for model storage
148163
dict_keys = kwargs.keys()
149-
if 'low' in dict_keys and 'gamma' not in dict_keys:
150-
kwargs['gamma'] = 4
164+
if "low" in dict_keys and "gamma" not in dict_keys:
165+
kwargs["gamma"] = 4
151166
# Record and return
152167
self._supplied_values[target] = {**kwargs}
153168
return result
@@ -192,16 +207,16 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):
192207
193208
{
194209
'Reputational': {
195-
'Secondary Loss Event Frequency': {'constant': 4000},
210+
'Secondary Loss Event Frequency': {'constant': 4000},
196211
'Secondary Loss Event Magnitude': {
197212
'low': 10, 'mode': 20, 'high': 100
198213
},
199214
},
200215
'Legal': {
201-
'Secondary Loss Event Frequency': {'constant': 2000},
216+
'Secondary Loss Event Frequency': {'constant': 2000},
202217
'Secondary Loss Event Magnitude': {
203218
'low': 10, 'mode': 20, 'high': 100
204-
},
219+
},
205220
}
206221
}
207222
@@ -242,7 +257,7 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):
242257
243258
"""
244259
# Remove prefix from target
245-
final_target = prefixed_target.lstrip('multi_')
260+
final_target = prefixed_target.lstrip("multi_")
246261
# Create a container for dataframes
247262
df_dict = {target: pd.DataFrame() for target in kwargs_dict.keys()}
248263
# For each target
@@ -255,9 +270,9 @@ def generate_multi(self, prefixed_target, count, kwargs_dict):
255270
# Put in dict
256271
df_dict[target][column] = s
257272
# Get partial secondary losses and sum up all the values
258-
summed = np.sum(df.prod(axis=1) for df in df_dict.values())
273+
summed = sum(df.prod(axis=1) for df in df_dict.values())
259274
# Record params
260-
new_target = 'multi_' + final_target
275+
new_target = "multi_" + final_target
261276
self._supplied_values[new_target] = kwargs_dict
262277
return summed
263278

@@ -294,12 +309,12 @@ def supply_raw(self, target, array):
294309
s = pd.Series(clean_array)
295310
# Check numeric and not null
296311
if s.isnull().any():
297-
raise FairException('Supplied data contains null values')
312+
raise FairException("Supplied data contains null values")
298313
# Ensure values are appropriate
299314
if target in self._le_1_targets:
300315
if s.max() > 1 or s.min() < 0:
301-
raise FairException(f'{target} data greater or less than one')
302-
self._supplied_values[target] = {'raw': s.values.tolist()}
316+
raise FairException(f"{target} data greater or less than one")
317+
self._supplied_values[target] = {"raw": s.values.tolist()}
303318
return s.values
304319

305320
def _determine_func(self, **kwargs):
@@ -309,24 +324,22 @@ def _determine_func(self, **kwargs):
309324
if key not in self._parameter_map.keys():
310325
raise FairException('"{}"" is not a recognized keyword'.format(key))
311326
# Check whether all keys go to same function via set comprension
312-
functions = list(set([
313-
self._parameter_map[key]
314-
for key
315-
in kwargs.keys()
316-
]))
327+
functions = list(set([self._parameter_map[key] for key in kwargs.keys()]))
317328
if len(functions) > 1:
318-
raise FairException('"{}" mixes incompatible keywords.'.format(str(kwargs.keys())))
329+
raise FairException(
330+
'"{}" mixes incompatible keywords.'.format(str(kwargs.keys()))
331+
)
319332
else:
320333
function = functions[0]
321334
return function
322335

323336
def _gen_constant(self, count, **kwargs):
324337
"""Generates constant array of size `count`"""
325-
return np.full(count, kwargs['constant'])
338+
return np.full(count, kwargs["constant"])
326339

327340
def _gen_normal(self, count, **kwargs):
328341
"""Geneates random normally-distributed array of size `count`"""
329-
normal = scipy.stats.norm(loc=kwargs['mean'], scale=kwargs['stdev'])
342+
normal = scipy.stats.norm(loc=kwargs["mean"], scale=kwargs["stdev"])
330343
rvs = normal.rvs(count)
331344
return rvs
332345

@@ -340,10 +353,12 @@ def _gen_pert(self, count, **kwargs):
340353
def _check_pert(self, **kwargs):
341354
"""Does the work of ensuring BetaPert distribution is valid"""
342355
conditions = {
343-
'mode >= low' : kwargs['mode'] >= kwargs['low'],
344-
'high >= mode' : kwargs['high'] >= kwargs['mode'],
356+
"mode >= low": kwargs["mode"] >= kwargs["low"],
357+
"high >= mode": kwargs["high"] >= kwargs["mode"],
345358
}
346359
for condition_name, condition_value in conditions.items():
347360
if condition_value == False:
348-
err = 'Param "{}" fails PERT requirement "{}".'.format(kwargs, condition_name)
361+
err = 'Param "{}" fails PERT requirement "{}".'.format(
362+
kwargs, condition_name
363+
)
349364
raise FairException(err)

0 commit comments

Comments
 (0)