Skip to content

Commit 1acb08a

Browse files
refactor: use model classes and also refactor original config implementation to use model classes
1 parent 26f0185 commit 1acb08a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3675
-332
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
.DS_Store
22
.idea
33
.ipynb_checkpoints/
4+
__pycache__/
5+
output_files

configuration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222

2323

2424
"output_directory_path": "toy_example"
25-
}
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"inputMatrix": {
3+
"Fuel Efficiency":{"A":30.0,"B":25.0,"C":35.0,"D":40.0,"E":38.0,"F":40.0},
4+
"Safety Rating":{"A":5.0,"B":4.0,"C":5.0,"D":3.0,"E":1.0,"F":4.0},
5+
"Price":{"A":25000.0,"B":30000.0,"C":20000.0,"D":24000.0,"E":15000.0,"F":28000.0},
6+
"Cargo 'Space":{"A":15.0,"B":20.0,"C":10.0,"D":30.0,"E":10.0,"F":15.0},
7+
"Acceleration":{"A":8.0,"B":6.0,"C":10.0,"D":7.0,"E":10.0,"F":9.0},
8+
"Warranty":{"A":3.0,"B":5.0,"C":2.0,"D":1.0,"E":3.0,"F":4.0}
9+
},
10+
"weights": [
11+
0.8, 0.9, 0.5, 0.5, 0.1, 0.7
12+
],
13+
"polarity": [
14+
"+","+","-","+","+","+"
15+
],
16+
"sensitivity": {
17+
"sensitivityOn": "yes",
18+
"normalization": "minmax",
19+
"aggregation": "weighted_sum"
20+
},
21+
"robustness": {
22+
"robustness": "weights",
23+
"onWeightsLevel": "all",
24+
"givenWeights": [
25+
0.8, 0.9, 0.5, 0.5, 0.1, 0.7
26+
]
27+
},
28+
"monteCarloSampling": {
29+
"monteCarloRuns": 10000,
30+
"marginalDistributions": [
31+
"normal", "normal", "normal", "normal", "normal", "normal"
32+
]
33+
}
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"inputMatrix": {
3+
"Fuel Efficiency":{"A":30.0,"B":25.0,"C":35.0,"D":40.0,"E":38.0,"F":40.0},
4+
"Safety Rating":{"A":5.0,"B":4.0,"C":5.0,"D":3.0,"E":1.0,"F":4.0},
5+
"Price":{"A":25000.0,"B":30000.0,"C":20000.0,"D":24000.0,"E":15000.0,"F":28000.0},
6+
"Cargo 'Space":{"A":15.0,"B":20.0,"C":10.0,"D":30.0,"E":10.0,"F":15.0},
7+
"Acceleration":{"A":8.0,"B":6.0,"C":10.0,"D":7.0,"E":10.0,"F":9.0},
8+
"Warranty":{"A":3.0,"B":5.0,"C":2.0,"D":1.0,"E":3.0,"F":4.0}
9+
},
10+
"weights": [
11+
0.8, 0.9, 0.5, 0.5, 0.1, 0.7
12+
],
13+
"polarity": [
14+
"+","+","-","+","+","+"
15+
],
16+
"sensitivity": {
17+
"sensitivityOn": "no",
18+
"normalization": "minmax",
19+
"aggregation": "weighted_sum"
20+
},
21+
"robustness": {
22+
"robustness": "none",
23+
"onWeightsLevel": "none",
24+
"givenWeights": [
25+
0.8, 0.9, 0.5, 0.5, 0.1, 0.7
26+
]
27+
},
28+
"monteCarloSampling": {
29+
"monteCarloRuns": 10000,
30+
"marginalDistributions": [
31+
"normal", "normal", "normal", "normal", "normal", "normal"
32+
]
33+
}
34+
}

input_files/toy_example/car_data.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ B,25,4,30000,20,6,5
44
C,35,5,20000,10,10,2
55
D,40,3,24000,30,7,1
66
E,38,1,15000,10,10,3
7-
F,40,4,28000,15,9,4
7+
F,40,4,28000,15,9,4

mcda/configuration/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(self, input_config: dict):
7171
# keys_of_dict_values = self._keys_of_dict_values
7272

7373
self._validate(input_config, valid_keys, str_values,
74-
int_values, list_values, dict_values)
74+
int_values, list_values, dict_values)
7575
self._config = copy.deepcopy(input_config)
7676

7777
def _validate(self, input_config, valid_keys, str_values, int_values, list_values, dict_values):

mcda/mcda_ranking_run.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#! /usr/bin/env python3
2+
3+
"""
4+
This script serves as the main entry point for running all pieces of functionality in a consequential way by
5+
following the settings given in the configuration file 'configuration.json'.
6+
7+
Usage (from root directory):
8+
$ python3 -m mcda.mcda_run -c configuration.json
9+
"""
10+
11+
import time
12+
from ProMCDA.mcda.utils.application_enums import *
13+
from ProMCDA.mcda.utils.utils_for_main import *
14+
from ProMCDA.mcda.utils.utils_for_plotting import *
15+
from ProMCDA.mcda.utils.utils_for_parallelization import *
16+
from ProMCDA.models.configuration import Configuration
17+
18+
log = logging.getLogger(__name__)
19+
20+
FORMATTER: str = '%(levelname)s: %(asctime)s - %(name)s - %(message)s'
21+
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format=FORMATTER)
22+
logger = logging.getLogger("ProMCDA")
23+
24+
# randomly assign seed if not specified as environment variable
25+
RANDOM_SEED = os.environ.get('random_seed') if os.environ.get('random_seed') else 67
26+
NUM_CORES = os.environ.get('num_cores') if os.environ.get('num_cores') else 1
27+
28+
29+
# noinspection PyTypeChecker
30+
def main_using_model(input_config: dict) -> dict:
31+
"""
32+
Execute the ProMCDA (Probabilistic Multi-Criteria Decision Analysis) process.
33+
34+
Parameters:
35+
- input_config : Configuration parameters for the ProMCDA process.
36+
37+
Raises:
38+
- ValueError: If there are issues with the input matrix, weights, or indicators.
39+
40+
This function performs the ProMCDA process based on the provided configuration.
41+
It handles various aspects such as the sensitivity analysis and the robustness analysis.
42+
The results are saved in output files, and plots are generated to visualize the scores and rankings.
43+
44+
Note: Ensure that the input matrix, weights, polarities and indicators (with or without uncertainty)
45+
are correctly specified in the input configuration.
46+
47+
:param input_config: dict
48+
:return: None
49+
"""
50+
is_robustness_indicators = 0
51+
is_robustness_weights = 0
52+
f_norm = None
53+
f_agg = None
54+
marginal_pdf = []
55+
56+
# Extracting relevant configuration values
57+
config = Configuration.from_dict(input_config)
58+
input_matrix = pd.DataFrame(config.input_matrix)
59+
index_column_name = input_matrix.index.name
60+
index_column_values = input_matrix.index.tolist()
61+
polar = config.polarity
62+
robustness = config.robustness.robustness
63+
mc_runs = config.monte_carlo_sampling.monte_carlo_runs
64+
65+
f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf = verify_input(config, f_agg, f_norm,
66+
is_robustness_indicators,
67+
is_robustness_weights,
68+
marginal_pdf, mc_runs,
69+
robustness)
70+
71+
# Check the input matrix for duplicated rows in the alternatives, rescale negative indicator values and
72+
# drop the column containing the alternatives
73+
input_matrix_no_alternatives = check_input_matrix(input_matrix)
74+
75+
if is_robustness_indicators == 0:
76+
num_indicators = input_matrix_no_alternatives.shape[1]
77+
# Process indicators and weights based on input parameters in the configuration
78+
polar, weights = get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators,
79+
is_robustness_weights, mc_runs, num_indicators, polar)
80+
return run_mcda_without_indicator_uncertainty(config, index_column_name, index_column_values,
81+
input_matrix_no_alternatives, weights, f_norm, f_agg,
82+
is_robustness_weights)
83+
else:
84+
num_non_exact_and_non_poisson = len(marginal_pdf) - marginal_pdf.count('exact') - marginal_pdf.count('poisson')
85+
num_indicators = (input_matrix_no_alternatives.shape[1] - num_non_exact_and_non_poisson)
86+
polar, weights = get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators,
87+
is_robustness_weights, mc_runs, num_indicators, polar)
88+
return run_mcda_with_indicator_uncertainty(config, input_matrix_no_alternatives, index_column_name,
89+
index_column_values, mc_runs, RANDOM_SEED,
90+
config.sensitivity.sensitivity_on, f_agg, f_norm,
91+
weights, polar, marginal_pdf)
92+
93+
94+
def get_polar_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators, is_robustness_weights,
95+
mc_runs, num_indicators, polar):
96+
# Process indicators and weights based on input parameters in the configuration
97+
polar, weights = process_indicators_and_weights(config, input_matrix_no_alternatives, is_robustness_indicators,
98+
is_robustness_weights, polar, mc_runs, num_indicators)
99+
try:
100+
check_indicator_weights_polarities(num_indicators, polar, config)
101+
except ValueError as e:
102+
logging.error(str(e), stack_info=True)
103+
raise
104+
return polar, weights
105+
106+
107+
def verify_input(config, f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf, mc_runs,
108+
robustness):
109+
# Check for sensitivity-related configuration errors
110+
if config.sensitivity.sensitivity_on == SensitivityAnalysis.NO.value:
111+
f_norm = config.sensitivity.normalization
112+
f_agg = config.sensitivity.aggregation
113+
check_valid_values(config.sensitivity.normalization, SensitivityNormalization,
114+
'The available normalization functions are: minmax, target, standardized, rank.')
115+
check_valid_values(config.sensitivity.aggregation, SensitivityAggregation,
116+
"""The available aggregation functions are: weighted_sum, geometric, harmonic, minimum.
117+
Watch the correct spelling in the configuration file.""")
118+
logger.info("ProMCDA will only use one pair of norm/agg functions: " + f_norm + '/' + f_agg)
119+
else:
120+
logger.info("ProMCDA will use a set of different pairs of norm/agg functions")
121+
122+
# Check for robustness-related configuration errors
123+
if robustness == RobustnessAnalysis.NONE.value:
124+
logger.info("ProMCDA will without uncertainty on the indicators or weights")
125+
logger.info("Read input matrix without uncertainties!")
126+
else:
127+
check_config_error((config.robustness.robustness == RobustnessAnalysis.NONE.value and
128+
config.robustness.on_weights_level != RobustnessWightLevels.NONE.value),
129+
'Robustness analysis is expected using weights but none is specified! Please clarify.')
130+
131+
check_config_error((config.robustness.robustness == RobustnessAnalysis.WEIGHTS.value and
132+
config.robustness.on_weights_level == RobustnessWightLevels.NONE.value),
133+
'Robustness analysis is requested on the weights: but on all or single? Please clarify.')
134+
135+
check_config_error((config.robustness.robustness == RobustnessAnalysis.INDICATORS.value and
136+
config.robustness.on_weights_level != RobustnessWightLevels.NONE.value),
137+
'Robustness analysis is requested: but on weights or indicators? Please clarify.')
138+
139+
# Check settings for robustness analysis on weights or indicators
140+
if config.robustness.robustness == RobustnessAnalysis.WEIGHTS.value and config.robustness.on_weights_level != RobustnessWightLevels.NONE.value:
141+
logger.info(f"""ProMCDA will consider uncertainty on the weights.
142+
Number of Monte Carlo runs: {mc_runs}
143+
logger.info("The random seed used is: {RANDOM_SEED}""")
144+
is_robustness_weights = 1
145+
146+
if config.robustness.robustness == RobustnessAnalysis.INDICATORS.value and config.robustness.on_weights_level == RobustnessWightLevels.NONE.value:
147+
logger.info(f"""ProMCDA will consider uncertainty on the indicators.
148+
Number of Monte Carlo runs: {mc_runs}
149+
logger.info("The random seed used is: {RANDOM_SEED}""")
150+
is_robustness_indicators = 1
151+
152+
marginal_pdf = config.monte_carlo_sampling.marginal_distributions
153+
logger.info("Read input matrix with uncertainty of the indicators!")
154+
return f_agg, f_norm, is_robustness_indicators, is_robustness_weights, marginal_pdf
155+
156+
157+
def read_matrix_from_file(column_names_list: list[str], file_from_stream) -> {}:
158+
result_dict = utils_for_main.read_matrix_from_file(file_from_stream).to_dict()
159+
if len(column_names_list) != len(result_dict.keys()):
160+
return {"error": "Number of provided column names does not match the CSV columns"}, 400
161+
return {col: result_dict[col] for col in column_names_list if col in result_dict}
162+
163+
164+
if __name__ == '__main__':
165+
t = time.time()
166+
config_path = parse_args()
167+
input_config = get_config(config_path)
168+
main_using_model(input_config)
169+
elapsed = time.time() - t
170+
logger.info("All calculations finished in seconds {}".format(elapsed))

0 commit comments

Comments
 (0)