From eae5fec6ec7199bc40e61fcb5dab9c9eb29bac0f Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 8 Oct 2024 18:33:29 +0200 Subject: [PATCH 01/27] refactor:start adding new logic for library --- mcda/configuration/configuration_validator.py | 517 ++++++++++++++++++ mcda/mcda_run.py | 1 - mcda/models/ProMCDA.py | 153 ++++++ mcda/utils/utils_for_main.py | 300 +--------- 4 files changed, 672 insertions(+), 299 deletions(-) create mode 100644 mcda/configuration/configuration_validator.py create mode 100644 mcda/models/ProMCDA.py diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py new file mode 100644 index 0000000..8154714 --- /dev/null +++ b/mcda/configuration/configuration_validator.py @@ -0,0 +1,517 @@ +import sys +import logging + +import numpy as np +import pandas as pd +from typing import Tuple, List, Union + +from sklearn.preprocessing import MinMaxScaler + +from mcda.utils.utils_for_main import pop_indexed_elements, check_norm_sum_weights, randomly_sample_all_weights, \ + randomly_sample_ix_weight + +log = logging.getLogger(__name__) +logging.getLogger('PIL').setLevel(logging.WARNING) +FORMATTER: str = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=FORMATTER) +logger = logging.getLogger("ProMCDA") + + +def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str], sensitivity: dict, robustness: dict, + monte_carlo: dict) -> dict: + + """ + Extracts relevant configuration values required for running the ProMCDA process. + + This function takes input parameters related to the decision matrix, polarity, sensitivity analysis, + robustness analysis, and Monte Carlo simulations, and returns a dictionary containing the necessary + configuration values for further processing. + + Parameters: + ----------- + The decision matrix containing the alternatives and indicators. + + A tuple indicating the polarity (positive/negative) of each indicator. + + A dictionary specifying the sensitivity analysis configuration, including whether sensitivity is enabled. + + A dictionary specifying the robustness analysis configuration, including which robustness options are + enabled (e.g., on single weights, on all weights, and on indicators). + + A dictionary containing Monte Carlo simulation parameters such as the number of runs and the random seed. + + :param input_matrix : pd.DataFrame + :param polarity : Tuple[str] + :param sensitivity : dict + :param robustness : dict + :param: monte_carlo : dict + :return: extracted_values: dict + """ + + extracted_values = { + "input_matrix": input_matrix, + "polarity": polarity, + # Sensitivity settings + "sensitivity_on": sensitivity["sensitivity_on"], + "normalization": sensitivity["normalization"], + "aggregation": sensitivity["aggregation"], + # Robustness settings + "robustness_on": robustness["robustness_on"], + "robustness_on_single_weights": robustness["on_single_weights"], + "robustness_on_all_weights": robustness["on_all_weights"], + "given_weights": robustness["given_weights"], + "robustness_on_indicators": robustness["on_indicators"], + # Monte Carlo settings + "monte_carlo_runs": monte_carlo["monte_carlo_runs"], + "num_cores": monte_carlo["num_cores"], + "random_seed": monte_carlo["random_seed"], + "marginal_distribution_for_each_indicator": monte_carlo["marginal_distribution_for_each_indicator"] + } + + return extracted_values + + +def check_configuration_values(extracted_values: dict) -> int: + """ + Validates the configuration settings for the ProMCDA process based on the input parameters. + + This function checks the validity of the input parameters related to sensitivity analysis, robustness analysis, + and Monte Carlo simulations. It ensures that the configuration is coherent and alerts the user to any inconsistencies. + + Parameters: + ----------- + A dictionary containing configuration values extracted from the input parameters. It includes: + - input_matrix (pd.DataFrame): The decision matrix for alternatives and indicators. + - polarity (Tuple[str]): A tuple indicating the polarity of each indicator. + - sensitivity_on (str): Indicates whether sensitivity analysis is enabled ("yes" or "no"). + - normalization (str): The normalization method to be used if sensitivity analysis is enabled. + - aggregation (str): The aggregation method to be used if sensitivity analysis is enabled. + - robustness_on (str): Indicates whether robustness analysis is enabled ("yes" or "no"). + - robustness_on_single_weights (str): Indicates if robustness is applied on individual weights. + - robustness_on_all_weights (str): Indicates if robustness is applied on all weights. + - robustness_on_indicators (str): Indicates if robustness is applied on indicators. + - monte_carlo_runs (int): The number of Monte Carlo simulation runs. + - random_seed (int): The seed for random number generation. + - marginal_distribution_for_each_indicator (List[str]): The distribution types for each indicator. + + Raises: + ------- + ValueError + If any configuration settings are found to be inconsistent or contradictory. + + Returns: + -------- + int + A flag indicating whether robustness analysis will be performed on indicators (1) or not (0). + + :param: extracted_values : dict + :return: is_robustness_indicators: int + """ + + is_robustness_indicators = 0 + is_robustness_weights = 0 + + # Access the values from the dictionary + input_matrix = extracted_values["input_matrix"] + polarity = extracted_values["polarity"] + sensitivity_on = extracted_values["sensitivity_on"] + normalization = extracted_values["normalization"] + aggregation = extracted_values["aggregation"] + robustness_on = extracted_values["robustness_on"] + robustness_on_single_weights = extracted_values["robustness_on_single_weights"] + robustness_on_all_weights = extracted_values["robustness_on_all_weights"] + robustness_on_indicators = extracted_values["robustness_on_indicators"] + monte_carlo_runs = extracted_values["monte_carlo_runs"] + random_seed = extracted_values["random_seed"] + marginal_distribution = extracted_values["marginal_distribution_for_each_indicator"] + + # Check for sensitivity-related configuration errors + if sensitivity_on == "no": + check_config_error(normalization not in ['minmax', 'target', 'standardized', 'rank'], + 'The available normalization functions are: minmax, target, standardized, rank.') + check_config_error(aggregation not in ['weighted_sum', 'geometric', 'harmonic', 'minimum'], + 'The available aggregation functions are: weighted_sum, geometric, harmonic, minimum.' + '\nWatch the correct spelling in the configuration.') + logger.info("ProMCDA will only use one pair of norm/agg functions: " + normalization + '/' + aggregation) + else: + logger.info("ProMCDA will use a set of different pairs of norm/agg functions") + + # Check for robustness-related configuration errors + if robustness_on == "no": + logger.info("ProMCDA will run without uncertainty on the indicators or weights") + else: + check_config_error((robustness_on_single_weights == "no" and + robustness_on_all_weights == "no" and + robustness_on_indicators == "no"), + 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' + 'weights or indicators. Please clarify it.') + + check_config_error((robustness_on_single_weights == "yes" and + robustness_on_all_weights == "yes" and + robustness_on_indicators == "no"), + 'Robustness analysis has been requested for the weights, but it’s unclear whether it should ' + 'be applied to all weights or just one at a time? Please clarify.') + + check_config_error(((robustness_on_single_weights == "yes" and + robustness_on_all_weights == "yes" and + robustness_on_indicators == "yes") or + (robustness_on_single_weights == "yes" and + robustness_on_all_weights == "no" and + robustness_on_indicators == "yes") or + (robustness_on_single_weights == "no" and + robustness_on_all_weights == "yes" and + robustness_on_indicators == "yes")), + 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' + 'weights or indicators. Please clarify.') + + # Check seetings for robustness analysis on weights or indicators + condition_robustness_on_weights = ( + (robustness_on_single_weights == 'yes' and + robustness_on_all_weights == 'no' and + robustness_on_indicators == 'no') or + (robustness_on_single_weights == 'no' and + robustness_on_all_weights == 'yes' and + robustness_on_indicators == 'no')) + + condition_robustness_on_indicators = ( + (robustness_on_single_weights == 'no' and + robustness_on_all_weights == 'no' and + robustness_on_indicators == 'yes')) + + is_robustness_weights, is_robustness_indicators = check_config_setting(condition_robustness_on_weights, + condition_robustness_on_indicators, + monte_carlo_runs, random_seed) + + # Check the input matrix for duplicated rows in the alternatives, + # rescale negative indicator values and drop the column containing the alternatives + input_matrix_no_alternatives = check_input_matrix(input_matrix) + + if is_robustness_indicators == 0: + num_indicators = input_matrix_no_alternatives.shape[1] + else: + num_non_exact_and_non_poisson = \ + len(marginal_distribution) - marginal_distribution.count('exact') - marginal_distribution.count('poisson') + num_indicators = (input_matrix_no_alternatives.shape[1] - num_non_exact_and_non_poisson) + + # Process indicators and weights based on input parameters in the configuration + polar, weights = process_indicators_and_weights(extracted_values, input_matrix_no_alternatives, is_robustness_indicators, + is_robustness_weights, polarity, monte_carlo_runs, num_indicators) + + # Check the number of indicators, weights, and polarities + try: + check_indicator_weights_polarities(num_indicators, polar, extracted_values) + except ValueError as e: + logging.error(str(e), stack_info=True) + raise + + return is_robustness_indicators + + # TODO: THIS PART GOES SOMEWHERE ELSE? + # MAYBE THE CHECKS RETURN A FLAG TO KNOW WHAT TO FUNCTION TO RUN + # If there is no uncertainty of the indicators: + if is_robustness_indicators == 0: + run_mcda_without_indicator_uncertainty(input_config, index_column_name, index_column_values, + input_matrix_no_alternatives, weights, f_norm, f_agg, + is_robustness_weights) + # else (i.e. there is uncertainty): + else: + run_mcda_with_indicator_uncertainty(input_config, input_matrix_no_alternatives, index_column_name, + index_column_values, mc_runs, random_seed, is_sensitivity, f_agg, + f_norm, + weights, polar, marginal_distribution) + + +def check_config_error(condition: bool, error_message: str): + """ + Check a condition and raise a ValueError with a specified error message if the condition is True. + + Parameters: + - condition (bool): The condition to check. + - error_message (str): The error message to raise if the condition is True. + + Raises: + - ValueError: If the condition is True, with the specified error message. + + :param error_message: str + :param condition: bool + :return: None + """ + + if condition: + logger.error('Error Message', stack_info=True) + raise ValueError(error_message) + + +def check_config_setting(condition_robustness_on_weights: bool, condition_robustness_on_indicators: bool, mc_runs: int, + random_seed: int) -> (int, int): + """ + Checks configuration settings and logs information messages. + + Returns: + - is_robustness_weights, is_robustness_indicators, booleans indicating if robustness is considered + on weights or indicators. + + Example: + ```python + is_robustness_weights, is_robustness_indicators = check_config_setting(True, False, 1000, 42) + ``` + + :param condition_robustness_on_weights: bool + :param condition_robustness_on_indicators: bool + :param mc_runs: int + :param random_seed: int + :return: (is_robustness_weights, is_robustness_indicators) + :rtype: Tuple[int, int] + """ + is_robustness_weights = 0 + is_robustness_indicators = 0 + + if condition_robustness_on_weights: + logger.info("ProMCDA will consider uncertainty on the weights.") + logger.info("Number of Monte Carlo runs: {}".format(mc_runs)) + logger.info("The random seed used is: {}".format(random_seed)) + is_robustness_weights = 1 + + elif condition_robustness_on_indicators: + logger.info("ProMCDA will consider uncertainty on the indicators.") + logger.info("Number of Monte Carlo runs: {}".format(mc_runs)) + logger.info("The random seed used is: {}".format(random_seed)) + is_robustness_indicators = 1 + + return is_robustness_weights, is_robustness_indicators + + +def process_indicators_and_weights(config: dict, input_matrix: pd.DataFrame, + is_robustness_indicators: int, is_robustness_weights: int, polar: List[str], + mc_runs: int, num_indicators: int) \ + -> Tuple[List[str], Union[list, List[list], dict]]: + """ + Process indicators and weights based on input parameters in the configuration. + + Parameters: + - config: the configuration dictionary. + - input_matrix: the input matrix without alternatives. + - is_robustness_indicators: a flag indicating whether the matrix should include indicator uncertainties + (0 or 1). + - is_robustness_weights: a flag indicating whether robustness analysis is considered for the weights (0 or 1). + - marginal_pdf: a list of marginal probability density functions for indicators. + - mc_runs: number of Monte Carlo runs for robustness analysis. + - num_indicators: the number of indicators in the input matrix. + + Raises: + - ValueError: If there are duplicated rows in the input matrix or if there is an issue with the configuration. + + Returns: + - a shorter list of polarities if one has been dropped together with the relative indicator, + which brings no information. Otherwise, the same list. + - the normalised weights (either fixed or random sampled weights, depending on the settings) + + Notes: + - For is_robustness_indicators == 0: + - Identifies and removes columns with constant values. + - Logs the number of alternatives and indicators. + + - For is_robustness_indicators == 1: + - Handles uncertainty in indicators. + - Logs the number of alternatives and indicators. + + - For is_robustness_weights == 0: + - Processes fixed weights if given. + - Logs weights and normalised weights. + + - For is_robustness_weights == 1: + - Performs robustness analysis on weights. + - Logs randomly sampled weights. + + :param mc_runs: int + :param polar: List[str] + :param is_robustness_weights: int + :param is_robustness_indicators: int + :param input_matrix: pd.DataFrame + :param config: dict + :param num_indicators: int + :return: polar, norm_weights + :rtype: Tuple[List[str], Union[List[list], dict]] + """ + num_unique = input_matrix.nunique() + cols_to_drop = num_unique[num_unique == 1].index + col_to_drop_indexes = input_matrix.columns.get_indexer(cols_to_drop) + + if is_robustness_indicators == 0: + _handle_no_robustness_indicators(input_matrix) + else: # matrix with uncertainty on indicators + logger.info("Number of alternatives: {}".format(input_matrix.shape[0])) + logger.info("Number of indicators: {}".format(num_indicators)) + # TODO: eliminate indicators with constant values (i.e. same mean and 0 std) - optional + + polarities_and_weights = _handle_polarities_and_weights(is_robustness_indicators, is_robustness_weights, num_unique, + col_to_drop_indexes, polar, config, mc_runs, num_indicators) + + polar, norm_weights = tuple(item for item in polarities_and_weights if item is not None) + + return polar, norm_weights + + +def _handle_polarities_and_weights(is_robustness_indicators: int, is_robustness_weights: int, num_unique, + col_to_drop_indexes: np.ndarray, polar: List[str], config: dict, mc_runs: int, + num_indicators: int) \ + -> Union[Tuple[List[str], list, None, None], Tuple[List[str], None, List[List], None], + Tuple[List[str], None, None, dict]]: + """ + Manage polarities and weights based on the specified robustness settings, ensuring that the appropriate adjustments + and normalizations are applied before returning the necessary data structures. + """ + norm_random_weights = [] + rand_weight_per_indicator = {} + + # Managing polarities + if is_robustness_indicators == 0: + if any(value == 1 for value in num_unique): + polar = pop_indexed_elements(col_to_drop_indexes, polar) + logger.info("Polarities: {}".format(polar)) + + # Managing weights + if is_robustness_weights == 0: + fixed_weights = config["given_weights"] + if any(value == 1 for value in num_unique): + fixed_weights = pop_indexed_elements(col_to_drop_indexes, fixed_weights) + norm_fixed_weights = check_norm_sum_weights(fixed_weights) + logger.info("Weights: {}".format(fixed_weights)) + logger.info("Normalized weights: {}".format(norm_fixed_weights)) + return polar, norm_fixed_weights, None, None + # Return None for norm_random_weights and rand_weight_per_indicator + else: + output_weights = _handle_robustness_weights(config, mc_runs, num_indicators) + if output_weights is not None: + norm_random_weights, rand_weight_per_indicator = output_weights + if norm_random_weights: + return polar, None, norm_random_weights, None + else: + return polar, None, None, rand_weight_per_indicator + # Return None for norm_fixed_weights and one of the other two cases of randomness + + +def _handle_robustness_weights(config: dict, mc_runs: int, num_indicators: int) \ + -> Tuple[Union[List[list], None], Union[dict, None]]: + """ + Handle the generation and normalization of random weights based on the specified settings + when a robustness analysis is requested on all the weights. + """ + norm_random_weights = [] + rand_weight_per_indicator = {} + + if mc_runs == 0: + logger.error('Error Message', stack_info=True) + raise ValueError('The number of MC runs should be larger than 0 for a robustness analysis') + + if config["robustness_on_single_weights"] == "no" and config["robustness_on_all_weights"] == "yes": + random_weights = randomly_sample_all_weights(num_indicators, mc_runs) + for weights in random_weights: + weights = check_norm_sum_weights(weights) + norm_random_weights.append(weights) + return norm_random_weights, None # Return norm_random_weights, and None for rand_weight_per_indicator + elif config["robustness_on_single_weights"] == "yes" and config["robustness_on_all_weights"] == "no": + i = 0 + while i < num_indicators: + random_weights = randomly_sample_ix_weight(num_indicators, i, mc_runs) + norm_random_weight = [] + for weights in random_weights: + weights = check_norm_sum_weights(weights) + norm_random_weight.append(weights) + rand_weight_per_indicator["indicator_{}".format(i + 1)] = norm_random_weight + i += 1 + return None, rand_weight_per_indicator # Return None for norm_random_weights, and rand_weight_per_indicator + + +def _handle_no_robustness_indicators(input_matrix: pd.DataFrame): + """ + Handle the indicators in case of no robustness analysis required. + (The input matrix is without the alternative column) + """ + num_unique = input_matrix.nunique() + cols_to_drop = num_unique[num_unique == 1].index + + if any(value == 1 for value in num_unique): + logger.info("Indicators {} have been dropped because they carry no information".format(cols_to_drop)) + input_matrix = input_matrix.drop(cols_to_drop, axis=1) + + num_indicators = input_matrix.shape[1] + logger.info("Number of alternatives: {}".format(input_matrix.shape[0])) + logger.info("Number of indicators: {}".format(num_indicators)) + + +def check_indicator_weights_polarities(num_indicators: int, polar: List[str], config: dict): + """ + Check the consistency of indicators, polarities, and fixed weights in a configuration. + + Parameters: + - num_indicators: the number of indicators in the input matrix. + - polar: a list containing the polarity associated to each indicator. + - config: the configuration dictionary. + + This function raises a ValueError if the following conditions are not met: + 1. The number of indicators does not match the number of polarities. + 2. "robustness_on_all_weights" is set to "no," and the number of fixed weights + does not correspond to the number of indicators. + + Raises: + - ValueError: if the conditions for indicator-polarity and fixed weights consistency are not met. + + :param num_indicators: int + :param polar: List[str] + :param config: dict + :return: None + """ + if num_indicators != len(polar): + raise ValueError('The number of polarities does not correspond to the no. of indicators') + + # Check the number of fixed weights if "robustness_on_all_weights" is set to "no" + if (config["robustness_on_all_weights"] == "no") and ( + num_indicators != len(config["given_weights"])): + raise ValueError('The no. of fixed weights does not correspond to the no. of indicators') + + +def check_input_matrix(input_matrix: pd.DataFrame) -> pd.DataFrame: + """ + Check the input matrix for duplicated rows in the alternatives column, rescale negative indicator values + and drop the index column of alternatives. + + Parameters: + - input_matrix: The input matrix containing the alternatives and indicators. + + Raises: + - ValueError: If duplicated rows are found in the alternative column. + - UserStoppedInfo: If the user chooses to stop when duplicates are found. + + :param input_matrix: pd.DataFrame + :rtype: pd.DataFrame + :return: input_matrix + """ + if input_matrix.duplicated().any(): + raise ValueError('Error: Duplicated rows in the alternatives column.') + elif input_matrix.iloc[:, 0].duplicated().any(): + logger.info('Duplicated rows in the alternatives column.') + + index_column_values = input_matrix.index.tolist() + logger.info("Alternatives are {}".format(index_column_values)) + input_matrix_no_alternatives = input_matrix.reset_index(drop=True) # drop the alternative + + input_matrix_no_alternatives = _check_and_rescale_negative_indicators( + input_matrix_no_alternatives) + + return input_matrix_no_alternatives + + +def _check_and_rescale_negative_indicators(input_matrix: pd.DataFrame) -> pd.DataFrame: + """ + Rescale indicators of the input matrix if negative into [0-1]. + """ + + if (input_matrix < 0).any().any(): + scaler = MinMaxScaler() + scaled_data = scaler.fit_transform(input_matrix) + scaled_matrix = pd.DataFrame( + scaled_data, columns=input_matrix.columns, index=input_matrix.index) + return scaled_matrix + else: + return input_matrix \ No newline at end of file diff --git a/mcda/mcda_run.py b/mcda/mcda_run.py index 6337882..3e064a9 100644 --- a/mcda/mcda_run.py +++ b/mcda/mcda_run.py @@ -50,7 +50,6 @@ def main(input_config: dict): f_norm = None f_agg = None marginal_pdf = [] - num_unique = [] t = time.time() diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py new file mode 100644 index 0000000..ec7699f --- /dev/null +++ b/mcda/models/ProMCDA.py @@ -0,0 +1,153 @@ +import sys +import time +import logging +import pandas as pd +from typing import Tuple + +from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values + +log = logging.getLogger(__name__) + +FORMATTER: str = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=FORMATTER) +logger = logging.getLogger("ProMCDA") + + +class ProMCDA: + def __init__(self, input_matrix:pd.DataFrame, polarity:Tuple[str], sensitivity:dict, robustness:dict, + monte_carlo:dict): + """ + Initialize the ProMCDA class with configuration parameters. + + :param input_matrix: DataFrame containing the alternatives and criteria. + :param polarity: List of polarity for each indicator (+ or -). + :param sensitivity: Sensitivity analysis configuration. + :param robustness: Robustness analysis configuration. + :param monte_carlo: Monte Carlo sampling configuration. + """ + self.input_matrix = input_matrix + self.polarity = polarity + self.sensitivity = sensitivity + self.robustness = robustness + self.monte_carlo = monte_carlo + + #self.validate_input_parameters_keys # TODO: still need a formal check as made in old config class, + # maybe use some of following functions validate_ + is_robustness_indicators = self.validate_inputs() + self.run_mcda(is_robustness_indicators) + + self.normalized_matrix = None + self.aggregated_matrix = None + self.ranked_matrix = None + + #self.logger = logging.getLogger("ProMCDA") + + + def validate_inputs(self) -> int: + """ + Extract and validate input configuration parameters to ensure they are correct. + Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). + """ + configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, + self.robustness, self.monte_carlo) + is_robustness_indicators = check_configuration_values(configuration_values) + + # Validate input TODO: move into a different function validate_input_parameters_keys + #self.validate_normalization(self.sensitivity['normalization']) + #self.validate_aggregation(self.sensitivity['aggregation']) + #self.validate_robustness(self.robustness) + + return is_robustness_indicators + + def validate_normalization(self, f_norm): + """ + Validate the normalization method. + """ + valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] + if f_norm not in valid_norm_methods: + raise ValueError(f"Invalid normalization method: {f_norm}. Available methods: {valid_norm_methods}") + + def validate_aggregation(self, f_agg): + """ + Validate the aggregation method. + """ + valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] + if f_agg not in valid_agg_methods: + raise ValueError(f"Invalid aggregation method: {f_agg}. Available methods: {valid_agg_methods}") + + def validate_robustness(self, robustness): + """ + Validate robustness analysis settings. + """ + if not isinstance(robustness, dict): + raise ValueError("Robustness settings must be a dictionary.") + + # Add more specific checks based on robustness config structure + if robustness['on_single_weights'] == 'yes' and robustness['on_all_weights'] == 'yes': + raise ValueError("Conflicting settings for robustness analysis on weights.") + + def normalize(self): + """ + Normalize the decision matrix based on the configuration. + """ + f_norm = self.sensitivity['normalization'] + self.logger.info("Normalizing matrix with method: %s", f_norm) + + # Perform normalization (replace this with actual logic) + self.normalized_matrix = normalize_matrix(self.input_matrix, f_norm) + + return self.normalized_matrix + + def aggregate(self): + """ + Aggregate the decision matrix based on the configuration. + """ + f_agg = self.sensitivity['aggregation'] + self.logger.info("Aggregating matrix with method: %s", f_agg) + + # Perform aggregation (replace this with actual logic) + self.aggregated_matrix = aggregate_matrix(self.normalized_matrix, f_agg) + + return self.aggregated_matrix + + def run_mcda(self, is_robustness_indicators: int): + """ + Execute the full ProMCDA process. + """ + start_time = time.time() + + # Normalize + self.normalize() + + # Aggregate + self.aggregate() + + # Run + # no uncertainty + if is_robustness_indicators == 0: + run_mcda_without_indicator_uncertainty(input_config, index_column_name, index_column_values, + input_matrix_no_alternatives, weights, f_norm, f_agg, + is_robustness_weights) + # uncertainty + else: + run_mcda_with_indicator_uncertainty(input_config, input_matrix_no_alternatives, index_column_name, + index_column_values, mc_runs, random_seed, is_sensitivity, f_agg, + f_norm, + weights, polar, marginal_distribution) + + elapsed_time = time.time() - start_time + self.logger.info("ProMCDA finished calculations in %s seconds", elapsed_time) + + def get_results(self): + """ + Return the final results as a DataFrame or other relevant structure. + """ + # Return the aggregated results (or any other relevant results) + return self.aggregated_matrix + + +# Example of instantiating the class and using it +promcda_object = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) +promcda_object.run_mcda() +df_normalized = promcda_object.normalized_matrix +df_aggregated = promcda_object.get_results() \ No newline at end of file diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index ea6f789..7f2b9a9 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -13,7 +13,6 @@ import pandas as pd from datetime import datetime from sklearn import preprocessing -from sklearn.preprocessing import MinMaxScaler import mcda.utils.utils_for_parallelization as utils_for_parallelization import mcda.utils.utils_for_plotting as utils_for_plotting @@ -39,287 +38,6 @@ logger = logging.getLogger("ProMCDA") -def check_config_error(condition: bool, error_message: str): - """ - Check a condition and raise a ValueError with a specified error message if the condition is True. - - Parameters: - - condition (bool): The condition to check. - - error_message (str): The error message to raise if the condition is True. - - Raises: - - ValueError: If the condition is True, with the specified error message. - - :param error_message: str - :param condition: bool - :return: None - """ - - if condition: - logger.error('Error Message', stack_info=True) - raise ValueError(error_message) - - -def check_config_setting(condition_robustness_on_weights: bool, condition_robustness_on_indicators: bool, mc_runs: int, - random_seed: int) -> (int, int): - """ - Checks configuration settings and logs information messages. - - Returns: - - is_robustness_weights, is_robustness_indicators, booleans indicating if robustness is considered - on weights or indicators. - - Example: - ```python - is_robustness_weights, is_robustness_indicators = check_config_setting(True, False, 1000, 42) - ``` - - :param condition_robustness_on_weights: bool - :param condition_robustness_on_indicators: bool - :param mc_runs: int - :param random_seed: int - :return: (is_robustness_weights, is_robustness_indicators) - :rtype: Tuple[int, int] - """ - is_robustness_weights = 0 - is_robustness_indicators = 0 - - if condition_robustness_on_weights: - logger.info("ProMCDA will consider uncertainty on the weights.") - logger.info("Number of Monte Carlo runs: {}".format(mc_runs)) - logger.info("The random seed used is: {}".format(random_seed)) - is_robustness_weights = 1 - - elif condition_robustness_on_indicators: - logger.info("ProMCDA will consider uncertainty on the indicators.") - logger.info("Number of Monte Carlo runs: {}".format(mc_runs)) - logger.info("The random seed used is: {}".format(random_seed)) - is_robustness_indicators = 1 - - return is_robustness_weights, is_robustness_indicators - - -def process_indicators_and_weights(config: dict, input_matrix: pd.DataFrame, - is_robustness_indicators: int, is_robustness_weights: int, polar: List[str], - mc_runs: int, num_indicators: int) \ - -> Tuple[List[str], Union[list, List[list], dict]]: - """ - Process indicators and weights based on input parameters in the configuration. - - Parameters: - - config: the configuration dictionary. - - input_matrix: the input matrix without alternatives. - - is_robustness_indicators: a flag indicating whether the matrix should include indicator uncertainties - (0 or 1). - - is_robustness_weights: a flag indicating whether robustness analysis is considered for the weights (0 or 1). - - marginal_pdf: a list of marginal probability density functions for indicators. - - mc_runs: number of Monte Carlo runs for robustness analysis. - - num_indicators: the number of indicators in the input matrix. - - Raises: - - ValueError: If there are duplicated rows in the input matrix or if there is an issue with the configuration. - - Returns: - - a shorter list of polarities if one has been dropped together with the relative indicator, - which brings no information. Otherwise, the same list. - - the normalised weights (either fixed or random sampled weights, depending on the settings) - - Notes: - - For is_robustness_indicators == 0: - - Identifies and removes columns with constant values. - - Logs the number of alternatives and indicators. - - - For is_robustness_indicators == 1: - - Handles uncertainty in indicators. - - Logs the number of alternatives and indicators. - - - For is_robustness_weights == 0: - - Processes fixed weights if given. - - Logs weights and normalised weights. - - - For is_robustness_weights == 1: - - Performs robustness analysis on weights. - - Logs randomly sampled weights. - - :param mc_runs: int - :param polar: List[str] - :param is_robustness_weights: int - :param is_robustness_indicators: int - :param input_matrix: pd.DataFrame - :param config: dict - :param num_indicators: int - :return: polar, norm_weights - :rtype: Tuple[List[str], Union[List[list], dict]] - """ - num_unique = input_matrix.nunique() - cols_to_drop = num_unique[num_unique == 1].index - col_to_drop_indexes = input_matrix.columns.get_indexer(cols_to_drop) - - if is_robustness_indicators == 0: - _handle_no_robustness_indicators(input_matrix) - else: # matrix with uncertainty on indicators - logger.info("Number of alternatives: {}".format(input_matrix.shape[0])) - logger.info("Number of indicators: {}".format(num_indicators)) - # TODO: eliminate indicators with constant values (i.e. same mean and 0 std) - optional - - polarities_and_weights = _handle_polarities_and_weights(is_robustness_indicators, is_robustness_weights, num_unique, - col_to_drop_indexes, polar, config, mc_runs, num_indicators) - - polar, norm_weights = tuple(item for item in polarities_and_weights if item is not None) - - return polar, norm_weights - - -def _handle_polarities_and_weights(is_robustness_indicators: int, is_robustness_weights: int, num_unique, - col_to_drop_indexes: np.ndarray, polar: List[str], config: dict, mc_runs: int, - num_indicators: int) \ - -> Union[Tuple[List[str], list, None, None], Tuple[List[str], None, List[List], None], - Tuple[List[str], None, None, dict]]: - """ - Manage polarities and weights based on the specified robustness settings, ensuring that the appropriate adjustments - and normalizations are applied before returning the necessary data structures. - """ - norm_random_weights = [] - rand_weight_per_indicator = {} - - # Managing polarities - if is_robustness_indicators == 0: - if any(value == 1 for value in num_unique): - polar = pop_indexed_elements(col_to_drop_indexes, polar) - logger.info("Polarities: {}".format(polar)) - - # Managing weights - if is_robustness_weights == 0: - fixed_weights = config.robustness["given_weights"] - if any(value == 1 for value in num_unique): - fixed_weights = pop_indexed_elements(col_to_drop_indexes, fixed_weights) - norm_fixed_weights = check_norm_sum_weights(fixed_weights) - logger.info("Weights: {}".format(fixed_weights)) - logger.info("Normalized weights: {}".format(norm_fixed_weights)) - return polar, norm_fixed_weights, None, None - # Return None for norm_random_weights and rand_weight_per_indicator - else: - output_weights = _handle_robustness_weights(config, mc_runs, num_indicators) - if output_weights is not None: - norm_random_weights, rand_weight_per_indicator = output_weights - if norm_random_weights: - return polar, None, norm_random_weights, None - else: - return polar, None, None, rand_weight_per_indicator - # Return None for norm_fixed_weights and one of the other two cases of randomness - - -def _handle_robustness_weights(config: dict, mc_runs: int, num_indicators: int) \ - -> Tuple[Union[List[list], None], Union[dict, None]]: - """ - Handle the generation and normalization of random weights based on the specified settings - when a robustness analysis is requested on all the weights. - """ - norm_random_weights = [] - rand_weight_per_indicator = {} - - if mc_runs == 0: - logger.error('Error Message', stack_info=True) - raise ValueError('The number of MC runs should be larger than 0 for a robustness analysis') - - if config.robustness["on_single_weights"] == "no" and config.robustness["on_all_weights"] == "yes": - random_weights = randomly_sample_all_weights(num_indicators, mc_runs) - for weights in random_weights: - weights = check_norm_sum_weights(weights) - norm_random_weights.append(weights) - return norm_random_weights, None # Return norm_random_weights, and None for rand_weight_per_indicator - elif config.robustness["on_single_weights"] == "yes" and config.robustness["on_all_weights"] == "no": - i = 0 - while i < num_indicators: - random_weights = randomly_sample_ix_weight(num_indicators, i, mc_runs) - norm_random_weight = [] - for weights in random_weights: - weights = check_norm_sum_weights(weights) - norm_random_weight.append(weights) - rand_weight_per_indicator["indicator_{}".format(i + 1)] = norm_random_weight - i += 1 - return None, rand_weight_per_indicator # Return None for norm_random_weights, and rand_weight_per_indicator - - -def _handle_no_robustness_indicators(input_matrix: pd.DataFrame): - """ - Handle the indicators in case of no robustness analysis required. - (The input matrix is without the alternative column) - """ - num_unique = input_matrix.nunique() - cols_to_drop = num_unique[num_unique == 1].index - - if any(value == 1 for value in num_unique): - logger.info("Indicators {} have been dropped because they carry no information".format(cols_to_drop)) - input_matrix = input_matrix.drop(cols_to_drop, axis=1) - - num_indicators = input_matrix.shape[1] - logger.info("Number of alternatives: {}".format(input_matrix.shape[0])) - logger.info("Number of indicators: {}".format(num_indicators)) - - -def check_indicator_weights_polarities(num_indicators: int, polar: List[str], config: dict): - """ - Check the consistency of indicators, polarities, and fixed weights in a configuration. - - Parameters: - - num_indicators: the number of indicators in the input matrix. - - polar: a list containing the polarity associated to each indicator. - - config: the configuration dictionary. - - This function raises a ValueError if the following conditions are not met: - 1. The number of indicators does not match the number of polarities. - 2. "on_all_weights" is set to "no," and the number of fixed weights - does not correspond to the number of indicators. - - Raises: - - ValueError: if the conditions for indicator-polarity and fixed weights consistency are not met. - - :param num_indicators: int - :param polar: List[str] - :param config: dict - :return: None - """ - if num_indicators != len(polar): - raise ValueError('The number of polarities does not correspond to the no. of indicators') - - # Check the number of fixed weights if "on_all_weights" is set to "no" - if (config.robustness["on_all_weights"] == "no") and ( - num_indicators != len(config.robustness["given_weights"])): - raise ValueError('The no. of fixed weights does not correspond to the no. of indicators') - - -def check_input_matrix(input_matrix: pd.DataFrame) -> pd.DataFrame: - """ - Check the input matrix for duplicated rows in the alternatives column, rescale negative indicator values - and drop the index column of alternatives. - - Parameters: - - input_matrix: The input matrix containing the alternatives and indicators. - - Raises: - - ValueError: If duplicated rows are found in the alternative column. - - UserStoppedInfo: If the user chooses to stop when duplicates are found. - - :param input_matrix: pd.DataFrame - :rtype: pd.DataFrame - :return: input_matrix - """ - if input_matrix.duplicated().any(): - raise ValueError('Error: Duplicated rows in the alternatives column.') - elif input_matrix.iloc[:, 0].duplicated().any(): - logger.info('Duplicated rows in the alternatives column.') - - index_column_values = input_matrix.index.tolist() - logger.info("Alternatives are {}".format(index_column_values)) - input_matrix_no_alternatives = input_matrix.reset_index(drop=True) # drop the alternative - - input_matrix_no_alternatives = _check_and_rescale_negative_indicators( - input_matrix_no_alternatives) - - return input_matrix_no_alternatives - - def ensure_directory_exists(path): """ Ensure that the directory specified by the given path exists. @@ -345,7 +63,7 @@ def ensure_directory_exists(path): raise # Re-raise the exception to propagate it to the caller - +# TODO: maybe give the option of giving either a pd.DataFrame or a path as input parameter in ProMCDA def read_matrix(input_matrix_path: str) -> pd.DataFrame: """ Read an input matrix from a CSV file and return it as a DataFrame. @@ -395,21 +113,6 @@ def reset_index_if_needed(series): return series -def _check_and_rescale_negative_indicators(input_matrix: pd.DataFrame) -> pd.DataFrame: - """ - Rescale indicators of the input matrix if negative into [0-1]. - """ - - if (input_matrix < 0).any().any(): - scaler = MinMaxScaler() - scaled_data = scaler.fit_transform(input_matrix) - scaled_matrix = pd.DataFrame( - scaled_data, columns=input_matrix.columns, index=input_matrix.index) - return scaled_matrix - else: - return input_matrix - - def parse_args(): """ Parse command line arguments for configuration path. @@ -487,6 +190,7 @@ def save_df(df: pd.DataFrame, folder_path: str, filename: str): except IOError as e: logging.error(f"Error while writing data frame into a CSV file: {e}") + def save_dict(dictionary: dict, folder_path: str, filename: str): """ Save a dictionary to a binary file using pickle with a timestamped filename. From 61bf9a448d8ce26e93b6dbf34b12cc4519eb79c3 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 10 Oct 2024 17:06:21 +0200 Subject: [PATCH 02/27] refactor: add unit test for ProMCDA.py --- mcda/configuration/configuration_validator.py | 28 +-- mcda/mcda_without_robustness.py | 157 --------------- mcda/models/ProMCDA.py | 186 +++++++++--------- mcda/models/__init__.py | 0 mcda/{ => models}/mcda_with_robustness.py | 0 mcda/models/mcda_without_robustness.py | 0 mcda/utils/utils_for_main.py | 89 +++++---- tests/unit_tests/test_mcda_with_robustness.py | 2 +- .../test_mcda_without_robustness.py | 2 +- tests/unit_tests/test_promcda.py | 65 ++++++ .../test_utils_for_parallelization.py | 2 +- 11 files changed, 216 insertions(+), 315 deletions(-) delete mode 100644 mcda/mcda_without_robustness.py create mode 100644 mcda/models/__init__.py rename mcda/{ => models}/mcda_with_robustness.py (100%) create mode 100644 mcda/models/mcda_without_robustness.py create mode 100644 tests/unit_tests/test_promcda.py diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index 8154714..02151e1 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -18,7 +18,7 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str], sensitivity: dict, robustness: dict, - monte_carlo: dict) -> dict: + monte_carlo: dict, output_path: str) -> dict: """ Extracts relevant configuration values required for running the ProMCDA process. @@ -45,6 +45,7 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str :param sensitivity : dict :param robustness : dict :param: monte_carlo : dict + :param: output_path: str :return: extracted_values: dict """ @@ -53,8 +54,8 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str "polarity": polarity, # Sensitivity settings "sensitivity_on": sensitivity["sensitivity_on"], - "normalization": sensitivity["normalization"], - "aggregation": sensitivity["aggregation"], + "normalization": None if sensitivity["sensitivity_on"] == 'yes' else sensitivity["normalization"], + "aggregation": None if sensitivity["sensitivity_on"] == 'yes' else sensitivity["aggregation"], # Robustness settings "robustness_on": robustness["robustness_on"], "robustness_on_single_weights": robustness["on_single_weights"], @@ -65,13 +66,14 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str "monte_carlo_runs": monte_carlo["monte_carlo_runs"], "num_cores": monte_carlo["num_cores"], "random_seed": monte_carlo["random_seed"], - "marginal_distribution_for_each_indicator": monte_carlo["marginal_distribution_for_each_indicator"] + "marginal_distribution_for_each_indicator": monte_carlo["marginal_distribution_for_each_indicator"], + "output_path": output_path } return extracted_values -def check_configuration_values(extracted_values: dict) -> int: +def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[str], Union[list, List[list], dict]]: """ Validates the configuration settings for the ProMCDA process based on the input parameters. @@ -204,21 +206,7 @@ def check_configuration_values(extracted_values: dict) -> int: logging.error(str(e), stack_info=True) raise - return is_robustness_indicators - - # TODO: THIS PART GOES SOMEWHERE ELSE? - # MAYBE THE CHECKS RETURN A FLAG TO KNOW WHAT TO FUNCTION TO RUN - # If there is no uncertainty of the indicators: - if is_robustness_indicators == 0: - run_mcda_without_indicator_uncertainty(input_config, index_column_name, index_column_values, - input_matrix_no_alternatives, weights, f_norm, f_agg, - is_robustness_weights) - # else (i.e. there is uncertainty): - else: - run_mcda_with_indicator_uncertainty(input_config, input_matrix_no_alternatives, index_column_name, - index_column_values, mc_runs, random_seed, is_sensitivity, f_agg, - f_norm, - weights, polar, marginal_distribution) + return is_robustness_indicators, is_robustness_weights, polar, weights def check_config_error(condition: bool, error_message: str): diff --git a/mcda/mcda_without_robustness.py b/mcda/mcda_without_robustness.py deleted file mode 100644 index 2eb52a4..0000000 --- a/mcda/mcda_without_robustness.py +++ /dev/null @@ -1,157 +0,0 @@ -import sys -import copy -import logging -import pandas as pd - -from mcda.configuration.config import Config -from mcda.mcda_functions.normalization import Normalization -from mcda.mcda_functions.aggregation import Aggregation - -log = logging.getLogger(__name__) - -formatter = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=formatter) -logger = logging.getLogger("ProMCDA aggregation") - - -class MCDAWithoutRobustness: - """ - Class MCDA without indicators' uncertainty - - This class allows one to run MCDA without considering the uncertainties related to the indicators. - All indicators are described by the exact marginal distribution. - However, it's possible to have randomly sampled weights. - """ - - def __init__(self, config: Config, input_matrix: pd.DataFrame): - self.normalized_indicators = None - self.weights = None - self._config = copy.deepcopy(config) - self._input_matrix = copy.deepcopy(input_matrix) - - def normalize_indicators(self, method=None) -> dict: - """ - Normalize the input matrix using the specified normalization method. - - Parameters: - - method (optional): the normalization method to use. If None, all available methods will be applied. - Supported methods: 'minmax', 'target', 'standardized', 'rank'. - - Returns: - - a dictionary containing the normalized values of each indicator per normalization method. - - Notes: - Some aggregation methods do not work with indicator values equal or smaller than zero. For that reason: - - for the 'minmax' method, two sets of normalized indicators are returned: one with the range (0, 1) and - another with the range (0.1, 1). - - for the 'target' method, two sets of normalized indicators are returned: one with the range (0, 1) and - another with the range (0.1, 1). - - for the 'standardized' method, two sets of normalized indicators are returned: one with the range (-inf, +inf) - and another with the range (0.1, +inf). - """ - norm = Normalization(self._input_matrix, - self._config.polarity_for_each_indicator) - - normalized_indicators = {} - - if method is None or method == 'minmax': - indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1)) - # for aggregation "geometric" and "harmonic" that do not accept 0 - indicators_scaled_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) - normalized_indicators["minmax_without_zero"] = indicators_scaled_minmax_without_zero - normalized_indicators["minmax_01"] = indicators_scaled_minmax_01 - if method is None or method == 'target': - indicators_scaled_target_01 = norm.target(feature_range=(0, 1)) - indicators_scaled_target_without_zero = norm.target( - feature_range=(0.1, 1)) # for aggregation "geometric" and "harmonic" that do not accept 0 - normalized_indicators["target_without_zero"] = indicators_scaled_target_without_zero - normalized_indicators["target_01"] = indicators_scaled_target_01 - if method is None or method == 'standardized': - indicators_scaled_standardized_any = norm.standardized( - feature_range=('-inf', '+inf')) - indicators_scaled_standardized_without_zero = norm.standardized( - feature_range=(0.1, '+inf')) - normalized_indicators["standardized_any"] = indicators_scaled_standardized_any - normalized_indicators["standardized_without_zero"] = indicators_scaled_standardized_without_zero - if method is None or method == 'rank': - indicators_scaled_rank = norm.rank() - normalized_indicators["rank"] = indicators_scaled_rank - if method is not None and method not in ['minmax', 'target', 'standardized', 'rank']: - logger.error('Error Message', stack_info=True) - raise ValueError( - 'The selected normalization method is not supported') - - return normalized_indicators - - def aggregate_indicators(self, normalized_indicators: dict, weights: list, method=None) -> pd.DataFrame: - """ - Aggregate the normalized indicators using the specified aggregation method. - - Parameters: - - normalized_indicators: a dictionary containing the normalized values of each indicator per normalization - method. - - weights: the weights to be applied during aggregation. - - method (optional): The aggregation method to use. If None, all available methods will be applied. - Supported methods: 'weighted_sum', 'geometric', 'harmonic', 'minimum'. - - Returns: - - a DataFrame containing the aggregated scores per each alternative, and per each normalization method. - - :param normalized_indicators: dict - :param weights: list - :param method: str - :return scores: pd.DataFrame - """ - self.normalized_indicators = normalized_indicators - self.weights = weights - - agg = Aggregation(self.weights) - - scores_weighted_sum = {} - scores_geometric = {} - scores_harmonic = {} - scores_minimum = {} - - scores = pd.DataFrame() - col_names_method = [] - col_names = ['ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', - 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', - 'geom-rank', 'harm-minmax_without_zero', 'harm-target_without_zero', - 'harm-standardized_without_zero', 'harm-rank', 'min-standardized_any'] - # column names has the same order as in the following loop - - for key, values in self.normalized_indicators.items(): - if method is None or method == 'weighted_sum': - if key in ["standardized_any", "minmax_01", "target_01", - "rank"]: # ws goes only with some specific normalizations - scores_weighted_sum[key] = agg.weighted_sum(values) - col_names_method.append("ws-" + key) - if method is None or method == 'geometric': - if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", - "rank"]: # geom goes only with some specific normalizations - scores_geometric[key] = pd.Series(agg.geometric(values)) - col_names_method.append("geom-" + key) - if method is None or method == 'harmonic': - if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", - "rank"]: # harm goes only with some specific normalizations - scores_harmonic[key] = pd.Series(agg.harmonic(values)) - col_names_method.append("harm-" + key) - if method is None or method == 'minimum': - if key == "standardized_any": - scores_minimum[key] = pd.Series(agg.minimum( - self.normalized_indicators["standardized_any"])) - col_names_method.append("min-" + key) - - dict_list = [scores_weighted_sum, scores_geometric, - scores_harmonic, scores_minimum] - - for d in dict_list: - if d: - scores = pd.concat([scores, pd.DataFrame.from_dict(d)], axis=1) - - if method is None: - scores.columns = col_names - else: - scores.columns = col_names_method - - return scores diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index ec7699f..7fe6529 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -2,9 +2,10 @@ import time import logging import pandas as pd -from typing import Tuple +from typing import Tuple, List, Union from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values +from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty log = logging.getLogger(__name__) @@ -14,8 +15,8 @@ class ProMCDA: - def __init__(self, input_matrix:pd.DataFrame, polarity:Tuple[str], sensitivity:dict, robustness:dict, - monte_carlo:dict): + def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensitivity: dict, robustness: dict, + monte_carlo: dict, output_path: str): """ Initialize the ProMCDA class with configuration parameters. @@ -24,130 +25,129 @@ def __init__(self, input_matrix:pd.DataFrame, polarity:Tuple[str], sensitivity:d :param sensitivity: Sensitivity analysis configuration. :param robustness: Robustness analysis configuration. :param monte_carlo: Monte Carlo sampling configuration. + :param output_path: path for saving output files. + + # Example of instantiating the class and using it + promcda = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) + sensitivity = sensitivity_class(input1, input2) + aggregate = aggregate_class(input1, input2) + promcda.run_mcda() + df_normalized = promcda.normalize() + df_aggregated = promcda.aggregate() """ self.input_matrix = input_matrix self.polarity = polarity self.sensitivity = sensitivity self.robustness = robustness self.monte_carlo = monte_carlo + self.output_path = output_path - #self.validate_input_parameters_keys # TODO: still need a formal check as made in old config class, - # maybe use some of following functions validate_ - is_robustness_indicators = self.validate_inputs() - self.run_mcda(is_robustness_indicators) + # self.validate_input_parameters_keys # TODO: still need a formal check as made in old config class, + # maybe use some of following functions validate_ + is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() + self.run_mcda(is_robustness_indicators, is_robustness_weights, weights, configuration_settings) self.normalized_matrix = None self.aggregated_matrix = None self.ranked_matrix = None - #self.logger = logging.getLogger("ProMCDA") - + self.logger = logging.getLogger("ProMCDA") - def validate_inputs(self) -> int: + def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: """ Extract and validate input configuration parameters to ensure they are correct. Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). """ configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, - self.robustness, self.monte_carlo) - is_robustness_indicators = check_configuration_values(configuration_values) + self.robustness, self.monte_carlo, self.output_path) + is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values(configuration_values) # Validate input TODO: move into a different function validate_input_parameters_keys - #self.validate_normalization(self.sensitivity['normalization']) - #self.validate_aggregation(self.sensitivity['aggregation']) - #self.validate_robustness(self.robustness) - - return is_robustness_indicators - - def validate_normalization(self, f_norm): - """ - Validate the normalization method. - """ - valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] - if f_norm not in valid_norm_methods: - raise ValueError(f"Invalid normalization method: {f_norm}. Available methods: {valid_norm_methods}") - - def validate_aggregation(self, f_agg): - """ - Validate the aggregation method. - """ - valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] - if f_agg not in valid_agg_methods: - raise ValueError(f"Invalid aggregation method: {f_agg}. Available methods: {valid_agg_methods}") - - def validate_robustness(self, robustness): - """ - Validate robustness analysis settings. - """ - if not isinstance(robustness, dict): - raise ValueError("Robustness settings must be a dictionary.") - - # Add more specific checks based on robustness config structure - if robustness['on_single_weights'] == 'yes' and robustness['on_all_weights'] == 'yes': - raise ValueError("Conflicting settings for robustness analysis on weights.") - - def normalize(self): - """ - Normalize the decision matrix based on the configuration. - """ - f_norm = self.sensitivity['normalization'] - self.logger.info("Normalizing matrix with method: %s", f_norm) - - # Perform normalization (replace this with actual logic) - self.normalized_matrix = normalize_matrix(self.input_matrix, f_norm) - - return self.normalized_matrix - - def aggregate(self): - """ - Aggregate the decision matrix based on the configuration. - """ - f_agg = self.sensitivity['aggregation'] - self.logger.info("Aggregating matrix with method: %s", f_agg) - - # Perform aggregation (replace this with actual logic) - self.aggregated_matrix = aggregate_matrix(self.normalized_matrix, f_agg) - - return self.aggregated_matrix - - def run_mcda(self, is_robustness_indicators: int): - """ - Execute the full ProMCDA process. + # self.validate_normalization(self.sensitivity['normalization']) + # self.validate_aggregation(self.sensitivity['aggregation']) + # self.validate_robustness(self.robustness) + + return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values + + # def validate_normalization(self, f_norm): + # """ + # Validate the normalization method. + # """ + # valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] + # if f_norm not in valid_norm_methods: + # raise ValueError(f"Invalid normalization method: {f_norm}. Available methods: {valid_norm_methods}") + # + # def validate_aggregation(self, f_agg): + # """ + # Validate the aggregation method. + # """ + # valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] + # if f_agg not in valid_agg_methods: + # raise ValueError(f"Invalid aggregation method: {f_agg}. Available methods: {valid_agg_methods}") + # + # def validate_robustness(self, robustness): + # """ + # Validate robustness analysis settings. + # """ + # if not isinstance(robustness, dict): + # raise ValueError("Robustness settings must be a dictionary.") + # + # # Add more specific checks based on robustness config structure + # if robustness['on_single_weights'] == 'yes' and robustness['on_all_weights'] == 'yes': + # raise ValueError("Conflicting settings for robustness analysis on weights.") + # + # def normalize(self): + # """ + # Normalize the decision matrix based on the configuration. + # """ + # f_norm = self.sensitivity['normalization'] + # self.logger.info("Normalizing matrix with method: %s", f_norm) + # + # # Perform normalization (replace this with actual logic) + # self.normalized_matrix = normalize_matrix(self.input_matrix, f_norm) + # + # return self.normalized_matrix + # + # def aggregate(self): + # """ + # Aggregate the decision matrix based on the configuration. + # """ + # f_agg = self.sensitivity['aggregation'] + # self.logger.info("Aggregating matrix with method: %s", f_agg) + # + # # Perform aggregation (replace this with actual logic) + # self.aggregated_matrix = aggregate_matrix(self.normalized_matrix, f_agg) + # + # return self.aggregated_matrix + + def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict], + configuration_settings: dict): + """ + Execute the full ProMCDA process, either with or without uncertainties on the indicators. """ start_time = time.time() # Normalize - self.normalize() + # self.normalize() # Aggregate - self.aggregate() + # self.aggregate() # Run # no uncertainty if is_robustness_indicators == 0: - run_mcda_without_indicator_uncertainty(input_config, index_column_name, index_column_values, - input_matrix_no_alternatives, weights, f_norm, f_agg, - is_robustness_weights) + run_mcda_without_indicator_uncertainty(configuration_settings, is_robustness_weights, weights) # uncertainty else: - run_mcda_with_indicator_uncertainty(input_config, input_matrix_no_alternatives, index_column_name, - index_column_values, mc_runs, random_seed, is_sensitivity, f_agg, - f_norm, - weights, polar, marginal_distribution) + run_mcda_with_indicator_uncertainty(configuration_settings) elapsed_time = time.time() - start_time self.logger.info("ProMCDA finished calculations in %s seconds", elapsed_time) - def get_results(self): - """ - Return the final results as a DataFrame or other relevant structure. - """ - # Return the aggregated results (or any other relevant results) - return self.aggregated_matrix - + # def get_results(self): + # """ + # Return the final results as a DataFrame or other relevant structure. + # """ + # # Return the aggregated results (or any other relevant results) + # return self.aggregated_matrix -# Example of instantiating the class and using it -promcda_object = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) -promcda_object.run_mcda() -df_normalized = promcda_object.normalized_matrix -df_aggregated = promcda_object.get_results() \ No newline at end of file diff --git a/mcda/models/__init__.py b/mcda/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcda/mcda_with_robustness.py b/mcda/models/mcda_with_robustness.py similarity index 100% rename from mcda/mcda_with_robustness.py rename to mcda/models/mcda_with_robustness.py diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py new file mode 100644 index 0000000..e69de29 diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 7f2b9a9..6b879ce 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -1,4 +1,3 @@ - import os import argparse import json @@ -6,7 +5,7 @@ import random import logging import sys -from typing import Union, Any, List, Tuple +from typing import Union, Any, List from typing import Optional import numpy as np @@ -17,8 +16,8 @@ import mcda.utils.utils_for_parallelization as utils_for_parallelization import mcda.utils.utils_for_plotting as utils_for_plotting from mcda.configuration.config import Config -from mcda.mcda_without_robustness import MCDAWithoutRobustness -from mcda.mcda_with_robustness import MCDAWithRobustness +from mcda.models.mcda_without_robustness import MCDAWithoutRobustness +from mcda.models.mcda_with_robustness import MCDAWithRobustness DEFAULT_INPUT_DIRECTORY_PATH = './input_files' # present in the root directory of ProMCDA DEFAULT_OUTPUT_DIRECTORY_PATH = './output_files' # present in the root directory of ProMCDA @@ -581,10 +580,8 @@ def check_if_pdf_is_uniform(marginal_pdf: list) -> list: return uniform_pdf_mask -def run_mcda_without_indicator_uncertainty(input_config: dict, index_column_name: str, index_column_values: list, - input_matrix: pd.DataFrame, - weights: Union[list, List[list], dict], - f_norm: str, f_agg: str, is_robustness_weights: int): +def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness_weights: int, + weights: Union[List[str], List[pd.DataFrame], dict, None]): """ Runs ProMCDA without uncertainty on the indicators, i.e. without performing a robustness analysis. @@ -594,18 +591,12 @@ def run_mcda_without_indicator_uncertainty(input_config: dict, index_column_name and logs the completion time. Parameters: - - input_matrix: the input_matrix without the alternatives. - - index_column_name: the name of the index column of the original input matrix. - - index_column_values: the values of the index column of the original input matrix. + - extracted_values: a dictionary containing configuration values extracted from the input parameters. + - is_robustness_weights: a flag indicating whether robustness analysis will be performed on indicators or not. - weights: the normalised weights (either fixed or random sampled weights, depending on the settings). - :param input_config: dict - :param index_column_name: str - :param index_column_values: list - :param input_matrix: pd:DataFrame :param weights: Union[List[str], List[pd.DataFrame], dict, None] - :param f_norm: str - :param f_agg: str + :param extracted_values: dict :param is_robustness_weights: int :return: None """ @@ -619,11 +610,20 @@ def run_mcda_without_indicator_uncertainty(input_config: dict, index_column_name iterative_random_w_score_means = {} iterative_random_w_score_stds = {} + # Extract relevant values + input_matrix = extracted_values["input_matrix"] + alternatives_column_name = input_matrix.columns[0] + input_matrix = input_matrix.set_index(alternatives_column_name) + index_column_name = input_matrix.index.name + index_column_values = input_matrix.index.tolist() + logger.info("Start ProMCDA without robustness of the indicators") - config = Config(input_config) + config = Config(extracted_values) is_sensitivity = config.sensitivity['sensitivity_on'] is_robustness = config.robustness['robustness_on'] mcda_no_uncert = MCDAWithoutRobustness(config, input_matrix) + f_norm = extracted_values["normalization"] + f_agg = extracted_values["aggregation"] normalized_indicators = mcda_no_uncert.normalize_indicators() if is_sensitivity == "yes" \ else mcda_no_uncert.normalize_indicators(f_norm) @@ -656,7 +656,7 @@ def run_mcda_without_indicator_uncertainty(input_config: dict, index_column_name iterative_random_w_score_means_normalized=iterative_random_w_score_means_normalized, iterative_random_w_score_stds=iterative_random_w_score_stds, index_column_name=index_column_name, index_column_values=index_column_values, - input_config=input_config) + input_config=extracted_values) _plot_and_save_charts(scores=scores, normalized_scores=normalized_scores, score_means=all_weights_score_means, score_stds=all_weights_score_stds, @@ -664,7 +664,7 @@ def run_mcda_without_indicator_uncertainty(input_config: dict, index_column_name iterative_random_w_score_means=iterative_random_w_score_means, iterative_random_w_score_stds=iterative_random_w_score_stds, iterative_random_w_score_means_normalized=iterative_random_w_score_means_normalized, - input_matrix=input_matrix, config=input_config, + input_matrix=input_matrix, config=extracted_values, is_robustness_weights=is_robustness_weights) @@ -690,6 +690,7 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat :param index_column_values: list :param input_matrix: pd:DataFrame :param mc_runs: int + :param random_seed: int :param is_sensitivity: str :param weights: Union[List[str], List[pd.DataFrame], dict, None] :param f_norm: str @@ -719,9 +720,11 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat n_random_input_matrices = mcda_with_uncert.create_n_randomly_sampled_matrices() if is_sensitivity == "yes": - n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(n_random_input_matrices, polar) + n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(n_random_input_matrices, + polar) else: - n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(n_random_input_matrices, polar, f_norm) + n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(n_random_input_matrices, + polar, f_norm) args_for_parallel_agg = [(weights, normalized_indicators) for normalized_indicators in n_normalized_input_matrices] @@ -892,33 +895,33 @@ def _save_output_files(scores: Optional[pd.DataFrame], """ Save output files based of the computed scores, ranks, and configuration data. """ - config = Config(input_config) - full_output_path = os.path.join(output_directory_path, config.output_file_path) + output_path = input_config["output_path"] + full_output_path = os.path.join(output_directory_path, output_path) logger.info("Saving results in {}".format(full_output_path)) - check_path_exists(config.output_file_path) + check_path_exists(output_path) if scores is not None and not scores.empty: scores.insert(0, index_column_name, index_column_values) normalized_scores.insert(0, index_column_name, index_column_values) ranks.insert(0, index_column_name, index_column_values) - save_df(scores, config.output_file_path, 'scores.csv') - save_df(normalized_scores, config.output_file_path, 'normalized_scores.csv') - save_df(ranks, config.output_file_path, 'ranks.csv') + save_df(scores, output_path, 'scores.csv') + save_df(normalized_scores, output_path, 'normalized_scores.csv') + save_df(ranks, output_path, 'ranks.csv') elif score_means is not None and not score_means.empty: score_means.insert(0, index_column_name, index_column_values) score_stds.insert(0, index_column_name, index_column_values) score_means_normalized.insert(0, index_column_name, index_column_values) - save_df(score_means, config.output_file_path, 'score_means.csv') - save_df(score_stds, config.output_file_path, 'score_stds.csv') - save_df(score_means_normalized, config.output_file_path, 'score_means_normalized.csv') + save_df(score_means, output_path, 'score_means.csv') + save_df(score_stds, output_path, 'score_stds.csv') + save_df(score_means_normalized, output_path, 'score_means_normalized.csv') elif iterative_random_w_score_means is not None: - save_dict(iterative_random_w_score_means, config.output_file_path, 'score_means.pkl') - save_dict(iterative_random_w_score_stds, config.output_file_path, 'score_stds.pkl') - save_dict(iterative_random_w_score_means_normalized, config.output_file_path, 'score_means_normalized.pkl') + save_dict(iterative_random_w_score_means, output_path, 'score_means.pkl') + save_dict(iterative_random_w_score_stds, output_path, 'score_stds.pkl') + save_dict(iterative_random_w_score_means_normalized, output_path, 'score_means_normalized.pkl') - save_config(input_config, config.output_file_path, 'configuration.json') + save_config(input_config, output_path, 'configuration.json') def _plot_and_save_charts(scores: Optional[pd.DataFrame], @@ -936,15 +939,15 @@ def _plot_and_save_charts(scores: Optional[pd.DataFrame], """ Generate plots based on the computed scores and save them. """ - config = Config(config) + output_path = config["output_path"] num_indicators = input_matrix.shape[1] if scores is not None and not scores.empty: plot_no_norm_scores = utils_for_plotting.plot_non_norm_scores_without_uncert(scores) - utils_for_plotting.save_figure(plot_no_norm_scores, config.output_file_path, "MCDA_rough_scores.png") + utils_for_plotting.save_figure(plot_no_norm_scores, output_path, "MCDA_rough_scores.png") plot_norm_scores = utils_for_plotting.plot_norm_scores_without_uncert(normalized_scores) - utils_for_plotting.save_figure(plot_norm_scores, config.output_file_path, "MCDA_norm_scores.png") + utils_for_plotting.save_figure(plot_norm_scores, output_path, "MCDA_norm_scores.png") elif score_means is not None and not score_means.empty: if is_robustness_weights is not None and is_robustness_weights == 1: @@ -956,8 +959,8 @@ def _plot_and_save_charts(scores: Optional[pd.DataFrame], chart_mean_scores_norm = utils_for_plotting.plot_mean_scores(score_means_normalized, "not_plot_std", "indicators", score_stds) - utils_for_plotting.save_figure(chart_mean_scores, config.output_file_path, "MCDA_rough_scores.png") - utils_for_plotting.save_figure(chart_mean_scores_norm, config.output_file_path, "MCDA_norm_scores.png") + utils_for_plotting.save_figure(chart_mean_scores, output_path, "MCDA_rough_scores.png") + utils_for_plotting.save_figure(chart_mean_scores_norm, output_path, "MCDA_norm_scores.png") elif iterative_random_w_score_means is not None: images = [] @@ -978,5 +981,7 @@ def _plot_and_save_charts(scores: Optional[pd.DataFrame], images.append(plot_weight_mean_scores) images_norm.append(plot_weight_mean_scores_norm) - utils_for_plotting.combine_images(images, config.output_file_path, "MCDA_one_weight_randomness_rough_scores.png") - utils_for_plotting.combine_images(images_norm, config.output_file_path, "MCDA_one_weight_randomness_norm_scores.png") + utils_for_plotting.combine_images(images, output_path, + "MCDA_one_weight_randomness_rough_scores.png") + utils_for_plotting.combine_images(images_norm, output_path, + "MCDA_one_weight_randomness_norm_scores.png") diff --git a/tests/unit_tests/test_mcda_with_robustness.py b/tests/unit_tests/test_mcda_with_robustness.py index c91ec7a..cc20f57 100644 --- a/tests/unit_tests/test_mcda_with_robustness.py +++ b/tests/unit_tests/test_mcda_with_robustness.py @@ -2,7 +2,7 @@ import numpy as np import unittest -from mcda.mcda_with_robustness import MCDAWithRobustness +from mcda.models.mcda_with_robustness import MCDAWithRobustness from mcda.configuration.config import Config diff --git a/tests/unit_tests/test_mcda_without_robustness.py b/tests/unit_tests/test_mcda_without_robustness.py index 687ab22..95a2506 100644 --- a/tests/unit_tests/test_mcda_without_robustness.py +++ b/tests/unit_tests/test_mcda_without_robustness.py @@ -4,7 +4,7 @@ from unittest import TestCase -from mcda.mcda_without_robustness import MCDAWithoutRobustness +from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.configuration.config import Config from mcda.mcda_functions.aggregation import Aggregation import mcda.utils.utils_for_main as utils_for_main diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py new file mode 100644 index 0000000..6a60995 --- /dev/null +++ b/tests/unit_tests/test_promcda.py @@ -0,0 +1,65 @@ +import unittest +import pandas as pd +from mcda.models.ProMCDA import ProMCDA + + +class TestProMCDA(unittest.TestCase): + + def setUp(self): + # Mock input data for testing + self.input_matrix = pd.DataFrame({ + 'Criteria 1': [0.5, 0.2, 0.8], + 'Criteria 2': [0.3, 0.6, 0.1] + }, index=['A', 'B', 'C']) + self.polarity = ('+', '-',) + + self.sensitivity = { + 'sensitivity_on': 'no', + 'normalization': 'minmax', + 'aggregation': 'weighted_sum' + } + + self.robustness = { + 'robustness_on': 'no', + 'on_single_weights': 'yes', + 'on_all_weights': 'no', + 'on_indicators': 'no', + 'given_weights': [0.6, 0.4] + } + + self.monte_carlo = { + 'monte_carlo_runs': 1000, + 'num_cores': 2, + 'random_seed': 42, + 'marginal_distribution_for_each_indicator': 'normal' + } + + self.output_path = 'mock_output/' + + def test_init(self): + """ + Test if ProMCDA initializes correctly. + """ + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + self.assertEqual(promcda.input_matrix.shape, (3, 3)) + self.assertEqual(promcda.polarity, self.polarity) + self.assertEqual(promcda.sensitivity, self.sensitivity) + self.assertEqual(promcda.robustness, self.robustness) + self.assertEqual(promcda.monte_carlo['monte_carlo_runs'], 1000) + + def test_validate_inputs(self): + """ + Test if input validation works and returns the expected values. + """ + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + is_robustness_indicators, polar, weights, config = promcda.validate_inputs() + + # Validate the result + self.assertIsInstance(is_robustness_indicators, int) + self.assertIsInstance(polar, tuple) + self.assertIsInstance(weights, list) + self.assertEqual(is_robustness_indicators, 0) + + # You can write additional tests for normalization, aggregation, etc. \ No newline at end of file diff --git a/tests/unit_tests/test_utils_for_parallelization.py b/tests/unit_tests/test_utils_for_parallelization.py index 36d1a02..144da64 100644 --- a/tests/unit_tests/test_utils_for_parallelization.py +++ b/tests/unit_tests/test_utils_for_parallelization.py @@ -3,7 +3,7 @@ from pandas.testing import assert_frame_equal -from mcda.mcda_without_robustness import * +from mcda.models.mcda_without_robustness import * from mcda.utils.utils_for_parallelization import * From 993a2119717d49d992a0404230859fc698413cf7 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Fri, 11 Oct 2024 17:51:56 +0200 Subject: [PATCH 03/27] refactor: solve some unit tests, circual imports --- mcda/configuration/configuration_validator.py | 49 +----- mcda/models/mcda_with_robustness.py | 6 +- mcda/models/mcda_without_robustness.py | 157 ++++++++++++++++++ mcda/utils/utils_for_main.py | 117 +++++++++---- 4 files changed, 245 insertions(+), 84 deletions(-) diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index 02151e1..3257865 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -5,10 +5,8 @@ import pandas as pd from typing import Tuple, List, Union -from sklearn.preprocessing import MinMaxScaler - from mcda.utils.utils_for_main import pop_indexed_elements, check_norm_sum_weights, randomly_sample_all_weights, \ - randomly_sample_ix_weight + randomly_sample_ix_weight, check_input_matrix log = logging.getLogger(__name__) logging.getLogger('PIL').setLevel(logging.WARNING) @@ -458,48 +456,3 @@ def check_indicator_weights_polarities(num_indicators: int, polar: List[str], co num_indicators != len(config["given_weights"])): raise ValueError('The no. of fixed weights does not correspond to the no. of indicators') - -def check_input_matrix(input_matrix: pd.DataFrame) -> pd.DataFrame: - """ - Check the input matrix for duplicated rows in the alternatives column, rescale negative indicator values - and drop the index column of alternatives. - - Parameters: - - input_matrix: The input matrix containing the alternatives and indicators. - - Raises: - - ValueError: If duplicated rows are found in the alternative column. - - UserStoppedInfo: If the user chooses to stop when duplicates are found. - - :param input_matrix: pd.DataFrame - :rtype: pd.DataFrame - :return: input_matrix - """ - if input_matrix.duplicated().any(): - raise ValueError('Error: Duplicated rows in the alternatives column.') - elif input_matrix.iloc[:, 0].duplicated().any(): - logger.info('Duplicated rows in the alternatives column.') - - index_column_values = input_matrix.index.tolist() - logger.info("Alternatives are {}".format(index_column_values)) - input_matrix_no_alternatives = input_matrix.reset_index(drop=True) # drop the alternative - - input_matrix_no_alternatives = _check_and_rescale_negative_indicators( - input_matrix_no_alternatives) - - return input_matrix_no_alternatives - - -def _check_and_rescale_negative_indicators(input_matrix: pd.DataFrame) -> pd.DataFrame: - """ - Rescale indicators of the input matrix if negative into [0-1]. - """ - - if (input_matrix < 0).any().any(): - scaler = MinMaxScaler() - scaled_data = scaler.fit_transform(input_matrix) - scaled_matrix = pd.DataFrame( - scaled_data, columns=input_matrix.columns, index=input_matrix.index) - return scaled_matrix - else: - return input_matrix \ No newline at end of file diff --git a/mcda/models/mcda_with_robustness.py b/mcda/models/mcda_with_robustness.py index 449d0cb..366a332 100644 --- a/mcda/models/mcda_with_robustness.py +++ b/mcda/models/mcda_with_robustness.py @@ -29,7 +29,7 @@ class MCDAWithRobustness: """ - def __init__(self, config: Config, input_matrix: pd.DataFrame(), is_exact_pdf_mask=None, is_poisson_pdf_mask=None, + def __init__(self, config: dict, input_matrix: pd.DataFrame(), is_exact_pdf_mask=None, is_poisson_pdf_mask=None, random_seed=None): self.is_exact_pdf_mask = is_exact_pdf_mask self.is_poisson_pdf_mask = is_poisson_pdf_mask @@ -99,8 +99,8 @@ def create_n_randomly_sampled_matrices(self) -> List[pd.DataFrame]: :return list_random_matrix: List[pd.DataFrame] """ - marginal_pdf = self._config.monte_carlo_sampling["marginal_distribution_for_each_indicator"] - num_runs = self._config.monte_carlo_sampling["monte_carlo_runs"] # N + marginal_pdf = self._config["marginal_distribution_for_each_indicator"] + num_runs = self._config["monte_carlo_runs"] # N input_matrix = self._input_matrix # (AxnI) is_exact_pdf_mask = self.is_exact_pdf_mask is_poisson_pdf_mask = self.is_poisson_pdf_mask diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index e69de29..8f7fba8 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -0,0 +1,157 @@ +import sys +import copy +import logging +import pandas as pd + +from mcda.configuration.config import Config +from mcda.mcda_functions.normalization import Normalization +from mcda.mcda_functions.aggregation import Aggregation + +log = logging.getLogger(__name__) + +formatter = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=formatter) +logger = logging.getLogger("ProMCDA aggregation") + + +class MCDAWithoutRobustness: + """ + Class MCDA without indicators' uncertainty + + This class allows one to run MCDA without considering the uncertainties related to the indicators. + All indicators are described by the exact marginal distribution. + However, it's possible to have randomly sampled weights. + """ + + def __init__(self, config: dict, input_matrix: pd.DataFrame): + self.normalized_indicators = None + self.weights = None + self._config = copy.deepcopy(config) + self._input_matrix = copy.deepcopy(input_matrix) + + def normalize_indicators(self, method=None) -> dict: + """ + Normalize the input matrix using the specified normalization method. + + Parameters: + - method (optional): the normalization method to use. If None, all available methods will be applied. + Supported methods: 'minmax', 'target', 'standardized', 'rank'. + + Returns: + - a dictionary containing the normalized values of each indicator per normalization method. + + Notes: + Some aggregation methods do not work with indicator values equal or smaller than zero. For that reason: + - for the 'minmax' method, two sets of normalized indicators are returned: one with the range (0, 1) and + another with the range (0.1, 1). + - for the 'target' method, two sets of normalized indicators are returned: one with the range (0, 1) and + another with the range (0.1, 1). + - for the 'standardized' method, two sets of normalized indicators are returned: one with the range (-inf, +inf) + and another with the range (0.1, +inf). + """ + norm = Normalization(self._input_matrix, + self._config["polarity"]) + + normalized_indicators = {} + + if method is None or method == 'minmax': + indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1)) + # for aggregation "geometric" and "harmonic" that do not accept 0 + indicators_scaled_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) + normalized_indicators["minmax_without_zero"] = indicators_scaled_minmax_without_zero + normalized_indicators["minmax_01"] = indicators_scaled_minmax_01 + if method is None or method == 'target': + indicators_scaled_target_01 = norm.target(feature_range=(0, 1)) + indicators_scaled_target_without_zero = norm.target( + feature_range=(0.1, 1)) # for aggregation "geometric" and "harmonic" that do not accept 0 + normalized_indicators["target_without_zero"] = indicators_scaled_target_without_zero + normalized_indicators["target_01"] = indicators_scaled_target_01 + if method is None or method == 'standardized': + indicators_scaled_standardized_any = norm.standardized( + feature_range=('-inf', '+inf')) + indicators_scaled_standardized_without_zero = norm.standardized( + feature_range=(0.1, '+inf')) + normalized_indicators["standardized_any"] = indicators_scaled_standardized_any + normalized_indicators["standardized_without_zero"] = indicators_scaled_standardized_without_zero + if method is None or method == 'rank': + indicators_scaled_rank = norm.rank() + normalized_indicators["rank"] = indicators_scaled_rank + if method is not None and method not in ['minmax', 'target', 'standardized', 'rank']: + logger.error('Error Message', stack_info=True) + raise ValueError( + 'The selected normalization method is not supported') + + return normalized_indicators + + def aggregate_indicators(self, normalized_indicators: dict, weights: list, method=None) -> pd.DataFrame: + """ + Aggregate the normalized indicators using the specified aggregation method. + + Parameters: + - normalized_indicators: a dictionary containing the normalized values of each indicator per normalization + method. + - weights: the weights to be applied during aggregation. + - method (optional): The aggregation method to use. If None, all available methods will be applied. + Supported methods: 'weighted_sum', 'geometric', 'harmonic', 'minimum'. + + Returns: + - a DataFrame containing the aggregated scores per each alternative, and per each normalization method. + + :param normalized_indicators: dict + :param weights: list + :param method: str + :return scores: pd.DataFrame + """ + self.normalized_indicators = normalized_indicators + self.weights = weights + + agg = Aggregation(self.weights) + + scores_weighted_sum = {} + scores_geometric = {} + scores_harmonic = {} + scores_minimum = {} + + scores = pd.DataFrame() + col_names_method = [] + col_names = ['ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', + 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', + 'geom-rank', 'harm-minmax_without_zero', 'harm-target_without_zero', + 'harm-standardized_without_zero', 'harm-rank', 'min-standardized_any'] + # column names has the same order as in the following loop + + for key, values in self.normalized_indicators.items(): + if method is None or method == 'weighted_sum': + if key in ["standardized_any", "minmax_01", "target_01", + "rank"]: # ws goes only with some specific normalizations + scores_weighted_sum[key] = agg.weighted_sum(values) + col_names_method.append("ws-" + key) + if method is None or method == 'geometric': + if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", + "rank"]: # geom goes only with some specific normalizations + scores_geometric[key] = pd.Series(agg.geometric(values)) + col_names_method.append("geom-" + key) + if method is None or method == 'harmonic': + if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", + "rank"]: # harm goes only with some specific normalizations + scores_harmonic[key] = pd.Series(agg.harmonic(values)) + col_names_method.append("harm-" + key) + if method is None or method == 'minimum': + if key == "standardized_any": + scores_minimum[key] = pd.Series(agg.minimum( + self.normalized_indicators["standardized_any"])) + col_names_method.append("min-" + key) + + dict_list = [scores_weighted_sum, scores_geometric, + scores_harmonic, scores_minimum] + + for d in dict_list: + if d: + scores = pd.concat([scores, pd.DataFrame.from_dict(d)], axis=1) + + if method is None: + scores.columns = col_names + else: + scores.columns = col_names_method + + return scores \ No newline at end of file diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 6b879ce..c936138 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -12,10 +12,10 @@ import pandas as pd from datetime import datetime from sklearn import preprocessing +from sklearn.preprocessing import MinMaxScaler import mcda.utils.utils_for_parallelization as utils_for_parallelization import mcda.utils.utils_for_plotting as utils_for_plotting -from mcda.configuration.config import Config from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.models.mcda_with_robustness import MCDAWithRobustness @@ -459,12 +459,11 @@ def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=F :param for_testing: bool :return: Union[list, None] """ - config = Config(config) satisfies_condition = False problem_logged = False - marginal_pdf = config.monte_carlo_sampling["marginal_distribution_for_each_indicator"] + marginal_pdf = config["marginal_distribution_for_each_indicator"] is_exact_pdf_mask = check_if_pdf_is_exact(marginal_pdf) is_poisson_pdf_mask = check_if_pdf_is_poisson(marginal_pdf) is_uniform_pdf_mask = check_if_pdf_is_uniform(marginal_pdf) @@ -616,15 +615,16 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness input_matrix = input_matrix.set_index(alternatives_column_name) index_column_name = input_matrix.index.name index_column_values = input_matrix.index.tolist() - - logger.info("Start ProMCDA without robustness of the indicators") - config = Config(extracted_values) - is_sensitivity = config.sensitivity['sensitivity_on'] - is_robustness = config.robustness['robustness_on'] - mcda_no_uncert = MCDAWithoutRobustness(config, input_matrix) + input_matrix_no_alternatives = check_input_matrix(input_matrix) + is_sensitivity = extracted_values['sensitivity_on'] + is_robustness = extracted_values['robustness_on'] f_norm = extracted_values["normalization"] f_agg = extracted_values["aggregation"] + mcda_no_uncert\ + = MCDAWithoutRobustness(extracted_values, input_matrix_no_alternatives) + logger.info("Start ProMCDA without robustness of the indicators") + normalized_indicators = mcda_no_uncert.normalize_indicators() if is_sensitivity == "yes" \ else mcda_no_uncert.normalize_indicators(f_norm) @@ -633,15 +633,16 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness if is_sensitivity == "yes" \ else mcda_no_uncert.aggregate_indicators(normalized_indicators, weights, f_agg) normalized_scores = rescale_minmax(scores) - elif config.robustness["on_all_weights"] == "yes" and config.robustness["robustness_on"] == "yes": + elif extracted_values["on_all_weights"] == "yes" and extracted_values["robustness_on"] == "yes": # ALL RANDOMLY SAMPLED WEIGHTS (MCDA runs num_samples times) all_weights_score_means, all_weights_score_stds, \ all_weights_score_means_normalized, all_weights_score_stds_normalized = \ _compute_scores_for_all_random_weights(normalized_indicators, is_sensitivity, weights, f_agg) - elif (config.robustness["on_single_weights"] == "yes") and (config.robustness["robustness_on"] == "yes"): + elif (extracted_values["on_single_weights"] == "yes") and (extracted_values["robustness_on"] == "yes"): # ONE RANDOMLY SAMPLED WEIGHT A TIME (MCDA runs (num_samples * num_indicators) times) iterative_random_weights_statistics: dict = _compute_scores_for_single_random_weight( - normalized_indicators, weights, is_sensitivity, index_column_name, index_column_values, f_agg, input_matrix) + normalized_indicators, weights, is_sensitivity, index_column_name, index_column_values, f_agg, + input_matrix_no_alternatives) iterative_random_w_score_means = iterative_random_weights_statistics['score_means'] iterative_random_w_score_stds = iterative_random_weights_statistics['score_stds'] iterative_random_w_score_means_normalized = iterative_random_weights_statistics['score_means_normalized'] @@ -664,14 +665,13 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness iterative_random_w_score_means=iterative_random_w_score_means, iterative_random_w_score_stds=iterative_random_w_score_stds, iterative_random_w_score_means_normalized=iterative_random_w_score_means_normalized, - input_matrix=input_matrix, config=extracted_values, + input_matrix=input_matrix_no_alternatives, config=extracted_values, is_robustness_weights=is_robustness_weights) -def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.DataFrame, index_column_name: str, - index_column_values: list, mc_runs: int, random_seed: int, is_sensitivity: str, - f_agg: str, f_norm: str, weights: Union[List[list], List[pd.DataFrame], dict], - polar: List[str], marginal_pdf: List[str]) -> None: +def run_mcda_with_indicator_uncertainty(extracted_values: dict, weights: Union[List[str], List[pd.DataFrame], + dict, None]) -> None: + """ Runs ProMCDA with uncertainty on the indicators, i.e. with a robustness analysis. @@ -685,25 +685,29 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat - weights: the normalised weights (either fixed or random sampled weights, depending on the settings). In the context of the robustness analysis, only fixed normalised weights are used, i.e. weights[0]. - :param input_config: dict - :param index_column_name: str - :param index_column_values: list - :param input_matrix: pd:DataFrame - :param mc_runs: int - :param random_seed: int - :param is_sensitivity: str + :param extracted_values: dict :param weights: Union[List[str], List[pd.DataFrame], dict, None] - :param f_norm: str - :param f_agg: str - :param polar: List[str] - :param marginal_pdf: List[str] :return: None """ logger.info("Start ProMCDA with uncertainty on the indicators") - config = Config(input_config) is_robustness_indicators = True all_indicators_scores_normalized = [] + # Extract relevant values + input_matrix = extracted_values["input_matrix"] + alternatives_column_name = input_matrix.columns[0] + input_matrix = input_matrix.set_index(alternatives_column_name) + index_column_name = input_matrix.index.name + index_column_values = input_matrix.index.tolist() + input_matrix_no_alternatives = check_input_matrix(input_matrix) + mc_runs = extracted_values["monte_carlo_runs"] + marginal_pdf = extracted_values["marginal_distribution_for_each_indicator"] + random_seed = extracted_values["random_seed"] + is_sensitivity = extracted_values['sensitivity_on'] + f_norm = extracted_values["normalization"] + f_agg = extracted_values["aggregation"] + polar = extracted_values["polarity_for_each_indicator"] + if mc_runs <= 0: logger.error('Error Message', stack_info=True) raise ValueError('The number of MC runs should be larger than 0 for a robustness analysis') @@ -712,11 +716,12 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat logger.info("The number of Monte-Carlo runs is only {}".format(mc_runs)) logger.info("A meaningful number of Monte-Carlo runs is equal or larger than 1000") - check_parameters_pdf(input_matrix, input_config) + check_parameters_pdf(input_matrix, extracted_values) is_exact_pdf_mask = check_if_pdf_is_exact(marginal_pdf) is_poisson_pdf_mask = check_if_pdf_is_poisson(marginal_pdf) - mcda_with_uncert = MCDAWithRobustness(config, input_matrix, is_exact_pdf_mask, is_poisson_pdf_mask, random_seed) + mcda_with_uncert = MCDAWithRobustness(extracted_values, input_matrix_no_alternatives, is_exact_pdf_mask, + is_poisson_pdf_mask, random_seed) n_random_input_matrices = mcda_with_uncert.create_n_randomly_sampled_matrices() if is_sensitivity == "yes": @@ -753,7 +758,7 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat iterative_random_w_score_means=None, iterative_random_w_score_means_normalized=None, iterative_random_w_score_stds=None, - input_config=input_config, + input_config=extracted_values, index_column_name=index_column_name, index_column_values=index_column_values) _plot_and_save_charts(scores=None, normalized_scores=None, @@ -762,10 +767,56 @@ def run_mcda_with_indicator_uncertainty(input_config: dict, input_matrix: pd.Dat iterative_random_w_score_means=None, iterative_random_w_score_stds=None, iterative_random_w_score_means_normalized=None, - input_matrix=input_matrix, config=input_config, + input_matrix=input_matrix, config=extracted_values, is_robustness_indicators=is_robustness_indicators) +def check_input_matrix(input_matrix: pd.DataFrame) -> pd.DataFrame: + """ + Check the input matrix for duplicated rows in the alternatives column, rescale negative indicator values + and drop the index column of alternatives. + + Parameters: + - input_matrix: The input matrix containing the alternatives and indicators. + + Raises: + - ValueError: If duplicated rows are found in the alternative column. + - UserStoppedInfo: If the user chooses to stop when duplicates are found. + + :param input_matrix: pd.DataFrame + :rtype: pd.DataFrame + :return: input_matrix + """ + if input_matrix.duplicated().any(): + raise ValueError('Error: Duplicated rows in the alternatives column.') + elif input_matrix.iloc[:, 0].duplicated().any(): + logger.info('Duplicated rows in the alternatives column.') + + index_column_values = input_matrix.index.tolist() + logger.info("Alternatives are {}".format(index_column_values)) + input_matrix_no_alternatives = input_matrix.reset_index(drop=True) # drop the alternative + + input_matrix_no_alternatives = _check_and_rescale_negative_indicators( + input_matrix_no_alternatives) + + return input_matrix_no_alternatives + + +def _check_and_rescale_negative_indicators(input_matrix: pd.DataFrame) -> pd.DataFrame: + """ + Rescale indicators of the input matrix if negative into [0-1]. + """ + + if (input_matrix < 0).any().any(): + scaler = MinMaxScaler() + scaled_data = scaler.fit_transform(input_matrix) + scaled_matrix = pd.DataFrame( + scaled_data, columns=input_matrix.columns, index=input_matrix.index) + return scaled_matrix + else: + return input_matrix + + def _compute_scores_for_all_random_weights(indicators: dict, is_sensitivity: str, weights: Union[List[str], List[pd.DataFrame], dict, None], f_agg: str) -> tuple[Any, Any, Any, Any]: From c8180db1bcf85d75dd8c723a828c0cc5629ba790 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Mon, 14 Oct 2024 15:24:12 +0200 Subject: [PATCH 04/27] unit test debugging 1 --- mcda/models/ProMCDA.py | 2 +- mcda/utils/utils_for_main.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 7fe6529..0094b01 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -35,6 +35,7 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit df_normalized = promcda.normalize() df_aggregated = promcda.aggregate() """ + self.logger = logging.getLogger("ProMCDA") self.input_matrix = input_matrix self.polarity = polarity self.sensitivity = sensitivity @@ -51,7 +52,6 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit self.aggregated_matrix = None self.ranked_matrix = None - self.logger = logging.getLogger("ProMCDA") def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: """ diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index c936138..4f5cb13 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -270,11 +270,34 @@ def save_config(config: dict, folder_path: str, filename: str): try: with open(full_output_path, 'w') as fp: - json.dump(config, fp) + serializable_config = _prepare_config_for_json(config) + json.dump(serializable_config, fp) except IOError as e: logging.error(f"Error while dumping the configuration into a JSON file: {e}") +def _convert_dataframe_to_serializable(df): + """ + Convert a pandas DataFrame into a serializable dictionary format. + """ + return { + 'data': df.values.tolist(), # Convert data to list of lists + 'columns': df.columns.tolist(), # Convert column names to list + 'index': df.index.tolist() # Convert index labels to list + } + + +def _prepare_config_for_json(config): + """ + Prepare the config dictionary by converting non-serializable objects into serializable ones. + """ + config_copy = config.copy() # Create a copy to avoid modifying the original config + if isinstance(config_copy['input_matrix'], pd.DataFrame): + # Convert DataFrame to serializable format + config_copy['input_matrix'] = _convert_dataframe_to_serializable(config_copy['input_matrix']) + return config_copy + + def check_path_exists(path: str): """ Check if a directory path exists, and create it if it doesn't. @@ -612,7 +635,7 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness # Extract relevant values input_matrix = extracted_values["input_matrix"] alternatives_column_name = input_matrix.columns[0] - input_matrix = input_matrix.set_index(alternatives_column_name) + # input_matrix = input_matrix.set_index(alternatives_column_name) index_column_name = input_matrix.index.name index_column_values = input_matrix.index.tolist() input_matrix_no_alternatives = check_input_matrix(input_matrix) @@ -621,7 +644,7 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness f_norm = extracted_values["normalization"] f_agg = extracted_values["aggregation"] - mcda_no_uncert\ + mcda_no_uncert \ = MCDAWithoutRobustness(extracted_values, input_matrix_no_alternatives) logger.info("Start ProMCDA without robustness of the indicators") @@ -670,8 +693,7 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness def run_mcda_with_indicator_uncertainty(extracted_values: dict, weights: Union[List[str], List[pd.DataFrame], - dict, None]) -> None: - +dict, None]) -> None: """ Runs ProMCDA with uncertainty on the indicators, i.e. with a robustness analysis. From 7742a1aa3f6f97e6dc4eaea57ba3c547317521de Mon Sep 17 00:00:00 2001 From: Flaminia Date: Mon, 21 Oct 2024 15:57:47 +0200 Subject: [PATCH 05/27] refactor: add basic uni tests and remove open socket warning --- mcda/models/ProMCDA.py | 8 ++++---- mcda/utils/utils_for_plotting.py | 6 +++--- tests/unit_tests/test_promcda.py | 25 ++++++++++++++++++++++--- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 0094b01..2b74c43 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -52,7 +52,6 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit self.aggregated_matrix = None self.ranked_matrix = None - def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: """ Extract and validate input configuration parameters to ensure they are correct. @@ -60,7 +59,8 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] """ configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values(configuration_values) + is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values( + configuration_values) # Validate input TODO: move into a different function validate_input_parameters_keys # self.validate_normalization(self.sensitivity['normalization']) @@ -120,7 +120,8 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] # # return self.aggregated_matrix - def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict], + def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, + weights: Union[list, List[list], dict], configuration_settings: dict): """ Execute the full ProMCDA process, either with or without uncertainties on the indicators. @@ -150,4 +151,3 @@ def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, we # """ # # Return the aggregated results (or any other relevant results) # return self.aggregated_matrix - diff --git a/mcda/utils/utils_for_plotting.py b/mcda/utils/utils_for_plotting.py index 2e9f516..a8de801 100644 --- a/mcda/utils/utils_for_plotting.py +++ b/mcda/utils/utils_for_plotting.py @@ -54,7 +54,7 @@ def plot_norm_scores_without_uncert(scores: pd.DataFrame) -> object: yaxis=dict( range=[scores.iloc[:, 1:].values.min() - 0.5, scores.iloc[:, 1:].values.max() + 0.5]) ) - fig.show() + # fig.show() return fig @@ -95,7 +95,7 @@ def plot_non_norm_scores_without_uncert(scores: pd.DataFrame) -> object: ticktext=scores[alternatives_column_name][:], tickangle=45) ) - fig.show() + # fig.show() it triggers an open socket warning when on return fig @@ -154,7 +154,7 @@ def plot_mean_scores(all_means: pd.DataFrame, plot_std: str, rand_on: str, all_s ticktext=all_means[alternatives_column_name][:], tickangle=45) ) - fig.show() + # fig.show() return fig diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 6a60995..e0b935c 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -1,4 +1,8 @@ +import os +import shutil import unittest +import warnings + import pandas as pd from mcda.models.ProMCDA import ProMCDA @@ -6,6 +10,7 @@ class TestProMCDA(unittest.TestCase): def setUp(self): + warnings.filterwarnings("error", category=ResourceWarning) # Mock input data for testing self.input_matrix = pd.DataFrame({ 'Criteria 1': [0.5, 0.2, 0.8], @@ -42,7 +47,7 @@ def test_init(self): """ promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - self.assertEqual(promcda.input_matrix.shape, (3, 3)) + self.assertEqual(promcda.input_matrix.shape, (3, 2)) self.assertEqual(promcda.polarity, self.polarity) self.assertEqual(promcda.sensitivity, self.sensitivity) self.assertEqual(promcda.robustness, self.robustness) @@ -54,12 +59,26 @@ def test_validate_inputs(self): """ promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - is_robustness_indicators, polar, weights, config = promcda.validate_inputs() + (is_robustness_indicators, is_robustness_weights, polar, weights, config) = promcda.validate_inputs() # Validate the result self.assertIsInstance(is_robustness_indicators, int) + self.assertIsInstance(is_robustness_weights, int) self.assertIsInstance(polar, tuple) self.assertIsInstance(weights, list) + self.assertIsInstance(config, dict) self.assertEqual(is_robustness_indicators, 0) + self.assertEqual(is_robustness_weights, 0) + + def tearDown(self): + """ + Clean up temporary directories and files after each test. + """ + if os.path.exists(self.output_path): + shutil.rmtree(self.output_path) + +if __name__ == '__main__': + unittest.main() + + # TODO: write additional tests for normalization, aggregation, etc. - # You can write additional tests for normalization, aggregation, etc. \ No newline at end of file From 9d747ca3879fde81aaeb6d0584b86898648dae44 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Mon, 21 Oct 2024 17:18:01 +0200 Subject: [PATCH 06/27] refactor: delete config.py and add a function to validate dictionary keys in config settings --- mcda/configuration/config.py | 143 ------------------ mcda/configuration/configuration_validator.py | 94 ++++++++---- mcda/models/ProMCDA.py | 25 ++- 3 files changed, 81 insertions(+), 181 deletions(-) delete mode 100644 mcda/configuration/config.py diff --git a/mcda/configuration/config.py b/mcda/configuration/config.py deleted file mode 100644 index 7b69db4..0000000 --- a/mcda/configuration/config.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -This module serves as a configuration object for ProMCDA. -It is designed to store and manage configuration settings in a structured way. -""" - -import copy -from typing import List, Dict, Any - - -# noinspection PyMethodMayBeStatic -class Config: - """ - Class representing configuration settings. - - This class encapsulates the configuration settings. - It expects the following keys in the input dictionary: - - input_matrix_path: path to the input matrix file. - - polarity_for_each_indicator: list of polarities, one for each indicator. - - sensitivity: sensitivity configuration. - - robustness: robustness configuration. - - monte_carlo_sampling: Monte Carlo sampling configuration. - - output_directory_path: path to the output file. - - Attributes: - _valid_keys (List[str]): list of valid keys expected in the input dictionary. - _list_values (List[str]): list of keys corresponding to list values. - _str_values (List[str]): list of keys corresponding to string values. - _int_values (List[str]): list of keys corresponding to integer values. - _dict_values (List[str]): list of keys corresponding to dictionary values. - _keys_of_dict_values (Dict[str, List[str]]): dictionary containing keys and their corresponding sub-keys. - - Methods: - __init__(input_config: dict): instantiate a configuration object. - _validate(input_config, valid_keys, str_values, int_values, list_values, dict_values): validate the input - configuration. - get_property(property_name: str): retrieve a property from the configuration. - check_dict_keys(dic: Dict[str, Any], keys: List[str]): check if a specific key is in a dictionary. - check_key(dic: dict, key: str): check if a key is in a dictionary. - """ - - _valid_keys: List[str] = ['input_matrix_path', - 'polarity_for_each_indicator', - 'sensitivity', - 'robustness', - 'monte_carlo_sampling', - 'output_directory_path'] - - _list_values: List[str] = [ - 'marginal_distribution_for_each_indicator', 'polarity_for_each_indicator'] - - _str_values: List[str] = ['input_matrix_path', 'output_directory_path', 'sensitivity_on', 'normalization', 'aggregation', - 'robustness_on', 'on_single_weights', 'on_all_weights', 'given_weights', 'on_indicators'] - - _int_values: List[str] = ['monte_carlo_runs', 'num_cores', 'random_seed'] - - _dict_values: List[str] = ['sensitivity', 'robustness', 'monte_carlo_sampling'] - - _keys_of_dict_values = {'sensitivity': ['sensitivity_on', 'normalization', 'aggregation'], - 'robustness': ['robustness_on', 'on_single_weights', 'on_all_weights', - 'given_weights', 'on_indicators'], - 'monte_carlo_sampling': ['monte_carlo_runs', 'num_cores', 'random_seed', - 'marginal_distribution_for_each_indicator']} - - def __init__(self, input_config: dict): - - valid_keys = self._valid_keys - str_values = self._str_values - int_values = self._int_values - list_values = self._list_values - dict_values = self._dict_values - # keys_of_dict_values = self._keys_of_dict_values - - self._validate(input_config, valid_keys, str_values, - int_values, list_values, dict_values) - self._config = copy.deepcopy(input_config) - - def _validate(self, input_config, valid_keys, str_values, int_values, list_values, dict_values): - if not isinstance(input_config, dict): - raise TypeError("input configuration file is not a dictionary") - - for key in valid_keys: - if key not in input_config: - raise KeyError("key {} is not in the input config".format(key)) - - if key in str_values: - if not isinstance(input_config[key], str): - raise TypeError( - "value of {} in the input config is not a string".format(key)) - - if key in int_values: - if not isinstance(input_config[key], int): - raise TypeError( - "value of {} in the input config is not an integer".format(key)) - - if key in list_values: - if not isinstance(input_config[key], list): - raise TypeError( - "value of {} in the input config is not a list".format(key)) - - if key in dict_values: - if not isinstance(input_config[key], dict): - raise TypeError( - "value of {} in the input config is not a dictionary".format(key)) - Config.check_dict_keys( - input_config[key], Config._keys_of_dict_values[key]) - - def get_property(self, property_name: str): - return self._config[property_name] - - @property - def input_matrix_path(self): - return self.get_property('input_matrix_path') - - @property - def polarity_for_each_indicator(self): - return self.get_property('polarity_for_each_indicator') - - @property - def sensitivity(self): - return self.get_property('sensitivity') - - @property - def robustness(self): - return self.get_property('robustness') - - @property - def monte_carlo_sampling(self): - return self.get_property('monte_carlo_sampling') - - @property - def output_file_path(self): - return self.get_property('output_directory_path') - - @staticmethod - def check_dict_keys(dic: Dict[str, Any], keys: List[str]): - for key in keys: - Config.check_key(dic, key) - - @staticmethod - def check_key(dic: dict, key: str): - if key not in dic.keys(): - raise KeyError( - "The key = {} is not present in dictionary: {}".format(key, dic)) diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index 3257865..67c85a1 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -3,7 +3,7 @@ import numpy as np import pandas as pd -from typing import Tuple, List, Union +from typing import Tuple, List, Union, Dict, Any from mcda.utils.utils_for_main import pop_indexed_elements, check_norm_sum_weights, randomly_sample_all_weights, \ randomly_sample_ix_weight, check_input_matrix @@ -14,10 +14,44 @@ logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=FORMATTER) logger = logging.getLogger("ProMCDA") +from typing import Dict, List, Any + + +def check_configuration_keys(sensitivity: dict, robustness: dict, monte_carlo: dict) -> bool: + """ + Checks for required keys in sensitivity, robustness, and monte_carlo dictionaries. + TODO: revisit this logic when substitute classes to handle configuration settings. + + :param sensitivity : dict + :param robustness : dict + :param: monte_carlo : dict + :rtype: bool + """ + + keys_of_dict_values = { + 'sensitivity': ['sensitivity_on', 'normalization', 'aggregation'], + 'robustness': ['robustness_on', 'on_single_weights', 'on_all_weights', 'given_weights', 'on_indicators'], + 'monte_carlo': ['monte_carlo_runs', 'num_cores', 'random_seed', 'marginal_distribution_for_each_indicator'] + } + + _check_dict_keys(sensitivity, keys_of_dict_values['sensitivity']) + _check_dict_keys(robustness, keys_of_dict_values['robustness']) + _check_dict_keys(monte_carlo, keys_of_dict_values['monte_carlo']) + + return True + + +def _check_dict_keys(dic: Dict[str, Any], expected_keys: List[str]) -> None: + """ + Helper function to check if the dictionary contains the required keys. + """ + for key in expected_keys: + if key not in dic: + raise KeyError(f"The key '{key}' is missing in the provided dictionary") + def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str], sensitivity: dict, robustness: dict, monte_carlo: dict, output_path: str) -> dict: - """ Extracts relevant configuration values required for running the ProMCDA process. @@ -128,10 +162,10 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s # Check for sensitivity-related configuration errors if sensitivity_on == "no": check_config_error(normalization not in ['minmax', 'target', 'standardized', 'rank'], - 'The available normalization functions are: minmax, target, standardized, rank.') + 'The available normalization functions are: minmax, target, standardized, rank.') check_config_error(aggregation not in ['weighted_sum', 'geometric', 'harmonic', 'minimum'], - 'The available aggregation functions are: weighted_sum, geometric, harmonic, minimum.' - '\nWatch the correct spelling in the configuration.') + 'The available aggregation functions are: weighted_sum, geometric, harmonic, minimum.' + '\nWatch the correct spelling in the configuration.') logger.info("ProMCDA will only use one pair of norm/agg functions: " + normalization + '/' + aggregation) else: logger.info("ProMCDA will use a set of different pairs of norm/agg functions") @@ -143,40 +177,40 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s check_config_error((robustness_on_single_weights == "no" and robustness_on_all_weights == "no" and robustness_on_indicators == "no"), - 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' - 'weights or indicators. Please clarify it.') + 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' + 'weights or indicators. Please clarify it.') check_config_error((robustness_on_single_weights == "yes" and robustness_on_all_weights == "yes" and robustness_on_indicators == "no"), - 'Robustness analysis has been requested for the weights, but it’s unclear whether it should ' - 'be applied to all weights or just one at a time? Please clarify.') + 'Robustness analysis has been requested for the weights, but it’s unclear whether it should ' + 'be applied to all weights or just one at a time? Please clarify.') check_config_error(((robustness_on_single_weights == "yes" and - robustness_on_all_weights == "yes" and - robustness_on_indicators == "yes") or - (robustness_on_single_weights == "yes" and - robustness_on_all_weights == "no" and - robustness_on_indicators == "yes") or - (robustness_on_single_weights == "no" and - robustness_on_all_weights == "yes" and - robustness_on_indicators == "yes")), - 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' - 'weights or indicators. Please clarify.') + robustness_on_all_weights == "yes" and + robustness_on_indicators == "yes") or + (robustness_on_single_weights == "yes" and + robustness_on_all_weights == "no" and + robustness_on_indicators == "yes") or + (robustness_on_single_weights == "no" and + robustness_on_all_weights == "yes" and + robustness_on_indicators == "yes")), + 'Robustness analysis has been requested, but it’s unclear whether it should be applied to ' + 'weights or indicators. Please clarify.') # Check seetings for robustness analysis on weights or indicators condition_robustness_on_weights = ( - (robustness_on_single_weights == 'yes' and - robustness_on_all_weights == 'no' and - robustness_on_indicators == 'no') or - (robustness_on_single_weights == 'no' and - robustness_on_all_weights == 'yes' and - robustness_on_indicators == 'no')) + (robustness_on_single_weights == 'yes' and + robustness_on_all_weights == 'no' and + robustness_on_indicators == 'no') or + (robustness_on_single_weights == 'no' and + robustness_on_all_weights == 'yes' and + robustness_on_indicators == 'no')) condition_robustness_on_indicators = ( (robustness_on_single_weights == 'no' and - robustness_on_all_weights == 'no' and - robustness_on_indicators == 'yes')) + robustness_on_all_weights == 'no' and + robustness_on_indicators == 'yes')) is_robustness_weights, is_robustness_indicators = check_config_setting(condition_robustness_on_weights, condition_robustness_on_indicators, @@ -194,7 +228,8 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s num_indicators = (input_matrix_no_alternatives.shape[1] - num_non_exact_and_non_poisson) # Process indicators and weights based on input parameters in the configuration - polar, weights = process_indicators_and_weights(extracted_values, input_matrix_no_alternatives, is_robustness_indicators, + polar, weights = process_indicators_and_weights(extracted_values, input_matrix_no_alternatives, + is_robustness_indicators, is_robustness_weights, polarity, monte_carlo_runs, num_indicators) # Check the number of indicators, weights, and polarities @@ -342,7 +377,7 @@ def _handle_polarities_and_weights(is_robustness_indicators: int, is_robustness_ col_to_drop_indexes: np.ndarray, polar: List[str], config: dict, mc_runs: int, num_indicators: int) \ -> Union[Tuple[List[str], list, None, None], Tuple[List[str], None, List[List], None], - Tuple[List[str], None, None, dict]]: + Tuple[List[str], None, None, dict]]: """ Manage polarities and weights based on the specified robustness settings, ensuring that the appropriate adjustments and normalizations are applied before returning the necessary data structures. @@ -455,4 +490,3 @@ def check_indicator_weights_polarities(num_indicators: int, polar: List[str], co if (config["robustness_on_all_weights"] == "no") and ( num_indicators != len(config["given_weights"])): raise ValueError('The no. of fixed weights does not correspond to the no. of indicators') - diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 2b74c43..c95bd24 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -4,7 +4,8 @@ import pandas as pd from typing import Tuple, List, Union -from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values +from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ + check_configuration_keys from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty log = logging.getLogger(__name__) @@ -43,8 +44,14 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit self.monte_carlo = monte_carlo self.output_path = output_path - # self.validate_input_parameters_keys # TODO: still need a formal check as made in old config class, - # maybe use some of following functions validate_ + # Check configuration dictionary keys and handle potential issues + # TODO: revisit this logic when substitute classes to handle configuration settings + try: + check_configuration_keys(self.sensitivity, self.robustness, self.monte_carlo) + except KeyError as e: + print(f"Configuration Error: {e}") + raise # Optionally re-raise the error after logging it + is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() self.run_mcda(is_robustness_indicators, is_robustness_weights, weights, configuration_settings) @@ -57,18 +64,20 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] Extract and validate input configuration parameters to ensure they are correct. Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). """ + configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values( configuration_values) - # Validate input TODO: move into a different function validate_input_parameters_keys - # self.validate_normalization(self.sensitivity['normalization']) - # self.validate_aggregation(self.sensitivity['aggregation']) - # self.validate_robustness(self.robustness) - return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values + + + # self.validate_normalization(self.sensitivity['normalization']) + # self.validate_aggregation(self.sensitivity['aggregation']) + # self.validate_robustness(self.robustness) + # def validate_normalization(self, f_norm): # """ # Validate the normalization method. From 88e31c5abf5c6636f46170ffcccdcc1bc9fc0d1b Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 22 Oct 2024 21:39:40 +0200 Subject: [PATCH 07/27] refactor: add normalization as a method, failing tests --- mcda/configuration/configuration_validator.py | 11 +- mcda/mcda_functions/normalization.py | 2 +- mcda/models/ProMCDA.py | 106 ++++++++++-------- mcda/models/mcda_with_robustness.py | 2 - mcda/models/mcda_without_robustness.py | 1 - tests/unit_tests/test_promcda.py | 47 +++++++- 6 files changed, 110 insertions(+), 59 deletions(-) diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index 67c85a1..e4de691 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -160,12 +160,13 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s marginal_distribution = extracted_values["marginal_distribution_for_each_indicator"] # Check for sensitivity-related configuration errors + valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] + valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] if sensitivity_on == "no": - check_config_error(normalization not in ['minmax', 'target', 'standardized', 'rank'], - 'The available normalization functions are: minmax, target, standardized, rank.') - check_config_error(aggregation not in ['weighted_sum', 'geometric', 'harmonic', 'minimum'], - 'The available aggregation functions are: weighted_sum, geometric, harmonic, minimum.' - '\nWatch the correct spelling in the configuration.') + check_config_error(normalization not in valid_norm_methods, + f'Invalid normalization method: {normalization}. Available methods: {valid_norm_methods}') + check_config_error(aggregation not in valid_agg_methods, + f'Invalid aggregation method: {aggregation}. Available methods: {valid_agg_methods}') logger.info("ProMCDA will only use one pair of norm/agg functions: " + normalization + '/' + aggregation) else: logger.info("ProMCDA will use a set of different pairs of norm/agg functions") diff --git a/mcda/mcda_functions/normalization.py b/mcda/mcda_functions/normalization.py index 1a3a0c6..ad1b9ad 100644 --- a/mcda/mcda_functions/normalization.py +++ b/mcda/mcda_functions/normalization.py @@ -16,7 +16,7 @@ class Normalization(object): Ratio: target. """ - def __init__(self, input_matrix: pd.DataFrame, polarities: list): + def __init__(self, input_matrix: pd.DataFrame, polarities: tuple): self._input_matrix = copy.deepcopy(input_matrix) self.polarities = polarities diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index c95bd24..bcb6bd1 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -6,6 +6,7 @@ from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys +from mcda.mcda_functions.normalization import Normalization from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty log = logging.getLogger(__name__) @@ -29,12 +30,10 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit :param output_path: path for saving output files. # Example of instantiating the class and using it - promcda = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) - sensitivity = sensitivity_class(input1, input2) - aggregate = aggregate_class(input1, input2) - promcda.run_mcda() - df_normalized = promcda.normalize() - df_aggregated = promcda.aggregate() + promcda = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) + promcda.run_mcda() + df_normalized = promcda.normalize() + df_aggregated = promcda.aggregate() """ self.logger = logging.getLogger("ProMCDA") self.input_matrix = input_matrix @@ -72,51 +71,62 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values + def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: + """ + Normalize the decision matrix based on the configuration `f_norm`. + If `f_norm` is a string representing a single normalization method, + it applies that method to the decision matrix. - # self.validate_normalization(self.sensitivity['normalization']) - # self.validate_aggregation(self.sensitivity['aggregation']) - # self.validate_robustness(self.robustness) + If `f_norm` is a list of functions, each normalization function will be + applied to the input matrix sequentially, and the results will be stored + in a dictionary where the keys are function names. + + Args: + feature_range (tuple): Range for normalization methods that require it, like MinMax normalization. + The range (0.1, 1) is not needed when no aggregation will follow. + + Returns: + A single normalized DataFrame or a dictionary of DataFrames if multiple + normalization methods are applied. + """ + normalization = Normalization(self.input_matrix, self.polarity) + + sensitivity_on = self.sensitivity['sensitivity_on'] + f_norm = self.sensitivity['normalization'] + f_norm_list = ['minmax', 'target', 'standardized', 'rank'] + + if sensitivity_on == "yes": + self.normalized_matrix = {} + for norm_function in f_norm_list: + self.logger.info("Applying normalization method: %s", norm_function) + norm_method = getattr(normalization, norm_function, None) + if norm_function in ['minmax', 'target', 'standardized']: + result = norm_method(feature_range) + if result is None: + raise ValueError(f"{norm_function} method returned None") + self.normalized_matrix[norm_function] = result + else: + result = normalization.rank() + if result is None: + raise ValueError(f"{norm_function} method returned None") + self.normalized_matrix[norm_function] = result + else: + self.logger.info("Normalizing matrix with method(s): %s", f_norm) + norm_method = getattr(normalization, f_norm, None) + if f_norm in ['minmax', 'target', 'standardized']: + result = norm_method(feature_range) + if result is None: + raise ValueError(f"{f_norm} method returned None") + self.normalized_matrix = result + else: + result = norm_method() + if result is None: + raise ValueError(f"{f_norm} method returned None") + self.normalized_matrix = result + + return self.normalized_matrix - # def validate_normalization(self, f_norm): - # """ - # Validate the normalization method. - # """ - # valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] - # if f_norm not in valid_norm_methods: - # raise ValueError(f"Invalid normalization method: {f_norm}. Available methods: {valid_norm_methods}") - # - # def validate_aggregation(self, f_agg): - # """ - # Validate the aggregation method. - # """ - # valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] - # if f_agg not in valid_agg_methods: - # raise ValueError(f"Invalid aggregation method: {f_agg}. Available methods: {valid_agg_methods}") - # - # def validate_robustness(self, robustness): - # """ - # Validate robustness analysis settings. - # """ - # if not isinstance(robustness, dict): - # raise ValueError("Robustness settings must be a dictionary.") - # - # # Add more specific checks based on robustness config structure - # if robustness['on_single_weights'] == 'yes' and robustness['on_all_weights'] == 'yes': - # raise ValueError("Conflicting settings for robustness analysis on weights.") - # - # def normalize(self): - # """ - # Normalize the decision matrix based on the configuration. - # """ - # f_norm = self.sensitivity['normalization'] - # self.logger.info("Normalizing matrix with method: %s", f_norm) - # - # # Perform normalization (replace this with actual logic) - # self.normalized_matrix = normalize_matrix(self.input_matrix, f_norm) - # - # return self.normalized_matrix - # # def aggregate(self): # """ # Aggregate the decision matrix based on the configuration. diff --git a/mcda/models/mcda_with_robustness.py b/mcda/models/mcda_with_robustness.py index 366a332..df8e677 100644 --- a/mcda/models/mcda_with_robustness.py +++ b/mcda/models/mcda_with_robustness.py @@ -6,8 +6,6 @@ import pandas as pd import numpy as np -from mcda.configuration.config import Config - log = logging.getLogger(__name__) formatter = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index 8f7fba8..ab9da71 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -3,7 +3,6 @@ import logging import pandas as pd -from mcda.configuration.config import Config from mcda.mcda_functions.normalization import Normalization from mcda.mcda_functions.aggregation import Aggregation diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index e0b935c..c3785d8 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -45,8 +45,10 @@ def test_init(self): """ Test if ProMCDA initializes correctly. """ + # Given promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) + # Then self.assertEqual(promcda.input_matrix.shape, (3, 2)) self.assertEqual(promcda.polarity, self.polarity) self.assertEqual(promcda.sensitivity, self.sensitivity) @@ -57,11 +59,13 @@ def test_validate_inputs(self): """ Test if input validation works and returns the expected values. """ + # Given promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) + # When (is_robustness_indicators, is_robustness_weights, polar, weights, config) = promcda.validate_inputs() - # Validate the result + # Then self.assertIsInstance(is_robustness_indicators, int) self.assertIsInstance(is_robustness_weights, int) self.assertIsInstance(polar, tuple) @@ -70,6 +74,46 @@ def test_validate_inputs(self): self.assertEqual(is_robustness_indicators, 0) self.assertEqual(is_robustness_weights, 0) + def test_normalize_single_method(self): + """ + Test normalization with a single methods. + Test the correctness of the output values happens in unit_tests/test_normalization.py + """ + # Given + self.sensitivity['sensitivity_on'] = 'no' + + # When + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + normalized_matrix = promcda.normalize() + + # Then + self.assertIsInstance(normalized_matrix, pd.DataFrame) + + def test_normalize_multiple_methods(self): + """ + Test normalization with multiple methods. + Test the correctness of the output values happens in unit_tests/test_normalization.py + """ + self.sensitivity['sensitivity_on'] = 'yes' + self.sensitivity['normalization'] = ['minmax', 'standardized', 'rank', 'target'] + + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + normalized_matrices = promcda.normalize() + + self.assertIsInstance(normalized_matrices, dict) + self.assertIn('minmax', normalized_matrices) + self.assertIn('standardized', normalized_matrices) + self.assertIn('rank', normalized_matrices) + self.assertIn('target', normalized_matrices) + + self.assertIsInstance(normalized_matrices['minmax'], pd.DataFrame) + self.assertIsInstance(normalized_matrices['standardized'], pd.DataFrame) + self.assertIsInstance(normalized_matrices['rank'], pd.DataFrame) + self.assertIsInstance(normalized_matrices['target'], pd.DataFrame) + + def tearDown(self): """ Clean up temporary directories and files after each test. @@ -80,5 +124,4 @@ def tearDown(self): if __name__ == '__main__': unittest.main() - # TODO: write additional tests for normalization, aggregation, etc. From d9e03b12661a778f5918a4cb0d2b6d3f9cc152e7 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 24 Oct 2024 15:41:27 +0200 Subject: [PATCH 08/27] refactor: add enums for enumerations --- mcda/configuration/configuration_validator.py | 10 ++- mcda/configuration/enums.py | 62 ++++++++++++++++ mcda/models/ProMCDA.py | 14 ++-- mcda/models/mcda_without_robustness.py | 72 +++++++++++-------- mcda/utils/utils_for_main.py | 27 ++++++- tests/unit_tests/test_promcda.py | 29 ++++---- 6 files changed, 164 insertions(+), 50 deletions(-) create mode 100644 mcda/configuration/enums.py diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index e4de691..9e79524 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -5,6 +5,7 @@ import pandas as pd from typing import Tuple, List, Union, Dict, Any +from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions from mcda.utils.utils_for_main import pop_indexed_elements, check_norm_sum_weights, randomly_sample_all_weights, \ randomly_sample_ix_weight, check_input_matrix @@ -160,8 +161,13 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s marginal_distribution = extracted_values["marginal_distribution_for_each_indicator"] # Check for sensitivity-related configuration errors - valid_norm_methods = ['minmax', 'target', 'standardized', 'rank'] - valid_agg_methods = ['weighted_sum', 'geometric', 'harmonic', 'minimum'] + valid_norm_methods = [method.value for method in NormalizationFunctions] + valid_agg_methods = [method.value for method in AggregationFunctions] + if isinstance(normalization, NormalizationFunctions): + normalization = normalization.value + if isinstance(aggregation, AggregationFunctions): + aggregation = aggregation.value + if sensitivity_on == "no": check_config_error(normalization not in valid_norm_methods, f'Invalid normalization method: {normalization}. Available methods: {valid_norm_methods}') diff --git a/mcda/configuration/enums.py b/mcda/configuration/enums.py new file mode 100644 index 0000000..ef96950 --- /dev/null +++ b/mcda/configuration/enums.py @@ -0,0 +1,62 @@ +from enum import Enum + +""" +This module defines enumerations for use throughout the package to enhance maintainability. + +Enumerations (enums) provide a way to define a set of named values, which can be used to represent options or +categories in a more manageable manner, avoiding string literals or hard-coded values. +""" + +class NormalizationFunctions(Enum): + """ + Implemented normalization functions + """ + MINMAX = 'minmax' + STANDARDIZED = 'standardized' + TARGET = 'target' + RANK = 'rank' + + +class AggregationFunctions(Enum): + """ + Implemented aggregation functions + """ + WEIGHTED_SUM = 'weighted_sum' + GEOMETRIC = 'geometric' + HARMONIC = 'harmonic' + MINIMUM = 'minimum' + + +class NormalizationNames4Sensitivity(Enum): + """ + Names of normalization functions in case of sensitivity analysis + """ + MINMAX_01 = 'minmax_01' + MINMAX_WITHOUT_ZERO = 'minmax_without_zero' + TARGET_01 = 'target_01' + TARGET_WITHOUT_ZERO = 'target_without_zero' + STANDARDIZED_ANY = 'standardized_any' + STANDARDIZED_WITHOUT_ZERO = 'standardized_without_zero' + RANK = 'rank' + + +class OutputColumNames4Sensitivity(Enum): + """ + Names of output columns in case of sensitivity analysis + """ + WS_MINMAX_01 = 'ws-minmax_01' + WS_TARGET_01 = 'ws-target_01' + WS_STANDARDIZED_ANY = 'ws-standardized_any' + WS_RANK = 'ws-rank' + GEOM_MINMAX_WITHOUT_ZERO = 'geom-minmax_without_zero' + GEOM_TARGET_WITHOUT_ZERO = 'geom-target_without_zero' + GEOM_STANDARDIZED_WITHOUT_ZERO = 'geom-standardized_without_zero' + GEOM_RANK = 'geom-rank' + HARM_MINMAX_WITHOUT_ZERO = 'harm-minmax_without_zero' + HARM_TARGET_WITHOUT_ZERO = 'harm-target_without_zero' + HARM_STANDARDIZED_WITHOUT_ZERO = 'harm-standardized_without_zero' + HARM_RANK = 'harm-rank' + MIN_STANDARDIZED_ANY = 'min-standardized_any' + + + diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index bcb6bd1..84b4469 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -6,6 +6,7 @@ from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys +from mcda.configuration.enums import NormalizationFunctions from mcda.mcda_functions.normalization import Normalization from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty @@ -94,14 +95,16 @@ def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: sensitivity_on = self.sensitivity['sensitivity_on'] f_norm = self.sensitivity['normalization'] - f_norm_list = ['minmax', 'target', 'standardized', 'rank'] + if isinstance(f_norm, NormalizationFunctions): + f_norm = f_norm.value if sensitivity_on == "yes": self.normalized_matrix = {} - for norm_function in f_norm_list: + for norm_function in f_norm: self.logger.info("Applying normalization method: %s", norm_function) norm_method = getattr(normalization, norm_function, None) - if norm_function in ['minmax', 'target', 'standardized']: + if norm_function in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, + NormalizationFunctions.TARGET.value}: result = norm_method(feature_range) if result is None: raise ValueError(f"{norm_function} method returned None") @@ -114,7 +117,8 @@ def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: else: self.logger.info("Normalizing matrix with method(s): %s", f_norm) norm_method = getattr(normalization, f_norm, None) - if f_norm in ['minmax', 'target', 'standardized']: + if f_norm in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, + NormalizationFunctions.TARGET.value}: result = norm_method(feature_range) if result is None: raise ValueError(f"{f_norm} method returned None") @@ -125,7 +129,7 @@ def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: raise ValueError(f"{f_norm} method returned None") self.normalized_matrix = result - return self.normalized_matrix + return self.normalized_matrix # def aggregate(self): # """ diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index ab9da71..9441a6a 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -3,6 +3,8 @@ import logging import pandas as pd +from mcda.configuration.enums import NormalizationFunctions, OutputColumNames4Sensitivity, \ + NormalizationNames4Sensitivity, AggregationFunctions from mcda.mcda_functions.normalization import Normalization from mcda.mcda_functions.aggregation import Aggregation @@ -53,29 +55,35 @@ def normalize_indicators(self, method=None) -> dict: normalized_indicators = {} - if method is None or method == 'minmax': + if isinstance(method, NormalizationFunctions): + method = method.value + if method is None or method == NormalizationFunctions.MINMAX.value: indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1)) # for aggregation "geometric" and "harmonic" that do not accept 0 indicators_scaled_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) - normalized_indicators["minmax_without_zero"] = indicators_scaled_minmax_without_zero - normalized_indicators["minmax_01"] = indicators_scaled_minmax_01 - if method is None or method == 'target': + normalized_indicators[NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value] = \ + indicators_scaled_minmax_without_zero + normalized_indicators[NormalizationNames4Sensitivity.MINMAX_01.value] = indicators_scaled_minmax_01 + if method is None or method == NormalizationFunctions.TARGET.value: indicators_scaled_target_01 = norm.target(feature_range=(0, 1)) indicators_scaled_target_without_zero = norm.target( feature_range=(0.1, 1)) # for aggregation "geometric" and "harmonic" that do not accept 0 - normalized_indicators["target_without_zero"] = indicators_scaled_target_without_zero - normalized_indicators["target_01"] = indicators_scaled_target_01 - if method is None or method == 'standardized': + normalized_indicators[NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value] = \ + indicators_scaled_target_without_zero + normalized_indicators[NormalizationNames4Sensitivity.TARGET_01.value] = indicators_scaled_target_01 + if method is None or method == NormalizationFunctions.STANDARDIZED.value: indicators_scaled_standardized_any = norm.standardized( feature_range=('-inf', '+inf')) indicators_scaled_standardized_without_zero = norm.standardized( feature_range=(0.1, '+inf')) - normalized_indicators["standardized_any"] = indicators_scaled_standardized_any - normalized_indicators["standardized_without_zero"] = indicators_scaled_standardized_without_zero - if method is None or method == 'rank': + normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_ANY.value] = indicators_scaled_standardized_any + normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value] = indicators_scaled_standardized_without_zero + if method is None or method == NormalizationFunctions.RANK.value: indicators_scaled_rank = norm.rank() - normalized_indicators["rank"] = indicators_scaled_rank - if method is not None and method not in ['minmax', 'target', 'standardized', 'rank']: + normalized_indicators[NormalizationNames4Sensitivity.RANK.value] = indicators_scaled_rank + if isinstance(method, NormalizationFunctions): + method = method.value + if method is not None and method not in [method.value for method in NormalizationFunctions]: logger.error('Error Message', stack_info=True) raise ValueError( 'The selected normalization method is not supported') @@ -113,32 +121,37 @@ def aggregate_indicators(self, normalized_indicators: dict, weights: list, metho scores = pd.DataFrame() col_names_method = [] - col_names = ['ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', - 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', - 'geom-rank', 'harm-minmax_without_zero', 'harm-target_without_zero', - 'harm-standardized_without_zero', 'harm-rank', 'min-standardized_any'] + col_names = [method.value for method in OutputColumNames4Sensitivity] # column names has the same order as in the following loop for key, values in self.normalized_indicators.items(): - if method is None or method == 'weighted_sum': - if key in ["standardized_any", "minmax_01", "target_01", - "rank"]: # ws goes only with some specific normalizations + if isinstance(method, AggregationFunctions): + method = method.value + if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: + if key in [NormalizationNames4Sensitivity.STANDARDIZED_ANY.value, + NormalizationNames4Sensitivity.MINMAX_01.value, + NormalizationNames4Sensitivity.TARGET_01.value, NormalizationNames4Sensitivity.RANK.value]: + # ws goes only with some specific normalizations scores_weighted_sum[key] = agg.weighted_sum(values) col_names_method.append("ws-" + key) - if method is None or method == 'geometric': - if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", - "rank"]: # geom goes only with some specific normalizations + if method is None or method == AggregationFunctions.GEOMETRIC.value: + if key in [NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.RANK.value]: # geom goes only with some specific normalizations scores_geometric[key] = pd.Series(agg.geometric(values)) col_names_method.append("geom-" + key) - if method is None or method == 'harmonic': - if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", - "rank"]: # harm goes only with some specific normalizations + if method is None or method == AggregationFunctions.HARMONIC.value: + if key in [NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value, + NormalizationNames4Sensitivity.RANK.value]: # harm goes only with some specific normalizations scores_harmonic[key] = pd.Series(agg.harmonic(values)) col_names_method.append("harm-" + key) - if method is None or method == 'minimum': - if key == "standardized_any": + if method is None or method == AggregationFunctions.MINIMUM.value: + if key == NormalizationNames4Sensitivity.STANDARDIZED_ANY.value: scores_minimum[key] = pd.Series(agg.minimum( - self.normalized_indicators["standardized_any"])) + self.normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_ANY.value])) col_names_method.append("min-" + key) dict_list = [scores_weighted_sum, scores_geometric, @@ -153,4 +166,5 @@ def aggregate_indicators(self, normalized_indicators: dict, weights: list, metho else: scores.columns = col_names_method - return scores \ No newline at end of file + return scores + diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 4f5cb13..3cf5ad5 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -5,6 +5,7 @@ import random import logging import sys +from enum import Enum from typing import Union, Any, List from typing import Optional @@ -232,6 +233,29 @@ def save_dict(dictionary: dict, folder_path: str, filename: str): logging.error(f"Error while dumping the dictionary into a pickle file: {e}") +def preprocess_enums(data) -> str: + """ + Preprocess data to convert enums to strings + + Parameters: + - data: to be processed + + Example: + ```python + preprocess_enums(data) + ``` + :param data: enums + :return: string + """ + if isinstance(data, dict): + return {k: preprocess_enums(v) for k, v in data.items()} + elif isinstance(data, list): + return [preprocess_enums(v) for v in data] + elif isinstance(data, Enum): + return data.value + return data + + def save_config(config: dict, folder_path: str, filename: str): """ Save a configuration dictionary to a JSON file with a timestamped filename. @@ -270,7 +294,8 @@ def save_config(config: dict, folder_path: str, filename: str): try: with open(full_output_path, 'w') as fp: - serializable_config = _prepare_config_for_json(config) + processed_config = preprocess_enums(config) + serializable_config = _prepare_config_for_json(processed_config) json.dump(serializable_config, fp) except IOError as e: logging.error(f"Error while dumping the configuration into a JSON file: {e}") diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index c3785d8..2b22920 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -4,7 +4,9 @@ import warnings import pandas as pd + from mcda.models.ProMCDA import ProMCDA +from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions class TestProMCDA(unittest.TestCase): @@ -20,8 +22,8 @@ def setUp(self): self.sensitivity = { 'sensitivity_on': 'no', - 'normalization': 'minmax', - 'aggregation': 'weighted_sum' + 'normalization': NormalizationFunctions.MINMAX, + 'aggregation': AggregationFunctions.WEIGHTED_SUM } self.robustness = { @@ -95,23 +97,24 @@ def test_normalize_multiple_methods(self): Test normalization with multiple methods. Test the correctness of the output values happens in unit_tests/test_normalization.py """ + self.sensitivity['sensitivity_on'] = 'yes' - self.sensitivity['normalization'] = ['minmax', 'standardized', 'rank', 'target'] + self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) + self.output_path) normalized_matrices = promcda.normalize() self.assertIsInstance(normalized_matrices, dict) - self.assertIn('minmax', normalized_matrices) - self.assertIn('standardized', normalized_matrices) - self.assertIn('rank', normalized_matrices) - self.assertIn('target', normalized_matrices) - - self.assertIsInstance(normalized_matrices['minmax'], pd.DataFrame) - self.assertIsInstance(normalized_matrices['standardized'], pd.DataFrame) - self.assertIsInstance(normalized_matrices['rank'], pd.DataFrame) - self.assertIsInstance(normalized_matrices['target'], pd.DataFrame) + self.assertIn(NormalizationFunctions.MINMAX.value, normalized_matrices) + self.assertIn(NormalizationFunctions.STANDARDIZED.value, normalized_matrices) + self.assertIn(NormalizationFunctions.RANK.value, normalized_matrices) + self.assertIn(NormalizationFunctions.TARGET.value, normalized_matrices) + + self.assertIsInstance(normalized_matrices[NormalizationFunctions.MINMAX.value], pd.DataFrame) + self.assertIsInstance(normalized_matrices[NormalizationFunctions.STANDARDIZED.value], pd.DataFrame) + self.assertIsInstance(normalized_matrices[NormalizationFunctions.RANK.value], pd.DataFrame) + self.assertIsInstance(normalized_matrices[NormalizationFunctions.TARGET.value], pd.DataFrame) def tearDown(self): From 5c0cd1d523815b3bc30b56ecd33b3280ab35e33f Mon Sep 17 00:00:00 2001 From: Flaminia Date: Fri, 1 Nov 2024 16:23:28 +0100 Subject: [PATCH 09/27] refactor: simplify normalize and aggregate methods and draft unit tests, without robustness --- mcda/configuration/enums.py | 6 +- mcda/models/ProMCDA.py | 224 ++++++++++++++++++------- mcda/models/mcda_without_robustness.py | 4 +- tests/unit_tests/test_promcda.py | 223 ++++++++++++++++++++---- 4 files changed, 356 insertions(+), 101 deletions(-) diff --git a/mcda/configuration/enums.py b/mcda/configuration/enums.py index ef96950..65aa346 100644 --- a/mcda/configuration/enums.py +++ b/mcda/configuration/enums.py @@ -31,16 +31,16 @@ class NormalizationNames4Sensitivity(Enum): """ Names of normalization functions in case of sensitivity analysis """ - MINMAX_01 = 'minmax_01' MINMAX_WITHOUT_ZERO = 'minmax_without_zero' - TARGET_01 = 'target_01' + MINMAX_01 = 'minmax_01' TARGET_WITHOUT_ZERO = 'target_without_zero' + TARGET_01 = 'target_01' STANDARDIZED_ANY = 'standardized_any' STANDARDIZED_WITHOUT_ZERO = 'standardized_without_zero' RANK = 'rank' -class OutputColumNames4Sensitivity(Enum): +class OutputColumnNames4Sensitivity(Enum): """ Names of output columns in case of sensitivity analysis """ diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 84b4469..fefc9ab 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -4,11 +4,12 @@ import pandas as pd from typing import Tuple, List, Union + from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys -from mcda.configuration.enums import NormalizationFunctions -from mcda.mcda_functions.normalization import Normalization -from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty +from mcda.models.mcda_without_robustness import MCDAWithoutRobustness +from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty, \ + check_input_matrix log = logging.getLogger(__name__) @@ -52,8 +53,10 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit print(f"Configuration Error: {e}") raise # Optionally re-raise the error after logging it + self.configuration_settings = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, + self.robustness, self.monte_carlo, self.output_path) is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() - self.run_mcda(is_robustness_indicators, is_robustness_weights, weights, configuration_settings) + self.run_mcda(is_robustness_indicators, is_robustness_weights, weights) self.normalized_matrix = None self.aggregated_matrix = None @@ -72,80 +75,170 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: + def normalize(self, method=None) -> dict: + """ + Normalize the input data using the specified method. + + Parameters: + - method (optional): The normalization method to use. If None, all available methods will be applied. + + Returns: + - A dictionary containing the normalized values of each indicator per normalization method. """ - Normalize the decision matrix based on the configuration `f_norm`. + input_matrix_no_alternatives = check_input_matrix(self.input_matrix) + mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) + normalized_values = mcda_without_robustness.normalize_indicators(method) - If `f_norm` is a string representing a single normalization method, - it applies that method to the decision matrix. + return normalized_values - If `f_norm` is a list of functions, each normalization function will be - applied to the input matrix sequentially, and the results will be stored - in a dictionary where the keys are function names. + def aggregate(self, normalization_method=None, aggregation_method=None, weights=None): + """ + Aggregate normalized indicators using the specified method. - Args: - feature_range (tuple): Range for normalization methods that require it, like MinMax normalization. - The range (0.1, 1) is not needed when no aggregation will follow. + Parameters (optional): + - normalization_method: The normalization method to use. If None, all available methods will be applied. + - aggregation_method: The aggregation method to use. If None, all available methods will be applied. + - weights: The weights to be used for aggregation. If None, they are set all the same. Returns: - A single normalized DataFrame or a dictionary of DataFrames if multiple - normalization methods are applied. + - A DataFrame containing the aggregated scores. """ - normalization = Normalization(self.input_matrix, self.polarity) - - sensitivity_on = self.sensitivity['sensitivity_on'] - f_norm = self.sensitivity['normalization'] - if isinstance(f_norm, NormalizationFunctions): - f_norm = f_norm.value - - if sensitivity_on == "yes": - self.normalized_matrix = {} - for norm_function in f_norm: - self.logger.info("Applying normalization method: %s", norm_function) - norm_method = getattr(normalization, norm_function, None) - if norm_function in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, - NormalizationFunctions.TARGET.value}: - result = norm_method(feature_range) - if result is None: - raise ValueError(f"{norm_function} method returned None") - self.normalized_matrix[norm_function] = result - else: - result = normalization.rank() - if result is None: - raise ValueError(f"{norm_function} method returned None") - self.normalized_matrix[norm_function] = result - else: - self.logger.info("Normalizing matrix with method(s): %s", f_norm) - norm_method = getattr(normalization, f_norm, None) - if f_norm in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, - NormalizationFunctions.TARGET.value}: - result = norm_method(feature_range) - if result is None: - raise ValueError(f"{f_norm} method returned None") - self.normalized_matrix = result - else: - result = norm_method() - if result is None: - raise ValueError(f"{f_norm} method returned None") - self.normalized_matrix = result - - return self.normalized_matrix - - # def aggregate(self): + + input_matrix_no_alternatives = check_input_matrix(self.input_matrix) + mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) + normalized_indicators = self.normalize(normalization_method) + + aggregated_scores = mcda_without_robustness.aggregate_indicators( + normalized_indicators=normalized_indicators, + weights=weights, + method=aggregation_method + ) + + return aggregated_scores + + + # def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: + # """ + # Normalize the decision matrix based on the configuration `f_norm`. + # + # If `f_norm` is a string representing a single normalization method, + # it applies that method to the decision matrix. + # + # If `f_norm` is a list of functions, each normalization function will be + # applied to the input matrix sequentially, and the results will be stored + # in a dictionary where the keys are function names. + # + # Args: + # feature_range (tuple): Range for normalization methods that require it, like MinMax normalization. + # The range (0.1, 1) is not needed when no aggregation will follow. + # + # Returns: + # A single normalized DataFrame or a dictionary of DataFrames if multiple + # normalization methods are applied. + # """ + # normalization = Normalization(self.input_matrix, self.polarity) + # + # sensitivity_on = self.sensitivity['sensitivity_on'] + # f_norm = self.sensitivity['normalization'] + # if isinstance(f_norm, NormalizationFunctions): + # f_norm = f_norm.value + # + # if sensitivity_on == "yes": + # self.normalized_matrix = {} + # for norm_function in f_norm: + # self.logger.info("Applying normalization method: %s", norm_function) + # norm_method = getattr(normalization, norm_function, None) + # if norm_function in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, + # NormalizationFunctions.TARGET.value}: + # result = norm_method(feature_range) + # if result is None: + # raise ValueError(f"{norm_function} method returned None") + # self.normalized_matrix[norm_function] = result + # else: + # result = normalization.rank() + # if result is None: + # raise ValueError(f"{norm_function} method returned None") + # self.normalized_matrix[norm_function] = result + # else: + # self.logger.info("Normalizing matrix with method(s): %s", f_norm) + # norm_method = getattr(normalization, f_norm, None) + # if f_norm in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, + # NormalizationFunctions.TARGET.value}: + # result = norm_method(feature_range) + # if result is None: + # raise ValueError(f"{f_norm} method returned None") + # self.normalized_matrix = result + # else: + # result = norm_method() + # if result is None: + # raise ValueError(f"{f_norm} method returned None") + # self.normalized_matrix = result + # + # return self.normalized_matrix + + + # def aggregate(self, normalized_matrix=None) -> Union[pd.DataFrame, dict]: # """ - # Aggregate the decision matrix based on the configuration. + # Aggregate the normalized indicators based on the configuration. + # + # Parameters: + # normalized_matrix (pd.DataFrame, optional): The matrix to aggregate. + # Defaults to self.normalized_matrix. + # Raises: + # ValueError: If no normalized matrix is provided or normalization was not performed. + # + # Returns: + # The aggregated matrix or dictionary of aggregated results. # """ + # normalized_matrix = normalized_matrix if normalized_matrix is not None else self.normalized_matrix + # + # if normalized_matrix is None or ( + # isinstance(self.normalized_matrix, pd.DataFrame) and normalized_matrix.empty) or \ + # (isinstance(self.normalized_matrix, dict) and all(df.empty for df in self.normalized_matrix.values())): + # raise ValueError("Normalization must be performed before aggregation.") + # + # configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, + # self.robustness, self.monte_carlo, self.output_path) + # + # weights = configuration_values["given_weights"] + # sensitivity_on = self.sensitivity['sensitivity_on'] + # aggregation = Aggregation(weights) # f_agg = self.sensitivity['aggregation'] - # self.logger.info("Aggregating matrix with method: %s", f_agg) + # if isinstance(f_agg, AggregationFunctions): + # f_agg = f_agg.value + # self.logger.info("Aggregating with method: %s", f_agg) + # + # if sensitivity_on == "yes": + # self.aggregated_matrix = {} + # for agg_function in f_agg: + # self.logger.info("Applying aggregation methods: %s", agg_function) + # + # if isinstance(normalized_matrix, dict): + # self.aggregated_matrix = {} + # for norm_method, norm_df in normalized_matrix.items(): + # agg_method = getattr(aggregation, agg_function, None) + # result = agg_method(norm_df) + # if result is None: + # raise ValueError(f"{f_agg} method returned None for {norm_method}") + # result_key = f"{f_agg}_{norm_method}" + # self.aggregated_matrix[result_key] = result # - # # Perform aggregation (replace this with actual logic) - # self.aggregated_matrix = aggregate_matrix(self.normalized_matrix, f_agg) + # if result is None: + # raise ValueError(f"{agg_function} method returned None") + # self.aggregated_matrix[agg_function] = result + # else: + # self.logger.info("Applying aggregation method: %s", f_agg) + # agg_method = getattr(aggregation, f_agg, None) + # result = agg_method(normalized_matrix) + # if result is None: + # raise ValueError(f"{f_agg} method returned None") + # self.aggregated_matrix = result # # return self.aggregated_matrix + def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, - weights: Union[list, List[list], dict], - configuration_settings: dict): + weights: Union[list, List[list], dict]): """ Execute the full ProMCDA process, either with or without uncertainties on the indicators. """ @@ -160,14 +253,15 @@ def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, # Run # no uncertainty if is_robustness_indicators == 0: - run_mcda_without_indicator_uncertainty(configuration_settings, is_robustness_weights, weights) + run_mcda_without_indicator_uncertainty(self.configuration_settings, is_robustness_weights, weights) # uncertainty else: - run_mcda_with_indicator_uncertainty(configuration_settings) + run_mcda_with_indicator_uncertainty(self.configuration_settings) elapsed_time = time.time() - start_time self.logger.info("ProMCDA finished calculations in %s seconds", elapsed_time) + # def get_results(self): # """ # Return the final results as a DataFrame or other relevant structure. diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index 9441a6a..fab3b4e 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -3,7 +3,7 @@ import logging import pandas as pd -from mcda.configuration.enums import NormalizationFunctions, OutputColumNames4Sensitivity, \ +from mcda.configuration.enums import NormalizationFunctions, OutputColumnNames4Sensitivity, \ NormalizationNames4Sensitivity, AggregationFunctions from mcda.mcda_functions.normalization import Normalization from mcda.mcda_functions.aggregation import Aggregation @@ -121,7 +121,7 @@ def aggregate_indicators(self, normalized_indicators: dict, weights: list, metho scores = pd.DataFrame() col_names_method = [] - col_names = [method.value for method in OutputColumNames4Sensitivity] + col_names = [method.value for method in OutputColumnNames4Sensitivity] # column names has the same order as in the following loop for key, values in self.normalized_indicators.items(): diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 2b22920..f99b999 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -6,7 +6,8 @@ import pandas as pd from mcda.models.ProMCDA import ProMCDA -from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions +from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions, OutputColumnNames4Sensitivity, \ + NormalizationNames4Sensitivity class TestProMCDA(unittest.TestCase): @@ -76,45 +77,205 @@ def test_validate_inputs(self): self.assertEqual(is_robustness_indicators, 0) self.assertEqual(is_robustness_weights, 0) - def test_normalize_single_method(self): - """ - Test normalization with a single methods. - Test the correctness of the output values happens in unit_tests/test_normalization.py - """ + def test_normalize_all_methods(self): # Given - self.sensitivity['sensitivity_on'] = 'no' + self.sensitivity['sensitivity_on'] = 'yes' + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + + expected_keys = [method.value for method in NormalizationNames4Sensitivity] # When + normalized_values = promcda.normalize() + + # Then + self.assertCountEqual(list(set(normalized_values.keys())), expected_keys, + "Not all methods were applied or extra keys found.") + self.assertEqual(list(normalized_values), (expected_keys)) + + def test_normalize_specific_method(self): + # Given promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - normalized_matrix = promcda.normalize() + self.output_path) + method = 'minmax' + + # When + normalized_values = promcda.normalize(method=method) # Then - self.assertIsInstance(normalized_matrix, pd.DataFrame) + expected_keys = ["minmax_without_zero", "minmax_01"] + self.assertCountEqual(expected_keys, list(normalized_values.keys())) + self.assertEqual(list(normalized_values), expected_keys) - def test_normalize_multiple_methods(self): - """ - Test normalization with multiple methods. - Test the correctness of the output values happens in unit_tests/test_normalization.py - """ + def test_aggregate_all_methods(self): + # Test when no specific method is given, so all methods should be applied + aggregated_scores = self.pro_mcda.aggregate() - self.sensitivity['sensitivity_on'] = 'yes' - self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] + # Verify the structure of the DataFrame + expected_columns = [ + 'ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', + 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', 'geom-rank', + 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', + 'min-standardized_any' + ] + self.assertCountEqual(aggregated_scores.columns, expected_columns, + "Not all methods were applied or extra columns found.") + self.assertEqual(len(aggregated_scores), len(self.input_matrix), + "Number of alternatives does not match input matrix rows.") - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - normalized_matrices = promcda.normalize() - - self.assertIsInstance(normalized_matrices, dict) - self.assertIn(NormalizationFunctions.MINMAX.value, normalized_matrices) - self.assertIn(NormalizationFunctions.STANDARDIZED.value, normalized_matrices) - self.assertIn(NormalizationFunctions.RANK.value, normalized_matrices) - self.assertIn(NormalizationFunctions.TARGET.value, normalized_matrices) - - self.assertIsInstance(normalized_matrices[NormalizationFunctions.MINMAX.value], pd.DataFrame) - self.assertIsInstance(normalized_matrices[NormalizationFunctions.STANDARDIZED.value], pd.DataFrame) - self.assertIsInstance(normalized_matrices[NormalizationFunctions.RANK.value], pd.DataFrame) - self.assertIsInstance(normalized_matrices[NormalizationFunctions.TARGET.value], pd.DataFrame) + def test_aggregate_with_specific_normalization_and_aggregation_methods(self): + # Test specific normalization and aggregation methods + normalization_method = 'minmax' + aggregation_method = 'weighted_sum' + aggregated_scores = self.pro_mcda.aggregate(normalization_method=normalization_method, + aggregation_method=aggregation_method) + + # Expected columns when only weighted_sum with minmax is applied + expected_columns = ['ws-minmax_01'] + self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") + + # Check if values are within expected range + self.assertTrue( + (aggregated_scores['ws-minmax_01'] >= 0).all() and (aggregated_scores['ws-minmax_01'] <= 1).all(), + "Values should be in the range [0, 1] for minmax normalization with weighted sum.") + + def test_aggregate_with_default_weights(self): + # Test with default weights (None) + aggregated_scores = self.pro_mcda.aggregate(weights=None) + + # Check if output DataFrame matches the expected structure and size + expected_columns = [ + 'ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', + 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', 'geom-rank', + 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', + 'min-standardized_any' + ] + self.assertCountEqual(aggregated_scores.columns, expected_columns, + "Not all methods were applied or extra columns found.") + + # Verify that weights were automatically set to equal distribution + self.assertTrue(np.allclose(self.pro_mcda.weights, [0.5, 0.5]), + "Default weights should be equal if None is passed.") + + # def test_normalize_single_method(self): + # """ + # Test normalization with a single methods. + # Test the correctness of the output values happens in unit_tests/test_normalization.py + # """ + # # Given + # self.sensitivity['sensitivity_on'] = 'no' + # + # # When + # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + # self.output_path) + # normalized_matrix = promcda.normalize() + # + # # Then + # self.assertIsInstance(normalized_matrix, pd.DataFrame) + # + # def test_normalize_multiple_methods(self): + # """ + # Test normalization with multiple methods. + # Test the correctness of the output values happens in unit_tests/test_normalization.py + # """ + # # Given + # self.sensitivity['sensitivity_on'] = 'yes' + # self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] + # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + # self.output_path) + # # When + # normalized_matrices = promcda.normalize() + # + # # Then + # self.assertIsInstance(normalized_matrices, dict) + # self.assertIn(NormalizationFunctions.MINMAX.value, normalized_matrices) + # self.assertIn(NormalizationFunctions.STANDARDIZED.value, normalized_matrices) + # self.assertIn(NormalizationFunctions.RANK.value, normalized_matrices) + # self.assertIn(NormalizationFunctions.TARGET.value, normalized_matrices) + # + # self.assertIsInstance(normalized_matrices[NormalizationFunctions.MINMAX.value], pd.DataFrame) + # self.assertIsInstance(normalized_matrices[NormalizationFunctions.STANDARDIZED.value], pd.DataFrame) + # self.assertIsInstance(normalized_matrices[NormalizationFunctions.RANK.value], pd.DataFrame) + # self.assertIsInstance(normalized_matrices[NormalizationFunctions.TARGET.value], pd.DataFrame) + # + # def test_aggregate_with_sensitivity_on(self): + # """ + # Test aggregation when sensitivity_on is 'yes' + # """ + # + # # Given + # self.sensitivity['sensitivity_on'] = 'yes' + # self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] + # self.sensitivity['aggregation'] = [method.value for method in AggregationFunctions] + # + # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + # self.output_path) + # + # normalized_matrix = { # Mock normalized matrices in a dictionary for sensitivity-on + # "minmax": pd.DataFrame({ + # "indicator_1": [0.1, 0.2, 0.3], + # "indicator_2": [0.4, 0.5, 0.6] + # }), + # "standardized": pd.DataFrame({ + # "indicator_1": [1, 0.1, 0.7], + # "indicator_2": [0.5, 0.1, 0.8] + # }), + # "target": pd.DataFrame({ + # "indicator_1": [0.15, 0.25, 0.35], + # "indicator_2": [0.45, 0.55, 0.65] + # }), + # "rank": pd.DataFrame({ + # "indicator_1": [1, 2, 3], + # "indicator_2": [1, 2, 3] + # }) + # } + # + # # When + # result = promcda.aggregate(normalized_matrix) + # expected_results = { + # OutputColumnNames4Sensitivity.WS_MINMAX_01.value: pd.Series([0.25, 0.35, 0.45], name="ws-minmax_01"), + # OutputColumnNames4Sensitivity.WS_TARGET_01.value: pd.Series([0.3, 0.4, 0.5], name="ws-target_01"), + # OutputColumnNames4Sensitivity.WS_STANDARDIZED_ANY.value: pd.Series([0.8, 0, 0.74], name="ws-standardized_any"), + # OutputColumnNames4Sensitivity.WS_RANK.value: pd.Series([1.5, 2.5, 3.5], name="ws-rank"), + # OutputColumnNames4Sensitivity.GEOM_MINMAX_WITHOUT_ZERO.value: pd.Series([0.2, 0.3, 0.4], name="geom-minmax_without_zero"), + # OutputColumnNames4Sensitivity.GEOM_TARGET_WITHOUT_ZERO.value: pd.Series([0.25, 0.35, 0.45], name="geom-target_without_zero"), + # OutputColumnNames4Sensitivity.GEOM_STANDARDIZED_WITHOUT_ZERO.value: pd.Series([0.1, 0, 0.3], name="geom-standardized_without_zero"), + # OutputColumnNames4Sensitivity.GEOM_RANK.value: pd.Series([1, 2, 3], name="geom-rank"), + # OutputColumnNames4Sensitivity.HARM_MINMAX_WITHOUT_ZERO.value: pd.Series([0.18, 0.27, 0.36], name="harm-minmax_without_zero"), + # OutputColumnNames4Sensitivity.HARM_TARGET_WITHOUT_ZERO.value: pd.Series([0.22, 0.33, 0.44], name="harm-target_without_zero"), + # OutputColumnNames4Sensitivity.HARM_STANDARDIZED_WITHOUT_ZERO.value: pd.Series([0.05, None, 0.15], name="harm-standardized_without_zero"), + # OutputColumnNames4Sensitivity.HARM_RANK.value: pd.Series([0.9, 1.8, 2.7], name="harm-rank"), + # OutputColumnNames4Sensitivity.MIN_STANDARDIZED_ANY.value: pd.Series([-1, 0, 1], name="min-standardized_any"), + # } + # expected_df = pd.DataFrame(expected_results, index=['A', 'B', 'C']) + # + # # Then + # #self.assertEqual(result, expected_df) + # self.assertIsInstance(result, pd.DataFrame) + # + # def test_aggregate_with_sensitivity_off(self): + # """ + # Test aggregation when sensitivity_on is 'no' + # """ + # # Given + # self.promcda.sensitivity['sensitivity_on'] = 'no' + # self.promcda.sensitivity['aggregation'] = 'weighted_sum' + # + # # When + # expected_result = { + # 'Min-Max + Weighted Sum': [0.5667, 0.4, 0.6], + # 'Standardized + Weighted Sum': [-0.1069, -0.3667, 0.3667], + # 'Rank + Weighted Sum': [2.0, 1.8, 2.2], + # 'Min-Max + Geometric': [0.5695, 0.0, 0.0], + # 'Rank + Geometric': [2.0, 1.933, 2.067], + # 'Min-Max + Harmonic': [0.5455, 0.0, 0.0], + # 'Rank + Harmonic': [2.0, 1.36, 1.85], + # 'Min-Max + Minimum': [0.5, 0.0, 0.0], + # 'Rank + Minimum': [2, 1, 1] + # } + # df = pd.DataFrame(expected_result, index=['A', 'B', 'C']) + # + # # Then def tearDown(self): From 13783546d2322a36699b3eb5144f31651a07e639 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 12 Nov 2024 17:29:31 +0100 Subject: [PATCH 10/27] add notebook for testing library function, change normalized_scores and scores from dict to dataframe, broken tests --- demo_in_notebook/Untitled.ipynb | 269 ++++++++++++++++++ .../2024-11-05_14-53-42_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_14-53-46_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_16-34-56_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_16-34-56_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_16-34-56_configuration.json | 1 + .../2024-11-05_16-34-56_normalized_scores.csv | 4 + .../mock_output/2024-11-05_16-34-56_ranks.csv | 4 + .../2024-11-05_16-34-56_scores.csv | 4 + .../2024-11-05_17-10-33_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_17-10-33_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_17-10-33_configuration.json | 1 + .../2024-11-05_17-10-33_normalized_scores.csv | 4 + .../mock_output/2024-11-05_17-10-33_ranks.csv | 4 + .../2024-11-05_17-10-33_scores.csv | 4 + .../2024-11-05_17-15-31_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_17-15-31_configuration.json | 1 + .../2024-11-05_17-15-31_normalized_scores.csv | 4 + .../mock_output/2024-11-05_17-15-31_ranks.csv | 4 + .../2024-11-05_17-15-31_scores.csv | 4 + .../2024-11-05_17-15-32_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_17-26-33_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_17-26-33_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_17-26-33_configuration.json | 1 + .../2024-11-05_17-26-33_normalized_scores.csv | 4 + .../mock_output/2024-11-05_17-26-33_ranks.csv | 4 + .../2024-11-05_17-26-33_scores.csv | 4 + .../2024-11-05_17-30-33_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_17-30-33_configuration.json | 1 + .../2024-11-05_17-30-33_normalized_scores.csv | 4 + .../mock_output/2024-11-05_17-30-33_ranks.csv | 4 + .../2024-11-05_17-30-33_scores.csv | 4 + .../2024-11-05_17-30-34_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_17-45-20_MCDA_rough_scores.png | Bin 0 -> 32090 bytes .../2024-11-05_17-45-20_configuration.json | 1 + .../2024-11-05_17-45-20_normalized_scores.csv | 4 + .../mock_output/2024-11-05_17-45-20_ranks.csv | 4 + .../2024-11-05_17-45-20_scores.csv | 4 + .../2024-11-05_17-45-21_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_20-09-57_configuration.json | 1 + .../2024-11-05_20-09-57_normalized_scores.csv | 4 + .../mock_output/2024-11-05_20-09-57_ranks.csv | 4 + .../2024-11-05_20-09-57_scores.csv | 4 + .../2024-11-05_20-09-58_MCDA_norm_scores.png | Bin 0 -> 31035 bytes .../2024-11-05_20-09-58_MCDA_rough_scores.png | Bin 0 -> 32090 bytes demo_in_notebook/test_import.py | 10 + mcda/models/ProMCDA.py | 150 +--------- mcda/models/mcda_without_robustness.py | 211 ++++++++------ mcda/utils/utils_for_main.py | 8 +- tests/unit_tests/test_promcda.py | 64 ++--- 50 files changed, 539 insertions(+), 264 deletions(-) create mode 100644 demo_in_notebook/Untitled.ipynb create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_14-53-46_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-32_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-34_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-21_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png create mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png create mode 100644 demo_in_notebook/test_import.py diff --git a/demo_in_notebook/Untitled.ipynb b/demo_in_notebook/Untitled.ipynb new file mode 100644 index 0000000..b96de1a --- /dev/null +++ b/demo_in_notebook/Untitled.ipynb @@ -0,0 +1,269 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "96df2c84-1509-4e93-952a-9beba8d0ec45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Import successful!\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import pandas as pd\n", + "\n", + "package_path = '/Users/flaminia/Documents/work/ProMCDA'\n", + "\n", + "if package_path not in sys.path:\n", + " sys.path.append(package_path)\n", + "\n", + "try:\n", + " from mcda.models.ProMCDA import ProMCDA\n", + " print(\"Import successful!\")\n", + "except ModuleNotFoundError as e:\n", + " print(f\"ModuleNotFoundError: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input_matrix': Criteria 1 Criteria 2\n", + " A 0.5 0.3\n", + " B 0.2 0.6\n", + " C 0.8 0.1,\n", + " 'polarity': ('+', '-'),\n", + " 'sensitivity': {'sensitivity_on': 'no',\n", + " 'normalization': 'minmax',\n", + " 'aggregation': 'weighted_sum'},\n", + " 'robustness': {'robustness_on': 'no',\n", + " 'on_single_weights': 'no',\n", + " 'on_all_weights': 'no',\n", + " 'on_indicators': 'no',\n", + " 'given_weights': [0.6, 0.4]},\n", + " 'monte_carlo': {'monte_carlo_runs': 1000,\n", + " 'num_cores': 2,\n", + " 'random_seed': 42,\n", + " 'marginal_distribution_for_each_indicator': 'normal'},\n", + " 'output_path': 'mock_output/'}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def setUp():\n", + " \n", + " # Mock input data for testing\n", + " input_matrix_without_uncertainty = pd.DataFrame({\n", + " 'Criteria 1': [0.5, 0.2, 0.8],\n", + " 'Criteria 2': [0.3, 0.6, 0.1]\n", + " }, index=['A', 'B', 'C'])\n", + "\n", + " input_matrix_with_uncertainty = pd.DataFrame({\n", + " 'alternatives': ['alt1', 'alt2', 'alt3', 'alt4'],\n", + " 'ind1_min': [-15.20, -12.40, 10.60, -39.70],\n", + " 'ind1_max': [8.20, 8.70, 2.00, 14.00],\n", + " 'ind2': [0.04, 0.05, 0.11, 0.01],\n", + " 'ind3_average': [24.50, 24.50, 14.00, 26.50],\n", + " 'ind3_std': [6.20, 4.80, 0.60, 4.41],\n", + " 'ind4_average': [-15.20, -12.40, 1.60, -39.70],\n", + " 'ind4_std': [8.20, 8.70, 2.00, 14.00],\n", + " 'ind5': [0.04, 0.05, 0.11, 0.01],\n", + " 'ind6_average': [24.50, 24.50, 14.00, 26.50],\n", + " 'ind6_std': [6.20, 4.80, 0.60, 4.41]\n", + " })\n", + " \n", + " input_matrix_with_uncertainty.set_index('alternatives', inplace=True)\n", + "\n", + " polarity = ('+', '-')\n", + "\n", + " sensitivity = {\n", + " 'sensitivity_on': 'no',\n", + " 'normalization': 'minmax',\n", + " 'aggregation': 'weighted_sum'\n", + " }\n", + "\n", + " robustness = {\n", + " 'robustness_on': 'no',\n", + " 'on_single_weights': 'no',\n", + " 'on_all_weights': 'no',\n", + " 'on_indicators': 'no',\n", + " 'given_weights': [0.6, 0.4]\n", + " }\n", + "\n", + " monte_carlo = {\n", + " 'monte_carlo_runs': 1000,\n", + " 'num_cores': 2,\n", + " 'random_seed': 42,\n", + " 'marginal_distribution_for_each_indicator': 'normal'\n", + " }\n", + "\n", + " output_path = 'mock_output/'\n", + "\n", + " # Return the setup parameters as a dictionary\n", + " return {\n", + " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", + " 'polarity': polarity,\n", + " 'sensitivity': sensitivity,\n", + " 'robustness': robustness,\n", + " 'monte_carlo': monte_carlo,\n", + " 'output_path': output_path\n", + " }\n", + "\n", + "# Run the setup and store parameters in a variable\n", + "setup_parameters = setUp()\n", + "\n", + "# Check the setup parameters\n", + "setup_parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-11-06 18:43:58,031 - ProMCDA - ProMCDA will only use one pair of norm/agg functions: minmax/weighted_sum\n", + "INFO: 2024-11-06 18:43:58,032 - ProMCDA - ProMCDA will run without uncertainty on the indicators or weights\n", + "INFO: 2024-11-06 18:43:58,034 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-06 18:43:58,036 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-11-06 18:43:58,036 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-11-06 18:43:58,037 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-11-06 18:43:58,037 - ProMCDA - Weights: [0.6, 0.4]\n", + "INFO: 2024-11-06 18:43:58,038 - ProMCDA - Normalized weights: [0.6, 0.4]\n", + "INFO: 2024-11-06 18:43:58,040 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-06 18:43:58,042 - ProMCDA - Start ProMCDA without robustness of the indicators\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Length mismatch: Expected axis has 1 elements, new values have 2 elements", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_2923/3015237431.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m promcda = ProMCDA(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0minput_matrix\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'input_matrix'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mpolarity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'polarity'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0msensitivity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'sensitivity'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mrobustness\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'robustness'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, input_matrix, polarity, sensitivity, robustness, monte_carlo, output_path)\u001b[0m\n\u001b[1;32m 47\u001b[0m self.robustness, self.monte_carlo, self.output_path)\n\u001b[1;32m 48\u001b[0m \u001b[0mis_robustness_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfiguration_settings\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidate_inputs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 49\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_mcda\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mis_robustness_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 50\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 51\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalized_matrix\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mrun_mcda\u001b[0;34m(self, is_robustness_indicators, is_robustness_weights, weights)\u001b[0m\n\u001b[1;32m 136\u001b[0m \u001b[0;31m# no uncertainty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_robustness_indicators\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 138\u001b[0;31m \u001b[0mrun_mcda_without_indicator_uncertainty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconfiguration_settings\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 139\u001b[0m \u001b[0;31m# uncertainty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_main.py\u001b[0m in \u001b[0;36mrun_mcda_without_indicator_uncertainty\u001b[0;34m(extracted_values, is_robustness_weights, weights)\u001b[0m\n\u001b[1;32m 680\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmcda_no_uncert\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate_indicators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 681\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_sensitivity\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 682\u001b[0;31m \u001b[0;32melse\u001b[0m \u001b[0mmcda_no_uncert\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate_indicators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf_agg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 683\u001b[0m \u001b[0mnormalized_scores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrescale_minmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 684\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mextracted_values\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"on_all_weights\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mextracted_values\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"robustness_on\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/mcda_without_robustness.py\u001b[0m in \u001b[0;36maggregate_indicators\u001b[0;34m(self, normalized_indicators, weights, method)\u001b[0m\n\u001b[1;32m 176\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcol_names\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 177\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 178\u001b[0;31m \u001b[0mscores\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcol_names_method\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 179\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 180\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m__setattr__\u001b[0;34m(self, name, value)\u001b[0m\n\u001b[1;32m 6311\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6312\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__getattribute__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 6313\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__setattr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6314\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mAttributeError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6315\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32mproperties.pyx\u001b[0m in \u001b[0;36mpandas._libs.properties.AxisProperty.__set__\u001b[0;34m()\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m_set_axis\u001b[0;34m(self, axis, labels)\u001b[0m\n\u001b[1;32m 812\u001b[0m \"\"\"\n\u001b[1;32m 813\u001b[0m \u001b[0mlabels\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mensure_index\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlabels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 814\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_mgr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlabels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 815\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_clear_item_cache\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 816\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/internals/managers.py\u001b[0m in \u001b[0;36mset_axis\u001b[0;34m(self, axis, new_labels)\u001b[0m\n\u001b[1;32m 236\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mset_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAxisInt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mIndex\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 237\u001b[0m \u001b[0;31m# Caller is responsible for ensuring we have an Index object.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 238\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_validate_set_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 239\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maxes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 240\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/internals/base.py\u001b[0m in \u001b[0;36m_validate_set_axis\u001b[0;34m(self, axis, new_labels)\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 97\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mnew_len\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mold_len\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 98\u001b[0;31m raise ValueError(\n\u001b[0m\u001b[1;32m 99\u001b[0m \u001b[0;34mf\"Length mismatch: Expected axis has {old_len} elements, new \"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 100\u001b[0m \u001b[0;34mf\"values have {new_len} elements\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: Length mismatch: Expected axis has 1 elements, new values have 2 elements" + ] + } + ], + "source": [ + "promcda = ProMCDA(\n", + " input_matrix=setup_parameters['input_matrix'],\n", + " polarity=setup_parameters['polarity'],\n", + " sensitivity=setup_parameters['sensitivity'],\n", + " robustness=setup_parameters['robustness'],\n", + " monte_carlo=setup_parameters['monte_carlo'],\n", + " output_path=setup_parameters['output_path']\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f910aa08-e230-4c6e-b7de-8a17233c206a", + "metadata": {}, + "outputs": [], + "source": [ + "promcda = ProMCDA(\n", + " input_matrix=setup_parameters['input_matrix'],\n", + " polarity=None,\n", + " sensitivity=None,\n", + " robustness=setup_parameters['robustness'],\n", + " monte_carlo=None,\n", + " output_path=None\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2de08fa4-9dc6-4469-aef1-5f9fc4090176", + "metadata": {}, + "outputs": [], + "source": [ + "input_matrix=setup_parameters['input_matrix']\n", + "input_matrix" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "test = promcda.normalize()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a75e64a1-0a60-4d44-9c60-ebe8708e66b6", + "metadata": {}, + "outputs": [], + "source": [ + "test\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ProMCDA (Python)", + "language": "python", + "name": "promcda" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.20" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 zcmeFaXH=9;w>AhZNrIvXh)7a`WB~z@)QV&U0inq#QL<#ojVOrZ3<3=bNX{8-zyy+; zETKUq=S-6trdoW?;hj0(nRm_1cYe(B53*SIeOJ}4UDv*LDuW)V$WxGBBE`eQqfk`1 ztB!|vY8DRS5?yMSB5pib%ebQ? zLGi*|F-oqLQub8PdA2BbgnH!L7?%4PF)XsOnXH+2r`@Ly1}aj=Ys4{hE8d0PD{~$z z6^BP#PYc(!(7Fv>LmGD+9UV)bGQ;s8Bqx8svmmDloK>-jm+=WGp?D{LD8u2MLI0P> z&k~T#N?n5}|I4G`P4KYT--rJZ;a_Q-)TRG%Y22YksV3j#wDuWe=1@h1pxD{jTVKv- z#=Lm(!g(|>{BmsS0QTG#rp9R|Dp!-sRQF9ZNA%Q+Z)ICRXwuXmMUv^bpTBzcBOkGi z@=`~i*s=`$#Pe$2m_>2s2JD(!?OI>0BO9Ay{Ye#<`R zJ8_(tpF!S|&emZT<8O&ps={76oT^nSQ07y>V0y>&u5~rs5}RKko9yD?o(^v9!U>=tc+T)W*=gZ*_6iMBmh z-@637J%=wzu4#JqBx0ozgpkZyrLU-X-R6Q_W6JR`hgAxZ&YqH2jbGCk?^+GXah}d8 zbze{sdp7%Ya^@Dk0fnQ=*L6Y5)n4`Tryt2@Bpf#r^c_qBv&A$-^#v`Sz4!j?I=s)B zmM6~`#vskeBDEExu3oOtBuuWy@crybw45coNQJVubk5xL`Bnc)yKURswEvCE96>M9 zX6oP(XVvKVRf9TgGXFk;@`e(Fx%de@t0*miuwa*W?h)q~w>hBh%pDENI zt%=gz`kCU~^~va&4_o5Sd^FwI@?py_`^j!b8KD9n75f6MuE{)X;gNT=&3KKx>(UVa z{x(z0rQj9?zrfnIy&t^0>s{TRy+(OOao{N>~ zMOPuRS$a|IRo{axRa3-s!gb|PtJvN|-u>z3><8Y4Q$m>!gWg5D&d2k&)$iV}v>D;l zt6GVJ?5-;%_I#ZxFU_3%Mb;T+wT}*8Fk7iV68L{1vNa8@RcAjbeU4VbBO!ZQGPATl#DlBUs**KF}ppt6l{0IP^M_vM4lU z9o%m5dxzjw_1;`rE52d%RHxRxPiV<%G)AUCY>V29dO0gh;Yj)AqDu7;lQ*X-h9SvJ zNO*9U56!+@`|5^wAjCVAZoL zbyGTBPMFzIF0Mv%8kDp_snW1pu+O${%i>Qp79%9;KJq*7*4oFI_^C~ewr_=!`;(cE zHB6VAmCN+wbDglajy%WV9vsm5OYgmz;2w94QQs5i81ob){c1gp2VT&YR;1yd)XEpC zjoI+A!ma!33JXb-L1XErW6#a#w1jM-MZTUh+3}xpxIfQlI+ZY%5I76R)v9~DPt000 z7kxOmLbt0vlbz$bILr+j8x@#e>GNQm?U9~e8S_m~&7K)rx?DoA8MD?Wgq_7?IXd;4T@%7bH9%XphPGlSbc z(G{7Ggb|%Q^ORD4C}L;o+CJNv`3FDu3nC>mEEks5-ttV%aN4Sm{i;7|WT{yU2u#mC zoIO?5D#M%f#f$yRC6o~9GrE_;TgErHo>#}nAcYS@AEOJPiF?rwoDCtb+}~b_I*+n{ z^0Y?Z`Qwmdlcm{RB%Kjqzv%cb>%7&od3hp1ve~J&*f?WZQk~R#Wlx?2fd{Q|hvOV` z>X6#KsX&h;q(|Y=PEhpveri$y?M9B?xJFnxyN;wkV%s8zN%FyjFfvQ;Aqmp}Mr~nf ze?E6N+FMSZSI3|R2E$?&B_D0MdyZKQ@NSk6ZI(C8s?AKLh++plynci?RDKg3Ke|#j z%})cVO%aN#JRyx2%FLVi-VYm^^GBLB!uqQG^YGk#gXt%0VC=G33AVI6i>#!IIcm){ zKjd(W%a&D>7tQz6vvkNr?~W|0^!lF&kym*?vh74uB_?F%M7^vYMqo{cp^EqJ9bqx; zG)0A%If50&9(E<9>?rTK0h zUaOhurtL+(xF1`h*Bm!c_CwMT5%Cxb`?>nwGc_pp**y3Al4Md>h({MbmD%)Y*SmoG z3@9O8)mEo1u2C!|Y*w}9{-w>Bz7IR2+HrK2>2?(`{FG* z^i-bDYnz`xwM3Qn_wC}!vA0Iug3E3D`>f)-`7iX1=NT>QJKttoV1tVq!kS6DcN z9AM$hJlMTJ=a_i`g00e;wj-x{0B2(SugY91doKQ;1(28blgalm)Y(%2+t&5))xqRO zkr2h~ypy?bq4)LNUzVwUoy*R}z_Qh^BxaA)Xg<^EipsiN6qd*`@xDFyK!>m?OT7Hm z!S2_$8iGg)$vT(Z)V#b_hE%$!Ddl$crV@Ho@0beWR$AW3Z0fxDt@UenT&*jpP`y(7 z1%(0p$zlqHb!EzZb%cFR{N^L_jPtPa<|5Ol_#xywD2Y4X7)9a zp3YWm^IE=V{-M%1m*l!?<108KA>o>f{rp90L%q>aXsxtI)rXD4nM5LIms>Zic1`!QUXIo$Xb zhK?xcMfF$=(91_#ZqoATpJ`3X?utgP_y(alfcQD@WP4%Jy1Q%f(vyKkA{qNHQZI2V z9oo{SoH+q%%#;fFCXiZ>O5K;dE|aaXtTU+_4L_35)SLJ9D%&bL>N2>re_Ebjw9YlB8mg^~xVcJibhM-c{Na_q?;YS3a}1zdneaFY<|u)P#=LJB4qR)~cKx;}R9kw>Dr?6%+LE@tq}O z$ULMLhU_w3pb0(cn~Tw2XHT<`i!p%0usuG*JmSlX5D!;uSDx4VdyMPOtl!KwLpR$E zwcIT=oG0MHGwh+(H#HV|1 zt1V{#jc8Q!h)A1AnvqLedZbw2(!ND3KlkaN)1~H1L^^9r!`Zvio)*QzOvEP%N&*`c z+BHoF&S|!2NOy3g&|mi<9$WFS#T;6ox2$3*y67(QoI!y#j>AdT zRRPwFB)WGGBeJ#UlB@iAFCNE%u9{>pYDmJoTei;1hNkZ6P0bTq7Cp*a=^aEa-pLF1 zkn+T_K?D`dDVugT6IpsR<@q^7yZerokxQNXJZ-%s>KD=60cvS(+vCTN$2H|@BiRl1 zUiW$D29ft`E8DaVuTj0PXEH<7M&|0Z=@rFJi}r`_u?6i7)Oi?6PALqfd#Is{gUTk6 zXX_O(2)~$0n4wSPXzya}x2lOUUK)y%%$(U7Cp$YWdGAa2B(uDBz6o$XU@nT`*SmH8 z5?WtgVtUWB&2^~SGHk&#A{GWDJ-KAh^+X)(Y}P>SbyGf|blGUN%ZTp8sZab1p_ zjBZhX|4@L_Aop3H=mf*ajZ-Wq!cfpO5U%p$q7ClIfriI(X!8@?6YCRgo!2uWsHczH(doQ%#9P?E&q;Y(_78CU$v67V1_)XZst<;6B+ z|H%VzzcFh=2sR(}2lrn&As?`$7Z2fZ#-2~5yeADV6wmL`X-bs8Ccjqjzbu=XQXL=S zp+MGFc~TR=#QskHujGMB{8by~|A_-zsPnzS?Q+j$zvYvM|HsS59JUQl0bZvo5jlAH z1{;&iY1WV)h7&*#4)BoiqYy8?q~Q#J55v-s6XzT11(0TK%glKnqJO)}o~GX)UU*u; z?F2ALoM%q+BvNNMk#4cAA04Sc5=sk~`a5+PXnqX_IJ~6*9Fn}~h zMsijBjnvNj=4z58vv1(e^8blyjaTs@weqX?lKvk~e)P;Nhbnv9d}?@0$;4>^TOLjU zoz9#DWzuQYkQ~X-=eTh@E}cgSn=I`XdMAR>SGpGil;t1o&vh`%Nco zOQWRUb>dCHl_U8Z46lMf%{K_cb*ncqT!s&I$H99u11W}4@=Vp5xv&Br4RhWI(Xm5? z%BAmDE#jnQhsQn3`trOT0$x6t@TpuB=hV-Jw_~5>#Bgie@o=%^Rc<)EJ?3Hm)issVTG_Pla9rgWUBNLc)IxA!Tn(OoO=4aSR~fMH7+M_CR_rWo4@xYA5fJMtAXsP zXf5rAg*fYcX?t|NUZoBCF`)~j*7L~FAbS*H9)DTpb!D* zXA4lNC=oZ&$e4PJi2XH~zV3YxY3GwZIH+sCyq_#Q_*oUnW#o$~lNgwG&3Wn}HYfJz zUBv+Lij~dFtrj7vavy;-pSfa-ar{JK-;%y;-67H*KoR*HY)5TET|oG_rm9ytiW?Rc z;+dI8PKT*L>r(PN*e#Vfo7JVm8vVW_9y2~mYdQFK|B=|jrsMjv5%b7~eTxyk z9A8fhNbUHcCKU>+b#B1_KMx($2J-mKtb90Q^!%XIwW702e8guMLqmy(>J_)z3gYDr44cBa)MOI8+e# zFmX~44=*&`2}t&PcKU@_8n+O2vNd)FGPHxyS{PU{^4V8bqUOJ{fA(@zMh5G`>*8AM z{#Ots9`+>THFsgtH~6U4rH5fy59rrom><|`9=@SWi_$3U%OaS#Dj%)`TNtMsA9*8` z2QRdvZrH0-DSgxgUC`Pr$M+I3ve)tp^=3lC2m6fI*!k~trB9pk)Ph;kJdNnJ zPH;-73c)8v6i2CwXbXRDqI3JQiK5H_b%sqa%Y8`-&9AC{@@2!XVVP*;CP zJ*g+rBVHQj9~h>vQP~x=cVW05OI!egO#F1E;_8UoN81w)k*c2!+$><0F8dXc7O4J< z`nS7O1XZMARlXA6&EQ*${vPK3a7KKG`IE|`B@1|m zk^C-|h(so-Idrz(CT^1n*Yep)6c(YNQe&F2opGo zNBYc*_y%T)x}hVA&HMI|b@p`w>snP_N2Q0Vi6XUGTQg?k2iF)7bm!d%KB#o$_AIOo zB>Iea_H3RtZFwy$_}-T`&v%%6mb0rqYxGy4VX@ZIhu7;5+SsEiS1K9mGRF#1@Zq~GB5;>t0oWzTdavRRb-QY1k5N6<6Aev13Emd^DuKo!I z6St(E|Kw-FV1b>Rw7a0#Ld?$78vmd+{($aE;qyMNGxPWw|*+=WSs{UX1uO&;vK4iVRWJLJ6 zUz#b_q?yX(jp%w|H&bX=32v2C?xV{Jg=(tY0JV}F#K7%GbG z_W2i-)aA8=kCE1csWIVFLB2aA&k3#XGGFy`>bf6C`-$fV(w0 zJ`MH^d2mDKSb6DNreEF}Qjt4F0J-)cZ$Po;@D0^Kk>Z4VwRf#{p6MRkX?xnpz4qZijXmW%#&;%{e7($hFCS5UCp>mR?EuX~_n8+mgManix6&?Q#XEQIRI*7vn=QU9?t14n4d=!FI5>ir;h!`O zb0|nXXh8h(L0m7H z$1TgD`tT1@F+aFU|%)Ja<2QD?`mXpX`I#em9#!I26c4|IyWaYA-9ea{y0X=@*w+hxum;= z8H5;be>KlDC*I`i3I`P=WQrL1l`yFOgQAL%XC-C&nhWoovzkAF!jnLY00sYA;mPR$ zm&~JARg?A8p_7$Ouyd&0<`@5q$qI|7N%y(HALcj*NQ_nL5WF-0T-lSSY+dvA`_fQ~INmhu<$XGODz0|W* zR&bNl3)6d=PTbFlJM0TN!&ARo712}M^i-@cV`F`{D1KFS{bq}&1dF5>-Gb|CpBEaE z4;GlTz0%Zzp;n%3B;=*`9t{d=4mS#>@fc zK~%J?@eWEJ>iSs_%eva;5xExqi(cZO+-TrA+`w^vQ)fV7Vhz}9IOgYqYzJ$> z2T4h~xw+6$P35s}g3Wq0bVJX>Tn^GDE{)i7dwNkkTh$<^P14@wn~6gyr3bDFqCi^4~o(q_fX ztVX7nIt?0f>b*A&7?)PnlXI!tUz^~_coO+sP5Me2@xlv>Kk2)F@`*&!WRxhZKEnWw zD!$37T8-@A2}$DP5WW+L-cR?vNtwO}ha-eUJqV59@IoD#69MPz58!aSN8LlEykNn+ ze3t@2M7~hx-rpzw%Hwy5`Ct9`dm8@#iVlsm_}+sE@AbL(Jkwqf`UBzFnzU9OLHmz_ zoH`#~=^IXX897m*ybTy>0)N|YI6vS;^*m(3>ydc)X!);?I15RiF``64{PO+& z*auFZo-I-8KJzEttB#ux8Oc+^6em2t5CVy_v$)is)x!cF113vGY81??PD|I#Ls1G; zY{hV?^;Pv>P1~=$!9#xh{y2!zdFx%GnWn-oOxw?(F^~vtTu1|4vIF~0AUVoSzv~M zQ7sdFCwr3+h@oqfo*(opL;C{{8n1>e<{n12{)bSKeo(~`%A70Wi{T7qoDxj&KVH&QJ`9OtqROQIR;4}CJ-lvcZ*ldgqAw1>_KWf0k8|RDhQo+2u4VmL;MRBry$2P7sIF>%yGe4uW zq@4SHWOgaEC`!WJv6%wu_6Sm2$c#NswqI8TR^G5Ve54j^p%uHHa*_DZg=l&IKxrtfsW93UQNkJ7tLBNb@wnpFYmPgT^C* z%^&?C)OFn8@xi2_gR4LC;PobhhGLGB*)g03`k?D+SDrICmJ)SJjh!aw_z)EVJ*(#; z4qkf;78NM_Sh0{~=So`Fn+9!AI>#sOwi0YptJ9m8+3R1md-&CXz!}!5!G63y%n!kc z4`QP`zJO{fM&gG7N$AT%uq7U#0`VKNgTB?fgI>1((Z)c^`nY*`OU7+g9oQh&128pRx`AWQ1?~&L ztU&U$Z8WqRnkGEIVnSBix zMrK-CS_bq+FiX)X;kQJx^Y%|E7xYEM9)0TyHk_0F8Um5>2){bA1nh%cIp_(@Qn;_ zHxxNZi*0O{_H2}Rz8k=RE~Ct^SCM8PL;Tb$ollQzg~OLedWx?f@2VG2W{j<+c7iHE zpylX0#pHv85)IGCK#spBzooxtFKi)Qqyf~#)_^prptmzW&Sn&T{&I^2Us8*6_)j$< z^flNNr+?X$5f^$^U;81xGWHLYn*K3*=Pe~s)NBWJ+z`LM?s8D0vqjUcci z94{Zy72@>bxGjjD2c*c@w-)+##w`Z4@*{^p)rB%&?v<-X^NY7$rvN~AEr0S)g^m*F zqjZPk!BAqvl;0Gt32OB2RrHH&m+xEG@E@qc3UKv-K(_Yf0!-DKB`)oQl`ZJ1~vqfM|(|SM=bpB0l zy+vmX*HD{%+i?R8SDEWA<$h5K35QC1(j%^l*b#ADMXzFT68$&#*siG0@?m%FKF*NV znq)p^J{D~V02_J@E0H%cz;`_JbC%{A?r^!dxyAID&}RG zEl<*BexjSY?5_oz4W{K~MKdAA1*btz0M1(D8b*~wReL>a6%W1KGeJXy*@m=uz7b71 zGAk#B*Wi z7B)~LV1cweL$amgc zx?tU6U%ycTdKUEyk@M%}gR=|M@@PN1EPNLfv;(1*{;q)IM#SRbJlZB*?5^No z6UbK)*!rgyps9#=DrO<(*RJDmtPjZ-SwM9q`NZ2yA8e8DbuEatCL9{%(+q*Y%ZR^Z z0rvu^1bR|obhOWIIb2)!K{z)H1mqx9)pa@&)5i+I%nBL(#Ta#`G_?9Oh`o$Hq5D4b zZ-iV-KIyHreW{w(^V9dv!uEFynSP`l|0ZvbH1$h#9hD^VN-4ECflg<%qHF!%sm;YwBiE2*K53D_~@$4$B&Px*^U+{nZ#MERInUx zb&|NBwKy>PENj$af|S#yhmCV|rHI2WeDb8)SNM5{P(Lup8a11@{=1xB8l zBO_2%8ewMAef-KZz%?;3sV%3N0q2B9Y8&elNSvMxj`QM9s`uJ#Ip4nlk$Y$Vgh06wsJO&VHtrdHs@|7I?gRDP^bX>j7(j=1!4(ePz5MBsYzhAXWuz1$cMIcqkWhNoUCyZarz(L3p&t20c2zLN=hF0!iei) z+}W({c+g&Zmnbwv5zOYs<3F>Ze5wwllfiECQVl3tQ*LRNHcm?8*lI&vi5@+m#2i~i z*?b^h5lKB>h`YXl1?wrn6M9LX-2lPF3EC&Xr*4out#^pA%mY!-r!v@XY0)MK$MV|UAeO{$M7Hx3f>v zxc&Enr^QE?p~bH|jGppvEVUUr2BQh&ok;mqMp!-}2v}zsr0V_)3)2 z(FI>*frx0eh$!FyD3B)METxauTJPn=wbSv*lF@GRX42z(9|kIxI^%GL1D|X||Hqgo z$t;4_kB}J-{}TtL2ly;nJZm$A(`A0cL!`~2g?z0edCF>-{(~tVx}G|dz7aH5OB9rE za((i+2aZ6Jw##-1v=|(hkA+Nm;SGR09X7T9P{p}P<~*QPIj&x2EQWuaUI_|_j`Y!1 zG1oW_(=B_6JSg^e9ogJ^4#xTN&*adzCw-XL!bGUUUDZ z%0eJ&ZTFyLhzr63SW;j{gw0huu=?t5!Qw7LFFKSe;$B=8xD3OasDW=|+)JnPW5b-8^6M#YRfXddfDaaT>UBHKIT9SR zG$k|>4+Hwgz+u)1Cjn>T)UTL1!PB$1iQfO z!6x0$xv_tjSpvv-wJ$2=i#*UUCYan-1JE!7BK6oh;xO1e*~TqHMq12cHwwCEBjRDeU1=O6Hocb&HAZ z8(FmitV{anUSpY{t+Q&5ZszlUcN0a&F(LZSheIH@ws0*MSBdK>-f$O68@bEG-qJI5 zT1q-rn-MYwVl&7x3v&X>Z-1D+AXu55O2Lq2Ihcy~kJ&jvpZR?)Xv5~O4O|!9dIzTA z+iXkO@4H|=CHk|v6|#;Gg#H^!34|SZhACFwlROe6_JZDVspS`wFQe709_B4t-#K>7 z$#INd{YhmI^c7Gt7A|(ZpkSZ{eEH;;@NkFdb$71tf+^bGx869)BZkb)~!xERY>Ea_Ajs95V)n25kf zFXnz@q`Z!^<66j}0&noLw~)sR8C;G`{y0bW6lbRKuE+qW%mdQM1JL9JK5wd%E$VP&QC9PQ|R^VJQ2PnCmk+mkETr~vi^rq1EYXHD;Sa>!sI2B$5 z+2e1(vVl}9yM5RGJ=f(iizSbt(R1q65BzpcKMA{cT%vgfNSFa?NImPlXqP58}ctRIcn45#X#qzZ&O+kv=<*f)yHa zxVg<<&5GgU{OU4rL;qXlYgm)-yU>=0xxxQCvNsXjK`B&Grxk;pT-`lar=8Wv>41Wn*K9 zHJ2rvwOix4c|or^_h5e>Cw1qw8PT%rtyS{hLBjt#EV4?+1#O}L5yeOys7vW@#%`sv zIRz+peg(sE}$ z#M%hbmdSg* z?ipVivoH`!k&VY-z;5BrGCXfII^~G}@f?qXeu%M*CsqWHu0vnhFtF1Uw5L z+sl@h;a&A{uDXwGf}O%gmn+3RcZCzs1O9VlH~xU&6$lHMY+!zxO|t{olZg!Si%M{8ni0lDOG!N`SLmQ~RoTi|nUS4)TA;rBkSwk= z#E2GWUqI4B2y}GWDZCwGhp+%wG%!2ChK&!XT6MAD6Fi*9L;N0-=xk~~lEfKBRTV(Ki1hmp$ zM^tMG0X9R@+I&48Ztx8IBgcX-{+IjD3fyJ5Lie#6?MV$*Ap(KjelhXNlx zIQ7^e03a*{=Z%s7lJs-nV{su@Io{Nmc+c`!`b+u6en%DR_TdKkE4VG#B3r>*z`XtAuGIs9@7Leuxvqi(Gyt|rxQn#_ z?9UYe?nPTSzahD=c*!kWA zZ8t2mO_T_e(V`53qrpJeXprKJVVM}mMVebVdWp<;1#eYMgU*|(_rSE}-WMkr1?;MB zqMZjIe=-oen6eyc7Ep#{7cQ9P&bWTIU~ zAJ=cu-(~+WeIV|7uh=BqoCI7KHai@xyYl+AY_83kSILcZ3P|B;Afx_eTo?l9RpBD6 z%k@}Yv59hxyM!CKj*oLiNN%o+XBYOf^$qH3(=USiWSBL%Qdq*PY-#AKjtkuuocZ%d z$}sR7*1gKhmafS1>Kea_%5r?{mmpL5!Fbo=363bUd2x=(;7o^n30KAaKs&C|vVPEz@Wc$scN*>8&kl4#2&2RA0qOn%5LlmL+_Wg0 z(sIVmq3BJ{&Ix*r>mu_B;E-V_TTBzh+#)xb#O?!M=W+$h)~`x%BOfogRIslk^gVe7 z2-&XHx!BOriI;KD;-c$}AXuV{_DONUnu3x?!I^Y6$L7$jsqH41PSux4QYfY6nGXRP@6ZdKf{@$u_)4Lk*)vyC@ZF*S+)v9*(k1``N_M70LpLCPI3Kyz zi)V}wESBusy`Rcpu^z9#;1Z!bIV)|rtUGd;w(S(J-K$ZnW#s~L8d>OBrA%#$1h9t3 zg?yeeAe;jn?txeXF)#_Y&Zx-eY(Ffas&93xe)1>YSUTm7@#oKwt8ki}vKUsV(NH3~ z69s2{C0O$9A5SO{N&>#c!o#KK2(H^$zQ*BCsDLY1!7}-_y~tV7!^g(-d^N-MHiCji z!sn6y#@oH{!wLR|HUPPkpJlo8S5l=%=jCb+=atz&8pMx+Qjf4ZyZ` z68*1itA?*R@6`T8qF;#5t^+RUZ1kb00HhyrA?^Rxp#xgs&?_zIFib4aEwT131=MdK z5{5mxU%{7O-g3G^iL0Xt0T)W70YLTF5*S#h1gdZAFuf5B02R%ji`(e&8>}x%xZ+#bSbXK7ZmTw? z*9aW{)CK|GB?Ojdv3c(@CE#69(yxLHw4j@&NViNyw{qnwxGRRezq8<)OjEnH&sqef|qq3T}{-eP+rVN5gd(W@O$vd_wg+$>~d1U(xnKUf&72HvfuR zml!q7{Os+uupegcUQwA{pp3c3_39Pm-A&36ctq|sbzS2c*g5J7xNdntRc^Y8%i;yu ztE4PdON%P^$<$rTx;7B#jb)3Y;14rmGZ-=Z%k_)X9lqFg(US4t@UXT{U)bXI4ns4w zPxBmYQ&}(VM7Zcm?t3cNXb76p~A&? zKbLfTB5pb@k2H?%q}nz(KU0rM_-;`oxeT*G?@^TOpeBAbTC7kv^LYnBnU1hi)C6xx zNH*EqMOdI|51?tZ#+QU+AQ10sMg21Op^usf_pHYc4!^t1Fo+OtXLlpI3@&jnRI#4% z+7?=lU;KhGx&Slsb<*Ej!-RTQkIoJ*x#g$eZA=;o3Z7p}%)3(W(TnwsKu@|AM?sGu zf&4=k>!dnYA`k6*s3;S}Amc|r6}+6M785rb5iyPCq>ISGre`1V&y_XrUZ3f>yf4o` z?)q_bk!0a?3~75clnHxgZaPjFl7)z|FGL=~_y{VvA z;-4J*EMjvxNSU80sA%*C%9fl1Rnd5^h%rdgHK}(?=;p!Zg^SpuA7YrbUDn=>a+Ut* z#;W={md*y0ulC|xXL3bBW9o|J`M?iPwQK}Di;q~W$Mla5QQPKAB8bKivdwO3|3fSj z_C}}pM72QD+9CN~Q|MKP>DS;oV3-%n;j~PfaW|hh`g31abvTA*qw6%eVPkmp*|G0u z2uS)!cg|sSReJ+eaeo?5@pu<>8m#STT-8CE1yMa>G9g2&PXnad`(D+-$bGKD0ULTG zvCX-XZe%aLl|!Rr1T^=z3H6)`7dj7+rIdhO6(v_oA+w zn5dGKINzl=)FusvUb9IP77f));R;2EUo1-|(wX>Bq7xm{nFK^^%qU_uYd(C4|9Q$; z|0rQMZ32S5;?azm@A`gpw6nf=x=xtkYV2S`sJz4?3uYISs(PU(yjt~P>jDY63Ybsl zfGLE4=qmWkgw^~iP{5xx%BrRA5FrtiX!zWbZw0g{>PBGWBq71x^pO=9ps#wQ zf(hSmtjj>mvH%WM^&s73O`9Sh1t$Uj-8ll18`PAu_iJni-ol|Pt38)%?n2Wz=m7ehLmLU{iw(SP<6LwmSOr=m{HLo5XsX|#QY%meunTT1o)8qgajTM zc1bSRm{H;%ZpibUqnuTg^Ft7nCqw<Y#ne)=051R~jT4HXTKqq;^(IZOCTj*=Pm5Sk`BRjCsQhWxDTGano%E5P9>4c^NS@c?Fe zW-!IAAZS8$SHbI$yDAL1t{ChZR;KE1VpFyBslwZMTzK$ENz1-Ou;`nml?lj@jCUr4<=<)tDjGiGcieoLH z>_sCp7@7HD2jGzrfF4KGnxFNDLzx2fHp!#G+$OS-JFlor*6KZ*&hdMWedjLP|HQlY zKDsm1x9;b=1YzsAl{D390}Bd2V?YtftuBKl(Sjv$RNe-q_s9)0=L1DDl1+!%ZLj5? zn^QZVo3D6pE{Go;M&F#Q;P?tF(q<&1m4K$H?iEVU(r%5*V*XZf!g3uUG&E)+t6XR6m_C?RBaG&*!XCI@eemSK|303;;$T;p%X(N3; zE9g6*Yj-*^9IM9sEZ1cS&uCv>i;;Dz^W6(QS2Dd!ftC&=X1=cA=dRZgG~_>;*1f3L zP$p7XU)XJ4$%1;@_zo@i%e`wzurgVQykSE81D-rW5VtYdSvS;p*`n|oY+u55F&1-p zW-kVv9D16P84AY(MXn^^mq*z^qBjS{_AF4C@s+u?H=oa3_MAcrw`hbHQ86r7+!A)9Z}OzTq4iubYnYij)cw|K6D62z18LN3i;sjmRar zCo9M!ik`_!eHmT_RB1SvyLl~QxhRV$;OkW7iXi(4Aw_cj?Jup4OwYs2Dq zPRMzT(seI#^v>X=d3L+MXxEJ1c!u~h1|McE(RFDaO)1k)PA}FRqIH`U z9QXR$2&BbCO_&{WS9c<~C}XWBC`}D85BKk8_ItxFjnFTN8+A`^F_%9In>#>E~%{xO-8sd7ovi@Z(Q zW^408-Jse~nL$Are#(h|d{n2@5`~bG>rZ1&Yq8vGLN0@5+5U2ijooAj&@)=tHvd0h zLZurt<|W|T2GhF+!hvuPFrLhExjiJk(r5^kjU)LxngVq_5t+w~2-nZvpJPL*<`%Ts zo1p(8nPW06fZczi*oAonl{ObEI~uzr{(hK)A;x`ZT~v}DBR<_kAf!B-vP?Qb&6)J+ z)Vc8aaRtw15EqRvl-Gc>MP5K66*u#Aw+oO?rr9Mc&say}jA@MVJdck+e{6X;ut*DN zr^HyEHLQZj7c#X)vR?KSD5XbW@bW@w3tOD2O_`{Oc6dszxE`8p>1O;|5%6fQ9Fhq> z5uB`lfro$KIoFvpVn=OWpwDwe9*qi&&5lNpRtM-VXz5M0&(}_MeMOgen7&7JpJvwb1sg6LisJQ64 ztSYRtiP$mGa^ZQ9xgL{#h8q%h*cM07cF~ptie$U;(j=s-J!rcA0L^RJT{XI8&8B^D zFySr9*pGTy5hcl9;5YI0=S-a9K$g^E!!cj%to0q*Aw+MhAFi`a(CSyREBy$+F%{ue zXNybx>|H?~)Dm{oO5&nMbx&RNZchaBJs3P4czVkSFO_s(3VKx?brprsu%3=qX+}u= zXVn9|P1&uWsb<#}+CE)ArW-hRksVItXKpW>9y?XF{P#JDH$zM&SMZJEQ5tMt8T zTwfGxZh#uLv>Vjw1mNiXZ|jr$Va2hj2|V_EA#Geff~=+*jp0p_V^^^$!7$Jq3RQLN z@?l)`FLYFfl(3)8XVdgoFnA8^0o!O~^lv^XyWwj&htl;c@)Kgd>cA7qsk@1#?*PAU zb@Nwg5e?&gnn#snOJ8iYx2ZywyGq3@={;+fwm=}S8w~8g){#zihrr+ufzt0g3qDh? zL!#Y(a{m_j#92he)>EXn!1iXmv7W!K0C&}w`_{0v7WvHtWBpPcVEiC1U0i<&%pG12 wDP9BppEWm9-KgruUfpP`{~fIUgsd*!iW+bsp1TSDnZD`ML&pzRgv4F`8+X5&=?N&PC?O?MQ)wwBrBOsWBqb)O zbSoVK@BJ)vE!Nt5pZ_@@&UKyZdiMuqt;u}GGsfNVyN3@o)fEntoFF-H;J{%e#S2;o z4&Y55IDmVZ=pcOZgQ1!IzyYQMN*82w?wZbG-O4q_hIW6n6}5EA2Sji^x*_h8CKG;+ zoh5_l;`30gqr}A2?AOQZLmMNWGHZpMQ@rp0^4t*xVs>H6{;ImdR)>XESMpWYxx$>n zp2D7)tvavaSr?Pu33u-(=fpMB8c}93F)@R6CiDSZqWyoNb8(6BZ<}o!oW#MWKpoik zmkJu)`tYA$M;*W?n#8$*%f5dNWJwfi;^f~Rht-%4;NmYIIOBWd?+fEoFkzg@{<#3f z0UTWS1J6;!f7>1`P0=n!_|FB1&DgSmcoUTkfW%$aQ^ulT=9v2knR934xEG#Lmcr>DjmRycivA*WNF_6a1y+BeC2;q z3CJw=&jtQV=KZw(KP2-#9!#R7n{ws$)yz0?7oJEF{TyQ_>+VNSLqkJ-ws?J{pMecFPR^#%C(<8hb)Qrv-@G{B+-D$qqb~UQ z8I^#L#2CY#>-=<$RODwDQ<}wF#G8WSJ+ykgOPXpr(&rCXcD;YOUtq5!pwe+SE4K;9 zN&_^^C*Q~7i8mGMn?`(HNNX`<6C_JGy0k_>IvSlj5z<-^ZkBbKw}MbD!o*{zKCd&{ z%+ptLusmVVc*JSu*~O+F#jW{VD-TO!FE?H5K}_nPCwAS zl^vJtOIP!3>jKBnFGhxY+4Z-pZB;FSdT4NV)KaCCwzWZN{N#G=}u_N$s?8UEy^7vbWQRYGm-2Pu&ia z+HWe*M;=^2>CN=0^Ih8x}L%e|@C)Kt~ z!qGV#8OCqoR6Q#)*{HcArk?xIO9=Fe+A`)__Txpj9S2ZumjPnR4xZ-pict zwEV`W#xJ{w)`SfBgsDl-hnje-HZpFJHC9TKEAM!^CEtDXvd4M5FsF>A^L4B)YItW! zps(n<{=;neNAIFnG3w^i(=b;{>F zx5=xwabA{68+8{a>Q0v3siE0es(#V+hE`WhuqK{@Q#~OfGE>xWfqh+f``W~$nb!2< zYg(!1jwN5p^Rw5EefDDXUD`c%<=}q1PUMF&$1Qzt`g4bZ_fYn(cjg(p0SWEhWt{B> z204qb9o<58WL{HB8D2Hi$3JG$7FUQ`y#bo52dua?Zn|0OPwS!tLw6S@n!AHW`|liQ zn=V-_NZgTP2zM{cZG9c=o@&w(yLjnrT7_Tk`|XIZ<31UoSYpwwaEp$@h0mP~sXgwg zc2w5=ul1%Krzz3XE(0GP&-}cseO%~A%_04b?(8iGGEco%I^vuw>$^L7M)o#4ws*!1 z+ML%WKMQkITT-`6n^npuOww%4?k@JRUX!hKe-Z2c#;CuoF5E2Kxs`|CXI7dimn@Ob zS=z+0zf>pD#$>H(?Qps)Zt~pK%{82K1-5|4@nK)zJSrefFXOZf_7O88CEM>te7_T4 z;K4`_O{-h?POH1ErhHob^j`4voaH9kG}E!}S@io1-kRwY=O5nRh$n9fxTI`8-+a>+ zJbbssLU_76SV(1ir!LXydJ^aL9rf-d#k3tEAJK()@0STy#Y+!4F=@?d2~+xQtC<>> zINP^pUYRkeGW_tIj$Ii!JbANKVIfnHo}q%%Wit!6wDg+UOahsZpxw-dDz9}1#&Y`d z%4a`!Pv)`k#T>uub-_(iA*&JmG38#;+m)tsJ%%R+4Qn=MItiBDNcEK`qU9|Hf`wBa zHN9LrXytK|aKGlG*70Q-+jS?kisLgwNQSi)Rzf8VT|$U0xTQ73IL7$r3CZchG97&Q zQfD-zoQex7t7HbPWXo}+^3l--m$d~0v;`liY@`=|tU6s@M$OgfZDHIf$ofq{=YkgQ z=3HTJSd)ObS!1G!VOQ)AT}`hw|F&y60cl?5OqpoY99wQ~y@J;(-L6y8q{DPA-5g)k(p&! ztYq10#)E%b!iuks<*zt_Lcq73aQV}ALXkZqysm=Yn4mr|c37Z-Q2p_Sn=(qjOiCJ1|{Q}3S`@EN4f`?U|;4|YMh@_Xeo1>A+t`HpBdHjKEt!}xs%}b%1L+ZR_TXk zG1m@WC)16*p8QqoVSbE##qQ5-=AOBwB+iziA=1#rOVV^h#s0=lS8H`NRtA5*_sNc0 zYD|pTTzr?6gJG2Rmfdk|Vij^p&-M3sZEeDxu)H>~={mn@66LkcZ`MMyRwY1o;V;w8 zWQ_ZQeA29ij-Om|ML8h)b%w|tk45hpgYOKvPuk94GH>0T@NcWTEjdTsc~_l40;ggE zO)PwUo>r(c_3&g&cle@l1&Ql!x#()Rf6x*v(4BTe%^Cm*Rd%rowjq`EDl1%1yHx(r>o$*(V?W6-1%lBFfX< z{aqtE>#2RNx#va!Qk4*+dHMW;{YdEaj=-ltPyh_Z>Y}7a}y<8wDq!LqSkJ9jwaO+3s z*eChqEYM|yu$b#-#-kH<`W#%&dVYG$td(Yv_RN(ueWJ2^ZfrU!XL5IGDDAp;f`Qr^ z)+I=C@b+nfVy(Cs+ok(=`}! z^$X1>rZ*Pnb1P$}^navCX?kHz^CfJId|XP=IkLMSrj{(ajPmn;?zo=~6gCrb#nf9}2`J~z=2xDX}6FI>r`j-AFj zd@$eeURt%SB9xlz&C0M!S@8Hd_DnQ5b0xLvlZwDAr<}q9caaj837n#~>a5CF{M#3% zXWr>lt8Q@R6KHRy4JqeTj!4Yqf3YW6Zcb=p5dCg26SWZ0cJZuHZNyftL5)@-wsoGy z^{l2;Y;MA*w&|oHzr@v7g{96c4he%j`MXBN3k9;JrP$rrLg^2KUoqneV__9OfU0w%J~v!PUXVJ zUDC=v6PewO-r>=n)Og`D`W0OBz8u8{!!Z@RZT@-Jx(~WJFB;DjO%@1hHqDZhl3bY{ zvmqp-4|Ay6y_D+tma9g$Tf(8tbW?I|EvPnOMnZnvGf%RR%q4S`U?Q?fGumC3-d?da~N0!EiHul4nq44;&bnP%p3 z0+`(**Q%AgQnHXC{wLEzS4`M7Hq2Ll?!1mpj4VhZ_^G{7OO3&R?TNG zT~|x#t|+d}dVyQIX(+aGsW3frgI$Hqpvu$W_4>>+vsB5hf&|IVBBdgn?bNaaWfM7U zrH_5dX?A9PKhLdgy^YP`;kvBdTEbkTnIHQjY2O>f6z%zL2TkJ1rVo#dqoxLQ3*bnO~esouUq z-nVEQ!X-ZGp`@{Kg060#OB-7W_ow?WIVoPDD|f4zGd_L!@l3#S?PKI}Esz}MDOWODFhIi*SWS?8ZkeF@mA`99u?gR1r4 zMOF-fGcGnJtnp?iBE=Q@#k!rl^@n=IEq4lrb#uH!cSwphy*KG{D!Wc3ftCv2kq?;o z(0yY1qW4tptYGX}<45e&MIT=cv~*T}p9>o=R;lZGa(Z^9{cDnwZbgxhkJ)aNO^Vr? z$noH9r6)49{0>=~=s5Wj`}}n!ez!-j*o3qM&Co9Xh@3XF}ot0`&M^X^p~;Q zrXQLA3bo+B>_J|!Pt6mmNz!?jdPN_537BEUhs!_hR#wH4@7Gc$3VbMIQIw(%8xnA+ z#tY)fYXljvW=79i^WSTr{9DZ4iBz_vWCby%uh(YgD>Alp1joy!`kQ2{97G=McX^iZ z@|J1Z?tRf8vblRYP}b`Bn8&|ASAsX>!_h2Pf^PopZ8-R{JW$xOzTQXkx9y_Q22cje z-@I73zX$=#GgC8R0t?>QlI~Xw3RE>84sNNz_DjRRztlIBgrZ%p|8mj&{TnA@x)OB& z2b<-6iU02}MJW;xO)}f@4g8HK_!Ot0=4h{1l}_{bmogA$rD+LWyRY z=>+}#r4(Ag0!6x}qJLBkn~yMIV!H)j{jGR`PeGIcwZ!Bn7DoS|z<+M~%Q@}^Hf@YrP$77WyQwV| zpj0E?t5oUfHrCkmN-_R#f@UBv}!jAFW} zZ3cX1ZH#u?p6zs<7K0IP8Fi+H*-w`jKIt_^@?S)`x~~c`>|-(`*uL}M@^7aijK0%$ zKgowaT&ijzoaH`=@|)!MLNz6R_WI!?`>0M702b<)7v@nFJWAGf5Vuqg6~OwJgZdH< zR+jT_REriGJ^y`_2_u1Ter_Lu&VvK-^0P9)EzNO8qjOZE-1aTr&Wl3z&NR)g61?@Z zALg^f$H6i))$IGicctnXwD+`5Ue@l{w1wE9SUp9wpt>_lgC7Ij(Vg*Af4AAo`HPUw*ur6TFf4 zI#P&@*30H(&T%Ad9OJkom?{kM}SKO;)lg{T2 z1ez*0^q}u@^eLeSc!N z{{gqtj+<4J$%=hM#2clHBn`6~QBC(JqKo~$cSq|N279@zo6p=?=O6HnO5yEA+6-)M zQbWcc)f0Gwb_(TlN`K0_Eq*OaQBH9-AKvj_yxjY4F)>@(=(->zk+k z;`2Nf%GA;KI|&=0kur)XdY(az#f*>Zqte4(nz?th-n=)+8z0!Me+ad2C=bM}+I%Yr zN!d-$SiF_*?u6Bit}kQsXKgemdsFW{^h9VlRRKt7w;A`L&He9 zBlRMl(($b;ouy9O@YK(XykljI-cOHT_b{@NS{=NyVL3;>csur^(~y`&3RH~rvO|XS zhe>9_hm5XlUqfwoT;27R6L#Lw0*a7kF-dthN^UrAsoUg0pP|v8-ZXNsgleqCH{N10 zcEL#heOj=$3<*uWPyf=b&A3`&$9~V4HT9~lk$zVnlUAv;EF{yK9-ix83hcT$7=~sj z@ae{1#<<*b-)CzdXfQS7Y=3?;;Hk|ehRm2 zYW^W;q+iK6)nz8X<{_U`k!RSf($*yeU6aC7LG08~fzNyLWml^zO$8muCT+L3^Kz>q zd*TV}U4u9ceTH@?m@^#fM)az6^+!4hmkX|U&n-Yc966S+ps4jpRu5J6?I~#%D(Af1 z$JWpmBiH%|mGoKCK9!zN%D?yDHQ4-mYNl=a&aw+#alOWXYvblZv7xV23K=z~dvN;I zmcC78^pwML#8P8)%j$=_&Wj3*KPJplb~;dpHQSPC1uj>YYBP<=q1vK467h&8ci!gn z>@#=j1Ryt&Pe@vOcZM(KZAW8!vWH{62btM}Xe0dtgB7n`d+BoVGInRX@q2@?T@Oo; zwh6Jm{_F0g9!S}*(`TNa?pcA}R|7@6c1~9s{zz3YeQ{2YHKn-(Zxx@4}mtlbX1 zzi>Fu{-OqBdtLcyg&zCQPfxU08CS?KcyFHNy_ab;z&N}9%H?9krnJD(!kp4VIGpYh z;Zl@hNbg4^6&r(;tj?wM7J-yN>X-YiAI5)U0T?cz$n71{5B=pad^?HnNntdeiJMh! zuC4XhjW}~wut?i)T{Jx*tn}z8&kd<9ubS@Y)Wvk(;pv;p& zJtL|TwCbhh|0Wj4YqrdJY&-d} z>>Mtc-M{nY%hefsoE4RT+4J0OnBA>TQSGV|IqLbX9eF>pGukkjVsHHt;*&L`IWb%K zZF&1l|GA??9w_}{ugp+)>q2E=3v=?BzS$Fp8H(+Vpv0AYl;2nr(q~Q|21!0>ILRJy zX0Kn`{;2fzJ;B)hVE)tHhm}!`LRV8gD}N3n4T_%AOROB;$DD(7&! z;=j61)MvcXQg=_g?Yc22qdlcr8@$P#X;dj{w~?#rk(9PFA*gQ`y4psr=GF7FbedKr z*UHdh13N^O=EK)iwWIfOdBOaNh7W<%Sl?O%)MH6|1TqXS84eGlIzq5ZTjw*}TnP=9 zhpx;J+?l3X+bt__nx31S%Ejaw=U+;gTrKsK@+p`dtkCZooaQOr9K5x{-V;0A%|Cy7 z+?#otWNWr~)6gMCzi#LUO7xOon#~%j#(>xOv+GU?(H`Y{^UiI3=PNxKO zU&>OPcK*?)p3s5=zS8es{iW|~!sWXL;o;$9YtlOfL$^#{qblzP5`24W=Dn>36@!Om z_AX^x8QKjoy$MV&D{+^fD5Ibf@oRDa;)I5gvFHOR!fHf;Bo9)feypyXf z*W@8?!4=+*d`X1l`TVY_GBWeoUo`9vI`Ih$O z1Q>k?411&d=16Z5U@HxpiT)E^|8K>ZdvvwrhG6@y(|=+MAC5_{7=)|rP38NH%_Nj6 zW>zfjY~dQq6g$wI_WMtC%|8mEv8334VoMY{XXYd<_0+<2AK8|nvazx$T;_$7gBcJF zI{qK&Y5-rPPdNFX*rI~!bs?i@cWDq7+~=#@L5khIVz8A5^xNlRYajoo*!{oa{-5aj zUvdAhxc|xs|7+d%rK z%FX!aw-5Lp#Q_|yY&%40j(5y=UPgBKbm`!_$C;iC_$$mpwS!ZYSaZ99Dy#Uy4h8nMaf31DSyoR!4#5~r!%wqX_mr%oJv zTxc(ZOg&z-yQM|2M5vp@3y zN{Fr$UQgNfAw3@&TEaj@5SrI9`}{VOA26FpB%Xf&LeYV5-@Z^lVUk`Xe4himpb49J zmzr;31nV0apFGKegvypj{)*Fmi6G8m{d~K~7HJj*1^LUdLCpABbzi_rAp;E{{Mgng zJ6PY&(ec&;*fd$J(!Pk8g$sClrL!-OAJ*p=AR`NdO^4&uqFBSQgDOZSZkWNaO z(g_0QJEGUjR?seip6jrE##Msl>;YHh?I;tA)eq;jrUvu1Mst`qG=^^~-r z1wZkxZ1sVd=Cgl8hmycr2#8v|$es@Y4;#8Ngq~<$o0njkwmSWBH45OgrxGI3_MW7D zzie0n)YGLntz>FIYy-|db~2%Ar)ftjb*)^cI-4#IR8;K~-lejo%WiU&aS~dOjtWfn z%l~}-06iZqIkT74T>+be9UQt;A__-yh%;cni`$R8nWj=1MkaVDdi=@Frp9i%6w4<* ziVv?{L~5f>6L!5Si4zRO3G!q-ESQM{Pndt16=TrIA5`c$D`*t7MLoci<-zb{cyp(m zX;gN@w8y@OC1rRP_g2c6y53-hBJ+K}2sFbZkv!r(KO_msnMY&GwSz#8!?q{b%UIy) z1*l>EbeVk?XYlIY?D~o*Z*sIme#B7~o>}FuGBNki3=!&@gnRTH2V>MSMIE`x3b}Dz zyzVwF&?0gEtZWzQSAgQR?wZ`pg4C5VI%BV8@n<(T2Y6|cWX@89UqGAqQ=5#IE zKjGi@e||Azk0$sR;M<|{n(|k`M)=wtzHdzfyQ-^{&8q%Xh<-&3!A8X}dkJicNDxsn>;ThhwhT7>Gnq;qjd`b;e$*?ZH*9JD# z`z?$1D56s9Kd3sKtMOwBuNX9&@<#Bo%9+{fdD|e5g2zq=W3YpM8aJ9 zI@2Kz4B-pXuDy=fpNNJbP$R&C*Ipq4V!1qbiIT9-W)6}5%w?HoRJ6^)Q(h&N6>IFM zGaXY6TjV?JW}X`=58$#yUt34a zBEpjMYQX19sA}SNiS1yyN$|;Sl`gtLxNjq6`3{sfE$`pV~ZPGa_i3vDdH7v34Voc-@q!?In;)ILSlscg;~Q3MyRS{HHVVb z#-QYyq_q#{9I%zz)5Wtl30z&8%BC1|FtI~2(|fJHY7Y>|wehSQzrZBfh>n(S@)Qo( zS1R`xmYz=SXDnWbILKetxaClJ!%L%d3QCJp^vT|PT2T%!_i$)^^c&Do$_cx7omvXu z+ubzUxZRHepWtw_7D3=Uw5{LEHE_-iZ2sMd3IJ6)Y;$bPT>u0#E{*^Xv0n}_lQCn{ z)0L*)l9{ZU@hxxCEYqT{RccYQ(|u8OfCGp5mfk`A_V^8#vF1&F5?kCb5sT=!=?Vy|g`9Rp%P zkCA4caagNmjy_U?s{KTJXVe|&%#Joje#z=T5DaSYnmb}|>!@lFUK|ICu?=MB6$D4W zgagwnk%^4k4Wf&R`2#7JwXSZA203I|wUN^DFafkTdeJI&_n>PIX6trO9^ak;4gf7W z?7%c;15~u(wDd(I{w&_~BzxKFPi6=584eHcVa~G)^GskA!nX;@40b(kS6SgIU(4l# zg4xoy8+*H_pkT&0(=gp~hXCG6QHP)aK!|5;TzjjO7;YFV48@jRc0c@#E3xeuOCTBJ zn@Ym%*pNC!-=98JSZvopigvZRlP-H5?=OiU!_pplB62`V`NZ@NFPvMW_f!wr$h9NwVT#uiuK=v;0i>Nak@SjQ<9BD4Q{h z#RH^pu(mZi&dgqir+RBWkwbW#$^%iZid%v6M^W&4Qp zH?Ci&TZiDgwGUlA1w@ZyzC;bSZ+EN7-t9|BQXp*rOo|ze=3D#A z{?K#9b=TRQdhM{ch!ZfXt%RO62`p%cd}5!4JQ!dTewW$Lj(sH_xpB^VQ(dPj?Q3Y1 zcnmM|M2zsy$A#|cHxq>#*q1X~tvMsV6z%H~5RRIQksJ2lAnps~E9|{u5YAdQ(_s4* zaX`6gvs~xlIZt&127oF2SuOs5IyQcLqTTGIq*9bu;dy}s=)|goy(btE7pW(tZ1*3e zzxj)|&w#gAR`0*^gQ$UAT`>KF5sp6IX7h&1Se>0jHhtFRmH1igTH2uLR{t}4wfI6s zo+v?HDzgP1g2joIJH;pTSTQyz3*|kc$pT-<-TeC!u7e%AF3WLBr@9-R(DP-7b5rpN zbcD(XPkd}wP0YU>@WjK6rb6`c;%I{%*TAp^Rr?bMX1YDlU$rwlJTc05;~9wW>wrr+ zHb|gx*tIB{!*!5vj zSW2`t>xd-CM}=LqZ{AI;<;FMv-jpl8o*5F1Vn5_yHc1ZG{d+3`&u@fX2*i>MGWn0+ zKsy?o^mIHOaW zDC%FH1d=V$45O~;KsW}uOV1%c#2JaK)kN(!K6rTAqGhus&vh7wH+lHZlJ25rzWY<1 zWKD|Ig|`4&0n+W!g2@TkQ(qW&cmUkN0RG7LcA^`HCtq9=Z;>aOyfi87u{XL=Z35>k zOK7G+gmm4#G58x|@$_^0xTZ6E|5KV!NrLBn6i4dI;g^zo+`|R8Nq$9}i(3<3iYoNK zCV3Q;vuf)rr*%haG}I)F$xceL2;R2kJ`CYOxRpS}`P?f0J+RR_!)13tB{2boOYf0m=xq@T323pU)Re`OY;D>!n$g^(~ddyqI6^jOSO7lI`ZPnKnUE z>U~}B7YS@(H?LhHwtvHmR4HLs-D?AAE@m5VrY7}|1@|M9XT*CX~B`aD2GI#Sx`7RLs|w32VVyE}7e z*3npo+}4I`udnP|4Ne4a-~w7d}N)?~BH`J+j&S{ju-|o`7VESnKsvXPw z%T88NPp>K6m+Pj;!34gFw>`KQPH2EEC!ETE@DASSAFD7GjD%-}bVZlVlJrtK#Doau zrEe*AIZr;B8D<0EiKC7u#X+ie|8|0#zjX08tZxCVHQ+w3okA8^#hOJUjn!cvZ-*Mg zj52J|^=OzT3F1pU&2qH31Qu{)(I(h)iX1?DwP~8dO)%AKa`9*VnJ{NDKE+d-pq0*Hu2WfBB`uj&tUpK~5I)jpu zVI)>~i=sC3Hx^)ajB^a;m6N@ja*SnqKdFuXo@rtQ3dk2cWbpz8+IrB|QAu)xeA3SF z%N62D%jRv_)F<=Y7fEQBofmSlT2#InUo}6nr-Up>DNU!W&z20{49T)07eaB($q#Rc z|A>$Me#y2Fa8#WLUtJKc6FBtn6*?2gtEtG$yp^+Q%$LK(yrp+0P$I5`GsLy>=jm{V|fvV9!5tWr8jFjLeDU z*^flMZJZiT%09{$DxGuP-xHpz$G#BRqn00ZLCG!6(s|ConZ7LhDVI~iW4p`+_SSv*JFsX4Zi*FOPwoS92W~I1 zU>tTG1jO#}^<7Olq`*{;Tl@5aa@6Rs;$4a6Rth2rxTHL#EEMM9wAdAM5od&V$S7xh zv1P*}hstogKr$uwZ-^4PJV#sGS*ltyPBghh%hvcP690B*8})QA=l3trj`u2UhGbM6L`+;K=Cmph@03e; z#b?B_F4%P%1 z<$}Qi=jC{q>e3&0Z%#A{)yxD|k%P==bO=z5@rOY_5$Fb`-Al#8pcC%dAL&!Dc&^BR z{L`SDMNb|56C!PL(m4ivhS*&eW>|hzdfH#7bZW%J#qlL^v_w%-SsD^Xap_$MSes*t z*3FEVszwsbaH#;x^}cD1kII$hZuijhZlnejzK0OaAOVQH+P}-vp;)1uJ(C-Hhx|;0 zg7``_iB)tM1HF-9TXLo}0$j?_nmXA=-{Vtri_hb2-1theVcXBt z5Xn1YH=4+A!fvt^Txl$Fgk zwrVasc3~=XjM|I3-Ge-iOGWx*lywE%MABdXlIR1`ErNI|TpWB=h*M*Ggr-*v!E_d$ zn<2Oe9#{JAGgvMF2d>p(5Nmo@>negq?po>Q zUuQL&;)05zLBMkzl+UZWQ`VO!#2caobB)~$s57+l-KUq9e!Y9|x{>M|z}!!BNq5QN z{C0OdO-U7pBjt9}tB{x|N}qK-^$-ja&dv&S1vcenLmGKyD;M@!MW zZ>0=L&(ApMNSyt`c+N?7Mo%MNIf~peh;K`4Mp!i%{RuZj@~*PLt8c?9oKHR`t#^{7 zuSKQT#jG=ZDXMmrj*AfG9Ch+A9G7D{gWKD!7$v`Q@rd)(wyRb$YQFv1o3L9*)HGf7 z*>+=!wy$Js?T}%wsau$!gQLEFdw0;+QYkl|n`&ugehHP|$;bJ_VAM6bkhIQVh4GxH z@l4FE>4I<@ybL>y-*O)N0&2o$sL=i|*>AuY|)APMzi_E&qf)1ef;nMLJ^o{?8%twj?2e|Qp(_;i7tmJiV*D^$at6oVQ8fE{*G%Do z;Iqh*hOgpAzegS+Ysd` zaL)FY!|DAJzz* zdS}pQR43-V)f)uMwK5^&pc?I{J)UC!wc}=FvQJ2n7EF)hx`&=n;SU2xpisJP%YefQ z7FfRM$xA74;2sZJw0x0jS>l4Iy7xet1^_ZSH0jj2$C0~A78rdRV^Z4cm$%wFX|jV6 zOc}RUItGfRC-b39LYP-^lv)T*@!>@49ZDi!u+{i#=A?$OzM)%;WHsWiTd^KviE0T^ z!P#_1FyOa42(kH7&X5fUH8u?4`fO@JJrr44J8z0SLfrsye__o|_X*ex zb@`26zYBdFJK9X}p>r84{_Wz6G2}Q~U<0-+A45##18>{e%y<6|Ki@4-QcwTEH6*whzY&e9 zCl})q8u5gA|A=fKD?+@?iuD!=L5IPHSqJ1lF@PhJ`e2!XLaL~zCj_ZiIMx8}8h%Z6(Nyzuy14IQ7&a zFY9sihuYzS4bmePwT<`I32(9CQi26j$ogta>q`MdmTSLIgwcg1BBLAd_`u`+u7pQO zm#LzQ7s}7%U?fD%58}tc7UM~TVQ!!t-h}8vDiU(e99t=6hsUI`Huq++uAn$3lSlOd zaBluc6OJi|H5Ca&aRC=z*z(3YT>_)Y!l8W}d7?_>+9S`Eg$0X`pNHgqKl4ZNPTr|K z$AGg2U!IQ@X2m7?4Wn9uPsZ0m=|@$;*6KUls+#zeAZ*patM$gSFHYC0nhUoSNA2Z@ zcI17-*F+wmCJ~vj;ihwf&hNZ1SLg9RT)CHP&Y|NHa!6LG+bYe8SD%SBQ~|Yl;@kL2 z6SwpSzd0T=T%Qz(d1#B{`v8dodTtfjKr&$;`xu8A5g-M%=j<~gKcBI8Rwsl&wMFvb z-GfrH zF&wOlU5_jiB<|u?2XTp>alp&=tP>8_#<7PKsl$5zTqm9qyW-bu_?pi$m%1@Bj9IWK zx{%&wB{Y66l~<>fs|cB*ew^mD>tzO-TXp;vN8 ziB(w`*=Sl7^xjkaj^X<~NXIvmuNrPUi z!T-J`_a7?fJ@X!rzN^vokVIV|%lt~>PWJU!06GW=Ln{)=U5#p8a)i}u9B$UlmUxHSRvPy zrh@W7AwwmzpEryGHghZTPMGD>Sn0d{?+EnH9J$e0j43NpVUDqP--x3`iYi}Fvos&5 zZ za%eCW%LnboR1Rn6(9@t#t%Jv4)MW>>cst+9;xvebn1f@;@FUhFNUcIFhx^Xra)2uGTCES?nk?qic(W>&q$YnP;(E3i{?t2p{C9zx@)URpd#AN@ zEGcY+wlRzH0@Uxl;6^q*rbLLLPM&wtfgu>dC)1{aF#XQI`*zY(Nl}sL<-_dfFrWr_ z56X7v5%#`sOa^ZZ9u8^!$^j3_FVgj0Aj3B%hyL0^WC(3`#M-z(6xeqI_U_VoCxZgp zC#np6asrup+=LbfU5Rj{-d4QGn*>THb=ur{gt`o5zB$u?(w4y%DP6#0&ub8xw!Y}+Nooo1VN#L%6^2$@Dn*gJ8+kH;E4QZB5o3*Q2}zA=(}2BQGun*GNbA4R}kwB{`HiSEgQDj@KH zCm>8u-m22yz|p{Ue}r~77z9LuMQd793S9ewzkE*P@E%}_1aoyPweRL zd;Ib>xSUXQ%Z#qE8!S*;9B*Om=Fty@0nvkG)z&h`GOj%Jck$yu$>ZsS@j!Qwh_NTO zyBWzSdr$M+Q9!96cbwU~mG%-{DlSUf?t&UPT4a7 zTS5uuq3q{tic5$lD?jC*!p(q#t3yrj-)K=U-sT84puY65(I}QuD*0z$%JtQfovmzS zAjj?JJNb@z-vAk*TRJe8K*{J&c?;2h64r1gG#ZHu$ln+=DuPml*(Kee#Wy}Yi8PpD zCPul<%1|l%;MvpsxeM*Cqe0X*7Vjc<1R@r8kK*6n^*|AT+#y$NLjHwYYE7SHnhOTO zG$`Lx6Io=sZwZ-VMrMvCA_Zfo7^IC#m66fM%56_SHa0f=TIMb|)^M&)arp{UU}Oc2RRspo1egirgPFiEa?WzT`=pi(m7=!&92%>~ zc$y|2hbFU(S8)XcghV?={3RdAj?-G71g<5>PRlHvt_LI=Ypu^C{n#<=n7aInxbBk5 zyAqZ=eisbhbo{Kt`8F)1z;Zj#W3A#|dBaK<_5R*daQ#P-! zxaYWDgHZnA%9y?sgRNd5d+oi4U8Gom62}X~i?&Da%#b$X6jG`(O|H_dmO6utrE-LO(z)3c1ZP?|FQgceKyY*5luH z+De!32J~`JEtrUU;2-ONyGStlcLR$7f(w{h001;K#k4V%(?Yi}%I|Of3?feW?UeBk|gSKgWTPPku7w`Ntt^A=UF;+r@(A z$wjgOKv49{JRHBX#faF1Nv_mm zofhZ|*)jkmrLIonVzc$kb-dBTnO0?@?zwWNhg*JoR z3@NEy{DkD6xf8R;a5_P&Q(Z$hT|kswNKgA{5`eGdrZcJV4u_VXL6QA1x0};M)DjQe z(75CHLZ<>FtI6-ALyIG&V0%cmS{H;MGfow7whSQPQYcH}Bv??sIOH2Wg0CUFDSVR) z2b+H9lI@?DdoX=~Adf;cI=ck1qW8m;4jlziTo~}t1jRo}Tl1vXl;+XEAn~MQVXQwz zJDj?=v1t_CIv`PVa3Z%1d>m1ipvRM@!wDx3dLD(A52 z7vEwOP!TF)G20nWJG_(@j#v!)*6S#>t5dW<~bWZedlh9+zvlVD9W5 zx>e~fOaLf!8j3;8T@@~#QkAcgGC~@UHQ6shma(y|6HRy z(|aHwk!WIe9nwDudgw2U1gX54BXt_}$SgPMcAe4faD~;-rdPTo)T}sCI&3d@r`GX9Qe!UF`=M=A-RL;10MY1JWJ#>q9ZD2!aC;iJwaSOd+>#r##Xw z*Q+IB{wr)0#{D-~Mj(Uksl?5%vd1@i_{2O%(9&xCr2X+V=Cpk1ICH0DCehJ?}I;2UpUXj$nx=KNg1`zK7h^{0E?o z1=8)1%QbyDb|83m!UykNfUf%va;Ir$h5Uz$r?1;OaBIE2VqAA{g9)l@WpZ|xkRU9j zbC$~YJ4k3vr^=m|0Jq7hi!|^@f-v%(JJ?*fq@a@fG|LsFm9GDI1T4G*EI&+tWgEHI z!heW)x)Vt4%B8Z0hcJesK^eOKC^)o#1)M?{FaQ5+C=Xa=^Izy}0iXs{h@g4!uTU~D ze9VA~L2i7w1zIhQqjS_=q6wt<3Ey-LT)Pd~y9yl6J3csYKfLa9pzhXOq zboJod5GcuiN%wzBx{&Z6)$eMYzKtqzC+M)jMI2Jn^`Q8IZ{5G+B;8_t48 z%fk}##;IiYVTq%5=2T)J;8y;t6(eAQzuW_K z^RKoV6$}=GfpM7@c0C5KzdFg{QU4GYqbu}anB#_vMx)_ASO@x&D?juXZ|8vvf$&7j#E{|O7l6JMQstw+!Y4I!iW_23fU=U)udp|AgR8Oi6!jB- zyYNLZR^*l$T=!rNse>$E5b1eS32P#9~4|4;j&qOKklt<`=nRyQyf8^MGd**$!2Dv3pyB6`mmN>5% z6+Es!QLwfm%hM$8fT9jFawzQ(7v846Ehz3fdv7y$4w*3F^))6YekvjK?k+|)PV!XhLC?FfO);n-kmCa z7|N0EBBRsCml5>h{y+lWpz zdB?QU*`2YFFG5Ba&D0inGAUwcOymejUQ zIwp-`mNuARlif;**X+dcWm#$Qx|WsZ5Zp4uBK773g5-ToPW7@ZEwO8(L#CNn3R)@H zgxtz06%tHHMFde16z;J>>wW!r|DXRB=j^riTI;N7uWx~40PiH@M02NRlqJ< zYu*j89_{7+0pfsF+dsb=+PNY5{XJ;jbG- z-3p;jFC`d8-L*&Y-JZX98)YU3+NOg@4mH5>)6$N+|C}A!QWW15ed?!j?4kR%PlKy| zE$Ev*!pxFoDE>>$d%?bPQ&ro+d8R*Srvy?{#mk?|;hB;0csz`ioHM4@F}03a>sSyN zi>+V1U@W$drL3_^;D0NyqUTEIu5~g`n(8pKME=-g%G@J-_wB*Wy%8;zV zX~K9=XsDTtE=8pH9^bJ1amevBLdrEx<uH2kx6fpx?3tAcgGC>r9%T(?X>Iv(UB6bWF4G1LzO4+XpwfzQl!3DyqFcv>361Qi z{Xxv;TevqQZ`$swPPD)ruc+-@$IJKYx@6XJ7%N2|5dodVz6L$kaulCD@OECRJXa1I zI#IbyEHI+ZNujm8>$C2|8e=Wx6n$q{OqpV8p0L#;$WXK1e0fqY(N@rDy_|olt+GFB z;;8(A0rrO@&=#pO1-H z4Vv&udS0RMC_`MZ-gf#z0OeRW(U{U7zfPO{DOi1-B>YX6uCO3awmGqFK zGbx%-ywWLh@IxK@jj1XHUE0-Vj~5KQ6#3QpVAL}lxkmeqM#|!0K*b#9IeTc9w|>G9 zma2TPr{F_!UzJ?5R*Dvu=y+s2C(yjYhiQA}ugp#C_k`$qKcFKbHqzOXReHT`R{)1f zYO-ekK9E%$+1x*&EKaMuNX+cbtnm_lWc7BW)^y;NH|i>rqpdmR7jTt#CANtsjmAj_ z{2X4_%>Qz3`1ZXU0F5o*8*3EZaxt5r*tNFM#G59IetzPDI7u*gvQKWluVB6>RpzZ2 z@-}1AXe|epiO&*AmH_Q#@|JvmL3};Ay15`4ZZ@j9VG|u@)Euh}NN=8%60+ZREG=Meb-$rlY;Hz6RvUO(%KPY`3%rIVUQm+EV5ZROeEb*EhG7i!WpTm}7`U-#0Jm&-$|^`xdK?a4jL zi+PoV@wg(tA(@`CBM3`TBgodiwg#}gh28k8XdM?<`GlstbZF=fPm3JP+p~^Vgq7c$ zMz@@<)+Ketnc~Wp2!hL#*NN`OHe&N7?Hz2B1+IywC-YJpbSyte>iRj5=M$IMX9fV& z9@T^VgR*dul6W+p)mLwG@M?EmeoGg*QRs3^_X*i?rT8Vv(ZvdhuPs8s^;)7h9FQsU`PNQa>AsSkCf)$55n+$qhL z7%f4L)R5lUqd@=7kla=ZOyv28hF-YmyZ7l|?+KIH$WoM|q_(*c%DP}aIe^oCKvK!p zcKT=!*fGY@-C^kH5UOxIHmPWjs4;~)$sQiiR)ftnWdd@^>8*c@LZCUm~!gJsyy>AY| zUjX2%5D4ki`;tlIk=QE$&L;-q>2d z$hv9J7n_JEyKQe7z)^4O5VITux!#^q+1>vbF{R* zC%-w2>ePNtw}KS`%R?R0$6}L6O~Je4k!#l^Mpav34i@TLx}GAW8|idw>O9H5VuxB` zB+eqd^*Tl(MoKH0pBzn4D*yn{Ak^ndD~{m&pEdUnyPX>2)+lfl&-@&Y=r(5R=QLe- zXF(5mRs9*W*NHh%m27US2 zh}@Ax`~|oRHCT4>9e!Aes6wImD^Xs~KkoIbArO_75&1n$1nQhKm=ORLLL)`rKfeK$ z3p<^AMw0Vq02LbpR>vHI=77F`zIi4>diO-zX!0GF1)Km$VDQ(ZA6BByK^+zk!`Gwz zoX-(^3M9e!KWBbV69uAnff=_arCR@(GiF2HH^F7uw-oXPWlXVSiUr_f77NV>{a=Lw bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b GIT binary patch literal 31035 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b GIT binary patch literal 31035 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json new file mode 100644 index 0000000..ecac483 --- /dev/null +++ b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json @@ -0,0 +1 @@ +{"input_matrix": {"data": [[0.5, 0.3], [0.2, 0.6], [0.8, 0.1]], "columns": ["Criteria 1", "Criteria 2"], "index": ["A", "B", "C"]}, "polarity": ["+", "-"], "sensitivity_on": "no", "normalization": "minmax", "aggregation": "weighted_sum", "robustness_on": "no", "robustness_on_single_weights": "no", "robustness_on_all_weights": "no", "given_weights": [0.6, 0.4], "robustness_on_indicators": "no", "monte_carlo_runs": 1000, "num_cores": 2, "random_seed": 42, "marginal_distribution_for_each_indicator": "normal", "output_path": "mock_output/"} \ No newline at end of file diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv new file mode 100644 index 0000000..f1ec464 --- /dev/null +++ b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv @@ -0,0 +1,4 @@ +,ws-minmax_01 +A,0.5399999999999999 +B,0.0 +C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv new file mode 100644 index 0000000..3fa3f42 --- /dev/null +++ b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv @@ -0,0 +1,4 @@ +,ws-minmax_01 +A,0.6666666666666666 +B,0.3333333333333333 +C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv new file mode 100644 index 0000000..f1ec464 --- /dev/null +++ b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv @@ -0,0 +1,4 @@ +,ws-minmax_01 +A,0.5399999999999999 +B,0.0 +C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b GIT binary patch literal 31035 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx literal 0 HcmV?d00001 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png new file mode 100644 index 0000000000000000000000000000000000000000..1fcba28264bacd213b30bde9a6227c4ca86013f0 GIT binary patch literal 32090 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 Tuple[int, int, list, Union[list, List[list], dict] return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - def normalize(self, method=None) -> dict: + def normalize(self, method=None) -> pd.DataFrame: """ Normalize the input data using the specified method. @@ -83,15 +73,25 @@ def normalize(self, method=None) -> dict: - method (optional): The normalization method to use. If None, all available methods will be applied. Returns: - - A dictionary containing the normalized values of each indicator per normalization method. + - A pd.DataFrame containing the normalized values of each indicator per normalization method. + + :param method: str + :return normalized_df: pd.DataFrame """ input_matrix_no_alternatives = check_input_matrix(self.input_matrix) mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) normalized_values = mcda_without_robustness.normalize_indicators(method) - return normalized_values + if method is None: + normalized_df = pd.concat(normalized_values, axis=1) + normalized_df.columns = [f"{col}_{method}" for method, cols in normalized_values.items() for col in + input_matrix_no_alternatives.columns] + else: + normalized_df = normalized_values + + return normalized_df - def aggregate(self, normalization_method=None, aggregation_method=None, weights=None): + def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: """ Aggregate normalized indicators using the specified method. @@ -117,126 +117,6 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= return aggregated_scores - # def normalize(self, feature_range=(0, 1)) -> Union[pd.DataFrame, dict]: - # """ - # Normalize the decision matrix based on the configuration `f_norm`. - # - # If `f_norm` is a string representing a single normalization method, - # it applies that method to the decision matrix. - # - # If `f_norm` is a list of functions, each normalization function will be - # applied to the input matrix sequentially, and the results will be stored - # in a dictionary where the keys are function names. - # - # Args: - # feature_range (tuple): Range for normalization methods that require it, like MinMax normalization. - # The range (0.1, 1) is not needed when no aggregation will follow. - # - # Returns: - # A single normalized DataFrame or a dictionary of DataFrames if multiple - # normalization methods are applied. - # """ - # normalization = Normalization(self.input_matrix, self.polarity) - # - # sensitivity_on = self.sensitivity['sensitivity_on'] - # f_norm = self.sensitivity['normalization'] - # if isinstance(f_norm, NormalizationFunctions): - # f_norm = f_norm.value - # - # if sensitivity_on == "yes": - # self.normalized_matrix = {} - # for norm_function in f_norm: - # self.logger.info("Applying normalization method: %s", norm_function) - # norm_method = getattr(normalization, norm_function, None) - # if norm_function in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, - # NormalizationFunctions.TARGET.value}: - # result = norm_method(feature_range) - # if result is None: - # raise ValueError(f"{norm_function} method returned None") - # self.normalized_matrix[norm_function] = result - # else: - # result = normalization.rank() - # if result is None: - # raise ValueError(f"{norm_function} method returned None") - # self.normalized_matrix[norm_function] = result - # else: - # self.logger.info("Normalizing matrix with method(s): %s", f_norm) - # norm_method = getattr(normalization, f_norm, None) - # if f_norm in {NormalizationFunctions.MINMAX.value, NormalizationFunctions.STANDARDIZED.value, - # NormalizationFunctions.TARGET.value}: - # result = norm_method(feature_range) - # if result is None: - # raise ValueError(f"{f_norm} method returned None") - # self.normalized_matrix = result - # else: - # result = norm_method() - # if result is None: - # raise ValueError(f"{f_norm} method returned None") - # self.normalized_matrix = result - # - # return self.normalized_matrix - - - # def aggregate(self, normalized_matrix=None) -> Union[pd.DataFrame, dict]: - # """ - # Aggregate the normalized indicators based on the configuration. - # - # Parameters: - # normalized_matrix (pd.DataFrame, optional): The matrix to aggregate. - # Defaults to self.normalized_matrix. - # Raises: - # ValueError: If no normalized matrix is provided or normalization was not performed. - # - # Returns: - # The aggregated matrix or dictionary of aggregated results. - # """ - # normalized_matrix = normalized_matrix if normalized_matrix is not None else self.normalized_matrix - # - # if normalized_matrix is None or ( - # isinstance(self.normalized_matrix, pd.DataFrame) and normalized_matrix.empty) or \ - # (isinstance(self.normalized_matrix, dict) and all(df.empty for df in self.normalized_matrix.values())): - # raise ValueError("Normalization must be performed before aggregation.") - # - # configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, - # self.robustness, self.monte_carlo, self.output_path) - # - # weights = configuration_values["given_weights"] - # sensitivity_on = self.sensitivity['sensitivity_on'] - # aggregation = Aggregation(weights) - # f_agg = self.sensitivity['aggregation'] - # if isinstance(f_agg, AggregationFunctions): - # f_agg = f_agg.value - # self.logger.info("Aggregating with method: %s", f_agg) - # - # if sensitivity_on == "yes": - # self.aggregated_matrix = {} - # for agg_function in f_agg: - # self.logger.info("Applying aggregation methods: %s", agg_function) - # - # if isinstance(normalized_matrix, dict): - # self.aggregated_matrix = {} - # for norm_method, norm_df in normalized_matrix.items(): - # agg_method = getattr(aggregation, agg_function, None) - # result = agg_method(norm_df) - # if result is None: - # raise ValueError(f"{f_agg} method returned None for {norm_method}") - # result_key = f"{f_agg}_{norm_method}" - # self.aggregated_matrix[result_key] = result - # - # if result is None: - # raise ValueError(f"{agg_function} method returned None") - # self.aggregated_matrix[agg_function] = result - # else: - # self.logger.info("Applying aggregation method: %s", f_agg) - # agg_method = getattr(aggregation, f_agg, None) - # result = agg_method(normalized_matrix) - # if result is None: - # raise ValueError(f"{f_agg} method returned None") - # self.aggregated_matrix = result - # - # return self.aggregated_matrix - - def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict]): """ @@ -259,8 +139,6 @@ def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, run_mcda_with_indicator_uncertainty(self.configuration_settings) elapsed_time = time.time() - start_time - self.logger.info("ProMCDA finished calculations in %s seconds", elapsed_time) - # def get_results(self): # """ diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index fab3b4e..aec7401 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -30,7 +30,9 @@ def __init__(self, config: dict, input_matrix: pd.DataFrame): self._config = copy.deepcopy(config) self._input_matrix = copy.deepcopy(input_matrix) - def normalize_indicators(self, method=None) -> dict: + import pandas as pd + + def normalize_indicators(self, method=None) -> pd.DataFrame: """ Normalize the input matrix using the specified normalization method. @@ -39,7 +41,8 @@ def normalize_indicators(self, method=None) -> dict: Supported methods: 'minmax', 'target', 'standardized', 'rank'. Returns: - - a dictionary containing the normalized values of each indicator per normalization method. + - A DataFrame containing the normalized values of each indicator per normalization method. + Columns are named according to the normalization method applied. Notes: Some aggregation methods do not work with indicator values equal or smaller than zero. For that reason: @@ -50,121 +53,147 @@ def normalize_indicators(self, method=None) -> dict: - for the 'standardized' method, two sets of normalized indicators are returned: one with the range (-inf, +inf) and another with the range (0.1, +inf). """ - norm = Normalization(self._input_matrix, - self._config["polarity"]) + norm = Normalization(self._input_matrix, self._config["polarity"]) + + normalized_dataframes = [] - normalized_indicators = {} + def add_normalized_df(df, method_name): + df.columns = [f"{col}_{method_name}" for col in df.columns] + normalized_dataframes.append(df) if isinstance(method, NormalizationFunctions): method = method.value + if method is None or method == NormalizationFunctions.MINMAX.value: - indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1)) - # for aggregation "geometric" and "harmonic" that do not accept 0 - indicators_scaled_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) - normalized_indicators[NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value] = \ - indicators_scaled_minmax_without_zero - normalized_indicators[NormalizationNames4Sensitivity.MINMAX_01.value] = indicators_scaled_minmax_01 + indicators_minmax_01 = norm.minmax(feature_range=(0, 1)) + indicators_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) + add_normalized_df(indicators_minmax_01, "minmax_01") + add_normalized_df(indicators_minmax_without_zero, "minmax_without_zero") + if method is None or method == NormalizationFunctions.TARGET.value: - indicators_scaled_target_01 = norm.target(feature_range=(0, 1)) - indicators_scaled_target_without_zero = norm.target( - feature_range=(0.1, 1)) # for aggregation "geometric" and "harmonic" that do not accept 0 - normalized_indicators[NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value] = \ - indicators_scaled_target_without_zero - normalized_indicators[NormalizationNames4Sensitivity.TARGET_01.value] = indicators_scaled_target_01 + indicators_target_01 = norm.target(feature_range=(0, 1)) + indicators_target_without_zero = norm.target(feature_range=(0.1, 1)) + add_normalized_df(indicators_target_01, "target_01") + add_normalized_df(indicators_target_without_zero, "target_without_zero") + if method is None or method == NormalizationFunctions.STANDARDIZED.value: - indicators_scaled_standardized_any = norm.standardized( - feature_range=('-inf', '+inf')) - indicators_scaled_standardized_without_zero = norm.standardized( - feature_range=(0.1, '+inf')) - normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_ANY.value] = indicators_scaled_standardized_any - normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value] = indicators_scaled_standardized_without_zero + indicators_standardized_any = norm.standardized(feature_range=('-inf', '+inf')) + indicators_standardized_without_zero = norm.standardized(feature_range=(0.1, '+inf')) + add_normalized_df(indicators_standardized_any, "standardized_any") + add_normalized_df(indicators_standardized_without_zero, "standardized_without_zero") + if method is None or method == NormalizationFunctions.RANK.value: - indicators_scaled_rank = norm.rank() - normalized_indicators[NormalizationNames4Sensitivity.RANK.value] = indicators_scaled_rank - if isinstance(method, NormalizationFunctions): - method = method.value + indicators_rank = norm.rank() + add_normalized_df(indicators_rank, "rank") + if method is not None and method not in [method.value for method in NormalizationFunctions]: logger.error('Error Message', stack_info=True) - raise ValueError( - 'The selected normalization method is not supported') + raise ValueError('The selected normalization method is not supported') - return normalized_indicators + # Concatenate all normalized DataFrames along columns + normalized_df = pd.concat(normalized_dataframes, axis=1) - def aggregate_indicators(self, normalized_indicators: dict, weights: list, method=None) -> pd.DataFrame: + return normalized_df + + def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: list, method=None) -> pd.DataFrame: """ Aggregate the normalized indicators using the specified aggregation method. Parameters: - - normalized_indicators: a dictionary containing the normalized values of each indicator per normalization + - normalized_indicators: a DataFrame containing the normalized values of each indicator per normalization method. - weights: the weights to be applied during aggregation. - method (optional): The aggregation method to use. If None, all available methods will be applied. - Supported methods: 'weighted_sum', 'geometric', 'harmonic', 'minimum'. + Supported methods: 'weighted_sum', 'geometric', 'harmonic', 'minimum'. Returns: - - a DataFrame containing the aggregated scores per each alternative, and per each normalization method. - - :param normalized_indicators: dict - :param weights: list - :param method: str - :return scores: pd.DataFrame + - A DataFrame containing the aggregated scores for each alternative and normalization method. """ + # Convert `method` to string if it’s an enum instance + if isinstance(method, AggregationFunctions): + method = method.value + self.normalized_indicators = normalized_indicators self.weights = weights agg = Aggregation(self.weights) - scores_weighted_sum = {} - scores_geometric = {} - scores_harmonic = {} - scores_minimum = {} - - scores = pd.DataFrame() - col_names_method = [] - col_names = [method.value for method in OutputColumnNames4Sensitivity] - # column names has the same order as in the following loop - - for key, values in self.normalized_indicators.items(): - if isinstance(method, AggregationFunctions): - method = method.value - if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: - if key in [NormalizationNames4Sensitivity.STANDARDIZED_ANY.value, - NormalizationNames4Sensitivity.MINMAX_01.value, - NormalizationNames4Sensitivity.TARGET_01.value, NormalizationNames4Sensitivity.RANK.value]: - # ws goes only with some specific normalizations - scores_weighted_sum[key] = agg.weighted_sum(values) - col_names_method.append("ws-" + key) - if method is None or method == AggregationFunctions.GEOMETRIC.value: - if key in [NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.RANK.value]: # geom goes only with some specific normalizations - scores_geometric[key] = pd.Series(agg.geometric(values)) - col_names_method.append("geom-" + key) - if method is None or method == AggregationFunctions.HARMONIC.value: - if key in [NormalizationNames4Sensitivity.STANDARDIZED_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.MINMAX_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.TARGET_WITHOUT_ZERO.value, - NormalizationNames4Sensitivity.RANK.value]: # harm goes only with some specific normalizations - scores_harmonic[key] = pd.Series(agg.harmonic(values)) - col_names_method.append("harm-" + key) - if method is None or method == AggregationFunctions.MINIMUM.value: - if key == NormalizationNames4Sensitivity.STANDARDIZED_ANY.value: - scores_minimum[key] = pd.Series(agg.minimum( - self.normalized_indicators[NormalizationNames4Sensitivity.STANDARDIZED_ANY.value])) - col_names_method.append("min-" + key) - - dict_list = [scores_weighted_sum, scores_geometric, - scores_harmonic, scores_minimum] - - for d in dict_list: - if d: - scores = pd.concat([scores, pd.DataFrame.from_dict(d)], axis=1) - - if method is None: - scores.columns = col_names - else: - scores.columns = col_names_method + # Dictionary to map aggregation methods to their corresponding score DataFrames + score_dfs = { + AggregationFunctions.WEIGHTED_SUM.value: pd.DataFrame(), + AggregationFunctions.GEOMETRIC.value: pd.DataFrame(), + AggregationFunctions.HARMONIC.value: pd.DataFrame(), + AggregationFunctions.MINIMUM.value: pd.DataFrame(), + } + + def _apply_aggregation(agg_method, df_subset, suffix): + """ + Apply the aggregation method to a subset of the DataFrame and store results in the appropriate DataFrame. + """ + agg_function = { + AggregationFunctions.WEIGHTED_SUM.value: agg.weighted_sum, + AggregationFunctions.GEOMETRIC.value: agg.geometric, + AggregationFunctions.HARMONIC.value: agg.harmonic, + AggregationFunctions.MINIMUM.value: agg.minimum, + }.get(agg_method) + + if agg_function: + aggregated_scores = agg_function(df_subset) + + if isinstance(aggregated_scores, pd.Series): + aggregated_scores = aggregated_scores.to_frame() + + aggregated_scores.columns = [f"{col}_{agg_method}_{suffix}" for col in + df_subset.columns.unique(level=0)] + + for base_col_name in self.normalized_indicators.columns.str.split("_").str[0].unique(): + relevant_columns = self.normalized_indicators.filter(regex=f"^{base_col_name}_") + + for suffix in relevant_columns.columns.str.split("_", n=1).str[1].unique(): + # Define the correct columns based on whether "without_zero" is in the suffix or not + if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: + if "without_zero" not in suffix: + # Only select columns ending with the exact suffix that doesn't contain "without_zero" + selected_columns = relevant_columns.filter(regex=f"_{suffix}$") + _apply_aggregation(AggregationFunctions.WEIGHTED_SUM.value, selected_columns, suffix) + + elif method in [AggregationFunctions.GEOMETRIC.value, AggregationFunctions.HARMONIC.value]: + if "without_zero" in suffix: + selected_columns = relevant_columns.filter(regex=f"_{suffix}$") + if method == AggregationFunctions.GEOMETRIC.value: + _apply_aggregation(AggregationFunctions.GEOMETRIC.value, selected_columns, suffix) + elif method == AggregationFunctions.HARMONIC.value: + _apply_aggregation(AggregationFunctions.HARMONIC.value, selected_columns, suffix) + + elif method == AggregationFunctions.MINIMUM.value: + if "without_zero" not in suffix: + selected_columns = relevant_columns.filter(regex=f"_{suffix}$") + _apply_aggregation(AggregationFunctions.MINIMUM.value, selected_columns, suffix) + + # Loop through all columns to detect normalization methods + # for normalization_col_name in self.normalized_indicators.columns.str.split("_").str[1].unique(): + # suffix = normalized_indicators.columns.str.split("_", n=1).str[1] + # relevant_columns = self.normalized_indicators.filter(regex=f"_{normalization_col_name}(_|$)") + # + # # weighted_sum + # if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: + # if "without_zero" not in suffix: + # _apply_aggregation(AggregationFunctions.WEIGHTED_SUM.value, relevant_columns, suffix) + # + # # geometric or harmonic + # if method in [AggregationFunctions.GEOMETRIC.value, + # AggregationFunctions.HARMONIC.value] and "without_zero" in suffix: + # # minimum + # if method == AggregationFunctions.GEOMETRIC.value: + # _apply_aggregation(AggregationFunctions.GEOMETRIC.value, relevant_columns, + # f"_geom_{suffix}") + # elif method == AggregationFunctions.HARMONIC.value: + # _apply_aggregation(AggregationFunctions.HARMONIC.value, relevant_columns, f"_harm_{suffix}") + # if method == AggregationFunctions.MINIMUM.value: + # if "without_zero" not in suffix: + # _apply_aggregation(AggregationFunctions.MINIMUM.value, selected_columns, f"_min_{suffix}") + + # Concatenate all score DataFrames into a single DataFrame + scores = pd.concat([df for df in score_dfs.values() if not df.empty], axis=1) return scores - diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 3cf5ad5..424ab79 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -233,7 +233,7 @@ def save_dict(dictionary: dict, folder_path: str, filename: str): logging.error(f"Error while dumping the dictionary into a pickle file: {e}") -def preprocess_enums(data) -> str: +def preprocess_enums(data) -> Union[Union[dict, list[str]], Any]: """ Preprocess data to convert enums to strings @@ -659,8 +659,6 @@ def run_mcda_without_indicator_uncertainty(extracted_values: dict, is_robustness # Extract relevant values input_matrix = extracted_values["input_matrix"] - alternatives_column_name = input_matrix.columns[0] - # input_matrix = input_matrix.set_index(alternatives_column_name) index_column_name = input_matrix.index.name index_column_values = input_matrix.index.tolist() input_matrix_no_alternatives = check_input_matrix(input_matrix) @@ -864,7 +862,7 @@ def _check_and_rescale_negative_indicators(input_matrix: pd.DataFrame) -> pd.Dat return input_matrix -def _compute_scores_for_all_random_weights(indicators: dict, is_sensitivity: str, +def _compute_scores_for_all_random_weights(indicators: pd.DataFrame, is_sensitivity: str, weights: Union[List[str], List[pd.DataFrame], dict, None], f_agg: str) -> tuple[Any, Any, Any, Any]: """ @@ -899,7 +897,7 @@ def _compute_scores_for_all_random_weights(indicators: dict, is_sensitivity: str all_weights_score_means_normalized, all_weights_score_stds_normalized -def _compute_scores_for_single_random_weight(indicators: dict, +def _compute_scores_for_single_random_weight(indicators: pd.DataFrame, weights: Union[List[str], List[pd.DataFrame], dict, None], is_sensitivity: str, index_column_name: str, index_column_values: list, f_agg: str, input_matrix: pd.DataFrame) -> dict: diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index f99b999..703ce1c 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -16,9 +16,11 @@ def setUp(self): warnings.filterwarnings("error", category=ResourceWarning) # Mock input data for testing self.input_matrix = pd.DataFrame({ + 'Alternatives': ['A', 'B', 'C'], 'Criteria 1': [0.5, 0.2, 0.8], 'Criteria 2': [0.3, 0.6, 0.1] - }, index=['A', 'B', 'C']) + }) + self.input_matrix.set_index('Alternatives', inplace=True) self.polarity = ('+', '-',) self.sensitivity = { @@ -83,79 +85,77 @@ def test_normalize_all_methods(self): promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - expected_keys = [method.value for method in NormalizationNames4Sensitivity] + # expected_keys = [method.value for method in NormalizationNames4Sensitivity] + # TODO: delete if return is not a dic + expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] # When normalized_values = promcda.normalize() # Then - self.assertCountEqual(list(set(normalized_values.keys())), expected_keys, - "Not all methods were applied or extra keys found.") - self.assertEqual(list(normalized_values), (expected_keys)) + #self.assertCountEqual(list(set(normalized_values.keys())), expected_keys, + # "Not all methods were applied or extra keys found.") + #self.assertEqual(list(normalized_values), (expected_keys)) + # TODO: delete if return is not a dic + actual_suffixes = {col.split('_',1)[1] for col in normalized_values.columns} + self.assertCountEqual(actual_suffixes, expected_suffixes, + "Not all methods were applied or extra suffixes found in column names.") def test_normalize_specific_method(self): # Given promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) + self.output_path) method = 'minmax' # When normalized_values = promcda.normalize(method=method) # Then - expected_keys = ["minmax_without_zero", "minmax_01"] + expected_keys = ['Criteria 1_minmax_without_zero', 'Criteria 2_minmax_without_zero', 'Criteria 1_minmax_01', + 'Criteria 2_minmax_01'] self.assertCountEqual(expected_keys, list(normalized_values.keys())) self.assertEqual(list(normalized_values), expected_keys) def test_aggregate_all_methods(self): - # Test when no specific method is given, so all methods should be applied - aggregated_scores = self.pro_mcda.aggregate() + # Given + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + aggregated_scores = promcda.aggregate(weights=self.robustness['given_weights']) - # Verify the structure of the DataFrame + # When expected_columns = [ 'ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', 'geom-rank', 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', 'min-standardized_any' ] + + # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, "Not all methods were applied or extra columns found.") self.assertEqual(len(aggregated_scores), len(self.input_matrix), "Number of alternatives does not match input matrix rows.") def test_aggregate_with_specific_normalization_and_aggregation_methods(self): - # Test specific normalization and aggregation methods + # Given normalization_method = 'minmax' aggregation_method = 'weighted_sum' - aggregated_scores = self.pro_mcda.aggregate(normalization_method=normalization_method, - aggregation_method=aggregation_method) + promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, + self.output_path) + aggregated_scores = promcda.aggregate(normalization_method=normalization_method, + aggregation_method=aggregation_method, + weights=self.robustness['given_weights']) - # Expected columns when only weighted_sum with minmax is applied + # When expected_columns = ['ws-minmax_01'] - self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") - # Check if values are within expected range + # Then + self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") self.assertTrue( (aggregated_scores['ws-minmax_01'] >= 0).all() and (aggregated_scores['ws-minmax_01'] <= 1).all(), "Values should be in the range [0, 1] for minmax normalization with weighted sum.") - def test_aggregate_with_default_weights(self): - # Test with default weights (None) - aggregated_scores = self.pro_mcda.aggregate(weights=None) - - # Check if output DataFrame matches the expected structure and size - expected_columns = [ - 'ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', - 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', 'geom-rank', - 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', - 'min-standardized_any' - ] - self.assertCountEqual(aggregated_scores.columns, expected_columns, - "Not all methods were applied or extra columns found.") - # Verify that weights were automatically set to equal distribution - self.assertTrue(np.allclose(self.pro_mcda.weights, [0.5, 0.5]), - "Default weights should be equal if None is passed.") # def test_normalize_single_method(self): # """ From c274e4be6ab9a1b516fdfd87a17bc73b353c9b10 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 12 Nov 2024 17:33:37 +0100 Subject: [PATCH 11/27] stop tracking files in demo_in_notebook/output_files/mock_output/ --- .../2024-11-05_14-53-42_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_14-53-46_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_16-34-56_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_16-34-56_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_16-34-56_configuration.json | 1 - .../2024-11-05_16-34-56_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_16-34-56_ranks.csv | 4 ---- .../mock_output/2024-11-05_16-34-56_scores.csv | 4 ---- .../2024-11-05_17-10-33_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_17-10-33_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_17-10-33_configuration.json | 1 - .../2024-11-05_17-10-33_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_17-10-33_ranks.csv | 4 ---- .../mock_output/2024-11-05_17-10-33_scores.csv | 4 ---- .../2024-11-05_17-15-31_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_17-15-31_configuration.json | 1 - .../2024-11-05_17-15-31_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_17-15-31_ranks.csv | 4 ---- .../mock_output/2024-11-05_17-15-31_scores.csv | 4 ---- .../2024-11-05_17-15-32_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_17-26-33_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_17-26-33_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_17-26-33_configuration.json | 1 - .../2024-11-05_17-26-33_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_17-26-33_ranks.csv | 4 ---- .../mock_output/2024-11-05_17-26-33_scores.csv | 4 ---- .../2024-11-05_17-30-33_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_17-30-33_configuration.json | 1 - .../2024-11-05_17-30-33_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_17-30-33_ranks.csv | 4 ---- .../mock_output/2024-11-05_17-30-33_scores.csv | 4 ---- .../2024-11-05_17-30-34_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_17-45-20_MCDA_rough_scores.png | Bin 32090 -> 0 bytes .../2024-11-05_17-45-20_configuration.json | 1 - .../2024-11-05_17-45-20_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_17-45-20_ranks.csv | 4 ---- .../mock_output/2024-11-05_17-45-20_scores.csv | 4 ---- .../2024-11-05_17-45-21_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_20-09-57_configuration.json | 1 - .../2024-11-05_20-09-57_normalized_scores.csv | 4 ---- .../mock_output/2024-11-05_20-09-57_ranks.csv | 4 ---- .../mock_output/2024-11-05_20-09-57_scores.csv | 4 ---- .../2024-11-05_20-09-58_MCDA_norm_scores.png | Bin 31035 -> 0 bytes .../2024-11-05_20-09-58_MCDA_rough_scores.png | Bin 32090 -> 0 bytes 44 files changed, 91 deletions(-) delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_14-53-46_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-31_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-15-32_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-33_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-30-34_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_17-45-21_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png delete mode 100644 demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_14-53-42_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_norm_scores.png deleted file mode 100644 index 5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31035 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_16-34-56_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-10-33_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_norm_scores.png deleted file mode 100644 index 5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31035 zcmeFaWmuKl`Y$XcrGyBmNOub=NXZ10E=g&F5Ger>=?N&PC?O?MQ)wwBrBOsWBqb)O zbSoVK@BJ)vE!Nt5pZ_@@&UKyZdiMuqt;u}GGsfNVyN3@o)fEntoFF-H;J{%e#S2;o z4&Y55IDmVZ=pcOZgQ1!IzyYQMN*82w?wZbG-O4q_hIW6n6}5EA2Sji^x*_h8CKG;+ zoh5_l;`30gqr}A2?AOQZLmMNWGHZpMQ@rp0^4t*xVs>H6{;ImdR)>XESMpWYxx$>n zp2D7)tvavaSr?Pu33u-(=fpMB8c}93F)@R6CiDSZqWyoNb8(6BZ<}o!oW#MWKpoik zmkJu)`tYA$M;*W?n#8$*%f5dNWJwfi;^f~Rht-%4;NmYIIOBWd?+fEoFkzg@{<#3f z0UTWS1J6;!f7>1`P0=n!_|FB1&DgSmcoUTkfW%$aQ^ulT=9v2knR934xEG#Lmcr>DjmRycivA*WNF_6a1y+BeC2;q z3CJw=&jtQV=KZw(KP2-#9!#R7n{ws$)yz0?7oJEF{TyQ_>+VNSLqkJ-ws?J{pMecFPR^#%C(<8hb)Qrv-@G{B+-D$qqb~UQ z8I^#L#2CY#>-=<$RODwDQ<}wF#G8WSJ+ykgOPXpr(&rCXcD;YOUtq5!pwe+SE4K;9 zN&_^^C*Q~7i8mGMn?`(HNNX`<6C_JGy0k_>IvSlj5z<-^ZkBbKw}MbD!o*{zKCd&{ z%+ptLusmVVc*JSu*~O+F#jW{VD-TO!FE?H5K}_nPCwAS zl^vJtOIP!3>jKBnFGhxY+4Z-pZB;FSdT4NV)KaCCwzWZN{N#G=}u_N$s?8UEy^7vbWQRYGm-2Pu&ia z+HWe*M;=^2>CN=0^Ih8x}L%e|@C)Kt~ z!qGV#8OCqoR6Q#)*{HcArk?xIO9=Fe+A`)__Txpj9S2ZumjPnR4xZ-pict zwEV`W#xJ{w)`SfBgsDl-hnje-HZpFJHC9TKEAM!^CEtDXvd4M5FsF>A^L4B)YItW! zps(n<{=;neNAIFnG3w^i(=b;{>F zx5=xwabA{68+8{a>Q0v3siE0es(#V+hE`WhuqK{@Q#~OfGE>xWfqh+f``W~$nb!2< zYg(!1jwN5p^Rw5EefDDXUD`c%<=}q1PUMF&$1Qzt`g4bZ_fYn(cjg(p0SWEhWt{B> z204qb9o<58WL{HB8D2Hi$3JG$7FUQ`y#bo52dua?Zn|0OPwS!tLw6S@n!AHW`|liQ zn=V-_NZgTP2zM{cZG9c=o@&w(yLjnrT7_Tk`|XIZ<31UoSYpwwaEp$@h0mP~sXgwg zc2w5=ul1%Krzz3XE(0GP&-}cseO%~A%_04b?(8iGGEco%I^vuw>$^L7M)o#4ws*!1 z+ML%WKMQkITT-`6n^npuOww%4?k@JRUX!hKe-Z2c#;CuoF5E2Kxs`|CXI7dimn@Ob zS=z+0zf>pD#$>H(?Qps)Zt~pK%{82K1-5|4@nK)zJSrefFXOZf_7O88CEM>te7_T4 z;K4`_O{-h?POH1ErhHob^j`4voaH9kG}E!}S@io1-kRwY=O5nRh$n9fxTI`8-+a>+ zJbbssLU_76SV(1ir!LXydJ^aL9rf-d#k3tEAJK()@0STy#Y+!4F=@?d2~+xQtC<>> zINP^pUYRkeGW_tIj$Ii!JbANKVIfnHo}q%%Wit!6wDg+UOahsZpxw-dDz9}1#&Y`d z%4a`!Pv)`k#T>uub-_(iA*&JmG38#;+m)tsJ%%R+4Qn=MItiBDNcEK`qU9|Hf`wBa zHN9LrXytK|aKGlG*70Q-+jS?kisLgwNQSi)Rzf8VT|$U0xTQ73IL7$r3CZchG97&Q zQfD-zoQex7t7HbPWXo}+^3l--m$d~0v;`liY@`=|tU6s@M$OgfZDHIf$ofq{=YkgQ z=3HTJSd)ObS!1G!VOQ)AT}`hw|F&y60cl?5OqpoY99wQ~y@J;(-L6y8q{DPA-5g)k(p&! ztYq10#)E%b!iuks<*zt_Lcq73aQV}ALXkZqysm=Yn4mr|c37Z-Q2p_Sn=(qjOiCJ1|{Q}3S`@EN4f`?U|;4|YMh@_Xeo1>A+t`HpBdHjKEt!}xs%}b%1L+ZR_TXk zG1m@WC)16*p8QqoVSbE##qQ5-=AOBwB+iziA=1#rOVV^h#s0=lS8H`NRtA5*_sNc0 zYD|pTTzr?6gJG2Rmfdk|Vij^p&-M3sZEeDxu)H>~={mn@66LkcZ`MMyRwY1o;V;w8 zWQ_ZQeA29ij-Om|ML8h)b%w|tk45hpgYOKvPuk94GH>0T@NcWTEjdTsc~_l40;ggE zO)PwUo>r(c_3&g&cle@l1&Ql!x#()Rf6x*v(4BTe%^Cm*Rd%rowjq`EDl1%1yHx(r>o$*(V?W6-1%lBFfX< z{aqtE>#2RNx#va!Qk4*+dHMW;{YdEaj=-ltPyh_Z>Y}7a}y<8wDq!LqSkJ9jwaO+3s z*eChqEYM|yu$b#-#-kH<`W#%&dVYG$td(Yv_RN(ueWJ2^ZfrU!XL5IGDDAp;f`Qr^ z)+I=C@b+nfVy(Cs+ok(=`}! z^$X1>rZ*Pnb1P$}^navCX?kHz^CfJId|XP=IkLMSrj{(ajPmn;?zo=~6gCrb#nf9}2`J~z=2xDX}6FI>r`j-AFj zd@$eeURt%SB9xlz&C0M!S@8Hd_DnQ5b0xLvlZwDAr<}q9caaj837n#~>a5CF{M#3% zXWr>lt8Q@R6KHRy4JqeTj!4Yqf3YW6Zcb=p5dCg26SWZ0cJZuHZNyftL5)@-wsoGy z^{l2;Y;MA*w&|oHzr@v7g{96c4he%j`MXBN3k9;JrP$rrLg^2KUoqneV__9OfU0w%J~v!PUXVJ zUDC=v6PewO-r>=n)Og`D`W0OBz8u8{!!Z@RZT@-Jx(~WJFB;DjO%@1hHqDZhl3bY{ zvmqp-4|Ay6y_D+tma9g$Tf(8tbW?I|EvPnOMnZnvGf%RR%q4S`U?Q?fGumC3-d?da~N0!EiHul4nq44;&bnP%p3 z0+`(**Q%AgQnHXC{wLEzS4`M7Hq2Ll?!1mpj4VhZ_^G{7OO3&R?TNG zT~|x#t|+d}dVyQIX(+aGsW3frgI$Hqpvu$W_4>>+vsB5hf&|IVBBdgn?bNaaWfM7U zrH_5dX?A9PKhLdgy^YP`;kvBdTEbkTnIHQjY2O>f6z%zL2TkJ1rVo#dqoxLQ3*bnO~esouUq z-nVEQ!X-ZGp`@{Kg060#OB-7W_ow?WIVoPDD|f4zGd_L!@l3#S?PKI}Esz}MDOWODFhIi*SWS?8ZkeF@mA`99u?gR1r4 zMOF-fGcGnJtnp?iBE=Q@#k!rl^@n=IEq4lrb#uH!cSwphy*KG{D!Wc3ftCv2kq?;o z(0yY1qW4tptYGX}<45e&MIT=cv~*T}p9>o=R;lZGa(Z^9{cDnwZbgxhkJ)aNO^Vr? z$noH9r6)49{0>=~=s5Wj`}}n!ez!-j*o3qM&Co9Xh@3XF}ot0`&M^X^p~;Q zrXQLA3bo+B>_J|!Pt6mmNz!?jdPN_537BEUhs!_hR#wH4@7Gc$3VbMIQIw(%8xnA+ z#tY)fYXljvW=79i^WSTr{9DZ4iBz_vWCby%uh(YgD>Alp1joy!`kQ2{97G=McX^iZ z@|J1Z?tRf8vblRYP}b`Bn8&|ASAsX>!_h2Pf^PopZ8-R{JW$xOzTQXkx9y_Q22cje z-@I73zX$=#GgC8R0t?>QlI~Xw3RE>84sNNz_DjRRztlIBgrZ%p|8mj&{TnA@x)OB& z2b<-6iU02}MJW;xO)}f@4g8HK_!Ot0=4h{1l}_{bmogA$rD+LWyRY z=>+}#r4(Ag0!6x}qJLBkn~yMIV!H)j{jGR`PeGIcwZ!Bn7DoS|z<+M~%Q@}^Hf@YrP$77WyQwV| zpj0E?t5oUfHrCkmN-_R#f@UBv}!jAFW} zZ3cX1ZH#u?p6zs<7K0IP8Fi+H*-w`jKIt_^@?S)`x~~c`>|-(`*uL}M@^7aijK0%$ zKgowaT&ijzoaH`=@|)!MLNz6R_WI!?`>0M702b<)7v@nFJWAGf5Vuqg6~OwJgZdH< zR+jT_REriGJ^y`_2_u1Ter_Lu&VvK-^0P9)EzNO8qjOZE-1aTr&Wl3z&NR)g61?@Z zALg^f$H6i))$IGicctnXwD+`5Ue@l{w1wE9SUp9wpt>_lgC7Ij(Vg*Af4AAo`HPUw*ur6TFf4 zI#P&@*30H(&T%Ad9OJkom?{kM}SKO;)lg{T2 z1ez*0^q}u@^eLeSc!N z{{gqtj+<4J$%=hM#2clHBn`6~QBC(JqKo~$cSq|N279@zo6p=?=O6HnO5yEA+6-)M zQbWcc)f0Gwb_(TlN`K0_Eq*OaQBH9-AKvj_yxjY4F)>@(=(->zk+k z;`2Nf%GA;KI|&=0kur)XdY(az#f*>Zqte4(nz?th-n=)+8z0!Me+ad2C=bM}+I%Yr zN!d-$SiF_*?u6Bit}kQsXKgemdsFW{^h9VlRRKt7w;A`L&He9 zBlRMl(($b;ouy9O@YK(XykljI-cOHT_b{@NS{=NyVL3;>csur^(~y`&3RH~rvO|XS zhe>9_hm5XlUqfwoT;27R6L#Lw0*a7kF-dthN^UrAsoUg0pP|v8-ZXNsgleqCH{N10 zcEL#heOj=$3<*uWPyf=b&A3`&$9~V4HT9~lk$zVnlUAv;EF{yK9-ix83hcT$7=~sj z@ae{1#<<*b-)CzdXfQS7Y=3?;;Hk|ehRm2 zYW^W;q+iK6)nz8X<{_U`k!RSf($*yeU6aC7LG08~fzNyLWml^zO$8muCT+L3^Kz>q zd*TV}U4u9ceTH@?m@^#fM)az6^+!4hmkX|U&n-Yc966S+ps4jpRu5J6?I~#%D(Af1 z$JWpmBiH%|mGoKCK9!zN%D?yDHQ4-mYNl=a&aw+#alOWXYvblZv7xV23K=z~dvN;I zmcC78^pwML#8P8)%j$=_&Wj3*KPJplb~;dpHQSPC1uj>YYBP<=q1vK467h&8ci!gn z>@#=j1Ryt&Pe@vOcZM(KZAW8!vWH{62btM}Xe0dtgB7n`d+BoVGInRX@q2@?T@Oo; zwh6Jm{_F0g9!S}*(`TNa?pcA}R|7@6c1~9s{zz3YeQ{2YHKn-(Zxx@4}mtlbX1 zzi>Fu{-OqBdtLcyg&zCQPfxU08CS?KcyFHNy_ab;z&N}9%H?9krnJD(!kp4VIGpYh z;Zl@hNbg4^6&r(;tj?wM7J-yN>X-YiAI5)U0T?cz$n71{5B=pad^?HnNntdeiJMh! zuC4XhjW}~wut?i)T{Jx*tn}z8&kd<9ubS@Y)Wvk(;pv;p& zJtL|TwCbhh|0Wj4YqrdJY&-d} z>>Mtc-M{nY%hefsoE4RT+4J0OnBA>TQSGV|IqLbX9eF>pGukkjVsHHt;*&L`IWb%K zZF&1l|GA??9w_}{ugp+)>q2E=3v=?BzS$Fp8H(+Vpv0AYl;2nr(q~Q|21!0>ILRJy zX0Kn`{;2fzJ;B)hVE)tHhm}!`LRV8gD}N3n4T_%AOROB;$DD(7&! z;=j61)MvcXQg=_g?Yc22qdlcr8@$P#X;dj{w~?#rk(9PFA*gQ`y4psr=GF7FbedKr z*UHdh13N^O=EK)iwWIfOdBOaNh7W<%Sl?O%)MH6|1TqXS84eGlIzq5ZTjw*}TnP=9 zhpx;J+?l3X+bt__nx31S%Ejaw=U+;gTrKsK@+p`dtkCZooaQOr9K5x{-V;0A%|Cy7 z+?#otWNWr~)6gMCzi#LUO7xOon#~%j#(>xOv+GU?(H`Y{^UiI3=PNxKO zU&>OPcK*?)p3s5=zS8es{iW|~!sWXL;o;$9YtlOfL$^#{qblzP5`24W=Dn>36@!Om z_AX^x8QKjoy$MV&D{+^fD5Ibf@oRDa;)I5gvFHOR!fHf;Bo9)feypyXf z*W@8?!4=+*d`X1l`TVY_GBWeoUo`9vI`Ih$O z1Q>k?411&d=16Z5U@HxpiT)E^|8K>ZdvvwrhG6@y(|=+MAC5_{7=)|rP38NH%_Nj6 zW>zfjY~dQq6g$wI_WMtC%|8mEv8334VoMY{XXYd<_0+<2AK8|nvazx$T;_$7gBcJF zI{qK&Y5-rPPdNFX*rI~!bs?i@cWDq7+~=#@L5khIVz8A5^xNlRYajoo*!{oa{-5aj zUvdAhxc|xs|7+d%rK z%FX!aw-5Lp#Q_|yY&%40j(5y=UPgBKbm`!_$C;iC_$$mpwS!ZYSaZ99Dy#Uy4h8nMaf31DSyoR!4#5~r!%wqX_mr%oJv zTxc(ZOg&z-yQM|2M5vp@3y zN{Fr$UQgNfAw3@&TEaj@5SrI9`}{VOA26FpB%Xf&LeYV5-@Z^lVUk`Xe4himpb49J zmzr;31nV0apFGKegvypj{)*Fmi6G8m{d~K~7HJj*1^LUdLCpABbzi_rAp;E{{Mgng zJ6PY&(ec&;*fd$J(!Pk8g$sClrL!-OAJ*p=AR`NdO^4&uqFBSQgDOZSZkWNaO z(g_0QJEGUjR?seip6jrE##Msl>;YHh?I;tA)eq;jrUvu1Mst`qG=^^~-r z1wZkxZ1sVd=Cgl8hmycr2#8v|$es@Y4;#8Ngq~<$o0njkwmSWBH45OgrxGI3_MW7D zzie0n)YGLntz>FIYy-|db~2%Ar)ftjb*)^cI-4#IR8;K~-lejo%WiU&aS~dOjtWfn z%l~}-06iZqIkT74T>+be9UQt;A__-yh%;cni`$R8nWj=1MkaVDdi=@Frp9i%6w4<* ziVv?{L~5f>6L!5Si4zRO3G!q-ESQM{Pndt16=TrIA5`c$D`*t7MLoci<-zb{cyp(m zX;gN@w8y@OC1rRP_g2c6y53-hBJ+K}2sFbZkv!r(KO_msnMY&GwSz#8!?q{b%UIy) z1*l>EbeVk?XYlIY?D~o*Z*sIme#B7~o>}FuGBNki3=!&@gnRTH2V>MSMIE`x3b}Dz zyzVwF&?0gEtZWzQSAgQR?wZ`pg4C5VI%BV8@n<(T2Y6|cWX@89UqGAqQ=5#IE zKjGi@e||Azk0$sR;M<|{n(|k`M)=wtzHdzfyQ-^{&8q%Xh<-&3!A8X}dkJicNDxsn>;ThhwhT7>Gnq;qjd`b;e$*?ZH*9JD# z`z?$1D56s9Kd3sKtMOwBuNX9&@<#Bo%9+{fdD|e5g2zq=W3YpM8aJ9 zI@2Kz4B-pXuDy=fpNNJbP$R&C*Ipq4V!1qbiIT9-W)6}5%w?HoRJ6^)Q(h&N6>IFM zGaXY6TjV?JW}X`=58$#yUt34a zBEpjMYQX19sA}SNiS1yyN$|;Sl`gtLxNjq6`3{sfE$`pV~ZPGa_i3vDdH7v34Voc-@q!?In;)ILSlscg;~Q3MyRS{HHVVb z#-QYyq_q#{9I%zz)5Wtl30z&8%BC1|FtI~2(|fJHY7Y>|wehSQzrZBfh>n(S@)Qo( zS1R`xmYz=SXDnWbILKetxaClJ!%L%d3QCJp^vT|PT2T%!_i$)^^c&Do$_cx7omvXu z+ubzUxZRHepWtw_7D3=Uw5{LEHE_-iZ2sMd3IJ6)Y;$bPT>u0#E{*^Xv0n}_lQCn{ z)0L*)l9{ZU@hxxCEYqT{RccYQ(|u8OfCGp5mfk`A_V^8#vF1&F5?kCb5sT=!=?Vy|g`9Rp%P zkCA4caagNmjy_U?s{KTJXVe|&%#Joje#z=T5DaSYnmb}|>!@lFUK|ICu?=MB6$D4W zgagwnk%^4k4Wf&R`2#7JwXSZA203I|wUN^DFafkTdeJI&_n>PIX6trO9^ak;4gf7W z?7%c;15~u(wDd(I{w&_~BzxKFPi6=584eHcVa~G)^GskA!nX;@40b(kS6SgIU(4l# zg4xoy8+*H_pkT&0(=gp~hXCG6QHP)aK!|5;TzjjO7;YFV48@jRc0c@#E3xeuOCTBJ zn@Ym%*pNC!-=98JSZvopigvZRlP-H5?=OiU!_pplB62`V`NZ@NFPvMW_f!wr$h9NwVT#uiuK=v;0i>Nak@SjQ<9BD4Q{h z#RH^pu(mZi&dgqir+RBWkwbW#$^%iZid%v6M^W&4Qp zH?Ci&TZiDgwGUlA1w@ZyzC;bSZ+EN7-t9|BQXp*rOo|ze=3D#A z{?K#9b=TRQdhM{ch!ZfXt%RO62`p%cd}5!4JQ!dTewW$Lj(sH_xpB^VQ(dPj?Q3Y1 zcnmM|M2zsy$A#|cHxq>#*q1X~tvMsV6z%H~5RRIQksJ2lAnps~E9|{u5YAdQ(_s4* zaX`6gvs~xlIZt&127oF2SuOs5IyQcLqTTGIq*9bu;dy}s=)|goy(btE7pW(tZ1*3e zzxj)|&w#gAR`0*^gQ$UAT`>KF5sp6IX7h&1Se>0jHhtFRmH1igTH2uLR{t}4wfI6s zo+v?HDzgP1g2joIJH;pTSTQyz3*|kc$pT-<-TeC!u7e%AF3WLBr@9-R(DP-7b5rpN zbcD(XPkd}wP0YU>@WjK6rb6`c;%I{%*TAp^Rr?bMX1YDlU$rwlJTc05;~9wW>wrr+ zHb|gx*tIB{!*!5vj zSW2`t>xd-CM}=LqZ{AI;<;FMv-jpl8o*5F1Vn5_yHc1ZG{d+3`&u@fX2*i>MGWn0+ zKsy?o^mIHOaW zDC%FH1d=V$45O~;KsW}uOV1%c#2JaK)kN(!K6rTAqGhus&vh7wH+lHZlJ25rzWY<1 zWKD|Ig|`4&0n+W!g2@TkQ(qW&cmUkN0RG7LcA^`HCtq9=Z;>aOyfi87u{XL=Z35>k zOK7G+gmm4#G58x|@$_^0xTZ6E|5KV!NrLBn6i4dI;g^zo+`|R8Nq$9}i(3<3iYoNK zCV3Q;vuf)rr*%haG}I)F$xceL2;R2kJ`CYOxRpS}`P?f0J+RR_!)13tB{2boOYf0m=xq@T323pU)Re`OY;D>!n$g^(~ddyqI6^jOSO7lI`ZPnKnUE z>U~}B7YS@(H?LhHwtvHmR4HLs-D?AAE@m5VrY7}|1@|M9XT*CX~B`aD2GI#Sx`7RLs|w32VVyE}7e z*3npo+}4I`udnP|4Ne4a-~w7d}N)?~BH`J+j&S{ju-|o`7VESnKsvXPw z%T88NPp>K6m+Pj;!34gFw>`KQPH2EEC!ETE@DASSAFD7GjD%-}bVZlVlJrtK#Doau zrEe*AIZr;B8D<0EiKC7u#X+ie|8|0#zjX08tZxCVHQ+w3okA8^#hOJUjn!cvZ-*Mg zj52J|^=OzT3F1pU&2qH31Qu{)(I(h)iX1?DwP~8dO)%AKa`9*VnJ{NDKE+d-pq0*Hu2WfBB`uj&tUpK~5I)jpu zVI)>~i=sC3Hx^)ajB^a;m6N@ja*SnqKdFuXo@rtQ3dk2cWbpz8+IrB|QAu)xeA3SF z%N62D%jRv_)F<=Y7fEQBofmSlT2#InUo}6nr-Up>DNU!W&z20{49T)07eaB($q#Rc z|A>$Me#y2Fa8#WLUtJKc6FBtn6*?2gtEtG$yp^+Q%$LK(yrp+0P$I5`GsLy>=jm{V|fvV9!5tWr8jFjLeDU z*^flMZJZiT%09{$DxGuP-xHpz$G#BRqn00ZLCG!6(s|ConZ7LhDVI~iW4p`+_SSv*JFsX4Zi*FOPwoS92W~I1 zU>tTG1jO#}^<7Olq`*{;Tl@5aa@6Rs;$4a6Rth2rxTHL#EEMM9wAdAM5od&V$S7xh zv1P*}hstogKr$uwZ-^4PJV#sGS*ltyPBghh%hvcP690B*8})QA=l3trj`u2UhGbM6L`+;K=Cmph@03e; z#b?B_F4%P%1 z<$}Qi=jC{q>e3&0Z%#A{)yxD|k%P==bO=z5@rOY_5$Fb`-Al#8pcC%dAL&!Dc&^BR z{L`SDMNb|56C!PL(m4ivhS*&eW>|hzdfH#7bZW%J#qlL^v_w%-SsD^Xap_$MSes*t z*3FEVszwsbaH#;x^}cD1kII$hZuijhZlnejzK0OaAOVQH+P}-vp;)1uJ(C-Hhx|;0 zg7``_iB)tM1HF-9TXLo}0$j?_nmXA=-{Vtri_hb2-1theVcXBt z5Xn1YH=4+A!fvt^Txl$Fgk zwrVasc3~=XjM|I3-Ge-iOGWx*lywE%MABdXlIR1`ErNI|TpWB=h*M*Ggr-*v!E_d$ zn<2Oe9#{JAGgvMF2d>p(5Nmo@>negq?po>Q zUuQL&;)05zLBMkzl+UZWQ`VO!#2caobB)~$s57+l-KUq9e!Y9|x{>M|z}!!BNq5QN z{C0OdO-U7pBjt9}tB{x|N}qK-^$-ja&dv&S1vcenLmGKyD;M@!MW zZ>0=L&(ApMNSyt`c+N?7Mo%MNIf~peh;K`4Mp!i%{RuZj@~*PLt8c?9oKHR`t#^{7 zuSKQT#jG=ZDXMmrj*AfG9Ch+A9G7D{gWKD!7$v`Q@rd)(wyRb$YQFv1o3L9*)HGf7 z*>+=!wy$Js?T}%wsau$!gQLEFdw0;+QYkl|n`&ugehHP|$;bJ_VAM6bkhIQVh4GxH z@l4FE>4I<@ybL>y-*O)N0&2o$sL=i|*>AuY|)APMzi_E&qf)1ef;nMLJ^o{?8%twj?2e|Qp(_;i7tmJiV*D^$at6oVQ8fE{*G%Do z;Iqh*hOgpAzegS+Ysd` zaL)FY!|DAJzz* zdS}pQR43-V)f)uMwK5^&pc?I{J)UC!wc}=FvQJ2n7EF)hx`&=n;SU2xpisJP%YefQ z7FfRM$xA74;2sZJw0x0jS>l4Iy7xet1^_ZSH0jj2$C0~A78rdRV^Z4cm$%wFX|jV6 zOc}RUItGfRC-b39LYP-^lv)T*@!>@49ZDi!u+{i#=A?$OzM)%;WHsWiTd^KviE0T^ z!P#_1FyOa42(kH7&X5fUH8u?4`fO@JJrr44J8z0SLfrsye__o|_X*ex zb@`26zYBdFJK9X}p>r84{_Wz6G2}Q~U<0-+A45##18>{e%y<6|Ki@4-QcwTEH6*whzY&e9 zCl})q8u5gA|A=fKD?+@?iuD!=L5IPHSqJ1lF@PhJ`e2!XLaL~zCj_ZiIMx8}8h%Z6(Nyzuy14IQ7&a zFY9sihuYzS4bmePwT<`I32(9CQi26j$ogta>q`MdmTSLIgwcg1BBLAd_`u`+u7pQO zm#LzQ7s}7%U?fD%58}tc7UM~TVQ!!t-h}8vDiU(e99t=6hsUI`Huq++uAn$3lSlOd zaBluc6OJi|H5Ca&aRC=z*z(3YT>_)Y!l8W}d7?_>+9S`Eg$0X`pNHgqKl4ZNPTr|K z$AGg2U!IQ@X2m7?4Wn9uPsZ0m=|@$;*6KUls+#zeAZ*patM$gSFHYC0nhUoSNA2Z@ zcI17-*F+wmCJ~vj;ihwf&hNZ1SLg9RT)CHP&Y|NHa!6LG+bYe8SD%SBQ~|Yl;@kL2 z6SwpSzd0T=T%Qz(d1#B{`v8dodTtfjKr&$;`xu8A5g-M%=j<~gKcBI8Rwsl&wMFvb z-GfrH zF&wOlU5_jiB<|u?2XTp>alp&=tP>8_#<7PKsl$5zTqm9qyW-bu_?pi$m%1@Bj9IWK zx{%&wB{Y66l~<>fs|cB*ew^mD>tzO-TXp;vN8 ziB(w`*=Sl7^xjkaj^X<~NXIvmuNrPUi z!T-J`_a7?fJ@X!rzN^vokVIV|%lt~>PWJU!06GW=Ln{)=U5#p8a)i}u9B$UlmUxHSRvPy zrh@W7AwwmzpEryGHghZTPMGD>Sn0d{?+EnH9J$e0j43NpVUDqP--x3`iYi}Fvos&5 zZ za%eCW%LnboR1Rn6(9@t#t%Jv4)MW>>cst+9;xvebn1f@;@FUhFNUcIFhx^Xra)2uGTCES?nk?qic(W>&q$YnP;(E3i{?t2p{C9zx@)URpd#AN@ zEGcY+wlRzH0@Uxl;6^q*rbLLLPM&wtfgu>dC)1{aF#XQI`*zY(Nl}sL<-_dfFrWr_ z56X7v5%#`sOa^ZZ9u8^!$^j3_FVgj0Aj3B%hyL0^WC(3`#M-z(6xeqI_U_VoCxZgp zC#np6asrup+=LbfU5Rj{-d4QGn*>THb=ur{gt`o5zB$u?(w4y%DP6#0&ub8xw!Y}+Nooo1VN#L%6^2$@Dn*gJ8+kH;E4QZB5o3*Q2}zA=(}2BQGun*GNbA4R}kwB{`HiSEgQDj@KH zCm>8u-m22yz|p{Ue}r~77z9LuMQd793S9ewzkE*P@E%}_1aoyPweRL zd;Ib>xSUXQ%Z#qE8!S*;9B*Om=Fty@0nvkG)z&h`GOj%Jck$yu$>ZsS@j!Qwh_NTO zyBWzSdr$M+Q9!96cbwU~mG%-{DlSUf?t&UPT4a7 zTS5uuq3q{tic5$lD?jC*!p(q#t3yrj-)K=U-sT84puY65(I}QuD*0z$%JtQfovmzS zAjj?JJNb@z-vAk*TRJe8K*{J&c?;2h64r1gG#ZHu$ln+=DuPml*(Kee#Wy}Yi8PpD zCPul<%1|l%;MvpsxeM*Cqe0X*7Vjc<1R@r8kK*6n^*|AT+#y$NLjHwYYE7SHnhOTO zG$`Lx6Io=sZwZ-VMrMvCA_Zfo7^IC#m66fM%56_SHa0f=TIMb|)^M&)arp{UU}Oc2RRspo1egirgPFiEa?WzT`=pi(m7=!&92%>~ zc$y|2hbFU(S8)XcghV?={3RdAj?-G71g<5>PRlHvt_LI=Ypu^C{n#<=n7aInxbBk5 zyAqZ=eisbhbo{Kt`8F)1z;Zj#W3A#|dBaK<_5R*daQ#P-! zxaYWDgHZnA%9y?sgRNd5d+oi4U8Gom62}X~i?&Da%#b$X6jG`(O|H_dmO6utrE-LO(z)3c1ZP?|FQgceKyY*5luH z+De!32J~`JEtrUU;2-ONyGStlcLR$7f(w{h001;K#k4V%(?Yi}%I|Of3?feW?UeBk|gSKgWTPPku7w`Ntt^A=UF;+r@(A z$wjgOKv49{JRHBX#faF1Nv_mm zofhZ|*)jkmrLIonVzc$kb-dBTnO0?@?zwWNhg*JoR z3@NEy{DkD6xf8R;a5_P&Q(Z$hT|kswNKgA{5`eGdrZcJV4u_VXL6QA1x0};M)DjQe z(75CHLZ<>FtI6-ALyIG&V0%cmS{H;MGfow7whSQPQYcH}Bv??sIOH2Wg0CUFDSVR) z2b+H9lI@?DdoX=~Adf;cI=ck1qW8m;4jlziTo~}t1jRo}Tl1vXl;+XEAn~MQVXQwz zJDj?=v1t_CIv`PVa3Z%1d>m1ipvRM@!wDx3dLD(A52 z7vEwOP!TF)G20nWJG_(@j#v!)*6S#>t5dW<~bWZedlh9+zvlVD9W5 zx>e~fOaLf!8j3;8T@@~#QkAcgGC~@UHQ6shma(y|6HRy z(|aHwk!WIe9nwDudgw2U1gX54BXt_}$SgPMcAe4faD~;-rdPTo)T}sCI&3d@r`GX9Qe!UF`=M=A-RL;10MY1JWJ#>q9ZD2!aC;iJwaSOd+>#r##Xw z*Q+IB{wr)0#{D-~Mj(Uksl?5%vd1@i_{2O%(9&xCr2X+V=Cpk1ICH0DCehJ?}I;2UpUXj$nx=KNg1`zK7h^{0E?o z1=8)1%QbyDb|83m!UykNfUf%va;Ir$h5Uz$r?1;OaBIE2VqAA{g9)l@WpZ|xkRU9j zbC$~YJ4k3vr^=m|0Jq7hi!|^@f-v%(JJ?*fq@a@fG|LsFm9GDI1T4G*EI&+tWgEHI z!heW)x)Vt4%B8Z0hcJesK^eOKC^)o#1)M?{FaQ5+C=Xa=^Izy}0iXs{h@g4!uTU~D ze9VA~L2i7w1zIhQqjS_=q6wt<3Ey-LT)Pd~y9yl6J3csYKfLa9pzhXOq zboJod5GcuiN%wzBx{&Z6)$eMYzKtqzC+M)jMI2Jn^`Q8IZ{5G+B;8_t48 z%fk}##;IiYVTq%5=2T)J;8y;t6(eAQzuW_K z^RKoV6$}=GfpM7@c0C5KzdFg{QU4GYqbu}anB#_vMx)_ASO@x&D?juXZ|8vvf$&7j#E{|O7l6JMQstw+!Y4I!iW_23fU=U)udp|AgR8Oi6!jB- zyYNLZR^*l$T=!rNse>$E5b1eS32P#9~4|4;j&qOKklt<`=nRyQyf8^MGd**$!2Dv3pyB6`mmN>5% z6+Es!QLwfm%hM$8fT9jFawzQ(7v846Ehz3fdv7y$4w*3F^))6YekvjK?k+|)PV!XhLC?FfO);n-kmCa z7|N0EBBRsCml5>h{y+lWpz zdB?QU*`2YFFG5Ba&D0inGAUwcOymejUQ zIwp-`mNuARlif;**X+dcWm#$Qx|WsZ5Zp4uBK773g5-ToPW7@ZEwO8(L#CNn3R)@H zgxtz06%tHHMFde16z;J>>wW!r|DXRB=j^riTI;N7uWx~40PiH@M02NRlqJ< zYu*j89_{7+0pfsF+dsb=+PNY5{XJ;jbG- z-3p;jFC`d8-L*&Y-JZX98)YU3+NOg@4mH5>)6$N+|C}A!QWW15ed?!j?4kR%PlKy| zE$Ev*!pxFoDE>>$d%?bPQ&ro+d8R*Srvy?{#mk?|;hB;0csz`ioHM4@F}03a>sSyN zi>+V1U@W$drL3_^;D0NyqUTEIu5~g`n(8pKME=-g%G@J-_wB*Wy%8;zV zX~K9=XsDTtE=8pH9^bJ1amevBLdrEx<uH2kx6fpx?3tAcgGC>r9%T(?X>Iv(UB6bWF4G1LzO4+XpwfzQl!3DyqFcv>361Qi z{Xxv;TevqQZ`$swPPD)ruc+-@$IJKYx@6XJ7%N2|5dodVz6L$kaulCD@OECRJXa1I zI#IbyEHI+ZNujm8>$C2|8e=Wx6n$q{OqpV8p0L#;$WXK1e0fqY(N@rDy_|olt+GFB z;;8(A0rrO@&=#pO1-H z4Vv&udS0RMC_`MZ-gf#z0OeRW(U{U7zfPO{DOi1-B>YX6uCO3awmGqFK zGbx%-ywWLh@IxK@jj1XHUE0-Vj~5KQ6#3QpVAL}lxkmeqM#|!0K*b#9IeTc9w|>G9 zma2TPr{F_!UzJ?5R*Dvu=y+s2C(yjYhiQA}ugp#C_k`$qKcFKbHqzOXReHT`R{)1f zYO-ekK9E%$+1x*&EKaMuNX+cbtnm_lWc7BW)^y;NH|i>rqpdmR7jTt#CANtsjmAj_ z{2X4_%>Qz3`1ZXU0F5o*8*3EZaxt5r*tNFM#G59IetzPDI7u*gvQKWluVB6>RpzZ2 z@-}1AXe|epiO&*AmH_Q#@|JvmL3};Ay15`4ZZ@j9VG|u@)Euh}NN=8%60+ZREG=Meb-$rlY;Hz6RvUO(%KPY`3%rIVUQm+EV5ZROeEb*EhG7i!WpTm}7`U-#0Jm&-$|^`xdK?a4jL zi+PoV@wg(tA(@`CBM3`TBgodiwg#}gh28k8XdM?<`GlstbZF=fPm3JP+p~^Vgq7c$ zMz@@<)+Ketnc~Wp2!hL#*NN`OHe&N7?Hz2B1+IywC-YJpbSyte>iRj5=M$IMX9fV& z9@T^VgR*dul6W+p)mLwG@M?EmeoGg*QRs3^_X*i?rT8Vv(ZvdhuPs8s^;)7h9FQsU`PNQa>AsSkCf)$55n+$qhL z7%f4L)R5lUqd@=7kla=ZOyv28hF-YmyZ7l|?+KIH$WoM|q_(*c%DP}aIe^oCKvK!p zcKT=!*fGY@-C^kH5UOxIHmPWjs4;~)$sQiiR)ftnWdd@^>8*c@LZCUm~!gJsyy>AY| zUjX2%5D4ki`;tlIk=QE$&L;-q>2d z$hv9J7n_JEyKQe7z)^4O5VITux!#^q+1>vbF{R* zC%-w2>ePNtw}KS`%R?R0$6}L6O~Je4k!#l^Mpav34i@TLx}GAW8|idw>O9H5VuxB` zB+eqd^*Tl(MoKH0pBzn4D*yn{Ak^ndD~{m&pEdUnyPX>2)+lfl&-@&Y=r(5R=QLe- zXF(5mRs9*W*NHh%m27US2 zh}@Ax`~|oRHCT4>9e!Aes6wImD^Xs~KkoIbArO_75&1n$1nQhKm=ORLLL)`rKfeK$ z3p<^AMw0Vq02LbpR>vHI=77F`zIi4>diO-zX!0GF1)Km$VDQ(ZA6BByK^+zk!`Gwz zoX-(^3M9e!KWBbV69uAnff=_arCR@(GiF2HH^F7uw-oXPWlXVSiUr_f77NV>{a=Lw bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-26-33_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_17-45-20_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json deleted file mode 100644 index ecac483..0000000 --- a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_configuration.json +++ /dev/null @@ -1 +0,0 @@ -{"input_matrix": {"data": [[0.5, 0.3], [0.2, 0.6], [0.8, 0.1]], "columns": ["Criteria 1", "Criteria 2"], "index": ["A", "B", "C"]}, "polarity": ["+", "-"], "sensitivity_on": "no", "normalization": "minmax", "aggregation": "weighted_sum", "robustness_on": "no", "robustness_on_single_weights": "no", "robustness_on_all_weights": "no", "given_weights": [0.6, 0.4], "robustness_on_indicators": "no", "monte_carlo_runs": 1000, "num_cores": 2, "random_seed": 42, "marginal_distribution_for_each_indicator": "normal", "output_path": "mock_output/"} \ No newline at end of file diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv deleted file mode 100644 index f1ec464..0000000 --- a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_normalized_scores.csv +++ /dev/null @@ -1,4 +0,0 @@ -,ws-minmax_01 -A,0.5399999999999999 -B,0.0 -C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv deleted file mode 100644 index 3fa3f42..0000000 --- a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_ranks.csv +++ /dev/null @@ -1,4 +0,0 @@ -,ws-minmax_01 -A,0.6666666666666666 -B,0.3333333333333333 -C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv deleted file mode 100644 index f1ec464..0000000 --- a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-57_scores.csv +++ /dev/null @@ -1,4 +0,0 @@ -,ws-minmax_01 -A,0.5399999999999999 -B,0.0 -C,1.0 diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_norm_scores.png deleted file mode 100644 index 5b53e468d5e2b3ecb2b9b1e7f9d359a362758b6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31035 zcmeFaWmuKl`Y$XcrGyBmNOub=NXZ10E=g&F5Ger>=?N&PC?O?MQ)wwBrBOsWBqb)O zbSoVK@BJ)vE!Nt5pZ_@@&UKyZdiMuqt;u}GGsfNVyN3@o)fEntoFF-H;J{%e#S2;o z4&Y55IDmVZ=pcOZgQ1!IzyYQMN*82w?wZbG-O4q_hIW6n6}5EA2Sji^x*_h8CKG;+ zoh5_l;`30gqr}A2?AOQZLmMNWGHZpMQ@rp0^4t*xVs>H6{;ImdR)>XESMpWYxx$>n zp2D7)tvavaSr?Pu33u-(=fpMB8c}93F)@R6CiDSZqWyoNb8(6BZ<}o!oW#MWKpoik zmkJu)`tYA$M;*W?n#8$*%f5dNWJwfi;^f~Rht-%4;NmYIIOBWd?+fEoFkzg@{<#3f z0UTWS1J6;!f7>1`P0=n!_|FB1&DgSmcoUTkfW%$aQ^ulT=9v2knR934xEG#Lmcr>DjmRycivA*WNF_6a1y+BeC2;q z3CJw=&jtQV=KZw(KP2-#9!#R7n{ws$)yz0?7oJEF{TyQ_>+VNSLqkJ-ws?J{pMecFPR^#%C(<8hb)Qrv-@G{B+-D$qqb~UQ z8I^#L#2CY#>-=<$RODwDQ<}wF#G8WSJ+ykgOPXpr(&rCXcD;YOUtq5!pwe+SE4K;9 zN&_^^C*Q~7i8mGMn?`(HNNX`<6C_JGy0k_>IvSlj5z<-^ZkBbKw}MbD!o*{zKCd&{ z%+ptLusmVVc*JSu*~O+F#jW{VD-TO!FE?H5K}_nPCwAS zl^vJtOIP!3>jKBnFGhxY+4Z-pZB;FSdT4NV)KaCCwzWZN{N#G=}u_N$s?8UEy^7vbWQRYGm-2Pu&ia z+HWe*M;=^2>CN=0^Ih8x}L%e|@C)Kt~ z!qGV#8OCqoR6Q#)*{HcArk?xIO9=Fe+A`)__Txpj9S2ZumjPnR4xZ-pict zwEV`W#xJ{w)`SfBgsDl-hnje-HZpFJHC9TKEAM!^CEtDXvd4M5FsF>A^L4B)YItW! zps(n<{=;neNAIFnG3w^i(=b;{>F zx5=xwabA{68+8{a>Q0v3siE0es(#V+hE`WhuqK{@Q#~OfGE>xWfqh+f``W~$nb!2< zYg(!1jwN5p^Rw5EefDDXUD`c%<=}q1PUMF&$1Qzt`g4bZ_fYn(cjg(p0SWEhWt{B> z204qb9o<58WL{HB8D2Hi$3JG$7FUQ`y#bo52dua?Zn|0OPwS!tLw6S@n!AHW`|liQ zn=V-_NZgTP2zM{cZG9c=o@&w(yLjnrT7_Tk`|XIZ<31UoSYpwwaEp$@h0mP~sXgwg zc2w5=ul1%Krzz3XE(0GP&-}cseO%~A%_04b?(8iGGEco%I^vuw>$^L7M)o#4ws*!1 z+ML%WKMQkITT-`6n^npuOww%4?k@JRUX!hKe-Z2c#;CuoF5E2Kxs`|CXI7dimn@Ob zS=z+0zf>pD#$>H(?Qps)Zt~pK%{82K1-5|4@nK)zJSrefFXOZf_7O88CEM>te7_T4 z;K4`_O{-h?POH1ErhHob^j`4voaH9kG}E!}S@io1-kRwY=O5nRh$n9fxTI`8-+a>+ zJbbssLU_76SV(1ir!LXydJ^aL9rf-d#k3tEAJK()@0STy#Y+!4F=@?d2~+xQtC<>> zINP^pUYRkeGW_tIj$Ii!JbANKVIfnHo}q%%Wit!6wDg+UOahsZpxw-dDz9}1#&Y`d z%4a`!Pv)`k#T>uub-_(iA*&JmG38#;+m)tsJ%%R+4Qn=MItiBDNcEK`qU9|Hf`wBa zHN9LrXytK|aKGlG*70Q-+jS?kisLgwNQSi)Rzf8VT|$U0xTQ73IL7$r3CZchG97&Q zQfD-zoQex7t7HbPWXo}+^3l--m$d~0v;`liY@`=|tU6s@M$OgfZDHIf$ofq{=YkgQ z=3HTJSd)ObS!1G!VOQ)AT}`hw|F&y60cl?5OqpoY99wQ~y@J;(-L6y8q{DPA-5g)k(p&! ztYq10#)E%b!iuks<*zt_Lcq73aQV}ALXkZqysm=Yn4mr|c37Z-Q2p_Sn=(qjOiCJ1|{Q}3S`@EN4f`?U|;4|YMh@_Xeo1>A+t`HpBdHjKEt!}xs%}b%1L+ZR_TXk zG1m@WC)16*p8QqoVSbE##qQ5-=AOBwB+iziA=1#rOVV^h#s0=lS8H`NRtA5*_sNc0 zYD|pTTzr?6gJG2Rmfdk|Vij^p&-M3sZEeDxu)H>~={mn@66LkcZ`MMyRwY1o;V;w8 zWQ_ZQeA29ij-Om|ML8h)b%w|tk45hpgYOKvPuk94GH>0T@NcWTEjdTsc~_l40;ggE zO)PwUo>r(c_3&g&cle@l1&Ql!x#()Rf6x*v(4BTe%^Cm*Rd%rowjq`EDl1%1yHx(r>o$*(V?W6-1%lBFfX< z{aqtE>#2RNx#va!Qk4*+dHMW;{YdEaj=-ltPyh_Z>Y}7a}y<8wDq!LqSkJ9jwaO+3s z*eChqEYM|yu$b#-#-kH<`W#%&dVYG$td(Yv_RN(ueWJ2^ZfrU!XL5IGDDAp;f`Qr^ z)+I=C@b+nfVy(Cs+ok(=`}! z^$X1>rZ*Pnb1P$}^navCX?kHz^CfJId|XP=IkLMSrj{(ajPmn;?zo=~6gCrb#nf9}2`J~z=2xDX}6FI>r`j-AFj zd@$eeURt%SB9xlz&C0M!S@8Hd_DnQ5b0xLvlZwDAr<}q9caaj837n#~>a5CF{M#3% zXWr>lt8Q@R6KHRy4JqeTj!4Yqf3YW6Zcb=p5dCg26SWZ0cJZuHZNyftL5)@-wsoGy z^{l2;Y;MA*w&|oHzr@v7g{96c4he%j`MXBN3k9;JrP$rrLg^2KUoqneV__9OfU0w%J~v!PUXVJ zUDC=v6PewO-r>=n)Og`D`W0OBz8u8{!!Z@RZT@-Jx(~WJFB;DjO%@1hHqDZhl3bY{ zvmqp-4|Ay6y_D+tma9g$Tf(8tbW?I|EvPnOMnZnvGf%RR%q4S`U?Q?fGumC3-d?da~N0!EiHul4nq44;&bnP%p3 z0+`(**Q%AgQnHXC{wLEzS4`M7Hq2Ll?!1mpj4VhZ_^G{7OO3&R?TNG zT~|x#t|+d}dVyQIX(+aGsW3frgI$Hqpvu$W_4>>+vsB5hf&|IVBBdgn?bNaaWfM7U zrH_5dX?A9PKhLdgy^YP`;kvBdTEbkTnIHQjY2O>f6z%zL2TkJ1rVo#dqoxLQ3*bnO~esouUq z-nVEQ!X-ZGp`@{Kg060#OB-7W_ow?WIVoPDD|f4zGd_L!@l3#S?PKI}Esz}MDOWODFhIi*SWS?8ZkeF@mA`99u?gR1r4 zMOF-fGcGnJtnp?iBE=Q@#k!rl^@n=IEq4lrb#uH!cSwphy*KG{D!Wc3ftCv2kq?;o z(0yY1qW4tptYGX}<45e&MIT=cv~*T}p9>o=R;lZGa(Z^9{cDnwZbgxhkJ)aNO^Vr? z$noH9r6)49{0>=~=s5Wj`}}n!ez!-j*o3qM&Co9Xh@3XF}ot0`&M^X^p~;Q zrXQLA3bo+B>_J|!Pt6mmNz!?jdPN_537BEUhs!_hR#wH4@7Gc$3VbMIQIw(%8xnA+ z#tY)fYXljvW=79i^WSTr{9DZ4iBz_vWCby%uh(YgD>Alp1joy!`kQ2{97G=McX^iZ z@|J1Z?tRf8vblRYP}b`Bn8&|ASAsX>!_h2Pf^PopZ8-R{JW$xOzTQXkx9y_Q22cje z-@I73zX$=#GgC8R0t?>QlI~Xw3RE>84sNNz_DjRRztlIBgrZ%p|8mj&{TnA@x)OB& z2b<-6iU02}MJW;xO)}f@4g8HK_!Ot0=4h{1l}_{bmogA$rD+LWyRY z=>+}#r4(Ag0!6x}qJLBkn~yMIV!H)j{jGR`PeGIcwZ!Bn7DoS|z<+M~%Q@}^Hf@YrP$77WyQwV| zpj0E?t5oUfHrCkmN-_R#f@UBv}!jAFW} zZ3cX1ZH#u?p6zs<7K0IP8Fi+H*-w`jKIt_^@?S)`x~~c`>|-(`*uL}M@^7aijK0%$ zKgowaT&ijzoaH`=@|)!MLNz6R_WI!?`>0M702b<)7v@nFJWAGf5Vuqg6~OwJgZdH< zR+jT_REriGJ^y`_2_u1Ter_Lu&VvK-^0P9)EzNO8qjOZE-1aTr&Wl3z&NR)g61?@Z zALg^f$H6i))$IGicctnXwD+`5Ue@l{w1wE9SUp9wpt>_lgC7Ij(Vg*Af4AAo`HPUw*ur6TFf4 zI#P&@*30H(&T%Ad9OJkom?{kM}SKO;)lg{T2 z1ez*0^q}u@^eLeSc!N z{{gqtj+<4J$%=hM#2clHBn`6~QBC(JqKo~$cSq|N279@zo6p=?=O6HnO5yEA+6-)M zQbWcc)f0Gwb_(TlN`K0_Eq*OaQBH9-AKvj_yxjY4F)>@(=(->zk+k z;`2Nf%GA;KI|&=0kur)XdY(az#f*>Zqte4(nz?th-n=)+8z0!Me+ad2C=bM}+I%Yr zN!d-$SiF_*?u6Bit}kQsXKgemdsFW{^h9VlRRKt7w;A`L&He9 zBlRMl(($b;ouy9O@YK(XykljI-cOHT_b{@NS{=NyVL3;>csur^(~y`&3RH~rvO|XS zhe>9_hm5XlUqfwoT;27R6L#Lw0*a7kF-dthN^UrAsoUg0pP|v8-ZXNsgleqCH{N10 zcEL#heOj=$3<*uWPyf=b&A3`&$9~V4HT9~lk$zVnlUAv;EF{yK9-ix83hcT$7=~sj z@ae{1#<<*b-)CzdXfQS7Y=3?;;Hk|ehRm2 zYW^W;q+iK6)nz8X<{_U`k!RSf($*yeU6aC7LG08~fzNyLWml^zO$8muCT+L3^Kz>q zd*TV}U4u9ceTH@?m@^#fM)az6^+!4hmkX|U&n-Yc966S+ps4jpRu5J6?I~#%D(Af1 z$JWpmBiH%|mGoKCK9!zN%D?yDHQ4-mYNl=a&aw+#alOWXYvblZv7xV23K=z~dvN;I zmcC78^pwML#8P8)%j$=_&Wj3*KPJplb~;dpHQSPC1uj>YYBP<=q1vK467h&8ci!gn z>@#=j1Ryt&Pe@vOcZM(KZAW8!vWH{62btM}Xe0dtgB7n`d+BoVGInRX@q2@?T@Oo; zwh6Jm{_F0g9!S}*(`TNa?pcA}R|7@6c1~9s{zz3YeQ{2YHKn-(Zxx@4}mtlbX1 zzi>Fu{-OqBdtLcyg&zCQPfxU08CS?KcyFHNy_ab;z&N}9%H?9krnJD(!kp4VIGpYh z;Zl@hNbg4^6&r(;tj?wM7J-yN>X-YiAI5)U0T?cz$n71{5B=pad^?HnNntdeiJMh! zuC4XhjW}~wut?i)T{Jx*tn}z8&kd<9ubS@Y)Wvk(;pv;p& zJtL|TwCbhh|0Wj4YqrdJY&-d} z>>Mtc-M{nY%hefsoE4RT+4J0OnBA>TQSGV|IqLbX9eF>pGukkjVsHHt;*&L`IWb%K zZF&1l|GA??9w_}{ugp+)>q2E=3v=?BzS$Fp8H(+Vpv0AYl;2nr(q~Q|21!0>ILRJy zX0Kn`{;2fzJ;B)hVE)tHhm}!`LRV8gD}N3n4T_%AOROB;$DD(7&! z;=j61)MvcXQg=_g?Yc22qdlcr8@$P#X;dj{w~?#rk(9PFA*gQ`y4psr=GF7FbedKr z*UHdh13N^O=EK)iwWIfOdBOaNh7W<%Sl?O%)MH6|1TqXS84eGlIzq5ZTjw*}TnP=9 zhpx;J+?l3X+bt__nx31S%Ejaw=U+;gTrKsK@+p`dtkCZooaQOr9K5x{-V;0A%|Cy7 z+?#otWNWr~)6gMCzi#LUO7xOon#~%j#(>xOv+GU?(H`Y{^UiI3=PNxKO zU&>OPcK*?)p3s5=zS8es{iW|~!sWXL;o;$9YtlOfL$^#{qblzP5`24W=Dn>36@!Om z_AX^x8QKjoy$MV&D{+^fD5Ibf@oRDa;)I5gvFHOR!fHf;Bo9)feypyXf z*W@8?!4=+*d`X1l`TVY_GBWeoUo`9vI`Ih$O z1Q>k?411&d=16Z5U@HxpiT)E^|8K>ZdvvwrhG6@y(|=+MAC5_{7=)|rP38NH%_Nj6 zW>zfjY~dQq6g$wI_WMtC%|8mEv8334VoMY{XXYd<_0+<2AK8|nvazx$T;_$7gBcJF zI{qK&Y5-rPPdNFX*rI~!bs?i@cWDq7+~=#@L5khIVz8A5^xNlRYajoo*!{oa{-5aj zUvdAhxc|xs|7+d%rK z%FX!aw-5Lp#Q_|yY&%40j(5y=UPgBKbm`!_$C;iC_$$mpwS!ZYSaZ99Dy#Uy4h8nMaf31DSyoR!4#5~r!%wqX_mr%oJv zTxc(ZOg&z-yQM|2M5vp@3y zN{Fr$UQgNfAw3@&TEaj@5SrI9`}{VOA26FpB%Xf&LeYV5-@Z^lVUk`Xe4himpb49J zmzr;31nV0apFGKegvypj{)*Fmi6G8m{d~K~7HJj*1^LUdLCpABbzi_rAp;E{{Mgng zJ6PY&(ec&;*fd$J(!Pk8g$sClrL!-OAJ*p=AR`NdO^4&uqFBSQgDOZSZkWNaO z(g_0QJEGUjR?seip6jrE##Msl>;YHh?I;tA)eq;jrUvu1Mst`qG=^^~-r z1wZkxZ1sVd=Cgl8hmycr2#8v|$es@Y4;#8Ngq~<$o0njkwmSWBH45OgrxGI3_MW7D zzie0n)YGLntz>FIYy-|db~2%Ar)ftjb*)^cI-4#IR8;K~-lejo%WiU&aS~dOjtWfn z%l~}-06iZqIkT74T>+be9UQt;A__-yh%;cni`$R8nWj=1MkaVDdi=@Frp9i%6w4<* ziVv?{L~5f>6L!5Si4zRO3G!q-ESQM{Pndt16=TrIA5`c$D`*t7MLoci<-zb{cyp(m zX;gN@w8y@OC1rRP_g2c6y53-hBJ+K}2sFbZkv!r(KO_msnMY&GwSz#8!?q{b%UIy) z1*l>EbeVk?XYlIY?D~o*Z*sIme#B7~o>}FuGBNki3=!&@gnRTH2V>MSMIE`x3b}Dz zyzVwF&?0gEtZWzQSAgQR?wZ`pg4C5VI%BV8@n<(T2Y6|cWX@89UqGAqQ=5#IE zKjGi@e||Azk0$sR;M<|{n(|k`M)=wtzHdzfyQ-^{&8q%Xh<-&3!A8X}dkJicNDxsn>;ThhwhT7>Gnq;qjd`b;e$*?ZH*9JD# z`z?$1D56s9Kd3sKtMOwBuNX9&@<#Bo%9+{fdD|e5g2zq=W3YpM8aJ9 zI@2Kz4B-pXuDy=fpNNJbP$R&C*Ipq4V!1qbiIT9-W)6}5%w?HoRJ6^)Q(h&N6>IFM zGaXY6TjV?JW}X`=58$#yUt34a zBEpjMYQX19sA}SNiS1yyN$|;Sl`gtLxNjq6`3{sfE$`pV~ZPGa_i3vDdH7v34Voc-@q!?In;)ILSlscg;~Q3MyRS{HHVVb z#-QYyq_q#{9I%zz)5Wtl30z&8%BC1|FtI~2(|fJHY7Y>|wehSQzrZBfh>n(S@)Qo( zS1R`xmYz=SXDnWbILKetxaClJ!%L%d3QCJp^vT|PT2T%!_i$)^^c&Do$_cx7omvXu z+ubzUxZRHepWtw_7D3=Uw5{LEHE_-iZ2sMd3IJ6)Y;$bPT>u0#E{*^Xv0n}_lQCn{ z)0L*)l9{ZU@hxxCEYqT{RccYQ(|u8OfCGp5mfk`A_V^8#vF1&F5?kCb5sT=!=?Vy|g`9Rp%P zkCA4caagNmjy_U?s{KTJXVe|&%#Joje#z=T5DaSYnmb}|>!@lFUK|ICu?=MB6$D4W zgagwnk%^4k4Wf&R`2#7JwXSZA203I|wUN^DFafkTdeJI&_n>PIX6trO9^ak;4gf7W z?7%c;15~u(wDd(I{w&_~BzxKFPi6=584eHcVa~G)^GskA!nX;@40b(kS6SgIU(4l# zg4xoy8+*H_pkT&0(=gp~hXCG6QHP)aK!|5;TzjjO7;YFV48@jRc0c@#E3xeuOCTBJ zn@Ym%*pNC!-=98JSZvopigvZRlP-H5?=OiU!_pplB62`V`NZ@NFPvMW_f!wr$h9NwVT#uiuK=v;0i>Nak@SjQ<9BD4Q{h z#RH^pu(mZi&dgqir+RBWkwbW#$^%iZid%v6M^W&4Qp zH?Ci&TZiDgwGUlA1w@ZyzC;bSZ+EN7-t9|BQXp*rOo|ze=3D#A z{?K#9b=TRQdhM{ch!ZfXt%RO62`p%cd}5!4JQ!dTewW$Lj(sH_xpB^VQ(dPj?Q3Y1 zcnmM|M2zsy$A#|cHxq>#*q1X~tvMsV6z%H~5RRIQksJ2lAnps~E9|{u5YAdQ(_s4* zaX`6gvs~xlIZt&127oF2SuOs5IyQcLqTTGIq*9bu;dy}s=)|goy(btE7pW(tZ1*3e zzxj)|&w#gAR`0*^gQ$UAT`>KF5sp6IX7h&1Se>0jHhtFRmH1igTH2uLR{t}4wfI6s zo+v?HDzgP1g2joIJH;pTSTQyz3*|kc$pT-<-TeC!u7e%AF3WLBr@9-R(DP-7b5rpN zbcD(XPkd}wP0YU>@WjK6rb6`c;%I{%*TAp^Rr?bMX1YDlU$rwlJTc05;~9wW>wrr+ zHb|gx*tIB{!*!5vj zSW2`t>xd-CM}=LqZ{AI;<;FMv-jpl8o*5F1Vn5_yHc1ZG{d+3`&u@fX2*i>MGWn0+ zKsy?o^mIHOaW zDC%FH1d=V$45O~;KsW}uOV1%c#2JaK)kN(!K6rTAqGhus&vh7wH+lHZlJ25rzWY<1 zWKD|Ig|`4&0n+W!g2@TkQ(qW&cmUkN0RG7LcA^`HCtq9=Z;>aOyfi87u{XL=Z35>k zOK7G+gmm4#G58x|@$_^0xTZ6E|5KV!NrLBn6i4dI;g^zo+`|R8Nq$9}i(3<3iYoNK zCV3Q;vuf)rr*%haG}I)F$xceL2;R2kJ`CYOxRpS}`P?f0J+RR_!)13tB{2boOYf0m=xq@T323pU)Re`OY;D>!n$g^(~ddyqI6^jOSO7lI`ZPnKnUE z>U~}B7YS@(H?LhHwtvHmR4HLs-D?AAE@m5VrY7}|1@|M9XT*CX~B`aD2GI#Sx`7RLs|w32VVyE}7e z*3npo+}4I`udnP|4Ne4a-~w7d}N)?~BH`J+j&S{ju-|o`7VESnKsvXPw z%T88NPp>K6m+Pj;!34gFw>`KQPH2EEC!ETE@DASSAFD7GjD%-}bVZlVlJrtK#Doau zrEe*AIZr;B8D<0EiKC7u#X+ie|8|0#zjX08tZxCVHQ+w3okA8^#hOJUjn!cvZ-*Mg zj52J|^=OzT3F1pU&2qH31Qu{)(I(h)iX1?DwP~8dO)%AKa`9*VnJ{NDKE+d-pq0*Hu2WfBB`uj&tUpK~5I)jpu zVI)>~i=sC3Hx^)ajB^a;m6N@ja*SnqKdFuXo@rtQ3dk2cWbpz8+IrB|QAu)xeA3SF z%N62D%jRv_)F<=Y7fEQBofmSlT2#InUo}6nr-Up>DNU!W&z20{49T)07eaB($q#Rc z|A>$Me#y2Fa8#WLUtJKc6FBtn6*?2gtEtG$yp^+Q%$LK(yrp+0P$I5`GsLy>=jm{V|fvV9!5tWr8jFjLeDU z*^flMZJZiT%09{$DxGuP-xHpz$G#BRqn00ZLCG!6(s|ConZ7LhDVI~iW4p`+_SSv*JFsX4Zi*FOPwoS92W~I1 zU>tTG1jO#}^<7Olq`*{;Tl@5aa@6Rs;$4a6Rth2rxTHL#EEMM9wAdAM5od&V$S7xh zv1P*}hstogKr$uwZ-^4PJV#sGS*ltyPBghh%hvcP690B*8})QA=l3trj`u2UhGbM6L`+;K=Cmph@03e; z#b?B_F4%P%1 z<$}Qi=jC{q>e3&0Z%#A{)yxD|k%P==bO=z5@rOY_5$Fb`-Al#8pcC%dAL&!Dc&^BR z{L`SDMNb|56C!PL(m4ivhS*&eW>|hzdfH#7bZW%J#qlL^v_w%-SsD^Xap_$MSes*t z*3FEVszwsbaH#;x^}cD1kII$hZuijhZlnejzK0OaAOVQH+P}-vp;)1uJ(C-Hhx|;0 zg7``_iB)tM1HF-9TXLo}0$j?_nmXA=-{Vtri_hb2-1theVcXBt z5Xn1YH=4+A!fvt^Txl$Fgk zwrVasc3~=XjM|I3-Ge-iOGWx*lywE%MABdXlIR1`ErNI|TpWB=h*M*Ggr-*v!E_d$ zn<2Oe9#{JAGgvMF2d>p(5Nmo@>negq?po>Q zUuQL&;)05zLBMkzl+UZWQ`VO!#2caobB)~$s57+l-KUq9e!Y9|x{>M|z}!!BNq5QN z{C0OdO-U7pBjt9}tB{x|N}qK-^$-ja&dv&S1vcenLmGKyD;M@!MW zZ>0=L&(ApMNSyt`c+N?7Mo%MNIf~peh;K`4Mp!i%{RuZj@~*PLt8c?9oKHR`t#^{7 zuSKQT#jG=ZDXMmrj*AfG9Ch+A9G7D{gWKD!7$v`Q@rd)(wyRb$YQFv1o3L9*)HGf7 z*>+=!wy$Js?T}%wsau$!gQLEFdw0;+QYkl|n`&ugehHP|$;bJ_VAM6bkhIQVh4GxH z@l4FE>4I<@ybL>y-*O)N0&2o$sL=i|*>AuY|)APMzi_E&qf)1ef;nMLJ^o{?8%twj?2e|Qp(_;i7tmJiV*D^$at6oVQ8fE{*G%Do z;Iqh*hOgpAzegS+Ysd` zaL)FY!|DAJzz* zdS}pQR43-V)f)uMwK5^&pc?I{J)UC!wc}=FvQJ2n7EF)hx`&=n;SU2xpisJP%YefQ z7FfRM$xA74;2sZJw0x0jS>l4Iy7xet1^_ZSH0jj2$C0~A78rdRV^Z4cm$%wFX|jV6 zOc}RUItGfRC-b39LYP-^lv)T*@!>@49ZDi!u+{i#=A?$OzM)%;WHsWiTd^KviE0T^ z!P#_1FyOa42(kH7&X5fUH8u?4`fO@JJrr44J8z0SLfrsye__o|_X*ex zb@`26zYBdFJK9X}p>r84{_Wz6G2}Q~U<0-+A45##18>{e%y<6|Ki@4-QcwTEH6*whzY&e9 zCl})q8u5gA|A=fKD?+@?iuD!=L5IPHSqJ1lF@PhJ`e2!XLaL~zCj_ZiIMx8}8h%Z6(Nyzuy14IQ7&a zFY9sihuYzS4bmePwT<`I32(9CQi26j$ogta>q`MdmTSLIgwcg1BBLAd_`u`+u7pQO zm#LzQ7s}7%U?fD%58}tc7UM~TVQ!!t-h}8vDiU(e99t=6hsUI`Huq++uAn$3lSlOd zaBluc6OJi|H5Ca&aRC=z*z(3YT>_)Y!l8W}d7?_>+9S`Eg$0X`pNHgqKl4ZNPTr|K z$AGg2U!IQ@X2m7?4Wn9uPsZ0m=|@$;*6KUls+#zeAZ*patM$gSFHYC0nhUoSNA2Z@ zcI17-*F+wmCJ~vj;ihwf&hNZ1SLg9RT)CHP&Y|NHa!6LG+bYe8SD%SBQ~|Yl;@kL2 z6SwpSzd0T=T%Qz(d1#B{`v8dodTtfjKr&$;`xu8A5g-M%=j<~gKcBI8Rwsl&wMFvb z-GfrH zF&wOlU5_jiB<|u?2XTp>alp&=tP>8_#<7PKsl$5zTqm9qyW-bu_?pi$m%1@Bj9IWK zx{%&wB{Y66l~<>fs|cB*ew^mD>tzO-TXp;vN8 ziB(w`*=Sl7^xjkaj^X<~NXIvmuNrPUi z!T-J`_a7?fJ@X!rzN^vokVIV|%lt~>PWJU!06GW=Ln{)=U5#p8a)i}u9B$UlmUxHSRvPy zrh@W7AwwmzpEryGHghZTPMGD>Sn0d{?+EnH9J$e0j43NpVUDqP--x3`iYi}Fvos&5 zZ za%eCW%LnboR1Rn6(9@t#t%Jv4)MW>>cst+9;xvebn1f@;@FUhFNUcIFhx^Xra)2uGTCES?nk?qic(W>&q$YnP;(E3i{?t2p{C9zx@)URpd#AN@ zEGcY+wlRzH0@Uxl;6^q*rbLLLPM&wtfgu>dC)1{aF#XQI`*zY(Nl}sL<-_dfFrWr_ z56X7v5%#`sOa^ZZ9u8^!$^j3_FVgj0Aj3B%hyL0^WC(3`#M-z(6xeqI_U_VoCxZgp zC#np6asrup+=LbfU5Rj{-d4QGn*>THb=ur{gt`o5zB$u?(w4y%DP6#0&ub8xw!Y}+Nooo1VN#L%6^2$@Dn*gJ8+kH;E4QZB5o3*Q2}zA=(}2BQGun*GNbA4R}kwB{`HiSEgQDj@KH zCm>8u-m22yz|p{Ue}r~77z9LuMQd793S9ewzkE*P@E%}_1aoyPweRL zd;Ib>xSUXQ%Z#qE8!S*;9B*Om=Fty@0nvkG)z&h`GOj%Jck$yu$>ZsS@j!Qwh_NTO zyBWzSdr$M+Q9!96cbwU~mG%-{DlSUf?t&UPT4a7 zTS5uuq3q{tic5$lD?jC*!p(q#t3yrj-)K=U-sT84puY65(I}QuD*0z$%JtQfovmzS zAjj?JJNb@z-vAk*TRJe8K*{J&c?;2h64r1gG#ZHu$ln+=DuPml*(Kee#Wy}Yi8PpD zCPul<%1|l%;MvpsxeM*Cqe0X*7Vjc<1R@r8kK*6n^*|AT+#y$NLjHwYYE7SHnhOTO zG$`Lx6Io=sZwZ-VMrMvCA_Zfo7^IC#m66fM%56_SHa0f=TIMb|)^M&)arp{UU}Oc2RRspo1egirgPFiEa?WzT`=pi(m7=!&92%>~ zc$y|2hbFU(S8)XcghV?={3RdAj?-G71g<5>PRlHvt_LI=Ypu^C{n#<=n7aInxbBk5 zyAqZ=eisbhbo{Kt`8F)1z;Zj#W3A#|dBaK<_5R*daQ#P-! zxaYWDgHZnA%9y?sgRNd5d+oi4U8Gom62}X~i?&Da%#b$X6jG`(O|H_dmO6utrE-LO(z)3c1ZP?|FQgceKyY*5luH z+De!32J~`JEtrUU;2-ONyGStlcLR$7f(w{h001;K#k4V%(?Yi}%I|Of3?feW?UeBk|gSKgWTPPku7w`Ntt^A=UF;+r@(A z$wjgOKv49{JRHBX#faF1Nv_mm zofhZ|*)jkmrLIonVzc$kb-dBTnO0?@?zwWNhg*JoR z3@NEy{DkD6xf8R;a5_P&Q(Z$hT|kswNKgA{5`eGdrZcJV4u_VXL6QA1x0};M)DjQe z(75CHLZ<>FtI6-ALyIG&V0%cmS{H;MGfow7whSQPQYcH}Bv??sIOH2Wg0CUFDSVR) z2b+H9lI@?DdoX=~Adf;cI=ck1qW8m;4jlziTo~}t1jRo}Tl1vXl;+XEAn~MQVXQwz zJDj?=v1t_CIv`PVa3Z%1d>m1ipvRM@!wDx3dLD(A52 z7vEwOP!TF)G20nWJG_(@j#v!)*6S#>t5dW<~bWZedlh9+zvlVD9W5 zx>e~fOaLf!8j3;8T@@~#QkAcgGC~@UHQ6shma(y|6HRy z(|aHwk!WIe9nwDudgw2U1gX54BXt_}$SgPMcAe4faD~;-rdPTo)T}sCI&3d@r`GX9Qe!UF`=M=A-RL;10MY1JWJ#>q9ZD2!aC;iJwaSOd+>#r##Xw z*Q+IB{wr)0#{D-~Mj(Uksl?5%vd1@i_{2O%(9&xCr2X+V=Cpk1ICH0DCehJ?}I;2UpUXj$nx=KNg1`zK7h^{0E?o z1=8)1%QbyDb|83m!UykNfUf%va;Ir$h5Uz$r?1;OaBIE2VqAA{g9)l@WpZ|xkRU9j zbC$~YJ4k3vr^=m|0Jq7hi!|^@f-v%(JJ?*fq@a@fG|LsFm9GDI1T4G*EI&+tWgEHI z!heW)x)Vt4%B8Z0hcJesK^eOKC^)o#1)M?{FaQ5+C=Xa=^Izy}0iXs{h@g4!uTU~D ze9VA~L2i7w1zIhQqjS_=q6wt<3Ey-LT)Pd~y9yl6J3csYKfLa9pzhXOq zboJod5GcuiN%wzBx{&Z6)$eMYzKtqzC+M)jMI2Jn^`Q8IZ{5G+B;8_t48 z%fk}##;IiYVTq%5=2T)J;8y;t6(eAQzuW_K z^RKoV6$}=GfpM7@c0C5KzdFg{QU4GYqbu}anB#_vMx)_ASO@x&D?juXZ|8vvf$&7j#E{|O7l6JMQstw+!Y4I!iW_23fU=U)udp|AgR8Oi6!jB- zyYNLZR^*l$T=!rNse>$E5b1eS32P#9~4|4;j&qOKklt<`=nRyQyf8^MGd**$!2Dv3pyB6`mmN>5% z6+Es!QLwfm%hM$8fT9jFawzQ(7v846Ehz3fdv7y$4w*3F^))6YekvjK?k+|)PV!XhLC?FfO);n-kmCa z7|N0EBBRsCml5>h{y+lWpz zdB?QU*`2YFFG5Ba&D0inGAUwcOymejUQ zIwp-`mNuARlif;**X+dcWm#$Qx|WsZ5Zp4uBK773g5-ToPW7@ZEwO8(L#CNn3R)@H zgxtz06%tHHMFde16z;J>>wW!r|DXRB=j^riTI;N7uWx~40PiH@M02NRlqJ< zYu*j89_{7+0pfsF+dsb=+PNY5{XJ;jbG- z-3p;jFC`d8-L*&Y-JZX98)YU3+NOg@4mH5>)6$N+|C}A!QWW15ed?!j?4kR%PlKy| zE$Ev*!pxFoDE>>$d%?bPQ&ro+d8R*Srvy?{#mk?|;hB;0csz`ioHM4@F}03a>sSyN zi>+V1U@W$drL3_^;D0NyqUTEIu5~g`n(8pKME=-g%G@J-_wB*Wy%8;zV zX~K9=XsDTtE=8pH9^bJ1amevBLdrEx<uH2kx6fpx?3tAcgGC>r9%T(?X>Iv(UB6bWF4G1LzO4+XpwfzQl!3DyqFcv>361Qi z{Xxv;TevqQZ`$swPPD)ruc+-@$IJKYx@6XJ7%N2|5dodVz6L$kaulCD@OECRJXa1I zI#IbyEHI+ZNujm8>$C2|8e=Wx6n$q{OqpV8p0L#;$WXK1e0fqY(N@rDy_|olt+GFB z;;8(A0rrO@&=#pO1-H z4Vv&udS0RMC_`MZ-gf#z0OeRW(U{U7zfPO{DOi1-B>YX6uCO3awmGqFK zGbx%-ywWLh@IxK@jj1XHUE0-Vj~5KQ6#3QpVAL}lxkmeqM#|!0K*b#9IeTc9w|>G9 zma2TPr{F_!UzJ?5R*Dvu=y+s2C(yjYhiQA}ugp#C_k`$qKcFKbHqzOXReHT`R{)1f zYO-ekK9E%$+1x*&EKaMuNX+cbtnm_lWc7BW)^y;NH|i>rqpdmR7jTt#CANtsjmAj_ z{2X4_%>Qz3`1ZXU0F5o*8*3EZaxt5r*tNFM#G59IetzPDI7u*gvQKWluVB6>RpzZ2 z@-}1AXe|epiO&*AmH_Q#@|JvmL3};Ay15`4ZZ@j9VG|u@)Euh}NN=8%60+ZREG=Meb-$rlY;Hz6RvUO(%KPY`3%rIVUQm+EV5ZROeEb*EhG7i!WpTm}7`U-#0Jm&-$|^`xdK?a4jL zi+PoV@wg(tA(@`CBM3`TBgodiwg#}gh28k8XdM?<`GlstbZF=fPm3JP+p~^Vgq7c$ zMz@@<)+Ketnc~Wp2!hL#*NN`OHe&N7?Hz2B1+IywC-YJpbSyte>iRj5=M$IMX9fV& z9@T^VgR*dul6W+p)mLwG@M?EmeoGg*QRs3^_X*i?rT8Vv(ZvdhuPs8s^;)7h9FQsU`PNQa>AsSkCf)$55n+$qhL z7%f4L)R5lUqd@=7kla=ZOyv28hF-YmyZ7l|?+KIH$WoM|q_(*c%DP}aIe^oCKvK!p zcKT=!*fGY@-C^kH5UOxIHmPWjs4;~)$sQiiR)ftnWdd@^>8*c@LZCUm~!gJsyy>AY| zUjX2%5D4ki`;tlIk=QE$&L;-q>2d z$hv9J7n_JEyKQe7z)^4O5VITux!#^q+1>vbF{R* zC%-w2>ePNtw}KS`%R?R0$6}L6O~Je4k!#l^Mpav34i@TLx}GAW8|idw>O9H5VuxB` zB+eqd^*Tl(MoKH0pBzn4D*yn{Ak^ndD~{m&pEdUnyPX>2)+lfl&-@&Y=r(5R=QLe- zXF(5mRs9*W*NHh%m27US2 zh}@Ax`~|oRHCT4>9e!Aes6wImD^Xs~KkoIbArO_75&1n$1nQhKm=ORLLL)`rKfeK$ z3p<^AMw0Vq02LbpR>vHI=77F`zIi4>diO-zX!0GF1)Km$VDQ(ZA6BByK^+zk!`Gwz zoX-(^3M9e!KWBbV69uAnff=_arCR@(GiF2HH^F7uw-oXPWlXVSiUr_f77NV>{a=Lw bow0FaF3J0pUI|@_JZ{Uz?JoEJ>3`yHQORkx diff --git a/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png b/demo_in_notebook/output_files/mock_output/2024-11-05_20-09-58_MCDA_rough_scores.png deleted file mode 100644 index 1fcba28264bacd213b30bde9a6227c4ca86013f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32090 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 Date: Tue, 12 Nov 2024 17:34:22 +0100 Subject: [PATCH 12/27] update gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index a2eb120..cfdde19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .DS_Store .idea .ipynb_checkpoints/ +.png +.CSV +demo_in_notebook/output_files/mock_output/ + From d98012789e5c82991c0aa6270b763da3718317ba Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 13 Nov 2024 14:31:30 +0100 Subject: [PATCH 13/27] fix test_aggregate_with_specific_normalization_and_aggregation_methods --- mcda/models/mcda_without_robustness.py | 102 +++++++++---------------- tests/unit_tests/test_promcda.py | 4 +- 2 files changed, 38 insertions(+), 68 deletions(-) diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index aec7401..e5ea1b7 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -1,3 +1,4 @@ +import re import sys import copy import logging @@ -109,24 +110,17 @@ def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: lis Returns: - A DataFrame containing the aggregated scores for each alternative and normalization method. """ - # Convert `method` to string if it’s an enum instance if isinstance(method, AggregationFunctions): method = method.value self.normalized_indicators = normalized_indicators self.weights = weights - agg = Aggregation(self.weights) + agg= Aggregation(weights) - # Dictionary to map aggregation methods to their corresponding score DataFrames - score_dfs = { - AggregationFunctions.WEIGHTED_SUM.value: pd.DataFrame(), - AggregationFunctions.GEOMETRIC.value: pd.DataFrame(), - AggregationFunctions.HARMONIC.value: pd.DataFrame(), - AggregationFunctions.MINIMUM.value: pd.DataFrame(), - } + score_list = [] - def _apply_aggregation(agg_method, df_subset, suffix): + def _apply_aggregation(norm_method, agg_method, df_subset): """ Apply the aggregation method to a subset of the DataFrame and store results in the appropriate DataFrame. """ @@ -137,63 +131,39 @@ def _apply_aggregation(agg_method, df_subset, suffix): AggregationFunctions.MINIMUM.value: agg.minimum, }.get(agg_method) - if agg_function: - aggregated_scores = agg_function(df_subset) - - if isinstance(aggregated_scores, pd.Series): - aggregated_scores = aggregated_scores.to_frame() - - aggregated_scores.columns = [f"{col}_{agg_method}_{suffix}" for col in - df_subset.columns.unique(level=0)] - - for base_col_name in self.normalized_indicators.columns.str.split("_").str[0].unique(): - relevant_columns = self.normalized_indicators.filter(regex=f"^{base_col_name}_") - - for suffix in relevant_columns.columns.str.split("_", n=1).str[1].unique(): - # Define the correct columns based on whether "without_zero" is in the suffix or not - if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: - if "without_zero" not in suffix: - # Only select columns ending with the exact suffix that doesn't contain "without_zero" - selected_columns = relevant_columns.filter(regex=f"_{suffix}$") - _apply_aggregation(AggregationFunctions.WEIGHTED_SUM.value, selected_columns, suffix) - - elif method in [AggregationFunctions.GEOMETRIC.value, AggregationFunctions.HARMONIC.value]: - if "without_zero" in suffix: - selected_columns = relevant_columns.filter(regex=f"_{suffix}$") - if method == AggregationFunctions.GEOMETRIC.value: - _apply_aggregation(AggregationFunctions.GEOMETRIC.value, selected_columns, suffix) - elif method == AggregationFunctions.HARMONIC.value: - _apply_aggregation(AggregationFunctions.HARMONIC.value, selected_columns, suffix) - - elif method == AggregationFunctions.MINIMUM.value: - if "without_zero" not in suffix: - selected_columns = relevant_columns.filter(regex=f"_{suffix}$") - _apply_aggregation(AggregationFunctions.MINIMUM.value, selected_columns, suffix) - - # Loop through all columns to detect normalization methods - # for normalization_col_name in self.normalized_indicators.columns.str.split("_").str[1].unique(): - # suffix = normalized_indicators.columns.str.split("_", n=1).str[1] - # relevant_columns = self.normalized_indicators.filter(regex=f"_{normalization_col_name}(_|$)") - # - # # weighted_sum - # if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: - # if "without_zero" not in suffix: - # _apply_aggregation(AggregationFunctions.WEIGHTED_SUM.value, relevant_columns, suffix) - # - # # geometric or harmonic - # if method in [AggregationFunctions.GEOMETRIC.value, - # AggregationFunctions.HARMONIC.value] and "without_zero" in suffix: - # # minimum - # if method == AggregationFunctions.GEOMETRIC.value: - # _apply_aggregation(AggregationFunctions.GEOMETRIC.value, relevant_columns, - # f"_geom_{suffix}") - # elif method == AggregationFunctions.HARMONIC.value: - # _apply_aggregation(AggregationFunctions.HARMONIC.value, relevant_columns, f"_harm_{suffix}") - # if method == AggregationFunctions.MINIMUM.value: - # if "without_zero" not in suffix: - # _apply_aggregation(AggregationFunctions.MINIMUM.value, selected_columns, f"_min_{suffix}") + aggregated_scores = agg_function(df_subset) + + if isinstance(aggregated_scores, pd.Series): + aggregated_scores = aggregated_scores.to_frame() + + aggregated_scores.columns = [f"{norm_method}_{agg_method}"] + + score_list.append(aggregated_scores) + + for norm_method in self.normalized_indicators.columns.str.split("_", n=1).str[1].unique(): + + without_zero_columns = self.normalized_indicators.filter(regex="without_zero$") + with_zero_columns = self.normalized_indicators[self.normalized_indicators.columns.difference(without_zero_columns.columns)] + + # Apply WEIGHTED_SUM only to columns with zero in the suffix + if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: + _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM.value, + with_zero_columns) + + # Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix + if method == AggregationFunctions.GEOMETRIC.value: + _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC.value, + without_zero_columns) + elif method == AggregationFunctions.HARMONIC.value: + _apply_aggregation(norm_method, AggregationFunctions.HARMONIC.value, + without_zero_columns) + + # Apply MINIMUM only to columns with zero in the suffix + if method == AggregationFunctions.MINIMUM.value: + _apply_aggregation(norm_method, AggregationFunctions.MINIMUM.value, + with_zero_columns) # Concatenate all score DataFrames into a single DataFrame - scores = pd.concat([df for df in score_dfs.values() if not df.empty], axis=1) + scores = pd.concat(score_list, axis=1) return scores diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 703ce1c..af05734 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -147,12 +147,12 @@ def test_aggregate_with_specific_normalization_and_aggregation_methods(self): weights=self.robustness['given_weights']) # When - expected_columns = ['ws-minmax_01'] + expected_columns = ['minmax_01_weighted_sum', 'minmax_without_zero_weighted_sum'] # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") self.assertTrue( - (aggregated_scores['ws-minmax_01'] >= 0).all() and (aggregated_scores['ws-minmax_01'] <= 1).all(), + (aggregated_scores['minmax_01_weighted_sum'] >= 0).all() and (aggregated_scores['minmax_01_weighted_sum'] <= 1).all(), "Values should be in the range [0, 1] for minmax normalization with weighted sum.") From 3852c11f1cf55f8800122051d45cdbac4eb54b29 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 14 Nov 2024 16:18:32 +0100 Subject: [PATCH 14/27] fix all test_promcda --- mcda/models/mcda_without_robustness.py | 44 +++++--- tests/unit_tests/test_promcda.py | 146 ++----------------------- 2 files changed, 37 insertions(+), 153 deletions(-) diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index e5ea1b7..a095c93 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -1,7 +1,8 @@ -import re import sys import copy import logging + +import numpy as np import pandas as pd from mcda.configuration.enums import NormalizationFunctions, OutputColumnNames4Sensitivity, \ @@ -96,7 +97,7 @@ def add_normalized_df(df, method_name): return normalized_df - def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: list, method=None) -> pd.DataFrame: + def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: list, agg_method=None) -> pd.DataFrame: """ Aggregate the normalized indicators using the specified aggregation method. @@ -110,8 +111,8 @@ def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: lis Returns: - A DataFrame containing the aggregated scores for each alternative and normalization method. """ - if isinstance(method, AggregationFunctions): - method = method.value + if isinstance(agg_method, AggregationFunctions): + method = agg_method.value self.normalized_indicators = normalized_indicators self.weights = weights @@ -124,42 +125,49 @@ def _apply_aggregation(norm_method, agg_method, df_subset): """ Apply the aggregation method to a subset of the DataFrame and store results in the appropriate DataFrame. """ - agg_function = { + agg_functions = { AggregationFunctions.WEIGHTED_SUM.value: agg.weighted_sum, AggregationFunctions.GEOMETRIC.value: agg.geometric, AggregationFunctions.HARMONIC.value: agg.harmonic, AggregationFunctions.MINIMUM.value: agg.minimum, - }.get(agg_method) + } + + agg_methods = list(agg_functions.keys()) if agg_method is None else [agg_method] - aggregated_scores = agg_function(df_subset) + for method in agg_methods: + agg_function = agg_functions[method] + aggregated_scores = agg_function(df_subset) - if isinstance(aggregated_scores, pd.Series): - aggregated_scores = aggregated_scores.to_frame() + if isinstance(aggregated_scores, np.ndarray): + aggregated_scores = pd.DataFrame(aggregated_scores, index=df_subset.index) + elif isinstance(aggregated_scores, pd.Series): + aggregated_scores = aggregated_scores.to_frame() - aggregated_scores.columns = [f"{norm_method}_{agg_method}"] + aggregated_scores.columns = [f"{norm_method}_{method}"] + score_list.append(aggregated_scores) - score_list.append(aggregated_scores) + for norm_method in self.normalized_indicators.columns.str.split("_", n=0).str[1].unique(): - for norm_method in self.normalized_indicators.columns.str.split("_", n=1).str[1].unique(): + norm_method_columns = self.normalized_indicators.filter(regex=rf"{norm_method}") - without_zero_columns = self.normalized_indicators.filter(regex="without_zero$") - with_zero_columns = self.normalized_indicators[self.normalized_indicators.columns.difference(without_zero_columns.columns)] + without_zero_columns = norm_method_columns.filter(regex="without_zero$") + with_zero_columns = norm_method_columns[norm_method_columns.columns.difference(without_zero_columns.columns)] # Apply WEIGHTED_SUM only to columns with zero in the suffix - if method is None or method == AggregationFunctions.WEIGHTED_SUM.value: + if agg_method is None or agg_method == AggregationFunctions.WEIGHTED_SUM.value: _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM.value, with_zero_columns) # Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix - if method == AggregationFunctions.GEOMETRIC.value: + if agg_method is None or agg_method == AggregationFunctions.GEOMETRIC.value: _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC.value, without_zero_columns) - elif method == AggregationFunctions.HARMONIC.value: + elif agg_method is None or agg_method == AggregationFunctions.HARMONIC.value: _apply_aggregation(norm_method, AggregationFunctions.HARMONIC.value, without_zero_columns) # Apply MINIMUM only to columns with zero in the suffix - if method == AggregationFunctions.MINIMUM.value: + if agg_method is None or agg_method == AggregationFunctions.MINIMUM.value: _apply_aggregation(norm_method, AggregationFunctions.MINIMUM.value, with_zero_columns) diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index af05734..7a2f048 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -85,7 +85,7 @@ def test_normalize_all_methods(self): promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - # expected_keys = [method.value for method in NormalizationNames4Sensitivity] + # TODO: delete if return is not a dic expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] @@ -93,9 +93,6 @@ def test_normalize_all_methods(self): normalized_values = promcda.normalize() # Then - #self.assertCountEqual(list(set(normalized_values.keys())), expected_keys, - # "Not all methods were applied or extra keys found.") - #self.assertEqual(list(normalized_values), (expected_keys)) # TODO: delete if return is not a dic actual_suffixes = {col.split('_',1)[1] for col in normalized_values.columns} self.assertCountEqual(actual_suffixes, expected_suffixes, @@ -109,26 +106,26 @@ def test_normalize_specific_method(self): # When normalized_values = promcda.normalize(method=method) + expected_keys = ['0_minmax_01', '1_minmax_01', '0_minmax_without_zero', '1_minmax_without_zero'] # Then - expected_keys = ['Criteria 1_minmax_without_zero', 'Criteria 2_minmax_without_zero', 'Criteria 1_minmax_01', - 'Criteria 2_minmax_01'] self.assertCountEqual(expected_keys, list(normalized_values.keys())) self.assertEqual(list(normalized_values), expected_keys) def test_aggregate_all_methods(self): # Given + self.sensitivity['sensitivity_on'] = 'yes' promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - aggregated_scores = promcda.aggregate(weights=self.robustness['given_weights']) + aggregated_scores = promcda.aggregate(normalization_method=None, + aggregation_method=None, + weights=self.robustness['given_weights']) # When expected_columns = [ - 'ws-minmax_01', 'ws-target_01', 'ws-standardized_any', 'ws-rank', - 'geom-minmax_without_zero', 'geom-target_without_zero', 'geom-standardized_without_zero', 'geom-rank', - 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', - 'min-standardized_any' - ] + 'minmax_weighted_sum', 'target_weighted_sum', 'standardized_weighted_sum', 'rank_weighted_sum', + 'minmax_geometric', 'minmax_minimum', 'target_geometric', 'target_minimum', 'standardized_geometric', + 'standardized_minimum', 'rank_geometric', 'rank_minimum'] # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, @@ -147,137 +144,16 @@ def test_aggregate_with_specific_normalization_and_aggregation_methods(self): weights=self.robustness['given_weights']) # When - expected_columns = ['minmax_01_weighted_sum', 'minmax_without_zero_weighted_sum'] + expected_columns = ['minmax_weighted_sum'] # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") self.assertTrue( - (aggregated_scores['minmax_01_weighted_sum'] >= 0).all() and (aggregated_scores['minmax_01_weighted_sum'] <= 1).all(), + (aggregated_scores['minmax_weighted_sum'] >= 0).all() and (aggregated_scores['minmax_weighted_sum'] <= 1).all(), "Values should be in the range [0, 1] for minmax normalization with weighted sum.") - # def test_normalize_single_method(self): - # """ - # Test normalization with a single methods. - # Test the correctness of the output values happens in unit_tests/test_normalization.py - # """ - # # Given - # self.sensitivity['sensitivity_on'] = 'no' - # - # # When - # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - # self.output_path) - # normalized_matrix = promcda.normalize() - # - # # Then - # self.assertIsInstance(normalized_matrix, pd.DataFrame) - # - # def test_normalize_multiple_methods(self): - # """ - # Test normalization with multiple methods. - # Test the correctness of the output values happens in unit_tests/test_normalization.py - # """ - # # Given - # self.sensitivity['sensitivity_on'] = 'yes' - # self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] - # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - # self.output_path) - # # When - # normalized_matrices = promcda.normalize() - # - # # Then - # self.assertIsInstance(normalized_matrices, dict) - # self.assertIn(NormalizationFunctions.MINMAX.value, normalized_matrices) - # self.assertIn(NormalizationFunctions.STANDARDIZED.value, normalized_matrices) - # self.assertIn(NormalizationFunctions.RANK.value, normalized_matrices) - # self.assertIn(NormalizationFunctions.TARGET.value, normalized_matrices) - # - # self.assertIsInstance(normalized_matrices[NormalizationFunctions.MINMAX.value], pd.DataFrame) - # self.assertIsInstance(normalized_matrices[NormalizationFunctions.STANDARDIZED.value], pd.DataFrame) - # self.assertIsInstance(normalized_matrices[NormalizationFunctions.RANK.value], pd.DataFrame) - # self.assertIsInstance(normalized_matrices[NormalizationFunctions.TARGET.value], pd.DataFrame) - # - # def test_aggregate_with_sensitivity_on(self): - # """ - # Test aggregation when sensitivity_on is 'yes' - # """ - # - # # Given - # self.sensitivity['sensitivity_on'] = 'yes' - # self.sensitivity['normalization'] = [method.value for method in NormalizationFunctions] - # self.sensitivity['aggregation'] = [method.value for method in AggregationFunctions] - # - # promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - # self.output_path) - # - # normalized_matrix = { # Mock normalized matrices in a dictionary for sensitivity-on - # "minmax": pd.DataFrame({ - # "indicator_1": [0.1, 0.2, 0.3], - # "indicator_2": [0.4, 0.5, 0.6] - # }), - # "standardized": pd.DataFrame({ - # "indicator_1": [1, 0.1, 0.7], - # "indicator_2": [0.5, 0.1, 0.8] - # }), - # "target": pd.DataFrame({ - # "indicator_1": [0.15, 0.25, 0.35], - # "indicator_2": [0.45, 0.55, 0.65] - # }), - # "rank": pd.DataFrame({ - # "indicator_1": [1, 2, 3], - # "indicator_2": [1, 2, 3] - # }) - # } - # - # # When - # result = promcda.aggregate(normalized_matrix) - # expected_results = { - # OutputColumnNames4Sensitivity.WS_MINMAX_01.value: pd.Series([0.25, 0.35, 0.45], name="ws-minmax_01"), - # OutputColumnNames4Sensitivity.WS_TARGET_01.value: pd.Series([0.3, 0.4, 0.5], name="ws-target_01"), - # OutputColumnNames4Sensitivity.WS_STANDARDIZED_ANY.value: pd.Series([0.8, 0, 0.74], name="ws-standardized_any"), - # OutputColumnNames4Sensitivity.WS_RANK.value: pd.Series([1.5, 2.5, 3.5], name="ws-rank"), - # OutputColumnNames4Sensitivity.GEOM_MINMAX_WITHOUT_ZERO.value: pd.Series([0.2, 0.3, 0.4], name="geom-minmax_without_zero"), - # OutputColumnNames4Sensitivity.GEOM_TARGET_WITHOUT_ZERO.value: pd.Series([0.25, 0.35, 0.45], name="geom-target_without_zero"), - # OutputColumnNames4Sensitivity.GEOM_STANDARDIZED_WITHOUT_ZERO.value: pd.Series([0.1, 0, 0.3], name="geom-standardized_without_zero"), - # OutputColumnNames4Sensitivity.GEOM_RANK.value: pd.Series([1, 2, 3], name="geom-rank"), - # OutputColumnNames4Sensitivity.HARM_MINMAX_WITHOUT_ZERO.value: pd.Series([0.18, 0.27, 0.36], name="harm-minmax_without_zero"), - # OutputColumnNames4Sensitivity.HARM_TARGET_WITHOUT_ZERO.value: pd.Series([0.22, 0.33, 0.44], name="harm-target_without_zero"), - # OutputColumnNames4Sensitivity.HARM_STANDARDIZED_WITHOUT_ZERO.value: pd.Series([0.05, None, 0.15], name="harm-standardized_without_zero"), - # OutputColumnNames4Sensitivity.HARM_RANK.value: pd.Series([0.9, 1.8, 2.7], name="harm-rank"), - # OutputColumnNames4Sensitivity.MIN_STANDARDIZED_ANY.value: pd.Series([-1, 0, 1], name="min-standardized_any"), - # } - # expected_df = pd.DataFrame(expected_results, index=['A', 'B', 'C']) - # - # # Then - # #self.assertEqual(result, expected_df) - # self.assertIsInstance(result, pd.DataFrame) - # - # def test_aggregate_with_sensitivity_off(self): - # """ - # Test aggregation when sensitivity_on is 'no' - # """ - # # Given - # self.promcda.sensitivity['sensitivity_on'] = 'no' - # self.promcda.sensitivity['aggregation'] = 'weighted_sum' - # - # # When - # expected_result = { - # 'Min-Max + Weighted Sum': [0.5667, 0.4, 0.6], - # 'Standardized + Weighted Sum': [-0.1069, -0.3667, 0.3667], - # 'Rank + Weighted Sum': [2.0, 1.8, 2.2], - # 'Min-Max + Geometric': [0.5695, 0.0, 0.0], - # 'Rank + Geometric': [2.0, 1.933, 2.067], - # 'Min-Max + Harmonic': [0.5455, 0.0, 0.0], - # 'Rank + Harmonic': [2.0, 1.36, 1.85], - # 'Min-Max + Minimum': [0.5, 0.0, 0.0], - # 'Rank + Minimum': [2, 1, 1] - # } - # df = pd.DataFrame(expected_result, index=['A', 'B', 'C']) - # - # # Then - - def tearDown(self): """ Clean up temporary directories and files after each test. From 8ca4f1d0d95e80f894c14925558083ee7a4597e6 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 14 Nov 2024 16:49:31 +0100 Subject: [PATCH 15/27] test in notebook basic methods normlize and aggregate --- demo_in_notebook/Untitled.ipynb | 269 --------- demo_in_notebook/use_promcda_library.ipynb | 604 +++++++++++++++++++++ mcda/models/ProMCDA.py | 20 +- tests/unit_tests/test_promcda.py | 2 +- 4 files changed, 612 insertions(+), 283 deletions(-) delete mode 100644 demo_in_notebook/Untitled.ipynb create mode 100644 demo_in_notebook/use_promcda_library.ipynb diff --git a/demo_in_notebook/Untitled.ipynb b/demo_in_notebook/Untitled.ipynb deleted file mode 100644 index b96de1a..0000000 --- a/demo_in_notebook/Untitled.ipynb +++ /dev/null @@ -1,269 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "96df2c84-1509-4e93-952a-9beba8d0ec45", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Import successful!\n" - ] - } - ], - "source": [ - "import os\n", - "import sys\n", - "import pandas as pd\n", - "\n", - "package_path = '/Users/flaminia/Documents/work/ProMCDA'\n", - "\n", - "if package_path not in sys.path:\n", - " sys.path.append(package_path)\n", - "\n", - "try:\n", - " from mcda.models.ProMCDA import ProMCDA\n", - " print(\"Import successful!\")\n", - "except ModuleNotFoundError as e:\n", - " print(f\"ModuleNotFoundError: {e}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'input_matrix': Criteria 1 Criteria 2\n", - " A 0.5 0.3\n", - " B 0.2 0.6\n", - " C 0.8 0.1,\n", - " 'polarity': ('+', '-'),\n", - " 'sensitivity': {'sensitivity_on': 'no',\n", - " 'normalization': 'minmax',\n", - " 'aggregation': 'weighted_sum'},\n", - " 'robustness': {'robustness_on': 'no',\n", - " 'on_single_weights': 'no',\n", - " 'on_all_weights': 'no',\n", - " 'on_indicators': 'no',\n", - " 'given_weights': [0.6, 0.4]},\n", - " 'monte_carlo': {'monte_carlo_runs': 1000,\n", - " 'num_cores': 2,\n", - " 'random_seed': 42,\n", - " 'marginal_distribution_for_each_indicator': 'normal'},\n", - " 'output_path': 'mock_output/'}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def setUp():\n", - " \n", - " # Mock input data for testing\n", - " input_matrix_without_uncertainty = pd.DataFrame({\n", - " 'Criteria 1': [0.5, 0.2, 0.8],\n", - " 'Criteria 2': [0.3, 0.6, 0.1]\n", - " }, index=['A', 'B', 'C'])\n", - "\n", - " input_matrix_with_uncertainty = pd.DataFrame({\n", - " 'alternatives': ['alt1', 'alt2', 'alt3', 'alt4'],\n", - " 'ind1_min': [-15.20, -12.40, 10.60, -39.70],\n", - " 'ind1_max': [8.20, 8.70, 2.00, 14.00],\n", - " 'ind2': [0.04, 0.05, 0.11, 0.01],\n", - " 'ind3_average': [24.50, 24.50, 14.00, 26.50],\n", - " 'ind3_std': [6.20, 4.80, 0.60, 4.41],\n", - " 'ind4_average': [-15.20, -12.40, 1.60, -39.70],\n", - " 'ind4_std': [8.20, 8.70, 2.00, 14.00],\n", - " 'ind5': [0.04, 0.05, 0.11, 0.01],\n", - " 'ind6_average': [24.50, 24.50, 14.00, 26.50],\n", - " 'ind6_std': [6.20, 4.80, 0.60, 4.41]\n", - " })\n", - " \n", - " input_matrix_with_uncertainty.set_index('alternatives', inplace=True)\n", - "\n", - " polarity = ('+', '-')\n", - "\n", - " sensitivity = {\n", - " 'sensitivity_on': 'no',\n", - " 'normalization': 'minmax',\n", - " 'aggregation': 'weighted_sum'\n", - " }\n", - "\n", - " robustness = {\n", - " 'robustness_on': 'no',\n", - " 'on_single_weights': 'no',\n", - " 'on_all_weights': 'no',\n", - " 'on_indicators': 'no',\n", - " 'given_weights': [0.6, 0.4]\n", - " }\n", - "\n", - " monte_carlo = {\n", - " 'monte_carlo_runs': 1000,\n", - " 'num_cores': 2,\n", - " 'random_seed': 42,\n", - " 'marginal_distribution_for_each_indicator': 'normal'\n", - " }\n", - "\n", - " output_path = 'mock_output/'\n", - "\n", - " # Return the setup parameters as a dictionary\n", - " return {\n", - " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", - " 'polarity': polarity,\n", - " 'sensitivity': sensitivity,\n", - " 'robustness': robustness,\n", - " 'monte_carlo': monte_carlo,\n", - " 'output_path': output_path\n", - " }\n", - "\n", - "# Run the setup and store parameters in a variable\n", - "setup_parameters = setUp()\n", - "\n", - "# Check the setup parameters\n", - "setup_parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 2024-11-06 18:43:58,031 - ProMCDA - ProMCDA will only use one pair of norm/agg functions: minmax/weighted_sum\n", - "INFO: 2024-11-06 18:43:58,032 - ProMCDA - ProMCDA will run without uncertainty on the indicators or weights\n", - "INFO: 2024-11-06 18:43:58,034 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-06 18:43:58,036 - ProMCDA - Number of alternatives: 3\n", - "INFO: 2024-11-06 18:43:58,036 - ProMCDA - Number of indicators: 2\n", - "INFO: 2024-11-06 18:43:58,037 - ProMCDA - Polarities: ('+', '-')\n", - "INFO: 2024-11-06 18:43:58,037 - ProMCDA - Weights: [0.6, 0.4]\n", - "INFO: 2024-11-06 18:43:58,038 - ProMCDA - Normalized weights: [0.6, 0.4]\n", - "INFO: 2024-11-06 18:43:58,040 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-06 18:43:58,042 - ProMCDA - Start ProMCDA without robustness of the indicators\n" - ] - }, - { - "ename": "ValueError", - "evalue": "Length mismatch: Expected axis has 1 elements, new values have 2 elements", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_2923/3015237431.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m promcda = ProMCDA(\n\u001b[0m\u001b[1;32m 2\u001b[0m \u001b[0minput_matrix\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'input_matrix'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0mpolarity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'polarity'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0msensitivity\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'sensitivity'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mrobustness\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'robustness'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, input_matrix, polarity, sensitivity, robustness, monte_carlo, output_path)\u001b[0m\n\u001b[1;32m 47\u001b[0m self.robustness, self.monte_carlo, self.output_path)\n\u001b[1;32m 48\u001b[0m \u001b[0mis_robustness_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mconfiguration_settings\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalidate_inputs\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 49\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_mcda\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mis_robustness_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 50\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 51\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalized_matrix\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mrun_mcda\u001b[0;34m(self, is_robustness_indicators, is_robustness_weights, weights)\u001b[0m\n\u001b[1;32m 136\u001b[0m \u001b[0;31m# no uncertainty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_robustness_indicators\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 138\u001b[0;31m \u001b[0mrun_mcda_without_indicator_uncertainty\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconfiguration_settings\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mis_robustness_weights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 139\u001b[0m \u001b[0;31m# uncertainty\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_main.py\u001b[0m in \u001b[0;36mrun_mcda_without_indicator_uncertainty\u001b[0;34m(extracted_values, is_robustness_weights, weights)\u001b[0m\n\u001b[1;32m 680\u001b[0m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmcda_no_uncert\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate_indicators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 681\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mis_sensitivity\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m\u001b[0;31m \u001b[0m\u001b[0;31m\\\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 682\u001b[0;31m \u001b[0;32melse\u001b[0m \u001b[0mmcda_no_uncert\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate_indicators\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mf_agg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 683\u001b[0m \u001b[0mnormalized_scores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mrescale_minmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mscores\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 684\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mextracted_values\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"on_all_weights\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0mextracted_values\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"robustness_on\"\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;34m\"yes\"\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/mcda_without_robustness.py\u001b[0m in \u001b[0;36maggregate_indicators\u001b[0;34m(self, normalized_indicators, weights, method)\u001b[0m\n\u001b[1;32m 176\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcol_names\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 177\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 178\u001b[0;31m \u001b[0mscores\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcol_names_method\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 179\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 180\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m__setattr__\u001b[0;34m(self, name, value)\u001b[0m\n\u001b[1;32m 6311\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6312\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__getattribute__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 6313\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mobject\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__setattr__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mname\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6314\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mAttributeError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6315\u001b[0m \u001b[0;32mpass\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32mproperties.pyx\u001b[0m in \u001b[0;36mpandas._libs.properties.AxisProperty.__set__\u001b[0;34m()\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/generic.py\u001b[0m in \u001b[0;36m_set_axis\u001b[0;34m(self, axis, labels)\u001b[0m\n\u001b[1;32m 812\u001b[0m \"\"\"\n\u001b[1;32m 813\u001b[0m \u001b[0mlabels\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mensure_index\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlabels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 814\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_mgr\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mset_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlabels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 815\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_clear_item_cache\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 816\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/internals/managers.py\u001b[0m in \u001b[0;36mset_axis\u001b[0;34m(self, axis, new_labels)\u001b[0m\n\u001b[1;32m 236\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mset_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mAxisInt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mIndex\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m->\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 237\u001b[0m \u001b[0;31m# Caller is responsible for ensuring we have an Index object.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 238\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_validate_set_axis\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 239\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maxes\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnew_labels\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 240\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/internals/base.py\u001b[0m in \u001b[0;36m_validate_set_axis\u001b[0;34m(self, axis, new_labels)\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 97\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0mnew_len\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mold_len\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 98\u001b[0;31m raise ValueError(\n\u001b[0m\u001b[1;32m 99\u001b[0m \u001b[0;34mf\"Length mismatch: Expected axis has {old_len} elements, new \"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 100\u001b[0m \u001b[0;34mf\"values have {new_len} elements\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: Length mismatch: Expected axis has 1 elements, new values have 2 elements" - ] - } - ], - "source": [ - "promcda = ProMCDA(\n", - " input_matrix=setup_parameters['input_matrix'],\n", - " polarity=setup_parameters['polarity'],\n", - " sensitivity=setup_parameters['sensitivity'],\n", - " robustness=setup_parameters['robustness'],\n", - " monte_carlo=setup_parameters['monte_carlo'],\n", - " output_path=setup_parameters['output_path']\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f910aa08-e230-4c6e-b7de-8a17233c206a", - "metadata": {}, - "outputs": [], - "source": [ - "promcda = ProMCDA(\n", - " input_matrix=setup_parameters['input_matrix'],\n", - " polarity=None,\n", - " sensitivity=None,\n", - " robustness=setup_parameters['robustness'],\n", - " monte_carlo=None,\n", - " output_path=None\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2de08fa4-9dc6-4469-aef1-5f9fc4090176", - "metadata": {}, - "outputs": [], - "source": [ - "input_matrix=setup_parameters['input_matrix']\n", - "input_matrix" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "test = promcda.normalize()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a75e64a1-0a60-4d44-9c60-ebe8708e66b6", - "metadata": {}, - "outputs": [], - "source": [ - "test\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "ProMCDA (Python)", - "language": "python", - "name": "promcda" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.20" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb new file mode 100644 index 0000000..6c9a731 --- /dev/null +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -0,0 +1,604 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "96df2c84-1509-4e93-952a-9beba8d0ec45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Import successful!\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import pandas as pd\n", + "\n", + "package_path = '/Users/flaminia/Documents/work/ProMCDA'\n", + "\n", + "if package_path not in sys.path:\n", + " sys.path.append(package_path)\n", + "\n", + "try:\n", + " from mcda.models.ProMCDA import ProMCDA\n", + " print(\"Import successful!\")\n", + "except ModuleNotFoundError as e:\n", + " print(f\"ModuleNotFoundError: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input_matrix': Criteria 1 Criteria 2\n", + " A 0.5 0.3\n", + " B 0.2 0.6\n", + " C 0.8 0.1,\n", + " 'polarity': ('+', '-'),\n", + " 'sensitivity': {'sensitivity_on': 'no',\n", + " 'normalization': 'minmax',\n", + " 'aggregation': 'weighted_sum'},\n", + " 'robustness': {'robustness_on': 'no',\n", + " 'on_single_weights': 'no',\n", + " 'on_all_weights': 'no',\n", + " 'on_indicators': 'no',\n", + " 'given_weights': [0.6, 0.4]},\n", + " 'monte_carlo': {'monte_carlo_runs': 1000,\n", + " 'num_cores': 2,\n", + " 'random_seed': 42,\n", + " 'marginal_distribution_for_each_indicator': 'normal'},\n", + " 'output_path': 'mock_output/'}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def setUp():\n", + " \n", + " # Mock input data for testing\n", + " input_matrix_without_uncertainty = pd.DataFrame({\n", + " 'Criteria 1': [0.5, 0.2, 0.8],\n", + " 'Criteria 2': [0.3, 0.6, 0.1]\n", + " }, index=['A', 'B', 'C'])\n", + "\n", + " input_matrix_with_uncertainty = pd.DataFrame({\n", + " 'alternatives': ['alt1', 'alt2', 'alt3', 'alt4'],\n", + " 'ind1_min': [-15.20, -12.40, 10.60, -39.70],\n", + " 'ind1_max': [8.20, 8.70, 2.00, 14.00],\n", + " 'ind2': [0.04, 0.05, 0.11, 0.01],\n", + " 'ind3_average': [24.50, 24.50, 14.00, 26.50],\n", + " 'ind3_std': [6.20, 4.80, 0.60, 4.41],\n", + " 'ind4_average': [-15.20, -12.40, 1.60, -39.70],\n", + " 'ind4_std': [8.20, 8.70, 2.00, 14.00],\n", + " 'ind5': [0.04, 0.05, 0.11, 0.01],\n", + " 'ind6_average': [24.50, 24.50, 14.00, 26.50],\n", + " 'ind6_std': [6.20, 4.80, 0.60, 4.41]\n", + " })\n", + " \n", + " input_matrix_with_uncertainty.set_index('alternatives', inplace=True)\n", + "\n", + " polarity = ('+', '-')\n", + "\n", + " sensitivity = {\n", + " 'sensitivity_on': 'no',\n", + " 'normalization': 'minmax',\n", + " 'aggregation': 'weighted_sum'\n", + " }\n", + "\n", + " robustness = {\n", + " 'robustness_on': 'no',\n", + " 'on_single_weights': 'no',\n", + " 'on_all_weights': 'no',\n", + " 'on_indicators': 'no',\n", + " 'given_weights': [0.6, 0.4]\n", + " }\n", + "\n", + " monte_carlo = {\n", + " 'monte_carlo_runs': 1000,\n", + " 'num_cores': 2,\n", + " 'random_seed': 42,\n", + " 'marginal_distribution_for_each_indicator': 'normal'\n", + " }\n", + "\n", + " output_path = 'mock_output/'\n", + "\n", + " # Return the setup parameters as a dictionary\n", + " return {\n", + " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", + " 'polarity': polarity,\n", + " 'sensitivity': sensitivity,\n", + " 'robustness': robustness,\n", + " 'monte_carlo': monte_carlo,\n", + " 'output_path': output_path\n", + " }\n", + "\n", + "# Run the setup and store parameters in a variable\n", + "setup_parameters = setUp()\n", + "\n", + "# Check the setup parameters\n", + "setup_parameters" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", + "metadata": {}, + "outputs": [], + "source": [ + "promcda = ProMCDA(\n", + " input_matrix=setup_parameters['input_matrix'],\n", + " polarity=setup_parameters['polarity'],\n", + " sensitivity=setup_parameters['sensitivity'],\n", + " robustness=setup_parameters['robustness'],\n", + " monte_carlo=setup_parameters['monte_carlo'],\n", + " output_path=setup_parameters['output_path']\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-11-14 16:47:50,738 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0_minmax_011_minmax_010_minmax_without_zero1_minmax_without_zero0_target_011_target_010_target_without_zero1_target_without_zero0_standardized_any1_standardized_any0_standardized_without_zero1_standardized_without_zero0_rank1_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", + "
" + ], + "text/plain": [ + " 0_minmax_01 1_minmax_01 0_minmax_without_zero 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 0.64 \n", + "1 0.0 0.0 0.1 0.1 \n", + "2 1.0 1.0 1.0 1.0 \n", + "\n", + " 0_target_01 1_target_01 0_target_without_zero 1_target_without_zero \\\n", + "0 0.625 0.5 0.6625 0.55 \n", + "1 0.25 0.0 0.325 0.1 \n", + "2 1.0 0.833333 1.0 0.85 \n", + "\n", + " 0_standardized_any 1_standardized_any 0_standardized_without_zero \\\n", + "0 0.0 0.132453 1.1 \n", + "1 -1.0 -1.059626 0.1 \n", + "2 1.0 0.927173 2.1 \n", + "\n", + " 1_standardized_without_zero 0_rank 1_rank \n", + "0 1.292079 2.0 2.0 \n", + "1 0.1 1.0 1.0 \n", + "2 2.086799 3.0 3.0 " + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-11-14 16:23:31,945 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0_target_011_target_010_target_without_zero1_target_without_zero
00.6250.50.66250.55
10.250.00.3250.1
21.00.8333331.00.85
\n", + "
" + ], + "text/plain": [ + " 0_target_01 1_target_01 0_target_without_zero 1_target_without_zero\n", + "0 0.625 0.5 0.6625 0.55\n", + "1 0.25 0.0 0.325 0.1\n", + "2 1.0 0.833333 1.0 0.85" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize(\"target\")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "e857a02a-aae8-453a-a302-46844c3e610f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-11-14 16:46:28,616 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-14 16:46:28,622 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
minmax_weighted_summinmax_geometricminmax_minimumtarget_weighted_sumtarget_geometrictarget_minimumstandardized_weighted_sumstandardized_geometricstandardized_minimumrank_weighted_sumrank_geometricrank_minimum
00.5583330.6008360.50.5520830.5943460.50.0772641.2082740.02.0NaN2.0
10.00.1000000.00.1041670.1634120.0-1.0347820.100000-1.0596261.0NaN1.0
21.01.0000001.00.9027780.9095520.8333330.9575172.0922890.9271733.0NaN3.0
\n", + "
" + ], + "text/plain": [ + " minmax_weighted_sum minmax_geometric minmax_minimum target_weighted_sum \\\n", + "0 0.558333 0.600836 0.5 0.552083 \n", + "1 0.0 0.100000 0.0 0.104167 \n", + "2 1.0 1.000000 1.0 0.902778 \n", + "\n", + " target_geometric target_minimum standardized_weighted_sum \\\n", + "0 0.594346 0.5 0.077264 \n", + "1 0.163412 0.0 -1.034782 \n", + "2 0.909552 0.833333 0.957517 \n", + "\n", + " standardized_geometric standardized_minimum rank_weighted_sum \\\n", + "0 1.208274 0.0 2.0 \n", + "1 0.100000 -1.059626 1.0 \n", + "2 2.092289 0.927173 3.0 \n", + "\n", + " rank_geometric rank_minimum \n", + "0 NaN 2.0 \n", + "1 NaN 1.0 \n", + "2 NaN 3.0 " + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.aggregate(None, None,[0.5, 0.7])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "6e0949ed-f2a7-4c90-87f8-5d61d27da7ac", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-11-14 16:47:27,275 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-14 16:47:27,279 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
minmax_geometric
00.600836
10.100000
21.000000
\n", + "
" + ], + "text/plain": [ + " minmax_geometric\n", + "0 0.600836\n", + "1 0.100000\n", + "2 1.000000" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.aggregate(\"minmax\", \"geometric\",[0.5, 0.7])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd367a88-d05f-4cdb-bf9d-488fb7d50082", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "ProMCDA (Python)", + "language": "python", + "name": "promcda" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.20" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index c0f25a5..93274c8 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -35,7 +35,6 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit self.monte_carlo = monte_carlo self.output_path = output_path - # Check configuration dictionary keys and handle potential issues # TODO: revisit this logic when substitute classes to handle configuration settings try: check_configuration_keys(self.sensitivity, self.robustness, self.monte_carlo) @@ -45,8 +44,10 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensit self.configuration_settings = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, self.output_path) - is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() - self.run_mcda(is_robustness_indicators, is_robustness_weights, weights) + + # TODO: remove this line after implementing the method run_promcda + #is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() + #self.run_mcda(is_robustness_indicators, is_robustness_weights, weights) self.normalized_matrix = None self.aggregated_matrix = None @@ -82,14 +83,7 @@ def normalize(self, method=None) -> pd.DataFrame: mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) normalized_values = mcda_without_robustness.normalize_indicators(method) - if method is None: - normalized_df = pd.concat(normalized_values, axis=1) - normalized_df.columns = [f"{col}_{method}" for method, cols in normalized_values.items() for col in - input_matrix_no_alternatives.columns] - else: - normalized_df = normalized_values - - return normalized_df + return normalized_values def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: """ @@ -101,7 +95,7 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= - weights: The weights to be used for aggregation. If None, they are set all the same. Returns: - - A DataFrame containing the aggregated scores. + - A DataFrame containing the aggregated scores per normalization and aggregation methods. """ input_matrix_no_alternatives = check_input_matrix(self.input_matrix) @@ -111,7 +105,7 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= aggregated_scores = mcda_without_robustness.aggregate_indicators( normalized_indicators=normalized_indicators, weights=weights, - method=aggregation_method + agg_method=aggregation_method ) return aggregated_scores diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 7a2f048..d043817 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -34,7 +34,7 @@ def setUp(self): 'on_single_weights': 'yes', 'on_all_weights': 'no', 'on_indicators': 'no', - 'given_weights': [0.6, 0.4] + 'given_weights': [0.6, 0.7] } self.monte_carlo = { From d3124332b83e72077d6793367e8911cdea9d3ce1 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Fri, 15 Nov 2024 15:36:38 +0100 Subject: [PATCH 16/27] improve column names in normalize --- demo_in_notebook/use_promcda_library.ipynb | 179 ++++++++++++--------- mcda/models/mcda_without_robustness.py | 2 +- 2 files changed, 106 insertions(+), 75 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 6c9a731..179abbd 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -132,6 +132,58 @@ "setup_parameters" ] }, + { + "cell_type": "code", + "execution_count": 9, + "id": "18ca51a7-87ba-458f-a505-29722e5b93ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Criteria 1\n", + "Criteria 2\n" + ] + }, + { + "data": { + "text/plain": [ + "[None, None]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[print(col) for col in setup_parameters[\"input_matrix\"].columns]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "49a4edae-3192-48cf-a50a-ef7c260d9171", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'method_name' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_36920/3827112317.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34mf\"{col}_{method_name}\"\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"input_matrix\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtolist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_36920/3827112317.py\u001b[0m in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34mf\"{col}_{method_name}\"\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"input_matrix\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtolist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;31mNameError\u001b[0m: name 'method_name' is not defined" + ] + } + ], + "source": [ + "columns = [f\"{col}_{method_name}\" for col in setup_parameters[\"input_matrix\"].columns.tolist()]" + ] + }, { "cell_type": "code", "execution_count": 3, @@ -151,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true @@ -161,7 +213,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-14 16:47:50,738 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-11-15 15:31:17,525 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n" ] }, { @@ -279,7 +345,7 @@ "2 2.086799 3.0 3.0 " ] }, - "execution_count": 14, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -290,7 +356,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", "metadata": {}, "outputs": [ @@ -298,7 +364,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-14 16:23:31,945 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-11-15 15:31:17,583 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "0\n", + "1\n", + "0\n", + "1\n" ] }, { @@ -361,7 +431,7 @@ "2 1.0 0.833333 1.0 0.85" ] }, - "execution_count": 6, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -372,7 +442,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "id": "e857a02a-aae8-453a-a302-46844c3e610f", "metadata": {}, "outputs": [ @@ -380,8 +450,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-14 16:46:28,616 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-14 16:46:28,622 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-11-15 15:31:20,745 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-15 15:31:20,748 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n", + "0\n", + "1\n" ] }, { @@ -491,7 +575,7 @@ "2 NaN 3.0 " ] }, - "execution_count": 11, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -502,71 +586,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "6e0949ed-f2a7-4c90-87f8-5d61d27da7ac", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 2024-11-14 16:47:27,275 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-14 16:47:27,279 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
minmax_geometric
00.600836
10.100000
21.000000
\n", - "
" - ], - "text/plain": [ - " minmax_geometric\n", - "0 0.600836\n", - "1 0.100000\n", - "2 1.000000" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "promcda.aggregate(\"minmax\", \"geometric\",[0.5, 0.7])" ] @@ -574,7 +597,15 @@ { "cell_type": "code", "execution_count": null, - "id": "dd367a88-d05f-4cdb-bf9d-488fb7d50082", + "id": "506619fd-9659-47cd-88fa-7acd985a1071", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15bb84ff-e194-4326-a368-c467c9b6e3a7", "metadata": {}, "outputs": [], "source": [] diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index a095c93..d53b289 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -60,7 +60,7 @@ def normalize_indicators(self, method=None) -> pd.DataFrame: normalized_dataframes = [] def add_normalized_df(df, method_name): - df.columns = [f"{col}_{method_name}" for col in df.columns] + df.columns = [f"{col}_{method_name}" for col in self._input_matrix.columns.tolist()] normalized_dataframes.append(df) if isinstance(method, NormalizationFunctions): From 7deee2dd9c381b9097a5d1dfbaad032e2cdbc32c Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 20 Nov 2024 11:24:39 +0100 Subject: [PATCH 17/27] change and simplify the __init__, add optional and default parameters --- demo_in_notebook/use_promcda_library.ipynb | 199 ++++++------------ mcda/configuration/configuration_validator.py | 33 +-- mcda/configuration/enums.py | 11 + mcda/models/ProMCDA.py | 142 +++++++++---- tests/unit_tests/test_promcda.py | 70 +++--- 5 files changed, 218 insertions(+), 237 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 179abbd..04b16fc 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -33,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", "metadata": {}, "outputs": [ @@ -60,7 +60,7 @@ " 'output_path': 'mock_output/'}" ] }, - "execution_count": 2, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -135,58 +135,6 @@ { "cell_type": "code", "execution_count": 9, - "id": "18ca51a7-87ba-458f-a505-29722e5b93ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Criteria 1\n", - "Criteria 2\n" - ] - }, - { - "data": { - "text/plain": [ - "[None, None]" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "[print(col) for col in setup_parameters[\"input_matrix\"].columns]" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "49a4edae-3192-48cf-a50a-ef7c260d9171", - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'method_name' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_36920/3827112317.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34mf\"{col}_{method_name}\"\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"input_matrix\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtolist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_36920/3827112317.py\u001b[0m in \u001b[0;36m\u001b[0;34m(.0)\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mcolumns\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34mf\"{col}_{method_name}\"\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcol\u001b[0m \u001b[0;32min\u001b[0m \u001b[0msetup_parameters\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m\"input_matrix\"\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtolist\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;31mNameError\u001b[0m: name 'method_name' is not defined" - ] - } - ], - "source": [ - "columns = [f\"{col}_{method_name}\" for col in setup_parameters[\"input_matrix\"].columns.tolist()]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", "metadata": {}, "outputs": [], @@ -203,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 10, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true @@ -213,21 +161,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-15 15:31:17,525 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n" + "INFO: 2024-11-15 18:27:41,569 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] }, { @@ -251,20 +185,20 @@ " \n", " \n", " \n", - " 0_minmax_01\n", - " 1_minmax_01\n", - " 0_minmax_without_zero\n", - " 1_minmax_without_zero\n", - " 0_target_01\n", - " 1_target_01\n", - " 0_target_without_zero\n", - " 1_target_without_zero\n", - " 0_standardized_any\n", - " 1_standardized_any\n", - " 0_standardized_without_zero\n", - " 1_standardized_without_zero\n", - " 0_rank\n", - " 1_rank\n", + " Criteria 1_minmax_01\n", + " Criteria 2_minmax_01\n", + " Criteria 1_minmax_without_zero\n", + " Criteria 2_minmax_without_zero\n", + " Criteria 1_target_01\n", + " Criteria 2_target_01\n", + " Criteria 1_target_without_zero\n", + " Criteria 2_target_without_zero\n", + " Criteria 1_standardized_any\n", + " Criteria 2_standardized_any\n", + " Criteria 1_standardized_without_zero\n", + " Criteria 2_standardized_without_zero\n", + " Criteria 1_rank\n", + " Criteria 2_rank\n", " \n", " \n", " \n", @@ -324,28 +258,38 @@ "" ], "text/plain": [ - " 0_minmax_01 1_minmax_01 0_minmax_without_zero 1_minmax_without_zero \\\n", - "0 0.5 0.6 0.55 0.64 \n", - "1 0.0 0.0 0.1 0.1 \n", - "2 1.0 1.0 1.0 1.0 \n", + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", "\n", - " 0_target_01 1_target_01 0_target_without_zero 1_target_without_zero \\\n", - "0 0.625 0.5 0.6625 0.55 \n", - "1 0.25 0.0 0.325 0.1 \n", - "2 1.0 0.833333 1.0 0.85 \n", + " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", + "0 0.64 0.625 0.5 \n", + "1 0.1 0.25 0.0 \n", + "2 1.0 1.0 0.833333 \n", "\n", - " 0_standardized_any 1_standardized_any 0_standardized_without_zero \\\n", - "0 0.0 0.132453 1.1 \n", - "1 -1.0 -1.059626 0.1 \n", - "2 1.0 0.927173 2.1 \n", + " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", + "0 0.6625 0.55 \n", + "1 0.325 0.1 \n", + "2 1.0 0.85 \n", "\n", - " 1_standardized_without_zero 0_rank 1_rank \n", - "0 1.292079 2.0 2.0 \n", - "1 0.1 1.0 1.0 \n", - "2 2.086799 3.0 3.0 " + " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", + "0 0.0 0.132453 \n", + "1 -1.0 -1.059626 \n", + "2 1.0 0.927173 \n", + "\n", + " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", + "0 1.1 1.292079 \n", + "1 0.1 0.1 \n", + "2 2.1 2.086799 \n", + "\n", + " Criteria 1_rank Criteria 2_rank \n", + "0 2.0 2.0 \n", + "1 1.0 1.0 \n", + "2 3.0 3.0 " ] }, - "execution_count": 4, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -356,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", "metadata": {}, "outputs": [ @@ -364,11 +308,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-15 15:31:17,583 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "0\n", - "1\n", - "0\n", - "1\n" + "INFO: 2024-11-15 18:27:42,533 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] }, { @@ -392,10 +332,10 @@ " \n", " \n", " \n", - " 0_target_01\n", - " 1_target_01\n", - " 0_target_without_zero\n", - " 1_target_without_zero\n", + " Criteria 1_target_01\n", + " Criteria 2_target_01\n", + " Criteria 1_target_without_zero\n", + " Criteria 2_target_without_zero\n", " \n", " \n", " \n", @@ -425,13 +365,18 @@ "" ], "text/plain": [ - " 0_target_01 1_target_01 0_target_without_zero 1_target_without_zero\n", - "0 0.625 0.5 0.6625 0.55\n", - "1 0.25 0.0 0.325 0.1\n", - "2 1.0 0.833333 1.0 0.85" + " Criteria 1_target_01 Criteria 2_target_01 Criteria 1_target_without_zero \\\n", + "0 0.625 0.5 0.6625 \n", + "1 0.25 0.0 0.325 \n", + "2 1.0 0.833333 1.0 \n", + "\n", + " Criteria 2_target_without_zero \n", + "0 0.55 \n", + "1 0.1 \n", + "2 0.85 " ] }, - "execution_count": 5, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -442,7 +387,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "id": "e857a02a-aae8-453a-a302-46844c3e610f", "metadata": {}, "outputs": [ @@ -450,22 +395,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-15 15:31:20,745 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-15 15:31:20,748 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n", - "0\n", - "1\n" + "INFO: 2024-11-15 18:27:43,078 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", + "INFO: 2024-11-15 18:27:43,082 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] }, { @@ -575,7 +506,7 @@ "2 NaN 3.0 " ] }, - "execution_count": 6, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index 9e79524..ef829d3 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -18,24 +18,20 @@ from typing import Dict, List, Any -def check_configuration_keys(sensitivity: dict, robustness: dict, monte_carlo: dict) -> bool: +def check_configuration_keys(robustness: dict, monte_carlo: dict) -> bool: """ Checks for required keys in sensitivity, robustness, and monte_carlo dictionaries. TODO: revisit this logic when substitute classes to handle configuration settings. - :param sensitivity : dict :param robustness : dict - :param: monte_carlo : dict + :param monte_carlo: dict :rtype: bool """ keys_of_dict_values = { - 'sensitivity': ['sensitivity_on', 'normalization', 'aggregation'], 'robustness': ['robustness_on', 'on_single_weights', 'on_all_weights', 'given_weights', 'on_indicators'], 'monte_carlo': ['monte_carlo_runs', 'num_cores', 'random_seed', 'marginal_distribution_for_each_indicator'] } - - _check_dict_keys(sensitivity, keys_of_dict_values['sensitivity']) _check_dict_keys(robustness, keys_of_dict_values['robustness']) _check_dict_keys(monte_carlo, keys_of_dict_values['monte_carlo']) @@ -51,8 +47,8 @@ def _check_dict_keys(dic: Dict[str, Any], expected_keys: List[str]) -> None: raise KeyError(f"The key '{key}' is missing in the provided dictionary") -def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str], sensitivity: dict, robustness: dict, - monte_carlo: dict, output_path: str) -> dict: +def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str], robustness: dict, + monte_carlo: dict) -> dict: """ Extracts relevant configuration values required for running the ProMCDA process. @@ -85,10 +81,6 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str extracted_values = { "input_matrix": input_matrix, "polarity": polarity, - # Sensitivity settings - "sensitivity_on": sensitivity["sensitivity_on"], - "normalization": None if sensitivity["sensitivity_on"] == 'yes' else sensitivity["normalization"], - "aggregation": None if sensitivity["sensitivity_on"] == 'yes' else sensitivity["aggregation"], # Robustness settings "robustness_on": robustness["robustness_on"], "robustness_on_single_weights": robustness["on_single_weights"], @@ -99,8 +91,7 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str "monte_carlo_runs": monte_carlo["monte_carlo_runs"], "num_cores": monte_carlo["num_cores"], "random_seed": monte_carlo["random_seed"], - "marginal_distribution_for_each_indicator": monte_carlo["marginal_distribution_for_each_indicator"], - "output_path": output_path + "marginal_distribution_for_each_indicator": monte_carlo["marginal_distribution_for_each_indicator"] } return extracted_values @@ -118,7 +109,6 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s A dictionary containing configuration values extracted from the input parameters. It includes: - input_matrix (pd.DataFrame): The decision matrix for alternatives and indicators. - polarity (Tuple[str]): A tuple indicating the polarity of each indicator. - - sensitivity_on (str): Indicates whether sensitivity analysis is enabled ("yes" or "no"). - normalization (str): The normalization method to be used if sensitivity analysis is enabled. - aggregation (str): The aggregation method to be used if sensitivity analysis is enabled. - robustness_on (str): Indicates whether robustness analysis is enabled ("yes" or "no"). @@ -149,8 +139,6 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s # Access the values from the dictionary input_matrix = extracted_values["input_matrix"] polarity = extracted_values["polarity"] - sensitivity_on = extracted_values["sensitivity_on"] - normalization = extracted_values["normalization"] aggregation = extracted_values["aggregation"] robustness_on = extracted_values["robustness_on"] robustness_on_single_weights = extracted_values["robustness_on_single_weights"] @@ -163,20 +151,9 @@ def check_configuration_values(extracted_values: dict) -> Tuple[int, int, List[s # Check for sensitivity-related configuration errors valid_norm_methods = [method.value for method in NormalizationFunctions] valid_agg_methods = [method.value for method in AggregationFunctions] - if isinstance(normalization, NormalizationFunctions): - normalization = normalization.value if isinstance(aggregation, AggregationFunctions): aggregation = aggregation.value - if sensitivity_on == "no": - check_config_error(normalization not in valid_norm_methods, - f'Invalid normalization method: {normalization}. Available methods: {valid_norm_methods}') - check_config_error(aggregation not in valid_agg_methods, - f'Invalid aggregation method: {aggregation}. Available methods: {valid_agg_methods}') - logger.info("ProMCDA will only use one pair of norm/agg functions: " + normalization + '/' + aggregation) - else: - logger.info("ProMCDA will use a set of different pairs of norm/agg functions") - # Check for robustness-related configuration errors if robustness_on == "no": logger.info("ProMCDA will run without uncertainty on the indicators or weights") diff --git a/mcda/configuration/enums.py b/mcda/configuration/enums.py index 65aa346..ede2d89 100644 --- a/mcda/configuration/enums.py +++ b/mcda/configuration/enums.py @@ -59,4 +59,15 @@ class OutputColumnNames4Sensitivity(Enum): MIN_STANDARDIZED_ANY = 'min-standardized_any' +class PDFType(Enum): + """ + Names of probability density functions, which describe the indicators in case of robustness analysis + """ + EXACT = "exact" + UNIFORM = "uniform" + NORMAL = "normal" + LOGNORMAL = "lognormal" + POISSON = "poisson" + + diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 93274c8..b071856 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -1,6 +1,6 @@ import time import pandas as pd -from typing import Tuple, List, Union +from typing import Tuple, List, Union, Optional from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys @@ -10,48 +10,66 @@ class ProMCDA: - def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], sensitivity: dict, robustness: dict, - monte_carlo: dict, output_path: str): + def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robustness_weights: Optional[bool] = None, + robustness_indicators: Optional[bool] = None, marginal_distributions: Optional[bool] = None, + num_runs: Optional[int] = 10000, num_cores: Optional[int] = 1, random_seed: Optional[int] = 43): """ Initialize the ProMCDA class with configuration parameters. + # Required parameters :param input_matrix: DataFrame containing the alternatives and criteria. - :param polarity: List of polarity for each indicator (+ or -). - :param sensitivity: Sensitivity analysis configuration. - :param robustness: Robustness analysis configuration. - :param monte_carlo: Monte Carlo sampling configuration. - :param output_path: path for saving output files. - - # Example of instantiating the class and using it - promcda = ProMCDA(input_matrix, polarity, sensitivity, robustness, monte_carlo) - promcda.run_mcda() - df_normalized = promcda.normalize() - df_aggregated = promcda.aggregate() + :param polarity: Tuple of polarities for each indicator ("+" or "-"). + + # Optional parameters + :param robustness_weights: Boolean flag indicating whether to perform robustness analysis on weights + (True or False). + :param robustness_indicators: Boolean flag indicating whether to perform robustness analysis on indicators + (True or False). + :param marginal_distributions: Tuple of marginal distributions, which describe the indicators + (the distribution types are defined in the enums class). + :param num_runs: Number of Monte Carlo sampling runs (default: 10000). + :param num_cores: Number of cores used for the calculations (default: 1). + :param random_seed: The random seed used for the sampling (default: 43). + + # Example of instantiating the class and using its methods: + from promcda import ProMCDA + + data = {'Criterion1': [3, 4, 5], 'Criterion2': [7, 2, 8], 'Criterion3': [1, 6, 4]} + input_matrix = pd.DataFrame(data, index=['Alternative1', 'Alternative2', 'Alternative3']) + + # Define polarities for each criterion + polarity = ("+", "-", "+") + + # Optional robustness and distributions + robustness_weights = True + robustness_indicators = False + marginal_distributions = (PDFType.NORMAL, PDFType.UNIFORM, PDFType.NORMAL) + + promcda = ProMCDA(input_matrix=input_matrix, + polarity=polarity, + robustness_weights=robustness_weights, + robustness_indicators=robustness_indicators, + marginal_distributions=marginal_distributions, + num_runs=5000, + num_cores=2, + random_seed=123) + + # Run normalization, aggregation, and MCDA methods + df_normalized = promcda.normalize() + df_aggregated = promcda.aggregate() + promcda.run_mcda() """ self.input_matrix = input_matrix self.polarity = polarity - self.sensitivity = sensitivity - self.robustness = robustness - self.monte_carlo = monte_carlo - self.output_path = output_path - - # TODO: revisit this logic when substitute classes to handle configuration settings - try: - check_configuration_keys(self.sensitivity, self.robustness, self.monte_carlo) - except KeyError as e: - print(f"Configuration Error: {e}") - raise # Optionally re-raise the error after logging it - - self.configuration_settings = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, - self.robustness, self.monte_carlo, self.output_path) - - # TODO: remove this line after implementing the method run_promcda - #is_robustness_indicators, is_robustness_weights, polar, weights, configuration_settings = self.validate_inputs() - #self.run_mcda(is_robustness_indicators, is_robustness_weights, weights) - + self.robustness_weights = robustness_weights + self.robustness_indicators = robustness_indicators + self.num_runs = num_runs + self.marginal_distributions = marginal_distributions + self.num_cores = num_cores + self.random_seed = random_seed self.normalized_matrix = None - self.aggregated_matrix = None - self.ranked_matrix = None + self.scores = None + def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: """ @@ -59,8 +77,8 @@ def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict] Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). """ - configuration_values = extract_configuration_values(self.input_matrix, self.polarity, self.sensitivity, - self.robustness, self.monte_carlo, self.output_path) + configuration_values = extract_configuration_values(self.input_matrix, self.polarity, + self.robustness, self.monte_carlo) is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values( configuration_values) @@ -70,8 +88,14 @@ def normalize(self, method=None) -> pd.DataFrame: """ Normalize the input data using the specified method. + # TODO: for now normalize works only with indicators without uncertanties. Review this logic if needed. + Notes: + The normalizations methods are defined in the NormalizationFunctions enum class. + This method expects the input matrix to not have uncertainties on the indicators. + Parameters: - - method (optional): The normalization method to use. If None, all available methods will be applied. + - method (optional): The normalization method to use. If None, all available methods will be applied for a + Sensitivity Analysis. Returns: - A pd.DataFrame containing the normalized values of each indicator per normalization method. @@ -89,8 +113,14 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= """ Aggregate normalized indicators using the specified method. + # TODO: for now aggregate works only with indicators without uncertanties. Review this logic if needed. + Notes: + The aggregation methods are defined in the AggregationFunctions enum class. + This method expects the input matrix to not have uncertainties on the indicators. + Parameters (optional): - - normalization_method: The normalization method to use. If None, all available methods will be applied. + - normalization_method: The normalization method to use. If None, all available methods will be applied for a + Sensitivity Analysis. - aggregation_method: The aggregation method to use. If None, all available methods will be applied. - weights: The weights to be used for aggregation. If None, they are set all the same. @@ -111,6 +141,40 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= return aggregated_scores + # def aggregate_with_robustness(self, normalization_method=None, aggregation_method=None, weights=None, + # polarity: list, robustness: dict, monte_carlo: dict) -> pd.DataFrame: + # """ + # Estimate scores of alternatives using the specified normalization and aggregation methods. + # + # + # Notes: + # The aggregation methods are defined in the AggregationFunctions enum class. + # + # Parameters (optional): + # - normalization_method: The normalization method to use. If None, all available methods will be applied. + # - aggregation_method: The aggregation method to use. If None, all available methods will be applied. + # - weights: The weights to be used for aggregation. If None, they are set all the same. + # - polarity: List of polarity for each indicator (+ or -). + # - robustness: Robustness analysis configuration. + # - monte_carlo: Monte Carlo sampling configuration. + # + # Returns: + # - A DataFrame containing the aggregated scores per normalization and aggregation methods. + # """ + # + # input_matrix_no_alternatives = check_input_matrix(self.input_matrix) + # mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) + # normalized_indicators = self.normalize(normalization_method) + # + # aggregated_scores = mcda_without_robustness.aggregate_indicators( + # normalized_indicators=normalized_indicators, + # weights=weights, + # agg_method=aggregation_method + # ) + # + # return aggregated_scores + + def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict]): """ diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index d043817..fd6f0c2 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -7,7 +7,7 @@ from mcda.models.ProMCDA import ProMCDA from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions, OutputColumnNames4Sensitivity, \ - NormalizationNames4Sensitivity + NormalizationNames4Sensitivity, PDFType class TestProMCDA(unittest.TestCase): @@ -23,50 +23,48 @@ def setUp(self): self.input_matrix.set_index('Alternatives', inplace=True) self.polarity = ('+', '-',) - self.sensitivity = { - 'sensitivity_on': 'no', - 'normalization': NormalizationFunctions.MINMAX, - 'aggregation': AggregationFunctions.WEIGHTED_SUM - } - - self.robustness = { - 'robustness_on': 'no', - 'on_single_weights': 'yes', - 'on_all_weights': 'no', - 'on_indicators': 'no', - 'given_weights': [0.6, 0.7] - } - - self.monte_carlo = { - 'monte_carlo_runs': 1000, - 'num_cores': 2, - 'random_seed': 42, - 'marginal_distribution_for_each_indicator': 'normal' - } - - self.output_path = 'mock_output/' + # Define optional parameters + self.robustness_weights = False + self.robustness_indicators = False + self.marginal_distributions = (PDFType.NORMAL, PDFType.UNIFORM) + self.num_runs = 5000 + self.num_cores = 2 + self.random_seed = 123 def test_init(self): """ Test if ProMCDA initializes correctly. """ # Given - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) + promcda = ProMCDA( + input_matrix=self.input_matrix, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=self.robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + # Then self.assertEqual(promcda.input_matrix.shape, (3, 2)) self.assertEqual(promcda.polarity, self.polarity) - self.assertEqual(promcda.sensitivity, self.sensitivity) - self.assertEqual(promcda.robustness, self.robustness) - self.assertEqual(promcda.monte_carlo['monte_carlo_runs'], 1000) + self.assertFalse(promcda.robustness_weights) + self.assertFalse(promcda.robustness_indicators) + self.assertEqual(promcda.marginal_distributions, self.marginal_distributions) + self.assertEqual(promcda.num_runs, self.num_runs) + self.assertEqual(promcda.num_cores, self.num_cores) + self.assertEqual(promcda.random_seed, self.random_seed) + self.assertIsNone(promcda.normalized_matrix) + self.assertIsNone(promcda.scores) def test_validate_inputs(self): """ Test if input validation works and returns the expected values. """ # Given - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) + promcda = ProMCDA(self.input_matrix, self.polarity, self.robustness, self.monte_carlo) # When (is_robustness_indicators, is_robustness_weights, polar, weights, config) = promcda.validate_inputs() @@ -154,12 +152,12 @@ def test_aggregate_with_specific_normalization_and_aggregation_methods(self): - def tearDown(self): - """ - Clean up temporary directories and files after each test. - """ - if os.path.exists(self.output_path): - shutil.rmtree(self.output_path) +# def tearDown(self): +# """ +# Clean up temporary directories and files after each test. +# """ +# if os.path.exists(self.output_path): +# shutil.rmtree(self.output_path) if __name__ == '__main__': unittest.main() From f7efe84b74c0738f3d90fd5fda50d8dd6691ff06 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 20 Nov 2024 14:03:58 +0100 Subject: [PATCH 18/27] modify normalize method and test for mcda without robustness --- mcda/models/ProMCDA.py | 45 +++++++-------- mcda/models/mcda_without_robustness.py | 10 ++-- tests/unit_tests/test_promcda.py | 78 +++++++++++++++----------- 3 files changed, 73 insertions(+), 60 deletions(-) diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index b071856..4ebad92 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -4,6 +4,7 @@ from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys +from mcda.configuration.enums import PDFType, NormalizationFunctions from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty, \ check_input_matrix @@ -11,7 +12,8 @@ class ProMCDA: def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robustness_weights: Optional[bool] = None, - robustness_indicators: Optional[bool] = None, marginal_distributions: Optional[bool] = None, + robustness_indicators: Optional[bool] = None, + marginal_distributions: Optional[Tuple[PDFType, ...]] = None, num_runs: Optional[int] = 10000, num_cores: Optional[int] = 1, random_seed: Optional[int] = 43): """ Initialize the ProMCDA class with configuration parameters. @@ -67,24 +69,23 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust self.marginal_distributions = marginal_distributions self.num_cores = num_cores self.random_seed = random_seed - self.normalized_matrix = None + self.normalized_values = None self.scores = None + # def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: + # """ + # Extract and validate input configuration parameters to ensure they are correct. + # Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). + # """ + # + # configuration_values = extract_configuration_values(self.input_matrix, self.polarity, + # self.robustness, self.monte_carlo) + # is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values( + # configuration_values) + # + # return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: - """ - Extract and validate input configuration parameters to ensure they are correct. - Return a flag indicating whether robustness analysis will be performed on indicators (1) or not (0). - """ - - configuration_values = extract_configuration_values(self.input_matrix, self.polarity, - self.robustness, self.monte_carlo) - is_robustness_indicators, is_robustness_weights, polar, weights = check_configuration_values( - configuration_values) - - return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - - def normalize(self, method=None) -> pd.DataFrame: + def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataFrame: """ Normalize the input data using the specified method. @@ -100,14 +101,16 @@ def normalize(self, method=None) -> pd.DataFrame: Returns: - A pd.DataFrame containing the normalized values of each indicator per normalization method. - :param method: str + :param method: NormalizationFunctions :return normalized_df: pd.DataFrame """ input_matrix_no_alternatives = check_input_matrix(self.input_matrix) - mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) - normalized_values = mcda_without_robustness.normalize_indicators(method) - return normalized_values + if not self.robustness_weights and not self.robustness_indicators: + mcda_without_robustness = MCDAWithoutRobustness(self.polarity, input_matrix_no_alternatives) + self.normalized_values = mcda_without_robustness.normalize_indicators(method) + + return self.normalized_values def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: """ @@ -140,7 +143,6 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= return aggregated_scores - # def aggregate_with_robustness(self, normalization_method=None, aggregation_method=None, weights=None, # polarity: list, robustness: dict, monte_carlo: dict) -> pd.DataFrame: # """ @@ -174,7 +176,6 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= # # return aggregated_scores - def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict]): """ diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index d53b289..c6a52a1 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -1,12 +1,12 @@ import sys import copy import logging +from typing import Tuple import numpy as np import pandas as pd -from mcda.configuration.enums import NormalizationFunctions, OutputColumnNames4Sensitivity, \ - NormalizationNames4Sensitivity, AggregationFunctions +from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions from mcda.mcda_functions.normalization import Normalization from mcda.mcda_functions.aggregation import Aggregation @@ -26,10 +26,10 @@ class MCDAWithoutRobustness: However, it's possible to have randomly sampled weights. """ - def __init__(self, config: dict, input_matrix: pd.DataFrame): + def __init__(self, polarity: Tuple[str, ...], input_matrix: pd.DataFrame): self.normalized_indicators = None self.weights = None - self._config = copy.deepcopy(config) + self.polarity = polarity self._input_matrix = copy.deepcopy(input_matrix) import pandas as pd @@ -55,7 +55,7 @@ def normalize_indicators(self, method=None) -> pd.DataFrame: - for the 'standardized' method, two sets of normalized indicators are returned: one with the range (-inf, +inf) and another with the range (0.1, +inf). """ - norm = Normalization(self._input_matrix, self._config["polarity"]) + norm = Normalization(self._input_matrix, self.polarity) normalized_dataframes = [] diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index fd6f0c2..48c2b9d 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -17,8 +17,8 @@ def setUp(self): # Mock input data for testing self.input_matrix = pd.DataFrame({ 'Alternatives': ['A', 'B', 'C'], - 'Criteria 1': [0.5, 0.2, 0.8], - 'Criteria 2': [0.3, 0.6, 0.1] + 'Criterion_1': [0.5, 0.2, 0.8], + 'Criterion_2': [0.3, 0.6, 0.1] }) self.input_matrix.set_index('Alternatives', inplace=True) self.polarity = ('+', '-',) @@ -59,52 +59,64 @@ def test_init(self): self.assertIsNone(promcda.normalized_matrix) self.assertIsNone(promcda.scores) - def test_validate_inputs(self): - """ - Test if input validation works and returns the expected values. - """ - # Given - promcda = ProMCDA(self.input_matrix, self.polarity, self.robustness, self.monte_carlo) - # When - (is_robustness_indicators, is_robustness_weights, polar, weights, config) = promcda.validate_inputs() - - # Then - self.assertIsInstance(is_robustness_indicators, int) - self.assertIsInstance(is_robustness_weights, int) - self.assertIsInstance(polar, tuple) - self.assertIsInstance(weights, list) - self.assertIsInstance(config, dict) - self.assertEqual(is_robustness_indicators, 0) - self.assertEqual(is_robustness_weights, 0) + # def test_validate_inputs(self): + # """ + # Test if input validation works and returns the expected values. + # """ + # # Given + # promcda = ProMCDA(self.input_matrix, self.polarity, self.robustness, self.monte_carlo) + # # When + # (is_robustness_indicators, is_robustness_weights, polar, weights, config) = promcda.validate_inputs() + # + # # Then + # self.assertIsInstance(is_robustness_indicators, int) + # self.assertIsInstance(is_robustness_weights, int) + # self.assertIsInstance(polar, tuple) + # self.assertIsInstance(weights, list) + # self.assertIsInstance(config, dict) + # self.assertEqual(is_robustness_indicators, 0) + # self.assertEqual(is_robustness_weights, 0) def test_normalize_all_methods(self): # Given - self.sensitivity['sensitivity_on'] = 'yes' - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - - - # TODO: delete if return is not a dic - expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] + normalization_method = None + promcda = ProMCDA( + input_matrix=self.input_matrix, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=self.robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) # When - normalized_values = promcda.normalize() + expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] + normalized_values = promcda.normalize(normalization_method) # Then - # TODO: delete if return is not a dic actual_suffixes = {col.split('_',1)[1] for col in normalized_values.columns} self.assertCountEqual(actual_suffixes, expected_suffixes, "Not all methods were applied or extra suffixes found in column names.") def test_normalize_specific_method(self): # Given - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - method = 'minmax' + normalization_method = 'minmax' + promcda = ProMCDA( + input_matrix=self.input_matrix, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=self.robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) # When - normalized_values = promcda.normalize(method=method) - expected_keys = ['0_minmax_01', '1_minmax_01', '0_minmax_without_zero', '1_minmax_without_zero'] + normalized_values = promcda.normalize(method=NormalizationFunctions.MINMAX) + expected_keys = ['Criterion_1_minmax_01', 'Criterion_2_minmax_01', 'Criterion_1_minmax_without_zero', 'Criterion_2_minmax_without_zero'] # Then self.assertCountEqual(expected_keys, list(normalized_values.keys())) From fbf90ee5134c9de05a4fa35060b5ce23a937696c Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 26 Nov 2024 17:17:33 +0100 Subject: [PATCH 19/27] add normalization with robustness --- mcda/models/ProMCDA.py | 37 +++++++++++++++++----- mcda/models/mcda_with_robustness.py | 3 +- mcda/utils/utils_for_main.py | 16 +++++----- mcda/utils/utils_for_parallelization.py | 17 +++++----- tests/unit_tests/test_promcda.py | 42 ++++++++++++++++++++++--- 5 files changed, 86 insertions(+), 29 deletions(-) diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 4ebad92..c0602c3 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -2,12 +2,14 @@ import pandas as pd from typing import Tuple, List, Union, Optional +from build.lib.mcda.mcda_with_robustness import MCDAWithRobustness from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys from mcda.configuration.enums import PDFType, NormalizationFunctions from mcda.models.mcda_without_robustness import MCDAWithoutRobustness +from mcda.utils import utils_for_parallelization from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty, \ - check_input_matrix + check_input_matrix, check_if_pdf_is_exact, check_if_pdf_is_poisson, check_parameters_pdf class ProMCDA: @@ -69,7 +71,8 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust self.marginal_distributions = marginal_distributions self.num_cores = num_cores self.random_seed = random_seed - self.normalized_values = None + self.normalized_values_without_robustness = None + self.normalized_values_with_robustness = None self.scores = None # def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: @@ -89,17 +92,18 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataF """ Normalize the input data using the specified method. - # TODO: for now normalize works only with indicators without uncertanties. Review this logic if needed. Notes: The normalizations methods are defined in the NormalizationFunctions enum class. - This method expects the input matrix to not have uncertainties on the indicators. Parameters: - method (optional): The normalization method to use. If None, all available methods will be applied for a Sensitivity Analysis. Returns: - - A pd.DataFrame containing the normalized values of each indicator per normalization method. + - A pd.DataFrame containing the normalized values of each indicator per normalization method, + if no robustness on indicators is performed. + - A dictionary containing the normalized values of each indicator per normalization method, + if robustness on indicators is performed. :param method: NormalizationFunctions :return normalized_df: pd.DataFrame @@ -108,9 +112,28 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataF if not self.robustness_weights and not self.robustness_indicators: mcda_without_robustness = MCDAWithoutRobustness(self.polarity, input_matrix_no_alternatives) - self.normalized_values = mcda_without_robustness.normalize_indicators(method) + self.normalized_values_without_robustness = mcda_without_robustness.normalize_indicators(method) + + return self.normalized_values_without_robustness + + elif self.robustness_indicators is not None: + check_parameters_pdf(input_matrix_no_alternatives, self.marginal_distributions, for_testing=False) + is_exact_pdf_mask = check_if_pdf_is_exact(self.marginal_distributions) + is_poisson_pdf_mask = check_if_pdf_is_poisson(self.marginal_distributions) + + mcda_with_robustness = MCDAWithRobustness(input_matrix_no_alternatives, self.marginal_distributions, + self.num_runs, is_exact_pdf_mask, is_poisson_pdf_mask, + self.random_seed) + n_random_input_matrices = mcda_with_robustness.create_n_randomly_sampled_matrices() + + if not method: + n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization( + n_random_input_matrices, self.polarity) + else: + n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization( + n_random_input_matrices, self.polarity, method) - return self.normalized_values + self.normalized_values_with_robustness = n_normalized_input_matrices def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: """ diff --git a/mcda/models/mcda_with_robustness.py b/mcda/models/mcda_with_robustness.py index df8e677..dceab06 100644 --- a/mcda/models/mcda_with_robustness.py +++ b/mcda/models/mcda_with_robustness.py @@ -27,12 +27,11 @@ class MCDAWithRobustness: """ - def __init__(self, config: dict, input_matrix: pd.DataFrame(), is_exact_pdf_mask=None, is_poisson_pdf_mask=None, + def __init__(self, input_matrix: pd.DataFrame(), is_exact_pdf_mask=None, is_poisson_pdf_mask=None, random_seed=None): self.is_exact_pdf_mask = is_exact_pdf_mask self.is_poisson_pdf_mask = is_poisson_pdf_mask self.random_seed = random_seed - self._config = copy.deepcopy(config) self._input_matrix = copy.deepcopy(input_matrix) @staticmethod diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 424ab79..4499d1a 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -6,7 +6,7 @@ import logging import sys from enum import Enum -from typing import Union, Any, List +from typing import Union, Any, List, Tuple from typing import Optional import numpy as np @@ -17,6 +17,7 @@ import mcda.utils.utils_for_parallelization as utils_for_parallelization import mcda.utils.utils_for_plotting as utils_for_plotting +from mcda.configuration.enums import PDFType from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.models.mcda_with_robustness import MCDAWithRobustness @@ -482,7 +483,8 @@ def pop_indexed_elements(indexes: np.ndarray, original_list: list) -> list: return new_list -def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=False) -> Union[List[bool], None]: +def check_parameters_pdf(input_matrix: pd.DataFrame, marginal_distributions: Tuple[PDFType, ...], for_testing=False) \ + -> Union[List[bool], None]: """ Check conditions on parameters based on the type of probability distribution function (PDF) for each indicator and raise logging information in case of any problem. @@ -495,7 +497,7 @@ def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=F Parameters: - input_matrix: the input matrix containing uncertainties for indicators, no alternatives. - - config: configuration dictionary containing the Monte Carlo sampling information. + - marginal_distributions: the PDFs associated to each indicator. - for_testing: true only for unit testing Returns: @@ -503,7 +505,7 @@ def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=F - None: default :param input_matrix: pd.DataFrame - :param config: dict + :param marginal_distributions: PDFType :param for_testing: bool :return: Union[list, None] """ @@ -511,7 +513,7 @@ def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=F satisfies_condition = False problem_logged = False - marginal_pdf = config["marginal_distribution_for_each_indicator"] + marginal_pdf = marginal_distributions is_exact_pdf_mask = check_if_pdf_is_exact(marginal_pdf) is_poisson_pdf_mask = check_if_pdf_is_poisson(marginal_pdf) is_uniform_pdf_mask = check_if_pdf_is_uniform(marginal_pdf) @@ -555,7 +557,7 @@ def check_parameters_pdf(input_matrix: pd.DataFrame, config: dict, for_testing=F return list_of_satisfied_conditions -def check_if_pdf_is_exact(marginal_pdf: list) -> list: +def check_if_pdf_is_exact(marginal_pdf: tuple[PDFType, ...]) -> list: """ Check if each indicator's probability distribution function (PDF) is of type 'exact'. @@ -579,7 +581,7 @@ def check_if_pdf_is_exact(marginal_pdf: list) -> list: return exact_pdf_mask -def check_if_pdf_is_poisson(marginal_pdf: list) -> list: +def check_if_pdf_is_poisson(marginal_pdf: tuple[PDFType, ...]) -> list: """ Check if each indicator's probability distribution function (PDF) is of type 'poisson'. diff --git a/mcda/utils/utils_for_parallelization.py b/mcda/utils/utils_for_parallelization.py index 73da4ce..94deb0c 100644 --- a/mcda/utils/utils_for_parallelization.py +++ b/mcda/utils/utils_for_parallelization.py @@ -1,3 +1,4 @@ +from mcda.configuration.enums import PDFType, NormalizationFunctions from mcda.mcda_functions.aggregation import Aggregation from mcda.mcda_functions.normalization import Normalization import sys @@ -52,13 +53,13 @@ def initialize_and_call_aggregation(args: Tuple[list, dict], method=None) -> pd. return scores_one_run -def initialize_and_call_normalization(args: Tuple[pd.DataFrame, list, str]) -> dict: +def initialize_and_call_normalization(args: Tuple[pd.DataFrame, Tuple[str, ...], NormalizationFunctions]) -> dict: """ Initialize a Normalization object with given matrix and polarities, and call the normalization method to calculate normalized indicators. Parameters: - - args: a tuple containing a DataFrame of indicators, a list of polarities, + - args: a tuple containing a DataFrame of indicators, a tuple of polarities, and a string specifying the normalization method. Returns: @@ -131,22 +132,22 @@ def normalize_indicators_in_parallel(norm: object, method=None) -> dict: indicators_scaled_target_without_zero = None indicators_scaled_rank = None - if method is None or method == 'minmax': + if method is None or method == NormalizationFunctions.MINMAX: indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1)) # for aggregation "geometric" and "harmonic" that accept no 0 indicators_scaled_minmax_without_zero = norm.minmax(feature_range=(0.1, 1)) - if method is None or method == 'target': + if method is None or method == NormalizationFunctions.TARGET: indicators_scaled_target_01 = norm.target(feature_range=(0, 1)) # for aggregation "geometric" and "harmonic" that accept no 0 indicators_scaled_target_without_zero = norm.target(feature_range=(0.1, 1)) - if method is None or method == 'standardized': + if method is None or method == NormalizationFunctions.STANDARDIZED: indicators_scaled_standardized_any = norm.standardized( feature_range=('-inf', '+inf')) indicators_scaled_standardized_without_zero = norm.standardized( feature_range=(0.1, '+inf')) - if method is None or method == 'rank': + if method is None or method == NormalizationFunctions.RANK: indicators_scaled_rank = norm.rank() - if method is not None and method not in ['minmax', 'target', 'standardized', 'rank']: + if method is not None and method not in [e for e in NormalizationFunctions]: logger.error('Error Message', stack_info=True) raise ValueError('The selected normalization method is not supported') @@ -253,7 +254,7 @@ def parallelize_aggregation(args: List[tuple], method=None) -> List[pd.DataFrame return res -def parallelize_normalization(input_matrices: List[pd.DataFrame], polar: list, method=None) -> List[dict]: +def parallelize_normalization(input_matrices: List[pd.DataFrame], polar: Tuple[str, ...], method=None) -> List[dict]: """ Parallelize the normalization process for multiple input matrices using multiprocessing. diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 48c2b9d..884a517 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -21,13 +21,23 @@ def setUp(self): 'Criterion_2': [0.3, 0.6, 0.1] }) self.input_matrix.set_index('Alternatives', inplace=True) + + self.input_matrix_with_uncertainty = pd.DataFrame({ + 'Alternatives': ['A', 'B', 'C'], + 'Criterion_1_mean': [0.5, 0.2, 0.8], + 'Criterion_1_std': [0.1, 0.02, 0.07], + 'Criterion_2_mean': [0.3, 0.6, 0.1], + 'Criterion_2_std': [0.03, 0.06, 0.01] + }) + self.input_matrix_with_uncertainty.set_index('Alternatives', inplace=True) + self.polarity = ('+', '-',) # Define optional parameters self.robustness_weights = False self.robustness_indicators = False - self.marginal_distributions = (PDFType.NORMAL, PDFType.UNIFORM) - self.num_runs = 5000 + self.marginal_distributions = (PDFType.NORMAL, PDFType.NORMAL) + self.num_runs = 5 self.num_cores = 2 self.random_seed = 123 @@ -56,7 +66,8 @@ def test_init(self): self.assertEqual(promcda.num_runs, self.num_runs) self.assertEqual(promcda.num_cores, self.num_cores) self.assertEqual(promcda.random_seed, self.random_seed) - self.assertIsNone(promcda.normalized_matrix) + self.assertIsNone(promcda.normalized_values_without_robustness) + self.assertIsNone(promcda.normalized_values_with_robustness) self.assertIsNone(promcda.scores) # def test_validate_inputs(self): @@ -94,15 +105,14 @@ def test_normalize_all_methods(self): # When expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] normalized_values = promcda.normalize(normalization_method) + actual_suffixes = {col.split('_', 2)[-1] for col in normalized_values.columns} # Then - actual_suffixes = {col.split('_',1)[1] for col in normalized_values.columns} self.assertCountEqual(actual_suffixes, expected_suffixes, "Not all methods were applied or extra suffixes found in column names.") def test_normalize_specific_method(self): # Given - normalization_method = 'minmax' promcda = ProMCDA( input_matrix=self.input_matrix, polarity=self.polarity, @@ -122,6 +132,28 @@ def test_normalize_specific_method(self): self.assertCountEqual(expected_keys, list(normalized_values.keys())) self.assertEqual(list(normalized_values), expected_keys) + def test_normalization_with_robustness(self): + # Given + promcda = ProMCDA( + input_matrix=self.input_matrix_with_uncertainty, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=True, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + + # When + n_normalized_matrices = promcda.normalize(method=NormalizationFunctions.MINMAX) + + print(n_normalized_matrices) + + # Then + #self.assertEqual(your_instance.normalized_values_with_robustness, + # ['normalized_matrix1', 'normalized_matrix2']) + def test_aggregate_all_methods(self): # Given self.sensitivity['sensitivity_on'] = 'yes' From 93441d20a148a4a6ce9abbf614fc74535f20e44e Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 27 Nov 2024 09:42:49 +0100 Subject: [PATCH 20/27] modify test for norm with robustness --- tests/unit_tests/test_promcda.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 884a517..5ba4bb6 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -148,11 +148,10 @@ def test_normalization_with_robustness(self): # When n_normalized_matrices = promcda.normalize(method=NormalizationFunctions.MINMAX) - print(n_normalized_matrices) # Then - #self.assertEqual(your_instance.normalized_values_with_robustness, - # ['normalized_matrix1', 'normalized_matrix2']) + self.assertCountEqual(n_normalized_matrices, self.num_runs) + def test_aggregate_all_methods(self): # Given From a064fc01f3061419a6510544c32211a1792bec45 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 27 Nov 2024 15:41:26 +0100 Subject: [PATCH 21/27] add and test normalization with robustness on indicators --- demo_in_notebook/use_promcda_library.ipynb | 529 +++++++-------------- mcda/models/ProMCDA.py | 13 + tests/unit_tests/test_promcda.py | 9 +- 3 files changed, 190 insertions(+), 361 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 04b16fc..cf0ce4e 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 7, "id": "96df2c84-1509-4e93-952a-9beba8d0ec45", "metadata": {}, "outputs": [ @@ -28,7 +28,9 @@ " from mcda.models.ProMCDA import ProMCDA\n", " print(\"Import successful!\")\n", "except ModuleNotFoundError as e:\n", - " print(f\"ModuleNotFoundError: {e}\")" + " print(f\"ModuleNotFoundError: {e}\")\n", + "\n", + "from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions, OutputColumnNames4Sensitivity, NormalizationNames4Sensitivity, PDFType" ] }, { @@ -40,23 +42,26 @@ { "data": { "text/plain": [ - "{'input_matrix': Criteria 1 Criteria 2\n", - " A 0.5 0.3\n", - " B 0.2 0.6\n", - " C 0.8 0.1,\n", + "{'input_matrix': ind1_min ind1_max ind2 ind3_average ind3_std ind4_average \\\n", + " alternatives \n", + " alt1 -15.2 8.2 0.04 24.5 6.20 -15.2 \n", + " alt2 -12.4 8.7 0.05 24.5 4.80 -12.4 \n", + " alt3 10.6 2.0 0.11 14.0 0.60 1.6 \n", + " alt4 -39.7 14.0 0.01 26.5 4.41 -39.7 \n", + " \n", + " ind4_std ind5 ind6_average ind6_std \n", + " alternatives \n", + " alt1 8.2 0.04 24.5 6.20 \n", + " alt2 8.7 0.05 24.5 4.80 \n", + " alt3 2.0 0.11 14.0 0.60 \n", + " alt4 14.0 0.01 26.5 4.41 ,\n", " 'polarity': ('+', '-'),\n", - " 'sensitivity': {'sensitivity_on': 'no',\n", - " 'normalization': 'minmax',\n", - " 'aggregation': 'weighted_sum'},\n", - " 'robustness': {'robustness_on': 'no',\n", - " 'on_single_weights': 'no',\n", - " 'on_all_weights': 'no',\n", - " 'on_indicators': 'no',\n", - " 'given_weights': [0.6, 0.4]},\n", - " 'monte_carlo': {'monte_carlo_runs': 1000,\n", - " 'num_cores': 2,\n", - " 'random_seed': 42,\n", - " 'marginal_distribution_for_each_indicator': 'normal'},\n", + " 'robustness_weights': False,\n", + " 'robustness_indicators': True,\n", + " 'marginal_distributions': [,\n", + " ],\n", + " 'num_runs': 5,\n", + " 'num_cores': 1,\n", " 'output_path': 'mock_output/'}" ] }, @@ -92,36 +97,25 @@ "\n", " polarity = ('+', '-')\n", "\n", - " sensitivity = {\n", - " 'sensitivity_on': 'no',\n", - " 'normalization': 'minmax',\n", - " 'aggregation': 'weighted_sum'\n", - " }\n", + " robustness_weights = False\n", + " robustness_indicators = True\n", "\n", - " robustness = {\n", - " 'robustness_on': 'no',\n", - " 'on_single_weights': 'no',\n", - " 'on_all_weights': 'no',\n", - " 'on_indicators': 'no',\n", - " 'given_weights': [0.6, 0.4]\n", - " }\n", + " marginal_distributions = [PDFType.NORMAL, PDFType.NORMAL]\n", "\n", - " monte_carlo = {\n", - " 'monte_carlo_runs': 1000,\n", - " 'num_cores': 2,\n", - " 'random_seed': 42,\n", - " 'marginal_distribution_for_each_indicator': 'normal'\n", - " }\n", + " num_runs = 5\n", + " num_cores = 1\n", "\n", " output_path = 'mock_output/'\n", "\n", " # Return the setup parameters as a dictionary\n", " return {\n", - " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", + " 'input_matrix': input_matrix_with_uncertainty, # Decide what type of input matrix\n", " 'polarity': polarity,\n", - " 'sensitivity': sensitivity,\n", - " 'robustness': robustness,\n", - " 'monte_carlo': monte_carlo,\n", + " 'robustness_weights': robustness_weights,\n", + " 'robustness_indicators': robustness_indicators,\n", + " 'marginal_distributions': marginal_distributions,\n", + " 'num_runs': num_runs,\n", + " 'num_cores': num_cores,\n", " 'output_path': output_path\n", " }\n", "\n", @@ -142,10 +136,12 @@ "promcda = ProMCDA(\n", " input_matrix=setup_parameters['input_matrix'],\n", " polarity=setup_parameters['polarity'],\n", - " sensitivity=setup_parameters['sensitivity'],\n", - " robustness=setup_parameters['robustness'],\n", - " monte_carlo=setup_parameters['monte_carlo'],\n", - " output_path=setup_parameters['output_path']\n", + " robustness_weights=setup_parameters['robustness_weights'],\n", + " robustness_indicators=setup_parameters['robustness_indicators'],\n", + " marginal_distributions=setup_parameters['marginal_distributions'],\n", + " num_runs=setup_parameters['num_runs'],\n", + " num_cores=setup_parameters['num_cores'],\n", + " #output_path=setup_parameters['output_path']\n", ")" ] }, @@ -161,132 +157,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-15 18:27:41,569 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-11-27 15:37:19,707 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n", + "INFO: 2024-11-27 15:37:19,713 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", + "INFO: 2024-11-27 15:37:19,714 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", + "INFO: 2024-11-27 15:37:19,715 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", + "INFO: 2024-11-27 15:37:19,716 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n" ] }, { "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zeroCriteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zeroCriteria 1_standardized_anyCriteria 2_standardized_anyCriteria 1_standardized_without_zeroCriteria 2_standardized_without_zeroCriteria 1_rankCriteria 2_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", - "
" - ], "text/plain": [ - " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", - "0 0.5 0.6 0.55 \n", - "1 0.0 0.0 0.1 \n", - "2 1.0 1.0 1.0 \n", - "\n", - " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", - "0 0.64 0.625 0.5 \n", - "1 0.1 0.25 0.0 \n", - "2 1.0 1.0 0.833333 \n", - "\n", - " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", - "0 0.6625 0.55 \n", - "1 0.325 0.1 \n", - "2 1.0 0.85 \n", - "\n", - " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", - "0 0.0 0.132453 \n", - "1 -1.0 -1.059626 \n", - "2 1.0 0.927173 \n", - "\n", - " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", - "0 1.1 1.292079 \n", - "1 0.1 0.1 \n", - "2 2.1 2.086799 \n", - "\n", - " Criteria 1_rank Criteria 2_rank \n", - "0 2.0 2.0 \n", - "1 1.0 1.0 \n", - "2 3.0 3.0 " + "'5 normalized matrices have been normalized.'" ] }, "execution_count": 10, @@ -308,209 +189,145 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-15 18:27:42,533 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-11-27 15:39:18,726 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n", + "INFO: 2024-11-27 15:39:18,735 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", + "INFO: 2024-11-27 15:39:18,736 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", + "INFO: 2024-11-27 15:39:18,737 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", + "INFO: 2024-11-27 15:39:18,738 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n", + "ERROR: 2024-11-27 15:39:19,851 - ProMCDA utils for parallelization - Error Message\n", + "Stack (most recent call last):\n", + " File \"\", line 1, in \n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", + " exitcode = _main(fd, parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", + " return self._bootstrap(parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", + " self.run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", + " result = (True, func(*args, **kwds))\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", + " return list(map(*args))\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", + " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", + " logger.error('Error Message', stack_info=True)\n", + "ERROR: 2024-11-27 15:39:19,897 - ProMCDA utils for parallelization - Error Message\n", + "Stack (most recent call last):\n", + " File \"\", line 1, in \n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", + " exitcode = _main(fd, parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", + " return self._bootstrap(parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", + " self.run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", + " result = (True, func(*args, **kwds))\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", + " return list(map(*args))\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", + " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", + " logger.error('Error Message', stack_info=True)\n", + "ERROR: 2024-11-27 15:39:19,898 - ProMCDA utils for parallelization - Error Message\n", + "Stack (most recent call last):\n", + " File \"\", line 1, in \n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", + " exitcode = _main(fd, parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", + " return self._bootstrap(parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", + " self.run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", + " result = (True, func(*args, **kwds))\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", + " return list(map(*args))\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", + " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", + " logger.error('Error Message', stack_info=True)\n", + "ERROR: 2024-11-27 15:39:19,899 - ProMCDA utils for parallelization - Error Message\n", + "Stack (most recent call last):\n", + " File \"\", line 1, in \n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", + " exitcode = _main(fd, parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", + " return self._bootstrap(parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", + " self.run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", + " result = (True, func(*args, **kwds))\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", + " return list(map(*args))\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", + " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", + " logger.error('Error Message', stack_info=True)\n", + "ERROR: 2024-11-27 15:39:19,903 - ProMCDA utils for parallelization - Error Message\n", + "Stack (most recent call last):\n", + " File \"\", line 1, in \n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", + " exitcode = _main(fd, parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", + " return self._bootstrap(parent_sentinel)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", + " self.run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", + " self._target(*self._args, **self._kwargs)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", + " result = (True, func(*args, **kwds))\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", + " return list(map(*args))\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", + " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", + " logger.error('Error Message', stack_info=True)\n" ] }, { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Criteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zero
00.6250.50.66250.55
10.250.00.3250.1
21.00.8333331.00.85
\n", - "
" - ], - "text/plain": [ - " Criteria 1_target_01 Criteria 2_target_01 Criteria 1_target_without_zero \\\n", - "0 0.625 0.5 0.6625 \n", - "1 0.25 0.0 0.325 \n", - "2 1.0 0.833333 1.0 \n", - "\n", - " Criteria 2_target_without_zero \n", - "0 0.55 \n", - "1 0.1 \n", - "2 0.85 " - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" + "ename": "ValueError", + "evalue": "The selected normalization method is not supported", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRemoteTraceback\u001b[0m Traceback (most recent call last)", + "\u001b[0;31mRemoteTraceback\u001b[0m: \n\"\"\"\nTraceback (most recent call last):\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n result = (True, func(*args, **kwds))\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n return list(map(*args))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 152, in normalize_indicators_in_parallel\n raise ValueError('The selected normalization method is not supported')\nValueError: The selected normalization method is not supported\n\"\"\"", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_26019/425727362.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"target\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mnormalize\u001b[0;34m(self, method)\u001b[0m\n\u001b[1;32m 131\u001b[0m n_random_input_matrices, self.polarity)\n\u001b[1;32m 132\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 133\u001b[0;31m n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(\n\u001b[0m\u001b[1;32m 134\u001b[0m n_random_input_matrices, self.polarity, method)\n\u001b[1;32m 135\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\u001b[0m in \u001b[0;36mparallelize_normalization\u001b[0;34m(input_matrices, polar, method)\u001b[0m\n\u001b[1;32m 289\u001b[0m \u001b[0mpool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmultiprocessing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdf\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minput_matrices\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 291\u001b[0;31m \u001b[0mres\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitialize_and_call_normalization\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 292\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mmap\u001b[0;34m(self, func, iterable, chunksize)\u001b[0m\n\u001b[1;32m 362\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m \u001b[0mlist\u001b[0m \u001b[0mthat\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0mreturned\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 363\u001b[0m '''\n\u001b[0;32m--> 364\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_map_async\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmapstar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 365\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstarmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mget\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 769\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 770\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 771\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 772\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 773\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_set\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: The selected normalization method is not supported" + ] } ], "source": [ - "promcda.normalize(\"target\")" + "promcda.normalize(Normalization)" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, + "id": "0038ddb3-5499-4fd4-a5de-d785593885a9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, "id": "e857a02a-aae8-453a-a302-46844c3e610f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 2024-11-15 18:27:43,078 - ProMCDA - Alternatives are ['A', 'B', 'C']\n", - "INFO: 2024-11-15 18:27:43,082 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
minmax_weighted_summinmax_geometricminmax_minimumtarget_weighted_sumtarget_geometrictarget_minimumstandardized_weighted_sumstandardized_geometricstandardized_minimumrank_weighted_sumrank_geometricrank_minimum
00.5583330.6008360.50.5520830.5943460.50.0772641.2082740.02.0NaN2.0
10.00.1000000.00.1041670.1634120.0-1.0347820.100000-1.0596261.0NaN1.0
21.01.0000001.00.9027780.9095520.8333330.9575172.0922890.9271733.0NaN3.0
\n", - "
" - ], - "text/plain": [ - " minmax_weighted_sum minmax_geometric minmax_minimum target_weighted_sum \\\n", - "0 0.558333 0.600836 0.5 0.552083 \n", - "1 0.0 0.100000 0.0 0.104167 \n", - "2 1.0 1.000000 1.0 0.902778 \n", - "\n", - " target_geometric target_minimum standardized_weighted_sum \\\n", - "0 0.594346 0.5 0.077264 \n", - "1 0.163412 0.0 -1.034782 \n", - "2 0.909552 0.833333 0.957517 \n", - "\n", - " standardized_geometric standardized_minimum rank_weighted_sum \\\n", - "0 1.208274 0.0 2.0 \n", - "1 0.100000 -1.059626 1.0 \n", - "2 2.092289 0.927173 3.0 \n", - "\n", - " rank_geometric rank_minimum \n", - "0 NaN 2.0 \n", - "1 NaN 1.0 \n", - "2 NaN 3.0 " - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "promcda.aggregate(None, None,[0.5, 0.7])" ] diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index c0602c3..1089a44 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -135,6 +135,19 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataF self.normalized_values_with_robustness = n_normalized_input_matrices + return f"{self.num_runs} normalized matrices have been normalized." + + + def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: + """ + Getter method to access normalized values when robustness on indicators is performed. + + Returns: + A dictionary containing normalized values if robustness is enabled; otherwise None. + """ + return getattr(self, 'normalized_values_with_robustness', None) + + def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: """ Aggregate normalized indicators using the specified method. diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 5ba4bb6..950d4a2 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -1,5 +1,3 @@ -import os -import shutil import unittest import warnings @@ -146,11 +144,12 @@ def test_normalization_with_robustness(self): ) # When - n_normalized_matrices = promcda.normalize(method=NormalizationFunctions.MINMAX) - + promcda.normalize(method=NormalizationFunctions.MINMAX) # Then - self.assertCountEqual(n_normalized_matrices, self.num_runs) + normalized_values = promcda.get_normalized_values_with_robustness() + self.assertIsNotNone(normalized_values) + self.assertEqual(len(normalized_values), self.num_runs) def test_aggregate_all_methods(self): From a770cc15561b8ea935f8929d72196cfaffaf02dd Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 28 Nov 2024 14:44:44 +0100 Subject: [PATCH 22/27] finalize normalization --- demo_in_notebook/use_promcda_library.ipynb | 354 ++++++++++++--------- mcda/models/ProMCDA.py | 20 +- 2 files changed, 215 insertions(+), 159 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index cf0ce4e..4e401fe 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "id": "96df2c84-1509-4e93-952a-9beba8d0ec45", "metadata": {}, "outputs": [ @@ -35,29 +35,20 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'input_matrix': ind1_min ind1_max ind2 ind3_average ind3_std ind4_average \\\n", - " alternatives \n", - " alt1 -15.2 8.2 0.04 24.5 6.20 -15.2 \n", - " alt2 -12.4 8.7 0.05 24.5 4.80 -12.4 \n", - " alt3 10.6 2.0 0.11 14.0 0.60 1.6 \n", - " alt4 -39.7 14.0 0.01 26.5 4.41 -39.7 \n", - " \n", - " ind4_std ind5 ind6_average ind6_std \n", - " alternatives \n", - " alt1 8.2 0.04 24.5 6.20 \n", - " alt2 8.7 0.05 24.5 4.80 \n", - " alt3 2.0 0.11 14.0 0.60 \n", - " alt4 14.0 0.01 26.5 4.41 ,\n", + "{'input_matrix': Criteria 1 Criteria 2\n", + " A 0.5 0.3\n", + " B 0.2 0.6\n", + " C 0.8 0.1,\n", " 'polarity': ('+', '-'),\n", - " 'robustness_weights': False,\n", - " 'robustness_indicators': True,\n", + " 'robustness_weights': True,\n", + " 'robustness_indicators': False,\n", " 'marginal_distributions': [,\n", " ],\n", " 'num_runs': 5,\n", @@ -65,7 +56,7 @@ " 'output_path': 'mock_output/'}" ] }, - "execution_count": 8, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -97,8 +88,8 @@ "\n", " polarity = ('+', '-')\n", "\n", - " robustness_weights = False\n", - " robustness_indicators = True\n", + " robustness_weights = True\n", + " robustness_indicators = False\n", "\n", " marginal_distributions = [PDFType.NORMAL, PDFType.NORMAL]\n", "\n", @@ -109,7 +100,7 @@ "\n", " # Return the setup parameters as a dictionary\n", " return {\n", - " 'input_matrix': input_matrix_with_uncertainty, # Decide what type of input matrix\n", + " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", " 'polarity': polarity,\n", " 'robustness_weights': robustness_weights,\n", " 'robustness_indicators': robustness_indicators,\n", @@ -128,7 +119,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 6, "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", "metadata": {}, "outputs": [], @@ -147,7 +138,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 7, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true @@ -157,20 +148,135 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-27 15:37:19,707 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n", - "INFO: 2024-11-27 15:37:19,713 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", - "INFO: 2024-11-27 15:37:19,714 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", - "INFO: 2024-11-27 15:37:19,715 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", - "INFO: 2024-11-27 15:37:19,716 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n" + "INFO: 2024-11-28 11:50:31,547 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] }, { "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zeroCriteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zeroCriteria 1_standardized_anyCriteria 2_standardized_anyCriteria 1_standardized_without_zeroCriteria 2_standardized_without_zeroCriteria 1_rankCriteria 2_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", + "
" + ], "text/plain": [ - "'5 normalized matrices have been normalized.'" + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", + "0 0.64 0.625 0.5 \n", + "1 0.1 0.25 0.0 \n", + "2 1.0 1.0 0.833333 \n", + "\n", + " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", + "0 0.6625 0.55 \n", + "1 0.325 0.1 \n", + "2 1.0 0.85 \n", + "\n", + " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", + "0 0.0 0.132453 \n", + "1 -1.0 -1.059626 \n", + "2 1.0 0.927173 \n", + "\n", + " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", + "0 1.1 1.292079 \n", + "1 0.1 0.1 \n", + "2 2.1 2.086799 \n", + "\n", + " Criteria 1_rank Criteria 2_rank \n", + "0 2.0 2.0 \n", + "1 1.0 1.0 \n", + "2 3.0 3.0 " ] }, - "execution_count": 10, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -181,7 +287,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 8, "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", "metadata": {}, "outputs": [ @@ -189,129 +295,81 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-27 15:39:18,726 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n", - "INFO: 2024-11-27 15:39:18,735 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", - "INFO: 2024-11-27 15:39:18,736 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", - "INFO: 2024-11-27 15:39:18,737 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", - "INFO: 2024-11-27 15:39:18,738 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n", - "ERROR: 2024-11-27 15:39:19,851 - ProMCDA utils for parallelization - Error Message\n", - "Stack (most recent call last):\n", - " File \"\", line 1, in \n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", - " exitcode = _main(fd, parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", - " return self._bootstrap(parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", - " self.run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", - " result = (True, func(*args, **kwds))\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", - " return list(map(*args))\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", - " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", - " logger.error('Error Message', stack_info=True)\n", - "ERROR: 2024-11-27 15:39:19,897 - ProMCDA utils for parallelization - Error Message\n", - "Stack (most recent call last):\n", - " File \"\", line 1, in \n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", - " exitcode = _main(fd, parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", - " return self._bootstrap(parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", - " self.run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", - " result = (True, func(*args, **kwds))\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", - " return list(map(*args))\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", - " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", - " logger.error('Error Message', stack_info=True)\n", - "ERROR: 2024-11-27 15:39:19,898 - ProMCDA utils for parallelization - Error Message\n", - "Stack (most recent call last):\n", - " File \"\", line 1, in \n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", - " exitcode = _main(fd, parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", - " return self._bootstrap(parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", - " self.run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", - " result = (True, func(*args, **kwds))\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", - " return list(map(*args))\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", - " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", - " logger.error('Error Message', stack_info=True)\n", - "ERROR: 2024-11-27 15:39:19,899 - ProMCDA utils for parallelization - Error Message\n", - "Stack (most recent call last):\n", - " File \"\", line 1, in \n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", - " exitcode = _main(fd, parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", - " return self._bootstrap(parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", - " self.run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", - " result = (True, func(*args, **kwds))\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", - " return list(map(*args))\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", - " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", - " logger.error('Error Message', stack_info=True)\n", - "ERROR: 2024-11-27 15:39:19,903 - ProMCDA utils for parallelization - Error Message\n", - "Stack (most recent call last):\n", - " File \"\", line 1, in \n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 116, in spawn_main\n", - " exitcode = _main(fd, parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/spawn.py\", line 129, in _main\n", - " return self._bootstrap(parent_sentinel)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 315, in _bootstrap\n", - " self.run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/process.py\", line 108, in run\n", - " self._target(*self._args, **self._kwargs)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n", - " result = (True, func(*args, **kwds))\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n", - " return list(map(*args))\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n", - " dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 151, in normalize_indicators_in_parallel\n", - " logger.error('Error Message', stack_info=True)\n" + "INFO: 2024-11-28 11:50:36,472 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] }, { - "ename": "ValueError", - "evalue": "The selected normalization method is not supported", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mRemoteTraceback\u001b[0m Traceback (most recent call last)", - "\u001b[0;31mRemoteTraceback\u001b[0m: \n\"\"\"\nTraceback (most recent call last):\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n result = (True, func(*args, **kwds))\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n return list(map(*args))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 95, in initialize_and_call_normalization\n dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 152, in normalize_indicators_in_parallel\n raise ValueError('The selected normalization method is not supported')\nValueError: The selected normalization method is not supported\n\"\"\"", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_26019/425727362.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"target\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mnormalize\u001b[0;34m(self, method)\u001b[0m\n\u001b[1;32m 131\u001b[0m n_random_input_matrices, self.polarity)\n\u001b[1;32m 132\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 133\u001b[0;31m n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(\n\u001b[0m\u001b[1;32m 134\u001b[0m n_random_input_matrices, self.polarity, method)\n\u001b[1;32m 135\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\u001b[0m in \u001b[0;36mparallelize_normalization\u001b[0;34m(input_matrices, polar, method)\u001b[0m\n\u001b[1;32m 289\u001b[0m \u001b[0mpool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmultiprocessing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 290\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdf\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minput_matrices\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 291\u001b[0;31m \u001b[0mres\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitialize_and_call_normalization\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 292\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 293\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mmap\u001b[0;34m(self, func, iterable, chunksize)\u001b[0m\n\u001b[1;32m 362\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m \u001b[0mlist\u001b[0m \u001b[0mthat\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0mreturned\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 363\u001b[0m '''\n\u001b[0;32m--> 364\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_map_async\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmapstar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 365\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstarmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mget\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 769\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 770\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 771\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 772\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 773\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_set\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: The selected normalization method is not supported" - ] + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Criteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zero
00.6250.50.66250.55
10.250.00.3250.1
21.00.8333331.00.85
\n", + "
" + ], + "text/plain": [ + " Criteria 1_target_01 Criteria 2_target_01 Criteria 1_target_without_zero \\\n", + "0 0.625 0.5 0.6625 \n", + "1 0.25 0.0 0.325 \n", + "2 1.0 0.833333 1.0 \n", + "\n", + " Criteria 2_target_without_zero \n", + "0 0.55 \n", + "1 0.1 \n", + "2 0.85 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "promcda.normalize(Normalization)" + "promcda.normalize(NormalizationFunctions.TARGET)" ] }, { diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 1089a44..05040d4 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -88,7 +88,7 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust # # return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataFrame: + def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd.DataFrame, str]: """ Normalize the input data using the specified method. @@ -110,13 +110,13 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataF """ input_matrix_no_alternatives = check_input_matrix(self.input_matrix) - if not self.robustness_weights and not self.robustness_indicators: + if not self.robustness_indicators: mcda_without_robustness = MCDAWithoutRobustness(self.polarity, input_matrix_no_alternatives) self.normalized_values_without_robustness = mcda_without_robustness.normalize_indicators(method) return self.normalized_values_without_robustness - elif self.robustness_indicators is not None: + elif self.robustness_indicators is not None and self.robustness_weights is None: check_parameters_pdf(input_matrix_no_alternatives, self.marginal_distributions, for_testing=False) is_exact_pdf_mask = check_if_pdf_is_exact(self.marginal_distributions) is_poisson_pdf_mask = check_if_pdf_is_poisson(self.marginal_distributions) @@ -135,7 +135,11 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> pd.DataF self.normalized_values_with_robustness = n_normalized_input_matrices - return f"{self.num_runs} normalized matrices have been normalized." + return f"{self.num_runs} randomly sampled matrices have been normalized." + + if self.robustness_weights and self.robustness_indicators: + raise ValueError( + "Inconsistent configuration: 'robustness_weights' and 'robustness_indicators' are both enabled.") def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: @@ -148,18 +152,14 @@ def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: return getattr(self, 'normalized_values_with_robustness', None) - def aggregate(self, normalization_method=None, aggregation_method=None, weights=None) -> pd.DataFrame: + def aggregate(self, aggregation_method=None, weights=None) -> pd.DataFrame: """ Aggregate normalized indicators using the specified method. - # TODO: for now aggregate works only with indicators without uncertanties. Review this logic if needed. Notes: The aggregation methods are defined in the AggregationFunctions enum class. - This method expects the input matrix to not have uncertainties on the indicators. Parameters (optional): - - normalization_method: The normalization method to use. If None, all available methods will be applied for a - Sensitivity Analysis. - aggregation_method: The aggregation method to use. If None, all available methods will be applied. - weights: The weights to be used for aggregation. If None, they are set all the same. @@ -167,8 +167,6 @@ def aggregate(self, normalization_method=None, aggregation_method=None, weights= - A DataFrame containing the aggregated scores per normalization and aggregation methods. """ - input_matrix_no_alternatives = check_input_matrix(self.input_matrix) - mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) normalized_indicators = self.normalize(normalization_method) aggregated_scores = mcda_without_robustness.aggregate_indicators( From d587fe3844227c494e01e2e22a2b38dfe712e2b3 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Tue, 3 Dec 2024 17:31:41 +0100 Subject: [PATCH 23/27] add aggregation with specific method --- demo_in_notebook/use_promcda_library.ipynb | 209 ++++++++------------- mcda/mcda_functions/aggregation.py | 3 + mcda/models/ProMCDA.py | 66 +++++-- mcda/models/mcda_without_robustness.py | 35 ++-- tests/unit_tests/test_promcda.py | 97 ++++++---- 5 files changed, 198 insertions(+), 212 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 4e401fe..9096074 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", "metadata": {}, "outputs": [ @@ -47,7 +47,7 @@ " B 0.2 0.6\n", " C 0.8 0.1,\n", " 'polarity': ('+', '-'),\n", - " 'robustness_weights': True,\n", + " 'robustness_weights': False,\n", " 'robustness_indicators': False,\n", " 'marginal_distributions': [,\n", " ],\n", @@ -56,7 +56,7 @@ " 'output_path': 'mock_output/'}" ] }, - "execution_count": 5, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -88,7 +88,7 @@ "\n", " polarity = ('+', '-')\n", "\n", - " robustness_weights = True\n", + " robustness_weights = False\n", " robustness_indicators = False\n", "\n", " marginal_distributions = [PDFType.NORMAL, PDFType.NORMAL]\n", @@ -119,10 +119,18 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-03 17:26:36,915 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + } + ], "source": [ "promcda = ProMCDA(\n", " input_matrix=setup_parameters['input_matrix'],\n", @@ -138,19 +146,22 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true }, + "outputs": [], + "source": [ + "#promcda.normalize()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: 2024-11-28 11:50:31,547 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" - ] - }, { "data": { "text/html": [ @@ -176,16 +187,6 @@ " Criteria 2_minmax_01\n", " Criteria 1_minmax_without_zero\n", " Criteria 2_minmax_without_zero\n", - " Criteria 1_target_01\n", - " Criteria 2_target_01\n", - " Criteria 1_target_without_zero\n", - " Criteria 2_target_without_zero\n", - " Criteria 1_standardized_any\n", - " Criteria 2_standardized_any\n", - " Criteria 1_standardized_without_zero\n", - " Criteria 2_standardized_without_zero\n", - " Criteria 1_rank\n", - " Criteria 2_rank\n", " \n", " \n", " \n", @@ -195,16 +196,6 @@ " 0.6\n", " 0.55\n", " 0.64\n", - " 0.625\n", - " 0.5\n", - " 0.6625\n", - " 0.55\n", - " 0.0\n", - " 0.132453\n", - " 1.1\n", - " 1.292079\n", - " 2.0\n", - " 2.0\n", " \n", " \n", " 1\n", @@ -212,16 +203,6 @@ " 0.0\n", " 0.1\n", " 0.1\n", - " 0.25\n", - " 0.0\n", - " 0.325\n", - " 0.1\n", - " -1.0\n", - " -1.059626\n", - " 0.1\n", - " 0.1\n", - " 1.0\n", - " 1.0\n", " \n", " \n", " 2\n", @@ -229,16 +210,6 @@ " 1.0\n", " 1.0\n", " 1.0\n", - " 1.0\n", - " 0.833333\n", - " 1.0\n", - " 0.85\n", - " 1.0\n", - " 0.927173\n", - " 2.1\n", - " 2.086799\n", - " 3.0\n", - " 3.0\n", " \n", " \n", "\n", @@ -250,52 +221,65 @@ "1 0.0 0.0 0.1 \n", "2 1.0 1.0 1.0 \n", "\n", - " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", - "0 0.64 0.625 0.5 \n", - "1 0.1 0.25 0.0 \n", - "2 1.0 1.0 0.833333 \n", - "\n", - " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", - "0 0.6625 0.55 \n", - "1 0.325 0.1 \n", - "2 1.0 0.85 \n", - "\n", - " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", - "0 0.0 0.132453 \n", - "1 -1.0 -1.059626 \n", - "2 1.0 0.927173 \n", - "\n", - " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", - "0 1.1 1.292079 \n", - "1 0.1 0.1 \n", - "2 2.1 2.086799 \n", - "\n", - " Criteria 1_rank Criteria 2_rank \n", - "0 2.0 2.0 \n", - "1 1.0 1.0 \n", - "2 3.0 3.0 " + " Criteria 2_minmax_without_zero \n", + "0 0.64 \n", + "1 0.1 \n", + "2 1.0 " ] }, - "execution_count": 7, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "promcda.normalize()" + "promcda.normalize(NormalizationFunctions.MINMAX)" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "execution_count": 6, + "id": "0038ddb3-5499-4fd4-a5de-d785593885a9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " Criteria 2_minmax_without_zero \n", + "0 0.64 \n", + "1 0.1 \n", + "2 1.0 \n" + ] + } + ], + "source": [ + "print(promcda.normalized_values_without_robustness)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e857a02a-aae8-453a-a302-46844c3e610f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-11-28 11:50:36,472 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "Normalization method minmax\n", + "Entering weighted sum\n", + "Columns: Criteria 1_minmax_01 Criteria 2_minmax_01\n", + "0 0.5 0.6\n", + "1 0.0 0.0\n", + "2 1.0 1.0\n", + "Normalized indicators shape: (3, 2)\n", + "Weights: [0.5, 0.5]\n" ] }, { @@ -319,85 +303,40 @@ " \n", " \n", " \n", - " Criteria 1_target_01\n", - " Criteria 2_target_01\n", - " Criteria 1_target_without_zero\n", - " Criteria 2_target_without_zero\n", + " minmax_weighted_sum\n", " \n", " \n", " \n", " \n", " 0\n", - " 0.625\n", - " 0.5\n", - " 0.6625\n", " 0.55\n", " \n", " \n", " 1\n", - " 0.25\n", " 0.0\n", - " 0.325\n", - " 0.1\n", " \n", " \n", " 2\n", " 1.0\n", - " 0.833333\n", - " 1.0\n", - " 0.85\n", " \n", " \n", "\n", "" ], "text/plain": [ - " Criteria 1_target_01 Criteria 2_target_01 Criteria 1_target_without_zero \\\n", - "0 0.625 0.5 0.6625 \n", - "1 0.25 0.0 0.325 \n", - "2 1.0 0.833333 1.0 \n", - "\n", - " Criteria 2_target_without_zero \n", - "0 0.55 \n", - "1 0.1 \n", - "2 0.85 " + " minmax_weighted_sum\n", + "0 0.55\n", + "1 0.0\n", + "2 1.0" ] }, - "execution_count": 8, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "promcda.normalize(NormalizationFunctions.TARGET)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0038ddb3-5499-4fd4-a5de-d785593885a9", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e857a02a-aae8-453a-a302-46844c3e610f", - "metadata": {}, - "outputs": [], - "source": [ - "promcda.aggregate(None, None,[0.5, 0.7])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6e0949ed-f2a7-4c90-87f8-5d61d27da7ac", - "metadata": {}, - "outputs": [], - "source": [ - "promcda.aggregate(\"minmax\", \"geometric\",[0.5, 0.7])" + "promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM, weights=None)" ] }, { diff --git a/mcda/mcda_functions/aggregation.py b/mcda/mcda_functions/aggregation.py index 77071ef..e819e29 100644 --- a/mcda/mcda_functions/aggregation.py +++ b/mcda/mcda_functions/aggregation.py @@ -36,6 +36,9 @@ def weighted_sum(self, norm_indicators: pd.DataFrame) -> pd.Series(dtype='object :returns scores: pd.Series """ + print("Normalized indicators shape:", norm_indicators.shape) + print("Weights:", self.weights if self.weights is not None else "Weights are None") + scores = (norm_indicators * self.weights).sum(axis=1) return scores diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index 05040d4..abbc64c 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -5,7 +5,7 @@ from build.lib.mcda.mcda_with_robustness import MCDAWithRobustness from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ check_configuration_keys -from mcda.configuration.enums import PDFType, NormalizationFunctions +from mcda.configuration.enums import PDFType, NormalizationFunctions, AggregationFunctions from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.utils import utils_for_parallelization from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty, \ @@ -75,6 +75,8 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust self.normalized_values_with_robustness = None self.scores = None + self.input_matrix_no_alternatives = check_input_matrix(self.input_matrix) + # def validate_inputs(self) -> Tuple[int, int, list, Union[list, List[list], dict], dict]: # """ # Extract and validate input configuration parameters to ensure they are correct. @@ -102,26 +104,23 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd Returns: - A pd.DataFrame containing the normalized values of each indicator per normalization method, if no robustness on indicators is performed. - - A dictionary containing the normalized values of each indicator per normalization method, - if robustness on indicators is performed. :param method: NormalizationFunctions - :return normalized_df: pd.DataFrame + :return normalized_df: pd.DataFrame or string """ - input_matrix_no_alternatives = check_input_matrix(self.input_matrix) if not self.robustness_indicators: - mcda_without_robustness = MCDAWithoutRobustness(self.polarity, input_matrix_no_alternatives) + mcda_without_robustness = MCDAWithoutRobustness(self.polarity, self.input_matrix_no_alternatives) self.normalized_values_without_robustness = mcda_without_robustness.normalize_indicators(method) return self.normalized_values_without_robustness - elif self.robustness_indicators is not None and self.robustness_weights is None: - check_parameters_pdf(input_matrix_no_alternatives, self.marginal_distributions, for_testing=False) + elif self.robustness_indicators and not self.robustness_weights: + check_parameters_pdf(self.input_matrix_no_alternatives, self.marginal_distributions, for_testing=False) is_exact_pdf_mask = check_if_pdf_is_exact(self.marginal_distributions) is_poisson_pdf_mask = check_if_pdf_is_poisson(self.marginal_distributions) - mcda_with_robustness = MCDAWithRobustness(input_matrix_no_alternatives, self.marginal_distributions, + mcda_with_robustness = MCDAWithRobustness(self.input_matrix_no_alternatives, self.marginal_distributions, self.num_runs, is_exact_pdf_mask, is_poisson_pdf_mask, self.random_seed) n_random_input_matrices = mcda_with_robustness.create_n_randomly_sampled_matrices() @@ -141,7 +140,6 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd raise ValueError( "Inconsistent configuration: 'robustness_weights' and 'robustness_indicators' are both enabled.") - def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: """ Getter method to access normalized values when robustness on indicators is performed. @@ -151,29 +149,57 @@ def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: """ return getattr(self, 'normalized_values_with_robustness', None) - - def aggregate(self, aggregation_method=None, weights=None) -> pd.DataFrame: + def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, weights: Optional[None] = None) \ + -> Union[pd.DataFrame, str]: """ Aggregate normalized indicators using the specified method. Notes: The aggregation methods are defined in the AggregationFunctions enum class. + This method should follow the normalization. It acquires the normalized + values from the normalization step. Parameters (optional): - aggregation_method: The aggregation method to use. If None, all available methods will be applied. - - weights: The weights to be used for aggregation. If None, they are set all the same. + - weights: The weights to be used for aggregation. If None, they are set all the same. Or, if robustness on + weights is enabled, then the weights are sampled from the Monte Carlo simulation. Returns: - - A DataFrame containing the aggregated scores per normalization and aggregation methods. + - A pd.DataFrame containing the aggregated scores per normalization and aggregation methods, + if no robustness on indicators is not performed. + + :param aggregation_method: AggregationFunctions + :param weights : list or None + :return scores_df: pd.DataFrame or string """ + # TODO: 2. Check if robustness on weights is enabled, then sample the weights from the Monte Carlo simulation. + # TODO: 3. Check if aggregation_method is None, then apply all available methods. + if weights is None: + if not self.robustness_indicators: + num_indicators = self.input_matrix_no_alternatives.shape[1] + else: + num_non_indicators = ( + len(self.marginal_distributions) - self.marginal_distributions.count('exact') + - self.marginal_distributions.count('poisson')) + num_indicators = (self.input_matrix_no_alternatives.shape[1] - num_non_indicators) + weights = [0.5] * num_indicators - normalized_indicators = self.normalize(normalization_method) + if self.robustness_weights is True: + raise ValueError("Robustness on weights is not yet implemented.") - aggregated_scores = mcda_without_robustness.aggregate_indicators( - normalized_indicators=normalized_indicators, - weights=weights, - agg_method=aggregation_method - ) + if not self.robustness_indicators: + mcda_without_robustness = MCDAWithoutRobustness(self.polarity, self.input_matrix_no_alternatives) + normalized_indicators = self.normalized_values_without_robustness + if normalized_indicators is None: + raise ValueError("Normalization must be performed before aggregation.") + aggregated_scores = mcda_without_robustness.aggregate_indicators( + normalized_indicators=normalized_indicators, + weights=weights, + agg_method=aggregation_method + ) + + #elif self.robustness_indicators is not None and self.robustness_weights is None: + # normalized_indicators = self.get_normalized_values_with_robustness return aggregated_scores diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index c6a52a1..c9d0757 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -111,17 +111,17 @@ def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: lis Returns: - A DataFrame containing the aggregated scores for each alternative and normalization method. """ - if isinstance(agg_method, AggregationFunctions): - method = agg_method.value + #if isinstance(agg_method, AggregationFunctions): + # method = agg_method.value self.normalized_indicators = normalized_indicators self.weights = weights - agg= Aggregation(weights) + agg= Aggregation(self.weights) score_list = [] - def _apply_aggregation(norm_method, agg_method, df_subset): + def _apply_aggregation(norm_function, agg_function, df_subset): """ Apply the aggregation method to a subset of the DataFrame and store results in the appropriate DataFrame. """ @@ -132,10 +132,10 @@ def _apply_aggregation(norm_method, agg_method, df_subset): AggregationFunctions.MINIMUM.value: agg.minimum, } - agg_methods = list(agg_functions.keys()) if agg_method is None else [agg_method] + agg_methods = list(agg_functions.keys()) if agg_function is None else [agg_function] - for method in agg_methods: - agg_function = agg_functions[method] + for agg_function in agg_methods: + agg_function = agg_functions[agg_function.value] aggregated_scores = agg_function(df_subset) if isinstance(aggregated_scores, np.ndarray): @@ -143,10 +143,11 @@ def _apply_aggregation(norm_method, agg_method, df_subset): elif isinstance(aggregated_scores, pd.Series): aggregated_scores = aggregated_scores.to_frame() - aggregated_scores.columns = [f"{norm_method}_{method}"] + aggregated_scores.columns = [f"{norm_function}_{agg_method.value}"] score_list.append(aggregated_scores) for norm_method in self.normalized_indicators.columns.str.split("_", n=0).str[1].unique(): + print('Normalization method',norm_method) norm_method_columns = self.normalized_indicators.filter(regex=rf"{norm_method}") @@ -154,21 +155,23 @@ def _apply_aggregation(norm_method, agg_method, df_subset): with_zero_columns = norm_method_columns[norm_method_columns.columns.difference(without_zero_columns.columns)] # Apply WEIGHTED_SUM only to columns with zero in the suffix - if agg_method is None or agg_method == AggregationFunctions.WEIGHTED_SUM.value: - _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM.value, + if agg_method is None or agg_method == AggregationFunctions.WEIGHTED_SUM: + print('Entering weighted sum') + print('Columns:', with_zero_columns) + _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM, with_zero_columns) # Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix - if agg_method is None or agg_method == AggregationFunctions.GEOMETRIC.value: - _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC.value, + if agg_method is None or agg_method == AggregationFunctions.GEOMETRIC: + _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC, without_zero_columns) - elif agg_method is None or agg_method == AggregationFunctions.HARMONIC.value: - _apply_aggregation(norm_method, AggregationFunctions.HARMONIC.value, + elif agg_method is None or agg_method == AggregationFunctions.HARMONIC: + _apply_aggregation(norm_method, AggregationFunctions.HARMONIC, without_zero_columns) # Apply MINIMUM only to columns with zero in the suffix - if agg_method is None or agg_method == AggregationFunctions.MINIMUM.value: - _apply_aggregation(norm_method, AggregationFunctions.MINIMUM.value, + if agg_method is None or agg_method == AggregationFunctions.MINIMUM: + _apply_aggregation(norm_method, AggregationFunctions.MINIMUM, with_zero_columns) # Concatenate all score DataFrames into a single DataFrame diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 950d4a2..cbd2744 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -15,17 +15,17 @@ def setUp(self): # Mock input data for testing self.input_matrix = pd.DataFrame({ 'Alternatives': ['A', 'B', 'C'], - 'Criterion_1': [0.5, 0.2, 0.8], - 'Criterion_2': [0.3, 0.6, 0.1] + 'Criterion1': [0.5, 0.2, 0.8], + 'Criterion2': [0.3, 0.6, 0.1] }) self.input_matrix.set_index('Alternatives', inplace=True) self.input_matrix_with_uncertainty = pd.DataFrame({ 'Alternatives': ['A', 'B', 'C'], - 'Criterion_1_mean': [0.5, 0.2, 0.8], - 'Criterion_1_std': [0.1, 0.02, 0.07], - 'Criterion_2_mean': [0.3, 0.6, 0.1], - 'Criterion_2_std': [0.03, 0.06, 0.01] + 'Criterion1_mean': [0.5, 0.2, 0.8], + 'Criterion1_std': [0.1, 0.02, 0.07], + 'Criterion2_mean': [0.3, 0.6, 0.1], + 'Criterion2_std': [0.03, 0.06, 0.01] }) self.input_matrix_with_uncertainty.set_index('Alternatives', inplace=True) @@ -103,7 +103,8 @@ def test_normalize_all_methods(self): # When expected_suffixes = [method.value for method in NormalizationNames4Sensitivity] normalized_values = promcda.normalize(normalization_method) - actual_suffixes = {col.split('_', 2)[-1] for col in normalized_values.columns} + #actual_suffixes = {col.split('_', 2)[1] for col in normalized_values.columns} + actual_suffixes = {"_".join(col.split("_", 2)[1:]) for col in normalized_values.columns} # Then self.assertCountEqual(actual_suffixes, expected_suffixes, @@ -124,42 +125,49 @@ def test_normalize_specific_method(self): # When normalized_values = promcda.normalize(method=NormalizationFunctions.MINMAX) - expected_keys = ['Criterion_1_minmax_01', 'Criterion_2_minmax_01', 'Criterion_1_minmax_without_zero', 'Criterion_2_minmax_without_zero'] + expected_keys = ['Criterion1_minmax_01', 'Criterion2_minmax_01', 'Criterion1_minmax_without_zero', 'Criterion2_minmax_without_zero'] # Then self.assertCountEqual(expected_keys, list(normalized_values.keys())) self.assertEqual(list(normalized_values), expected_keys) def test_normalization_with_robustness(self): - # Given - promcda = ProMCDA( - input_matrix=self.input_matrix_with_uncertainty, - polarity=self.polarity, - robustness_weights=self.robustness_weights, - robustness_indicators=True, - marginal_distributions=self.marginal_distributions, - num_runs=self.num_runs, - num_cores=self.num_cores, - random_seed=self.random_seed - ) - - # When - promcda.normalize(method=NormalizationFunctions.MINMAX) - - # Then - normalized_values = promcda.get_normalized_values_with_robustness() - self.assertIsNotNone(normalized_values) - self.assertEqual(len(normalized_values), self.num_runs) + # Given + robustness_indicators=True + promcda = ProMCDA( + input_matrix=self.input_matrix_with_uncertainty, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + + # When + promcda.normalize(method=NormalizationFunctions.MINMAX) + + # Then + normalized_values = promcda.get_normalized_values_with_robustness() + self.assertIsNotNone(normalized_values) + self.assertEqual(len(normalized_values), self.num_runs) def test_aggregate_all_methods(self): # Given - self.sensitivity['sensitivity_on'] = 'yes' - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - aggregated_scores = promcda.aggregate(normalization_method=None, - aggregation_method=None, - weights=self.robustness['given_weights']) + promcda = ProMCDA( + input_matrix=self.input_matrix, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=self.robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + promcda.normalize() + aggregated_scores = promcda.aggregate() # When expected_columns = [ @@ -173,17 +181,24 @@ def test_aggregate_all_methods(self): self.assertEqual(len(aggregated_scores), len(self.input_matrix), "Number of alternatives does not match input matrix rows.") - def test_aggregate_with_specific_normalization_and_aggregation_methods(self): + def test_aggregate_with_specific_aggregation_method(self): # Given - normalization_method = 'minmax' - aggregation_method = 'weighted_sum' - promcda = ProMCDA(self.input_matrix, self.polarity, self.sensitivity, self.robustness, self.monte_carlo, - self.output_path) - aggregated_scores = promcda.aggregate(normalization_method=normalization_method, - aggregation_method=aggregation_method, - weights=self.robustness['given_weights']) + normalization_method = NormalizationFunctions.MINMAX + aggregation_method = AggregationFunctions.WEIGHTED_SUM # When + promcda = ProMCDA( + input_matrix=self.input_matrix, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=self.robustness_indicators, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + promcda.normalize(normalization_method) + aggregated_scores = promcda.aggregate(aggregation_method=aggregation_method) expected_columns = ['minmax_weighted_sum'] # Then From d95aaa9e68b10fda2ba1b397c4e23b56ee38be0d Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 4 Dec 2024 18:13:01 +0100 Subject: [PATCH 24/27] add aggregation without a specific method --- mcda/mcda_functions/aggregation.py | 3 -- mcda/models/ProMCDA.py | 31 ++++++++++++------ mcda/models/mcda_without_robustness.py | 44 ++++++++++++++++---------- tests/unit_tests/test_promcda.py | 7 ++-- 4 files changed, 53 insertions(+), 32 deletions(-) diff --git a/mcda/mcda_functions/aggregation.py b/mcda/mcda_functions/aggregation.py index e819e29..77071ef 100644 --- a/mcda/mcda_functions/aggregation.py +++ b/mcda/mcda_functions/aggregation.py @@ -36,9 +36,6 @@ def weighted_sum(self, norm_indicators: pd.DataFrame) -> pd.Series(dtype='object :returns scores: pd.Series """ - print("Normalized indicators shape:", norm_indicators.shape) - print("Weights:", self.weights if self.weights is not None else "Weights are None") - scores = (norm_indicators * self.weights).sum(axis=1) return scores diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index abbc64c..bf12b5b 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -90,7 +90,7 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust # # return is_robustness_indicators, is_robustness_weights, polar, weights, configuration_values - def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd.DataFrame, str]: + def normalize(self, normalization_method: Optional[NormalizationFunctions] = None) -> Union[pd.DataFrame, str]: """ Normalize the input data using the specified method. @@ -105,13 +105,13 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd - A pd.DataFrame containing the normalized values of each indicator per normalization method, if no robustness on indicators is performed. - :param method: NormalizationFunctions + :param normalization_method: NormalizationFunctions :return normalized_df: pd.DataFrame or string """ if not self.robustness_indicators: mcda_without_robustness = MCDAWithoutRobustness(self.polarity, self.input_matrix_no_alternatives) - self.normalized_values_without_robustness = mcda_without_robustness.normalize_indicators(method) + self.normalized_values_without_robustness = mcda_without_robustness.normalize_indicators(normalization_method) return self.normalized_values_without_robustness @@ -125,12 +125,12 @@ def normalize(self, method: Optional[NormalizationFunctions] = None) -> Union[pd self.random_seed) n_random_input_matrices = mcda_with_robustness.create_n_randomly_sampled_matrices() - if not method: + if not normalization_method: n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization( n_random_input_matrices, self.polarity) else: n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization( - n_random_input_matrices, self.polarity, method) + n_random_input_matrices, self.polarity, normalization_method) self.normalized_values_with_robustness = n_normalized_input_matrices @@ -152,15 +152,15 @@ def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, weights: Optional[None] = None) \ -> Union[pd.DataFrame, str]: """ - Aggregate normalized indicators using the specified method. + Aggregate normalized indicators using the specified agg_method. Notes: The aggregation methods are defined in the AggregationFunctions enum class. - This method should follow the normalization. It acquires the normalized + This agg_method should follow the normalization. It acquires the normalized values from the normalization step. Parameters (optional): - - aggregation_method: The aggregation method to use. If None, all available methods will be applied. + - aggregation_method: The aggregation agg_method to use. If None, all available methods will be applied. - weights: The weights to be used for aggregation. If None, they are set all the same. Or, if robustness on weights is enabled, then the weights are sampled from the Monte Carlo simulation. @@ -192,7 +192,20 @@ def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, w normalized_indicators = self.normalized_values_without_robustness if normalized_indicators is None: raise ValueError("Normalization must be performed before aggregation.") - aggregated_scores = mcda_without_robustness.aggregate_indicators( + + if aggregation_method is None: + aggregated_scores = pd.DataFrame() + for agg_method in AggregationFunctions: + result = mcda_without_robustness.aggregate_indicators( + normalized_indicators=normalized_indicators, + weights=weights, + agg_method=agg_method + ) + aggregated_scores = pd.concat([aggregated_scores, result], axis=1) + #for col in result.columns: + # aggregated_scores[f"{col}"] = result[col] + else: + aggregated_scores = mcda_without_robustness.aggregate_indicators( normalized_indicators=normalized_indicators, weights=weights, agg_method=aggregation_method diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index c9d0757..fa9d72d 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -5,6 +5,7 @@ import numpy as np import pandas as pd +from pandas import DataFrame from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions from mcda.mcda_functions.normalization import Normalization @@ -119,7 +120,7 @@ def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: lis agg= Aggregation(self.weights) - score_list = [] + final_scores = pd.DataFrame() def _apply_aggregation(norm_function, agg_function, df_subset): """ @@ -144,37 +145,46 @@ def _apply_aggregation(norm_function, agg_function, df_subset): aggregated_scores = aggregated_scores.to_frame() aggregated_scores.columns = [f"{norm_function}_{agg_method.value}"] + score_list.append(aggregated_scores) for norm_method in self.normalized_indicators.columns.str.split("_", n=0).str[1].unique(): - print('Normalization method',norm_method) + score_list = [] norm_method_columns = self.normalized_indicators.filter(regex=rf"{norm_method}") without_zero_columns = norm_method_columns.filter(regex="without_zero$") with_zero_columns = norm_method_columns[norm_method_columns.columns.difference(without_zero_columns.columns)] + rank_columns = norm_method_columns.filter(regex="rank$") + without_zero_columns_rank = pd.concat([without_zero_columns, rank_columns], axis=1) # Apply WEIGHTED_SUM only to columns with zero in the suffix if agg_method is None or agg_method == AggregationFunctions.WEIGHTED_SUM: - print('Entering weighted sum') - print('Columns:', with_zero_columns) - _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM, + # Apply WEIGHTED_SUM to columns with zero in the suffix and only some normalization methods + if norm_method in [NormalizationFunctions.STANDARDIZED.value, NormalizationFunctions.MINMAX.value, + NormalizationFunctions.TARGET.value, NormalizationFunctions.RANK.value]: + _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM, with_zero_columns) - - # Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix + # Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix and only some normalization methods if agg_method is None or agg_method == AggregationFunctions.GEOMETRIC: - _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC, - without_zero_columns) + if norm_method in [NormalizationFunctions.STANDARDIZED.value, NormalizationFunctions.MINMAX.value, + NormalizationFunctions.TARGET.value, NormalizationFunctions.RANK.value]: + _apply_aggregation(norm_method, AggregationFunctions.GEOMETRIC, + without_zero_columns_rank) elif agg_method is None or agg_method == AggregationFunctions.HARMONIC: - _apply_aggregation(norm_method, AggregationFunctions.HARMONIC, - without_zero_columns) - - # Apply MINIMUM only to columns with zero in the suffix + if norm_method in [NormalizationFunctions.STANDARDIZED.value, NormalizationFunctions.MINMAX.value, + NormalizationFunctions.TARGET.value, NormalizationFunctions.RANK.value]: + _apply_aggregation(norm_method, AggregationFunctions.HARMONIC, + without_zero_columns_rank) + # Apply MINIMUM to columns with zero in the suffix and only some normalization methods if agg_method is None or agg_method == AggregationFunctions.MINIMUM: - _apply_aggregation(norm_method, AggregationFunctions.MINIMUM, + if norm_method in [NormalizationFunctions.STANDARDIZED.value]: + _apply_aggregation(norm_method, AggregationFunctions.MINIMUM, with_zero_columns) - # Concatenate all score DataFrames into a single DataFrame - scores = pd.concat(score_list, axis=1) + # Concatenate all score DataFrames into a single DataFrame if there are any + if score_list: + scores: DataFrame = pd.concat(score_list, axis=1) + final_scores = pd.concat([final_scores, scores], axis=1) - return scores + return final_scores diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index cbd2744..283a371 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -167,13 +167,14 @@ def test_aggregate_all_methods(self): random_seed=self.random_seed ) promcda.normalize() - aggregated_scores = promcda.aggregate() # When + aggregated_scores = promcda.aggregate() expected_columns = [ 'minmax_weighted_sum', 'target_weighted_sum', 'standardized_weighted_sum', 'rank_weighted_sum', - 'minmax_geometric', 'minmax_minimum', 'target_geometric', 'target_minimum', 'standardized_geometric', - 'standardized_minimum', 'rank_geometric', 'rank_minimum'] + 'minmax_geometric', 'target_geometric', 'standardized_geometric', 'rank_geometric', + 'minmax_harmonic', 'target_harmonic', 'standardized_harmonic', 'rank_harmonic', + 'standardized_minimum'] # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, From 20cacda84553f06209271f5d4593a4d9165411ea Mon Sep 17 00:00:00 2001 From: Flaminia Date: Wed, 11 Dec 2024 16:48:26 +0100 Subject: [PATCH 25/27] add aggregation with robustness --- demo_in_notebook/use_promcda_library.ipynb | 136 +++++++++++++++++- mcda/configuration/configuration_validator.py | 121 +++++++++------- mcda/models/ProMCDA.py | 136 +++++++++++------- mcda/models/mcda_without_robustness.py | 1 + mcda/utils/utils_for_main.py | 2 +- mcda/utils/utils_for_parallelization.py | 12 +- requirements.txt | 17 ++- tests/unit_tests/test_promcda.py | 32 ++++- 8 files changed, 333 insertions(+), 124 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 9096074..4b0d43e 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -146,14 +146,144 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 8, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zeroCriteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zeroCriteria 1_standardized_anyCriteria 2_standardized_anyCriteria 1_standardized_without_zeroCriteria 2_standardized_without_zeroCriteria 1_rankCriteria 2_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", + "
" + ], + "text/plain": [ + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", + "0 0.64 0.625 0.5 \n", + "1 0.1 0.25 0.0 \n", + "2 1.0 1.0 0.833333 \n", + "\n", + " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", + "0 0.6625 0.55 \n", + "1 0.325 0.1 \n", + "2 1.0 0.85 \n", + "\n", + " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", + "0 0.0 0.132453 \n", + "1 -1.0 -1.059626 \n", + "2 1.0 0.927173 \n", + "\n", + " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", + "0 1.1 1.292079 \n", + "1 0.1 0.1 \n", + "2 2.1 2.086799 \n", + "\n", + " Criteria 1_rank Criteria 2_rank \n", + "0 2.0 2.0 \n", + "1 1.0 1.0 \n", + "2 3.0 3.0 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "#promcda.normalize()" + "promcda.normalize()" ] }, { diff --git a/mcda/configuration/configuration_validator.py b/mcda/configuration/configuration_validator.py index ef829d3..c5a9e77 100644 --- a/mcda/configuration/configuration_validator.py +++ b/mcda/configuration/configuration_validator.py @@ -3,7 +3,9 @@ import numpy as np import pandas as pd -from typing import Tuple, List, Union, Dict, Any +from typing import Tuple, Union + +from pandas.core import series from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions from mcda.utils.utils_for_main import pop_indexed_elements, check_norm_sum_weights, randomly_sample_all_weights, \ @@ -71,9 +73,6 @@ def extract_configuration_values(input_matrix: pd.DataFrame, polarity: Tuple[str :param input_matrix : pd.DataFrame :param polarity : Tuple[str] - :param sensitivity : dict - :param robustness : dict - :param: monte_carlo : dict :param: output_path: str :return: extracted_values: dict """ @@ -286,22 +285,29 @@ def check_config_setting(condition_robustness_on_weights: bool, condition_robust return is_robustness_weights, is_robustness_indicators -def process_indicators_and_weights(config: dict, input_matrix: pd.DataFrame, - is_robustness_indicators: int, is_robustness_weights: int, polar: List[str], - mc_runs: int, num_indicators: int) \ +def process_indicators_and_weights(input_matrix: pd.DataFrame, + robustness_indicators: bool, + robustness_weights: bool, + robustness_single_weights: bool, + polarity: Tuple[str, ...], + mc_runs: int, + num_indicators: int, + weights: List[str]) \ -> Tuple[List[str], Union[list, List[list], dict]]: """ Process indicators and weights based on input parameters in the configuration. Parameters: - - config: the configuration dictionary. - input_matrix: the input matrix without alternatives. - - is_robustness_indicators: a flag indicating whether the matrix should include indicator uncertainties - (0 or 1). - - is_robustness_weights: a flag indicating whether robustness analysis is considered for the weights (0 or 1). - - marginal_pdf: a list of marginal probability density functions for indicators. + - robustness_indicators: a flag indicating whether the matrix should include indicator uncertainties + (True or False). + - robustness_weights: a flag indicating whether robustness analysis is considered for the weights (True or False). + - robustness_single_weights: a flag indicating whether robustness analysis is considered for a single weight + at time (True or False). + - polarity: a tuple containing the original polarity associated to each indicator. - mc_runs: number of Monte Carlo runs for robustness analysis. - num_indicators: the number of indicators in the input matrix. + - weights: a list containing the assigned weights. Raises: - ValueError: If there are duplicated rows in the input matrix or if there is an issue with the configuration. @@ -312,54 +318,62 @@ def process_indicators_and_weights(config: dict, input_matrix: pd.DataFrame, - the normalised weights (either fixed or random sampled weights, depending on the settings) Notes: - - For is_robustness_indicators == 0: - - Identifies and removes columns with constant values. - - Logs the number of alternatives and indicators. + - For robustness_indicators == False: + - Identifies and removes columns with constant values. + - Logs the number of alternatives and indicators. - - For is_robustness_indicators == 1: - - Handles uncertainty in indicators. - - Logs the number of alternatives and indicators. + - For robustness_indicators == True: + - Handles uncertainty in indicators. + - Logs the number of alternatives and indicators. - - For is_robustness_weights == 0: - - Processes fixed weights if given. - - Logs weights and normalised weights. + - For robustness_weights == False: + - Processes fixed weights if given. + - Logs weights and normalised weights. - - For is_robustness_weights == 1: - - Performs robustness analysis on weights. - - Logs randomly sampled weights. + - For robustness_weights == True: + - Performs robustness analysis on weights. + - Logs randomly sampled weights. - :param mc_runs: int - :param polar: List[str] - :param is_robustness_weights: int - :param is_robustness_indicators: int :param input_matrix: pd.DataFrame - :param config: dict + :param robustness_weights: bool + :param robustness_single_weights: + :param robustness_indicators: bool + :param polarity: List[str] + :param mc_runs: int :param num_indicators: int - :return: polar, norm_weights + :param weights: List[str] :rtype: Tuple[List[str], Union[List[list], dict]] """ num_unique = input_matrix.nunique() cols_to_drop = num_unique[num_unique == 1].index col_to_drop_indexes = input_matrix.columns.get_indexer(cols_to_drop) - if is_robustness_indicators == 0: + if robustness_indicators is False: _handle_no_robustness_indicators(input_matrix) else: # matrix with uncertainty on indicators logger.info("Number of alternatives: {}".format(input_matrix.shape[0])) logger.info("Number of indicators: {}".format(num_indicators)) # TODO: eliminate indicators with constant values (i.e. same mean and 0 std) - optional - polarities_and_weights = _handle_polarities_and_weights(is_robustness_indicators, is_robustness_weights, num_unique, - col_to_drop_indexes, polar, config, mc_runs, num_indicators) + polarities_and_weights = _handle_polarities_and_weights(robustness_indicators, robustness_weights, + robustness_single_weights, num_unique, + col_to_drop_indexes, polarity, mc_runs, num_indicators, + weights) polar, norm_weights = tuple(item for item in polarities_and_weights if item is not None) return polar, norm_weights -def _handle_polarities_and_weights(is_robustness_indicators: int, is_robustness_weights: int, num_unique, - col_to_drop_indexes: np.ndarray, polar: List[str], config: dict, mc_runs: int, - num_indicators: int) \ +def _handle_polarities_and_weights(robustness_indicators: bool, + robustness_weights: bool, + robustness_single_weights: bool, + num_unique: series, + col_to_drop_indexes: np.ndarray, + polarity: Tuple[str, ...], + mc_runs: int, + num_indicators: int, + weights: List[str]) \ -> Union[Tuple[List[str], list, None, None], Tuple[List[str], None, List[List], None], Tuple[List[str], None, None, dict]]: """ @@ -370,34 +384,35 @@ def _handle_polarities_and_weights(is_robustness_indicators: int, is_robustness_ rand_weight_per_indicator = {} # Managing polarities - if is_robustness_indicators == 0: + if robustness_indicators is False: if any(value == 1 for value in num_unique): - polar = pop_indexed_elements(col_to_drop_indexes, polar) - logger.info("Polarities: {}".format(polar)) + polarity = pop_indexed_elements(col_to_drop_indexes, polarity) + logger.info("Polarities: {}".format(polarity)) # Managing weights - if is_robustness_weights == 0: - fixed_weights = config["given_weights"] + if robustness_weights is False: + fixed_weights = weights if any(value == 1 for value in num_unique): fixed_weights = pop_indexed_elements(col_to_drop_indexes, fixed_weights) norm_fixed_weights = check_norm_sum_weights(fixed_weights) logger.info("Weights: {}".format(fixed_weights)) logger.info("Normalized weights: {}".format(norm_fixed_weights)) - return polar, norm_fixed_weights, None, None + return polarity, norm_fixed_weights, None, None # Return None for norm_random_weights and rand_weight_per_indicator else: - output_weights = _handle_robustness_weights(config, mc_runs, num_indicators) + output_weights = _handle_robustness_weights(mc_runs, num_indicators, robustness_indicators, robustness_weights, + robustness_single_weights) if output_weights is not None: norm_random_weights, rand_weight_per_indicator = output_weights if norm_random_weights: - return polar, None, norm_random_weights, None + return polarity, None, norm_random_weights, None else: - return polar, None, None, rand_weight_per_indicator + return polarity, None, None, rand_weight_per_indicator # Return None for norm_fixed_weights and one of the other two cases of randomness -def _handle_robustness_weights(config: dict, mc_runs: int, num_indicators: int) \ - -> Tuple[Union[List[list], None], Union[dict, None]]: +def _handle_robustness_weights(mc_runs: int, num_indicators: int, robustness_indicators: bool, robustness_weights: bool, + robustness_single_weight: bool) -> Tuple[Union[List[list], None], Union[dict, None]]: """ Handle the generation and normalization of random weights based on the specified settings when a robustness analysis is requested on all the weights. @@ -407,15 +422,15 @@ def _handle_robustness_weights(config: dict, mc_runs: int, num_indicators: int) if mc_runs == 0: logger.error('Error Message', stack_info=True) - raise ValueError('The number of MC runs should be larger than 0 for a robustness analysis') + raise ValueError('The number of MC runs should be larger than 0 for robustness analysis') - if config["robustness_on_single_weights"] == "no" and config["robustness_on_all_weights"] == "yes": + if robustness_single_weight is False and robustness_weights is True: random_weights = randomly_sample_all_weights(num_indicators, mc_runs) for weights in random_weights: weights = check_norm_sum_weights(weights) norm_random_weights.append(weights) return norm_random_weights, None # Return norm_random_weights, and None for rand_weight_per_indicator - elif config["robustness_on_single_weights"] == "yes" and config["robustness_on_all_weights"] == "no": + elif robustness_single_weight is True and robustness_weights is False: i = 0 while i < num_indicators: random_weights = randomly_sample_ix_weight(num_indicators, i, mc_runs) @@ -445,7 +460,8 @@ def _handle_no_robustness_indicators(input_matrix: pd.DataFrame): logger.info("Number of indicators: {}".format(num_indicators)) -def check_indicator_weights_polarities(num_indicators: int, polar: List[str], config: dict): +def check_indicator_weights_polarities(num_indicators: int, polar: List[str], robustness_weights: bool, + weights: List[int]): """ Check the consistency of indicators, polarities, and fixed weights in a configuration. @@ -471,6 +487,5 @@ def check_indicator_weights_polarities(num_indicators: int, polar: List[str], co raise ValueError('The number of polarities does not correspond to the no. of indicators') # Check the number of fixed weights if "robustness_on_all_weights" is set to "no" - if (config["robustness_on_all_weights"] == "no") and ( - num_indicators != len(config["given_weights"])): + if (robustness_weights is False) and (num_indicators != len(weights)): raise ValueError('The no. of fixed weights does not correspond to the no. of indicators') diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index bf12b5b..daf9c2d 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -1,20 +1,27 @@ +import logging +import sys import time import pandas as pd from typing import Tuple, List, Union, Optional from build.lib.mcda.mcda_with_robustness import MCDAWithRobustness from mcda.configuration.configuration_validator import extract_configuration_values, check_configuration_values, \ - check_configuration_keys + check_configuration_keys, check_indicator_weights_polarities, process_indicators_and_weights from mcda.configuration.enums import PDFType, NormalizationFunctions, AggregationFunctions +from mcda.models import mcda_with_robustness from mcda.models.mcda_without_robustness import MCDAWithoutRobustness from mcda.utils import utils_for_parallelization from mcda.utils.utils_for_main import run_mcda_without_indicator_uncertainty, run_mcda_with_indicator_uncertainty, \ - check_input_matrix, check_if_pdf_is_exact, check_if_pdf_is_poisson, check_parameters_pdf + check_input_matrix, check_if_pdf_is_exact, check_if_pdf_is_poisson, check_parameters_pdf, rescale_minmax +log = logging.getLogger(__name__) +formatter = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=formatter) +logger = logging.getLogger("ProMCDA") class ProMCDA: def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robustness_weights: Optional[bool] = None, - robustness_indicators: Optional[bool] = None, + robustness_single_weights: Optional[bool] = None, robustness_indicators: Optional[bool] = None, marginal_distributions: Optional[Tuple[PDFType, ...]] = None, num_runs: Optional[int] = 10000, num_cores: Optional[int] = 1, random_seed: Optional[int] = 43): """ @@ -27,6 +34,8 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust # Optional parameters :param robustness_weights: Boolean flag indicating whether to perform robustness analysis on weights (True or False). + :param robustness_single_weights: Boolean flag indicating whether to perform robustness analysis on one single + weight at time (True or False). :param robustness_indicators: Boolean flag indicating whether to perform robustness analysis on indicators (True or False). :param marginal_distributions: Tuple of marginal distributions, which describe the indicators @@ -66,6 +75,7 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust self.input_matrix = input_matrix self.polarity = polarity self.robustness_weights = robustness_weights + self.robustness_single_weights = robustness_single_weights self.robustness_indicators = robustness_indicators self.num_runs = num_runs self.marginal_distributions = marginal_distributions @@ -73,7 +83,11 @@ def __init__(self, input_matrix: pd.DataFrame, polarity: Tuple[str, ...], robust self.random_seed = random_seed self.normalized_values_without_robustness = None self.normalized_values_with_robustness = None - self.scores = None + self.aggregated_scores = None + self.all_indicators_scores_means = None + self.all_indicators_scores_stds = None + self.all_indicators_means_scores_normalized = None + self.all_indicators_scores_stds_normalized = None self.input_matrix_no_alternatives = check_input_matrix(self.input_matrix) @@ -132,7 +146,7 @@ def normalize(self, normalization_method: Optional[NormalizationFunctions] = Non n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization( n_random_input_matrices, self.polarity, normalization_method) - self.normalized_values_with_robustness = n_normalized_input_matrices + self.normalized_values_with_robustness = n_normalized_input_matrices return f"{self.num_runs} randomly sampled matrices have been normalized." @@ -149,7 +163,7 @@ def get_normalized_values_with_robustness(self) -> Optional[pd.DataFrame]: """ return getattr(self, 'normalized_values_with_robustness', None) - def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, weights: Optional[None] = None) \ + def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, weights: Optional[List[str]] = None) \ -> Union[pd.DataFrame, str]: """ Aggregate normalized indicators using the specified agg_method. @@ -166,26 +180,36 @@ def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, w Returns: - A pd.DataFrame containing the aggregated scores per normalization and aggregation methods, - if no robustness on indicators is not performed. + if robustness on indicators is not performed. :param aggregation_method: AggregationFunctions :param weights : list or None :return scores_df: pd.DataFrame or string """ - # TODO: 2. Check if robustness on weights is enabled, then sample the weights from the Monte Carlo simulation. - # TODO: 3. Check if aggregation_method is None, then apply all available methods. + num_indicators = self.input_matrix_no_alternatives.shape[1] if weights is None: - if not self.robustness_indicators: - num_indicators = self.input_matrix_no_alternatives.shape[1] - else: + if self.robustness_indicators: num_non_indicators = ( len(self.marginal_distributions) - self.marginal_distributions.count('exact') - self.marginal_distributions.count('poisson')) num_indicators = (self.input_matrix_no_alternatives.shape[1] - num_non_indicators) - weights = [0.5] * num_indicators - - if self.robustness_weights is True: - raise ValueError("Robustness on weights is not yet implemented.") + weights = [0.5] * num_indicators + else: + weights = [0.5] * num_indicators + + # Process indicators and weights based on input parameters in the configuration + polar, weights = process_indicators_and_weights(self.input_matrix_no_alternatives, + self.robustness_indicators, + self.robustness_weights, self.robustness_single_weights, + self.polarity, self.num_runs, num_indicators, weights) + + # Check the number of indicators, weights, and polarities, assign random weights if uncertainty is enabled + try: + check_indicator_weights_polarities(num_indicators, polar, robustness_weights=self.robustness_weights, + weights=weights) + except ValueError as e: + logging.error(str(e), stack_info=True) + raise if not self.robustness_indicators: mcda_without_robustness = MCDAWithoutRobustness(self.polarity, self.input_matrix_no_alternatives) @@ -202,8 +226,6 @@ def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, w agg_method=agg_method ) aggregated_scores = pd.concat([aggregated_scores, result], axis=1) - #for col in result.columns: - # aggregated_scores[f"{col}"] = result[col] else: aggregated_scores = mcda_without_robustness.aggregate_indicators( normalized_indicators=normalized_indicators, @@ -211,43 +233,51 @@ def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, w agg_method=aggregation_method ) - #elif self.robustness_indicators is not None and self.robustness_weights is None: - # normalized_indicators = self.get_normalized_values_with_robustness + self.aggregated_scores = aggregated_scores + return self.aggregated_scores - return aggregated_scores + elif self.robustness_indicators and not self.robustness_weights: + all_indicators_scores_normalized = [] + logger.info("Start ProMCDA with uncertainty on the indicators") + n_normalized_input_matrices = self.normalized_values_with_robustness + + if self.num_runs <= 0: + logger.error('Error Message', stack_info=True) + raise ValueError('The number of MC runs should be larger than 0 for a robustness analysis') + + if self.num_runs < 1000: + logger.info("The number of Monte-Carlo runs is only {}".format(self.num_runs)) + logger.info("A meaningful number of Monte-Carlo runs is equal or larger than 1000") + + args_for_parallel_agg = [(weights, normalized_indicators) + for normalized_indicators in n_normalized_input_matrices] + + if aggregation_method is None: + all_indicators_scores = utils_for_parallelization.parallelize_aggregation(args_for_parallel_agg) + else: + all_indicators_scores = utils_for_parallelization.parallelize_aggregation(args_for_parallel_agg, + aggregation_method) + + for matrix in all_indicators_scores: + normalized_matrix = rescale_minmax(matrix) + all_indicators_scores_normalized.append(normalized_matrix) + + all_indicators_scores_means, all_indicators_scores_stds = \ + utils_for_parallelization.estimate_runs_mean_std(all_indicators_scores) + all_indicators_means_scores_normalized, all_indicators_scores_stds_normalized = \ + utils_for_parallelization.estimate_runs_mean_std(all_indicators_scores_normalized) + + self.aggregated_scores = all_indicators_scores_normalized + self.all_indicators_scores_means = all_indicators_scores_means + self.all_indicators_scores_stds = all_indicators_scores_stds + self.all_indicators_means_scores_normalized = all_indicators_means_scores_normalized + self.all_indicators_scores_stds_normalized = all_indicators_scores_stds_normalized + + else: + # write message error + + return ("Aggregation considered uncertainty on indicators, resulsts are not explicitly shown.") - # def aggregate_with_robustness(self, normalization_method=None, aggregation_method=None, weights=None, - # polarity: list, robustness: dict, monte_carlo: dict) -> pd.DataFrame: - # """ - # Estimate scores of alternatives using the specified normalization and aggregation methods. - # - # - # Notes: - # The aggregation methods are defined in the AggregationFunctions enum class. - # - # Parameters (optional): - # - normalization_method: The normalization method to use. If None, all available methods will be applied. - # - aggregation_method: The aggregation method to use. If None, all available methods will be applied. - # - weights: The weights to be used for aggregation. If None, they are set all the same. - # - polarity: List of polarity for each indicator (+ or -). - # - robustness: Robustness analysis configuration. - # - monte_carlo: Monte Carlo sampling configuration. - # - # Returns: - # - A DataFrame containing the aggregated scores per normalization and aggregation methods. - # """ - # - # input_matrix_no_alternatives = check_input_matrix(self.input_matrix) - # mcda_without_robustness = MCDAWithoutRobustness(self.configuration_settings, input_matrix_no_alternatives) - # normalized_indicators = self.normalize(normalization_method) - # - # aggregated_scores = mcda_without_robustness.aggregate_indicators( - # normalized_indicators=normalized_indicators, - # weights=weights, - # agg_method=aggregation_method - # ) - # - # return aggregated_scores def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, weights: Union[list, List[list], dict]): diff --git a/mcda/models/mcda_without_robustness.py b/mcda/models/mcda_without_robustness.py index fa9d72d..fd591a0 100644 --- a/mcda/models/mcda_without_robustness.py +++ b/mcda/models/mcda_without_robustness.py @@ -111,6 +111,7 @@ def aggregate_indicators(self, normalized_indicators: pd.DataFrame, weights: lis Returns: - A DataFrame containing the aggregated scores for each alternative and normalization method. + :rtype: object """ #if isinstance(agg_method, AggregationFunctions): # method = agg_method.value diff --git a/mcda/utils/utils_for_main.py b/mcda/utils/utils_for_main.py index 4499d1a..8388c14 100644 --- a/mcda/utils/utils_for_main.py +++ b/mcda/utils/utils_for_main.py @@ -454,7 +454,7 @@ def check_norm_sum_weights(weights: list) -> list: return weights -def pop_indexed_elements(indexes: np.ndarray, original_list: list) -> list: +def pop_indexed_elements(indexes: np.ndarray, original_list: List[str]) -> list: """ Eliminate elements from a list at specified indexes. diff --git a/mcda/utils/utils_for_parallelization.py b/mcda/utils/utils_for_parallelization.py index 94deb0c..48e19ca 100644 --- a/mcda/utils/utils_for_parallelization.py +++ b/mcda/utils/utils_for_parallelization.py @@ -1,19 +1,21 @@ -from mcda.configuration.enums import PDFType, NormalizationFunctions -from mcda.mcda_functions.aggregation import Aggregation -from mcda.mcda_functions.normalization import Normalization import sys import logging import pandas as pd import multiprocessing from functools import partial -from typing import List, Tuple +from typing import List, Tuple, Optional + +from mcda.mcda_functions.aggregation import Aggregation +from mcda.mcda_functions.normalization import Normalization +from mcda.configuration.enums import NormalizationFunctions, AggregationFunctions formatter = '%(levelname)s: %(asctime)s - %(name)s - %(message)s' logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=formatter) logger = logging.getLogger("ProMCDA utils for parallelization") -def initialize_and_call_aggregation(args: Tuple[list, dict], method=None) -> pd.DataFrame: +def initialize_and_call_aggregation(args: Tuple[list, dict], method: Optional[AggregationFunctions] = None) \ + -> pd.DataFrame: """ Initialize an Aggregation object with given weights and call the aggregation method to calculate scores. diff --git a/requirements.txt b/requirements.txt index f8ef914..c1dd98b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,16 @@ # Requirements automatically generated by pigar. # https://github.com/damnever/pigar. -numpy -pandas -plotly -pytest -scikit-learn -scipy -Pillow +numpy~=2.0.2 +pandas~=2.2.3 +plotly~=5.24.1 +pytest~=8.3.3 +scikit-learn~=1.5.2 +scipy~=1.13.1 +Pillow~=11.0.0 kaleido + +matplotlib~=3.9.3 +setuptools~=68.2.0 \ No newline at end of file diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 283a371..7eeee91 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -124,7 +124,7 @@ def test_normalize_specific_method(self): ) # When - normalized_values = promcda.normalize(method=NormalizationFunctions.MINMAX) + normalized_values = promcda.normalize(normalization_method=NormalizationFunctions.MINMAX) expected_keys = ['Criterion1_minmax_01', 'Criterion2_minmax_01', 'Criterion1_minmax_without_zero', 'Criterion2_minmax_without_zero'] # Then @@ -146,7 +146,7 @@ def test_normalization_with_robustness(self): ) # When - promcda.normalize(method=NormalizationFunctions.MINMAX) + promcda.normalize(normalization_method=NormalizationFunctions.MINMAX) # Then normalized_values = promcda.get_normalized_values_with_robustness() @@ -208,6 +208,33 @@ def test_aggregate_with_specific_aggregation_method(self): (aggregated_scores['minmax_weighted_sum'] >= 0).all() and (aggregated_scores['minmax_weighted_sum'] <= 1).all(), "Values should be in the range [0, 1] for minmax normalization with weighted sum.") + def test_aggregate_with_robustness(self): + # Given + normalization_method = NormalizationFunctions.MINMAX + aggregation_method = AggregationFunctions.WEIGHTED_SUM + + # When + promcda = ProMCDA( + input_matrix=self.input_matrix_with_uncertainty, + polarity=self.polarity, + robustness_weights=self.robustness_weights, + robustness_indicators=True, + marginal_distributions=self.marginal_distributions, + num_runs=self.num_runs, + num_cores=self.num_cores, + random_seed=self.random_seed + ) + promcda.normalize(normalization_method) + aggregated_scores = promcda.aggregate(aggregation_method=aggregation_method) + expected_columns = ['minmax_weighted_sum'] + + # Then + self.assertCountEqual(aggregated_scores.columns, expected_columns, + "Only specified methods should be applied.") + self.assertTrue( + (aggregated_scores['minmax_weighted_sum'] >= 0).all() and ( + aggregated_scores['minmax_weighted_sum'] <= 1).all(), + "Values should be in the range [0, 1] for minmax normalization with weighted sum.") # def tearDown(self): @@ -217,6 +244,7 @@ def test_aggregate_with_specific_aggregation_method(self): # if os.path.exists(self.output_path): # shutil.rmtree(self.output_path) + if __name__ == '__main__': unittest.main() From 39c90612a5c6cf8d70cb33241c4ec36cdfc5ee1b Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 12 Dec 2024 14:27:11 +0100 Subject: [PATCH 26/27] fix unit test for aggregation, all variations of it --- demo_in_notebook/use_promcda_library.ipynb | 412 ++++++--------------- mcda/models/ProMCDA.py | 25 +- mcda/utils/utils_for_parallelization.py | 10 +- tests/unit_tests/test_promcda.py | 9 +- 4 files changed, 154 insertions(+), 302 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index 4b0d43e..ea0bace 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -35,20 +35,29 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 9, "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'input_matrix': Criteria 1 Criteria 2\n", - " A 0.5 0.3\n", - " B 0.2 0.6\n", - " C 0.8 0.1,\n", - " 'polarity': ('+', '-'),\n", + "{'input_matrix': ind1_min ind1_max ind2 ind3_average ind3_std ind4_average \\\n", + " alternatives \n", + " alt1 -15.2 8.2 0.04 24.5 6.20 -15.2 \n", + " alt2 -12.4 8.7 0.05 24.5 4.80 -12.4 \n", + " alt3 10.6 2.0 0.11 14.0 0.60 1.6 \n", + " alt4 -39.7 14.0 0.01 26.5 4.41 -39.7 \n", + " \n", + " ind4_std ind5 ind6_average ind6_std \n", + " alternatives \n", + " alt1 8.2 0.04 24.5 6.20 \n", + " alt2 8.7 0.05 24.5 4.80 \n", + " alt3 2.0 0.11 14.0 0.60 \n", + " alt4 14.0 0.01 26.5 4.41 ,\n", + " 'polarity': ('+', '-', '+', '+', '+', '+'),\n", " 'robustness_weights': False,\n", - " 'robustness_indicators': False,\n", + " 'robustness_indicators': True,\n", " 'marginal_distributions': [,\n", " ],\n", " 'num_runs': 5,\n", @@ -56,7 +65,7 @@ " 'output_path': 'mock_output/'}" ] }, - "execution_count": 2, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } @@ -86,10 +95,10 @@ " \n", " input_matrix_with_uncertainty.set_index('alternatives', inplace=True)\n", "\n", - " polarity = ('+', '-')\n", + " polarity = ('+', '-', '+', '+', '+', '+')\n", "\n", " robustness_weights = False\n", - " robustness_indicators = False\n", + " robustness_indicators = True\n", "\n", " marginal_distributions = [PDFType.NORMAL, PDFType.NORMAL]\n", "\n", @@ -100,7 +109,7 @@ "\n", " # Return the setup parameters as a dictionary\n", " return {\n", - " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", + " 'input_matrix': input_matrix_with_uncertainty, # Decide what type of input matrix\n", " 'polarity': polarity,\n", " 'robustness_weights': robustness_weights,\n", " 'robustness_indicators': robustness_indicators,\n", @@ -119,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 10, "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", "metadata": {}, "outputs": [ @@ -127,7 +136,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-12-03 17:26:36,915 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + "INFO: 2024-12-12 14:24:40,588 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n" ] } ], @@ -146,140 +155,39 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", "metadata": { "scrolled": true }, "outputs": [ { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zeroCriteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zeroCriteria 1_standardized_anyCriteria 2_standardized_anyCriteria 1_standardized_without_zeroCriteria 2_standardized_without_zeroCriteria 1_rankCriteria 2_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", - "
" - ], - "text/plain": [ - " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", - "0 0.5 0.6 0.55 \n", - "1 0.0 0.0 0.1 \n", - "2 1.0 1.0 1.0 \n", - "\n", - " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", - "0 0.64 0.625 0.5 \n", - "1 0.1 0.25 0.0 \n", - "2 1.0 1.0 0.833333 \n", - "\n", - " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", - "0 0.6625 0.55 \n", - "1 0.325 0.1 \n", - "2 1.0 0.85 \n", - "\n", - " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", - "0 0.0 0.132453 \n", - "1 -1.0 -1.059626 \n", - "2 1.0 0.927173 \n", - "\n", - " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", - "0 1.1 1.292079 \n", - "1 0.1 0.1 \n", - "2 2.1 2.086799 \n", - "\n", - " Criteria 1_rank Criteria 2_rank \n", - "0 2.0 2.0 \n", - "1 1.0 1.0 \n", - "2 3.0 3.0 " - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 14:24:42,348 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", + "INFO: 2024-12-12 14:24:42,349 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", + "INFO: 2024-12-12 14:24:42,350 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", + "INFO: 2024-12-12 14:24:42,350 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n" + ] + }, + { + "ename": "IndexError", + "evalue": "positional indexers are out-of-bounds", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRemoteTraceback\u001b[0m Traceback (most recent call last)", + "\u001b[0;31mRemoteTraceback\u001b[0m: \n\"\"\"\nTraceback (most recent call last):\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n result = (True, func(*args, **kwds))\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n return list(map(*args))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 97, in initialize_and_call_normalization\n dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 138, in normalize_indicators_in_parallel\n indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/mcda_functions/normalization.py\", line 99, in minmax\n pol = Normalization._cast_polarities(self)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/mcda_functions/normalization.py\", line 30, in _cast_polarities\n indicators_plus = self._input_matrix.iloc[:, ind_plus]\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1184, in __getitem__\n return self._getitem_tuple(key)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1690, in _getitem_tuple\n tup = self._validate_tuple_indexer(tup)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 966, in _validate_tuple_indexer\n self._validate_key(k, i)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1612, in _validate_key\n raise IndexError(\"positional indexers are out-of-bounds\")\nIndexError: positional indexers are out-of-bounds\n\"\"\"", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/3655398325.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mnormalize\u001b[0;34m(self, normalization_method)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 142\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mnormalization_method\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 143\u001b[0;31m n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(\n\u001b[0m\u001b[1;32m 144\u001b[0m n_random_input_matrices, self.polarity)\n\u001b[1;32m 145\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\u001b[0m in \u001b[0;36mparallelize_normalization\u001b[0;34m(input_matrices, polar, method)\u001b[0m\n\u001b[1;32m 291\u001b[0m \u001b[0mpool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmultiprocessing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdf\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minput_matrices\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 293\u001b[0;31m \u001b[0mres\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitialize_and_call_normalization\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 294\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 295\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mmap\u001b[0;34m(self, func, iterable, chunksize)\u001b[0m\n\u001b[1;32m 362\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m \u001b[0mlist\u001b[0m \u001b[0mthat\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0mreturned\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 363\u001b[0m '''\n\u001b[0;32m--> 364\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_map_async\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmapstar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 365\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstarmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mget\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 769\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 770\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 771\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 772\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 773\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_set\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mIndexError\u001b[0m: positional indexers are out-of-bounds" + ] } ], "source": [ @@ -288,113 +196,27 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zero
00.50.60.550.64
10.00.00.10.1
21.01.01.01.0
\n", - "
" - ], - "text/plain": [ - " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", - "0 0.5 0.6 0.55 \n", - "1 0.0 0.0 0.1 \n", - "2 1.0 1.0 1.0 \n", - "\n", - " Criteria 2_minmax_without_zero \n", - "0 0.64 \n", - "1 0.1 \n", - "2 1.0 " - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "promcda.normalize(NormalizationFunctions.MINMAX)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "0038ddb3-5499-4fd4-a5de-d785593885a9", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", - "0 0.5 0.6 0.55 \n", - "1 0.0 0.0 0.1 \n", - "2 1.0 1.0 1.0 \n", - "\n", - " Criteria 2_minmax_without_zero \n", - "0 0.64 \n", - "1 0.1 \n", - "2 1.0 \n" - ] - } - ], + "outputs": [], "source": [ - "print(promcda.normalized_values_without_robustness)" + "promcda.normalized_values_without_robustness" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "e857a02a-aae8-453a-a302-46844c3e610f", "metadata": {}, "outputs": [ @@ -402,67 +224,75 @@ "name": "stdout", "output_type": "stream", "text": [ - "Normalization method minmax\n", - "Entering weighted sum\n", - "Columns: Criteria 1_minmax_01 Criteria 2_minmax_01\n", - "0 0.5 0.6\n", - "1 0.0 0.0\n", - "2 1.0 1.0\n", - "Normalized indicators shape: (3, 2)\n", - "Weights: [0.5, 0.5]\n" + "INFO: 2024-12-12 14:22:21,046 - ProMCDA - Number of alternatives: 4\n", + "INFO: 2024-12-12 14:22:21,048 - ProMCDA - Number of indicators: 8\n", + "INFO: 2024-12-12 14:22:21,049 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-12-12 14:22:21,050 - ProMCDA - Weights: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]\n", + "INFO: 2024-12-12 14:22:21,050 - ProMCDA - Normalized weights: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]\n", + "ERROR: 2024-12-12 14:22:21,066 - root - The number of polarities does not correspond to the no. of indicators\n", + "Stack (most recent call last):\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/runpy.py\", line 197, in _run_module_as_main\n", + " return _run_code(code, main_globals, None,\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/runpy.py\", line 87, in _run_code\n", + " exec(code, run_globals)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel_launcher.py\", line 18, in \n", + " app.launch_new_instance()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/traitlets/config/application.py\", line 1075, in launch_instance\n", + " app.start()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelapp.py\", line 739, in start\n", + " self.io_loop.start()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/tornado/platform/asyncio.py\", line 205, in start\n", + " self.asyncio_loop.run_forever()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/base_events.py\", line 601, in run_forever\n", + " self._run_once()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/base_events.py\", line 1905, in _run_once\n", + " handle._run()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/events.py\", line 80, in _run\n", + " self._context.run(self._callback, *self._args)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 545, in dispatch_queue\n", + " await self.process_one()\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 534, in process_one\n", + " await dispatch(*args)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 437, in dispatch_shell\n", + " await result\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/ipkernel.py\", line 362, in execute_request\n", + " await super().execute_request(stream, ident, parent)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 778, in execute_request\n", + " reply_content = await reply_content\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/ipkernel.py\", line 456, in do_execute\n", + " res = shell.run_cell(code, store_history=store_history, silent=silent)\n", + " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/zmqshell.py\", line 549, in run_cell\n", + " return super().run_cell(*args, **kwargs)\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 2914, in run_cell\n", + " result = self._run_cell(\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 2960, in _run_cell\n", + " return runner(coro)\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/async_helpers.py\", line 78, in _pseudo_sync_runner\n", + " coro.send(None)\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3185, in run_cell_async\n", + " has_raised = await self.run_ast_nodes(code_ast.body, cell_name,\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3377, in run_ast_nodes\n", + " if (await self.run_code(code, result, async_=asy)):\n", + " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3457, in run_code\n", + " exec(code_obj, self.user_global_ns, self.user_ns)\n", + " File \"/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/497488867.py\", line 1, in \n", + " promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM, weights=None)\n", + " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/models/ProMCDA.py\", line 211, in aggregate\n", + " logging.error(str(e), stack_info=True)\n" ] }, { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
minmax_weighted_sum
00.55
10.0
21.0
\n", - "
" - ], - "text/plain": [ - " minmax_weighted_sum\n", - "0 0.55\n", - "1 0.0\n", - "2 1.0" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" + "ename": "ValueError", + "evalue": "The number of polarities does not correspond to the no. of indicators", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/497488867.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maggregation_method\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mAggregationFunctions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mWEIGHTED_SUM\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36maggregate\u001b[0;34m(self, aggregation_method, weights)\u001b[0m\n\u001b[1;32m 206\u001b[0m \u001b[0;31m# Check the number of indicators, weights, and polarities, assign random weights if uncertainty is enabled\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 207\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 208\u001b[0;31m check_indicator_weights_polarities(num_indicators, polar, robustness_weights=self.robustness_weights,\n\u001b[0m\u001b[1;32m 209\u001b[0m weights=weights)\n\u001b[1;32m 210\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mValueError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/configuration/configuration_validator.py\u001b[0m in \u001b[0;36mcheck_indicator_weights_polarities\u001b[0;34m(num_indicators, polar, robustness_weights, weights)\u001b[0m\n\u001b[1;32m 485\u001b[0m \"\"\"\n\u001b[1;32m 486\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnum_indicators\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpolar\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 487\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'The number of polarities does not correspond to the no. of indicators'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 488\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0;31m# Check the number of fixed weights if \"robustness_on_all_weights\" is set to \"no\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mValueError\u001b[0m: The number of polarities does not correspond to the no. of indicators" + ] } ], "source": [ diff --git a/mcda/models/ProMCDA.py b/mcda/models/ProMCDA.py index daf9c2d..40b5bd4 100644 --- a/mcda/models/ProMCDA.py +++ b/mcda/models/ProMCDA.py @@ -273,10 +273,31 @@ def aggregate(self, aggregation_method: Optional[AggregationFunctions] = None, w self.all_indicators_means_scores_normalized = all_indicators_means_scores_normalized self.all_indicators_scores_stds_normalized = all_indicators_scores_stds_normalized + return "Aggregation considered uncertainty on indicators, resulsts are not explicitly shown." + else: - # write message error - return ("Aggregation considered uncertainty on indicators, resulsts are not explicitly shown.") + logger.error('Error Message', stack_info=True) + raise ValueError('Inconsistent configuration: robustness_weights and robustness_indicators are both enabled.') + + + def get_aggregated_values_with_robustness(self) -> Optional[Tuple[pd.DataFrame, pd.DataFrame]]: + """ + Getter method to access aggregated scores when robustness on indicators is performed. + + Returns: + A tuple containing two DataFrames: + - The mean scores of the aggregated indicators. + - The standard deviations of the aggregated indicators. + If robustness is not enabled, returns None. + """ + + means = getattr(self, 'all_indicators_means_scores_normalized', None) + stds = getattr(self, 'all_indicators_scores_stds_normalized', None) + + if means is not None and stds is not None: + return means, stds + return None def run_mcda(self, is_robustness_indicators: int, is_robustness_weights: int, diff --git a/mcda/utils/utils_for_parallelization.py b/mcda/utils/utils_for_parallelization.py index 48e19ca..7022f67 100644 --- a/mcda/utils/utils_for_parallelization.py +++ b/mcda/utils/utils_for_parallelization.py @@ -168,7 +168,7 @@ def normalize_indicators_in_parallel(norm: object, method=None) -> dict: return normalized_indicators -def aggregate_indicators_in_parallel(agg: object, normalized_indicators: dict, method=None) -> pd.DataFrame: +def aggregate_indicators_in_parallel(agg: object, normalized_indicators: dict, method: Optional[AggregationFunctions] = None) -> pd.DataFrame: """ Aggregate normalized indicators in parallel using different aggregation methods. @@ -209,22 +209,22 @@ def aggregate_indicators_in_parallel(agg: object, normalized_indicators: dict, m 'harm-minmax_without_zero', 'harm-target_without_zero', 'harm-standardized_without_zero', 'harm-rank', 'min-standardized_any'] # same order as in the following loop for key, values in normalized_indicators.items(): - if method is None or method == 'weighted_sum': + if method is None or method == AggregationFunctions.WEIGHTED_SUM: # ws goes only with some specific normalizations if key in ["standardized_any", "minmax_01", "target_01", "rank"]: scores_weighted_sum[key] = agg.weighted_sum(values) col_names_method.append("ws-" + key) - if method is None or method == 'geometric': + if method is None or method == AggregationFunctions.GEOMETRIC: # geom goes only with some specific normalizations if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", "rank"]: scores_geometric[key] = pd.Series(agg.geometric(values)) col_names_method.append("geom-" + key) - if method is None or method == 'harmonic': + if method is None or method == AggregationFunctions.HARMONIC: # harm goes only with some specific normalizations if key in ["standardized_without_zero", "minmax_without_zero", "target_without_zero", "rank"]: scores_harmonic[key] = pd.Series(agg.harmonic(values)) col_names_method.append("harm-" + key) - if method is None or method == 'minimum': + if method is None or method == AggregationFunctions.MINIMUM: if key == "standardized_any": scores_minimum[key] = pd.Series(agg.minimum( normalized_indicators["standardized_any"])) diff --git a/tests/unit_tests/test_promcda.py b/tests/unit_tests/test_promcda.py index 7eeee91..1a48950 100644 --- a/tests/unit_tests/test_promcda.py +++ b/tests/unit_tests/test_promcda.py @@ -225,15 +225,16 @@ def test_aggregate_with_robustness(self): random_seed=self.random_seed ) promcda.normalize(normalization_method) - aggregated_scores = promcda.aggregate(aggregation_method=aggregation_method) - expected_columns = ['minmax_weighted_sum'] + promcda.aggregate(aggregation_method=aggregation_method) + aggregated_scores, aggregated_stds = promcda.get_aggregated_values_with_robustness() + expected_columns = ['ws-minmax_01'] # Then self.assertCountEqual(aggregated_scores.columns, expected_columns, "Only specified methods should be applied.") self.assertTrue( - (aggregated_scores['minmax_weighted_sum'] >= 0).all() and ( - aggregated_scores['minmax_weighted_sum'] <= 1).all(), + (aggregated_scores['ws-minmax_01'] >= 0).all() and ( + aggregated_scores['ws-minmax_01'] <= 1).all(), "Values should be in the range [0, 1] for minmax normalization with weighted sum.") From b9272a0151f36115b1a345d9e54e6acd04ce6886 Mon Sep 17 00:00:00 2001 From: Flaminia Date: Thu, 12 Dec 2024 18:37:05 +0100 Subject: [PATCH 27/27] update notebook with end to end tests --- demo_in_notebook/use_promcda_library.ipynb | 1195 +++++++++++++++++--- 1 file changed, 1050 insertions(+), 145 deletions(-) diff --git a/demo_in_notebook/use_promcda_library.ipynb b/demo_in_notebook/use_promcda_library.ipynb index ea0bace..da2e4ed 100644 --- a/demo_in_notebook/use_promcda_library.ipynb +++ b/demo_in_notebook/use_promcda_library.ipynb @@ -35,29 +35,21 @@ }, { "cell_type": "code", - "execution_count": 9, - "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", + "execution_count": 2, + "id": "9dbbd731-e201-47cc-8bfb-159a52559b25", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'input_matrix': ind1_min ind1_max ind2 ind3_average ind3_std ind4_average \\\n", - " alternatives \n", - " alt1 -15.2 8.2 0.04 24.5 6.20 -15.2 \n", - " alt2 -12.4 8.7 0.05 24.5 4.80 -12.4 \n", - " alt3 10.6 2.0 0.11 14.0 0.60 1.6 \n", - " alt4 -39.7 14.0 0.01 26.5 4.41 -39.7 \n", - " \n", - " ind4_std ind5 ind6_average ind6_std \n", - " alternatives \n", - " alt1 8.2 0.04 24.5 6.20 \n", - " alt2 8.7 0.05 24.5 4.80 \n", - " alt3 2.0 0.11 14.0 0.60 \n", - " alt4 14.0 0.01 26.5 4.41 ,\n", - " 'polarity': ('+', '-', '+', '+', '+', '+'),\n", + "{'input_matrix': Criteria 1 Criteria 2\n", + " Alternatives \n", + " A 0.5 0.3\n", + " B 0.2 0.6\n", + " C 0.8 0.1,\n", + " 'polarity': ('+', '-'),\n", " 'robustness_weights': False,\n", - " 'robustness_indicators': True,\n", + " 'robustness_indicators': False,\n", " 'marginal_distributions': [,\n", " ],\n", " 'num_runs': 5,\n", @@ -65,37 +57,98 @@ " 'output_path': 'mock_output/'}" ] }, - "execution_count": 9, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def setUp():\n", + "def setUpNoRobustnessIndicators():\n", " \n", " # Mock input data for testing\n", " input_matrix_without_uncertainty = pd.DataFrame({\n", + " 'Alternatives': ['A', 'B', 'C'],\n", " 'Criteria 1': [0.5, 0.2, 0.8],\n", " 'Criteria 2': [0.3, 0.6, 0.1]\n", " }, index=['A', 'B', 'C'])\n", + " \n", + " input_matrix_without_uncertainty.set_index('Alternatives', inplace=True)\n", + "\n", + " polarity = ('+', '-')\n", + "\n", + " robustness_weights = False\n", + " robustness_indicators = False\n", + "\n", + " marginal_distributions = [PDFType.NORMAL, PDFType.NORMAL]\n", + "\n", + " num_runs = 5\n", + " num_cores = 1\n", + "\n", + " output_path = 'mock_output/'\n", "\n", + " # Return the setup parameters as a dictionary\n", + " return {\n", + " 'input_matrix': input_matrix_without_uncertainty, # Decide what type of input matrix\n", + " 'polarity': polarity,\n", + " 'robustness_weights': robustness_weights,\n", + " 'robustness_indicators': robustness_indicators,\n", + " 'marginal_distributions': marginal_distributions,\n", + " 'num_runs': num_runs,\n", + " 'num_cores': num_cores,\n", + " 'output_path': output_path\n", + " }\n", + "\n", + "# Run the setup and store parameters in a variable\n", + "setup_no_robustness_indicators = setUpNoRobustnessIndicators()\n", + "\n", + "# Check the setup parameters\n", + "setup_no_robustness_indicators" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "cef40536-5942-44a4-9a2c-2a9e9b02b7a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'input_matrix': Criterion1_mean Criterion1_std Criterion2_mean Criterion2_std\n", + " Alternatives \n", + " A 0.5 0.10 0.3 0.03\n", + " B 0.2 0.02 0.6 0.06\n", + " C 0.8 0.07 0.1 0.01,\n", + " 'polarity': ('+', '-'),\n", + " 'robustness_weights': False,\n", + " 'robustness_indicators': True,\n", + " 'marginal_distributions': [,\n", + " ],\n", + " 'num_runs': 5,\n", + " 'num_cores': 1,\n", + " 'output_path': 'mock_output/'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def setUpRobustnessIndicators():\n", + " \n", + " # Mock input data for testing\n", " input_matrix_with_uncertainty = pd.DataFrame({\n", - " 'alternatives': ['alt1', 'alt2', 'alt3', 'alt4'],\n", - " 'ind1_min': [-15.20, -12.40, 10.60, -39.70],\n", - " 'ind1_max': [8.20, 8.70, 2.00, 14.00],\n", - " 'ind2': [0.04, 0.05, 0.11, 0.01],\n", - " 'ind3_average': [24.50, 24.50, 14.00, 26.50],\n", - " 'ind3_std': [6.20, 4.80, 0.60, 4.41],\n", - " 'ind4_average': [-15.20, -12.40, 1.60, -39.70],\n", - " 'ind4_std': [8.20, 8.70, 2.00, 14.00],\n", - " 'ind5': [0.04, 0.05, 0.11, 0.01],\n", - " 'ind6_average': [24.50, 24.50, 14.00, 26.50],\n", - " 'ind6_std': [6.20, 4.80, 0.60, 4.41]\n", - " })\n", + " 'Alternatives': ['A', 'B', 'C'],\n", + " 'Criterion1_mean': [0.5, 0.2, 0.8],\n", + " 'Criterion1_std': [0.1, 0.02, 0.07],\n", + " 'Criterion2_mean': [0.3, 0.6, 0.1],\n", + " 'Criterion2_std': [0.03, 0.06, 0.01]\n", + " })\n", " \n", - " input_matrix_with_uncertainty.set_index('alternatives', inplace=True)\n", + " input_matrix_with_uncertainty.set_index('Alternatives', inplace=True)\n", "\n", - " polarity = ('+', '-', '+', '+', '+', '+')\n", + " polarity = ('+', '-')\n", "\n", " robustness_weights = False\n", " robustness_indicators = True\n", @@ -120,197 +173,1049 @@ " }\n", "\n", "# Run the setup and store parameters in a variable\n", - "setup_parameters = setUp()\n", + "setup_robustness_indicators = setUpRobustnessIndicators()\n", "\n", "# Check the setup parameters\n", - "setup_parameters" + "setup_robustness_indicators" + ] + }, + { + "cell_type": "markdown", + "id": "07775901-e7d2-447c-a96e-b882748e9f4f", + "metadata": {}, + "source": [ + "## TEST NO ROBUSTNESS" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", + "execution_count": 11, + "id": "c9ffcdc6-0243-4303-8afd-33c7f3de5c21", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-12-12 14:24:40,588 - ProMCDA - Alternatives are ['alt1', 'alt2', 'alt3', 'alt4']\n" + "INFO: 2024-12-12 16:48:33,048 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" ] } ], "source": [ "promcda = ProMCDA(\n", - " input_matrix=setup_parameters['input_matrix'],\n", - " polarity=setup_parameters['polarity'],\n", - " robustness_weights=setup_parameters['robustness_weights'],\n", - " robustness_indicators=setup_parameters['robustness_indicators'],\n", - " marginal_distributions=setup_parameters['marginal_distributions'],\n", - " num_runs=setup_parameters['num_runs'],\n", - " num_cores=setup_parameters['num_cores'],\n", + " input_matrix=setup_no_robustness_indicators['input_matrix'],\n", + " polarity=setup_no_robustness_indicators['polarity'],\n", + " robustness_weights=setup_no_robustness_indicators['robustness_weights'],\n", + " robustness_indicators=setup_no_robustness_indicators['robustness_indicators'],\n", + " marginal_distributions=setup_no_robustness_indicators['marginal_distributions'],\n", + " num_runs=setup_no_robustness_indicators['num_runs'],\n", + " num_cores=setup_no_robustness_indicators['num_cores'],\n", " #output_path=setup_parameters['output_path']\n", ")" ] }, + { + "cell_type": "markdown", + "id": "94796bf7-fd25-4f0d-bdd6-f828f3e0b5f1", + "metadata": {}, + "source": [ + "### Test normalize with sensitivity¶" + ] + }, { "cell_type": "code", - "execution_count": 11, - "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", - "metadata": { - "scrolled": true - }, + "execution_count": 12, + "id": "dfd69479-446e-4919-8b3e-c888d9e675f7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zeroCriteria 1_target_01Criteria 2_target_01Criteria 1_target_without_zeroCriteria 2_target_without_zeroCriteria 1_standardized_anyCriteria 2_standardized_anyCriteria 1_standardized_without_zeroCriteria 2_standardized_without_zeroCriteria 1_rankCriteria 2_rank
00.50.60.550.640.6250.50.66250.550.00.1324531.11.2920792.02.0
10.00.00.10.10.250.00.3250.1-1.0-1.0596260.10.11.01.0
21.01.01.01.01.00.8333331.00.851.00.9271732.12.0867993.03.0
\n", + "
" + ], + "text/plain": [ + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " Criteria 2_minmax_without_zero Criteria 1_target_01 Criteria 2_target_01 \\\n", + "0 0.64 0.625 0.5 \n", + "1 0.1 0.25 0.0 \n", + "2 1.0 1.0 0.833333 \n", + "\n", + " Criteria 1_target_without_zero Criteria 2_target_without_zero \\\n", + "0 0.6625 0.55 \n", + "1 0.325 0.1 \n", + "2 1.0 0.85 \n", + "\n", + " Criteria 1_standardized_any Criteria 2_standardized_any \\\n", + "0 0.0 0.132453 \n", + "1 -1.0 -1.059626 \n", + "2 1.0 0.927173 \n", + "\n", + " Criteria 1_standardized_without_zero Criteria 2_standardized_without_zero \\\n", + "0 1.1 1.292079 \n", + "1 0.1 0.1 \n", + "2 2.1 2.086799 \n", + "\n", + " Criteria 1_rank Criteria 2_rank \n", + "0 2.0 2.0 \n", + "1 1.0 1.0 \n", + "2 3.0 3.0 " + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize()" + ] + }, + { + "cell_type": "markdown", + "id": "267aae1e-3b4d-4ad0-8337-fa46a68081f8", + "metadata": {}, + "source": [ + "### Test normalize with specific method" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "2ba273b0-18ac-43a1-8afc-f3280f6e2d9f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Criteria 1_minmax_01Criteria 2_minmax_01Criteria 1_minmax_without_zeroCriteria 2_minmax_without_zero
00.50.60.550.64
10.00.00.10.1
21.01.01.01.0
\n", + "
" + ], + "text/plain": [ + " Criteria 1_minmax_01 Criteria 2_minmax_01 Criteria 1_minmax_without_zero \\\n", + "0 0.5 0.6 0.55 \n", + "1 0.0 0.0 0.1 \n", + "2 1.0 1.0 1.0 \n", + "\n", + " Criteria 2_minmax_without_zero \n", + "0 0.64 \n", + "1 0.1 \n", + "2 1.0 " + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize(NormalizationFunctions.MINMAX)" + ] + }, + { + "cell_type": "markdown", + "id": "eb96c3cb-6a6c-493a-9e30-089fc3676052", + "metadata": {}, + "source": [ + "### Test aggregate with full sensitivity" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "dccaf3ad-79e2-4110-a286-8bfef5abbd6a", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-12-12 14:24:42,348 - ProMCDA - There is a problem with the parameters given in the input matrix with uncertainties. Check your data!\n", - "INFO: 2024-12-12 14:24:42,349 - ProMCDA - Either standard deviation values of normal/lognormal indicators are larger than their means\n", - "INFO: 2024-12-12 14:24:42,350 - ProMCDA - or max. values of uniform distributed indicators are smaller than their min. values.\n", - "INFO: 2024-12-12 14:24:42,350 - ProMCDA - If you continue, the negative values will be rescaled internally to a positive range.\n" + "INFO: 2024-12-12 17:20:58,500 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-12-12 17:20:58,501 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-12-12 17:20:58,502 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-12-12 17:20:58,502 - ProMCDA - Weights: [0.5, 0.5]\n", + "INFO: 2024-12-12 17:20:58,503 - ProMCDA - Normalized weights: [0.5, 0.5]\n" ] }, { - "ename": "IndexError", - "evalue": "positional indexers are out-of-bounds", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mRemoteTraceback\u001b[0m Traceback (most recent call last)", - "\u001b[0;31mRemoteTraceback\u001b[0m: \n\"\"\"\nTraceback (most recent call last):\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 125, in worker\n result = (True, func(*args, **kwds))\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\", line 48, in mapstar\n return list(map(*args))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 97, in initialize_and_call_normalization\n dict_normalized_matrix = normalize_indicators_in_parallel(norm, method)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\", line 138, in normalize_indicators_in_parallel\n indicators_scaled_minmax_01 = norm.minmax(feature_range=(0, 1))\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/mcda_functions/normalization.py\", line 99, in minmax\n pol = Normalization._cast_polarities(self)\n File \"/Users/flaminia/Documents/work/ProMCDA/mcda/mcda_functions/normalization.py\", line 30, in _cast_polarities\n indicators_plus = self._input_matrix.iloc[:, ind_plus]\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1184, in __getitem__\n return self._getitem_tuple(key)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1690, in _getitem_tuple\n tup = self._validate_tuple_indexer(tup)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 966, in _validate_tuple_indexer\n self._validate_key(k, i)\n File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/indexing.py\", line 1612, in _validate_key\n raise IndexError(\"positional indexers are out-of-bounds\")\nIndexError: positional indexers are out-of-bounds\n\"\"\"", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[0;31mIndexError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/3655398325.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36mnormalize\u001b[0;34m(self, normalization_method)\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 142\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mnormalization_method\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 143\u001b[0;31m n_normalized_input_matrices = utils_for_parallelization.parallelize_normalization(\n\u001b[0m\u001b[1;32m 144\u001b[0m n_random_input_matrices, self.polarity)\n\u001b[1;32m 145\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/utils/utils_for_parallelization.py\u001b[0m in \u001b[0;36mparallelize_normalization\u001b[0;34m(input_matrices, polar, method)\u001b[0m\n\u001b[1;32m 291\u001b[0m \u001b[0mpool\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmultiprocessing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mPool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 292\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpolar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mdf\u001b[0m \u001b[0;32min\u001b[0m \u001b[0minput_matrices\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 293\u001b[0;31m \u001b[0mres\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minitialize_and_call_normalization\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0margs_for_parallel_norm\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 294\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 295\u001b[0m \u001b[0mpool\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mmap\u001b[0;34m(self, func, iterable, chunksize)\u001b[0m\n\u001b[1;32m 362\u001b[0m \u001b[0;32min\u001b[0m \u001b[0ma\u001b[0m \u001b[0mlist\u001b[0m \u001b[0mthat\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0mreturned\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 363\u001b[0m '''\n\u001b[0;32m--> 364\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_map_async\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmapstar\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mget\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 365\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 366\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mstarmap\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0miterable\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mchunksize\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/multiprocessing/pool.py\u001b[0m in \u001b[0;36mget\u001b[0;34m(self, timeout)\u001b[0m\n\u001b[1;32m 769\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 770\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 771\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_value\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 772\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 773\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_set\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mi\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobj\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mIndexError\u001b[0m: positional indexers are out-of-bounds" + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
minmax_weighted_sumtarget_weighted_sumstandardized_weighted_sumrank_weighted_summinmax_geometrictarget_geometricstandardized_geometricrank_geometricminmax_harmonictarget_harmonicstandardized_harmonicrank_harmonicstandardized_minimum
00.550.56250.0662272.00.5932960.6036351.1921772.00.5915970.6010311.1883282.00.0
10.00.125-1.0298131.00.1000000.1802780.1000001.00.10.1529410.11.0-1.059626
21.00.9166670.9635863.01.0000000.9219542.0933893.01.00.9189192.0933783.00.927173
\n", + "
" + ], + "text/plain": [ + " minmax_weighted_sum target_weighted_sum standardized_weighted_sum \\\n", + "0 0.55 0.5625 0.066227 \n", + "1 0.0 0.125 -1.029813 \n", + "2 1.0 0.916667 0.963586 \n", + "\n", + " rank_weighted_sum minmax_geometric target_geometric \\\n", + "0 2.0 0.593296 0.603635 \n", + "1 1.0 0.100000 0.180278 \n", + "2 3.0 1.000000 0.921954 \n", + "\n", + " standardized_geometric rank_geometric minmax_harmonic target_harmonic \\\n", + "0 1.192177 2.0 0.591597 0.601031 \n", + "1 0.100000 1.0 0.1 0.152941 \n", + "2 2.093389 3.0 1.0 0.918919 \n", + "\n", + " standardized_harmonic rank_harmonic standardized_minimum \n", + "0 1.188328 2.0 0.0 \n", + "1 0.1 1.0 -1.059626 \n", + "2 2.093378 3.0 0.927173 " + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize()\n", + "promcda.aggregate()" + ] + }, + { + "cell_type": "markdown", + "id": "3d221807-cd40-41fc-b295-a9e0e1c4dc08", + "metadata": {}, + "source": [ + "### Test aggregate with sensitivity on aggregation" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "587b6905-e9bc-4cbc-a923-501f35e225ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 17:21:45,563 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-12-12 17:21:45,564 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-12-12 17:21:45,567 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-12-12 17:21:45,567 - ProMCDA - Weights: [0.5, 0.5]\n", + "INFO: 2024-12-12 17:21:45,568 - ProMCDA - Normalized weights: [0.5, 0.5]\n" ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
minmax_weighted_summinmax_geometricminmax_harmonic
00.550.5932960.591597
10.00.1000000.1
21.01.0000001.0
\n", + "
" + ], + "text/plain": [ + " minmax_weighted_sum minmax_geometric minmax_harmonic\n", + "0 0.55 0.593296 0.591597\n", + "1 0.0 0.100000 0.1\n", + "2 1.0 1.000000 1.0" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "promcda.normalize()" + "promcda.normalize(NormalizationFunctions.MINMAX)\n", + "promcda.aggregate()" + ] + }, + { + "cell_type": "markdown", + "id": "29e6c4e2-e347-4e9c-8897-2113770441e0", + "metadata": {}, + "source": [ + "### Test aggregate with sensitivity on normalization" ] }, { "cell_type": "code", - "execution_count": null, - "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "execution_count": 20, + "id": "89425df3-407c-4d13-945a-4355182d2900", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 17:23:04,104 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-12-12 17:23:04,105 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-12-12 17:23:04,105 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-12-12 17:23:04,106 - ProMCDA - Weights: [0.5, 0.5]\n", + "INFO: 2024-12-12 17:23:04,106 - ProMCDA - Normalized weights: [0.5, 0.5]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
minmax_weighted_sumtarget_weighted_sumstandardized_weighted_sumrank_weighted_sum
00.550.56250.0662272.0
10.00.125-1.0298131.0
21.00.9166670.9635863.0
\n", + "
" + ], + "text/plain": [ + " minmax_weighted_sum target_weighted_sum standardized_weighted_sum \\\n", + "0 0.55 0.5625 0.066227 \n", + "1 0.0 0.125 -1.029813 \n", + "2 1.0 0.916667 0.963586 \n", + "\n", + " rank_weighted_sum \n", + "0 2.0 \n", + "1 1.0 \n", + "2 3.0 " + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "promcda.normalize(NormalizationFunctions.MINMAX)" + "promcda.normalize()\n", + "promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM)" + ] + }, + { + "cell_type": "markdown", + "id": "cd50f9eb-2987-451b-bf1d-c67a0b2a80e8", + "metadata": {}, + "source": [ + "### Test aggregate with robustness on weights" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "0038ddb3-5499-4fd4-a5de-d785593885a9", + "execution_count": 21, + "id": "3e8bfc0c-9b78-4058-bdc2-90d00bc8ae7e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 17:25:09,956 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + } + ], "source": [ - "promcda.normalized_values_without_robustness" + "promcda = ProMCDA(\n", + " input_matrix=setup_no_robustness_indicators['input_matrix'],\n", + " polarity=setup_no_robustness_indicators['polarity'],\n", + " robustness_weights=True,\n", + " robustness_indicators=setup_no_robustness_indicators['robustness_indicators'],\n", + " marginal_distributions=setup_no_robustness_indicators['marginal_distributions'],\n", + " num_runs=setup_no_robustness_indicators['num_runs'],\n", + " num_cores=setup_no_robustness_indicators['num_cores'],\n", + " #output_path=setup_parameters['output_path']\n", + ")" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "e857a02a-aae8-453a-a302-46844c3e610f", + "execution_count": 22, + "id": "8ab04ddd-69a0-4e41-808b-bf57506eda95", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "INFO: 2024-12-12 14:22:21,046 - ProMCDA - Number of alternatives: 4\n", - "INFO: 2024-12-12 14:22:21,048 - ProMCDA - Number of indicators: 8\n", - "INFO: 2024-12-12 14:22:21,049 - ProMCDA - Polarities: ('+', '-')\n", - "INFO: 2024-12-12 14:22:21,050 - ProMCDA - Weights: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]\n", - "INFO: 2024-12-12 14:22:21,050 - ProMCDA - Normalized weights: [0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125, 0.125]\n", - "ERROR: 2024-12-12 14:22:21,066 - root - The number of polarities does not correspond to the no. of indicators\n", - "Stack (most recent call last):\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/runpy.py\", line 197, in _run_module_as_main\n", - " return _run_code(code, main_globals, None,\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/runpy.py\", line 87, in _run_code\n", - " exec(code, run_globals)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel_launcher.py\", line 18, in \n", - " app.launch_new_instance()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/traitlets/config/application.py\", line 1075, in launch_instance\n", - " app.start()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelapp.py\", line 739, in start\n", - " self.io_loop.start()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/tornado/platform/asyncio.py\", line 205, in start\n", - " self.asyncio_loop.run_forever()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/base_events.py\", line 601, in run_forever\n", - " self._run_once()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/base_events.py\", line 1905, in _run_once\n", - " handle._run()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/asyncio/events.py\", line 80, in _run\n", - " self._context.run(self._callback, *self._args)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 545, in dispatch_queue\n", - " await self.process_one()\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 534, in process_one\n", - " await dispatch(*args)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 437, in dispatch_shell\n", - " await result\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/ipkernel.py\", line 362, in execute_request\n", - " await super().execute_request(stream, ident, parent)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/kernelbase.py\", line 778, in execute_request\n", - " reply_content = await reply_content\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/ipkernel.py\", line 456, in do_execute\n", - " res = shell.run_cell(code, store_history=store_history, silent=silent)\n", - " File \"/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/ipykernel/zmqshell.py\", line 549, in run_cell\n", - " return super().run_cell(*args, **kwargs)\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 2914, in run_cell\n", - " result = self._run_cell(\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 2960, in _run_cell\n", - " return runner(coro)\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/async_helpers.py\", line 78, in _pseudo_sync_runner\n", - " coro.send(None)\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3185, in run_cell_async\n", - " has_raised = await self.run_ast_nodes(code_ast.body, cell_name,\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3377, in run_ast_nodes\n", - " if (await self.run_code(code, result, async_=asy)):\n", - " File \"/Users/flaminia/.local/lib/python3.9/site-packages/IPython/core/interactiveshell.py\", line 3457, in run_code\n", - " exec(code_obj, self.user_global_ns, self.user_ns)\n", - " File \"/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/497488867.py\", line 1, in \n", - " promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM, weights=None)\n", - " File \"/Users/flaminia/Documents/work/ProMCDA/mcda/models/ProMCDA.py\", line 211, in aggregate\n", - " logging.error(str(e), stack_info=True)\n" + "INFO: 2024-12-12 17:25:50,608 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-12-12 17:25:50,610 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-12-12 17:25:50,611 - ProMCDA - Polarities: ('+', '-')\n" ] }, { "ename": "ValueError", - "evalue": "The number of polarities does not correspond to the no. of indicators", + "evalue": "Unable to coerce to Series, length must be 2: given 0", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_47279/497488867.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maggregation_method\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mAggregationFunctions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mWEIGHTED_SUM\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36maggregate\u001b[0;34m(self, aggregation_method, weights)\u001b[0m\n\u001b[1;32m 206\u001b[0m \u001b[0;31m# Check the number of indicators, weights, and polarities, assign random weights if uncertainty is enabled\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 207\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 208\u001b[0;31m check_indicator_weights_polarities(num_indicators, polar, robustness_weights=self.robustness_weights,\n\u001b[0m\u001b[1;32m 209\u001b[0m weights=weights)\n\u001b[1;32m 210\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mValueError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/work/ProMCDA/mcda/configuration/configuration_validator.py\u001b[0m in \u001b[0;36mcheck_indicator_weights_polarities\u001b[0;34m(num_indicators, polar, robustness_weights, weights)\u001b[0m\n\u001b[1;32m 485\u001b[0m \"\"\"\n\u001b[1;32m 486\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mnum_indicators\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpolar\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 487\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'The number of polarities does not correspond to the no. of indicators'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 488\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0;31m# Check the number of fixed weights if \"robustness_on_all_weights\" is set to \"no\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mValueError\u001b[0m: The number of polarities does not correspond to the no. of indicators" + "\u001b[0;32m/var/folders/mt/lx9bxf895gq_bfbn6l6_m3kr0000gn/T/ipykernel_48790/1777039636.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mnormalize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mNormalizationFunctions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mMINMAX\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mpromcda\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maggregate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maggregation_method\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mAggregationFunctions\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mWEIGHTED_SUM\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/ProMCDA.py\u001b[0m in \u001b[0;36maggregate\u001b[0;34m(self, aggregation_method, weights)\u001b[0m\n\u001b[1;32m 228\u001b[0m \u001b[0maggregated_scores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mpd\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconcat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maggregated_scores\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 229\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 230\u001b[0;31m aggregated_scores = mcda_without_robustness.aggregate_indicators(\n\u001b[0m\u001b[1;32m 231\u001b[0m \u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mnormalized_indicators\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 232\u001b[0m \u001b[0mweights\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mweights\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/mcda_without_robustness.py\u001b[0m in \u001b[0;36maggregate_indicators\u001b[0;34m(self, normalized_indicators, weights, agg_method)\u001b[0m\n\u001b[1;32m 165\u001b[0m if norm_method in [NormalizationFunctions.STANDARDIZED.value, NormalizationFunctions.MINMAX.value,\n\u001b[1;32m 166\u001b[0m NormalizationFunctions.TARGET.value, NormalizationFunctions.RANK.value]:\n\u001b[0;32m--> 167\u001b[0;31m _apply_aggregation(norm_method, AggregationFunctions.WEIGHTED_SUM,\n\u001b[0m\u001b[1;32m 168\u001b[0m with_zero_columns)\n\u001b[1;32m 169\u001b[0m \u001b[0;31m# Apply GEOMETRIC and HARMONIC only to columns without zero in the suffix and only some normalization methods\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/models/mcda_without_robustness.py\u001b[0m in \u001b[0;36m_apply_aggregation\u001b[0;34m(norm_function, agg_function, df_subset)\u001b[0m\n\u001b[1;32m 139\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0magg_function\u001b[0m \u001b[0;32min\u001b[0m \u001b[0magg_methods\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[0magg_function\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0magg_functions\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0magg_function\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalue\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 141\u001b[0;31m \u001b[0maggregated_scores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0magg_function\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf_subset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 142\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 143\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maggregated_scores\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mndarray\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/Documents/work/ProMCDA/mcda/mcda_functions/aggregation.py\u001b[0m in \u001b[0;36mweighted_sum\u001b[0;34m(self, norm_indicators)\u001b[0m\n\u001b[1;32m 37\u001b[0m \"\"\"\n\u001b[1;32m 38\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 39\u001b[0;31m \u001b[0mscores\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mnorm_indicators\u001b[0m \u001b[0;34m*\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mweights\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msum\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 40\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mscores\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/ops/common.py\u001b[0m in \u001b[0;36mnew_method\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 74\u001b[0m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mitem_from_zerodim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 75\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 76\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mmethod\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 77\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 78\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mnew_method\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/arraylike.py\u001b[0m in \u001b[0;36m__mul__\u001b[0;34m(self, other)\u001b[0m\n\u001b[1;32m 200\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0munpack_zerodim_and_defer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"__mul__\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 201\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m__mul__\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 202\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_arith_method\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moperator\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmul\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 203\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 204\u001b[0m \u001b[0;34m@\u001b[0m\u001b[0munpack_zerodim_and_defer\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"__rmul__\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36m_arith_method\u001b[0;34m(self, other, op)\u001b[0m\n\u001b[1;32m 7908\u001b[0m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mops\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmaybe_prepare_scalar_for_op\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0maxis\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7909\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 7910\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_align_for_op\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maxis\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mflex\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mlevel\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7911\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7912\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0merrstate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mall\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m\"ignore\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36m_align_for_op\u001b[0;34m(self, other, axis, flex, level)\u001b[0m\n\u001b[1;32m 8185\u001b[0m )\n\u001b[1;32m 8186\u001b[0m \u001b[0;31m# GH#17901\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 8187\u001b[0;31m \u001b[0mright\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mto_series\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mright\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8188\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8189\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mflex\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m \u001b[0;32mand\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mright\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mDataFrame\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/opt/homebrew/Caskroom/miniforge/base/envs/ProMCDA/lib/python3.9/site-packages/pandas/core/frame.py\u001b[0m in \u001b[0;36mto_series\u001b[0;34m(right)\u001b[0m\n\u001b[1;32m 8131\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8132\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mleft\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mright\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 8133\u001b[0;31m raise ValueError(\n\u001b[0m\u001b[1;32m 8134\u001b[0m \u001b[0mmsg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreq_len\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mleft\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolumns\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgiven_len\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mright\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8135\u001b[0m )\n", + "\u001b[0;31mValueError\u001b[0m: Unable to coerce to Series, length must be 2: given 0" + ] + } + ], + "source": [ + "promcda.normalize(NormalizationFunctions.MINMAX)\n", + "promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM)" + ] + }, + { + "cell_type": "markdown", + "id": "04da27ae-0303-447a-b454-5324361fbdf3", + "metadata": {}, + "source": [ + "## TEST ROBUSTNESS INDICATORS" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cd0e175f-9d59-4c96-bcb5-a53d8555988c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 16:45:43,773 - ProMCDA - Alternatives are ['A', 'B', 'C']\n" + ] + } + ], + "source": [ + "promcda = ProMCDA(\n", + " input_matrix=setup_robustness_indicators['input_matrix'],\n", + " polarity=setup_robustness_indicators['polarity'],\n", + " robustness_weights=setup_robustness_indicators['robustness_weights'],\n", + " robustness_indicators=setup_robustness_indicators['robustness_indicators'],\n", + " marginal_distributions=setup_robustness_indicators['marginal_distributions'],\n", + " num_runs=setup_robustness_indicators['num_runs'],\n", + " num_cores=setup_robustness_indicators['num_cores'],\n", + " #output_path=setup_parameters['output_path']\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "448f86cd-6136-4636-9c5e-b803de97ca74", + "metadata": {}, + "source": [ + "### Test normalize with sensitivity" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1518d4a0-9351-4a5e-91db-806f21d32e96", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'standardized_any': 0 1\n", + " 0 0.107857 0.228287\n", + " 1 -1.049557 -1.094406\n", + " 2 0.941699 0.866119,\n", + " 'standardized_without_zero': 0 1\n", + " 0 1.257414 1.422692\n", + " 1 0.1 0.1\n", + " 2 2.091256 2.060524,\n", + " 'minmax_01': 0 1\n", + " 0 0.581248 0.674663\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.623123 0.707196\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0,\n", + " 'target_01': 0 1\n", + " 0 0.679686 0.586277\n", + " 1 0.235073 0.0\n", + " 2 1.0 0.868992,\n", + " 'target_without_zero': 0 1\n", + " 0 0.711717 0.627649\n", + " 1 0.311566 0.1\n", + " 2 1.0 0.882093,\n", + " 'rank': 0 1\n", + " 0 2.0 2.0\n", + " 1 1.0 1.0\n", + " 2 3.0 3.0},\n", + " {'standardized_any': 0 1\n", + " 0 -0.114056 0.161992\n", + " 1 -0.938082 -1.071107\n", + " 2 1.052138 0.909114,\n", + " 'standardized_without_zero': 0 1\n", + " 0 0.924026 1.333099\n", + " 1 0.1 0.1\n", + " 2 2.09022 2.080221,\n", + " 'minmax_01': 0 1\n", + " 0 0.414038 0.622708\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.472634 0.660437\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0,\n", + " 'target_01': 0 1\n", + " 0 0.579062 0.508629\n", + " 1 0.281629 0.0\n", + " 2 1.0 0.816802,\n", + " 'target_without_zero': 0 1\n", + " 0 0.621156 0.557766\n", + " 1 0.353467 0.1\n", + " 2 1.0 0.835122,\n", + " 'rank': 0 1\n", + " 0 2.0 2.0\n", + " 1 1.0 1.0\n", + " 2 3.0 3.0},\n", + " {'standardized_any': 0 1\n", + " 0 -0.029782 0.244632\n", + " 1 -0.984776 -1.099617\n", + " 2 1.014558 0.854985,\n", + " 'standardized_without_zero': 0 1\n", + " 0 1.054994 1.444249\n", + " 1 0.1 0.1\n", + " 2 2.099335 2.054601,\n", + " 'minmax_01': 0 1\n", + " 0 0.477656 0.687736\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.52989 0.718962\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0,\n", + " 'target_01': 0 1\n", + " 0 0.618886 0.597756\n", + " 1 0.270378 0.0\n", + " 2 1.0 0.869166,\n", + " 'target_without_zero': 0 1\n", + " 0 0.656998 0.637981\n", + " 1 0.34334 0.1\n", + " 2 1.0 0.882249,\n", + " 'rank': 0 1\n", + " 0 2.0 2.0\n", + " 1 1.0 1.0\n", + " 2 3.0 3.0},\n", + " {'standardized_any': 0 1\n", + " 0 -0.1171 0.176795\n", + " 1 -0.936295 -1.076607\n", + " 2 1.053394 0.899812,\n", + " 'standardized_without_zero': 0 1\n", + " 0 0.919195 1.353401\n", + " 1 0.1 0.1\n", + " 2 2.089689 2.076419,\n", + " 'minmax_01': 0 1\n", + " 0 0.41172 0.634178\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.470548 0.67076\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0,\n", + " 'target_01': 0 1\n", + " 0 0.548255 0.538117\n", + " 1 0.232092 0.0\n", + " 2 1.0 0.848526,\n", + " 'target_without_zero': 0 1\n", + " 0 0.59343 0.584305\n", + " 1 0.308883 0.1\n", + " 2 1.0 0.863673,\n", + " 'rank': 0 1\n", + " 0 2.0 2.0\n", + " 1 1.0 1.0\n", + " 2 3.0 3.0},\n", + " {'standardized_any': 0 1\n", + " 0 -0.34284 0.126952\n", + " 1 -0.783486 -1.057414\n", + " 2 1.126326 0.930462,\n", + " 'standardized_without_zero': 0 1\n", + " 0 0.540646 1.284365\n", + " 1 0.1 0.1\n", + " 2 2.009812 2.087876,\n", + " 'minmax_01': 0 1\n", + " 0 0.230727 0.595794\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.307655 0.636215\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0,\n", + " 'target_01': 0 1\n", + " 0 0.3992 0.49227\n", + " 1 0.219002 0.0\n", + " 2 1.0 0.826242,\n", + " 'target_without_zero': 0 1\n", + " 0 0.45928 0.543043\n", + " 1 0.297102 0.1\n", + " 2 1.0 0.843618,\n", + " 'rank': 0 1\n", + " 0 2.0 2.0\n", + " 1 1.0 1.0\n", + " 2 3.0 3.0}]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize()\n", + "promcda.get_normalized_values_with_robustness() # If robustness_indicators" + ] + }, + { + "cell_type": "markdown", + "id": "e3d4c24e-6203-4ab6-bfb4-72defe219ac3", + "metadata": {}, + "source": [ + "### Test normalize with specific method" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1aec0eab-5c5a-4279-91b1-891a3fc9a868", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'minmax_01': 0 1\n", + " 0 0.581248 0.674663\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.623123 0.707196\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0},\n", + " {'minmax_01': 0 1\n", + " 0 0.414038 0.622708\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.472634 0.660437\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0},\n", + " {'minmax_01': 0 1\n", + " 0 0.477656 0.687736\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.52989 0.718962\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0},\n", + " {'minmax_01': 0 1\n", + " 0 0.41172 0.634178\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.470548 0.67076\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0},\n", + " {'minmax_01': 0 1\n", + " 0 0.230727 0.595794\n", + " 1 0.0 0.0\n", + " 2 1.0 1.0,\n", + " 'minmax_without_zero': 0 1\n", + " 0 0.307655 0.636215\n", + " 1 0.1 0.1\n", + " 2 1.0 1.0}]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.normalize(NormalizationFunctions.MINMAX)\n", + "promcda.get_normalized_values_with_robustness() # If robustness_indicators" + ] + }, + { + "cell_type": "markdown", + "id": "2c6ad5ee-a10e-4c89-8bb3-c60190bb587d", + "metadata": {}, + "source": [ + "### Test aggregate with robustness - need setUpRobustnessIndicators" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "15bb84ff-e194-4326-a368-c467c9b6e3a7", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: 2024-12-12 16:46:06,406 - ProMCDA - Number of alternatives: 3\n", + "INFO: 2024-12-12 16:46:06,406 - ProMCDA - Number of indicators: 2\n", + "INFO: 2024-12-12 16:46:06,407 - ProMCDA - Polarities: ('+', '-')\n", + "INFO: 2024-12-12 16:46:06,407 - ProMCDA - Weights: [0.5, 0.5]\n", + "INFO: 2024-12-12 16:46:06,407 - ProMCDA - Normalized weights: [0.5, 0.5]\n", + "INFO: 2024-12-12 16:46:06,407 - ProMCDA - Start ProMCDA with uncertainty on the indicators\n", + "INFO: 2024-12-12 16:46:06,407 - ProMCDA - The number of Monte-Carlo runs is only 5\n", + "INFO: 2024-12-12 16:46:06,408 - ProMCDA - A meaningful number of Monte-Carlo runs is equal or larger than 1000\n" ] + }, + { + "data": { + "text/plain": [ + "'Aggregation considered uncertainty on indicators, resulsts are not explicitly shown.'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ + "promcda.normalize(normalization_method=NormalizationFunctions.MINMAX)\n", "promcda.aggregate(aggregation_method=AggregationFunctions.WEIGHTED_SUM, weights=None)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "506619fd-9659-47cd-88fa-7acd985a1071", + "execution_count": 8, + "id": "128cc88d-928e-439e-896a-be71faa60075", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "data": { + "text/plain": [ + "( ws-minmax_01\n", + " 0 0.533047\n", + " 1 0.000000\n", + " 2 1.000000,\n", + " ws-minmax_01\n", + " 0 0.080837\n", + " 1 0.000000\n", + " 2 0.000000)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "promcda.get_aggregated_values_with_robustness()" + ] }, { "cell_type": "code", "execution_count": null, - "id": "15bb84ff-e194-4326-a368-c467c9b6e3a7", + "id": "9cf166df-6244-4364-8006-bacf98570225", "metadata": {}, "outputs": [], "source": []