Skip to content

Conversation

@Flaminietta
Copy link
Collaborator

@Flaminietta Flaminietta commented Jan 28, 2025

I finally came to have a good working basis of ProMCDA refactored into a library. In this first part, you can find the following major improvements:

  • ProMCDA library can be instantiated, and its two main methods can be used independently: normalize and aggregate.
  • ProMCDA library has only two required input parameters; all others are optional and have default values.
  • There are no more configuration files or dictionaries defining the input parameters.
  • Class Enums have been introduced to reduce hard-coded values.
  • normalize and aggregate implement the sensitivity analysis (full or partial); the robustness analysis (on the indicators or weights); and simple usage with set parameters.
  • New functions and classes are unit-tested.
  • A notebook helps to test all functionalities physically.

It's a good moment for you to start reviewing the changes. In particular, I wish that:

  • @kapil-agnihotri can review the code quality, design, and efficiency.
  • @mspada can review the overall logic, and the notebook for physical testing and help with the design of end-to-end tests to prove that the new ProMCDA works as expected. Please check a couple of TODOS where I had doubts.

In the meantime, I will continue the refactoring; in particular, we still need to:

  • implement get_ranks
  • implement run
  • clean the code
  • remove unused functions/classes
  • fix unit tests
  • end-to-end tests (in the notebook)
  • update the README
  • add changelog
  • release in GitHub and notes
  • release also in PyPi

Future, optional improvements:

  • implement a few other methods (e.g., get_plots, save_output)
  • dynamic method combinations: if the number of methods/combinations (like 13) grows, consider making that dynamic
    or stored as an internal attribute so tests remain flexible
  • improve plot quality - explore plugin option
  • performance tests
  • adjust logging as desired

…nd scores from dict to dataframe, broken tests
Copy link
Collaborator

@kapil-agnihotri kapil-agnihotri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still reviewing the PR but here are some more comments.

validate_configuration(
input_matrix=self.input_matrix,
polarity=self.polarity,
weights=self.weights,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think same applies to all the parameters. You have already used them in the code but validating them now.

Copy link
Collaborator Author

@Flaminietta Flaminietta Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand, this is one of the first blocks of ProMCDA.py, this validation is happening just before any other processes.

If you refer to the block just before this one, the logic and order of these two blocks are consistent with the needed checks and processing of data.

I'd leave it as it is.

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two previous steps are necessary to validate_configuration.

Example: if the input matrix has a column with no information, this column is dropped in check_input_matrix. The weights and polarities are consequently shortened in process_indicators_and_weights. Only then can we validate the entire configuration settings, for example, estimate the number of indicators in the case of robustness analysis in validate_configuration.

This logic and all the controls could be rewritten more organically and clearly. I leave this improvement to a later version of the library.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think then this is a potential comment for a follow up ticket?

Copy link
Collaborator

@kapil-agnihotri kapil-agnihotri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still in review but here are some more comments.

Copy link
Collaborator

@kapil-agnihotri kapil-agnihotri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am finished with the review.

all_weights_score_means_normalized, all_weights_score_stds_normalized = \
compute_scores_for_all_random_weights(self.normalized_values_without_robustness, norm_weights,
aggregation_method)
self.all_weights_score_means = all_weights_score_means
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to create local variable just to reassign them to member variable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning these variables to self. is intentional because it makes them accessible throughout the class instance, beyond the scope of the current method. Without this, the variables would be local and not available to other methods or after initialization. So this assignment is necessary for maintaining the state within the class.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned about the local variables and not self variables. What I meant you can do is:

self.all_weights_score_means, self.all_weights_score_stds, \
self.all_weights_score_means_normalized, self.all_weights_score_stds_normalized = \
                compute_scores_for_all_random_weights(self.normalized_values_without_robustness, norm_weights,
                                                      aggregation_method)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is still pending :(. Please assign self.___ variables directly instead of using intermediate variable.

Copy link
Collaborator

@kapil-agnihotri kapil-agnihotri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that a lot of comments were unaddressed (most of them were from collapsable messages). Can you please address or reject them however you feel them fit? Few of them also corresponds to some logic.
Some comments were resolved without any changes so I have unresolved them.
I have also added some comments on your new logic.

if not isinstance(robustness, RobustnessAnalysisType):
raise TypeError(f"'robustness' must be of type RobustnessAnalysisType, got {type(robustness).__name__}")

if self.weights is None and RobustnessAnalysisType.INDICATORS.value is not "indicators":
Copy link
Collaborator

@kapil-agnihotri kapil-agnihotri Jun 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I missed this before but I doubt that condition RobustnessAnalysisType.INDICATORS.value is not "indicators" would ever fail.
You are comparing enum value to a string, this would always validate to false, i.e. indicators != indicators.

validate_configuration(
input_matrix=self.input_matrix,
polarity=self.polarity,
weights=self.weights,

This comment was marked as resolved.

# Apply aggregation in the different configuration settings
# NO UNCERTAINTY ON INDICATORS AND WEIGHTS
if not self.robustness_indicators and not self.robustness_weights and not self.robustness_single_weights:
if (not self.robustness == RobustnessAnalysisType.INDICATORS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not simply check for None instead of checking for != for all the other values of enum? If I understand correctly then here you are not checking for individual values but just for a single value?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any update here? Looks like this was missed.


# NO UNCERTAINTY ON INDICATORS, ALL RANDOMLY SAMPLED WEIGHTS (MCDA runs num_samples times)
elif self.robustness_weights and not self.robustness_single_weights and not self.robustness_indicators:
elif (self.robustness == RobustnessAnalysisType.ALL_WEIGHTS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can self.robustness have 3 different values at a same time? If not then why do you check for all the values? i would just check for self.robustness == RobustnessAnalysisType.ALL_WEIGHTS.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any update here?


# NO UNCERTAINTY ON INDICATORS, ONE SINGLE RANDOM WEIGHT AT TIME
elif self.robustness_single_weights and not self.robustness_weights and not self.robustness_indicators:
elif (self.robustness == RobustnessAnalysisType.SINGLE_WEIGHTS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before Can self.robustness have 3 different values at a same time?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?


# UNCERTAINTY ON INDICATORS, NO UNCERTAINTY ON WEIGHTS
elif self.robustness_indicators and not self.robustness_weights and not self.robustness_single_weights:
elif (self.robustness == RobustnessAnalysisType.INDICATORS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before Can self.robustness have 3 different values at a same time?

print(f"MCDA ranks are retrieved")

if not any([self.robustness_weights, self.robustness_indicators, self.robustness_single_weights]):
if not any([self.robustness == RobustnessAnalysisType.ALL_WEIGHTS,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just check for None instead of all 3?

README.md Outdated
Together, these examples show both the mechanics of using the library and the applicability of its concepts.

In particular, `demo_in_notebook` contains:
- Two examples of setups for instatiating the ProMCDA object: one with a dataset without uncertainties and one with a dataset with uncertainties.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see only python notebook in demo_in_notebook folder. But you mentioned that there are two examples setups contained in demo_in_notebook.
If they are created within notebook then it would be great mention it here because you are referring to the folder instead of notebook. Otherwise someone might search for input files in the folder.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are created in the notebook, at the beginning. I edit the README.

weights: Optional[list] = None,
robustness_weights: Optional[bool] = False,
robustness_single_weights: Optional[bool] = False, robustness_indicators: Optional[bool] = False,
robustness: RobustnessAnalysisType = RobustnessAnalysisType.NONE,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the change to enum. It would also be great to update docs for this method as they still refer to old implementation with combination of robustness and weights.

- The input_matrix should contain the alternatives as rows and the criteria as columns.
- If weights are not provided, they are set to 0.5 for each criterion.
- If robustness_weights is enabled, the robustness_single_weights should be disabled, and viceversa.
- If robustness_indicators is enabled, the robustness on weights should be disabled.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the change. Can you please also update docs for this method.

if not isinstance(robustness, RobustnessAnalysisType):
raise TypeError(f"'robustness' must be of type RobustnessAnalysisType, got {type(robustness).__name__}")

if self.weights is None and RobustnessAnalysisType.INDICATORS.value != "indicators":
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this logic still needs to be corrected. Let me explain what you are doing here. You are trying to compare enum with a string.
Your enum RobustnessAnalysisType.INDICATORS.value will give you value indicators and this is compared with negation to indicators. i.e., indicators != indicators.
What you might want to do here could be robustness.value != indicators.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were probably referring to a previous commit here. The check here is already of the kind: self.robustness != RobustnessAnalysisType.INDICATORS, that is correct and tested.

validate_configuration(
input_matrix=self.input_matrix,
polarity=self.polarity,
weights=self.weights,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think then this is a potential comment for a follow up ticket?

# Apply aggregation in the different configuration settings
# NO UNCERTAINTY ON INDICATORS AND WEIGHTS
if not self.robustness_indicators and not self.robustness_weights and not self.robustness_single_weights:
if (not self.robustness == RobustnessAnalysisType.INDICATORS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any update here? Looks like this was missed.


# NO UNCERTAINTY ON INDICATORS, ALL RANDOMLY SAMPLED WEIGHTS (MCDA runs num_samples times)
elif self.robustness_weights and not self.robustness_single_weights and not self.robustness_indicators:
elif (self.robustness == RobustnessAnalysisType.ALL_WEIGHTS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any update here?

all_weights_score_means_normalized, all_weights_score_stds_normalized = \
compute_scores_for_all_random_weights(self.normalized_values_without_robustness, norm_weights,
aggregation_method)
self.all_weights_score_means = all_weights_score_means
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is still pending :(. Please assign self.___ variables directly instead of using intermediate variable.


# NO UNCERTAINTY ON INDICATORS, ONE SINGLE RANDOM WEIGHT AT TIME
elif self.robustness_single_weights and not self.robustness_weights and not self.robustness_indicators:
elif (self.robustness == RobustnessAnalysisType.SINGLE_WEIGHTS
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

return polarity, norm_fixed_weights, None, None
# Return None for norm_random_weights and rand_weight_per_indicator
else:
output_weights = handle_robustness_weights(mc_runs, num_indicators, robustness_weights,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if you want then you can further improve it as (as per my initial suggestion)

 norm_random_weights, rand_weight_per_indicator  = handle_robustness_weights(mc_runs, num_indicators, robustness)

@Flaminietta
Copy link
Collaborator Author

@kapil-agnihotri Please approve.

@stempler stempler merged commit 056fa10 into main Jul 2, 2025
2 checks passed
@stempler stempler deleted the refactor_in_to_library branch July 2, 2025 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactoring Refactoring from package to library

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants