Skip to content

Commit

Permalink
add normalization as method, failing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Flaminietta committed Oct 22, 2024
1 parent 9d747ca commit d232c1d
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 59 deletions.
11 changes: 6 additions & 5 deletions mcda/configuration/configuration_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion mcda/mcda_functions/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 58 additions & 48 deletions mcda/models/ProMCDA.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions mcda/models/mcda_with_robustness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 0 additions & 1 deletion mcda/models/mcda_without_robustness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 45 additions & 2 deletions tests/unit_tests/test_promcda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -80,5 +124,4 @@ def tearDown(self):
if __name__ == '__main__':
unittest.main()

# TODO: write additional tests for normalization, aggregation, etc.

0 comments on commit d232c1d

Please sign in to comment.