From 7533553032de5a26a57dcd8698b839f930b1b276 Mon Sep 17 00:00:00 2001 From: coreyneskey Date: Wed, 28 May 2025 22:58:11 -0400 Subject: [PATCH 1/2] Fixes for vuln calculation and deprecations. --- pyfair/__init__.py | 2 +- pyfair/_version.py | 1 + pyfair/model/model_calc.py | 34 ++--- pyfair/model/model_input.py | 94 ++++++++------ pyfair/report/base_report.py | 221 +++++++++++++++++---------------- test_fair.py | 20 --- tests/model/test_model_calc.py | 46 +++---- tests/test_runner.py | 24 ++-- 8 files changed, 210 insertions(+), 232 deletions(-) create mode 100644 pyfair/_version.py delete mode 100644 test_fair.py diff --git a/pyfair/__init__.py b/pyfair/__init__.py index 08fd1e4..5dca5ad 100644 --- a/pyfair/__init__.py +++ b/pyfair/__init__.py @@ -1,6 +1,6 @@ """PyFair is an open source implementation of the FAIR methodology.""" -VERSION = '0.1-alpha.12' +from ._version import __version__ from . import model diff --git a/pyfair/_version.py b/pyfair/_version.py new file mode 100644 index 0000000..2a3adcf --- /dev/null +++ b/pyfair/_version.py @@ -0,0 +1 @@ +__version__ = "0.1-alpha.12" diff --git a/pyfair/model/model_calc.py b/pyfair/model/model_calc.py index b75511e..168ded9 100644 --- a/pyfair/model/model_calc.py +++ b/pyfair/model/model_calc.py @@ -13,16 +13,17 @@ class FairCalculations(object): 3) a multiplication function. """ + def __init__(self): # Lookup table for functions (no leaf nodes required) self._function_dict = { - 'Risk' : self._calculate_multiplication, - 'Loss Event Frequency' : self._calculate_multiplication, - 'Threat Event Frequency': self._calculate_multiplication, - 'Vulnerability' : self._calculate_step_average, - 'Loss Magnitude' : self._calculate_addition, - 'Primary Loss' : self._calculate_multiplication, - 'Secondary Loss' : self._calculate_multiplication, + "Risk": self._calculate_multiplication, + "Loss Event Frequency": self._calculate_multiplication, + "Threat Event Frequency": self._calculate_multiplication, + "Vulnerability": self._calculate_step_average, + "Loss Magnitude": self._calculate_addition, + "Primary Loss": self._calculate_multiplication, + "Secondary Loss": self._calculate_multiplication, } 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): return calculated_result def _calculate_step_average(self, child_1_data, child_2_data): - """Get bool series based on step function, then average for vuln""" + """Return per-simulation boolean (as float) for Vulnerability: 1.0 if TC > CS, else 0.0""" # Get Trues (1) where child_2 (TCap) is greater than child_1 (CS) - # Otherwise False (0) - bool_series = child_1_data < child_2_data - # Treat those bools as 1 and 0 and get mean - bool_scalar_average = bool_series.mean() - # Create a long array of that mean - vuln_data = np.full( - len(bool_series), - bool_scalar_average - ) - # And put it in a series - vuln = pd.Series( - data=vuln_data, - index=bool_series.index - ) + bool_series = (child_1_data < child_2_data).astype(float) + # Return the per-simulation result as a Series + vuln = pd.Series(data=bool_series.values, index=bool_series.index) return vuln def _calculate_addition(self, child_1_data, child_2_data): diff --git a/pyfair/model/model_input.py b/pyfair/model/model_input.py index 769329f..bc2a3c1 100644 --- a/pyfair/model/model_input.py +++ b/pyfair/model/model_input.py @@ -29,26 +29,32 @@ class FairDataInput(object): is stored when converting to JSON or another serialization format. """ + def __init__(self): # These targets must be less than or equal to one - self._le_1_targets = ['Probability of Action', 'Vulnerability', 'Control Strength', 'Threat Capability'] - self._le_1_keywords = ['constant', 'high', 'mode', 'low', 'mean'] + self._le_1_targets = [ + "Probability of Action", + "Vulnerability", + "Control Strength", + "Threat Capability", + ] + self._le_1_keywords = ["constant", "high", "mode", "low", "mean"] # Parameter map associates parameters with functions self._parameter_map = { - 'constant': self._gen_constant, - 'high' : self._gen_pert, - 'mode' : self._gen_pert, - 'low' : self._gen_pert, - 'gamma' : self._gen_pert, - 'mean' : self._gen_normal, - 'stdev' : self._gen_normal, + "constant": self._gen_constant, + "high": self._gen_pert, + "mode": self._gen_pert, + "low": self._gen_pert, + "gamma": self._gen_pert, + "mean": self._gen_normal, + "stdev": self._gen_normal, } # List of keywords with function keys self._required_keywords = { - self._gen_constant: ['constant'], - self._gen_pert : ['low', 'mode', 'high'], - self._gen_normal : ['mean', 'stdev'], - } + self._gen_constant: ["constant"], + self._gen_pert: ["low", "mode", "high"], + self._gen_normal: ["mean", "stdev"], + } # Storage of inputs self._supplied_values = {} @@ -59,7 +65,7 @@ def get_supplied_values(self): ------- dict A dictionary of the values supplied to generate function. The - keys for the dict will be the target node as a string (e.g. + keys for the dict will be the target node as a string (e.g. 'Loss Event Frequency') and the values will be a sub-dictionary of keyword arguments ({'low': 50, 'mode}: 51, 'high': 52}). @@ -80,7 +86,11 @@ def _check_le_1(self, target, **kwargs): pass # If not, raise error else: - raise FairException('"{}" must have "{}" value between zero and one.'.format(target, key)) + raise FairException( + '"{}" must have "{}" value between zero and one.'.format( + target, key + ) + ) def _check_parameters(self, target_function, **kwargs): """Runs parameter checks @@ -94,7 +104,7 @@ def _check_parameters(self, target_function, **kwargs): for keyword, value in kwargs.items(): # Two conditions value_is_less_than_zero = value < 0 - keyword_is_relevant = keyword in ['mean', 'constant', 'low', 'mode', 'high'] + keyword_is_relevant = keyword in ["mean", "constant", "low", "mode", "high"] # Test conditions if keyword_is_relevant and value_is_less_than_zero: raise FairException('"{}" is less than zero.'.format(keyword)) @@ -104,7 +114,11 @@ def _check_parameters(self, target_function, **kwargs): if required_keyword in kwargs.keys(): pass else: - raise FairException('"{}" is missing "{}".'.format(str(target_function), required_keyword)) + raise FairException( + '"{}" is missing "{}".'.format( + str(target_function), required_keyword + ) + ) def generate(self, target, count, **kwargs): """Executes request, records parameters, and return random values @@ -123,7 +137,7 @@ def generate(self, target, count, **kwargs): The number of random numbers generated (or alternatively, the length of the Series returned). **kwargs - Keyword arguments with one of the following values: {`mean`, + Keyword arguments with one of the following values: {`mean`, `stdev`, `low`, `mode`, `high`, `gamma`, or `constant`}. Raises @@ -146,8 +160,8 @@ def generate(self, target, count, **kwargs): result = self._generate_single(target, count, **kwargs) # Explicitly insert optional keywords for model storage dict_keys = kwargs.keys() - if 'low' in dict_keys and 'gamma' not in dict_keys: - kwargs['gamma'] = 4 + if "low" in dict_keys and "gamma" not in dict_keys: + kwargs["gamma"] = 4 # Record and return self._supplied_values[target] = {**kwargs} return result @@ -192,16 +206,16 @@ def generate_multi(self, prefixed_target, count, kwargs_dict): { 'Reputational': { - 'Secondary Loss Event Frequency': {'constant': 4000}, + 'Secondary Loss Event Frequency': {'constant': 4000}, 'Secondary Loss Event Magnitude': { 'low': 10, 'mode': 20, 'high': 100 }, }, 'Legal': { - 'Secondary Loss Event Frequency': {'constant': 2000}, + 'Secondary Loss Event Frequency': {'constant': 2000}, 'Secondary Loss Event Magnitude': { 'low': 10, 'mode': 20, 'high': 100 - }, + }, } } @@ -242,7 +256,7 @@ def generate_multi(self, prefixed_target, count, kwargs_dict): """ # Remove prefix from target - final_target = prefixed_target.lstrip('multi_') + final_target = prefixed_target.lstrip("multi_") # Create a container for dataframes df_dict = {target: pd.DataFrame() for target in kwargs_dict.keys()} # For each target @@ -255,9 +269,9 @@ def generate_multi(self, prefixed_target, count, kwargs_dict): # Put in dict df_dict[target][column] = s # Get partial secondary losses and sum up all the values - summed = np.sum(df.prod(axis=1) for df in df_dict.values()) + summed = sum(df.prod(axis=1) for df in df_dict.values()) # Record params - new_target = 'multi_' + final_target + new_target = "multi_" + final_target self._supplied_values[new_target] = kwargs_dict return summed @@ -294,12 +308,12 @@ def supply_raw(self, target, array): s = pd.Series(clean_array) # Check numeric and not null if s.isnull().any(): - raise FairException('Supplied data contains null values') + raise FairException("Supplied data contains null values") # Ensure values are appropriate if target in self._le_1_targets: if s.max() > 1 or s.min() < 0: - raise FairException(f'{target} data greater or less than one') - self._supplied_values[target] = {'raw': s.values.tolist()} + raise FairException(f"{target} data greater or less than one") + self._supplied_values[target] = {"raw": s.values.tolist()} return s.values def _determine_func(self, **kwargs): @@ -309,24 +323,22 @@ def _determine_func(self, **kwargs): if key not in self._parameter_map.keys(): raise FairException('"{}"" is not a recognized keyword'.format(key)) # Check whether all keys go to same function via set comprension - functions = list(set([ - self._parameter_map[key] - for key - in kwargs.keys() - ])) + functions = list(set([self._parameter_map[key] for key in kwargs.keys()])) if len(functions) > 1: - raise FairException('"{}" mixes incompatible keywords.'.format(str(kwargs.keys()))) + raise FairException( + '"{}" mixes incompatible keywords.'.format(str(kwargs.keys())) + ) else: function = functions[0] return function def _gen_constant(self, count, **kwargs): """Generates constant array of size `count`""" - return np.full(count, kwargs['constant']) + return np.full(count, kwargs["constant"]) def _gen_normal(self, count, **kwargs): """Geneates random normally-distributed array of size `count`""" - normal = scipy.stats.norm(loc=kwargs['mean'], scale=kwargs['stdev']) + normal = scipy.stats.norm(loc=kwargs["mean"], scale=kwargs["stdev"]) rvs = normal.rvs(count) return rvs @@ -340,10 +352,12 @@ def _gen_pert(self, count, **kwargs): def _check_pert(self, **kwargs): """Does the work of ensuring BetaPert distribution is valid""" conditions = { - 'mode >= low' : kwargs['mode'] >= kwargs['low'], - 'high >= mode' : kwargs['high'] >= kwargs['mode'], + "mode >= low": kwargs["mode"] >= kwargs["low"], + "high >= mode": kwargs["high"] >= kwargs["mode"], } for condition_name, condition_value in conditions.items(): if condition_value == False: - err = 'Param "{}" fails PERT requirement "{}".'.format(kwargs, condition_name) + err = 'Param "{}" fails PERT requirement "{}".'.format( + kwargs, condition_name + ) raise FairException(err) diff --git a/pyfair/report/base_report.py b/pyfair/report/base_report.py index 73477d4..df9f601 100644 --- a/pyfair/report/base_report.py +++ b/pyfair/report/base_report.py @@ -11,61 +11,57 @@ import numpy as np import pandas as pd -from .. import VERSION +from .._version import __version__ from .tree_graph import FairTreeGraph from .distribution import FairDistributionCurve from .exceedence import FairExceedenceCurves from ..utility.fair_exception import FairException from .violin import FairViolinPlot +from ..model.meta_model import FairMetaModel +from ..utility.beta_pert import FairBetaPert class FairBaseReport(object): """A base class for creating FairModel and FairMetaModel reports This class exists to provide a common base for mutliple report types. - It carries with it formatting data, file paths, and a variety of + It carries with it formatting data, file paths, and a variety of methods for creating report components. It is not intended to be instantiated on its own. """ - def __init__(self, currency_prefix='$'): + + def __init__(self, currency_prefix="$"): # Add formatting strings self._currency_prefix = currency_prefix self._model_or_models = None - self._currency_format_string = currency_prefix + '{0:,.0f}' - self._float_format_string = '{0:.2f}' + self._currency_format_string = currency_prefix + "{0:,.0f}" + self._float_format_string = "{0:.2f}" self._format_strings = { - 'Risk' : self._currency_format_string, - 'Loss Event Frequency' : self._float_format_string, - 'Threat Event Frequency' : self._float_format_string, - 'Vulnerability' : self._float_format_string, - 'Contact Frequency' : self._float_format_string, - 'Probability of Action' : self._float_format_string, - 'Threat Capability' : self._float_format_string, - 'Control Strength' : self._float_format_string, - 'Loss Magnitude' : self._currency_format_string, - 'Primary Loss' : self._currency_format_string, - 'Secondary Loss' : self._currency_format_string, - 'Secondary Loss Event Frequency' : self._float_format_string, - 'Secondary Loss Event Magnitude' : self._currency_format_string, + "Risk": self._currency_format_string, + "Loss Event Frequency": self._float_format_string, + "Threat Event Frequency": self._float_format_string, + "Vulnerability": self._float_format_string, + "Contact Frequency": self._float_format_string, + "Probability of Action": self._float_format_string, + "Threat Capability": self._float_format_string, + "Control Strength": self._float_format_string, + "Loss Magnitude": self._currency_format_string, + "Primary Loss": self._currency_format_string, + "Secondary Loss": self._currency_format_string, + "Secondary Loss Event Frequency": self._float_format_string, + "Secondary Loss Event Magnitude": self._currency_format_string, } # Add locations self._fair_location = pathlib.Path(__file__).parent.parent - self._static_location = self._fair_location / 'static' - self._logo_location = self._static_location / 'white_python_logo.png' + self._static_location = self._fair_location / "static" + self._logo_location = self._static_location / "white_python_logo.png" self._template_paths = { - 'css' : self._static_location / 'fair.css', - 'simple': self._static_location / 'simple.html' + "css": self._static_location / "fair.css", + "simple": self._static_location / "simple.html", } - self._param_cols = [ - 'low', - 'most_likely', - 'high', - 'constant', - 'mean', - 'stdev' - ] + self._param_cols = ["low", "most_likely", "high", "constant", "mean", "stdev"] def _input_check(self, value): """Check input value for report is appropriate @@ -76,25 +72,29 @@ def _input_check(self, value): """ # If it's a model or metamodel, plug it in a dict. rv = {} - if value.__class__.__name__ in ['FairModel', 'FairMetaModel']: + if value.__class__.__name__ in ["FairModel", "FairMetaModel"]: rv[value.get_name()] = value return rv # Check for iterable. - if not hasattr(value, '__iter__'): - raise FairException('Input is not a FairModel, FairMetaModel, or an iterable.') + if not hasattr(value, "__iter__"): + raise FairException( + "Input is not a FairModel, FairMetaModel, or an iterable." + ) if len(value) == 0: - raise FairException('Empty iterable where iterable of models expected.') + raise FairException("Empty iterable where iterable of models expected.") # Iterate and process remainder. for proported_model in value: # Check if model - if proported_model.__class__.__name__ in ['FairModel', 'FairMetaModel']: + if proported_model.__class__.__name__ in ["FairModel", "FairMetaModel"]: # Check if calculated if proported_model.calculation_completed(): rv[proported_model.get_name()] = proported_model else: - raise FairException('Model or FairModel has not been calculated.') + raise FairException("Model or FairModel has not been calculated.") else: - raise FairException('Iterable member is not a FairModel or FairMetaModel') + raise FairException( + "Iterable member is not a FairModel or FairMetaModel" + ) return rv def get_format_strings(self): @@ -107,16 +107,16 @@ def get_format_strings(self): """ return self._format_strings - def base64ify(self, image, alternative_text='', options=''): + def base64ify(self, image, alternative_text="", options=""): """Binary data into embeddable tag with base64 data - + To avoid having separate image files, pyfair simply embeds report images as base64 image tags. base64ify() is a convenience function that creates these tags. image : [bytes, str, pathlib.Path] The binary data, path string, or pathlib.Path containing either the data itself or a file of data. - + alternative_text: str, optional Alternative text to be showed in the event the image does not properly render @@ -134,15 +134,15 @@ def base64ify(self, image, alternative_text='', options=''): """ # If path, open and read. if type(image) == str or isinstance(image, pathlib.Path): - with open(image, 'rb') as f: + with open(image, "rb") as f: binary_data = f.read() # If bytes, jsut write elif type(image) == bytes: binary_data = image else: - raise TypeError(str(image) + ' is not a string, path, or bytes.') + raise TypeError(str(image) + " is not a string, path, or bytes.") # Get base64 string - base64_string = base64.b64encode(binary_data).decode('utf8') + base64_string = base64.b64encode(binary_data).decode("utf8") # Create tag tag = f'{alternative_text}' return tag @@ -162,13 +162,13 @@ def to_html(self, output_path): The output path to which the HTML data is written """ output = self._construct_output() - with open(output_path, 'w+') as f: + with open(output_path, "w+") as f: f.write(output) def _fig_to_img_tag(self, fig): """Converts matplotlib fig to base64 encoded img tag""" data = io.BytesIO() - fig.savefig(data, format='png', transparent=True) + fig.savefig(data, format="png", transparent=True) data.seek(0) img_tag = self.base64ify(data.read()) return img_tag @@ -176,11 +176,7 @@ def _fig_to_img_tag(self, fig): def _get_data_table(self, model): """Takes model and gnerates HTML table from the model's results""" data = model.export_results().dropna(axis=1) - table = data.to_html( - border=0, - justify='left', - classes='fair_metadata_table' - ) + table = data.to_html(border=0, justify="left", classes="fair_metadata_table") return table def _get_parameter_table(self, model): @@ -197,14 +193,22 @@ def _get_metadata_table(self): username = getpass.getuser() # The exception this throws is not conspicuously documented except Exception: - username = 'Unknown' + username = "Unknown" # Add metadata - metadata = pd.Series({ - 'Author': username, - 'Created': str(datetime.datetime.now()).partition('.')[0], - 'PyFair Version': VERSION, - 'Type': type(self).__name__ - }).to_frame().to_html(border=0, header=False, justify='left', classes='fair_metadata_table') + metadata = ( + pd.Series( + { + "Author": username, + "Created": str(datetime.datetime.now()).partition(".")[0], + "PyFair Version": __version__, + "Type": type(self).__name__, + } + ) + .to_frame() + .to_html( + border=0, header=False, justify="left", classes="fair_metadata_table" + ) + ) return metadata def _get_tree(self, model): @@ -224,7 +228,7 @@ def _get_distribution(self, model_or_models, currency_prefix): def _get_distribution_icon(self, model, target): """Create base64 icon string using FairDistributionCurve""" fdc = FairDistributionCurve(model, self._currency_prefix) - fig, ax = fdc.generate_icon(model.get_name(), target) + fig, ax = fdc.generate_icon(model.get_name(), target) img_tag = self._fig_to_img_tag(fig) return img_tag @@ -246,28 +250,34 @@ def _get_overview_table(self, model_or_models): """Create a risk overview table using a model or list of models""" # Get final Risk vectors for all models try: - risk_results = pd.DataFrame({ - name: model.export_results()['Risk'] - for name, model - in model_or_models.items() - }) + risk_results = pd.DataFrame( + { + name: model.export_results()["Risk"] + for name, model in model_or_models.items() + } + ) except KeyError: raise FairException("No 'Risk' key. Model likely uncalculated.") # Get aggregate statistics and set titles - risk_results = risk_results.agg(['mean', 'std', 'min', 'max']) - risk_results.index = ['Mean', 'Stdev', 'Minimum', 'Maximum'] + risk_results = risk_results.agg(["mean", "std", "min", "max"]) + risk_results.index = ["Mean", "Stdev", "Minimum", "Maximum"] # Format risk results into dataframe - overview_df = risk_results.applymap(lambda x: self._format_strings['Risk'].format(x)) - overview_df.loc['Simulations'] = [ - '{0:,.0f}'.format(len(model.export_results())) - for model - in model_or_models.values() + overview_df = risk_results.map(lambda x: self._format_strings["Risk"].format(x)) + overview_df.loc["Simulations"] = [ + "{0:,.0f}".format(len(model.export_results())) + for model in model_or_models.values() ] # Add data - overview_df.loc['Identifier'] = [model.get_uuid() for model in model_or_models.values()] - overview_df.loc['Model Type'] = [model.__class__.__name__ for model in model_or_models.values()] + overview_df.loc["Identifier"] = [ + model.get_uuid() for model in model_or_models.values() + ] + overview_df.loc["Model Type"] = [ + model.__class__.__name__ for model in model_or_models.values() + ] # Export df to HTML and return - overview_html = overview_df.to_html(border=0, header=True, justify='left', classes='fair_table') + overview_html = overview_df.to_html( + border=0, header=True, justify="left", classes="fair_table" + ) return overview_html def _get_model_parameter_table(self, model): @@ -276,8 +286,7 @@ def _get_model_parameter_table(self, model): # Remove items we don't want. params = { key: value - for key, value - in params.items() + for key, value in params.items() if key in self._format_strings.keys() } # Set up alias and dataframe @@ -291,10 +300,10 @@ def _get_model_parameter_table(self, model): param_df[column] = np.nan # Create descriptive statistics from parameter df param_df = param_df[self._param_cols] - param_df['mean'] = model.export_results().mean(axis=0) - param_df['stdev'] = model.export_results().std(axis=0) - param_df['min'] = model.export_results().min(axis=0) - param_df['max'] = model.export_results().max(axis=0) + param_df["mean"] = model.export_results().mean(axis=0) + param_df["stdev"] = model.export_results().std(axis=0) + param_df["min"] = model.export_results().min(axis=0) + param_df["max"] = model.export_results().max(axis=0) # Transform param_df in place param_df = param_df.apply( lambda row: pd.Series( @@ -302,31 +311,25 @@ def _get_model_parameter_table(self, model): # ... by getting the format string and formatting fs[row.name].format(item) # For each item - for item - in row + for item in row ], # And keep the index - index=row.index.values + index=row.index.values, ), # On a column basis axis=1, ) - param_df = param_df.applymap(lambda x: '' if 'nan' in x else x) + param_df = param_df.map(lambda x: "" if "nan" in x else x) # Do not truncate our base64 images. - pd.set_option('display.max_colwidth', None) + pd.set_option("display.max_colwidth", None) # Create our distribution icons as strings in table - param_df['distribution'] = [ + param_df["distribution"] = [ self._get_distribution_icon(model, target) - for target - in param_df.index.values + for target in param_df.index.values ] # Export table to html detail_table = param_df.to_html( - border=0, - header=True, - justify='left', - classes='fair_table', - escape=False + border=0, header=True, justify="left", classes="fair_table", escape=False ) return detail_table @@ -334,30 +337,30 @@ def _get_metamodel_parameter_table(self, metamodel): """Create table for metamodel""" # Create our table, transpose it, get descriptive statistics risk_df = metamodel.export_results().T - risk_df = pd.DataFrame({ - 'mean' : risk_df.mean(axis=1), - 'stdev': risk_df.std(axis=1), - 'min' : risk_df.min(axis=1), - 'max' : risk_df.max(axis=1), - 'geo_mean': risk_df.apply(lambda x: np.exp(np.mean(np.log(x[x > 0]))), axis=1), - 'mode': risk_df.mode(axis=1)[0], - '90th_percentile': risk_df.quantile(0.90, axis=1), - '99th_percentile': risk_df.quantile(0.99, axis=1) - }) + risk_df = pd.DataFrame( + { + "mean": risk_df.mean(axis=1), + "stdev": risk_df.std(axis=1), + "min": risk_df.min(axis=1), + "max": risk_df.max(axis=1), + "geo_mean": risk_df.apply( + lambda x: np.exp(np.mean(np.log(x[x > 0]))), axis=1 + ), + "mode": risk_df.mode(axis=1)[0], + "90th_percentile": risk_df.quantile(0.90, axis=1), + "99th_percentile": risk_df.quantile(0.99, axis=1), + } + ) # Format the risk DF with the appropriate strings risk_df = risk_df.apply( lambda row: pd.Series( - [self._format_strings['Risk'].format(item) for item in row], - index=row.index.values + [self._format_strings["Risk"].format(item) for item in row], + index=row.index.values, ), axis=1, ) # Do not truncate our base64 images. detail_table = risk_df.to_html( - border=0, - header=True, - justify='left', - classes='fair_table', - escape=False + border=0, header=True, justify="left", classes="fair_table", escape=False ) return detail_table diff --git a/test_fair.py b/test_fair.py deleted file mode 100644 index d210961..0000000 --- a/test_fair.py +++ /dev/null @@ -1,20 +0,0 @@ -from pyfair import FairModel - -# Create a simple FAIR model -model = FairModel(name="Basic Model", n_simulations=10000) - -# Set parameters -model.input_data("Loss Event Frequency", mean=0.3, stdev=0.1) -model.input_data("Loss Magnitude", constant=5000000) - -# Calculate and display results -model.calculate_all() - -# Get results and print summary statistics -results = model.export_results() -print("\nModel Results Summary:") -print("-" * 20) -print(f"Risk Statistics:") -print(f"Mean: ${results['Risk'].mean():,.2f}") -print(f"Median: ${results['Risk'].median():,.2f}") -print(f"95th Percentile: ${results['Risk'].quantile(0.95):,.2f}") diff --git a/tests/model/test_model_calc.py b/tests/model/test_model_calc.py index 6d6794e..fca9f47 100644 --- a/tests/model/test_model_calc.py +++ b/tests/model/test_model_calc.py @@ -9,59 +9,47 @@ class TestFairModelCalc(unittest.TestCase): # Raw data - _CHILD_1_DATA = pd.Series([1,2,3,4,5]) - _CHILD_2_DATA = pd.Series([5,4,3,2,1]) - _MULT_OUTPUT = pd.Series([5,8,9,8,5]) - _ADD_OUTPUT = pd.Series([6,6,6,6,6]) - _STEP_OUTPUT = pd.Series([.4, .4, .4, .4, .4]) + _CHILD_1_DATA = pd.Series([1, 2, 3, 4, 5]) + _CHILD_2_DATA = pd.Series([5, 4, 3, 2, 1]) + _MULT_OUTPUT = pd.Series([5, 8, 9, 8, 5]) + _ADD_OUTPUT = pd.Series([6, 6, 6, 6, 6]) + _STEP_OUTPUT = pd.Series([1, 1, 0, 0, 0], dtype=float) # Keys _MULTIPLICATION_ITEMS = [ - 'Risk', - 'Loss Event Frequency', - 'Threat Event Frequency', - 'Primary Loss', - 'Secondary Loss', + "Risk", + "Loss Event Frequency", + "Threat Event Frequency", + "Primary Loss", + "Secondary Loss", ] - _ADDITION_ITEMS = ['Loss Magnitude'] - _STEP_ITEMS = ['Vulnerability'] + _ADDITION_ITEMS = ["Loss Magnitude"] + _STEP_ITEMS = ["Vulnerability"] def setUp(self): self._calc = FairCalculations() - + def tearDown(self): self._calc = None def test_multiplication(self): """Test multiplication keywords and functions""" for key in self._MULTIPLICATION_ITEMS: - result = self._calc.calculate( - key, - self._CHILD_1_DATA, - self._CHILD_2_DATA - ) + result = self._calc.calculate(key, self._CHILD_1_DATA, self._CHILD_2_DATA) self.assertTrue(result.equals(self._MULT_OUTPUT)) def test_addition(self): """Test addition keywords and functions""" for key in self._ADDITION_ITEMS: - result = self._calc.calculate( - key, - self._CHILD_1_DATA, - self._CHILD_2_DATA - ) + result = self._calc.calculate(key, self._CHILD_1_DATA, self._CHILD_2_DATA) self.assertTrue(result.equals(self._ADD_OUTPUT)) def test_step_average(self): """Test step function keywords and functions""" for key in self._STEP_ITEMS: - result = self._calc.calculate( - key, - self._CHILD_1_DATA, - self._CHILD_2_DATA - ) + result = self._calc.calculate(key, self._CHILD_1_DATA, self._CHILD_2_DATA) self.assertTrue(result.equals(self._STEP_OUTPUT)) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_runner.py b/tests/test_runner.py index a11ab32..d2f368a 100644 --- a/tests/test_runner.py +++ b/tests/test_runner.py @@ -1,4 +1,5 @@ """Script to create and run a test suite.""" + import pathlib import sys import unittest @@ -54,16 +55,17 @@ utility.test_fair_exception, ] -# Create loader and suite -loader = unittest.TestLoader() -suite = unittest.TestSuite() +if __name__ == "__main__": + # Create loader and suite + loader = unittest.TestLoader() + suite = unittest.TestSuite() -# Add to suite -for test_module in test_modules: - loaded_test = loader.loadTestsFromModule(test_module) - suite.addTest(loaded_test) + # Add to suite + for test_module in test_modules: + loaded_test = loader.loadTestsFromModule(test_module) + suite.addTest(loaded_test) -# Create runner and run -runner = unittest.TextTestRunner(verbosity=5) -result = runner.run(suite) -sys.exit(0 if result.wasSuccessful() else 1) + # Create runner and run + runner = unittest.TextTestRunner(verbosity=5) + result = runner.run(suite) + sys.exit(0 if result.wasSuccessful() else 1) From 9488c40aaf685676acd4ba2e3e5d6d7feb129f26 Mon Sep 17 00:00:00 2001 From: coreyneskey Date: Wed, 28 May 2025 23:02:26 -0400 Subject: [PATCH 2/2] Fixes for vuln calculation and deprecations. --- pyfair/report/base_report.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyfair/report/base_report.py b/pyfair/report/base_report.py index df9f601..2692412 100644 --- a/pyfair/report/base_report.py +++ b/pyfair/report/base_report.py @@ -262,7 +262,9 @@ def _get_overview_table(self, model_or_models): risk_results = risk_results.agg(["mean", "std", "min", "max"]) risk_results.index = ["Mean", "Stdev", "Minimum", "Maximum"] # Format risk results into dataframe - overview_df = risk_results.map(lambda x: self._format_strings["Risk"].format(x)) + overview_df = risk_results.applymap( + lambda x: self._format_strings["Risk"].format(x) + ) overview_df.loc["Simulations"] = [ "{0:,.0f}".format(len(model.export_results())) for model in model_or_models.values() @@ -319,7 +321,7 @@ def _get_model_parameter_table(self, model): # On a column basis axis=1, ) - param_df = param_df.map(lambda x: "" if "nan" in x else x) + param_df = param_df.applymap(lambda x: "" if "nan" in x else x) # Do not truncate our base64 images. pd.set_option("display.max_colwidth", None) # Create our distribution icons as strings in table